@turnkey/gas-station 0.2.0

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 (45) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +726 -0
  3. package/dist/abi/gas-station.d.ts +546 -0
  4. package/dist/abi/gas-station.d.ts.map +1 -0
  5. package/dist/abi/gas-station.js +275 -0
  6. package/dist/abi/gas-station.js.map +1 -0
  7. package/dist/abi/gas-station.mjs +273 -0
  8. package/dist/abi/gas-station.mjs.map +1 -0
  9. package/dist/config.d.ts +44 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/config.js +57 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/config.mjs +51 -0
  14. package/dist/config.mjs.map +1 -0
  15. package/dist/gasStationClient.d.ts +71 -0
  16. package/dist/gasStationClient.d.ts.map +1 -0
  17. package/dist/gasStationClient.js +237 -0
  18. package/dist/gasStationClient.js.map +1 -0
  19. package/dist/gasStationClient.mjs +235 -0
  20. package/dist/gasStationClient.mjs.map +1 -0
  21. package/dist/gasStationUtils.d.ts +7069 -0
  22. package/dist/gasStationUtils.d.ts.map +1 -0
  23. package/dist/gasStationUtils.js +129 -0
  24. package/dist/gasStationUtils.js.map +1 -0
  25. package/dist/gasStationUtils.mjs +120 -0
  26. package/dist/gasStationUtils.mjs.map +1 -0
  27. package/dist/index.d.ts +8 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +29 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/index.mjs +6 -0
  32. package/dist/index.mjs.map +1 -0
  33. package/dist/intentBuilder.d.ts +61 -0
  34. package/dist/intentBuilder.d.ts.map +1 -0
  35. package/dist/intentBuilder.js +153 -0
  36. package/dist/intentBuilder.js.map +1 -0
  37. package/dist/intentBuilder.mjs +151 -0
  38. package/dist/intentBuilder.mjs.map +1 -0
  39. package/dist/policyUtils.d.ts +271 -0
  40. package/dist/policyUtils.d.ts.map +1 -0
  41. package/dist/policyUtils.js +387 -0
  42. package/dist/policyUtils.js.map +1 -0
  43. package/dist/policyUtils.mjs +381 -0
  44. package/dist/policyUtils.mjs.map +1 -0
  45. package/package.json +47 -0
