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