remitmd 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +7 -17
- data/lib/remitmd/cli_signer.rb +2 -2
- data/lib/remitmd/mock.rb +13 -0
- data/lib/remitmd/signer.rb +6 -0
- data/lib/remitmd/wallet.rb +48 -122
- data/lib/remitmd/x402_client.rb +33 -82
- data/lib/remitmd.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1d8076d587f455a8a8f5bd4e80dc542a14cd11226bda0784c9816b45710f526c
|
|
4
|
+
data.tar.gz: '005762609902918f053fa2ce4c6b387f6e9af441770c212c9eb913d083113bf8'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 939eea68d169df0628da6639dccb8f3e99961c7c12790f28c26c79e95933f1a93e8543034402eef3360a0f0697fa8346ef5409ac51b50d6801a6943faed94db4
|
|
7
|
+
data.tar.gz: bb7e3e396f9db6a9d8b03ea1cd8d01af21bf21d4682072212fbd0de7ef23250beb0659b1a5d20c97416a7fe7703a03f4ac0567db4bbbbbecf2f760de64541d58
|
data/README.md
CHANGED
|
@@ -43,7 +43,7 @@ wallet = Remitmd::RemitWallet.from_env
|
|
|
43
43
|
# Optional: REMITMD_CHAIN (default: "base"), REMITMD_API_URL
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
Permits are auto-signed. Every payment method
|
|
46
|
+
Permits are auto-signed. Every payment method calls the server's `/permits/prepare` endpoint, signs the hash, and includes the permit automatically.
|
|
47
47
|
|
|
48
48
|
## CLI Signer (Recommended)
|
|
49
49
|
|
|
@@ -55,7 +55,7 @@ The CLI signer delegates key management to the `remit` CLI binary, which holds y
|
|
|
55
55
|
# Windows: winget install remit-md.remit
|
|
56
56
|
# Linux: curl -fsSL https://remit.md/install.sh | sh
|
|
57
57
|
|
|
58
|
-
export
|
|
58
|
+
export REMIT_SIGNER_KEY=your-keystore-password
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
```ruby
|
|
@@ -200,8 +200,7 @@ wallet.close_tab(tab_id, final_amount: nil, provider_sig: nil)
|
|
|
200
200
|
wallet.sign_tab_charge(tab_contract, tab_id, total_charged, call_count) # String
|
|
201
201
|
|
|
202
202
|
# EIP-2612 Permit (auto-signed when omitted from payment methods)
|
|
203
|
-
wallet.sign_permit(
|
|
204
|
-
wallet.sign_usdc_permit(spender, value, deadline, nonce, usdc_address: nil) # PermitSignature
|
|
203
|
+
wallet.sign_permit(flow, amount) # PermitSignature
|
|
205
204
|
|
|
206
205
|
# Streams
|
|
207
206
|
wallet.create_stream(payee, rate_per_second, max_total, permit: nil) # Stream
|
|
@@ -275,24 +274,15 @@ Remitmd::RemitWallet.new(private_key: key, chain: "base_sepolia") # Base Sepoli
|
|
|
275
274
|
|
|
276
275
|
### Manual Permit Signing
|
|
277
276
|
|
|
278
|
-
Permits are auto-signed by default. If you need manual control (
|
|
277
|
+
Permits are auto-signed by default via the server's `/permits/prepare` endpoint. If you need manual control (pre-signed permits, multi-step workflows), pass a `PermitSignature` explicitly:
|
|
279
278
|
|
|
280
279
|
```ruby
|
|
281
|
-
|
|
282
|
-
permit = wallet.sign_permit("0xRouterAddress...", 5.00, deadline: Time.now.to_i + 7200)
|
|
283
|
-
tx = wallet.pay("0xRecipient...", 5.00, permit: permit)
|
|
284
|
-
|
|
285
|
-
# sign_usdc_permit: full control - raw base units, explicit nonce
|
|
286
|
-
permit = wallet.sign_usdc_permit(
|
|
287
|
-
"0xRouterAddress...", # spender
|
|
288
|
-
5_000_000, # value in base units (6 decimals)
|
|
289
|
-
Time.now.to_i + 3600, # deadline
|
|
290
|
-
0, # nonce
|
|
291
|
-
usdc_address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
292
|
-
)
|
|
280
|
+
permit = wallet.sign_permit("direct", 5.00)
|
|
293
281
|
tx = wallet.pay("0xRecipient...", 5.00, permit: permit)
|
|
294
282
|
```
|
|
295
283
|
|
|
284
|
+
Supported flows: `"direct"`, `"escrow"`, `"tab"`, `"stream"`, `"bounty"`, `"deposit"`.
|
|
285
|
+
|
|
296
286
|
## License
|
|
297
287
|
|
|
298
288
|
MIT - see [LICENSE](LICENSE)
|
data/lib/remitmd/cli_signer.rb
CHANGED
|
@@ -71,7 +71,7 @@ module Remitmd
|
|
|
71
71
|
#
|
|
72
72
|
# 1. CLI binary found on PATH (via `which` / `where`)
|
|
73
73
|
# 2. Meta file at ~/.remit/keys/default.meta (keychain — no password needed), OR
|
|
74
|
-
# 3. Keystore file at ~/.remit/keys/default.enc AND
|
|
74
|
+
# 3. Keystore file at ~/.remit/keys/default.enc AND REMIT_SIGNER_KEY env var set
|
|
75
75
|
#
|
|
76
76
|
# @param cli_path [String] path or name of the remit CLI binary
|
|
77
77
|
# @return [Boolean]
|
|
@@ -90,7 +90,7 @@ module Remitmd
|
|
|
90
90
|
keystore = File.join(keys_dir, "default.enc")
|
|
91
91
|
return false unless File.exist?(keystore)
|
|
92
92
|
|
|
93
|
-
password = ENV["REMIT_KEY_PASSWORD"]
|
|
93
|
+
password = ENV["REMIT_SIGNER_KEY"] || ENV["REMIT_KEY_PASSWORD"]
|
|
94
94
|
return false if password.nil? || password.empty?
|
|
95
95
|
|
|
96
96
|
true
|
data/lib/remitmd/mock.rb
CHANGED
|
@@ -108,6 +108,10 @@ module Remitmd
|
|
|
108
108
|
def sign(_message)
|
|
109
109
|
"0x" + SecureRandom.hex(32) + SecureRandom.hex(32) + "1b"
|
|
110
110
|
end
|
|
111
|
+
|
|
112
|
+
def sign_hash(_hash_bytes)
|
|
113
|
+
"0x" + SecureRandom.hex(32) + SecureRandom.hex(32) + "1b"
|
|
114
|
+
end
|
|
111
115
|
end
|
|
112
116
|
|
|
113
117
|
# ─── Internal: MockTransport ───────────────────────────────────────────────
|
|
@@ -135,6 +139,15 @@ module Remitmd
|
|
|
135
139
|
def handle(method, path, b) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
136
140
|
case [method, path]
|
|
137
141
|
|
|
142
|
+
# Permits prepare (server-side permit signing)
|
|
143
|
+
in ["POST", "/permits/prepare"]
|
|
144
|
+
{
|
|
145
|
+
"hash" => "0x#{SecureRandom.hex(32)}",
|
|
146
|
+
"value" => ((b[:amount] || b["amount"]).to_f * 1_000_000).round.to_i,
|
|
147
|
+
"deadline" => Time.now.to_i + 3600,
|
|
148
|
+
"nonce" => 0,
|
|
149
|
+
}
|
|
150
|
+
|
|
138
151
|
# Contracts (for auto_permit)
|
|
139
152
|
in ["GET", "/contracts"]
|
|
140
153
|
{
|
data/lib/remitmd/signer.rb
CHANGED
|
@@ -23,6 +23,12 @@ module Remitmd
|
|
|
23
23
|
raise NotImplementedError, "#{self.class}#sign is not implemented"
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# Sign a 32-byte hash (raw binary bytes). Returns 0x-prefixed 65-byte hex signature.
|
|
27
|
+
# Used by /permits/prepare and /x402/prepare flows.
|
|
28
|
+
def sign_hash(hash_bytes)
|
|
29
|
+
sign(hash_bytes)
|
|
30
|
+
end
|
|
31
|
+
|
|
26
32
|
# The Ethereum address corresponding to the signing key (0x-prefixed).
|
|
27
33
|
def address
|
|
28
34
|
raise NotImplementedError, "#{self.class}#address is not implemented"
|
data/lib/remitmd/wallet.rb
CHANGED
|
@@ -5,11 +5,15 @@ require "bigdecimal/util"
|
|
|
5
5
|
require "json"
|
|
6
6
|
|
|
7
7
|
module Remitmd
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
8
|
+
# Contract name -> flow name for /permits/prepare.
|
|
9
|
+
CONTRACT_TO_FLOW = {
|
|
10
|
+
"router" => "direct",
|
|
11
|
+
"escrow" => "escrow",
|
|
12
|
+
"tab" => "tab",
|
|
13
|
+
"stream" => "stream",
|
|
14
|
+
"bounty" => "bounty",
|
|
15
|
+
"deposit" => "deposit",
|
|
16
|
+
"relayer" => "direct",
|
|
13
17
|
}.freeze
|
|
14
18
|
|
|
15
19
|
# Primary remit.md client. All payment operations are methods on RemitWallet.
|
|
@@ -89,7 +93,7 @@ module Remitmd
|
|
|
89
93
|
unless key
|
|
90
94
|
raise ArgumentError,
|
|
91
95
|
"No signing method available. Either:\n" \
|
|
92
|
-
" 1. Install the remit CLI and set
|
|
96
|
+
" 1. Install the remit CLI and set REMIT_SIGNER_KEY:\n" \
|
|
93
97
|
" macOS: brew install remit-md/tap/remit\n" \
|
|
94
98
|
" Windows: winget install remit-md.remit\n" \
|
|
95
99
|
" Linux: curl -fsSL https://remit.md/install.sh | sh\n" \
|
|
@@ -346,86 +350,37 @@ module Remitmd
|
|
|
346
350
|
@signer.sign(digest)
|
|
347
351
|
end
|
|
348
352
|
|
|
349
|
-
# ─── EIP-2612 Permit
|
|
353
|
+
# ─── EIP-2612 Permit (via /permits/prepare) ────────────────────────────
|
|
350
354
|
|
|
351
|
-
# Sign
|
|
352
|
-
#
|
|
353
|
-
#
|
|
354
|
-
#
|
|
355
|
-
#
|
|
356
|
-
# @param
|
|
357
|
-
# @param nonce [Integer] current permit nonce for this wallet
|
|
358
|
-
# @param usdc_address [String, nil] override the USDC contract address
|
|
359
|
-
# @return [PermitSignature]
|
|
360
|
-
def sign_usdc_permit(spender, value, deadline, nonce = 0, usdc_address: nil)
|
|
361
|
-
usdc_addr = usdc_address || USDC_ADDRESSES[@chain_key]
|
|
362
|
-
if usdc_addr.nil? || usdc_addr.empty?
|
|
363
|
-
raise RemitError.new(
|
|
364
|
-
RemitError::INVALID_ADDRESS,
|
|
365
|
-
"No USDC address configured for chain #{@chain_key.inspect}. " \
|
|
366
|
-
"Valid chains: #{USDC_ADDRESSES.keys.join(", ")}",
|
|
367
|
-
context: { chain: @chain_key }
|
|
368
|
-
)
|
|
369
|
-
end
|
|
370
|
-
chain_id = @chain_id || ChainId::BASE_SEPOLIA
|
|
371
|
-
|
|
372
|
-
# Domain separator for USDC (EIP-2612)
|
|
373
|
-
domain_type_hash = keccak256_raw(
|
|
374
|
-
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
|
|
375
|
-
)
|
|
376
|
-
name_hash = keccak256_raw("USD Coin")
|
|
377
|
-
version_hash = keccak256_raw("2")
|
|
378
|
-
chain_id_enc = abi_uint256(chain_id)
|
|
379
|
-
contract_enc = abi_address(usdc_addr)
|
|
380
|
-
|
|
381
|
-
domain_data = domain_type_hash + name_hash + version_hash + chain_id_enc + contract_enc
|
|
382
|
-
domain_sep = keccak256_raw(domain_data)
|
|
383
|
-
|
|
384
|
-
# Permit struct hash
|
|
385
|
-
type_hash = keccak256_raw(
|
|
386
|
-
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
|
|
387
|
-
)
|
|
388
|
-
owner_enc = abi_address(address)
|
|
389
|
-
spender_enc = abi_address(spender)
|
|
390
|
-
value_enc = abi_uint256(value)
|
|
391
|
-
nonce_enc = abi_uint256(nonce)
|
|
392
|
-
deadline_enc = abi_uint256(deadline)
|
|
393
|
-
|
|
394
|
-
struct_data = type_hash + owner_enc + spender_enc + value_enc + nonce_enc + deadline_enc
|
|
395
|
-
struct_hash = keccak256_raw(struct_data)
|
|
396
|
-
|
|
397
|
-
# EIP-712 digest
|
|
398
|
-
digest = keccak256_raw("\x19\x01".b + domain_sep + struct_hash)
|
|
399
|
-
sig_hex = @signer.sign(digest)
|
|
400
|
-
|
|
401
|
-
# Parse r, s, v from the 65-byte signature
|
|
402
|
-
sig_bytes = sig_hex.delete_prefix("0x")
|
|
403
|
-
r = "0x#{sig_bytes[0, 64]}"
|
|
404
|
-
s = "0x#{sig_bytes[64, 64]}"
|
|
405
|
-
v = sig_bytes[128, 2].to_i(16)
|
|
406
|
-
|
|
407
|
-
PermitSignature.new(value: value, deadline: deadline, v: v, r: r, s: s)
|
|
408
|
-
end
|
|
409
|
-
|
|
410
|
-
# Convenience: sign a USDC permit. Auto-fetches nonce, defaults deadline to 1 hour.
|
|
411
|
-
# @param spender [String] contract address to approve (e.g. router, escrow)
|
|
355
|
+
# Sign a USDC permit via the server's /permits/prepare endpoint.
|
|
356
|
+
#
|
|
357
|
+
# The server computes the EIP-712 hash, manages nonces, and resolves
|
|
358
|
+
# contract addresses. The SDK only signs the hash.
|
|
359
|
+
#
|
|
360
|
+
# @param flow [String] payment flow ("direct", "escrow", "tab", "stream", "bounty", "deposit")
|
|
412
361
|
# @param amount [Numeric] amount in USDC (e.g. 5.0 for $5.00)
|
|
413
|
-
# @param deadline [Integer, nil] optional Unix timestamp; defaults to 1 hour from now
|
|
414
362
|
# @return [PermitSignature]
|
|
415
|
-
def sign_permit(
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
363
|
+
def sign_permit(flow, amount)
|
|
364
|
+
data = @transport.post("/permits/prepare", {
|
|
365
|
+
flow: flow,
|
|
366
|
+
amount: amount.to_s,
|
|
367
|
+
owner: address
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
hash_hex = data["hash"]
|
|
371
|
+
hash_bytes = [hash_hex.delete_prefix("0x")].pack("H*")
|
|
372
|
+
|
|
373
|
+
sig = @signer.sign_hash(hash_bytes)
|
|
374
|
+
sig_hex = sig.start_with?("0x") ? sig[2..] : sig
|
|
375
|
+
r = "0x#{sig_hex[0, 64]}"
|
|
376
|
+
s = "0x#{sig_hex[64, 64]}"
|
|
377
|
+
v = sig_hex[128, 2].to_i(16)
|
|
378
|
+
|
|
379
|
+
PermitSignature.new(
|
|
380
|
+
value: data["value"].to_i,
|
|
381
|
+
deadline: data["deadline"].to_i,
|
|
382
|
+
v: v, r: r, s: s
|
|
383
|
+
)
|
|
429
384
|
end
|
|
430
385
|
|
|
431
386
|
# ─── Streams (Payment Streaming) ─────────────────────────────────────────
|
|
@@ -676,7 +631,7 @@ module Remitmd
|
|
|
676
631
|
)
|
|
677
632
|
end
|
|
678
633
|
|
|
679
|
-
# ─── EIP-712 helpers (used by sign_tab_charge
|
|
634
|
+
# ─── EIP-712 helpers (used by sign_tab_charge) ─────────────────────
|
|
680
635
|
|
|
681
636
|
def keccak256_raw(data)
|
|
682
637
|
Remitmd::Keccak.digest(data.b)
|
|
@@ -693,51 +648,22 @@ module Remitmd
|
|
|
693
648
|
|
|
694
649
|
# ─── Permit helpers ──────────────────────────────────────────────────
|
|
695
650
|
|
|
696
|
-
#
|
|
697
|
-
#
|
|
698
|
-
#
|
|
699
|
-
def fetch_permit_nonce(_usdc_address)
|
|
700
|
-
return 0 if @mock_mode
|
|
701
|
-
|
|
702
|
-
data = @transport.get("/status/#{address}")
|
|
703
|
-
nonce = data.is_a?(Hash) ? data["permit_nonce"] : nil
|
|
704
|
-
if nonce.nil?
|
|
705
|
-
raise RemitError.new(
|
|
706
|
-
RemitError::NETWORK_ERROR,
|
|
707
|
-
"permit_nonce not available from API for #{address}. " \
|
|
708
|
-
"Ensure the server supports the permit_nonce field in GET /api/v1/status.",
|
|
709
|
-
context: { address: address }
|
|
710
|
-
)
|
|
711
|
-
end
|
|
712
|
-
nonce.to_i
|
|
713
|
-
end
|
|
714
|
-
|
|
715
|
-
# Auto-sign a permit for the given contract type and amount.
|
|
716
|
-
# Returns nil on failure instead of raising, so callers can proceed without a permit.
|
|
651
|
+
# Internal: auto-sign a permit via /permits/prepare.
|
|
652
|
+
# Maps the contract name to a flow and calls sign_permit().
|
|
653
|
+
# Returns nil on failure so callers degrade gracefully.
|
|
717
654
|
# @param contract [String] contract key - "router", "escrow", "tab", etc.
|
|
718
655
|
# @param amount [Numeric] amount in USDC
|
|
719
656
|
# @return [PermitSignature, nil]
|
|
720
657
|
def auto_permit(contract, amount)
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
658
|
+
flow = CONTRACT_TO_FLOW[contract]
|
|
659
|
+
unless flow
|
|
660
|
+
warn "[remitmd] unknown contract for permit: #{contract}"
|
|
661
|
+
return nil
|
|
662
|
+
end
|
|
663
|
+
sign_permit(flow, amount)
|
|
726
664
|
rescue => e
|
|
727
665
|
warn "[remitmd] auto-permit failed for #{contract} (amount=#{amount}): #{e.message}"
|
|
728
666
|
nil
|
|
729
667
|
end
|
|
730
|
-
|
|
731
|
-
# Spender contract mapping for auto_permit.
|
|
732
|
-
PERMIT_SPENDER = {
|
|
733
|
-
pay: :router,
|
|
734
|
-
create_escrow: :escrow,
|
|
735
|
-
create_tab: :tab,
|
|
736
|
-
create_stream: :stream,
|
|
737
|
-
create_bounty: :bounty,
|
|
738
|
-
place_deposit: :deposit,
|
|
739
|
-
create_fund_link: :relayer,
|
|
740
|
-
create_withdraw_link: :relayer,
|
|
741
|
-
}.freeze
|
|
742
668
|
end
|
|
743
669
|
end
|
data/lib/remitmd/x402_client.rb
CHANGED
|
@@ -26,22 +26,25 @@ module Remitmd
|
|
|
26
26
|
# On receiving a 402, the client:
|
|
27
27
|
# 1. Decodes the PAYMENT-REQUIRED header (base64 JSON)
|
|
28
28
|
# 2. Checks the amount is within max_auto_pay_usdc
|
|
29
|
-
# 3.
|
|
30
|
-
# 4.
|
|
31
|
-
# 5.
|
|
29
|
+
# 3. Calls /x402/prepare to get hash + authorization fields
|
|
30
|
+
# 4. Signs the hash
|
|
31
|
+
# 5. Base64-encodes the PAYMENT-SIGNATURE header
|
|
32
|
+
# 6. Retries the original request with payment attached
|
|
32
33
|
#
|
|
33
34
|
# @example
|
|
34
35
|
# signer = Remitmd::PrivateKeySigner.new("0x...")
|
|
35
|
-
# client = Remitmd::X402Client.new(wallet: signer)
|
|
36
|
+
# client = Remitmd::X402Client.new(wallet: signer, api_transport: transport)
|
|
36
37
|
# response = client.fetch("https://api.provider.com/v1/data")
|
|
37
38
|
#
|
|
38
39
|
class X402Client
|
|
39
40
|
attr_reader :last_payment
|
|
40
41
|
|
|
41
|
-
# @param wallet [#
|
|
42
|
+
# @param wallet [#sign_hash, #address] a signer that can sign raw hashes
|
|
43
|
+
# @param api_transport [#post] authenticated HTTP transport for calling /x402/prepare
|
|
42
44
|
# @param max_auto_pay_usdc [Float] maximum USDC amount to auto-pay per request (default: 0.10)
|
|
43
|
-
def initialize(wallet:, max_auto_pay_usdc: 0.10)
|
|
45
|
+
def initialize(wallet:, api_transport: nil, max_auto_pay_usdc: 0.10)
|
|
44
46
|
@wallet = wallet
|
|
47
|
+
@api_transport = api_transport
|
|
45
48
|
@max_auto_pay_usdc = max_auto_pay_usdc
|
|
46
49
|
@last_payment = nil
|
|
47
50
|
end
|
|
@@ -106,96 +109,44 @@ module Remitmd
|
|
|
106
109
|
raise AllowanceExceededError.new(amount_usdc, @max_auto_pay_usdc)
|
|
107
110
|
end
|
|
108
111
|
|
|
109
|
-
# 4.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
value: amount_base_units,
|
|
125
|
-
valid_after: 0,
|
|
126
|
-
valid_before: valid_before,
|
|
127
|
-
nonce_bytes: nonce_bytes
|
|
128
|
-
)
|
|
129
|
-
signature = @wallet.sign(digest)
|
|
112
|
+
# 4. Call /x402/prepare to get the hash + authorization fields.
|
|
113
|
+
unless @api_transport
|
|
114
|
+
raise RemitError.new("SERVER_ERROR",
|
|
115
|
+
"x402 auto-pay requires an api_transport for calling /x402/prepare")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
prepare_data = @api_transport.post("/x402/prepare", {
|
|
119
|
+
payment_required: raw,
|
|
120
|
+
payer: @wallet.address
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
# 5. Sign the hash.
|
|
124
|
+
hash_hex = prepare_data["hash"]
|
|
125
|
+
hash_bytes = [hash_hex.delete_prefix("0x")].pack("H*")
|
|
126
|
+
signature = @wallet.sign_hash(hash_bytes)
|
|
130
127
|
|
|
131
|
-
#
|
|
128
|
+
# 6. Build PAYMENT-SIGNATURE JSON payload.
|
|
132
129
|
payment_payload = {
|
|
133
130
|
scheme: required["scheme"],
|
|
134
131
|
network: required["network"],
|
|
135
132
|
x402Version: 1,
|
|
136
133
|
payload: {
|
|
137
|
-
signature:
|
|
134
|
+
signature: signature,
|
|
138
135
|
authorization: {
|
|
139
|
-
from:
|
|
140
|
-
to:
|
|
141
|
-
value:
|
|
142
|
-
validAfter: "
|
|
143
|
-
validBefore: valid_before
|
|
144
|
-
nonce:
|
|
136
|
+
from: prepare_data["from"],
|
|
137
|
+
to: prepare_data["to"],
|
|
138
|
+
value: prepare_data["value"],
|
|
139
|
+
validAfter: prepare_data["valid_after"] || prepare_data["validAfter"],
|
|
140
|
+
validBefore: prepare_data["valid_before"] || prepare_data["validBefore"],
|
|
141
|
+
nonce: prepare_data["nonce"],
|
|
145
142
|
},
|
|
146
143
|
},
|
|
147
144
|
}
|
|
148
145
|
payment_header = Base64.strict_encode64(JSON.generate(payment_payload))
|
|
149
146
|
|
|
150
|
-
#
|
|
147
|
+
# 7. Retry with PAYMENT-SIGNATURE header.
|
|
151
148
|
new_headers = headers.merge("PAYMENT-SIGNATURE" => payment_header)
|
|
152
149
|
make_request(uri, method, new_headers, body)
|
|
153
150
|
end
|
|
154
|
-
|
|
155
|
-
# Compute the EIP-712 hash for EIP-3009 TransferWithAuthorization.
|
|
156
|
-
def eip3009_digest(chain_id:, asset:, from:, to:, value:, valid_after:, valid_before:, nonce_bytes:)
|
|
157
|
-
# Domain separator: USD Coin / version 2
|
|
158
|
-
domain_type_hash = keccak256(
|
|
159
|
-
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
|
|
160
|
-
)
|
|
161
|
-
name_hash = keccak256("USD Coin")
|
|
162
|
-
version_hash = keccak256("2")
|
|
163
|
-
chain_id_enc = abi_uint256(chain_id)
|
|
164
|
-
contract_enc = abi_address(asset)
|
|
165
|
-
|
|
166
|
-
domain_data = domain_type_hash + name_hash + version_hash + chain_id_enc + contract_enc
|
|
167
|
-
domain_separator = keccak256(domain_data)
|
|
168
|
-
|
|
169
|
-
# TransferWithAuthorization struct hash
|
|
170
|
-
type_hash = keccak256(
|
|
171
|
-
"TransferWithAuthorization(address from,address to,uint256 value," \
|
|
172
|
-
"uint256 validAfter,uint256 validBefore,bytes32 nonce)"
|
|
173
|
-
)
|
|
174
|
-
struct_data = type_hash +
|
|
175
|
-
abi_address(from) +
|
|
176
|
-
abi_address(to) +
|
|
177
|
-
abi_uint256(value) +
|
|
178
|
-
abi_uint256(valid_after) +
|
|
179
|
-
abi_uint256(valid_before) +
|
|
180
|
-
nonce_bytes
|
|
181
|
-
|
|
182
|
-
struct_hash = keccak256(struct_data)
|
|
183
|
-
|
|
184
|
-
# Final EIP-712 hash
|
|
185
|
-
keccak256("\x19\x01" + domain_separator + struct_hash)
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
def keccak256(data)
|
|
189
|
-
Remitmd::Keccak.digest(data.b)
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def abi_uint256(value)
|
|
193
|
-
[value.to_i.to_s(16).rjust(64, "0")].pack("H*")
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def abi_address(addr)
|
|
197
|
-
hex = addr.to_s.delete_prefix("0x").rjust(64, "0")
|
|
198
|
-
[hex].pack("H*")
|
|
199
|
-
end
|
|
200
151
|
end
|
|
201
152
|
end
|
data/lib/remitmd.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: remitmd
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- remit.md
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rspec
|