@towns-labs/relayer-client 2.0.1

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.
Files changed (102) hide show
  1. package/README.md +752 -0
  2. package/dist/actions/checkHealth.d.ts +17 -0
  3. package/dist/actions/checkHealth.d.ts.map +1 -0
  4. package/dist/actions/checkHealth.js +45 -0
  5. package/dist/actions/checkHealth.js.map +1 -0
  6. package/dist/actions/createAccount.d.ts +151 -0
  7. package/dist/actions/createAccount.d.ts.map +1 -0
  8. package/dist/actions/createAccount.js +203 -0
  9. package/dist/actions/createAccount.js.map +1 -0
  10. package/dist/actions/executeIntent.d.ts +119 -0
  11. package/dist/actions/executeIntent.d.ts.map +1 -0
  12. package/dist/actions/executeIntent.js +118 -0
  13. package/dist/actions/executeIntent.js.map +1 -0
  14. package/dist/actions/getBundleStatus.d.ts +21 -0
  15. package/dist/actions/getBundleStatus.d.ts.map +1 -0
  16. package/dist/actions/getBundleStatus.js +73 -0
  17. package/dist/actions/getBundleStatus.js.map +1 -0
  18. package/dist/actions/getCapabilities.d.ts +24 -0
  19. package/dist/actions/getCapabilities.d.ts.map +1 -0
  20. package/dist/actions/getCapabilities.js +72 -0
  21. package/dist/actions/getCapabilities.js.map +1 -0
  22. package/dist/actions/index.d.ts +17 -0
  23. package/dist/actions/index.d.ts.map +1 -0
  24. package/dist/actions/index.js +17 -0
  25. package/dist/actions/index.js.map +1 -0
  26. package/dist/actions/prepareIntent.d.ts +59 -0
  27. package/dist/actions/prepareIntent.d.ts.map +1 -0
  28. package/dist/actions/prepareIntent.js +87 -0
  29. package/dist/actions/prepareIntent.js.map +1 -0
  30. package/dist/actions/signIntent.d.ts +244 -0
  31. package/dist/actions/signIntent.d.ts.map +1 -0
  32. package/dist/actions/signIntent.js +319 -0
  33. package/dist/actions/signIntent.js.map +1 -0
  34. package/dist/actions/signPayment.d.ts +71 -0
  35. package/dist/actions/signPayment.d.ts.map +1 -0
  36. package/dist/actions/signPayment.js +81 -0
  37. package/dist/actions/signPayment.js.map +1 -0
  38. package/dist/actions/submitIntent.d.ts +53 -0
  39. package/dist/actions/submitIntent.d.ts.map +1 -0
  40. package/dist/actions/submitIntent.js +122 -0
  41. package/dist/actions/submitIntent.js.map +1 -0
  42. package/dist/actions/waitForBundle.d.ts +36 -0
  43. package/dist/actions/waitForBundle.d.ts.map +1 -0
  44. package/dist/actions/waitForBundle.js +55 -0
  45. package/dist/actions/waitForBundle.js.map +1 -0
  46. package/dist/chains.d.ts +24 -0
  47. package/dist/chains.d.ts.map +1 -0
  48. package/dist/chains.js +68 -0
  49. package/dist/chains.js.map +1 -0
  50. package/dist/client.d.ts +27 -0
  51. package/dist/client.d.ts.map +1 -0
  52. package/dist/client.js +28 -0
  53. package/dist/client.js.map +1 -0
  54. package/dist/decorators/index.d.ts +2 -0
  55. package/dist/decorators/index.d.ts.map +1 -0
  56. package/dist/decorators/index.js +2 -0
  57. package/dist/decorators/index.js.map +1 -0
  58. package/dist/decorators/relayer.d.ts +80 -0
  59. package/dist/decorators/relayer.d.ts.map +1 -0
  60. package/dist/decorators/relayer.js +66 -0
  61. package/dist/decorators/relayer.js.map +1 -0
  62. package/dist/index.d.ts +52 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +58 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/transport.d.ts +81 -0
  67. package/dist/transport.d.ts.map +1 -0
  68. package/dist/transport.js +95 -0
  69. package/dist/transport.js.map +1 -0
  70. package/dist/types.d.ts +359 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +42 -0
  73. package/dist/types.js.map +1 -0
  74. package/dist/utils/constants.d.ts +47 -0
  75. package/dist/utils/constants.d.ts.map +1 -0
  76. package/dist/utils/constants.js +46 -0
  77. package/dist/utils/constants.js.map +1 -0
  78. package/dist/utils/erc1271.d.ts +43 -0
  79. package/dist/utils/erc1271.d.ts.map +1 -0
  80. package/dist/utils/erc1271.js +50 -0
  81. package/dist/utils/erc1271.js.map +1 -0
  82. package/dist/utils/errors.d.ts +54 -0
  83. package/dist/utils/errors.d.ts.map +1 -0
  84. package/dist/utils/errors.js +60 -0
  85. package/dist/utils/errors.js.map +1 -0
  86. package/dist/utils/index.d.ts +7 -0
  87. package/dist/utils/index.d.ts.map +1 -0
  88. package/dist/utils/index.js +7 -0
  89. package/dist/utils/index.js.map +1 -0
  90. package/dist/utils/keyHash.d.ts +50 -0
  91. package/dist/utils/keyHash.d.ts.map +1 -0
  92. package/dist/utils/keyHash.js +52 -0
  93. package/dist/utils/keyHash.js.map +1 -0
  94. package/dist/utils/serialize.d.ts +51 -0
  95. package/dist/utils/serialize.d.ts.map +1 -0
  96. package/dist/utils/serialize.js +52 -0
  97. package/dist/utils/serialize.js.map +1 -0
  98. package/dist/utils/wallet.d.ts +9 -0
  99. package/dist/utils/wallet.d.ts.map +1 -0
  100. package/dist/utils/wallet.js +25 -0
  101. package/dist/utils/wallet.js.map +1 -0
  102. package/package.json +35 -0
