remitmd 0.1.1 → 0.1.3
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 +46 -15
- data/lib/remitmd/mock.rb +17 -1
- data/lib/remitmd/wallet.rb +198 -27
- 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: 92f3192f6e981e0a39fd2faf98437858b5b25f8f462ae4daea65163c1b9d210b
|
|
4
|
+
data.tar.gz: 2d20d2c5dfda21c3ce11ff9876a8d27a488076091535b7186d3d4252b7f78b7d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8c90a39e2ce798a689270c82ec4cc8e69a65494f3969f0f4263f0476decf45ffd5ee2886574992113dd467dc50946e7bf00213ae81ec9e3f01e89c3db7a9a820
|
|
7
|
+
data.tar.gz: a36878cebc127b81bdf28ea8a5b9e9a9a1c4dd809e3ca59a0ef54c4060fd4c0216e3ccd2af87b9be1214fbc3a6958d74a833f2b67f9e794c62fd629a09da7a62
|
data/README.md
CHANGED
|
@@ -38,26 +38,17 @@ Or from environment variables:
|
|
|
38
38
|
```ruby
|
|
39
39
|
wallet = Remitmd::RemitWallet.from_env
|
|
40
40
|
# Requires: REMITMD_PRIVATE_KEY
|
|
41
|
-
# Optional: REMITMD_CHAIN (default: "base"), REMITMD_API_URL
|
|
41
|
+
# Optional: REMITMD_CHAIN (default: "base"), REMITMD_API_URL, REMITMD_RPC_URL
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
```ruby
|
|
47
|
-
contracts = wallet.get_contracts
|
|
48
|
-
|
|
49
|
-
# Use permits on any payment method
|
|
50
|
-
tx = wallet.pay("0xRecipient...", 5.00, permit: permit)
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
Permits are optional on: `pay`, `create_escrow`, `create_tab`, `create_stream`, `create_bounty`, `place_deposit`.
|
|
44
|
+
Permits are auto-signed. Every payment method fetches the on-chain USDC nonce, signs an EIP-2612 permit, and includes it automatically.
|
|
54
45
|
|
|
55
46
|
## Payment Models
|
|
56
47
|
|
|
57
48
|
### Direct Payment
|
|
58
49
|
|
|
59
50
|
```ruby
|
|
60
|
-
tx = wallet.pay("0xRecipient...", 5.00, memo: "AI inference fee"
|
|
51
|
+
tx = wallet.pay("0xRecipient...", 5.00, memo: "AI inference fee")
|
|
61
52
|
```
|
|
62
53
|
|
|
63
54
|
### Escrow
|
|
@@ -73,10 +64,11 @@ tx = wallet.cancel_escrow(escrow.id) # refund yourself
|
|
|
73
64
|
### Metered Tab (off-chain billing)
|
|
74
65
|
|
|
75
66
|
```ruby
|
|
76
|
-
tab = wallet.create_tab("0xProvider...", 50.00, 0.003
|
|
67
|
+
tab = wallet.create_tab("0xProvider...", 50.00, 0.003)
|
|
77
68
|
|
|
78
69
|
# Provider charges with EIP-712 signature
|
|
79
|
-
|
|
70
|
+
contracts = wallet.get_contracts
|
|
71
|
+
sig = wallet.sign_tab_charge(contracts.tab, tab.id, 3_000_000, 1)
|
|
80
72
|
wallet.charge_tab(tab.id, 0.003, 0.003, 1, sig)
|
|
81
73
|
|
|
82
74
|
# Close when done — unused funds return
|
|
@@ -104,7 +96,7 @@ tx = wallet.award_bounty(bounty.id, "0xWinner...")
|
|
|
104
96
|
### Security Deposit
|
|
105
97
|
|
|
106
98
|
```ruby
|
|
107
|
-
dep = wallet.place_deposit("0xCounterpart...", 100.00, expires_in_secs: 86_400
|
|
99
|
+
dep = wallet.place_deposit("0xCounterpart...", 100.00, expires_in_secs: 86_400)
|
|
108
100
|
wallet.return_deposit(dep.id)
|
|
109
101
|
```
|
|
110
102
|
|
|
@@ -181,6 +173,10 @@ wallet.close_tab(tab_id, final_amount: nil, provider_sig: nil)
|
|
|
181
173
|
# Tab provider (signing charges)
|
|
182
174
|
wallet.sign_tab_charge(tab_contract, tab_id, total_charged, call_count) # String
|
|
183
175
|
|
|
176
|
+
# EIP-2612 Permit (auto-signed when omitted from payment methods)
|
|
177
|
+
wallet.sign_permit(spender, amount, deadline: nil) # PermitSignature
|
|
178
|
+
wallet.sign_usdc_permit(spender, value, deadline, nonce, usdc_address: nil) # PermitSignature
|
|
179
|
+
|
|
184
180
|
# Streams
|
|
185
181
|
wallet.create_stream(payee, rate_per_second, max_total, permit: nil) # Stream
|
|
186
182
|
wallet.close_stream(stream_id) # Stream
|
|
@@ -249,6 +245,41 @@ Remitmd::RemitWallet.new(private_key: key, chain: "base") # Base mainne
|
|
|
249
245
|
Remitmd::RemitWallet.new(private_key: key, chain: "base_sepolia") # Base Sepolia testnet
|
|
250
246
|
```
|
|
251
247
|
|
|
248
|
+
## Advanced
|
|
249
|
+
|
|
250
|
+
### Manual Permit Signing
|
|
251
|
+
|
|
252
|
+
Permits are auto-signed by default. If you need manual control (custom deadline, pre-signed permits, or offline signing), pass a `PermitSignature` explicitly:
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
# sign_permit: convenience — auto-fetches nonce, converts amount to base units
|
|
256
|
+
permit = wallet.sign_permit("0xRouterAddress...", 5.00, deadline: Time.now.to_i + 7200)
|
|
257
|
+
tx = wallet.pay("0xRecipient...", 5.00, permit: permit)
|
|
258
|
+
|
|
259
|
+
# sign_usdc_permit: full control — raw base units, explicit nonce
|
|
260
|
+
permit = wallet.sign_usdc_permit(
|
|
261
|
+
"0xRouterAddress...", # spender
|
|
262
|
+
5_000_000, # value in base units (6 decimals)
|
|
263
|
+
Time.now.to_i + 3600, # deadline
|
|
264
|
+
0, # nonce
|
|
265
|
+
usdc_address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
266
|
+
)
|
|
267
|
+
tx = wallet.pay("0xRecipient...", 5.00, permit: permit)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Custom RPC URL
|
|
271
|
+
|
|
272
|
+
Override the JSON-RPC endpoint used for nonce fetching:
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
wallet = Remitmd::RemitWallet.new(
|
|
276
|
+
private_key: key,
|
|
277
|
+
chain: "base_sepolia",
|
|
278
|
+
rpc_url: "https://your-rpc-provider.com/v1/base-sepolia"
|
|
279
|
+
)
|
|
280
|
+
# Or via environment: REMITMD_RPC_URL
|
|
281
|
+
```
|
|
282
|
+
|
|
252
283
|
## License
|
|
253
284
|
|
|
254
285
|
MIT — see [LICENSE](LICENSE)
|
data/lib/remitmd/mock.rb
CHANGED
|
@@ -106,7 +106,7 @@ module Remitmd
|
|
|
106
106
|
attr_reader :address
|
|
107
107
|
|
|
108
108
|
def sign(_message)
|
|
109
|
-
"0x" + SecureRandom.hex(32)
|
|
109
|
+
"0x" + SecureRandom.hex(32) + SecureRandom.hex(32) + "1b"
|
|
110
110
|
end
|
|
111
111
|
end
|
|
112
112
|
|
|
@@ -135,6 +135,22 @@ module Remitmd
|
|
|
135
135
|
def handle(method, path, b) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
136
136
|
case [method, path]
|
|
137
137
|
|
|
138
|
+
# Contracts (for auto_permit)
|
|
139
|
+
in ["GET", "/contracts"]
|
|
140
|
+
{
|
|
141
|
+
"chain_id" => ChainId::BASE_SEPOLIA,
|
|
142
|
+
"usdc" => "0xMockUSDC0000000000000000000000000000001",
|
|
143
|
+
"router" => "0xMockRouter000000000000000000000000001",
|
|
144
|
+
"escrow" => "0xMockEscrow000000000000000000000000001",
|
|
145
|
+
"tab" => "0xMockTab00000000000000000000000000001",
|
|
146
|
+
"stream" => "0xMockStream000000000000000000000000001",
|
|
147
|
+
"bounty" => "0xMockBounty000000000000000000000000001",
|
|
148
|
+
"deposit" => "0xMockDeposit00000000000000000000000001",
|
|
149
|
+
"fee_calculator" => "0xMockFeeCalc0000000000000000000000001",
|
|
150
|
+
"key_registry" => "0xMockKeyReg000000000000000000000000001",
|
|
151
|
+
"arbitration" => "0xMockArbitr000000000000000000000000001",
|
|
152
|
+
}
|
|
153
|
+
|
|
138
154
|
# Balance
|
|
139
155
|
in ["GET", "/wallet/balance"]
|
|
140
156
|
{
|
data/lib/remitmd/wallet.rb
CHANGED
|
@@ -2,8 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
require "bigdecimal"
|
|
4
4
|
require "bigdecimal/util"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "uri"
|
|
7
|
+
require "json"
|
|
5
8
|
|
|
6
9
|
module Remitmd
|
|
10
|
+
# Known USDC contract addresses per chain (EIP-2612 compatible).
|
|
11
|
+
USDC_ADDRESSES = {
|
|
12
|
+
"base-sepolia" => "0x142aD61B8d2edD6b3807D9266866D97C35Ee0317",
|
|
13
|
+
"base" => "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
14
|
+
"localhost" => "0x5FbDB2315678afecb367f032d93F642f64180aa3",
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
# Default JSON-RPC endpoints per chain (for nonce fetching).
|
|
18
|
+
DEFAULT_RPC_URLS = {
|
|
19
|
+
"base-sepolia" => "https://sepolia.base.org",
|
|
20
|
+
"base" => "https://mainnet.base.org",
|
|
21
|
+
"localhost" => "http://127.0.0.1:8545",
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
7
24
|
# Primary remit.md client. All payment operations are methods on RemitWallet.
|
|
8
25
|
#
|
|
9
26
|
# @example Quickstart
|
|
@@ -22,12 +39,17 @@ module Remitmd
|
|
|
22
39
|
# @param signer [Signer, nil] custom signer (pass instead of private_key)
|
|
23
40
|
# @param chain [String] chain name — "base", "base_sepolia"
|
|
24
41
|
# @param api_url [String, nil] override API base URL
|
|
42
|
+
# @param rpc_url [String, nil] override JSON-RPC URL for on-chain queries
|
|
25
43
|
# @param transport [Object, nil] inject mock transport (used by MockRemit)
|
|
26
|
-
def initialize(private_key: nil, signer: nil, chain: "base", api_url: nil, router_address: nil, transport: nil)
|
|
44
|
+
def initialize(private_key: nil, signer: nil, chain: "base", api_url: nil, router_address: nil, rpc_url: nil, transport: nil)
|
|
27
45
|
if transport
|
|
28
46
|
# MockRemit path: transport + signer injected directly
|
|
29
47
|
@signer = signer
|
|
30
48
|
@transport = transport
|
|
49
|
+
@chain_key = "base-sepolia"
|
|
50
|
+
@chain_id = ChainId::BASE_SEPOLIA
|
|
51
|
+
@rpc_url = DEFAULT_RPC_URLS["base-sepolia"]
|
|
52
|
+
@mock_mode = true
|
|
31
53
|
return
|
|
32
54
|
end
|
|
33
55
|
|
|
@@ -39,6 +61,8 @@ module Remitmd
|
|
|
39
61
|
end
|
|
40
62
|
|
|
41
63
|
@signer = signer || PrivateKeySigner.new(private_key)
|
|
64
|
+
# Normalize chain key for USDC/RPC lookups (underscore → hyphen).
|
|
65
|
+
@chain_key = chain.tr("_", "-")
|
|
42
66
|
# Normalize to the base chain name (strip testnet suffix) for use in pay body.
|
|
43
67
|
# The server accepts "base" — not "base_sepolia" etc.
|
|
44
68
|
@chain = chain.sub(/_sepolia\z/, "").sub(/-sepolia\z/, "")
|
|
@@ -46,24 +70,26 @@ module Remitmd
|
|
|
46
70
|
raise ArgumentError, "Unknown chain: #{chain}. Valid: #{CHAIN_CONFIG.keys.join(", ")}"
|
|
47
71
|
end
|
|
48
72
|
base_url = api_url || cfg[:url]
|
|
49
|
-
chain_id
|
|
73
|
+
@chain_id = cfg[:chain_id]
|
|
50
74
|
router_address ||= ""
|
|
75
|
+
@rpc_url = rpc_url || ENV["REMITMD_RPC_URL"] || DEFAULT_RPC_URLS[@chain_key] || DEFAULT_RPC_URLS["base-sepolia"]
|
|
51
76
|
@transport = HttpTransport.new(
|
|
52
77
|
base_url: base_url,
|
|
53
78
|
signer: @signer,
|
|
54
|
-
chain_id: chain_id,
|
|
79
|
+
chain_id: @chain_id,
|
|
55
80
|
router_address: router_address
|
|
56
81
|
)
|
|
57
82
|
end
|
|
58
83
|
|
|
59
84
|
# Build a RemitWallet from environment variables.
|
|
60
|
-
# Reads: REMITMD_PRIVATE_KEY, REMITMD_CHAIN, REMITMD_API_URL, REMITMD_ROUTER_ADDRESS.
|
|
85
|
+
# Reads: REMITMD_PRIVATE_KEY, REMITMD_CHAIN, REMITMD_API_URL, REMITMD_ROUTER_ADDRESS, REMITMD_RPC_URL.
|
|
61
86
|
def self.from_env
|
|
62
87
|
key = ENV.fetch("REMITMD_PRIVATE_KEY") { raise ArgumentError, "REMITMD_PRIVATE_KEY not set" }
|
|
63
88
|
chain = ENV.fetch("REMITMD_CHAIN", "base")
|
|
64
89
|
api_url = ENV["REMITMD_API_URL"]
|
|
65
90
|
router_address = ENV["REMITMD_ROUTER_ADDRESS"]
|
|
66
|
-
|
|
91
|
+
rpc_url = ENV["REMITMD_RPC_URL"]
|
|
92
|
+
new(private_key: key, chain: chain, api_url: api_url, router_address: router_address, rpc_url: rpc_url)
|
|
67
93
|
end
|
|
68
94
|
|
|
69
95
|
# The Ethereum address associated with this wallet.
|
|
@@ -128,14 +154,15 @@ module Remitmd
|
|
|
128
154
|
# @param to [String] recipient 0x-prefixed address
|
|
129
155
|
# @param amount [Numeric, BigDecimal] amount in USDC (e.g. 1.50)
|
|
130
156
|
# @param memo [String, nil] optional note
|
|
131
|
-
# @param permit [PermitSignature, nil]
|
|
157
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit — auto-signed if nil
|
|
132
158
|
# @return [Transaction]
|
|
133
159
|
def pay(to, amount, memo: nil, permit: nil)
|
|
134
160
|
validate_address!(to)
|
|
135
161
|
validate_amount!(amount)
|
|
162
|
+
resolved = permit || auto_permit("router", amount.to_f)
|
|
136
163
|
nonce = SecureRandom.hex(16)
|
|
137
164
|
body = { to: to, amount: amount.to_s, task: memo || "", chain: @chain, nonce: nonce, signature: "0x" }
|
|
138
|
-
body[:permit] =
|
|
165
|
+
body[:permit] = resolved.to_h
|
|
139
166
|
Transaction.new(@transport.post("/payments/direct", body))
|
|
140
167
|
end
|
|
141
168
|
|
|
@@ -146,11 +173,12 @@ module Remitmd
|
|
|
146
173
|
# @param amount [Numeric] amount in USDC
|
|
147
174
|
# @param memo [String, nil] optional note
|
|
148
175
|
# @param expires_in_secs [Integer, nil] optional expiry in seconds from now
|
|
149
|
-
# @param permit [PermitSignature, nil]
|
|
176
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit — auto-signed if nil
|
|
150
177
|
# @return [Escrow]
|
|
151
178
|
def create_escrow(payee, amount, memo: nil, expires_in_secs: nil, permit: nil)
|
|
152
179
|
validate_address!(payee)
|
|
153
180
|
validate_amount!(amount)
|
|
181
|
+
resolved = permit || auto_permit("escrow", amount.to_f)
|
|
154
182
|
|
|
155
183
|
# Step 1: create invoice on server.
|
|
156
184
|
invoice_id = SecureRandom.hex(16)
|
|
@@ -165,8 +193,7 @@ module Remitmd
|
|
|
165
193
|
@transport.post("/invoices", inv_body)
|
|
166
194
|
|
|
167
195
|
# Step 2: fund the escrow.
|
|
168
|
-
esc_body = { invoice_id: invoice_id }
|
|
169
|
-
esc_body[:permit] = permit.to_h if permit
|
|
196
|
+
esc_body = { invoice_id: invoice_id, permit: resolved.to_h }
|
|
170
197
|
Escrow.new(@transport.post("/escrows", esc_body))
|
|
171
198
|
end
|
|
172
199
|
|
|
@@ -207,19 +234,20 @@ module Remitmd
|
|
|
207
234
|
# @param limit_amount [Numeric] maximum tab credit in USDC
|
|
208
235
|
# @param per_unit [Numeric] USDC per API call
|
|
209
236
|
# @param expires_in_secs [Integer] optional expiry duration in seconds (default: 86400)
|
|
210
|
-
# @param permit [PermitSignature, nil]
|
|
237
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit — auto-signed if nil
|
|
211
238
|
# @return [Tab]
|
|
212
239
|
def create_tab(provider, limit_amount, per_unit = 0.0, expires_in_secs: 86_400, permit: nil)
|
|
213
240
|
validate_address!(provider)
|
|
214
241
|
validate_amount!(limit_amount)
|
|
242
|
+
resolved = permit || auto_permit("tab", limit_amount.to_f)
|
|
215
243
|
body = {
|
|
216
244
|
chain: @chain,
|
|
217
245
|
provider: provider,
|
|
218
246
|
limit_amount: limit_amount.to_f,
|
|
219
247
|
per_unit: per_unit.to_f,
|
|
220
|
-
expiry: Time.now.to_i + expires_in_secs
|
|
248
|
+
expiry: Time.now.to_i + expires_in_secs,
|
|
249
|
+
permit: resolved.to_h
|
|
221
250
|
}
|
|
222
|
-
body[:permit] = permit.to_h if permit
|
|
223
251
|
Tab.new(@transport.post("/tabs", body))
|
|
224
252
|
end
|
|
225
253
|
|
|
@@ -310,25 +338,92 @@ module Remitmd
|
|
|
310
338
|
@signer.sign(digest)
|
|
311
339
|
end
|
|
312
340
|
|
|
341
|
+
# ─── EIP-2612 Permit ─────────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
# Sign an EIP-2612 permit for USDC approval.
|
|
344
|
+
# Domain: name="USD Coin", version="2", chainId, verifyingContract=USDC address
|
|
345
|
+
# Type: Permit(address owner, address spender, uint256 value, uint256 nonce, uint256 deadline)
|
|
346
|
+
# @param spender [String] contract address that will be approved
|
|
347
|
+
# @param value [Integer] amount in USDC base units (6 decimals)
|
|
348
|
+
# @param deadline [Integer] permit deadline (Unix timestamp)
|
|
349
|
+
# @param nonce [Integer] current permit nonce for this wallet
|
|
350
|
+
# @param usdc_address [String, nil] override the USDC contract address
|
|
351
|
+
# @return [PermitSignature]
|
|
352
|
+
def sign_usdc_permit(spender, value, deadline, nonce = 0, usdc_address: nil)
|
|
353
|
+
usdc_addr = usdc_address || USDC_ADDRESSES[@chain_key] || ""
|
|
354
|
+
chain_id = @chain_id || ChainId::BASE_SEPOLIA
|
|
355
|
+
|
|
356
|
+
# Domain separator for USDC (EIP-2612)
|
|
357
|
+
domain_type_hash = keccak256_raw(
|
|
358
|
+
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
|
|
359
|
+
)
|
|
360
|
+
name_hash = keccak256_raw("USD Coin")
|
|
361
|
+
version_hash = keccak256_raw("2")
|
|
362
|
+
chain_id_enc = abi_uint256(chain_id)
|
|
363
|
+
contract_enc = abi_address(usdc_addr)
|
|
364
|
+
|
|
365
|
+
domain_data = domain_type_hash + name_hash + version_hash + chain_id_enc + contract_enc
|
|
366
|
+
domain_sep = keccak256_raw(domain_data)
|
|
367
|
+
|
|
368
|
+
# Permit struct hash
|
|
369
|
+
type_hash = keccak256_raw(
|
|
370
|
+
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
|
|
371
|
+
)
|
|
372
|
+
owner_enc = abi_address(address)
|
|
373
|
+
spender_enc = abi_address(spender)
|
|
374
|
+
value_enc = abi_uint256(value)
|
|
375
|
+
nonce_enc = abi_uint256(nonce)
|
|
376
|
+
deadline_enc = abi_uint256(deadline)
|
|
377
|
+
|
|
378
|
+
struct_data = type_hash + owner_enc + spender_enc + value_enc + nonce_enc + deadline_enc
|
|
379
|
+
struct_hash = keccak256_raw(struct_data)
|
|
380
|
+
|
|
381
|
+
# EIP-712 digest
|
|
382
|
+
digest = keccak256_raw("\x19\x01".b + domain_sep + struct_hash)
|
|
383
|
+
sig_hex = @signer.sign(digest)
|
|
384
|
+
|
|
385
|
+
# Parse r, s, v from the 65-byte signature
|
|
386
|
+
sig_bytes = sig_hex.delete_prefix("0x")
|
|
387
|
+
r = "0x#{sig_bytes[0, 64]}"
|
|
388
|
+
s = "0x#{sig_bytes[64, 64]}"
|
|
389
|
+
v = sig_bytes[128, 2].to_i(16)
|
|
390
|
+
|
|
391
|
+
PermitSignature.new(value: value, deadline: deadline, v: v, r: r, s: s)
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Convenience: sign a USDC permit. Auto-fetches nonce, defaults deadline to 1 hour.
|
|
395
|
+
# @param spender [String] contract address to approve (e.g. router, escrow)
|
|
396
|
+
# @param amount [Numeric] amount in USDC (e.g. 5.0 for $5.00)
|
|
397
|
+
# @param deadline [Integer, nil] optional Unix timestamp; defaults to 1 hour from now
|
|
398
|
+
# @return [PermitSignature]
|
|
399
|
+
def sign_permit(spender, amount, deadline: nil)
|
|
400
|
+
usdc_addr = USDC_ADDRESSES[@chain_key] || ""
|
|
401
|
+
nonce = fetch_usdc_nonce(usdc_addr)
|
|
402
|
+
dl = deadline || (Time.now.to_i + 3600)
|
|
403
|
+
raw = (amount * 1_000_000).round.to_i
|
|
404
|
+
sign_usdc_permit(spender, raw, dl, nonce, usdc_address: usdc_addr)
|
|
405
|
+
end
|
|
406
|
+
|
|
313
407
|
# ─── Streams (Payment Streaming) ─────────────────────────────────────────
|
|
314
408
|
|
|
315
409
|
# Create a real-time payment stream.
|
|
316
410
|
# @param payee [String] 0x-prefixed address of the stream recipient
|
|
317
411
|
# @param rate_per_second [Numeric] USDC per second
|
|
318
412
|
# @param max_total [Numeric] maximum total USDC for the stream
|
|
319
|
-
# @param permit [PermitSignature, nil]
|
|
413
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit — auto-signed if nil
|
|
320
414
|
# @return [Stream]
|
|
321
415
|
def create_stream(payee, rate_per_second, max_total, permit: nil)
|
|
322
416
|
validate_address!(payee)
|
|
323
417
|
validate_amount!(rate_per_second)
|
|
324
418
|
validate_amount!(max_total)
|
|
419
|
+
resolved = permit || auto_permit("stream", max_total.to_f)
|
|
325
420
|
body = {
|
|
326
421
|
chain: @chain,
|
|
327
422
|
payee: payee,
|
|
328
423
|
rate_per_second: rate_per_second.to_s,
|
|
329
|
-
max_total: max_total.to_s
|
|
424
|
+
max_total: max_total.to_s,
|
|
425
|
+
permit: resolved.to_h
|
|
330
426
|
}
|
|
331
|
-
body[:permit] = permit.to_h if permit
|
|
332
427
|
Stream.new(@transport.post("/streams", body))
|
|
333
428
|
end
|
|
334
429
|
|
|
@@ -353,18 +448,19 @@ module Remitmd
|
|
|
353
448
|
# @param task_description [String] task description
|
|
354
449
|
# @param deadline [Integer] deadline as Unix timestamp
|
|
355
450
|
# @param max_attempts [Integer] maximum submission attempts (default: 10)
|
|
356
|
-
# @param permit [PermitSignature, nil]
|
|
451
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit — auto-signed if nil
|
|
357
452
|
# @return [Bounty]
|
|
358
453
|
def create_bounty(amount, task_description, deadline, max_attempts: 10, permit: nil)
|
|
359
454
|
validate_amount!(amount)
|
|
455
|
+
resolved = permit || auto_permit("bounty", amount.to_f)
|
|
360
456
|
body = {
|
|
361
457
|
chain: @chain,
|
|
362
458
|
amount: amount.to_f,
|
|
363
459
|
task_description: task_description,
|
|
364
460
|
deadline: deadline,
|
|
365
|
-
max_attempts: max_attempts
|
|
461
|
+
max_attempts: max_attempts,
|
|
462
|
+
permit: resolved.to_h
|
|
366
463
|
}
|
|
367
|
-
body[:permit] = permit.to_h if permit
|
|
368
464
|
Bounty.new(@transport.post("/bounties", body))
|
|
369
465
|
end
|
|
370
466
|
|
|
@@ -406,18 +502,19 @@ module Remitmd
|
|
|
406
502
|
# @param provider [String] 0x-prefixed provider address
|
|
407
503
|
# @param amount [Numeric] amount in USDC
|
|
408
504
|
# @param expires_in_secs [Integer] expiry duration in seconds (default: 3600)
|
|
409
|
-
# @param permit [PermitSignature, nil]
|
|
505
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit — auto-signed if nil
|
|
410
506
|
# @return [Deposit]
|
|
411
507
|
def place_deposit(provider, amount, expires_in_secs: 3600, permit: nil)
|
|
412
508
|
validate_address!(provider)
|
|
413
509
|
validate_amount!(amount)
|
|
510
|
+
resolved = permit || auto_permit("deposit", amount.to_f)
|
|
414
511
|
body = {
|
|
415
512
|
chain: @chain,
|
|
416
513
|
provider: provider,
|
|
417
514
|
amount: amount.to_f,
|
|
418
|
-
expiry: Time.now.to_i + expires_in_secs
|
|
515
|
+
expiry: Time.now.to_i + expires_in_secs,
|
|
516
|
+
permit: resolved.to_h
|
|
419
517
|
}
|
|
420
|
-
body[:permit] = permit.to_h if permit
|
|
421
518
|
Deposit.new(@transport.post("/deposits", body))
|
|
422
519
|
end
|
|
423
520
|
|
|
@@ -464,15 +561,25 @@ module Remitmd
|
|
|
464
561
|
# ─── One-time operator links ───────────────────────────────────────────────
|
|
465
562
|
|
|
466
563
|
# Generate a one-time URL for the operator to fund this wallet.
|
|
564
|
+
# @param messages [Array<Hash>, nil] chat-style messages (each with :role and :text)
|
|
565
|
+
# @param agent_name [String, nil] agent display name shown on the funding page
|
|
467
566
|
# @return [LinkResponse]
|
|
468
|
-
def create_fund_link
|
|
469
|
-
|
|
567
|
+
def create_fund_link(messages: nil, agent_name: nil)
|
|
568
|
+
body = {}
|
|
569
|
+
body[:messages] = messages if messages
|
|
570
|
+
body[:agent_name] = agent_name if agent_name
|
|
571
|
+
LinkResponse.new(@transport.post("/links/fund", body))
|
|
470
572
|
end
|
|
471
573
|
|
|
472
574
|
# Generate a one-time URL for the operator to withdraw funds.
|
|
575
|
+
# @param messages [Array<Hash>, nil] chat-style messages (each with :role and :text)
|
|
576
|
+
# @param agent_name [String, nil] agent display name shown on the withdraw page
|
|
473
577
|
# @return [LinkResponse]
|
|
474
|
-
def create_withdraw_link
|
|
475
|
-
|
|
578
|
+
def create_withdraw_link(messages: nil, agent_name: nil)
|
|
579
|
+
body = {}
|
|
580
|
+
body[:messages] = messages if messages
|
|
581
|
+
body[:agent_name] = agent_name if agent_name
|
|
582
|
+
LinkResponse.new(@transport.post("/links/withdraw", body))
|
|
476
583
|
end
|
|
477
584
|
|
|
478
585
|
# ─── Testnet ──────────────────────────────────────────────────────────────
|
|
@@ -514,7 +621,7 @@ module Remitmd
|
|
|
514
621
|
)
|
|
515
622
|
end
|
|
516
623
|
|
|
517
|
-
# ─── EIP-712 helpers (used by sign_tab_charge)
|
|
624
|
+
# ─── EIP-712 helpers (used by sign_tab_charge / sign_usdc_permit) ─────
|
|
518
625
|
|
|
519
626
|
def keccak256_raw(data)
|
|
520
627
|
Remitmd::Keccak.digest(data.b)
|
|
@@ -528,5 +635,69 @@ module Remitmd
|
|
|
528
635
|
hex = addr.to_s.delete_prefix("0x").rjust(64, "0")
|
|
529
636
|
[hex].pack("H*")
|
|
530
637
|
end
|
|
638
|
+
|
|
639
|
+
# ─── Permit helpers ──────────────────────────────────────────────────
|
|
640
|
+
|
|
641
|
+
# Fetch the current EIP-2612 nonce for this wallet from the USDC contract.
|
|
642
|
+
# Uses JSON-RPC eth_call with selector 0x7ecebe00 (nonces(address)).
|
|
643
|
+
# @param usdc_address [String] the USDC contract address
|
|
644
|
+
# @return [Integer] current nonce
|
|
645
|
+
def fetch_usdc_nonce(usdc_address)
|
|
646
|
+
return 0 if @mock_mode
|
|
647
|
+
|
|
648
|
+
padded = address.downcase.delete_prefix("0x").rjust(64, "0")
|
|
649
|
+
data = "0x7ecebe00#{padded}"
|
|
650
|
+
payload = {
|
|
651
|
+
jsonrpc: "2.0",
|
|
652
|
+
id: 1,
|
|
653
|
+
method: "eth_call",
|
|
654
|
+
params: [{ to: usdc_address, data: data }, "latest"]
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
uri = URI.parse(@rpc_url)
|
|
658
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
659
|
+
http.use_ssl = (uri.scheme == "https")
|
|
660
|
+
http.read_timeout = 10
|
|
661
|
+
http.open_timeout = 5
|
|
662
|
+
|
|
663
|
+
req = Net::HTTP::Post.new(uri.request_uri)
|
|
664
|
+
req["Content-Type"] = "application/json"
|
|
665
|
+
req.body = payload.to_json
|
|
666
|
+
|
|
667
|
+
resp = http.request(req)
|
|
668
|
+
result = JSON.parse(resp.body)
|
|
669
|
+
|
|
670
|
+
if result["error"]
|
|
671
|
+
msg = result["error"].is_a?(Hash) ? result["error"]["message"] : result["error"].to_s
|
|
672
|
+
raise RemitError.new(RemitError::NETWORK_ERROR, "RPC error fetching nonce: #{msg}")
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
(result["result"] || "0x0").to_i(16)
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
# Auto-sign a permit for the given contract type and amount.
|
|
679
|
+
# @param contract [String] contract key — "router", "escrow", "tab", etc.
|
|
680
|
+
# @param amount [Numeric] amount in USDC
|
|
681
|
+
# @return [PermitSignature]
|
|
682
|
+
def auto_permit(contract, amount)
|
|
683
|
+
contracts = get_contracts
|
|
684
|
+
spender = contracts.send(contract.to_sym)
|
|
685
|
+
raise RemitError.new(
|
|
686
|
+
RemitError::SERVER_ERROR,
|
|
687
|
+
"No #{contract} contract address available"
|
|
688
|
+
) unless spender
|
|
689
|
+
|
|
690
|
+
sign_permit(spender, amount)
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
# Spender contract mapping for auto_permit.
|
|
694
|
+
PERMIT_SPENDER = {
|
|
695
|
+
pay: :router,
|
|
696
|
+
create_escrow: :escrow,
|
|
697
|
+
create_tab: :tab,
|
|
698
|
+
create_stream: :stream,
|
|
699
|
+
create_bounty: :bounty,
|
|
700
|
+
place_deposit: :deposit,
|
|
701
|
+
}.freeze
|
|
531
702
|
end
|
|
532
703
|
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.1.
|
|
4
|
+
version: 0.1.3
|
|
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-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rspec
|