@x402x/client 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.
- package/LICENSE +190 -0
- package/README.md +712 -0
- package/dist/index.cjs +705 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +5126 -0
- package/dist/index.d.ts +5126 -0
- package/dist/index.js +676 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
import { getNetworkConfig, calculateCommitment, settle as settle$1, TransferHook, calculateFacilitatorFee } from '@x402x/core';
|
|
2
|
+
export { AmountError, formatDefaultAssetAmount, parseDefaultAssetAmount } from '@x402x/core';
|
|
3
|
+
import { getAddress, publicActions, isAddress } from 'viem';
|
|
4
|
+
import { signTypedData } from 'viem/actions';
|
|
5
|
+
import { useMemo, useState, useCallback } from 'react';
|
|
6
|
+
import { useWalletClient, useAccount, useChainId } from 'wagmi';
|
|
7
|
+
|
|
8
|
+
// src/client.ts
|
|
9
|
+
|
|
10
|
+
// src/errors.ts
|
|
11
|
+
var X402ClientError = class _X402ClientError extends Error {
|
|
12
|
+
constructor(message, code) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.name = "X402ClientError";
|
|
16
|
+
Object.setPrototypeOf(this, _X402ClientError.prototype);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var NetworkError = class _NetworkError extends X402ClientError {
|
|
20
|
+
constructor(message, code) {
|
|
21
|
+
super(message, code);
|
|
22
|
+
this.name = "NetworkError";
|
|
23
|
+
Object.setPrototypeOf(this, _NetworkError.prototype);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var SigningError = class _SigningError extends X402ClientError {
|
|
27
|
+
constructor(message, code) {
|
|
28
|
+
super(message, code);
|
|
29
|
+
this.name = "SigningError";
|
|
30
|
+
Object.setPrototypeOf(this, _SigningError.prototype);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var FacilitatorError = class _FacilitatorError extends X402ClientError {
|
|
34
|
+
constructor(message, code, statusCode, response) {
|
|
35
|
+
super(message, code);
|
|
36
|
+
this.statusCode = statusCode;
|
|
37
|
+
this.response = response;
|
|
38
|
+
this.name = "FacilitatorError";
|
|
39
|
+
Object.setPrototypeOf(this, _FacilitatorError.prototype);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var TransactionError = class _TransactionError extends X402ClientError {
|
|
43
|
+
constructor(message, code, txHash) {
|
|
44
|
+
super(message, code);
|
|
45
|
+
this.txHash = txHash;
|
|
46
|
+
this.name = "TransactionError";
|
|
47
|
+
Object.setPrototypeOf(this, _TransactionError.prototype);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var ValidationError = class _ValidationError extends X402ClientError {
|
|
51
|
+
constructor(message, code) {
|
|
52
|
+
super(message, code);
|
|
53
|
+
this.name = "ValidationError";
|
|
54
|
+
Object.setPrototypeOf(this, _ValidationError.prototype);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
function generateSalt() {
|
|
58
|
+
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
|
|
59
|
+
return `0x${Array.from(randomBytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
60
|
+
}
|
|
61
|
+
function normalizeAddress(address, name = "address") {
|
|
62
|
+
if (!address || typeof address !== "string") {
|
|
63
|
+
throw new ValidationError(`${name} is required`);
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return getAddress(address);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
throw new ValidationError(
|
|
69
|
+
`${name} is not a valid Ethereum address: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function validateAddress(address, name) {
|
|
74
|
+
if (!address || typeof address !== "string") {
|
|
75
|
+
throw new ValidationError(`${name} is required`);
|
|
76
|
+
}
|
|
77
|
+
if (!isAddress(address)) {
|
|
78
|
+
throw new ValidationError(`${name} must be a valid Ethereum address`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function validateHex(hex, name, expectedLength) {
|
|
82
|
+
if (!hex || typeof hex !== "string") {
|
|
83
|
+
throw new ValidationError(`${name} is required`);
|
|
84
|
+
}
|
|
85
|
+
if (!hex.startsWith("0x")) {
|
|
86
|
+
throw new ValidationError(`${name} must start with 0x`);
|
|
87
|
+
}
|
|
88
|
+
if (!/^0x[0-9a-fA-F]*$/.test(hex)) {
|
|
89
|
+
throw new ValidationError(`${name} must be a valid hex string`);
|
|
90
|
+
}
|
|
91
|
+
if (expectedLength !== void 0) {
|
|
92
|
+
const actualLength = (hex.length - 2) / 2;
|
|
93
|
+
if (actualLength !== expectedLength) {
|
|
94
|
+
throw new ValidationError(
|
|
95
|
+
`${name} must be ${expectedLength} bytes, got ${actualLength} bytes`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function validateAmount(amount, name) {
|
|
101
|
+
if (amount === null || amount === void 0 || amount === "") {
|
|
102
|
+
throw new ValidationError(`${name} is required`);
|
|
103
|
+
}
|
|
104
|
+
const amountStr = typeof amount === "number" ? amount.toString() : amount;
|
|
105
|
+
if (typeof amountStr !== "string" || amountStr.trim() === "") {
|
|
106
|
+
throw new ValidationError(`${name} must be a non-empty string`);
|
|
107
|
+
}
|
|
108
|
+
if (!/^\d+$/.test(amountStr)) {
|
|
109
|
+
throw new ValidationError(
|
|
110
|
+
`${name} must be a positive integer string (atomic units). Use parseDefaultAssetAmount() from @x402x/core to convert USD amounts.`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const atomicAmount = BigInt(amountStr);
|
|
115
|
+
if (atomicAmount < 0n) {
|
|
116
|
+
throw new ValidationError(`${name} cannot be negative`);
|
|
117
|
+
}
|
|
118
|
+
if (atomicAmount === 0n) {
|
|
119
|
+
throw new ValidationError(`${name} cannot be zero`);
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error instanceof ValidationError) {
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
throw new ValidationError(
|
|
126
|
+
`${name} is not a valid amount: ${error instanceof Error ? error.message : "Invalid format"}`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function formatFacilitatorUrl(url) {
|
|
131
|
+
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
132
|
+
}
|
|
133
|
+
function calculateTimeWindow(maxTimeoutSeconds = 300) {
|
|
134
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
135
|
+
return {
|
|
136
|
+
validAfter: (now - 600).toString(),
|
|
137
|
+
// 10 minutes before
|
|
138
|
+
validBefore: (now + maxTimeoutSeconds).toString()
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/core/prepare.ts
|
|
143
|
+
async function queryFacilitatorFee(facilitatorUrl, network, hook, hookData = "0x") {
|
|
144
|
+
try {
|
|
145
|
+
return await calculateFacilitatorFee(facilitatorUrl, network, hook, hookData);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error instanceof Error) {
|
|
148
|
+
throw new FacilitatorError(
|
|
149
|
+
`Failed to query facilitator fee: ${error.message}`,
|
|
150
|
+
"FEE_QUERY_FAILED"
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
throw new FacilitatorError("Failed to query facilitator fee: Unknown error", "UNKNOWN_ERROR");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function prepareSettlement(params) {
|
|
157
|
+
validateAddress(params.hook, "hook");
|
|
158
|
+
validateHex(params.hookData, "hookData");
|
|
159
|
+
validateAddress(params.payTo, "payTo");
|
|
160
|
+
validateAmount(params.amount, "amount");
|
|
161
|
+
const atomicAmount = params.amount;
|
|
162
|
+
if (params.customSalt) {
|
|
163
|
+
validateHex(params.customSalt, "customSalt", 32);
|
|
164
|
+
}
|
|
165
|
+
const from = params.wallet.account?.address;
|
|
166
|
+
if (!from) {
|
|
167
|
+
throw new ValidationError("Wallet client must have an account");
|
|
168
|
+
}
|
|
169
|
+
const networkConfig = params.networkConfig || getNetworkConfig(params.network);
|
|
170
|
+
if (!networkConfig) {
|
|
171
|
+
throw new NetworkError(
|
|
172
|
+
`Network '${params.network}' is not supported. Please provide custom networkConfig.`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
const asset = params.asset || networkConfig.defaultAsset.address;
|
|
176
|
+
validateAddress(asset, "asset");
|
|
177
|
+
const salt = params.customSalt || generateSalt();
|
|
178
|
+
let facilitatorFee = params.facilitatorFee || "0";
|
|
179
|
+
if (!params.facilitatorFee && params.facilitatorUrl) {
|
|
180
|
+
try {
|
|
181
|
+
const feeEstimate = await queryFacilitatorFee(
|
|
182
|
+
params.facilitatorUrl,
|
|
183
|
+
params.network,
|
|
184
|
+
params.hook,
|
|
185
|
+
params.hookData
|
|
186
|
+
// Pass hookData for accurate fee calculation
|
|
187
|
+
);
|
|
188
|
+
if (!feeEstimate.hookAllowed) {
|
|
189
|
+
throw new ValidationError(
|
|
190
|
+
`Hook ${params.hook} is not allowed on network ${params.network}.`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
facilitatorFee = feeEstimate.facilitatorFee;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.warn(
|
|
196
|
+
`[x402x] Failed to query facilitator fee, using 0. This may cause settlement to fail. Error: ${error instanceof Error ? error.message : "Unknown"}`
|
|
197
|
+
);
|
|
198
|
+
facilitatorFee = "0";
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const timeWindow = params.validAfter && params.validBefore ? { validAfter: params.validAfter, validBefore: params.validBefore } : calculateTimeWindow(300);
|
|
202
|
+
const totalAmount = (BigInt(atomicAmount) + BigInt(facilitatorFee)).toString();
|
|
203
|
+
const commitment = calculateCommitment({
|
|
204
|
+
chainId: networkConfig.chainId,
|
|
205
|
+
hub: networkConfig.settlementRouter,
|
|
206
|
+
asset,
|
|
207
|
+
from,
|
|
208
|
+
value: totalAmount,
|
|
209
|
+
validAfter: timeWindow.validAfter,
|
|
210
|
+
validBefore: timeWindow.validBefore,
|
|
211
|
+
salt,
|
|
212
|
+
payTo: params.payTo,
|
|
213
|
+
facilitatorFee,
|
|
214
|
+
hook: params.hook,
|
|
215
|
+
hookData: params.hookData
|
|
216
|
+
});
|
|
217
|
+
return {
|
|
218
|
+
network: params.network,
|
|
219
|
+
networkConfig,
|
|
220
|
+
asset,
|
|
221
|
+
from,
|
|
222
|
+
amount: atomicAmount,
|
|
223
|
+
validAfter: timeWindow.validAfter,
|
|
224
|
+
validBefore: timeWindow.validBefore,
|
|
225
|
+
salt,
|
|
226
|
+
payTo: params.payTo,
|
|
227
|
+
facilitatorFee,
|
|
228
|
+
hook: params.hook,
|
|
229
|
+
hookData: params.hookData,
|
|
230
|
+
commitment
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
var AUTHORIZATION_TYPES = {
|
|
234
|
+
TransferWithAuthorization: [
|
|
235
|
+
{ name: "from", type: "address" },
|
|
236
|
+
{ name: "to", type: "address" },
|
|
237
|
+
{ name: "value", type: "uint256" },
|
|
238
|
+
{ name: "validAfter", type: "uint256" },
|
|
239
|
+
{ name: "validBefore", type: "uint256" },
|
|
240
|
+
{ name: "nonce", type: "bytes32" }
|
|
241
|
+
]
|
|
242
|
+
};
|
|
243
|
+
async function signAuthorization(wallet, settlement) {
|
|
244
|
+
try {
|
|
245
|
+
if (!wallet.account) {
|
|
246
|
+
throw new SigningError("Wallet client must have an account");
|
|
247
|
+
}
|
|
248
|
+
const domain = {
|
|
249
|
+
name: settlement.networkConfig.defaultAsset.eip712.name,
|
|
250
|
+
version: settlement.networkConfig.defaultAsset.eip712.version,
|
|
251
|
+
chainId: settlement.networkConfig.chainId,
|
|
252
|
+
verifyingContract: getAddress(settlement.asset)
|
|
253
|
+
};
|
|
254
|
+
const totalAmount = BigInt(settlement.amount) + BigInt(settlement.facilitatorFee);
|
|
255
|
+
const message = {
|
|
256
|
+
from: getAddress(settlement.from),
|
|
257
|
+
to: getAddress(settlement.networkConfig.settlementRouter),
|
|
258
|
+
value: totalAmount,
|
|
259
|
+
validAfter: BigInt(settlement.validAfter),
|
|
260
|
+
validBefore: BigInt(settlement.validBefore),
|
|
261
|
+
nonce: settlement.commitment
|
|
262
|
+
// Use commitment as nonce
|
|
263
|
+
};
|
|
264
|
+
console.log("[x402x/client] EIP-712 Message for signing:", {
|
|
265
|
+
from: message.from,
|
|
266
|
+
to: message.to,
|
|
267
|
+
value: message.value.toString(),
|
|
268
|
+
validAfter: message.validAfter.toString(),
|
|
269
|
+
validBefore: message.validBefore.toString(),
|
|
270
|
+
nonce: message.nonce
|
|
271
|
+
});
|
|
272
|
+
const signature = await signTypedData(wallet, {
|
|
273
|
+
account: wallet.account,
|
|
274
|
+
domain,
|
|
275
|
+
types: AUTHORIZATION_TYPES,
|
|
276
|
+
primaryType: "TransferWithAuthorization",
|
|
277
|
+
message
|
|
278
|
+
});
|
|
279
|
+
const authorization = {
|
|
280
|
+
from: settlement.from,
|
|
281
|
+
to: settlement.networkConfig.settlementRouter,
|
|
282
|
+
value: totalAmount.toString(),
|
|
283
|
+
// Use total amount (business + fee)
|
|
284
|
+
validAfter: settlement.validAfter,
|
|
285
|
+
validBefore: settlement.validBefore,
|
|
286
|
+
nonce: settlement.commitment
|
|
287
|
+
};
|
|
288
|
+
return {
|
|
289
|
+
settlement,
|
|
290
|
+
signature,
|
|
291
|
+
authorization
|
|
292
|
+
};
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (error instanceof SigningError) {
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
if (error instanceof Error) {
|
|
298
|
+
if (error.message.includes("User rejected")) {
|
|
299
|
+
throw new SigningError("User rejected the signing request", "USER_REJECTED");
|
|
300
|
+
}
|
|
301
|
+
if (error.message.includes("account")) {
|
|
302
|
+
throw new SigningError("Wallet account not available", "NO_ACCOUNT");
|
|
303
|
+
}
|
|
304
|
+
throw new SigningError(`Failed to sign authorization: ${error.message}`, "SIGNING_FAILED");
|
|
305
|
+
}
|
|
306
|
+
throw new SigningError("Failed to sign authorization: Unknown error", "UNKNOWN_ERROR");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async function settle(facilitatorUrl, signed, timeout = 3e4) {
|
|
310
|
+
try {
|
|
311
|
+
const paymentPayload = {
|
|
312
|
+
x402Version: 1,
|
|
313
|
+
scheme: "exact",
|
|
314
|
+
network: signed.settlement.network,
|
|
315
|
+
// Network type compatibility
|
|
316
|
+
payload: {
|
|
317
|
+
signature: signed.signature,
|
|
318
|
+
authorization: {
|
|
319
|
+
from: signed.authorization.from,
|
|
320
|
+
to: signed.authorization.to,
|
|
321
|
+
value: signed.authorization.value,
|
|
322
|
+
validAfter: signed.authorization.validAfter,
|
|
323
|
+
validBefore: signed.authorization.validBefore,
|
|
324
|
+
nonce: signed.authorization.nonce
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
const paymentRequirements = {
|
|
329
|
+
scheme: "exact",
|
|
330
|
+
network: signed.settlement.network,
|
|
331
|
+
// Network type compatibility
|
|
332
|
+
maxAmountRequired: signed.settlement.amount,
|
|
333
|
+
asset: signed.settlement.asset,
|
|
334
|
+
payTo: signed.settlement.networkConfig.settlementRouter,
|
|
335
|
+
maxTimeoutSeconds: 300,
|
|
336
|
+
// 5 minutes
|
|
337
|
+
// Required by x402 protocol (even though not used in serverless mode)
|
|
338
|
+
// In the future, the x402 v2 will remove the resource field from the payment requirements
|
|
339
|
+
// (https://github.com/coinbase/x402/pull/446)
|
|
340
|
+
resource: "https://x402x.dev/serverless",
|
|
341
|
+
// Placeholder for serverless mode
|
|
342
|
+
description: "x402x Serverless Settlement",
|
|
343
|
+
mimeType: "application/json",
|
|
344
|
+
extra: {
|
|
345
|
+
name: signed.settlement.networkConfig.defaultAsset.eip712.name,
|
|
346
|
+
version: signed.settlement.networkConfig.defaultAsset.eip712.version,
|
|
347
|
+
settlementRouter: signed.settlement.networkConfig.settlementRouter,
|
|
348
|
+
salt: signed.settlement.salt,
|
|
349
|
+
payTo: signed.settlement.payTo,
|
|
350
|
+
facilitatorFee: signed.settlement.facilitatorFee,
|
|
351
|
+
hook: signed.settlement.hook,
|
|
352
|
+
hookData: signed.settlement.hookData
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
paymentPayload.paymentRequirements = paymentRequirements;
|
|
356
|
+
const result = await settle$1(facilitatorUrl, paymentPayload, paymentRequirements, timeout);
|
|
357
|
+
return {
|
|
358
|
+
success: result.success,
|
|
359
|
+
transaction: result.transaction,
|
|
360
|
+
network: result.network || signed.settlement.network,
|
|
361
|
+
payer: result.payer || signed.settlement.from,
|
|
362
|
+
errorReason: result.errorReason
|
|
363
|
+
};
|
|
364
|
+
} catch (error) {
|
|
365
|
+
if (error instanceof FacilitatorError) {
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
if (error instanceof Error) {
|
|
369
|
+
throw new FacilitatorError(`Failed to settle payment: ${error.message}`, "SETTLEMENT_ERROR");
|
|
370
|
+
}
|
|
371
|
+
throw new FacilitatorError("Failed to settle payment: Unknown error", "UNKNOWN_ERROR");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/client.ts
|
|
376
|
+
var DEFAULT_FACILITATOR_URL = "https://facilitator.x402x.dev";
|
|
377
|
+
var X402Client = class {
|
|
378
|
+
/**
|
|
379
|
+
* Create a new X402Client instance
|
|
380
|
+
*
|
|
381
|
+
* @param config - Client configuration
|
|
382
|
+
* @throws NetworkError if network is unsupported
|
|
383
|
+
* @throws ValidationError if configuration is invalid
|
|
384
|
+
*/
|
|
385
|
+
constructor(config) {
|
|
386
|
+
if (!config.wallet) {
|
|
387
|
+
throw new ValidationError("wallet is required");
|
|
388
|
+
}
|
|
389
|
+
if (!config.network) {
|
|
390
|
+
throw new ValidationError("network is required");
|
|
391
|
+
}
|
|
392
|
+
this.config = {
|
|
393
|
+
...config,
|
|
394
|
+
facilitatorUrl: config.facilitatorUrl || DEFAULT_FACILITATOR_URL,
|
|
395
|
+
timeout: config.timeout || 3e4,
|
|
396
|
+
confirmationTimeout: config.confirmationTimeout || 6e4
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Execute a settlement transaction
|
|
401
|
+
*
|
|
402
|
+
* This is the main method that orchestrates the entire settlement flow:
|
|
403
|
+
* 1. Validates parameters
|
|
404
|
+
* 2. Prepares settlement data (queries fee if needed)
|
|
405
|
+
* 3. Signs EIP-3009 authorization
|
|
406
|
+
* 4. Submits to facilitator
|
|
407
|
+
* 5. Optionally waits for transaction confirmation
|
|
408
|
+
*
|
|
409
|
+
* @param params - Execution parameters
|
|
410
|
+
* @param waitForConfirmation - Whether to wait for transaction confirmation (default: true)
|
|
411
|
+
* @returns Execution result with transaction hash and optional receipt
|
|
412
|
+
*
|
|
413
|
+
* @throws ValidationError if parameters are invalid
|
|
414
|
+
* @throws NetworkError if network is unsupported
|
|
415
|
+
* @throws SigningError if user rejects signing
|
|
416
|
+
* @throws FacilitatorError if facilitator request fails
|
|
417
|
+
* @throws TransactionError if transaction fails
|
|
418
|
+
*
|
|
419
|
+
* @example Simple transfer (uses TransferHook by default)
|
|
420
|
+
* ```typescript
|
|
421
|
+
* import { parseDefaultAssetAmount } from '@x402x/core';
|
|
422
|
+
*
|
|
423
|
+
* // Convert USD amount to atomic units
|
|
424
|
+
* const atomicAmount = parseDefaultAssetAmount('1', 'base-sepolia'); // '1000000'
|
|
425
|
+
*
|
|
426
|
+
* const result = await client.execute({
|
|
427
|
+
* amount: atomicAmount, // Must be atomic units
|
|
428
|
+
* payTo: '0x...'
|
|
429
|
+
* });
|
|
430
|
+
* console.log('Transaction:', result.txHash);
|
|
431
|
+
* ```
|
|
432
|
+
*
|
|
433
|
+
* @example Custom hook
|
|
434
|
+
* ```typescript
|
|
435
|
+
* import { parseDefaultAssetAmount } from '@x402x/core';
|
|
436
|
+
*
|
|
437
|
+
* const atomicAmount = parseDefaultAssetAmount('5', 'base-sepolia'); // '5000000'
|
|
438
|
+
*
|
|
439
|
+
* const result = await client.execute({
|
|
440
|
+
* hook: '0x...',
|
|
441
|
+
* hookData: '0x...',
|
|
442
|
+
* amount: atomicAmount, // Must be atomic units
|
|
443
|
+
* payTo: '0x...'
|
|
444
|
+
* });
|
|
445
|
+
* ```
|
|
446
|
+
*/
|
|
447
|
+
async execute(params, waitForConfirmation = false) {
|
|
448
|
+
const hook = params.hook ? normalizeAddress(params.hook, "hook") : TransferHook.getAddress(this.config.network);
|
|
449
|
+
const payTo = normalizeAddress(params.payTo, "payTo");
|
|
450
|
+
const hookData = params.hookData || "0x";
|
|
451
|
+
if (params.hookData) {
|
|
452
|
+
validateHex(params.hookData, "hookData");
|
|
453
|
+
}
|
|
454
|
+
validateAmount(params.amount, "amount");
|
|
455
|
+
const settlement = await prepareSettlement({
|
|
456
|
+
wallet: this.config.wallet,
|
|
457
|
+
network: this.config.network,
|
|
458
|
+
hook,
|
|
459
|
+
hookData,
|
|
460
|
+
asset: params.asset,
|
|
461
|
+
amount: params.amount,
|
|
462
|
+
payTo,
|
|
463
|
+
facilitatorFee: params.facilitatorFee,
|
|
464
|
+
customSalt: params.customSalt,
|
|
465
|
+
validAfter: params.validAfter,
|
|
466
|
+
validBefore: params.validBefore,
|
|
467
|
+
networkConfig: this.config.networkConfig,
|
|
468
|
+
facilitatorUrl: this.config.facilitatorUrl
|
|
469
|
+
});
|
|
470
|
+
const signed = await signAuthorization(this.config.wallet, settlement);
|
|
471
|
+
const settleResult = await settle(this.config.facilitatorUrl, signed, this.config.timeout);
|
|
472
|
+
let receipt;
|
|
473
|
+
if (waitForConfirmation) {
|
|
474
|
+
receipt = await this.waitForTransaction(settleResult.transaction);
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
txHash: settleResult.transaction,
|
|
478
|
+
network: settleResult.network,
|
|
479
|
+
payer: settleResult.payer,
|
|
480
|
+
receipt,
|
|
481
|
+
settlement
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Calculate facilitator fee for a hook with optional hook data
|
|
486
|
+
*
|
|
487
|
+
* Queries the facilitator for the recommended fee based on current gas prices
|
|
488
|
+
* and hook gas usage. The returned fee includes a safety margin to ensure
|
|
489
|
+
* settlement will succeed.
|
|
490
|
+
*
|
|
491
|
+
* @param hook - Hook contract address
|
|
492
|
+
* @param hookData - Optional encoded hook parameters (default: '0x')
|
|
493
|
+
* @returns Fee calculation result from facilitator
|
|
494
|
+
*
|
|
495
|
+
* @throws FacilitatorError if query fails
|
|
496
|
+
*
|
|
497
|
+
* @example
|
|
498
|
+
* ```typescript
|
|
499
|
+
* const fee = await client.calculateFee('0x...', '0x');
|
|
500
|
+
* console.log('Facilitator fee:', fee.facilitatorFee);
|
|
501
|
+
* console.log('Fee in USD:', fee.facilitatorFeeUSD);
|
|
502
|
+
* console.log('Valid for:', fee.validitySeconds, 'seconds');
|
|
503
|
+
* ```
|
|
504
|
+
*/
|
|
505
|
+
async calculateFee(hook, hookData = "0x") {
|
|
506
|
+
const normalizedHook = normalizeAddress(hook, "hook");
|
|
507
|
+
validateHex(hookData, "hookData");
|
|
508
|
+
try {
|
|
509
|
+
return await calculateFacilitatorFee(
|
|
510
|
+
this.config.facilitatorUrl,
|
|
511
|
+
this.config.network,
|
|
512
|
+
normalizedHook,
|
|
513
|
+
hookData
|
|
514
|
+
);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
if (error instanceof Error) {
|
|
517
|
+
throw new FacilitatorError(
|
|
518
|
+
`Failed to calculate facilitator fee: ${error.message}`,
|
|
519
|
+
"FEE_QUERY_FAILED"
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
throw new FacilitatorError(
|
|
523
|
+
"Failed to calculate facilitator fee: Unknown error",
|
|
524
|
+
"UNKNOWN_ERROR"
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Wait for transaction confirmation
|
|
530
|
+
*
|
|
531
|
+
* @param txHash - Transaction hash to wait for
|
|
532
|
+
* @returns Transaction receipt
|
|
533
|
+
*
|
|
534
|
+
* @throws TransactionError if transaction fails or times out
|
|
535
|
+
*
|
|
536
|
+
* @example
|
|
537
|
+
* ```typescript
|
|
538
|
+
* const receipt = await client.waitForTransaction('0x...');
|
|
539
|
+
* console.log('Status:', receipt.status);
|
|
540
|
+
* ```
|
|
541
|
+
*/
|
|
542
|
+
async waitForTransaction(txHash) {
|
|
543
|
+
const publicClient = this.config.wallet;
|
|
544
|
+
if (typeof publicClient.waitForTransactionReceipt !== "function") {
|
|
545
|
+
throw new Error(
|
|
546
|
+
"Wallet client does not support waitForTransactionReceipt. Please use a viem PublicClient or WalletClient with public actions."
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
const receipt = await publicClient.waitForTransactionReceipt({
|
|
551
|
+
hash: txHash,
|
|
552
|
+
timeout: this.config.confirmationTimeout
|
|
553
|
+
});
|
|
554
|
+
if (receipt.status !== "success") {
|
|
555
|
+
throw new Error(`Transaction failed with status: ${receipt.status}`);
|
|
556
|
+
}
|
|
557
|
+
return receipt;
|
|
558
|
+
} catch (error) {
|
|
559
|
+
if (error instanceof Error) {
|
|
560
|
+
throw new Error(`Failed to confirm transaction: ${error.message}`);
|
|
561
|
+
}
|
|
562
|
+
throw new Error("Failed to confirm transaction: Unknown error");
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Get the network name
|
|
567
|
+
*/
|
|
568
|
+
get network() {
|
|
569
|
+
return this.config.network;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Get the facilitator URL
|
|
573
|
+
*/
|
|
574
|
+
get facilitatorUrl() {
|
|
575
|
+
return this.config.facilitatorUrl;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Get the wallet client
|
|
579
|
+
*/
|
|
580
|
+
get wallet() {
|
|
581
|
+
return this.config.wallet;
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
var CHAIN_ID_TO_NETWORK = {
|
|
585
|
+
84532: "base-sepolia",
|
|
586
|
+
8453: "base",
|
|
587
|
+
196: "x-layer",
|
|
588
|
+
1952: "x-layer-testnet"
|
|
589
|
+
};
|
|
590
|
+
function useX402Client(config) {
|
|
591
|
+
const { data: walletClient } = useWalletClient();
|
|
592
|
+
const { isConnected } = useAccount();
|
|
593
|
+
const chainId = useChainId();
|
|
594
|
+
return useMemo(() => {
|
|
595
|
+
if (!isConnected || !walletClient) {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
const network = config?.network || CHAIN_ID_TO_NETWORK[chainId];
|
|
599
|
+
if (!network) {
|
|
600
|
+
console.warn(
|
|
601
|
+
`[x402x] Unknown chainId ${chainId}. Please provide network name explicitly in config.`
|
|
602
|
+
);
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
const extendedWallet = walletClient.extend(publicActions);
|
|
607
|
+
return new X402Client({
|
|
608
|
+
wallet: extendedWallet,
|
|
609
|
+
network,
|
|
610
|
+
facilitatorUrl: config?.facilitatorUrl,
|
|
611
|
+
networkConfig: config?.networkConfig,
|
|
612
|
+
timeout: config?.timeout,
|
|
613
|
+
confirmationTimeout: config?.confirmationTimeout
|
|
614
|
+
});
|
|
615
|
+
} catch (error) {
|
|
616
|
+
console.error("[x402x] Failed to create X402Client:", error);
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
}, [
|
|
620
|
+
isConnected,
|
|
621
|
+
walletClient,
|
|
622
|
+
chainId,
|
|
623
|
+
config?.network,
|
|
624
|
+
config?.facilitatorUrl,
|
|
625
|
+
config?.networkConfig,
|
|
626
|
+
config?.timeout,
|
|
627
|
+
config?.confirmationTimeout
|
|
628
|
+
]);
|
|
629
|
+
}
|
|
630
|
+
function useExecute(config) {
|
|
631
|
+
const client = useX402Client(config);
|
|
632
|
+
const [status, setStatus] = useState("idle");
|
|
633
|
+
const [error, setError] = useState(null);
|
|
634
|
+
const [result, setResult] = useState(null);
|
|
635
|
+
const execute = useCallback(
|
|
636
|
+
async (params, waitForConfirmation = true) => {
|
|
637
|
+
if (!client) {
|
|
638
|
+
throw new Error("X402Client not available. Please connect your wallet.");
|
|
639
|
+
}
|
|
640
|
+
setStatus("preparing");
|
|
641
|
+
setError(null);
|
|
642
|
+
setResult(null);
|
|
643
|
+
try {
|
|
644
|
+
const executeResult = await client.execute(params, waitForConfirmation);
|
|
645
|
+
setStatus("success");
|
|
646
|
+
setResult(executeResult);
|
|
647
|
+
return executeResult;
|
|
648
|
+
} catch (err) {
|
|
649
|
+
const error2 = err instanceof Error ? err : new Error("Unknown error");
|
|
650
|
+
setStatus("error");
|
|
651
|
+
setError(error2);
|
|
652
|
+
throw error2;
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
[client]
|
|
656
|
+
);
|
|
657
|
+
const reset = useCallback(() => {
|
|
658
|
+
setStatus("idle");
|
|
659
|
+
setError(null);
|
|
660
|
+
setResult(null);
|
|
661
|
+
}, []);
|
|
662
|
+
return {
|
|
663
|
+
execute,
|
|
664
|
+
status,
|
|
665
|
+
error,
|
|
666
|
+
result,
|
|
667
|
+
reset,
|
|
668
|
+
isExecuting: ["preparing", "signing", "submitting", "confirming"].includes(status),
|
|
669
|
+
isSuccess: status === "success",
|
|
670
|
+
isError: status === "error"
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
export { DEFAULT_FACILITATOR_URL, FacilitatorError, NetworkError, SigningError, TransactionError, ValidationError, X402Client, X402ClientError, calculateTimeWindow, formatFacilitatorUrl, generateSalt, normalizeAddress, prepareSettlement, settle, signAuthorization, useExecute, useX402Client };
|
|
675
|
+
//# sourceMappingURL=index.js.map
|
|
676
|
+
//# sourceMappingURL=index.js.map
|