package/README.md ADDED
@@ -0,0 +1,752 @@
1
+ # @towns-labs/relayer-client
2
+
3
+ A slim, viem-style SDK for interacting with the EIP-7702 Relayer Orchestrator system. This SDK enables gasless transactions through account delegation and intent-based execution.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @towns-labs/relayer-client
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { createPublicClient, http, encodeFunctionData, erc20Abi } from "viem";
15
+ import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
16
+ import { base } from "viem/chains";
17
+ import { relayerActions } from "@towns-labs/relayer-client";
18
+
19
+ // 1. Create the relayer client (just needs relayerUrl!)
20
+ const client = createPublicClient({
21
+ chain: base,
22
+ transport: http("https://mainnet.base.org"),
23
+ }).extend(
24
+ relayerActions({
25
+ relayerUrl: "https://your-relayer.example.com",
26
+ }),
27
+ );
28
+
29
+ // 2. Generate a new account
30
+ const privateKey = generatePrivateKey();
31
+ const account = privateKeyToAccount(privateKey);
32
+
33
+ // 3. Create the delegated account via the relayer (two-step flow handled internally)
34
+ const createResult = await client.createAccount({
35
+ accountAddress: account.address,
36
+ signerKey: privateKey,
37
+ delegation: "0x...TownsAccountAddress", // Get from checkHealth() or getCapabilities()
38
+ });
39
+
40
+ // 4. Execute gasless transactions via intents
41
+ const signedIntent = await client.signIntent({
42
+ accountAddress: account.address,
43
+ signerKey: privateKey,
44
+ calls: [
45
+ {
46
+ target: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
47
+ value: 0n,
48
+ data: encodeFunctionData({
49
+ abi: erc20Abi,
50
+ functionName: "transfer",
51
+ args: ["0xrecipient...", 1000000n], // 1 USDC
52
+ }),
53
+ },
54
+ ],
55
+ });
56
+
57
+ const result = await client.submitIntent({ intent: signedIntent.intent });
58
+ console.log("Bundle ID:", result.bundleId);
59
+ ```
60
+
61
+ ## Gas Payment Models
62
+
63
+ The SDK supports three gas payment models:
64
+
65
+ ### 1. Fully Sponsored (Gasless)
66
+
67
+ The relayer pays for gas. User pays nothing.
68
+
69
+ ```typescript
70
+ const signedIntent = await client.signIntent({
71
+ accountAddress: account.address,
72
+ signerKey: privateKey,
73
+ calls: [...],
74
+ // No payer specified = fully sponsored
75
+ });
76
+
77
+ await client.submitIntent({ intent: signedIntent.intent });
78
+ ```
79
+
80
+ ### 2. User Pays Gas (Non-Sponsored)
81
+
82
+ The user reimburses the relayer for gas costs.
83
+
84
+ ```typescript
85
+ import { zeroAddress, parseEther } from "viem";
86
+
87
+ const signedIntent = await client.signIntent({
88
+ accountAddress: account.address,
89
+ signerKey: privateKey,
90
+ calls: [...],
91
+ payer: account.address, // User pays
92
+ paymentToken: zeroAddress, // Native ETH (or ERC20 address)
93
+ paymentMaxAmount: parseEther("0.01"), // Max willing to pay
94
+ });
95
+
96
+ await client.submitIntent({ intent: signedIntent.intent });
97
+ ```
98
+
99
+ ### 3. Third-Party Sponsor
100
+
101
+ A separate account (sponsor) pays for the user's gas. The sponsor must sign a payment authorization.
102
+
103
+ ```typescript
104
+ import { zeroAddress, parseEther } from "viem";
105
+
106
+ // User signs intent specifying sponsor as payer
107
+ const signedIntent = await client.signIntent({
108
+ accountAddress: userAccount.address,
109
+ signerKey: userPrivateKey,
110
+ calls: [...],
111
+ payer: sponsorAccount.address, // Sponsor pays
112
+ paymentToken: zeroAddress,
113
+ paymentMaxAmount: parseEther("0.01"),
114
+ });
115
+
116
+ // Sponsor signs payment authorization
117
+ const paymentSignature = await client.signPayment({
118
+ signedIntent,
119
+ sponsorKey: sponsorPrivateKey,
120
+ });
121
+
122
+ // Submit with both signatures
123
+ await client.submitIntent({
124
+ intent: { ...signedIntent.intent, paymentSignature },
125
+ });
126
+ ```
127
+
128
+ ## Delegated Signers (Bot/Agent Authorization)
129
+
130
+ A powerful pattern is authorizing a bot or agent to act on behalf of a user's account with limited permissions. This enables automated actions while maintaining security through spend limits and call restrictions.
131
+
132
+ ### Setup: User Authorizes Bot
133
+
134
+ ```typescript
135
+ import {
136
+ encodeSecp256k1Key,
137
+ computeKeyHash,
138
+ ANY_TARGET,
139
+ EMPTY_CALLDATA_SELECTOR,
140
+ } from "@towns-labs/relayer-client";
141
+ import { zeroAddress, parseEther } from "viem";
142
+
143
+ // Bot is a separate delegated account
144
+ await client.createAccount({
145
+ accountAddress: botAccount.address,
146
+ signerKey: botPrivateKey,
147
+ delegation: townsAccountAddress,
148
+ });
149
+
150
+ // User creates account and authorizes bot with limited permissions
151
+ const botPublicKey = encodeSecp256k1Key(botAccount.address);
152
+
153
+ await client.createAccount({
154
+ accountAddress: userAccount.address,
155
+ signerKey: userPrivateKey,
156
+ delegation: townsAccountAddress,
157
+ authorizeKeys: [
158
+ {
159
+ expiry: "0",
160
+ type: "secp256k1",
161
+ role: "normal", // Limited permissions, not admin
162
+ publicKey: botPublicKey,
163
+ permissions: [
164
+ {
165
+ type: "spend",
166
+ token: zeroAddress, // ETH
167
+ limit: parseEther("0.1").toString(), // Max 0.1 ETH per day
168
+ period: "day",
169
+ },
170
+ {
171
+ type: "call",
172
+ to: ANY_TARGET,
173
+ selector: EMPTY_CALLDATA_SELECTOR, // ETH transfers only
174
+ },
175
+ ],
176
+ },
177
+ ],
178
+ });
179
+ ```
180
+
181
+ ### Bot Signs on Behalf of User
182
+
183
+ Since the bot is a delegated account (has bytecode), use `signIntentAsDelegate`:
184
+
185
+ ```typescript
186
+ const botKeyHash = computeKeyHash("secp256k1", botPublicKey);
187
+
188
+ // Bot signs intent to transfer from user's account
189
+ const signedIntent = await client.signIntentAsDelegate({
190
+ accountAddress: userAccount.address,
191
+ signerAddress: botAccount.address,
192
+ signerKey: botPrivateKey,
193
+ keyHash: botKeyHash,
194
+ calls: [
195
+ {
196
+ target: recipientAddress,
197
+ value: parseEther("0.05"), // Within daily limit
198
+ data: "0x",
199
+ },
200
+ ],
201
+ });
202
+
203
+ await client.submitIntent({ intent: signedIntent.intent });
204
+ ```
205
+
206
+ ## API Reference
207
+
208
+ ### Client Creation
209
+
210
+ #### `relayerActions(config)`
211
+
212
+ Extends a viem PublicClient with relayer actions:
213
+
214
+ ```typescript
215
+ import { createPublicClient, http } from "viem";
216
+ import { base } from "viem/chains";
217
+ import { relayerActions } from "@towns-labs/relayer-client";
218
+
219
+ const client = createPublicClient({
220
+ chain: base,
221
+ transport: http("https://mainnet.base.org"),
222
+ }).extend(
223
+ relayerActions({
224
+ relayerUrl: "https://your-relayer.example.com",
225
+ }),
226
+ );
227
+ ```
228
+
229
+ ### Client Actions
230
+
231
+ #### `checkHealth()`
232
+
233
+ Checks the relayer's health status and returns contract addresses.
234
+
235
+ ```typescript
236
+ const health = await client.checkHealth();
237
+ // {
238
+ // status: 'ok',
239
+ // chainId: 8453,
240
+ // relayerAddress: '0x...',
241
+ // contracts: { orchestrator, simulator, townsAccount, accountProxy, simpleFunder }
242
+ // }
243
+ ```
244
+
245
+ #### `getCapabilities()`
246
+
247
+ Gets the relayer's capabilities.
248
+
249
+ ```typescript
250
+ const caps = await client.getCapabilities();
251
+ // { capabilities: { accountCreation, intentExecution, simulation, ... } }
252
+ ```
253
+
254
+ #### `createAccount(params)`
255
+
256
+ Creates a new EIP-7702 delegated account. Uses a two-step JSON-RPC flow internally.
257
+
258
+ ```typescript
259
+ const result = await client.createAccount({
260
+ accountAddress: Address, // EOA to delegate
261
+ signerKey: Hex, // Private key for signing the authorization
262
+ delegation: Address, // TownsAccount implementation address
263
+ authorizeKeys?: AuthorizeKey[], // Optional keys to authorize during upgrade
264
+ });
265
+ // { success, accountAddress, bundleIds }
266
+ ```
267
+
268
+ **Authorizing Keys as SuperAdmin:**
269
+
270
+ When creating an account, you can authorize additional keys with admin or normal roles:
271
+
272
+ ```typescript
273
+ import { encodeAbiParameters } from "viem";
274
+
275
+ const result = await client.createAccount({
276
+ accountAddress: account.address,
277
+ signerKey: privateKey,
278
+ delegation: townsAccountAddress,
279
+ authorizeKeys: [
280
+ {
281
+ expiry: "0", // 0 = never expires
282
+ type: "secp256k1", // or 'external' for ISigner contracts
283
+ role: "admin", // 'admin' = superadmin, 'normal' = limited permissions
284
+ publicKey: encodeAbiParameters([{ type: "address" }], [signerAddress]),
285
+ permissions: [], // Empty for admin, or specify CallPermission/SpendPermission
286
+ },
287
+ ],
288
+ });
289
+ ```
290
+
291
+ **Key Types:**
292
+
293
+ | Type | Description |
294
+ | ----------- | ------------------------------------------------------------------- |
295
+ | `secp256k1` | Standard Ethereum EOA keys (same curve as EOAs) |
296
+ | `external` | Delegated to an external `ISigner` contract for custom verification |
297
+
298
+ **Roles:**
299
+
300
+ | Role | Description |
301
+ | -------- | ----------------------------------------------------------------------- |
302
+ | `admin` | SuperAdmin - can call `authorize()` and `revoke()` to manage other keys |
303
+ | `normal` | Limited to specified permissions only |
304
+
305
+ #### `signIntent(params)`
306
+
307
+ Signs an intent for execution. Returns a `SignedIntent` containing the intent, digest, and typed data (for third-party sponsorship).
308
+
309
+ ```typescript
310
+ const signedIntent = await client.signIntent({
311
+ accountAddress: Address, // The delegated account address
312
+ signerKey: Hex, // Private key for signing
313
+ calls: Call[], // Array of calls to execute
314
+ // Optional:
315
+ nonce?: bigint, // Override nonce
316
+ seqKey?: bigint, // Sequence key for parallel execution (2D nonce)
317
+ combinedGas?: bigint, // Override gas limit
318
+ expiry?: bigint, // Override expiry timestamp
319
+ // Payment options (see Gas Payment Models):
320
+ payer?: Address, // Who pays for gas
321
+ paymentToken?: Address, // zeroAddress for ETH, or ERC20 address
322
+ paymentMaxAmount?: bigint, // Maximum willing to pay
323
+ });
324
+ // Returns: { intent, digest, typedData }
325
+ ```
326
+
327
+ #### `signIntentWithWallet(params)`
328
+
329
+ Signs an intent using a viem WalletClient instead of a raw private key. Useful for browser wallets and hardware wallets.
330
+
331
+ ```typescript
332
+ import { createWalletClient, custom } from "viem";
333
+
334
+ const walletClient = createWalletClient({
335
+ chain: base,
336
+ transport: custom(window.ethereum),
337
+ });
338
+
339
+ const signedIntent = await client.signIntentWithWallet({
340
+ accountAddress: Address,
341
+ walletClient: WalletClient,
342
+ calls: Call[],
343
+ });
344
+ // Returns: { intent, digest, typedData }
345
+ ```
346
+
347
+ #### `signIntentAsDelegate(params)`
348
+
349
+ Signs an intent when the signer is itself a delegated account (smart contract). This handles the ERC-1271 replay-safe digest transformation required for smart contract signatures.
350
+
351
+ Use this when:
352
+
353
+ - A bot account (delegated) signs on behalf of a user account
354
+ - An agent with limited permissions acts on behalf of another delegated account
355
+
356
+ ```typescript
357
+ import { encodeSecp256k1Key, computeKeyHash } from "@towns-labs/relayer-client";
358
+
359
+ // Bot is a delegated account that was authorized on user's account
360
+ const botPublicKey = encodeSecp256k1Key(botAccount.address);
361
+ const botKeyHash = computeKeyHash("secp256k1", botPublicKey);
362
+
363
+ const signedIntent = await client.signIntentAsDelegate({
364
+ accountAddress: userAccount.address, // Account to execute on
365
+ signerAddress: botAccount.address, // Delegated signer's address
366
+ signerKey: botPrivateKey, // Delegated signer's private key
367
+ keyHash: botKeyHash, // Key hash in the target account
368
+ calls: [
369
+ {
370
+ target: recipientAddress,
371
+ value: parseEther("0.01"),
372
+ data: "0x",
373
+ },
374
+ ],
375
+ });
376
+
377
+ await client.submitIntent({ intent: signedIntent.intent });
378
+ ```
379
+
380
+ **Why is this needed?**
381
+
382
+ When a signer has bytecode (is a smart contract/delegated account), signature verification follows the ERC-1271 standard. The digest must be transformed to include the signer's address for replay protection. This method handles that transformation automatically.
383
+
384
+ #### `signPayment(params)`
385
+
386
+ Signs a payment authorization for third-party gas sponsorship. The sponsor calls this to authorize paying for someone else's transaction.
387
+
388
+ ```typescript
389
+ const paymentSignature = await client.signPayment({
390
+ signedIntent: SignedIntent, // From signIntent()
391
+ sponsorKey: Hex, // Sponsor's private key
392
+ });
393
+ // Returns: Hex (the payment signature)
394
+ ```
395
+
396
+ #### `signPaymentWithWallet(params)`
397
+
398
+ Signs a payment authorization using a WalletClient instead of a raw private key.
399
+
400
+ ```typescript
401
+ const paymentSignature = await client.signPaymentWithWallet({
402
+ signedIntent: SignedIntent,
403
+ walletClient: WalletClient, // Sponsor's wallet client
404
+ });
405
+ // Returns: Hex (the payment signature)
406
+ ```
407
+
408
+ #### `submitIntent(params)`
409
+
410
+ Submits a signed intent for execution.
411
+
412
+ ```typescript
413
+ const result = await client.submitIntent({ intent: signedIntent.intent });
414
+ // { success, bundleId }
415
+ ```
416
+
417
+ #### `submitBatch(params)`
418
+
419
+ Submits multiple intents as a single batch for gas-efficient execution. The relayer optimizes homogeneous batches into a single on-chain transaction, while each intent gets its own bundleId for status tracking. All intents execute atomically (all succeed or all fail).
420
+
421
+ ```typescript
422
+ // Sign multiple intents (can be different accounts)
423
+ const signedIntents = await Promise.all([
424
+ client.signIntent({ accountAddress: addr1, signerKey: key1, calls: [...] }),
425
+ client.signIntent({ accountAddress: addr2, signerKey: key2, calls: [...] }),
426
+ ]);
427
+
428
+ // Submit as batch - executes in single on-chain transaction
429
+ const result = await client.submitBatch({
430
+ intents: signedIntents.map((s) => s.intent),
431
+ });
432
+ // { success, bundleIds, succeeded, failed }
433
+
434
+ // Poll status for each bundle
435
+ for (const bundleId of result.bundleIds) {
436
+ const status = await client.getBundleStatus({ bundleId });
437
+ }
438
+ ```
439
+
440
+ For parallel intents from the same account, use different `seqKey` values:
441
+
442
+ ```typescript
443
+ const signedIntents = await Promise.all([
444
+ client.signIntent({ accountAddress, signerKey, seqKey: 0n, calls: [...] }),
445
+ client.signIntent({ accountAddress, signerKey, seqKey: 1n, calls: [...] }),
446
+ ]);
447
+ await client.submitBatch({ intents: signedIntents.map((s) => s.intent) });
448
+ ```
449
+
450
+ #### `prepareIntent(params)`
451
+
452
+ Prepares an intent for signing (advanced use). Returns EIP-712 typed data for manual signing.
453
+ Also provides gas estimation via `combinedGas` - use this instead of `simulateIntent`.
454
+
455
+ ```typescript
456
+ const prepared = await client.prepareIntent({
457
+ accountAddress: Address,
458
+ calls: Call[],
459
+ });
460
+ // { success, typedData, nonce, combinedGas, expiry, digest }
461
+ ```
462
+
463
+ #### `getBundleStatus(params)`
464
+
465
+ Gets the status of a submitted bundle.
466
+
467
+ ```typescript
468
+ const status = await client.getBundleStatus({ bundleId });
469
+ // { status: 'pending' | 'confirmed' | 'failed' | 'reverted', statusCode, receipt }
470
+ ```
471
+
472
+ **Status Codes:**
473
+
474
+ | Code | Status | Meaning |
475
+ | ---- | ----------- | ---------------------------------------------------- |
476
+ | 100 | `pending` | Still waiting for confirmation |
477
+ | 200 | `confirmed` | Intent executed successfully |
478
+ | 300 | `failed` | Transaction never submitted/mined (offchain failure) |
479
+ | 400 | `reverted` | Transaction mined but intent failed on-chain |
480
+ | 500 | `reverted` | Some intents in bundle failed (partial revert) |
481
+
482
+ #### `waitForBundle(params)`
483
+
484
+ Waits for a bundle to reach a final status (confirmed, failed, or reverted).
485
+
486
+ ```typescript
487
+ const status = await client.waitForBundle({
488
+ bundleId: result.bundleId!,
489
+ timeoutMs: 60_000, // Optional, default 30s
490
+ });
491
+
492
+ if (status.status === "confirmed") {
493
+ console.log("Success!", status.receipt?.transactionHash);
494
+ } else if (status.status === "reverted") {
495
+ console.log("Intent failed on-chain:", status.receipt?.intentError);
496
+ }
497
+ ```
498
+
499
+ #### `executeIntent(params)`
500
+
501
+ Convenience method that combines `signIntent`, `submitIntent`, and `waitForBundle` into a single call.
502
+
503
+ ```typescript
504
+ // Simple execution with waiting (default)
505
+ const result = await client.executeIntent({
506
+ accountAddress: account.address,
507
+ signerKey: privateKey,
508
+ calls: [
509
+ {
510
+ target: recipientAddress,
511
+ value: parseEther("0.1"),
512
+ data: "0x",
513
+ },
514
+ ],
515
+ });
516
+
517
+ if (result.success) {
518
+ console.log("Transaction hash:", result.txHash);
519
+ } else {
520
+ console.log("Failed:", result.error);
521
+ }
522
+
523
+ // Fire and forget (don't wait for confirmation)
524
+ const result = await client.executeIntent({
525
+ accountAddress: account.address,
526
+ signerKey: privateKey,
527
+ calls: [...],
528
+ waitForConfirmation: false,
529
+ });
530
+ console.log("Bundle submitted:", result.bundleId);
531
+ ```
532
+
533
+ ### Utilities
534
+
535
+ #### `encodeSecp256k1Key(address)`
536
+
537
+ Encodes an address as a secp256k1 public key for TownsAccount. This is a convenience wrapper for the common pattern of ABI-encoding an address.
538
+
539
+ ```typescript
540
+ import { encodeSecp256k1Key, computeKeyHash } from "@towns-labs/relayer-client";
541
+
542
+ const publicKey = encodeSecp256k1Key(signerAddress);
543
+ const keyHash = computeKeyHash("secp256k1", publicKey);
544
+ ```
545
+
546
+ #### `computeKeyHash(keyType, publicKey)`
547
+
548
+ Computes the key hash for an authorized key. This matches TownsAccount's key hash computation.
549
+
550
+ ```typescript
551
+ import { encodeSecp256k1Key, computeKeyHash } from "@towns-labs/relayer-client";
552
+
553
+ // For secp256k1 keys
554
+ const publicKey = encodeSecp256k1Key(signerAddress);
555
+ const keyHash = computeKeyHash("secp256k1", publicKey);
556
+
557
+ // For external signer contracts
558
+ const externalPublicKey = concat([signerContract, `0x${"00".repeat(12)}`]);
559
+ const externalKeyHash = computeKeyHash("external", externalPublicKey);
560
+ ```
561
+
562
+ #### `wrapSignature(signature, keyHash, prehash?)`
563
+
564
+ Wraps a signature with keyHash and prehash flag for TownsAccount validation. This is the format expected by `TownsAccount.unwrapAndValidateSignature`.
565
+
566
+ ```typescript
567
+ import { wrapSignature } from "@towns-labs/relayer-client";
568
+
569
+ // Wrap a raw signature with key hash
570
+ const wrappedSignature = wrapSignature(rawSignature, keyHash);
571
+
572
+ // With prehash flag (for certain signing scenarios)
573
+ const wrappedSignature = wrapSignature(rawSignature, keyHash, true);
574
+ ```
575
+
576
+ #### `computeErc1271Digest(digest, accountAddress)`
577
+
578
+ Computes the ERC-1271 replay-safe digest for smart contract signatures. Use this when manually signing as a delegated account.
579
+
580
+ ```typescript
581
+ import { computeErc1271Digest } from "@towns-labs/relayer-client";
582
+
583
+ // Transform digest for ERC-1271 signing
584
+ const erc1271Digest = computeErc1271Digest(originalDigest, signerAddress);
585
+
586
+ // Sign the transformed digest
587
+ const signature = await sign({ hash: erc1271Digest, privateKey });
588
+ ```
589
+
590
+ #### `decodeIntentError(selector)`
591
+
592
+ Decodes an intent error selector (bytes4) to a human-readable name.
593
+
594
+ ```typescript
595
+ import { decodeIntentError } from "@towns-labs/relayer-client";
596
+
597
+ const status = await client.getBundleStatus({ bundleId });
598
+ if (status.receipt?.intentError) {
599
+ const errorName = decodeIntentError(status.receipt.intentError);
600
+ console.log("Intent failed:", errorName); // e.g., 'ExceededSpendLimit'
601
+ }
602
+ ```
603
+
604
+ #### Permission Constants
605
+
606
+ Constants for configuring session key permissions:
607
+
608
+ ```typescript
609
+ import {
610
+ ANY_TARGET,
611
+ EMPTY_CALLDATA_SELECTOR,
612
+ ERC20_SELECTORS,
613
+ } from "@towns-labs/relayer-client";
614
+
615
+ // Allow ETH transfers to any address
616
+ const ethTransferPermission: CallPermission = {
617
+ type: "call",
618
+ to: ANY_TARGET,
619
+ selector: EMPTY_CALLDATA_SELECTOR,
620
+ };
621
+
622
+ // Allow ERC20 transfers to any address
623
+ const erc20TransferPermission: CallPermission = {
624
+ type: "call",
625
+ to: ANY_TARGET,
626
+ selector: ERC20_SELECTORS.TRANSFER,
627
+ };
628
+ ```
629
+
630
+ | Constant | Value | Description |
631
+ | ------------------------------- | --------------- | ------------------------------------------------- |
632
+ | `ANY_TARGET` | `0x3232...3232` | Matches any contract address |
633
+ | `EMPTY_CALLDATA_SELECTOR` | `0xe0e0e0e0` | Matches calls with empty calldata (ETH transfers) |
634
+ | `ERC20_SELECTORS.TRANSFER` | `0xa9059cbb` | ERC20 `transfer(address,uint256)` |
635
+ | `ERC20_SELECTORS.APPROVE` | `0x095ea7b3` | ERC20 `approve(address,uint256)` |
636
+ | `ERC20_SELECTORS.TRANSFER_FROM` | `0x23b872dd` | ERC20 `transferFrom(address,address,uint256)` |
637
+
638
+ ### Types
639
+
640
+ ```typescript
641
+ interface Call {
642
+ target: Address; // Contract to call
643
+ value: bigint; // ETH value to send
644
+ data: Hex; // Calldata
645
+ }
646
+
647
+ interface Intent {
648
+ eoa: Address;
649
+ calls: Call[];
650
+ nonce: bigint;
651
+ combinedGas: bigint;
652
+ expiry: bigint;
653
+ signature: Hex;
654
+ // Payment fields (optional)
655
+ payer?: Address;
656
+ paymentToken?: Address;
657
+ paymentMaxAmount?: bigint;
658
+ paymentAmount?: bigint;
659
+ paymentSignature?: Hex;
660
+ }
661
+
662
+ interface SignedIntent {
663
+ intent: Intent;
664
+ digest: Hex; // EIP-712 digest (for third-party sponsorship)
665
+ typedData: {
666
+ domain: EIP712Domain;
667
+ types: typeof INTENT_TYPES;
668
+ primaryType: "Intent";
669
+ message: Record<string, unknown>;
670
+ };
671
+ }
672
+
673
+ interface SubmitBatchParams {
674
+ intents: Intent[];
675
+ }
676
+
677
+ interface BatchIntentsResponse {
678
+ success: boolean;
679
+ bundleIds?: string[]; // One per intent for status tracking
680
+ succeeded?: number;
681
+ failed?: number;
682
+ error?: string;
683
+ }
684
+
685
+ // Key authorization types
686
+ type KeyType = "secp256k1" | "external";
687
+
688
+ interface AuthorizeKey {
689
+ expiry: string; // Unix timestamp, "0" = never expires
690
+ type: KeyType;
691
+ role: "admin" | "normal"; // admin = superadmin
692
+ publicKey: Hex; // For secp256k1: encodeAbiParameters([{type:'address'}], [addr])
693
+ permissions: Permission[];
694
+ }
695
+
696
+ interface CallPermission {
697
+ type: "call";
698
+ to: Address;
699
+ selector: Hex; // 4-byte function selector
700
+ }
701
+
702
+ interface SpendPermission {
703
+ type: "spend";
704
+ token: Address;
705
+ limit: string;
706
+ period: "minute" | "hour" | "day" | "week" | "month" | "year";
707
+ }
708
+
709
+ type Permission = CallPermission | SpendPermission;
710
+ ```
711
+
712
+ ## Running Tests
713
+
714
+ The integration tests serve as comprehensive examples of SDK usage.
715
+
716
+ ### Prerequisites
717
+
718
+ - [Foundry](https://getfoundry.sh) (for Anvil)
719
+ - [Bun](https://bun.sh)
720
+ - A Base mainnet RPC URL (e.g., from Alchemy)
721
+
722
+ ### Run Tests
723
+
724
+ From the `packages/relayer-client` directory:
725
+
726
+ ```bash
727
+ # Start services and run tests (recommended)
728
+ FORK_RPC_URL=https://your-rpc-url ./scripts/local-dev.sh --test
729
+
730
+ # Or start services in dev mode, then run tests separately
731
+ FORK_RPC_URL=https://your-rpc-url ./scripts/local-dev.sh
732
+ # In another terminal:
733
+ bun run test:integration
734
+ ```
735
+
736
+ ### Test Scenarios
737
+
738
+ The tests in `test/scenarios/` demonstrate:
739
+
740
+ 1. **Account Delegation** - Creating delegated EIP-7702 accounts
741
+ 2. **ERC20 Gasless Transfers** - Transferring tokens without paying gas
742
+ 3. **ETH Gasless Transfers** - Sending ETH without paying gas, user-paid gas, and third-party sponsorship
743
+ 4. **Nonce Management** - Sequential and parallel intent execution
744
+ 5. **Batch Execution** - Submitting multiple intents in a single transaction
745
+
746
+ ## Supported Chains
747
+
748
+ | Chain | Chain ID | Status |
749
+ | ------------ | -------- | --------- |
750
+ | Base Mainnet | 8453 | Supported |
751
+ | Base Sepolia | 84532 | Supported |
752
+ | Local Anvil | 31337 | Supported |