package/README.md ADDED
@@ -0,0 +1,726 @@
1
+ # Turnkey Gas Station SDK
2
+
3
+ > **⚠️ BETA WARNING**: This SDK is currently in beta. The underlying smart contracts are **unaudited** and should not be used in production environments. Use at your own risk.
4
+
5
+ A reusable SDK for implementing gasless transactions using EIP-7702, Turnkey wallet management, and your own paymaster. This package provides clean abstractions and utility methods to quickly integrate with Turnkey's contracts for sponsored transaction execution.
6
+
7
+ ## What is This?
8
+
9
+ This SDK enables you to:
10
+
11
+ - **Bring your own paymaster** to sponsor user transactions
12
+ - **Use Turnkey** for secure wallet management and transaction signing
13
+ - **Execute gasless transactions** via EIP-7702 delegation and EIP-712 signed intents
14
+ - **Support any on-chain action** through generic execution parameters
15
+
16
+ Perfect for building dApps where users don't need ETH for gas, enabling seamless onboarding and better UX.
17
+
18
+ ## How It Works
19
+
20
+ 1. **EIP-7702 Authorization**: One-time setup where an EOA authorizes a gas station contract
21
+ 2. **EIP-712 Signed Intents**: User signs off-chain intents for what they want to execute
22
+ 3. **Paymaster Execution**: Your paymaster submits the transaction and pays for gas
23
+ 4. **Turnkey Integration**: All signatures handled securely through Turnkey
24
+
25
+ ## Quick Start
26
+
27
+ ### 1. Install Dependencies
28
+
29
+ ```bash
30
+ pnpm install @turnkey/sdk-server @turnkey/viem viem
31
+ ```
32
+
33
+ ### 2. Set Up Environment
34
+
35
+ Create `.env.local`:
36
+
37
+ ```bash
38
+ # Turnkey Configuration
39
+ BASE_URL=https://api.turnkey.com
40
+ API_PRIVATE_KEY=your_turnkey_api_private_key
41
+ API_PUBLIC_KEY=your_turnkey_api_public_key
42
+ ORGANIZATION_ID=your_turnkey_organization_id
43
+
44
+ # Wallet Addresses
45
+ EOA_ADDRESS=0x... # User's wallet address
46
+ PAYMASTER_ADDRESS=0x... # Your paymaster address
47
+
48
+ # RPC Configuration
49
+ BASE_RPC_URL=https://mainnet.base.org
50
+ ETH_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/...
51
+
52
+ # Gas Station Contracts (Optional - defaults to deterministic addresses)
53
+ DELEGATE_CONTRACT=0x... # EIP-7702 delegate contract
54
+ EXECUTION_CONTRACT=0x... # Gas Sponsorship entrypoint contract which calls the delegate.
55
+ ```
56
+
57
+ **Note**: The gas station contracts are currently deployed at deterministic addresses on the following chains:
58
+
59
+ - **Ethereum Mainnet**
60
+ - **Base Mainnet**
61
+
62
+ These addresses are built into the SDK, so you don't need to specify them unless you are using custom deployments.
63
+
64
+ ### 3. Initialize and Use
65
+
66
+ ```typescript
67
+ import { GasStationClient } from "@turnkey/gas-station";
68
+ import { Turnkey } from "@turnkey/sdk-server";
69
+ import { createAccount } from "@turnkey/viem";
70
+ import { parseEther, parseUnits, createWalletClient, http } from "viem";
71
+ import { base } from "viem/chains";
72
+
73
+ // Initialize Turnkey
74
+ const turnkeyClient = new Turnkey({
75
+ apiBaseUrl: process.env.BASE_URL!,
76
+ apiPrivateKey: process.env.API_PRIVATE_KEY!,
77
+ apiPublicKey: process.env.API_PUBLIC_KEY!,
78
+ defaultOrganizationId: process.env.ORGANIZATION_ID!,
79
+ });
80
+
81
+ // Create Turnkey accounts
82
+ const userAccount = await createAccount({
83
+ client: turnkeyClient.apiClient(),
84
+ organizationId: process.env.ORGANIZATION_ID!,
85
+ signWith: process.env.EOA_ADDRESS as `0x${string}`,
86
+ });
87
+
88
+ const paymasterAccount = await createAccount({
89
+ client: turnkeyClient.apiClient(),
90
+ organizationId: process.env.ORGANIZATION_ID!,
91
+ signWith: process.env.PAYMASTER as `0x${string}`,
92
+ });
93
+
94
+ // Create viem wallet clients
95
+ const userWalletClient = createWalletClient({
96
+ account: userAccount,
97
+ chain: base,
98
+ transport: http(process.env.BASE_RPC_URL!),
99
+ });
100
+
101
+ const paymasterWalletClient = createWalletClient({
102
+ account: paymasterAccount,
103
+ chain: base,
104
+ transport: http(process.env.BASE_RPC_URL!),
105
+ });
106
+
107
+ // Create Gas Station clients
108
+ const userClient = new GasStationClient({
109
+ walletClient: userWalletClient,
110
+ });
111
+
112
+ const paymasterClient = new GasStationClient({
113
+ walletClient: paymasterWalletClient,
114
+ });
115
+
116
+ // One-time: Authorize the EOA to use gas station
117
+ const authorization = await userClient.signAuthorization();
118
+ await paymasterClient.submitAuthorizations([authorization]);
119
+
120
+ // Execute a gasless ETH transfer
121
+ let nonce = await userClient.getNonce();
122
+ const ethIntent = await userClient
123
+ .createIntent()
124
+ .transferETH("0xRecipient...", parseEther("0.1"))
125
+ .sign(nonce);
126
+ await paymasterClient.execute(ethIntent);
127
+
128
+ // Execute a gasless token transfer
129
+ nonce = await userClient.getNonce();
130
+ const usdcIntent = await userClient
131
+ .createIntent()
132
+ .transferToken(
133
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
134
+ "0xRecipient...",
135
+ parseUnits("10", 6),
136
+ )
137
+ .sign(nonce);
138
+ await paymasterClient.execute(usdcIntent);
139
+ ```
140
+
141
+ ## Core API
142
+
143
+ ### GasStationClient
144
+
145
+ Main client for gas station operations. Each client instance wraps a viem wallet client.
146
+
147
+ #### Constructor
148
+
149
+ ```typescript
150
+ new GasStationClient({
151
+ walletClient: WalletClient, // Viem wallet client (e.g., with Turnkey account)
152
+ delegateContract?: `0x${string}`, // Optional: defaults to deterministic address
153
+ executionContract?: `0x${string}`, // Optional: defaults to deterministic address
154
+ })
155
+ ```
156
+
157
+ #### Methods
158
+
159
+ **End User Methods** (call with user client):
160
+
161
+ **`signAuthorization(): Promise<SignedAuthorization>`**
162
+
163
+ - Sign an EIP-7702 authorization for the gas station contract
164
+ - Returns authorization that can be submitted by paymaster
165
+
166
+ **`createIntent(): Promise<IntentBuilder>`**
167
+
168
+ - Create a builder for composing transactions
169
+ - Intent must be signed before execution
170
+
171
+ **`getNonce(address?: Address): Promise<bigint>`**
172
+
173
+ - Get current nonce from gas station contract
174
+ - Defaults to the signer's address if not specified
175
+
176
+ **Paymaster Methods** (call with paymaster client):
177
+
178
+ **`submitAuthorizations(authorizations: SignedAuthorization[]): Promise<{ txHash, blockNumber }>`**
179
+
180
+ - Submit signed EIP-7702 authorization transaction(s)
181
+ - Supports authorizing multiple EOAs in a single transaction
182
+ - Paymaster pays for gas
183
+
184
+ **`execute(intent: ExecutionIntent): Promise<{ txHash, blockNumber, gasUsed }>`**
185
+
186
+ - Execute a signed intent through the gas station
187
+ - Paymaster pays for gas
188
+
189
+ ### IntentBuilder
190
+
191
+ Composable builder for complex multi-step transactions.
192
+
193
+ ```typescript
194
+ const nonce = await userClient.getNonce();
195
+ const builder = userClient.createIntent();
196
+
197
+ const intent = await builder
198
+ .transferToken(usdcAddress, recipient, amount)
199
+ .sign(nonce);
200
+
201
+ await paymasterClient.execute(intent);
202
+ ```
203
+
204
+ ## Common Use Cases
205
+
206
+ ### Simple Payment
207
+
208
+ ```typescript
209
+ // Gasless USDC payment
210
+ const nonce = await userClient.getNonce();
211
+ const intent = await userClient
212
+ .createIntent()
213
+ .transferToken(usdcAddress, recipientAddress, parseUnits("50", 6))
214
+ .sign(nonce);
215
+
216
+ const result = await paymasterClient.execute(intent);
217
+ console.log(`Payment sent: ${result.txHash}`);
218
+ ```
219
+
220
+ ### Token Approval + DEX Swap
221
+
222
+ ```typescript
223
+ // Step 1: Approve DEX to spend tokens
224
+ let nonce = await userClient.getNonce();
225
+ const approvalIntent = await userClient
226
+ .createIntent()
227
+ .approveToken(usdcAddress, dexAddress, parseUnits("100", 6))
228
+ .sign(nonce);
229
+ await paymasterClient.execute(approvalIntent);
230
+
231
+ // Step 2: Execute swap
232
+ nonce = await userClient.getNonce();
233
+ const swapIntent = await userClient
234
+ .createIntent()
235
+ .callContract({
236
+ contract: dexAddress,
237
+ abi: DEX_ABI,
238
+ functionName: "swapExactTokensForTokens",
239
+ args: [amountIn, amountOutMin, path, recipient, deadline],
240
+ })
241
+ .sign(nonce);
242
+ await paymasterClient.execute(swapIntent);
243
+ ```
244
+
245
+ ### NFT Minting
246
+
247
+ ```typescript
248
+ const nonce = await userClient.getNonce();
249
+ const mintIntent = await userClient
250
+ .createIntent()
251
+ .callContract({
252
+ contract: nftContract,
253
+ abi: NFT_ABI,
254
+ functionName: "mint",
255
+ args: [tokenId],
256
+ value: parseEther("0.1"), // Optional ETH to send
257
+ })
258
+ .sign(nonce);
259
+
260
+ await paymasterClient.execute(mintIntent);
261
+ ```
262
+
263
+ ### User Onboarding
264
+
265
+ ```typescript
266
+ async function onboardUser(userAddress: string) {
267
+ // Create viem wallet client for user
268
+ const userAccount = await createAccount({
269
+ client: turnkeyClient.apiClient(),
270
+ organizationId: ORGANIZATION_ID,
271
+ signWith: userAddress as `0x${string}`,
272
+ });
273
+
274
+ const userWalletClient = createWalletClient({
275
+ account: userAccount,
276
+ chain: base,
277
+ transport: http(BASE_RPC_URL),
278
+ });
279
+
280
+ // Create Gas Station clients
281
+ const userClient = new GasStationClient({
282
+ walletClient: userWalletClient,
283
+ });
284
+
285
+ // Authorize user (paymaster pays)
286
+ const authorization = await userClient.signAuthorization();
287
+ await paymasterClient.submitAuthorizations([authorization]);
288
+
289
+ // User can now execute transactions without ETH
290
+ console.log("✅ User ready for gasless transactions!");
291
+ }
292
+ ```
293
+
294
+ ## Architecture
295
+
296
+ ### Gas Station Pattern
297
+
298
+ 1. **Delegate Contract**: Authorized to EOA via EIP-7702
299
+ 2. **Execution Contract**: Contains execution logic and nonce management
300
+ 3. **EOA**: Signs EIP-712 intents off-chain
301
+ 4. **Paymaster**: Submits transactions and pays gas
302
+
303
+ ### Transaction Flow
304
+
305
+ ```
306
+ User (EOA)
307
+ ↓ Signs EIP-712 intent off-chain
308
+ ↓
309
+ SDK (GasStationClient)
310
+ ↓ Builds transaction
311
+ ↓
312
+ Paymaster
313
+ ↓ Submits transaction, pays gas
314
+ ↓
315
+ Gas Station Contract
316
+ ↓ Validates signature & nonce
317
+ ↓ Executes on behalf of EOA
318
+ ↓
319
+ Target Contract (USDC, NFT, DEX, etc.)
320
+ ```
321
+
322
+ ## Chain Support
323
+
324
+ Available presets for quick setup:
325
+
326
+ - **BASE_MAINNET** - Base mainnet (includes USDC address)
327
+ - **ETHEREUM_MAINNET** - Ethereum mainnet (includes USDC address)
328
+
329
+ ```typescript
330
+ // Chain presets are available for quick configuration
331
+ import { CHAIN_PRESETS, GasStationClient } from "@turnkey/gas-station";
332
+ import { createWalletClient, http } from "viem";
333
+
334
+ const basePreset = CHAIN_PRESETS.BASE_MAINNET;
335
+ const userWalletClient = createWalletClient({
336
+ account: userAccount,
337
+ chain: basePreset.chain,
338
+ transport: http(basePreset.rpcUrl),
339
+ });
340
+
341
+ const userClient = new GasStationClient({
342
+ walletClient: userWalletClient,
343
+ });
344
+ ```
345
+
346
+ ## Security
347
+
348
+ - **EIP-712 Signed Intents**: All executions require valid typed signatures
349
+ - **EIP-7702 Scoping**: Authorization is per-EOA and can be revoked
350
+ - **Deadline Enforcement**: Each transaction includes a deadline (Unix timestamp) to prevent replay attacks; signatures expire after this time
351
+ - **Turnkey Integration**: Private keys never leave Turnkey's secure infrastructure
352
+
353
+ ### Security Policies
354
+
355
+ Turnkey policies provide additional security layers by restricting what transactions can be signed and executed. The Gas Station SDK includes helpers for creating these policies.
356
+
357
+ #### EOA Intent Signing Policies
358
+
359
+ Restrict what EIP-712 intents the EOA can sign:
360
+
361
+ ```typescript
362
+ import { buildIntentSigningPolicy } from "@turnkey/gas-station";
363
+
364
+ // USDC-only policy
365
+ const eoaPolicy = buildIntentSigningPolicy({
366
+ organizationId: "your-org-id",
367
+ eoaUserId: "user-id",
368
+ restrictions: {
369
+ allowedContracts: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"], // USDC on Base
370
+ disallowEthTransfer: true, // Disallow ETH transfers
371
+ },
372
+ policyName: "USDC Only Policy",
373
+ });
374
+
375
+ // Resulting policy restricts signing to USDC transfers only:
376
+ // {
377
+ // organizationId: "your-org-id",
378
+ // policyName: "USDC Only Policy",
379
+ // effect: "EFFECT_ALLOW",
380
+ // consensus: "approvers.any(user, user.id == 'user-id')",
381
+ // condition: "activity.resource == 'PRIVATE_KEY' && " +
382
+ // "activity.action == 'SIGN' && " +
383
+ // "eth.eip_712.primary_type == 'Execution' && " +
384
+ // "(eth.eip_712.message['outputContract'] == '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913') && " +
385
+ // "eth.eip_712.message['ethAmount'] == '0'",
386
+ // notes: "Restricts which EIP-712 intents the EOA can sign for gas station execution"
387
+ // }
388
+ ```
389
+
390
+ #### Paymaster Execution Policies
391
+
392
+ Restrict what on-chain transactions the paymaster can submit:
393
+
394
+ ```typescript
395
+ import {
396
+ buildPaymasterExecutionPolicy,
397
+ DEFAULT_EXECUTION_CONTRACT,
398
+ ensureGasStationInterface,
399
+ } from "@turnkey/gas-station";
400
+ import { parseGwei, parseEther } from "viem";
401
+
402
+ // First, ensure the Gas Station ABI is uploaded (enables ABI-based policies)
403
+ await ensureGasStationInterface(
404
+ turnkeyClient.apiClient(),
405
+ "your-org-id",
406
+ DEFAULT_EXECUTION_CONTRACT,
407
+ undefined,
408
+ "Base Mainnet",
409
+ );
410
+
411
+ // Paymaster protection policy with ETH amount limit
412
+ const paymasterPolicy = buildPaymasterExecutionPolicy({
413
+ organizationId: "paymaster-org-id",
414
+ paymasterUserId: "paymaster-user-id",
415
+ executionContractAddress: DEFAULT_EXECUTION_CONTRACT,
416
+ restrictions: {
417
+ allowedEOAs: ["0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"],
418
+ allowedContracts: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"],
419
+ maxEthAmount: parseEther("0.1"), // Max 0.1 ETH per EOA transaction
420
+ maxGasPrice: parseGwei("50"), // Max 50 gwei gas price
421
+ maxGasLimit: 500000n, // Max 500k gas limit
422
+ },
423
+ policyName: "Paymaster Protection",
424
+ });
425
+
426
+ // Resulting policy uses ABI parsing for direct argument access:
427
+ // {
428
+ // organizationId: "paymaster-org-id",
429
+ // policyName: "Paymaster Protection",
430
+ // effect: "EFFECT_ALLOW",
431
+ // consensus: "approvers.any(user, user.id == 'paymaster-user-id')",
432
+ // condition: "activity.resource == 'PRIVATE_KEY' && " +
433
+ // "activity.action == 'SIGN' && " +
434
+ // "eth.tx.to == '0xe511ad0a281c10b8408381e2ab8525abe587827b' && " +
435
+ // "(eth.tx.contract_call_args['_to'] == '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913') && " +
436
+ // "(eth.tx.contract_call_args['_targetEoA'] == '0x742d35cc6634c0532925a3b844bc9e7595f0beb') && " +
437
+ // "eth.tx.contract_call_args['ethAmount'] <= 100000000000000000 && " +
438
+ // "eth.tx.gasPrice <= 50000000000 && " +
439
+ // "eth.tx.gas <= 500000",
440
+ // notes: "Restricts which transactions the paymaster can execute on the gas station"
441
+ // }
442
+ ```
443
+
444
+ **Note:** The `ensureGasStationInterface()` function uploads the Gas Station ABI to Turnkey's Smart Contract Interface feature. This enables Turnkey's policy engine to parse the ABI-encoded transaction data and directly compare the `ethAmount` parameter as a uint256 value, rather than raw bytes. The function checks if the ABI already exists before uploading to avoid duplicates.
445
+
446
+ #### Defense in Depth
447
+
448
+ Combine both policy types for maximum security:
449
+
450
+ ```typescript
451
+ import {
452
+ buildIntentSigningPolicy,
453
+ buildPaymasterExecutionPolicy,
454
+ DEFAULT_EXECUTION_CONTRACT,
455
+ } from "@turnkey/gas-station";
456
+ import { parseGwei } from "viem";
457
+
458
+ // Layer 1: EOA can only sign USDC intents
459
+ const eoaPolicy = buildIntentSigningPolicy({
460
+ organizationId: "user-org",
461
+ eoaUserId: "user-id",
462
+ restrictions: {
463
+ allowedContracts: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"],
464
+ allowEthTransfer: false, // No ETH transfers
465
+ },
466
+ });
467
+
468
+ // Layer 2: Paymaster can only execute for specific users with gas limits
469
+ const paymasterPolicy = buildPaymasterExecutionPolicy({
470
+ organizationId: "paymaster-org",
471
+ paymasterUserId: "paymaster-user-id",
472
+ executionContractAddress: DEFAULT_EXECUTION_CONTRACT,
473
+ restrictions: {
474
+ allowedEOAs: ["0xUserAddress..."],
475
+ allowedContracts: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"],
476
+ maxGasPrice: parseGwei("50"),
477
+ maxGasLimit: 500000n,
478
+ },
479
+ });
480
+ ```
481
+
482
+ ### Advanced: Writing Custom Paymaster Policies
483
+
484
+ When using `buildPaymasterExecutionPolicy`, the SDK creates Turnkey policies that parse the transaction calldata to enforce restrictions. Understanding the transaction structure allows you to write custom policies for advanced use cases.
485
+
486
+ #### Transaction Data Structure
487
+
488
+ When the paymaster signs an execution transaction calling `execute(address _target, address _to, uint256 _ethAmount, bytes _data)`, the transaction data (`eth.tx.data`) has the following structure:
489
+
490
+ | Position in eth.tx.data | Length | Content | Example |
491
+ | ----------------------- | --------- | ---------------------- | ------------------------------------------------- |
492
+ | `[2..10]` | 8 chars | Function selector | `6c5c2ed9` (execute) |
493
+ | `[10..74]` | 64 chars | \_target (EOA, padded) | `0000...742d35cc6634c0532925a3b844bc9e7595f0beb` |
494
+ | `[74..138]` | 64 chars | \_to (output contract) | `0000...833589fcd6edb6e08f4c7c32d4f71b54bda02913` |
495
+ | `[138..202]` | 64 chars | \_ethAmount (uint256) | `0000...0000` (0 ETH) or amount in wei |
496
+ | `[202..266]` | 64 chars | Offset to \_data bytes | `0000...0080` (128 bytes) |
497
+ | `[266..330]` | 64 chars | Packed data length | `0000...0055` (85 bytes: 65+16+4) |
498
+ | `[330..460]` | 130 chars | Signature (65 bytes) | EIP-712 signature from EOA |
499
+ | `[460..492]` | 32 chars | Nonce (16 bytes) | `00000000000000000000000000000000` |
500
+ | `[492..500]` | 8 chars | Deadline (4 bytes) | `6ac7d340` (Unix timestamp) |
501
+ | `[500+]` | Variable | Call data | Encoded function call for target contract |
502
+
503
+ **Important:** Turnkey's `eth.tx.data` includes the `0x` prefix, so positions start at index 2 (after `0x`).
504
+
505
+ **Note:** The deadline is a Unix timestamp that prevents replay attacks by expiring signatures after a specified time. The SDK defaults to 1 hour, customizable with `withDeadline()`.
506
+
507
+ #### Policy Conditions Reference
508
+
509
+ **Check execution contract address:**
510
+
511
+ ```typescript
512
+ eth.tx.to == "0x576a4d741b96996cc93b4919a04c16545734481f";
513
+ ```
514
+
515
+ **Check which EOA is executing:**
516
+
517
+ ```typescript
518
+ eth.tx.data[10..74] == '0000000000000000000000742d35cc6634c0532925a3b844bc9e7595f0beb'
519
+ ```
520
+
521
+ **Check target contract (output contract):**
522
+
523
+ ```typescript
524
+ eth.tx.data[74..138] == '0000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913'
525
+ ```
526
+
527
+ **Check ETH amount:**
528
+
529
+ ```typescript
530
+ eth.tx.data[138..202].hex_to_uint() <= 100000000000000000; // Max 0.1 ETH in wei
531
+ ```
532
+
533
+ **Check gas price:**
534
+
535
+ ```typescript
536
+ eth.tx.gasPrice <= 50000000000; // 50 gwei in wei
537
+ ```
538
+
539
+ **Check gas limit:**
540
+
541
+ ```typescript
542
+ eth.tx.gas <= 500000;
543
+ ```
544
+
545
+ #### Example: Custom Multi-Contract Policy
546
+
547
+ Allow paymaster to execute for USDC or DAI only:
548
+
549
+ ```typescript
550
+ const policy = {
551
+ organizationId: "paymaster-org-id",
552
+ policyName: "Stablecoin Execution Policy",
553
+ effect: "EFFECT_ALLOW",
554
+ consensus: `approvers.any(user, user.id == '${paymasterUserId}')`,
555
+ condition: [
556
+ "activity.resource == 'PRIVATE_KEY'",
557
+ "activity.action == 'SIGN'",
558
+ "eth.tx.to == '0x576a4d741b96996cc93b4919a04c16545734481f'",
559
+ // Allow USDC or DAI
560
+ "(eth.tx.data[74..138] == '0000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913' || eth.tx.data[74..138] == '00000000000000000000006b175474e89094c44da98b954eedeac495271d0f')",
561
+ // Gas limits
562
+ "eth.tx.gasPrice <= 100000000000",
563
+ "eth.tx.gas <= 500000",
564
+ ].join(" && "),
565
+ notes: "Allow USDC and DAI execution with gas limits",
566
+ };
567
+
568
+ await turnkeyClient.apiClient().createPolicy(policy);
569
+ ```
570
+
571
+ #### Example: Whitelist Specific EOAs
572
+
573
+ Only allow execution for approved user wallets:
574
+
575
+ ```typescript
576
+ const approvedEOAs = [
577
+ "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
578
+ "0x1234567890123456789012345678901234567890",
579
+ ];
580
+
581
+ const eoaConditions = approvedEOAs
582
+ .map((addr) => {
583
+ const padded = addr.slice(2).toLowerCase().padStart(64, "0");
584
+ return `eth.tx.data[10..74] == '${padded}'`;
585
+ })
586
+ .join(" || ");
587
+
588
+ const policy = {
589
+ organizationId: "paymaster-org-id",
590
+ policyName: "Approved Users Only",
591
+ effect: "EFFECT_ALLOW",
592
+ consensus: `approvers.any(user, user.id == '${paymasterUserId}')`,
593
+ condition: [
594
+ "activity.resource == 'PRIVATE_KEY'",
595
+ "activity.action == 'SIGN'",
596
+ "eth.tx.to == '0x576a4d741b96996cc93b4919a04c16545734481f'",
597
+ `(${eoaConditions})`,
598
+ ].join(" && "),
599
+ };
600
+
601
+ await turnkeyClient.apiClient().createPolicy(policy);
602
+ ```
603
+
604
+ #### Using the Helper Functions
605
+
606
+ For most cases, use the built-in helpers which handle the byte positions correctly:
607
+
608
+ ```typescript
609
+ import {
610
+ buildPaymasterExecutionPolicy,
611
+ DEFAULT_EXECUTION_CONTRACT,
612
+ } from "@turnkey/gas-station";
613
+ import { parseGwei } from "viem";
614
+
615
+ const policy = buildPaymasterExecutionPolicy({
616
+ organizationId: subOrgId,
617
+ paymasterUserId: paymasterUserId,
618
+ executionContractAddress: DEFAULT_EXECUTION_CONTRACT,
619
+ restrictions: {
620
+ allowedContracts: [
621
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC
622
+ "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb", // DAI
623
+ ],
624
+ allowedEOAs: ["0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"],
625
+ maxGasPrice: parseGwei("100"),
626
+ maxGasLimit: 500000n,
627
+ },
628
+ policyName: "Production Paymaster Policy",
629
+ });
630
+
631
+ await turnkeyClient.apiClient().createPolicy(policy);
632
+ ```
633
+
634
+ The helper functions automatically:
635
+
636
+ - Convert addresses to lowercase
637
+ - Add proper padding for EOA addresses
638
+ - Calculate correct byte positions (accounting for `0x` prefix)
639
+ - Generate proper OR conditions for multiple allowed values
640
+
641
+ ## Troubleshooting
642
+
643
+ ### Authorization Failed
644
+
645
+ - Ensure paymaster has ETH for gas
646
+ - Verify delegate contract address is correct
647
+ - Check EOA hasn't already been authorized
648
+
649
+ ### Execution Failed
650
+
651
+ - Confirm EOA is authorized (check with `isAuthorized()`)
652
+ - Verify execution contract address matches deployment
653
+ - Check nonce hasn't been reused
654
+ - Ensure target contract call is valid
655
+
656
+ ### Insufficient Funds
657
+
658
+ - EOA must have sufficient token balance for transfers
659
+ - Paymaster must have ETH for gas
660
+
661
+ ### Invalid Signature
662
+
663
+ - Verify EOA address is correct
664
+ - Ensure chain ID matches the network
665
+ - Check intent was signed with correct nonce
666
+
667
+ ## Advanced: Custom Contract Integration
668
+
669
+ For contracts beyond standard transfers:
670
+
671
+ ```typescript
672
+ // Define your contract ABI
673
+ const MY_CONTRACT_ABI = [
674
+ {
675
+ name: "customFunction",
676
+ type: "function",
677
+ inputs: [
678
+ { name: "param1", type: "uint256" },
679
+ { name: "param2", type: "address" },
680
+ ],
681
+ outputs: [],
682
+ },
683
+ ];
684
+
685
+ // Build and execute
686
+ const nonce = await userClient.getNonce();
687
+ const intent = await userClient
688
+ .createIntent()
689
+ .callContract({
690
+ contract: myContractAddress,
691
+ abi: MY_CONTRACT_ABI,
692
+ functionName: "customFunction",
693
+ args: [12345, recipientAddress],
694
+ })
695
+ .sign(nonce);
696
+
697
+ // Execute gaslessly
698
+ await paymasterClient.execute(intent);
699
+ ```
700
+
701
+ ## Contract Deployment
702
+
703
+ You need to deploy two contracts:
704
+
705
+ 1. **Delegate Contract** - Slim contract authorized via EIP-7702
706
+ 2. **Execution Contract** - Contains `execute()` function and nonce storage
707
+
708
+ The execution contract should implement:
709
+
710
+ - `execute(address eoaAddress, uint128 nonce, address outputContract, uint256 ethAmount, bytes callData, bytes signature)`
711
+ - `getNonce(address eoaAddress) returns (uint128)`
712
+
713
+ See `abi/gas-station.ts` for the expected interface.
714
+
715
+ ## Best Practices
716
+
717
+ 1. **Client Separation**: Create separate client instances for users and paymasters
718
+ 2. **Authorization**: Only call `authorize()` once per EOA
719
+ 3. **Nonce Management**: Always fetch fresh nonce before creating intents
720
+ 4. **Error Handling**: Wrap executions in try/catch for robust error handling
721
+ 5. **Rate Limiting**: Implement paymaster rate limits to prevent abuse
722
+ 6. **Monitoring**: Track gas costs and failed transactions for optimization
723
+
724
+ ## License
725
+
726
+ See the main SDK repository for license information.