Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions lib/mpp/methods/tempo/client_method.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,7 @@ def create_credential(challenge, mode: nil)
nonce_key = nonce_key.start_with?("0x") ? nonce_key.to_i(16) : nonce_key.to_i
end

memo = method_details["memo"]
memo ||= Attribution.encode(server_id: challenge.realm, client_id: @client_id, challenge_id: challenge.id)
memo = Attribution.encode(server_id: challenge.realm, client_id: @client_id, challenge_id: challenge.id)

# Resolve RPC URL from challenge's chainId. Normalize the configured pin
# once (it may be a String from ENV/config) so it compares equal to
Expand Down
63 changes: 17 additions & 46 deletions lib/mpp/methods/tempo/intents.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ def validate_hash(payload, request, credential:)
raise Mpp::VerificationError,
"Transaction must contain a Transfer log matching request parameters"
end
assert_challenge_bound_memo(matched_logs, credential.challenge) unless request.method_details.memo
assert_challenge_bound_memo(matched_logs, credential.challenge)

Mpp::Receipt.success(payload.hash)
end
Expand Down Expand Up @@ -302,15 +302,14 @@ def verify_transaction_receipt!(receipt_data, request, credential:)
raise Mpp::VerificationError,
"Transaction must contain a Transfer log matching request parameters"
end
assert_challenge_bound_memo(matched_logs, credential.challenge) unless request.method_details.memo
assert_challenge_bound_memo(matched_logs, credential.challenge)
end

def verify_transfer_logs(receipt, request, expected_sender: nil)
match_transfer_logs(receipt, request, expected_sender: expected_sender).any?
end

def match_transfer_logs(receipt, request, expected_sender: nil, source: nil, validate_sender: nil)
expected_memo = request.method_details.memo
matched_logs = []

