@circle-fin/x402-batching 2.0.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.
- package/README.md +123 -0
- package/dist/client/index.d.mts +653 -0
- package/dist/client/index.d.ts +653 -0
- package/dist/client/index.js +1313 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/index.mjs +1282 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/index.d.mts +62 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +33 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server/index.d.mts +318 -0
- package/dist/server/index.d.ts +318 -0
- package/dist/server/index.js +697 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +668 -0
- package/dist/server/index.mjs.map +1 -0
- package/dist/types-DnHgU28a.d.mts +90 -0
- package/dist/types-DnHgU28a.d.ts +90 -0
- package/package.json +95 -0
|
@@ -0,0 +1,1282 @@
|
|
|
1
|
+
// src/client/BatchEvmScheme.ts
|
|
2
|
+
import { getAddress } from "viem";
|
|
3
|
+
|
|
4
|
+
// src/constants.ts
|
|
5
|
+
var CIRCLE_BATCHING_NAME = "GatewayWalletBatched";
|
|
6
|
+
var CIRCLE_BATCHING_VERSION = "1";
|
|
7
|
+
var CIRCLE_BATCHING_SCHEME = "exact";
|
|
8
|
+
|
|
9
|
+
// src/detection.ts
|
|
10
|
+
function supportsBatching(requirements) {
|
|
11
|
+
const extra = requirements.extra;
|
|
12
|
+
if (!extra) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return extra.name === CIRCLE_BATCHING_NAME && extra.version === CIRCLE_BATCHING_VERSION;
|
|
16
|
+
}
|
|
17
|
+
function getVerifyingContract(requirements) {
|
|
18
|
+
if (!supportsBatching(requirements)) {
|
|
19
|
+
return void 0;
|
|
20
|
+
}
|
|
21
|
+
const verifyingContract = requirements.extra?.verifyingContract;
|
|
22
|
+
if (typeof verifyingContract === "string") {
|
|
23
|
+
return verifyingContract;
|
|
24
|
+
}
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/client/BatchEvmScheme.ts
|
|
29
|
+
var authorizationTypes = {
|
|
30
|
+
TransferWithAuthorization: [
|
|
31
|
+
{ name: "from", type: "address" },
|
|
32
|
+
{ name: "to", type: "address" },
|
|
33
|
+
{ name: "value", type: "uint256" },
|
|
34
|
+
{ name: "validAfter", type: "uint256" },
|
|
35
|
+
{ name: "validBefore", type: "uint256" },
|
|
36
|
+
{ name: "nonce", type: "bytes32" }
|
|
37
|
+
]
|
|
38
|
+
};
|
|
39
|
+
function createNonce() {
|
|
40
|
+
const bytes = new Uint8Array(32);
|
|
41
|
+
crypto.getRandomValues(bytes);
|
|
42
|
+
return `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
43
|
+
}
|
|
44
|
+
var BatchEvmScheme = class {
|
|
45
|
+
/**
|
|
46
|
+
* Creates a new BatchEvmScheme.
|
|
47
|
+
*
|
|
48
|
+
* @param signer - The EVM signer for signing payment authorizations
|
|
49
|
+
*/
|
|
50
|
+
constructor(signer) {
|
|
51
|
+
this.signer = signer;
|
|
52
|
+
}
|
|
53
|
+
scheme = CIRCLE_BATCHING_SCHEME;
|
|
54
|
+
/**
|
|
55
|
+
* Creates a payment payload for Circle batching.
|
|
56
|
+
*
|
|
57
|
+
* @param x402Version - The x402 protocol version
|
|
58
|
+
* @param paymentRequirements - The payment requirements (must be a Circle batching option)
|
|
59
|
+
* @returns Promise resolving to the payment payload
|
|
60
|
+
* @throws Error if requirements are not a Circle batching option
|
|
61
|
+
*/
|
|
62
|
+
async createPaymentPayload(x402Version, paymentRequirements) {
|
|
63
|
+
if (!supportsBatching(paymentRequirements)) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`BatchEvmScheme can only handle Circle batching options. Expected extra.name="${CIRCLE_BATCHING_NAME}" and extra.version="${CIRCLE_BATCHING_VERSION}"`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const verifyingContract = getVerifyingContract(paymentRequirements);
|
|
69
|
+
if (!verifyingContract) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
"Circle batching option missing extra.verifyingContract (GatewayWallet address)"
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const nonce = createNonce();
|
|
75
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
76
|
+
const authorization = {
|
|
77
|
+
from: this.signer.address,
|
|
78
|
+
to: getAddress(paymentRequirements.payTo),
|
|
79
|
+
value: paymentRequirements.amount,
|
|
80
|
+
validAfter: (now - 600).toString(),
|
|
81
|
+
// 10 minutes before
|
|
82
|
+
validBefore: (now + paymentRequirements.maxTimeoutSeconds).toString(),
|
|
83
|
+
nonce
|
|
84
|
+
};
|
|
85
|
+
const signature = await this.signAuthorization(
|
|
86
|
+
authorization,
|
|
87
|
+
paymentRequirements,
|
|
88
|
+
verifyingContract
|
|
89
|
+
);
|
|
90
|
+
const payload = {
|
|
91
|
+
authorization,
|
|
92
|
+
signature
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
x402Version,
|
|
96
|
+
payload
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Sign the EIP-3009 authorization using EIP-712.
|
|
101
|
+
* Uses the GatewayWallet contract as verifyingContract instead of USDC.
|
|
102
|
+
*/
|
|
103
|
+
async signAuthorization(authorization, requirements, verifyingContract) {
|
|
104
|
+
if (!requirements.network.startsWith("eip155:")) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`BatchEvmScheme: unsupported network format "${requirements.network}". Expected "eip155:<chainId>"`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const chainId = parseInt(requirements.network.split(":")[1]);
|
|
110
|
+
const domain = {
|
|
111
|
+
name: CIRCLE_BATCHING_NAME,
|
|
112
|
+
version: CIRCLE_BATCHING_VERSION,
|
|
113
|
+
chainId,
|
|
114
|
+
verifyingContract: getAddress(verifyingContract)
|
|
115
|
+
};
|
|
116
|
+
const message = {
|
|
117
|
+
from: getAddress(authorization.from),
|
|
118
|
+
to: getAddress(authorization.to),
|
|
119
|
+
value: BigInt(authorization.value),
|
|
120
|
+
validAfter: BigInt(authorization.validAfter),
|
|
121
|
+
validBefore: BigInt(authorization.validBefore),
|
|
122
|
+
nonce: authorization.nonce
|
|
123
|
+
};
|
|
124
|
+
return await this.signer.signTypedData({
|
|
125
|
+
domain,
|
|
126
|
+
types: authorizationTypes,
|
|
127
|
+
primaryType: "TransferWithAuthorization",
|
|
128
|
+
message
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// src/client/CompositeEvmScheme.ts
|
|
134
|
+
var CompositeEvmScheme = class {
|
|
135
|
+
constructor(batchScheme, fallbackScheme) {
|
|
136
|
+
this.batchScheme = batchScheme;
|
|
137
|
+
this.fallbackScheme = fallbackScheme;
|
|
138
|
+
if (fallbackScheme.scheme !== CIRCLE_BATCHING_SCHEME) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`CompositeEvmScheme: fallbackScheme.scheme must be "${CIRCLE_BATCHING_SCHEME}", got "${fallbackScheme.scheme}"`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
scheme = CIRCLE_BATCHING_SCHEME;
|
|
145
|
+
async createPaymentPayload(x402Version, paymentRequirements) {
|
|
146
|
+
if (supportsBatching(paymentRequirements)) {
|
|
147
|
+
return this.batchScheme.createPaymentPayload(x402Version, paymentRequirements);
|
|
148
|
+
}
|
|
149
|
+
return this.fallbackScheme.createPaymentPayload(x402Version, paymentRequirements);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// src/client/register.ts
|
|
154
|
+
function registerBatchScheme(client, config) {
|
|
155
|
+
const batchScheme = new BatchEvmScheme(config.signer);
|
|
156
|
+
const schemeToRegister = config.fallbackScheme ? new CompositeEvmScheme(batchScheme, config.fallbackScheme) : batchScheme;
|
|
157
|
+
const networks = config.networks ?? ["eip155:*"];
|
|
158
|
+
for (const network of networks) {
|
|
159
|
+
client.register(network, schemeToRegister);
|
|
160
|
+
}
|
|
161
|
+
return schemeToRegister;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/client/GatewayClient.ts
|
|
165
|
+
import {
|
|
166
|
+
createPublicClient,
|
|
167
|
+
createWalletClient,
|
|
168
|
+
http,
|
|
169
|
+
parseUnits,
|
|
170
|
+
formatUnits,
|
|
171
|
+
pad,
|
|
172
|
+
zeroAddress,
|
|
173
|
+
maxUint256,
|
|
174
|
+
defineChain,
|
|
175
|
+
erc20Abi
|
|
176
|
+
} from "viem";
|
|
177
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
178
|
+
import * as chains from "viem/chains";
|
|
179
|
+
import { randomBytes } from "crypto";
|
|
180
|
+
var sonicTestnet = defineChain({
|
|
181
|
+
id: 14601,
|
|
182
|
+
name: "Sonic Testnet",
|
|
183
|
+
nativeCurrency: { decimals: 18, name: "Sonic", symbol: "S" },
|
|
184
|
+
rpcUrls: {
|
|
185
|
+
default: { http: ["https://rpc.testnet.soniclabs.com"] }
|
|
186
|
+
},
|
|
187
|
+
blockExplorers: {
|
|
188
|
+
default: { name: "Sonic Testnet Explorer", url: "https://testnet.soniclabs.com/" }
|
|
189
|
+
},
|
|
190
|
+
testnet: true
|
|
191
|
+
});
|
|
192
|
+
var GATEWAY_API_TESTNET = "https://gateway-api-testnet.circle.com/v1";
|
|
193
|
+
var GATEWAY_API_MAINNET = "https://gateway-api.circle.com/v1";
|
|
194
|
+
var GATEWAY_MINTER_ABI = [
|
|
195
|
+
{
|
|
196
|
+
name: "gatewayMint",
|
|
197
|
+
type: "function",
|
|
198
|
+
stateMutability: "nonpayable",
|
|
199
|
+
inputs: [
|
|
200
|
+
{ name: "attestationPayload", type: "bytes" },
|
|
201
|
+
{ name: "signature", type: "bytes" }
|
|
202
|
+
],
|
|
203
|
+
outputs: []
|
|
204
|
+
}
|
|
205
|
+
];
|
|
206
|
+
var GATEWAY_WALLET_ABI = [
|
|
207
|
+
{
|
|
208
|
+
name: "deposit",
|
|
209
|
+
type: "function",
|
|
210
|
+
stateMutability: "nonpayable",
|
|
211
|
+
inputs: [
|
|
212
|
+
{ name: "token", type: "address" },
|
|
213
|
+
{ name: "value", type: "uint256" }
|
|
214
|
+
],
|
|
215
|
+
outputs: []
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: "depositFor",
|
|
219
|
+
type: "function",
|
|
220
|
+
stateMutability: "nonpayable",
|
|
221
|
+
inputs: [
|
|
222
|
+
{ name: "token", type: "address" },
|
|
223
|
+
{ name: "depositor", type: "address" },
|
|
224
|
+
{ name: "value", type: "uint256" }
|
|
225
|
+
],
|
|
226
|
+
outputs: []
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: "totalBalance",
|
|
230
|
+
type: "function",
|
|
231
|
+
stateMutability: "view",
|
|
232
|
+
inputs: [
|
|
233
|
+
{ name: "token", type: "address" },
|
|
234
|
+
{ name: "depositor", type: "address" }
|
|
235
|
+
],
|
|
236
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: "availableBalance",
|
|
240
|
+
type: "function",
|
|
241
|
+
stateMutability: "view",
|
|
242
|
+
inputs: [
|
|
243
|
+
{ name: "token", type: "address" },
|
|
244
|
+
{ name: "depositor", type: "address" }
|
|
245
|
+
],
|
|
246
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: "withdrawingBalance",
|
|
250
|
+
type: "function",
|
|
251
|
+
stateMutability: "view",
|
|
252
|
+
inputs: [
|
|
253
|
+
{ name: "token", type: "address" },
|
|
254
|
+
{ name: "depositor", type: "address" }
|
|
255
|
+
],
|
|
256
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: "withdrawableBalance",
|
|
260
|
+
type: "function",
|
|
261
|
+
stateMutability: "view",
|
|
262
|
+
inputs: [
|
|
263
|
+
{ name: "token", type: "address" },
|
|
264
|
+
{ name: "depositor", type: "address" }
|
|
265
|
+
],
|
|
266
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
name: "withdrawalDelay",
|
|
270
|
+
type: "function",
|
|
271
|
+
stateMutability: "view",
|
|
272
|
+
inputs: [],
|
|
273
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: "withdrawalBlock",
|
|
277
|
+
type: "function",
|
|
278
|
+
stateMutability: "view",
|
|
279
|
+
inputs: [
|
|
280
|
+
{ name: "token", type: "address" },
|
|
281
|
+
{ name: "depositor", type: "address" }
|
|
282
|
+
],
|
|
283
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: "initiateWithdrawal",
|
|
287
|
+
type: "function",
|
|
288
|
+
stateMutability: "nonpayable",
|
|
289
|
+
inputs: [
|
|
290
|
+
{ name: "token", type: "address" },
|
|
291
|
+
{ name: "value", type: "uint256" }
|
|
292
|
+
],
|
|
293
|
+
outputs: []
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
name: "withdraw",
|
|
297
|
+
type: "function",
|
|
298
|
+
stateMutability: "nonpayable",
|
|
299
|
+
inputs: [{ name: "token", type: "address" }],
|
|
300
|
+
outputs: []
|
|
301
|
+
}
|
|
302
|
+
];
|
|
303
|
+
var GATEWAY_DOMAINS = {
|
|
304
|
+
// Testnet
|
|
305
|
+
arbitrumSepolia: 3,
|
|
306
|
+
arcTestnet: 26,
|
|
307
|
+
avalancheFuji: 1,
|
|
308
|
+
baseSepolia: 6,
|
|
309
|
+
sepolia: 0,
|
|
310
|
+
hyperEvmTestnet: 19,
|
|
311
|
+
optimismSepolia: 2,
|
|
312
|
+
polygonAmoy: 7,
|
|
313
|
+
seiAtlantic: 16,
|
|
314
|
+
sonicTestnet: 13,
|
|
315
|
+
unichainSepolia: 10,
|
|
316
|
+
worldChainSepolia: 14,
|
|
317
|
+
// Mainnet
|
|
318
|
+
arbitrum: 3,
|
|
319
|
+
avalanche: 1,
|
|
320
|
+
base: 6,
|
|
321
|
+
mainnet: 0,
|
|
322
|
+
hyperEvm: 19,
|
|
323
|
+
optimism: 2,
|
|
324
|
+
polygon: 7,
|
|
325
|
+
sei: 16,
|
|
326
|
+
sonic: 13,
|
|
327
|
+
unichain: 10,
|
|
328
|
+
worldChain: 14
|
|
329
|
+
};
|
|
330
|
+
var TESTNET_GATEWAY_WALLET = "0x0077777d7EBA4688BDeF3E311b846F25870A19B9";
|
|
331
|
+
var TESTNET_GATEWAY_MINTER = "0x0022222ABE238Cc2C7Bb1f21003F0a260052475B";
|
|
332
|
+
var MAINNET_GATEWAY_WALLET = "0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE";
|
|
333
|
+
var MAINNET_GATEWAY_MINTER = "0x2222222d7164433c4C09B0b0D809a9b52C04C205";
|
|
334
|
+
var CHAIN_CONFIGS = {
|
|
335
|
+
// Testnet chains
|
|
336
|
+
arbitrumSepolia: {
|
|
337
|
+
chain: chains.arbitrumSepolia,
|
|
338
|
+
domain: GATEWAY_DOMAINS.arbitrumSepolia,
|
|
339
|
+
usdc: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d",
|
|
340
|
+
gatewayWallet: TESTNET_GATEWAY_WALLET,
|
|
341
|
+
gatewayMinter: TESTNET_GATEWAY_MINTER
|
|
342
|
+
},
|
|
343
|
+
arcTestnet: {
|
|
344
|
+
chain: chains.arcTestnet,
|
|
345
|
+
domain: GATEWAY_DOMAINS.arcTestnet,
|
|
346
|
+
usdc: "0x3600000000000000000000000000000000000000",
|
|
347
|
+
gatewayWallet: TESTNET_GATEWAY_WALLET,
|
|
348
|
+
gatewayMinter: TESTNET_GATEWAY_MINTER,
|
|
349
|
+
rpcUrl: "https://rpc.testnet.arc.network"
|
|
350
|
+
},
|
|
351
|
+
avalancheFuji: {
|
|
352
|
+
chain: chains.avalancheFuji,
|
|
353
|
+
domain: GATEWAY_DOMAINS.avalancheFuji,
|
|
354
|
+
usdc: "0x5425890298aed601595a70AB815c96711a31Bc65",
|
|
355
|
+
gatewayWallet: TESTNET_GATEWAY_WALLET,
|
|
356
|
+
gatewayMinter: TESTNET_GATEWAY_MINTER
|
|
357
|
+
},
|
|
358
|
+
baseSepolia: {
|
|
359
|
+
chain: chains.baseSepolia,
|
|
360
|
+
domain: GATEWAY_DOMAINS.baseSepolia,
|
|
361
|
+
usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
362
|
+
gatewayWallet: TESTNET_GATEWAY_WALLET,
|
|
363
|
+
gatewayMinter: TESTNET_GATEWAY_MINTER,
|
|
364
|
+
rpcUrl: "https://sepolia-preconf.base.org"
|
|
365
|
+
},
|
|
366
|
+
sepolia: {
|
|
367
|
+
chain: chains.sepolia,
|
|
368
|
+
domain: GATEWAY_DOMAINS.sepolia,
|
|
369
|
+
usdc: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
|
|
370
|
+
gatewayWallet: TESTNET_GATEWAY_WALLET,
|
|
371
|
+
gatewayMinter: TESTNET_GATEWAY_MINTER
|
|
372
|
+
},
|
|
373
|
+
hyperEvmTestnet: {
|
|
374
|
+
chain: chains.hyperliquidEvmTestnet,
|
|
375
|
+
domain: GATEWAY_DOMAINS.hyperEvmTestnet,
|
|
376
|
+
usdc: "0x2B3370eE501B4a559b57D449569354196457D8Ab",
|
|
377
|
+
gatewayWallet: TESTNET_GATEWAY_WALLET,
|
|
378
|
+
gatewayMinter: TESTNET_GATEWAY_MINTER
|
|
379
|
+
},
|
|
380
|
+
optimismSepolia: {
|
|
381
|
+
chain: chains.optimismSepolia,
|
|
382
|
+
domain: GATEWAY_DOMAINS.optimismSepolia,
|
|
383
|
+
usdc: "0x5fd84259d66Cd46123540766Be93DFE6D43130D7",
|
|
384
|
+
gatewayWallet: TESTNET_GATEWAY_WALLET,
|
|
385
|
+
gatewayMinter: TESTNET_GATEWAY_MINTER
|
|
386
|
+
},
|
|
387
|
+
polygonAmoy: {
|
|
388
|
+
chain: chains.polygonAmoy,
|
|
389
|
+
domain: GATEWAY_DOMAINS.polygonAmoy,
|
|
390
|
+
usdc: "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582",
|
|
391
|
+
gatewayWallet: TESTNET_GATEWAY_WALLET,
|
|
392
|
+
gatewayMinter: TESTNET_GATEWAY_MINTER
|
|
393
|
+
},
|
|
394
|
+
seiAtlantic: {
|
|
395
|
+
chain: chains.seiTestnet,
|
|
396
|
+
domain: GATEWAY_DOMAINS.seiAtlantic,
|
|
397
|
+
usdc: "0x4fCF1784B31630811181f670Aea7A7bEF803eaED",
|
|
398
|
+
gatewayWallet: TESTNET_GATEWAY_WALLET,
|
|
399
|
+
gatewayMinter: TESTNET_GATEWAY_MINTER
|
|
400
|
+
},
|
|
401
|
+
sonicTestnet: {
|
|
402
|
+
chain: sonicTestnet,
|
|
403
|
+
domain: GATEWAY_DOMAINS.sonicTestnet,
|
|
404
|
+
usdc: "0x0BA304580ee7c9a980CF72e55f5Ed2E9fd30Bc51",
|
|
405
|
+
gatewayWallet: TESTNET_GATEWAY_WALLET,
|
|
406
|
+
gatewayMinter: TESTNET_GATEWAY_MINTER
|
|
407
|
+
},
|
|
408
|
+
unichainSepolia: {
|
|
409
|
+
chain: chains.unichainSepolia,
|
|
410
|
+
domain: GATEWAY_DOMAINS.unichainSepolia,
|
|
411
|
+
usdc: "0x31d0220469e10c4E71834a79b1f276d740d3768F",
|
|
412
|
+
gatewayWallet: TESTNET_GATEWAY_WALLET,
|
|
413
|
+
gatewayMinter: TESTNET_GATEWAY_MINTER
|
|
414
|
+
},
|
|
415
|
+
worldChainSepolia: {
|
|
416
|
+
chain: chains.worldchainSepolia,
|
|
417
|
+
domain: GATEWAY_DOMAINS.worldChainSepolia,
|
|
418
|
+
usdc: "0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88",
|
|
419
|
+
gatewayWallet: TESTNET_GATEWAY_WALLET,
|
|
420
|
+
gatewayMinter: TESTNET_GATEWAY_MINTER
|
|
421
|
+
},
|
|
422
|
+
// Mainnet chains
|
|
423
|
+
arbitrum: {
|
|
424
|
+
chain: chains.arbitrum,
|
|
425
|
+
domain: GATEWAY_DOMAINS.arbitrum,
|
|
426
|
+
usdc: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
|
427
|
+
gatewayWallet: MAINNET_GATEWAY_WALLET,
|
|
428
|
+
gatewayMinter: MAINNET_GATEWAY_MINTER
|
|
429
|
+
},
|
|
430
|
+
avalanche: {
|
|
431
|
+
chain: chains.avalanche,
|
|
432
|
+
domain: GATEWAY_DOMAINS.avalanche,
|
|
433
|
+
usdc: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
|
|
434
|
+
gatewayWallet: MAINNET_GATEWAY_WALLET,
|
|
435
|
+
gatewayMinter: MAINNET_GATEWAY_MINTER
|
|
436
|
+
},
|
|
437
|
+
base: {
|
|
438
|
+
chain: chains.base,
|
|
439
|
+
domain: GATEWAY_DOMAINS.base,
|
|
440
|
+
usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
441
|
+
gatewayWallet: MAINNET_GATEWAY_WALLET,
|
|
442
|
+
gatewayMinter: MAINNET_GATEWAY_MINTER
|
|
443
|
+
},
|
|
444
|
+
mainnet: {
|
|
445
|
+
chain: chains.mainnet,
|
|
446
|
+
domain: GATEWAY_DOMAINS.mainnet,
|
|
447
|
+
usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
448
|
+
gatewayWallet: MAINNET_GATEWAY_WALLET,
|
|
449
|
+
gatewayMinter: MAINNET_GATEWAY_MINTER
|
|
450
|
+
},
|
|
451
|
+
hyperEvm: {
|
|
452
|
+
chain: chains.hyperEvm,
|
|
453
|
+
domain: GATEWAY_DOMAINS.hyperEvm,
|
|
454
|
+
usdc: "0xb88339CB7199b77E23DB6E890353E22632Ba630f",
|
|
455
|
+
gatewayWallet: MAINNET_GATEWAY_WALLET,
|
|
456
|
+
gatewayMinter: MAINNET_GATEWAY_MINTER
|
|
457
|
+
},
|
|
458
|
+
optimism: {
|
|
459
|
+
chain: chains.optimism,
|
|
460
|
+
domain: GATEWAY_DOMAINS.optimism,
|
|
461
|
+
usdc: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
|
|
462
|
+
gatewayWallet: MAINNET_GATEWAY_WALLET,
|
|
463
|
+
gatewayMinter: MAINNET_GATEWAY_MINTER
|
|
464
|
+
},
|
|
465
|
+
polygon: {
|
|
466
|
+
chain: chains.polygon,
|
|
467
|
+
domain: GATEWAY_DOMAINS.polygon,
|
|
468
|
+
usdc: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
469
|
+
gatewayWallet: MAINNET_GATEWAY_WALLET,
|
|
470
|
+
gatewayMinter: MAINNET_GATEWAY_MINTER
|
|
471
|
+
},
|
|
472
|
+
sei: {
|
|
473
|
+
chain: chains.sei,
|
|
474
|
+
domain: GATEWAY_DOMAINS.sei,
|
|
475
|
+
usdc: "0xe15fC38F6D8c56aF07bbCBe3BAf5708A2Bf42392",
|
|
476
|
+
gatewayWallet: MAINNET_GATEWAY_WALLET,
|
|
477
|
+
gatewayMinter: MAINNET_GATEWAY_MINTER
|
|
478
|
+
},
|
|
479
|
+
sonic: {
|
|
480
|
+
chain: chains.sonic,
|
|
481
|
+
domain: GATEWAY_DOMAINS.sonic,
|
|
482
|
+
usdc: "0x29219dd400f2Bf60E5a23d13Be72B486D4038894",
|
|
483
|
+
gatewayWallet: MAINNET_GATEWAY_WALLET,
|
|
484
|
+
gatewayMinter: MAINNET_GATEWAY_MINTER
|
|
485
|
+
},
|
|
486
|
+
unichain: {
|
|
487
|
+
chain: chains.unichain,
|
|
488
|
+
domain: GATEWAY_DOMAINS.unichain,
|
|
489
|
+
usdc: "0x078D782b760474a361dDA0AF3839290b0EF57AD6",
|
|
490
|
+
gatewayWallet: MAINNET_GATEWAY_WALLET,
|
|
491
|
+
gatewayMinter: MAINNET_GATEWAY_MINTER
|
|
492
|
+
},
|
|
493
|
+
worldChain: {
|
|
494
|
+
chain: chains.worldchain,
|
|
495
|
+
domain: GATEWAY_DOMAINS.worldChain,
|
|
496
|
+
usdc: "0x79A02482A880bCe3F13E09da970dC34dB4cD24D1",
|
|
497
|
+
gatewayWallet: MAINNET_GATEWAY_WALLET,
|
|
498
|
+
gatewayMinter: MAINNET_GATEWAY_MINTER
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
var GatewayClient = class {
|
|
502
|
+
chainConfig;
|
|
503
|
+
account;
|
|
504
|
+
publicClient;
|
|
505
|
+
walletClient;
|
|
506
|
+
/**
|
|
507
|
+
* Creates a new GatewayClient.
|
|
508
|
+
*
|
|
509
|
+
* @param config - Configuration including chain and private key
|
|
510
|
+
*/
|
|
511
|
+
constructor(config) {
|
|
512
|
+
const chainConfig = CHAIN_CONFIGS[config.chain];
|
|
513
|
+
if (!chainConfig) {
|
|
514
|
+
throw new Error(`Unsupported chain: ${config.chain}`);
|
|
515
|
+
}
|
|
516
|
+
this.chainConfig = chainConfig;
|
|
517
|
+
this.account = privateKeyToAccount(config.privateKey);
|
|
518
|
+
const rpcUrl = config.rpcUrl ?? chainConfig.rpcUrl;
|
|
519
|
+
const transport = rpcUrl ? http(rpcUrl) : http();
|
|
520
|
+
this.publicClient = createPublicClient({
|
|
521
|
+
chain: chainConfig.chain,
|
|
522
|
+
transport
|
|
523
|
+
});
|
|
524
|
+
this.walletClient = createWalletClient({
|
|
525
|
+
account: this.account,
|
|
526
|
+
chain: chainConfig.chain,
|
|
527
|
+
transport
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Get the account address.
|
|
532
|
+
*/
|
|
533
|
+
get address() {
|
|
534
|
+
return this.account.address;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Get the chain name.
|
|
538
|
+
*/
|
|
539
|
+
get chainName() {
|
|
540
|
+
return this.chainConfig.chain.name;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Get the Gateway domain for this chain.
|
|
544
|
+
*/
|
|
545
|
+
get domain() {
|
|
546
|
+
return this.chainConfig.domain;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Get the chain name key for this client.
|
|
550
|
+
*/
|
|
551
|
+
getChainName() {
|
|
552
|
+
for (const [name, config] of Object.entries(CHAIN_CONFIGS)) {
|
|
553
|
+
if (config.domain === this.chainConfig.domain && config.chain.id === this.chainConfig.chain.id) {
|
|
554
|
+
return name;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
throw new Error("Unknown chain configuration");
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Get the USDC balance of the account (not in Gateway).
|
|
561
|
+
*
|
|
562
|
+
* @param address - Optional address to check (defaults to account address)
|
|
563
|
+
* @returns USDC balance in atomic units and formatted
|
|
564
|
+
*/
|
|
565
|
+
async getUsdcBalance(address) {
|
|
566
|
+
const target = address ?? this.account.address;
|
|
567
|
+
const balance = await this.publicClient.readContract({
|
|
568
|
+
address: this.chainConfig.usdc,
|
|
569
|
+
abi: erc20Abi,
|
|
570
|
+
functionName: "balanceOf",
|
|
571
|
+
args: [target]
|
|
572
|
+
});
|
|
573
|
+
return {
|
|
574
|
+
balance,
|
|
575
|
+
formatted: formatUnits(balance, 6)
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Get all balances (wallet + gateway) in one call.
|
|
580
|
+
*
|
|
581
|
+
* @param address - Optional address to check (defaults to account address)
|
|
582
|
+
* @returns All balances
|
|
583
|
+
*/
|
|
584
|
+
async getBalances(address) {
|
|
585
|
+
const target = address ?? this.account.address;
|
|
586
|
+
const [wallet, gateway] = await Promise.all([
|
|
587
|
+
this.getUsdcBalance(target),
|
|
588
|
+
this.getGatewayBalance(target)
|
|
589
|
+
]);
|
|
590
|
+
return {
|
|
591
|
+
wallet,
|
|
592
|
+
gateway
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Get the Gateway balance for an address.
|
|
597
|
+
*
|
|
598
|
+
* @param address - Optional address to check (defaults to account address)
|
|
599
|
+
* @returns Gateway balance information
|
|
600
|
+
* @deprecated Use `getBalances()` instead for a unified view
|
|
601
|
+
*/
|
|
602
|
+
async getBalance(address) {
|
|
603
|
+
return this.getGatewayBalance(address);
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Get the Gateway balance for an address via Gateway API.
|
|
607
|
+
*/
|
|
608
|
+
async getGatewayBalance(address) {
|
|
609
|
+
const target = address ?? this.account.address;
|
|
610
|
+
const apiBaseUrl = this.isTestnet() ? GATEWAY_API_TESTNET : GATEWAY_API_MAINNET;
|
|
611
|
+
const response = await fetch(`${apiBaseUrl}/balances`, {
|
|
612
|
+
method: "POST",
|
|
613
|
+
headers: { "Content-Type": "application/json" },
|
|
614
|
+
body: JSON.stringify({
|
|
615
|
+
token: "USDC",
|
|
616
|
+
sources: [{ depositor: target, domain: this.chainConfig.domain }]
|
|
617
|
+
})
|
|
618
|
+
});
|
|
619
|
+
const data = await response.json();
|
|
620
|
+
if (!response.ok) {
|
|
621
|
+
throw new Error(
|
|
622
|
+
`Gateway API balance fetch failed: ${data.message ?? response.statusText}`
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
if (!data.balances || data.balances.length === 0) {
|
|
626
|
+
throw new Error("Gateway API returned no balances for the depositor.");
|
|
627
|
+
}
|
|
628
|
+
const balanceData = data.balances[0];
|
|
629
|
+
const available = parseUnits(balanceData.balance, 6);
|
|
630
|
+
const withdrawing = parseUnits(balanceData.withdrawing ?? "0", 6);
|
|
631
|
+
const withdrawable = parseUnits(balanceData.withdrawable ?? "0", 6);
|
|
632
|
+
const total = available + withdrawing;
|
|
633
|
+
return {
|
|
634
|
+
total,
|
|
635
|
+
available,
|
|
636
|
+
withdrawing,
|
|
637
|
+
withdrawable,
|
|
638
|
+
formattedTotal: formatUnits(total, 6),
|
|
639
|
+
formattedAvailable: formatUnits(available, 6),
|
|
640
|
+
formattedWithdrawing: formatUnits(withdrawing, 6),
|
|
641
|
+
formattedWithdrawable: formatUnits(withdrawable, 6)
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Deposit USDC into the Gateway Wallet.
|
|
646
|
+
*
|
|
647
|
+
* This method first approves the Gateway Wallet contract to spend USDC,
|
|
648
|
+
* then deposits the specified amount.
|
|
649
|
+
*
|
|
650
|
+
* @param amount - Amount of USDC to deposit (as a decimal string, e.g., "10.5")
|
|
651
|
+
* @param options - Optional deposit options
|
|
652
|
+
* @returns Deposit result with transaction hashes
|
|
653
|
+
*/
|
|
654
|
+
async deposit(amount, options) {
|
|
655
|
+
const depositAmount = parseUnits(amount, 6);
|
|
656
|
+
const approveAmount = parseUnits(options?.approveAmount ?? amount, 6);
|
|
657
|
+
const { balance } = await this.getUsdcBalance();
|
|
658
|
+
if (balance < depositAmount) {
|
|
659
|
+
throw new Error(
|
|
660
|
+
`Insufficient USDC balance. Have: ${formatUnits(balance, 6)}, Need: ${amount}`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
let approvalTxHash;
|
|
664
|
+
if (!options?.skipApprovalCheck) {
|
|
665
|
+
const allowance = await this.publicClient.readContract({
|
|
666
|
+
address: this.chainConfig.usdc,
|
|
667
|
+
abi: erc20Abi,
|
|
668
|
+
functionName: "allowance",
|
|
669
|
+
args: [this.account.address, this.chainConfig.gatewayWallet]
|
|
670
|
+
});
|
|
671
|
+
if (allowance < depositAmount) {
|
|
672
|
+
approvalTxHash = await this.walletClient.writeContract({
|
|
673
|
+
address: this.chainConfig.usdc,
|
|
674
|
+
abi: erc20Abi,
|
|
675
|
+
functionName: "approve",
|
|
676
|
+
args: [this.chainConfig.gatewayWallet, approveAmount]
|
|
677
|
+
});
|
|
678
|
+
try {
|
|
679
|
+
await this.publicClient.waitForTransactionReceipt({ hash: approvalTxHash });
|
|
680
|
+
} catch (err) {
|
|
681
|
+
throw new Error(`Approval transaction failed: ${approvalTxHash}`, {
|
|
682
|
+
cause: err
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const depositTxHash = await this.walletClient.writeContract({
|
|
688
|
+
address: this.chainConfig.gatewayWallet,
|
|
689
|
+
abi: GATEWAY_WALLET_ABI,
|
|
690
|
+
functionName: "deposit",
|
|
691
|
+
args: [this.chainConfig.usdc, depositAmount],
|
|
692
|
+
gas: 120000n
|
|
693
|
+
});
|
|
694
|
+
try {
|
|
695
|
+
await this.publicClient.waitForTransactionReceipt({ hash: depositTxHash });
|
|
696
|
+
} catch (err) {
|
|
697
|
+
throw new Error(`Deposit transaction failed: ${depositTxHash}`, {
|
|
698
|
+
cause: err
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
return {
|
|
702
|
+
approvalTxHash,
|
|
703
|
+
depositTxHash,
|
|
704
|
+
amount: depositAmount,
|
|
705
|
+
formattedAmount: amount,
|
|
706
|
+
depositor: this.account.address
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Deposit USDC into the Gateway Wallet on behalf of another address.
|
|
711
|
+
*
|
|
712
|
+
* The resulting balance belongs to the specified depositor address,
|
|
713
|
+
* not the caller.
|
|
714
|
+
*
|
|
715
|
+
* @param amount - Amount of USDC to deposit (as a decimal string)
|
|
716
|
+
* @param depositor - Address that will own the resulting balance
|
|
717
|
+
* @param options - Optional deposit options
|
|
718
|
+
* @returns Deposit result with transaction hashes
|
|
719
|
+
*/
|
|
720
|
+
async depositFor(amount, depositor, options) {
|
|
721
|
+
const depositAmount = parseUnits(amount, 6);
|
|
722
|
+
const approveAmount = parseUnits(options?.approveAmount ?? amount, 6);
|
|
723
|
+
const { balance } = await this.getUsdcBalance();
|
|
724
|
+
if (balance < depositAmount) {
|
|
725
|
+
throw new Error(
|
|
726
|
+
`Insufficient USDC balance. Have: ${formatUnits(balance, 6)}, Need: ${amount}`
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
let approvalTxHash;
|
|
730
|
+
if (!options?.skipApprovalCheck) {
|
|
731
|
+
const allowance = await this.publicClient.readContract({
|
|
732
|
+
address: this.chainConfig.usdc,
|
|
733
|
+
abi: erc20Abi,
|
|
734
|
+
functionName: "allowance",
|
|
735
|
+
args: [this.account.address, this.chainConfig.gatewayWallet]
|
|
736
|
+
});
|
|
737
|
+
if (allowance < depositAmount) {
|
|
738
|
+
approvalTxHash = await this.walletClient.writeContract({
|
|
739
|
+
address: this.chainConfig.usdc,
|
|
740
|
+
abi: erc20Abi,
|
|
741
|
+
functionName: "approve",
|
|
742
|
+
args: [this.chainConfig.gatewayWallet, approveAmount]
|
|
743
|
+
});
|
|
744
|
+
try {
|
|
745
|
+
await this.publicClient.waitForTransactionReceipt({ hash: approvalTxHash });
|
|
746
|
+
} catch (err) {
|
|
747
|
+
throw new Error(`Approval transaction failed: ${approvalTxHash}`, {
|
|
748
|
+
cause: err
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
const depositTxHash = await this.walletClient.writeContract({
|
|
754
|
+
address: this.chainConfig.gatewayWallet,
|
|
755
|
+
abi: GATEWAY_WALLET_ABI,
|
|
756
|
+
functionName: "depositFor",
|
|
757
|
+
args: [this.chainConfig.usdc, depositor, depositAmount],
|
|
758
|
+
gas: 120000n
|
|
759
|
+
});
|
|
760
|
+
try {
|
|
761
|
+
await this.publicClient.waitForTransactionReceipt({ hash: depositTxHash });
|
|
762
|
+
} catch (err) {
|
|
763
|
+
throw new Error(`Deposit transaction failed: ${depositTxHash}`, {
|
|
764
|
+
cause: err
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
approvalTxHash,
|
|
769
|
+
depositTxHash,
|
|
770
|
+
amount: depositAmount,
|
|
771
|
+
formattedAmount: amount,
|
|
772
|
+
depositor
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
// ============================================================================
|
|
776
|
+
// PAYMENT (Pay for x402 resources)
|
|
777
|
+
// ============================================================================
|
|
778
|
+
/**
|
|
779
|
+
* Check if a URL supports Gateway batching before paying.
|
|
780
|
+
*
|
|
781
|
+
* @param url - The URL to check
|
|
782
|
+
* @returns Whether batching is supported and payment requirements
|
|
783
|
+
*/
|
|
784
|
+
async supports(url) {
|
|
785
|
+
try {
|
|
786
|
+
const response = await fetch(url);
|
|
787
|
+
if (response.status !== 402) {
|
|
788
|
+
return { supported: false, error: "Resource does not require payment (not 402)" };
|
|
789
|
+
}
|
|
790
|
+
const paymentRequiredHeader = response.headers.get("PAYMENT-REQUIRED");
|
|
791
|
+
if (!paymentRequiredHeader) {
|
|
792
|
+
return {
|
|
793
|
+
supported: false,
|
|
794
|
+
error: "Missing PAYMENT-REQUIRED header in 402 response"
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
const data = JSON.parse(
|
|
798
|
+
Buffer.from(paymentRequiredHeader, "base64").toString("utf-8")
|
|
799
|
+
);
|
|
800
|
+
const accepts = data.accepts;
|
|
801
|
+
if (!accepts || accepts.length === 0) {
|
|
802
|
+
return { supported: false, error: "No payment options in 402 response" };
|
|
803
|
+
}
|
|
804
|
+
const expectedNetwork = `eip155:${this.chainConfig.chain.id}`;
|
|
805
|
+
const batchingOption = accepts.find((opt) => {
|
|
806
|
+
const extra = opt.extra;
|
|
807
|
+
return opt.network === expectedNetwork && extra?.name === "GatewayWalletBatched" && extra?.version === "1" && typeof extra?.verifyingContract === "string";
|
|
808
|
+
});
|
|
809
|
+
if (!batchingOption) {
|
|
810
|
+
return {
|
|
811
|
+
supported: false,
|
|
812
|
+
error: `No Gateway batching option available for network ${expectedNetwork} (${this.chainConfig.chain.name})`
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
return { supported: true, requirements: batchingOption };
|
|
816
|
+
} catch (error) {
|
|
817
|
+
return { supported: false, error: error.message };
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Pay for an x402-protected resource.
|
|
822
|
+
*
|
|
823
|
+
* This method handles the full 402 payment flow automatically:
|
|
824
|
+
* 1. Makes initial request
|
|
825
|
+
* 2. If 402, finds Gateway batching option
|
|
826
|
+
* 3. Signs payment authorization
|
|
827
|
+
* 4. Retries with Payment-Signature header
|
|
828
|
+
*
|
|
829
|
+
* @param url - The URL to pay for
|
|
830
|
+
* @param options - Optional request options
|
|
831
|
+
* @returns The response data and payment info
|
|
832
|
+
*
|
|
833
|
+
* @example
|
|
834
|
+
* ```typescript
|
|
835
|
+
* const { data, amount } = await gateway.pay('https://api.example.com/resource');
|
|
836
|
+
* console.log('Paid', amount, 'USDC for:', data);
|
|
837
|
+
* ```
|
|
838
|
+
*/
|
|
839
|
+
async pay(url, options) {
|
|
840
|
+
const method = options?.method ?? "GET";
|
|
841
|
+
const headers = {
|
|
842
|
+
"Content-Type": "application/json",
|
|
843
|
+
...options?.headers
|
|
844
|
+
};
|
|
845
|
+
const serializedBody = options?.body !== void 0 ? typeof options.body === "string" ? options.body : JSON.stringify(options.body) : void 0;
|
|
846
|
+
const initialResponse = await fetch(url, {
|
|
847
|
+
method,
|
|
848
|
+
headers,
|
|
849
|
+
body: serializedBody
|
|
850
|
+
});
|
|
851
|
+
if (initialResponse.status !== 402) {
|
|
852
|
+
if (initialResponse.ok) {
|
|
853
|
+
const data2 = await initialResponse.json();
|
|
854
|
+
return {
|
|
855
|
+
data: data2,
|
|
856
|
+
amount: 0n,
|
|
857
|
+
formattedAmount: "0",
|
|
858
|
+
transaction: "",
|
|
859
|
+
status: initialResponse.status
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
throw new Error(`Request failed with status ${initialResponse.status}`);
|
|
863
|
+
}
|
|
864
|
+
const paymentRequiredHeader = initialResponse.headers.get("PAYMENT-REQUIRED");
|
|
865
|
+
if (!paymentRequiredHeader) {
|
|
866
|
+
throw new Error("Missing PAYMENT-REQUIRED header in 402 response");
|
|
867
|
+
}
|
|
868
|
+
const paymentRequired = JSON.parse(
|
|
869
|
+
Buffer.from(paymentRequiredHeader, "base64").toString("utf-8")
|
|
870
|
+
);
|
|
871
|
+
const accepts = paymentRequired.accepts;
|
|
872
|
+
if (!accepts || accepts.length === 0) {
|
|
873
|
+
throw new Error("No payment options in 402 response");
|
|
874
|
+
}
|
|
875
|
+
const expectedNetwork = `eip155:${this.chainConfig.chain.id}`;
|
|
876
|
+
const batchingOption = accepts.find((opt) => {
|
|
877
|
+
const extra = opt.extra;
|
|
878
|
+
return opt.network === expectedNetwork && extra?.name === "GatewayWalletBatched" && extra?.version === "1" && typeof extra?.verifyingContract === "string";
|
|
879
|
+
});
|
|
880
|
+
if (!batchingOption) {
|
|
881
|
+
throw new Error(
|
|
882
|
+
`No Gateway batching option available for network ${expectedNetwork} (${this.chainConfig.chain.name}). The seller may not support this chain. Use supports() to check first.`
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
const paymentPayload = await this.createPaymentPayload(
|
|
886
|
+
paymentRequired.x402Version ?? 2,
|
|
887
|
+
batchingOption
|
|
888
|
+
);
|
|
889
|
+
const paymentHeader = Buffer.from(
|
|
890
|
+
JSON.stringify({
|
|
891
|
+
...paymentPayload,
|
|
892
|
+
resource: paymentRequired.resource,
|
|
893
|
+
accepted: batchingOption
|
|
894
|
+
})
|
|
895
|
+
).toString("base64");
|
|
896
|
+
const paidResponse = await fetch(url, {
|
|
897
|
+
method,
|
|
898
|
+
headers: {
|
|
899
|
+
...headers,
|
|
900
|
+
"Payment-Signature": paymentHeader
|
|
901
|
+
},
|
|
902
|
+
body: serializedBody
|
|
903
|
+
});
|
|
904
|
+
if (!paidResponse.ok) {
|
|
905
|
+
const error = await paidResponse.json().catch(() => ({}));
|
|
906
|
+
throw new Error(
|
|
907
|
+
`Payment failed: ${error.error || paidResponse.statusText}`
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
const data = await paidResponse.json();
|
|
911
|
+
const amount = BigInt(batchingOption.amount);
|
|
912
|
+
let transaction = "";
|
|
913
|
+
const paymentResponseHeader = paidResponse.headers.get("PAYMENT-RESPONSE");
|
|
914
|
+
if (paymentResponseHeader) {
|
|
915
|
+
const settleResponse = JSON.parse(
|
|
916
|
+
Buffer.from(paymentResponseHeader, "base64").toString("utf-8")
|
|
917
|
+
);
|
|
918
|
+
transaction = settleResponse.transaction ?? "";
|
|
919
|
+
}
|
|
920
|
+
return {
|
|
921
|
+
data,
|
|
922
|
+
amount,
|
|
923
|
+
formattedAmount: formatUnits(amount, 6),
|
|
924
|
+
transaction,
|
|
925
|
+
status: paidResponse.status
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Create a payment payload for x402 (low-level).
|
|
930
|
+
*/
|
|
931
|
+
async createPaymentPayload(x402Version, requirements) {
|
|
932
|
+
const extra = requirements.extra;
|
|
933
|
+
const verifyingContract = extra.verifyingContract;
|
|
934
|
+
const network = requirements.network;
|
|
935
|
+
const chainId = parseInt(network.split(":")[1]);
|
|
936
|
+
const nonce = `0x${randomBytes(32).toString("hex")}`;
|
|
937
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
938
|
+
const authorization = {
|
|
939
|
+
from: this.account.address,
|
|
940
|
+
to: requirements.payTo,
|
|
941
|
+
value: requirements.amount,
|
|
942
|
+
validAfter: (now - 600).toString(),
|
|
943
|
+
validBefore: (now + requirements.maxTimeoutSeconds).toString(),
|
|
944
|
+
nonce
|
|
945
|
+
};
|
|
946
|
+
const signature = await this.account.signTypedData({
|
|
947
|
+
domain: {
|
|
948
|
+
name: "GatewayWalletBatched",
|
|
949
|
+
version: "1",
|
|
950
|
+
chainId,
|
|
951
|
+
verifyingContract
|
|
952
|
+
},
|
|
953
|
+
types: {
|
|
954
|
+
TransferWithAuthorization: [
|
|
955
|
+
{ name: "from", type: "address" },
|
|
956
|
+
{ name: "to", type: "address" },
|
|
957
|
+
{ name: "value", type: "uint256" },
|
|
958
|
+
{ name: "validAfter", type: "uint256" },
|
|
959
|
+
{ name: "validBefore", type: "uint256" },
|
|
960
|
+
{ name: "nonce", type: "bytes32" }
|
|
961
|
+
]
|
|
962
|
+
},
|
|
963
|
+
primaryType: "TransferWithAuthorization",
|
|
964
|
+
message: {
|
|
965
|
+
from: authorization.from,
|
|
966
|
+
to: authorization.to,
|
|
967
|
+
value: BigInt(authorization.value),
|
|
968
|
+
validAfter: BigInt(authorization.validAfter),
|
|
969
|
+
validBefore: BigInt(authorization.validBefore),
|
|
970
|
+
nonce: authorization.nonce
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
return {
|
|
974
|
+
x402Version,
|
|
975
|
+
payload: { authorization, signature }
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
// ============================================================================
|
|
979
|
+
// WITHDRAW (Instant way to get USDC out)
|
|
980
|
+
// ============================================================================
|
|
981
|
+
/**
|
|
982
|
+
* Withdraw USDC from Gateway to your wallet.
|
|
983
|
+
*
|
|
984
|
+
* By default, withdraws to the same chain (instant, no 7-day delay).
|
|
985
|
+
* Optionally, withdraw to a different chain (requires gas on destination).
|
|
986
|
+
*
|
|
987
|
+
* @param amount - Amount of USDC to withdraw (as a decimal string)
|
|
988
|
+
* @param options - Optional withdrawal options
|
|
989
|
+
* @returns Withdrawal result with transaction hash
|
|
990
|
+
*
|
|
991
|
+
* @example
|
|
992
|
+
* ```typescript
|
|
993
|
+
* // Withdraw to same chain (instant!)
|
|
994
|
+
* await gateway.withdraw('50');
|
|
995
|
+
*
|
|
996
|
+
* // Withdraw to Base Sepolia (requires ETH on Base for gas)
|
|
997
|
+
* await gateway.withdraw('25', { chain: 'baseSepolia' });
|
|
998
|
+
* ```
|
|
999
|
+
*/
|
|
1000
|
+
async withdraw(amount, options) {
|
|
1001
|
+
const destinationChainName = options?.chain ?? this.getChainName();
|
|
1002
|
+
const destConfig = CHAIN_CONFIGS[destinationChainName];
|
|
1003
|
+
if (!destConfig) {
|
|
1004
|
+
throw new Error(`Unsupported destination chain: ${destinationChainName}`);
|
|
1005
|
+
}
|
|
1006
|
+
const withdrawAmount = parseUnits(amount, 6);
|
|
1007
|
+
const recipient = options?.recipient ?? this.account.address;
|
|
1008
|
+
const maxFee = parseUnits(options?.maxFee ?? "2.01", 6);
|
|
1009
|
+
const balances = await this.getBalances();
|
|
1010
|
+
if (balances.gateway.available < withdrawAmount) {
|
|
1011
|
+
throw new Error(
|
|
1012
|
+
`Insufficient available balance. Have: ${balances.gateway.formattedAvailable}, Need: ${amount}`
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
const burnIntent = this.createBurnIntent(
|
|
1016
|
+
this.chainConfig,
|
|
1017
|
+
destConfig,
|
|
1018
|
+
withdrawAmount,
|
|
1019
|
+
recipient,
|
|
1020
|
+
maxFee
|
|
1021
|
+
);
|
|
1022
|
+
const signature = await this.account.signTypedData({
|
|
1023
|
+
domain: { name: "GatewayWallet", version: "1" },
|
|
1024
|
+
types: {
|
|
1025
|
+
EIP712Domain: [
|
|
1026
|
+
{ name: "name", type: "string" },
|
|
1027
|
+
{ name: "version", type: "string" }
|
|
1028
|
+
],
|
|
1029
|
+
TransferSpec: [
|
|
1030
|
+
{ name: "version", type: "uint32" },
|
|
1031
|
+
{ name: "sourceDomain", type: "uint32" },
|
|
1032
|
+
{ name: "destinationDomain", type: "uint32" },
|
|
1033
|
+
{ name: "sourceContract", type: "bytes32" },
|
|
1034
|
+
{ name: "destinationContract", type: "bytes32" },
|
|
1035
|
+
{ name: "sourceToken", type: "bytes32" },
|
|
1036
|
+
{ name: "destinationToken", type: "bytes32" },
|
|
1037
|
+
{ name: "sourceDepositor", type: "bytes32" },
|
|
1038
|
+
{ name: "destinationRecipient", type: "bytes32" },
|
|
1039
|
+
{ name: "sourceSigner", type: "bytes32" },
|
|
1040
|
+
{ name: "destinationCaller", type: "bytes32" },
|
|
1041
|
+
{ name: "value", type: "uint256" },
|
|
1042
|
+
{ name: "salt", type: "bytes32" },
|
|
1043
|
+
{ name: "hookData", type: "bytes" }
|
|
1044
|
+
],
|
|
1045
|
+
BurnIntent: [
|
|
1046
|
+
{ name: "maxBlockHeight", type: "uint256" },
|
|
1047
|
+
{ name: "maxFee", type: "uint256" },
|
|
1048
|
+
{ name: "spec", type: "TransferSpec" }
|
|
1049
|
+
]
|
|
1050
|
+
},
|
|
1051
|
+
primaryType: "BurnIntent",
|
|
1052
|
+
message: burnIntent
|
|
1053
|
+
});
|
|
1054
|
+
const apiUrl = this.isTestnet() ? GATEWAY_API_TESTNET : GATEWAY_API_MAINNET;
|
|
1055
|
+
const response = await fetch(`${apiUrl}/transfer`, {
|
|
1056
|
+
method: "POST",
|
|
1057
|
+
headers: { "Content-Type": "application/json" },
|
|
1058
|
+
body: JSON.stringify(
|
|
1059
|
+
[{ burnIntent, signature }],
|
|
1060
|
+
(_, v) => typeof v === "bigint" ? v.toString() : v
|
|
1061
|
+
)
|
|
1062
|
+
});
|
|
1063
|
+
const result = await response.json();
|
|
1064
|
+
if (result.success === false || result.error || !result.attestation || !result.signature) {
|
|
1065
|
+
throw new Error(
|
|
1066
|
+
`Gateway API error: ${result.message || result.error || JSON.stringify(result)}`
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
const destTransport = destConfig.rpcUrl ? http(destConfig.rpcUrl) : http();
|
|
1070
|
+
const destWalletClient = createWalletClient({
|
|
1071
|
+
account: this.account,
|
|
1072
|
+
chain: destConfig.chain,
|
|
1073
|
+
transport: destTransport
|
|
1074
|
+
});
|
|
1075
|
+
const destPublicClient = createPublicClient({
|
|
1076
|
+
chain: destConfig.chain,
|
|
1077
|
+
transport: destTransport
|
|
1078
|
+
});
|
|
1079
|
+
const mintTxHash = await destWalletClient.writeContract({
|
|
1080
|
+
address: destConfig.gatewayMinter,
|
|
1081
|
+
abi: GATEWAY_MINTER_ABI,
|
|
1082
|
+
functionName: "gatewayMint",
|
|
1083
|
+
args: [result.attestation, result.signature]
|
|
1084
|
+
});
|
|
1085
|
+
try {
|
|
1086
|
+
await destPublicClient.waitForTransactionReceipt({ hash: mintTxHash });
|
|
1087
|
+
} catch (err) {
|
|
1088
|
+
throw new Error(`Mint transaction failed: ${mintTxHash}`, { cause: err });
|
|
1089
|
+
}
|
|
1090
|
+
return {
|
|
1091
|
+
mintTxHash,
|
|
1092
|
+
amount: withdrawAmount,
|
|
1093
|
+
formattedAmount: amount,
|
|
1094
|
+
sourceChain: this.chainConfig.chain.name,
|
|
1095
|
+
destinationChain: destConfig.chain.name,
|
|
1096
|
+
recipient
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Transfer USDC to any supported chain (alias for withdraw with destination chain).
|
|
1101
|
+
* @deprecated Use `withdraw({ chain })` instead
|
|
1102
|
+
*/
|
|
1103
|
+
async transfer(amount, destinationChain, recipient) {
|
|
1104
|
+
return this.withdraw(amount, { chain: destinationChain, recipient });
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Create a burn intent for a transfer.
|
|
1108
|
+
*/
|
|
1109
|
+
createBurnIntent(from, to, value, recipient, maxFee) {
|
|
1110
|
+
const addressToBytes32 = (addr) => pad(addr.toLowerCase(), { size: 32 });
|
|
1111
|
+
return {
|
|
1112
|
+
maxBlockHeight: maxUint256,
|
|
1113
|
+
maxFee,
|
|
1114
|
+
spec: {
|
|
1115
|
+
version: 1,
|
|
1116
|
+
sourceDomain: from.domain,
|
|
1117
|
+
destinationDomain: to.domain,
|
|
1118
|
+
sourceContract: addressToBytes32(from.gatewayWallet),
|
|
1119
|
+
destinationContract: addressToBytes32(to.gatewayMinter),
|
|
1120
|
+
sourceToken: addressToBytes32(from.usdc),
|
|
1121
|
+
destinationToken: addressToBytes32(to.usdc),
|
|
1122
|
+
sourceDepositor: addressToBytes32(this.account.address),
|
|
1123
|
+
destinationRecipient: addressToBytes32(recipient),
|
|
1124
|
+
sourceSigner: addressToBytes32(this.account.address),
|
|
1125
|
+
destinationCaller: addressToBytes32(zeroAddress),
|
|
1126
|
+
value,
|
|
1127
|
+
salt: `0x${randomBytes(32).toString("hex")}`,
|
|
1128
|
+
hookData: "0x"
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Check if this client is connected to a testnet.
|
|
1134
|
+
*/
|
|
1135
|
+
isTestnet() {
|
|
1136
|
+
const testnetChains = [
|
|
1137
|
+
"arbitrumSepolia",
|
|
1138
|
+
"arcTestnet",
|
|
1139
|
+
"avalancheFuji",
|
|
1140
|
+
"baseSepolia",
|
|
1141
|
+
"sepolia",
|
|
1142
|
+
"hyperEvmTestnet",
|
|
1143
|
+
"seiAtlantic",
|
|
1144
|
+
"sonicTestnet",
|
|
1145
|
+
"worldChainSepolia"
|
|
1146
|
+
];
|
|
1147
|
+
return testnetChains.some(
|
|
1148
|
+
(name) => CHAIN_CONFIGS[name].domain === this.chainConfig.domain
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
// ============================================================================
|
|
1152
|
+
// TRUSTLESS WITHDRAWAL (Emergency use only)
|
|
1153
|
+
// ============================================================================
|
|
1154
|
+
/**
|
|
1155
|
+
* Get the trustless withdrawal delay in blocks (~7 days).
|
|
1156
|
+
*
|
|
1157
|
+
* NOTE: This is for the emergency trustless withdrawal flow only.
|
|
1158
|
+
* For normal withdrawals, use `transfer()` to the same chain.
|
|
1159
|
+
*
|
|
1160
|
+
* @returns Number of blocks to wait after initiating trustless withdrawal
|
|
1161
|
+
*/
|
|
1162
|
+
async getTrustlessWithdrawalDelay() {
|
|
1163
|
+
return this.publicClient.readContract({
|
|
1164
|
+
address: this.chainConfig.gatewayWallet,
|
|
1165
|
+
abi: GATEWAY_WALLET_ABI,
|
|
1166
|
+
functionName: "withdrawalDelay"
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Get the block at which a pending trustless withdrawal becomes available.
|
|
1171
|
+
*
|
|
1172
|
+
* @param address - Optional address to check (defaults to account address)
|
|
1173
|
+
* @returns Block number, or 0n if no pending withdrawal
|
|
1174
|
+
*/
|
|
1175
|
+
async getTrustlessWithdrawalBlock(address) {
|
|
1176
|
+
const target = address ?? this.account.address;
|
|
1177
|
+
return this.publicClient.readContract({
|
|
1178
|
+
address: this.chainConfig.gatewayWallet,
|
|
1179
|
+
abi: GATEWAY_WALLET_ABI,
|
|
1180
|
+
functionName: "withdrawalBlock",
|
|
1181
|
+
args: [this.chainConfig.usdc, target]
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Initiate a trustless withdrawal from the Gateway Wallet.
|
|
1186
|
+
*
|
|
1187
|
+
* ⚠️ **WARNING: This is for emergency use only!**
|
|
1188
|
+
*
|
|
1189
|
+
* Use this only when Circle's Gateway API is unavailable. For normal
|
|
1190
|
+
* withdrawals, use `transfer()` to the same chain - it's instant.
|
|
1191
|
+
*
|
|
1192
|
+
* After calling this, you must wait ~7 days (`withdrawalDelay` blocks)
|
|
1193
|
+
* before calling `completeTrustlessWithdrawal()`.
|
|
1194
|
+
*
|
|
1195
|
+
* @param amount - Amount of USDC to withdraw (as a decimal string)
|
|
1196
|
+
* @returns Withdrawal initiation result
|
|
1197
|
+
*/
|
|
1198
|
+
async initiateTrustlessWithdrawal(amount) {
|
|
1199
|
+
const withdrawAmount = parseUnits(amount, 6);
|
|
1200
|
+
const balance = await this.getBalance();
|
|
1201
|
+
if (balance.available < withdrawAmount) {
|
|
1202
|
+
throw new Error(
|
|
1203
|
+
`Insufficient available balance. Have: ${balance.formattedAvailable}, Need: ${amount}`
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
const txHash = await this.walletClient.writeContract({
|
|
1207
|
+
address: this.chainConfig.gatewayWallet,
|
|
1208
|
+
abi: GATEWAY_WALLET_ABI,
|
|
1209
|
+
functionName: "initiateWithdrawal",
|
|
1210
|
+
args: [this.chainConfig.usdc, withdrawAmount],
|
|
1211
|
+
gas: 100000n
|
|
1212
|
+
});
|
|
1213
|
+
try {
|
|
1214
|
+
await this.publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
1215
|
+
} catch (err) {
|
|
1216
|
+
throw new Error(`Trustless withdrawal initiation failed: ${txHash}`, {
|
|
1217
|
+
cause: err
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
const withdrawalBlock = await this.getTrustlessWithdrawalBlock();
|
|
1221
|
+
return {
|
|
1222
|
+
txHash,
|
|
1223
|
+
amount: withdrawAmount,
|
|
1224
|
+
formattedAmount: amount,
|
|
1225
|
+
withdrawalBlock
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Complete a trustless withdrawal after the ~7-day delay period.
|
|
1230
|
+
*
|
|
1231
|
+
* ⚠️ **WARNING: This is for emergency use only!**
|
|
1232
|
+
*
|
|
1233
|
+
* Use this only when Circle's Gateway API is unavailable. For normal
|
|
1234
|
+
* withdrawals, use `transfer()` to the same chain - it's instant.
|
|
1235
|
+
*
|
|
1236
|
+
* @returns Withdrawal result
|
|
1237
|
+
*/
|
|
1238
|
+
async completeTrustlessWithdrawal() {
|
|
1239
|
+
const balance = await this.getBalance();
|
|
1240
|
+
if (balance.withdrawable === 0n) {
|
|
1241
|
+
const withdrawalBlock = await this.getTrustlessWithdrawalBlock();
|
|
1242
|
+
const currentBlock = await this.publicClient.getBlockNumber();
|
|
1243
|
+
if (withdrawalBlock > 0n && currentBlock < withdrawalBlock) {
|
|
1244
|
+
throw new Error(
|
|
1245
|
+
`Trustless withdrawal not yet available. Current block: ${currentBlock}, Available at: ${withdrawalBlock}`
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
throw new Error(
|
|
1249
|
+
"No withdrawable balance. Call initiateTrustlessWithdrawal() first."
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
const withdrawAmount = balance.withdrawable;
|
|
1253
|
+
const txHash = await this.walletClient.writeContract({
|
|
1254
|
+
address: this.chainConfig.gatewayWallet,
|
|
1255
|
+
abi: GATEWAY_WALLET_ABI,
|
|
1256
|
+
functionName: "withdraw",
|
|
1257
|
+
args: [this.chainConfig.usdc],
|
|
1258
|
+
gas: 100000n
|
|
1259
|
+
});
|
|
1260
|
+
try {
|
|
1261
|
+
await this.publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
1262
|
+
} catch (err) {
|
|
1263
|
+
throw new Error(`Trustless withdrawal completion failed: ${txHash}`, {
|
|
1264
|
+
cause: err
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
return {
|
|
1268
|
+
txHash,
|
|
1269
|
+
amount: withdrawAmount,
|
|
1270
|
+
formattedAmount: formatUnits(withdrawAmount, 6)
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
export {
|
|
1275
|
+
BatchEvmScheme,
|
|
1276
|
+
CHAIN_CONFIGS,
|
|
1277
|
+
CompositeEvmScheme,
|
|
1278
|
+
GATEWAY_DOMAINS,
|
|
1279
|
+
GatewayClient,
|
|
1280
|
+
registerBatchScheme
|
|
1281
|
+
};
|
|
1282
|
+
//# sourceMappingURL=index.mjs.map
|