(receipt["logs"] || []).each do |log|
Expand All @@ -325,41 +324,25 @@ def match_transfer_logs(receipt, request, expected_sender: nil, source: nil, val
next unless to_address.downcase == request.recipient.downcase

matched =
if expected_memo
next unless topics[0] == TRANSFER_WITH_MEMO_TOPIC
case topics[0]
when TRANSFER_WITH_MEMO_TOPIC
next if topics.length < 4

data = log.fetch("data", "0x")
next if data.length < 66

amount = data[2, 64].to_i(16)
memo = topics[3]
memo_clean = expected_memo.downcase
memo_clean = "0x#{memo_clean}" unless memo_clean.start_with?("0x")
next unless amount == Integer(request.amount) && memo.downcase == memo_clean
next unless amount == Integer(request.amount)

{kind: :memo, memo: memo}
else
case topics[0]
when TRANSFER_WITH_MEMO_TOPIC
next if topics.length < 4

data = log.fetch("data", "0x")
next if data.length < 66

amount = data[2, 64].to_i(16)
next unless amount == Integer(request.amount)

{kind: :memo, memo: topics[3]}
when TRANSFER_TOPIC
data = log.fetch("data", "0x")
next if data.length < 66
{kind: :memo, memo: topics[3]}
when TRANSFER_TOPIC
data = log.fetch("data", "0x")
next if data.length < 66

amount = data.delete_prefix("0x").to_i(16)
next unless amount == Integer(request.amount)
amount = data.delete_prefix("0x").to_i(16)
next unless amount == Integer(request.amount)

{kind: :transfer}
end
{kind: :transfer}
end

next unless matched
Expand Down Expand Up @@ -489,13 +472,8 @@ def match_transfer_calldata(call_data_hex, request, challenge: nil)
return false if call_data_hex.length < 136

selector = call_data_hex[0, 8].downcase
expected_memo = request.method_details.memo

if expected_memo
return false unless selector == TRANSFER_WITH_MEMO_SELECTOR
elsif selector != TRANSFER_WITH_MEMO_SELECTOR
return false
end
return false unless selector == TRANSFER_WITH_MEMO_SELECTOR

decoded_to = "0x#{call_data_hex[32, 40]}"
decoded_amount = call_data_hex[72, 64].to_i(16)
Expand All @@ -504,18 +482,11 @@ def match_transfer_calldata(call_data_hex, request, challenge: nil)
return false unless decoded_amount == Integer(request.amount)
return false if call_data_hex.length < 200

if expected_memo
decoded_memo = "0x#{call_data_hex[136, 64]}"
memo_clean = expected_memo.downcase
memo_clean = "0x#{memo_clean}" unless memo_clean.start_with?("0x")
return false unless decoded_memo.downcase == memo_clean
else
return false unless challenge
return false unless challenge

decoded_memo = "0x#{call_data_hex[136, 64]}"
return false unless Attribution.verify_server(decoded_memo, challenge.realm)
return false unless Attribution.verify_challenge_binding(decoded_memo, challenge.id)
end
decoded_memo = "0x#{call_data_hex[136, 64]}"
return false unless Attribution.verify_server(decoded_memo, challenge.realm)
return false unless Attribution.verify_challenge_binding(decoded_memo, challenge.id)

true
end
Expand Down
7 changes: 3 additions & 4 deletions lib/mpp/methods/tempo/schemas.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ module Tempo
module Schemas
HEX_PATTERN = /\A0x[a-fA-F0-9]+\z/

MethodDetails = Data.define(:chain_id, :fee_payer, :fee_payer_url, :memo) do
def initialize(chain_id: 4217, fee_payer: false, fee_payer_url: nil, memo: nil)
MethodDetails = Data.define(:chain_id, :fee_payer, :fee_payer_url) do
def initialize(chain_id: 4217, fee_payer: false, fee_payer_url: nil)
super
end

Expand All @@ -18,8 +18,7 @@ def self.from_hash(hash)
new(
chain_id: hash["chainId"] || 4217,
fee_payer: hash["feePayer"] || false,
fee_payer_url: hash["feePayerUrl"],
memo: hash["memo"]
fee_payer_url: hash["feePayerUrl"]
)
end
end
Expand Down
4 changes: 1 addition & 3 deletions lib/mpp/server/mpp_handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,6 @@ def build_charge_request(method, amount, **kwargs)
currency = kwargs[:currency]
recipient = kwargs[:recipient]
external_id = kwargs[:external_id]
memo = kwargs[:memo]
fee_payer = if kwargs.key?(:fee_payer)
kwargs[:fee_payer]
else
Expand Down Expand Up @@ -257,10 +256,9 @@ def build_charge_request(method, amount, **kwargs)
resolved_chain_id = chain_id
resolved_chain_id ||= method.chain_id if method.respond_to?(:chain_id)

if memo || fee_payer || !resolved_chain_id.nil?
if fee_payer || !resolved_chain_id.nil?
method_details = {}
method_details["chainId"] = resolved_chain_id unless resolved_chain_id.nil?
method_details["memo"] = memo if memo
method_details["feePayer"] = true if fee_payer
request["methodDetails"] = method_details
end
Expand Down
18 changes: 6 additions & 12 deletions test/integration/tempo_live_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ def setup
def test_transaction_credential_verifies_against_live_node
payer = funded_account
recipient = funded_account
memo = Mpp::Methods::Tempo::Attribution.encode(server_id: REALM, challenge_id: "tx-#{SecureRandom.hex(6)}")
challenge = challenge_for(recipient: recipient.address, memo: memo)
challenge = challenge_for(recipient: recipient.address)

credential = client_method(account: payer).create_credential(challenge)
intent = charge_intent
Expand Down Expand Up @@ -72,13 +71,12 @@ def test_client_transport_completes_fee_payer_402_roundtrip
def test_hash_credential_verifies_live_transfer
payer = funded_account
recipient = funded_account
memo = Mpp::Methods::Tempo::Attribution.encode(server_id: REALM, challenge_id: "hash-#{SecureRandom.hex(6)}")
challenge = challenge_for(recipient: recipient.address, memo: memo)
challenge = challenge_for(recipient: recipient.address)
tx_hash = send_transfer(
account: payer,
recipient: recipient.address,
amount: 1_000_000,
memo: memo
memo: Mpp::Methods::Tempo::Attribution.encode(server_id: REALM, challenge_id: challenge.id)
)
credential = Mpp::Credential.new(
challenge: challenge.to_echo,
Expand All @@ -95,13 +93,12 @@ def test_hash_credential_verifies_live_transfer
def test_hash_credential_replay_rejected_with_store
payer = funded_account
recipient = funded_account
memo = Mpp::Methods::Tempo::Attribution.encode(server_id: REALM, challenge_id: "replay-#{SecureRandom.hex(6)}")
challenge = challenge_for(recipient: recipient.address, memo: memo)
challenge = challenge_for(recipient: recipient.address)
tx_hash = send_transfer(
account: payer,
recipient: recipient.address,
amount: 1_000_000,
memo: memo
memo: Mpp::Methods::Tempo::Attribution.encode(server_id: REALM, challenge_id: challenge.id)
)
credential = Mpp::Credential.new(
challenge: challenge.to_echo,
Expand Down Expand Up @@ -231,9 +228,8 @@ def charge_intent(**opts)
Mpp::Methods::Tempo::ChargeIntent.new(rpc_url: RPC_URL, **opts)
end

def challenge_for(recipient:, memo: nil)
def challenge_for(recipient:)
method_details = {"chainId" => chain_id}
method_details["memo"] = memo if memo

Mpp::Challenge.create(
secret_key: SECRET_KEY,
Expand Down Expand Up @@ -264,7 +260,6 @@ def initialize(recipient:, chain_id:, fee_payer: nil)
)
@sponsored = !fee_payer.nil?
@handler = Mpp.create(method: method, realm: REALM, secret_key: SECRET_KEY)
@memo = Mpp::Methods::Tempo::Attribution.encode(server_id: REALM, challenge_id: SecureRandom.hex(6))
@server = TCPServer.new("127.0.0.1", 0)
@url = "http://127.0.0.1:#{@server.addr[1]}/paid"
@thread = Thread.new { serve }
Expand Down Expand Up @@ -303,7 +298,6 @@ def handle(socket)
headers["authorization"],
"1.00",
chain_id: chain_id,
memo: @memo,
fee_payer: @sponsored
)
if result.is_a?(Mpp::Challenge)
Expand Down
31 changes: 8 additions & 23 deletions test/mpp/methods/tempo/test_charge_intent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,6 @@ def test_hash_without_explicit_memo_rejects_wrong_realm
assert_match(/memo is not bound to this challenge/, error.message)
end

def test_hash_with_explicit_memo_accepts_exact_memo
explicit_memo = "0x#{"ab" * 32}"
result = verify_hash(
receipt([transfer_log(memo: explicit_memo)]),
challenge_id: "challenge-123",
memo: explicit_memo
)

assert_equal HASH, result.reference
end

def test_initialize_rejects_nil_store
error = assert_raises(ArgumentError) do
Mpp::Methods::Tempo::ChargeIntent.new(store: nil)
Expand Down Expand Up @@ -731,17 +720,15 @@ def test_hash_validate_sender_not_called_when_sender_matches
end

def test_hash_validate_sender_not_called_for_non_candidate_logs
explicit_memo = "0x#{"ab" * 32}"
other_memo = "0x#{"cd" * 32}"
intent = Mpp::Methods::Tempo::ChargeIntent.new(
rpc_url: "https://rpc.example.test",
validate_sender: ->(expected_sender:, sender:, source:) { raise "must not be called" }
)
# First log has a wrong sender and a non-matching memo (non-candidate);
# First log has a wrong sender and a non-matching amount (non-candidate);
# second log matches fully, so the callback is never reached.
receipt = receipt([
transfer_log(memo: other_memo, from: RELAYER),
transfer_log(memo: explicit_memo, from: SOURCE_ADDR)
transfer_log(memo: bound_memo, from: RELAYER).merge("data" => "0x#{1.to_s(16).rjust(64, "0")}"),
transfer_log(memo: bound_memo, from: SOURCE_ADDR)
])
credential = Mpp::Credential.new(
challenge: Mpp::ChallengeEcho.new(id: "challenge-123", realm: REALM, method: "tempo",
Expand All @@ -750,7 +737,7 @@ def test_hash_validate_sender_not_called_for_non_candidate_logs
source: did_pkh(CHAIN_ID, SOURCE_ADDR)
)
result = Mpp::Methods::Tempo::Rpc.stub(:call, ->(_rpc_url, _method, _params) { receipt }) do
intent.verify(credential, request_hash(memo: explicit_memo))
intent.verify(credential, request_hash)
end
assert_equal HASH, result.reference
end
Expand Down Expand Up @@ -958,7 +945,7 @@ def transaction_credential(raw_tx, challenge_id:, source: nil)
)
end

def verify_hash(receipt, challenge_id:, memo: nil)
def verify_hash(receipt, challenge_id:)
credential = Mpp::Credential.new(
challenge: Mpp::ChallengeEcho.new(
id: challenge_id,
Expand All @@ -975,18 +962,16 @@ def verify_hash(receipt, challenge_id:, memo: nil)
assert_equal [HASH], params
receipt
}) do
@intent.verify(credential, request_hash(memo: memo))
@intent.verify(credential, request_hash)
end
end

def request_hash(memo: nil)
request = {
def request_hash
{
"amount" => "1000",
"currency" => CURRENCY,
"recipient" => RECIPIENT
}
request["methodDetails"] = {"memo" => memo} if memo
request
end

def receipt(logs)
Expand Down
1 change: 0 additions & 1 deletion test/mpp/methods/tempo/test_schemas.rb
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,5 @@ def test_method_details_defaults
assert_equal 4217, md.chain_id
refute md.fee_payer
assert_nil md.fee_payer_url
assert_nil md.memo
end
end
Loading