@faremeter/payment-evm 0.10.1 → 0.10.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/dist/src/exact/client.d.ts +24 -0
- package/dist/src/exact/client.d.ts.map +1 -0
- package/dist/src/exact/client.js +81 -0
- package/dist/src/exact/common.d.ts +18 -0
- package/dist/src/exact/common.d.ts.map +1 -0
- package/dist/src/exact/common.js +36 -0
- package/dist/src/exact/constants.d.ts +119 -0
- package/dist/src/exact/constants.d.ts.map +1 -0
- package/dist/src/exact/constants.js +86 -0
- package/dist/src/exact/facilitator.d.ts +10 -0
- package/dist/src/exact/facilitator.d.ts.map +1 -0
- package/dist/src/exact/facilitator.js +267 -0
- package/dist/src/exact/index.d.ts +4 -0
- package/dist/src/exact/index.d.ts.map +1 -0
- package/dist/src/exact/index.js +3 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PaymentHandler } from "@faremeter/types/client";
|
|
2
|
+
import { type AssetNameOrContractInfo } from "@faremeter/info/evm";
|
|
3
|
+
import type { Hex } from "viem";
|
|
4
|
+
interface WalletForPayment {
|
|
5
|
+
chain: {
|
|
6
|
+
id: number;
|
|
7
|
+
name: string;
|
|
8
|
+
};
|
|
9
|
+
address: Hex;
|
|
10
|
+
account: {
|
|
11
|
+
signTypedData: (params: {
|
|
12
|
+
domain: Record<string, unknown>;
|
|
13
|
+
types: Record<string, unknown>;
|
|
14
|
+
primaryType: string;
|
|
15
|
+
message: Record<string, unknown>;
|
|
16
|
+
}) => Promise<Hex>;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export type CreatePaymentHandlerOpts = {
|
|
20
|
+
asset?: AssetNameOrContractInfo;
|
|
21
|
+
};
|
|
22
|
+
export declare function createPaymentHandler(wallet: WalletForPayment, opts?: CreatePaymentHandlerOpts): PaymentHandler;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/exact/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,cAAc,EAGf,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAGL,KAAK,uBAAuB,EAC7B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,MAAM,CAAC;AAWhC,UAAU,gBAAgB;IACxB,KAAK,EAAE;QACL,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,OAAO,EAAE,GAAG,CAAC;IACb,OAAO,EAAE;QACP,aAAa,EAAE,CAAC,MAAM,EAAE;YACtB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAC/B,WAAW,EAAE,MAAM,CAAC;YACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;SAClC,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;KACpB,CAAC;CACH;AAED,MAAM,MAAM,wBAAwB,GAAG;IACrC,KAAK,CAAC,EAAE,uBAAuB,CAAC;CACjC,CAAC;AAEF,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,gBAAgB,EACxB,IAAI,GAAE,wBAA6B,GAClC,cAAc,CAkGhB"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { randomBytes } from "crypto";
|
|
2
|
+
import { lookupX402Network, findAssetInfo, } from "@faremeter/info/evm";
|
|
3
|
+
import { isAddress } from "viem";
|
|
4
|
+
import { type } from "arktype";
|
|
5
|
+
import { X402_EXACT_SCHEME, EIP712_TYPES, eip712Domain, } from "./constants.js";
|
|
6
|
+
export function createPaymentHandler(wallet, opts = {}) {
|
|
7
|
+
const x402Network = lookupX402Network(wallet.chain.id);
|
|
8
|
+
const assetInfo = findAssetInfo(x402Network, opts.asset ?? "USDC");
|
|
9
|
+
if (!assetInfo) {
|
|
10
|
+
throw new Error(`Couldn't look up USDC information on network '${x402Network}'`);
|
|
11
|
+
}
|
|
12
|
+
return async function handlePayment(context, accepts) {
|
|
13
|
+
const compatibleRequirements = accepts.filter((req) => req.scheme === X402_EXACT_SCHEME && req.network === x402Network);
|
|
14
|
+
return compatibleRequirements.map((requirements) => ({
|
|
15
|
+
requirements,
|
|
16
|
+
exec: async () => {
|
|
17
|
+
if (!isAddress(requirements.payTo)) {
|
|
18
|
+
throw new Error(`Invalid payTo address: ${requirements.payTo}`);
|
|
19
|
+
}
|
|
20
|
+
const payToAddress = requirements.payTo;
|
|
21
|
+
// Generate nonce for EIP-3009 authorization (32 bytes hex with 0x prefix)
|
|
22
|
+
const nonce = `0x${randomBytes(32).toString("hex")}`;
|
|
23
|
+
const now = Math.floor(Date.now() / 1000);
|
|
24
|
+
const validAfter = now - 60; // Valid from 60 seconds ago
|
|
25
|
+
const validBefore = now + requirements.maxTimeoutSeconds;
|
|
26
|
+
// Create the authorization parameters for EIP-3009
|
|
27
|
+
const authorization = {
|
|
28
|
+
from: wallet.address,
|
|
29
|
+
to: payToAddress,
|
|
30
|
+
value: requirements.maxAmountRequired, // String value of amount
|
|
31
|
+
validAfter: validAfter.toString(),
|
|
32
|
+
validBefore: validBefore.toString(),
|
|
33
|
+
nonce: nonce,
|
|
34
|
+
};
|
|
35
|
+
// Validate and extract EIP-712 domain parameters from requirements.extra
|
|
36
|
+
const extraResult = eip712Domain(requirements.extra ?? {});
|
|
37
|
+
if (extraResult instanceof type.errors) {
|
|
38
|
+
throw new Error(`Invalid EIP-712 domain parameters: ${extraResult.summary}`);
|
|
39
|
+
}
|
|
40
|
+
const verifyingContract = extraResult.verifyingContract ??
|
|
41
|
+
requirements.asset ??
|
|
42
|
+
assetInfo.address;
|
|
43
|
+
if (!isAddress(verifyingContract)) {
|
|
44
|
+
throw new Error(`Invalid verifying contract: ${verifyingContract}`);
|
|
45
|
+
}
|
|
46
|
+
const domain = {
|
|
47
|
+
name: extraResult.name ?? assetInfo.contractName,
|
|
48
|
+
version: extraResult.version ?? "2",
|
|
49
|
+
chainId: extraResult.chainId ?? wallet.chain.id,
|
|
50
|
+
verifyingContract,
|
|
51
|
+
};
|
|
52
|
+
const types = EIP712_TYPES;
|
|
53
|
+
// Message for EIP-712 signing (using BigInt for signing)
|
|
54
|
+
const message = {
|
|
55
|
+
from: wallet.address,
|
|
56
|
+
to: payToAddress,
|
|
57
|
+
value: BigInt(requirements.maxAmountRequired),
|
|
58
|
+
validAfter: BigInt(validAfter),
|
|
59
|
+
validBefore: BigInt(validBefore),
|
|
60
|
+
nonce: nonce,
|
|
61
|
+
};
|
|
62
|
+
// Sign the EIP-712 typed data
|
|
63
|
+
const signature = await wallet.account.signTypedData({
|
|
64
|
+
domain,
|
|
65
|
+
types,
|
|
66
|
+
primaryType: "TransferWithAuthorization",
|
|
67
|
+
message,
|
|
68
|
+
});
|
|
69
|
+
// Create the x402 exact scheme payload
|
|
70
|
+
const payload = {
|
|
71
|
+
signature: signature,
|
|
72
|
+
authorization: authorization,
|
|
73
|
+
};
|
|
74
|
+
// Return the EIP-3009 authorization payload
|
|
75
|
+
return {
|
|
76
|
+
payload,
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
}));
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PublicClient, Hex } from "viem";
|
|
2
|
+
export declare function generateDomain(publicClient: PublicClient, chainId: number, asset: Hex): Promise<{
|
|
3
|
+
name: string;
|
|
4
|
+
version: string;
|
|
5
|
+
chainId: number;
|
|
6
|
+
verifyingContract: `0x${string}`;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function generateForwarderDomain(chainId: number, domainInfo: {
|
|
9
|
+
version: string;
|
|
10
|
+
name: string;
|
|
11
|
+
verifyingContract: `0x${string}`;
|
|
12
|
+
}): {
|
|
13
|
+
chainId: number;
|
|
14
|
+
version: string;
|
|
15
|
+
name: string;
|
|
16
|
+
verifyingContract: `0x${string}`;
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=common.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/exact/common.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,MAAM,CAAC;AAI9C,wBAAsB,cAAc,CAClC,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,GAAG;;;;;GA8BX;AAED,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE;IACV,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,iBAAiB,EAAE,KAAK,MAAM,EAAE,CAAC;CAClC;;aAHU,MAAM;UACT,MAAM;uBACO,KAAK,MAAM,EAAE;EAOnC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { TRANSFER_WITH_AUTHORIZATION_ABI } from "./constants.js";
|
|
2
|
+
export async function generateDomain(publicClient, chainId, asset) {
|
|
3
|
+
// Read domain parameters from chain
|
|
4
|
+
let tokenName;
|
|
5
|
+
let tokenVersion;
|
|
6
|
+
try {
|
|
7
|
+
[tokenName, tokenVersion] = await Promise.all([
|
|
8
|
+
publicClient.readContract({
|
|
9
|
+
address: asset,
|
|
10
|
+
abi: TRANSFER_WITH_AUTHORIZATION_ABI,
|
|
11
|
+
functionName: "name",
|
|
12
|
+
}),
|
|
13
|
+
publicClient.readContract({
|
|
14
|
+
address: asset,
|
|
15
|
+
abi: TRANSFER_WITH_AUTHORIZATION_ABI,
|
|
16
|
+
functionName: "version",
|
|
17
|
+
}),
|
|
18
|
+
]);
|
|
19
|
+
}
|
|
20
|
+
catch (cause) {
|
|
21
|
+
throw new Error("Failed to read contract parameters", { cause });
|
|
22
|
+
}
|
|
23
|
+
const domain = {
|
|
24
|
+
name: tokenName,
|
|
25
|
+
version: tokenVersion,
|
|
26
|
+
chainId,
|
|
27
|
+
verifyingContract: asset,
|
|
28
|
+
};
|
|
29
|
+
return domain;
|
|
30
|
+
}
|
|
31
|
+
export function generateForwarderDomain(chainId, domainInfo) {
|
|
32
|
+
return {
|
|
33
|
+
...domainInfo,
|
|
34
|
+
chainId,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
export declare const X402_EXACT_SCHEME = "exact";
|
|
2
|
+
export declare const TRANSFER_WITH_AUTHORIZATION_ABI: readonly [{
|
|
3
|
+
readonly name: "transferWithAuthorization";
|
|
4
|
+
readonly type: "function";
|
|
5
|
+
readonly stateMutability: "nonpayable";
|
|
6
|
+
readonly inputs: readonly [{
|
|
7
|
+
readonly name: "from";
|
|
8
|
+
readonly type: "address";
|
|
9
|
+
}, {
|
|
10
|
+
readonly name: "to";
|
|
11
|
+
readonly type: "address";
|
|
12
|
+
}, {
|
|
13
|
+
readonly name: "value";
|
|
14
|
+
readonly type: "uint256";
|
|
15
|
+
}, {
|
|
16
|
+
readonly name: "validAfter";
|
|
17
|
+
readonly type: "uint256";
|
|
18
|
+
}, {
|
|
19
|
+
readonly name: "validBefore";
|
|
20
|
+
readonly type: "uint256";
|
|
21
|
+
}, {
|
|
22
|
+
readonly name: "nonce";
|
|
23
|
+
readonly type: "bytes32";
|
|
24
|
+
}, {
|
|
25
|
+
readonly name: "v";
|
|
26
|
+
readonly type: "uint8";
|
|
27
|
+
}, {
|
|
28
|
+
readonly name: "r";
|
|
29
|
+
readonly type: "bytes32";
|
|
30
|
+
}, {
|
|
31
|
+
readonly name: "s";
|
|
32
|
+
readonly type: "bytes32";
|
|
33
|
+
}];
|
|
34
|
+
readonly outputs: readonly [];
|
|
35
|
+
}, {
|
|
36
|
+
readonly name: "authorizationState";
|
|
37
|
+
readonly type: "function";
|
|
38
|
+
readonly stateMutability: "view";
|
|
39
|
+
readonly inputs: readonly [{
|
|
40
|
+
readonly name: "authorizer";
|
|
41
|
+
readonly type: "address";
|
|
42
|
+
}, {
|
|
43
|
+
readonly name: "nonce";
|
|
44
|
+
readonly type: "bytes32";
|
|
45
|
+
}];
|
|
46
|
+
readonly outputs: readonly [{
|
|
47
|
+
readonly name: "";
|
|
48
|
+
readonly type: "bool";
|
|
49
|
+
}];
|
|
50
|
+
}, {
|
|
51
|
+
readonly name: "name";
|
|
52
|
+
readonly type: "function";
|
|
53
|
+
readonly stateMutability: "view";
|
|
54
|
+
readonly inputs: readonly [];
|
|
55
|
+
readonly outputs: readonly [{
|
|
56
|
+
readonly name: "";
|
|
57
|
+
readonly type: "string";
|
|
58
|
+
}];
|
|
59
|
+
}, {
|
|
60
|
+
readonly name: "version";
|
|
61
|
+
readonly type: "function";
|
|
62
|
+
readonly stateMutability: "view";
|
|
63
|
+
readonly inputs: readonly [];
|
|
64
|
+
readonly outputs: readonly [{
|
|
65
|
+
readonly name: "";
|
|
66
|
+
readonly type: "string";
|
|
67
|
+
}];
|
|
68
|
+
}, {
|
|
69
|
+
readonly name: "DOMAIN_SEPARATOR";
|
|
70
|
+
readonly type: "function";
|
|
71
|
+
readonly stateMutability: "view";
|
|
72
|
+
readonly inputs: readonly [];
|
|
73
|
+
readonly outputs: readonly [{
|
|
74
|
+
readonly name: "";
|
|
75
|
+
readonly type: "bytes32";
|
|
76
|
+
}];
|
|
77
|
+
}];
|
|
78
|
+
export declare const EIP712_TYPES: {
|
|
79
|
+
readonly TransferWithAuthorization: readonly [{
|
|
80
|
+
readonly name: "from";
|
|
81
|
+
readonly type: "address";
|
|
82
|
+
}, {
|
|
83
|
+
readonly name: "to";
|
|
84
|
+
readonly type: "address";
|
|
85
|
+
}, {
|
|
86
|
+
readonly name: "value";
|
|
87
|
+
readonly type: "uint256";
|
|
88
|
+
}, {
|
|
89
|
+
readonly name: "validAfter";
|
|
90
|
+
readonly type: "uint256";
|
|
91
|
+
}, {
|
|
92
|
+
readonly name: "validBefore";
|
|
93
|
+
readonly type: "uint256";
|
|
94
|
+
}, {
|
|
95
|
+
readonly name: "nonce";
|
|
96
|
+
readonly type: "bytes32";
|
|
97
|
+
}];
|
|
98
|
+
};
|
|
99
|
+
export declare const x402ExactPayload: import("arktype/internal/methods/object.ts").ObjectType<{
|
|
100
|
+
signature: (In: string) => import("arktype").Out<`0x${string}`>;
|
|
101
|
+
authorization: {
|
|
102
|
+
from: (In: string) => import("arktype").Out<`0x${string}`>;
|
|
103
|
+
to: (In: string) => import("arktype").Out<`0x${string}`>;
|
|
104
|
+
value: string;
|
|
105
|
+
validAfter: string;
|
|
106
|
+
validBefore: string;
|
|
107
|
+
nonce: (In: string) => import("arktype").Out<`0x${string}`>;
|
|
108
|
+
};
|
|
109
|
+
}, {}>;
|
|
110
|
+
export type x402ExactPayload = typeof x402ExactPayload.infer;
|
|
111
|
+
export type eip3009Authorization = x402ExactPayload["authorization"];
|
|
112
|
+
export declare const eip712Domain: import("arktype/internal/methods/object.ts").ObjectType<{
|
|
113
|
+
name?: string;
|
|
114
|
+
version?: string;
|
|
115
|
+
chainId?: number;
|
|
116
|
+
verifyingContract?: string;
|
|
117
|
+
}, {}>;
|
|
118
|
+
export type eip712Domain = typeof eip712Domain.infer;
|
|
119
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/exact/constants.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,iBAAiB,UAAU,CAAC;AAEzC,eAAO,MAAM,+BAA+B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiDlC,CAAC;AAEX,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;CASf,CAAC;AAEX,eAAO,MAAM,gBAAgB;;;;;;;;;;MAU3B,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,OAAO,gBAAgB,CAAC,KAAK,CAAC;AAC7D,MAAM,MAAM,oBAAoB,GAAG,gBAAgB,CAAC,eAAe,CAAC,CAAC;AAErE,eAAO,MAAM,YAAY;;;;;MAKvB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,OAAO,YAAY,CAAC,KAAK,CAAC"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
import { toHex, isHex } from "viem";
|
|
3
|
+
const prefixedHexString = type("string").pipe.try((x) => {
|
|
4
|
+
if (isHex(x)) {
|
|
5
|
+
return x;
|
|
6
|
+
}
|
|
7
|
+
return toHex(x);
|
|
8
|
+
});
|
|
9
|
+
export const X402_EXACT_SCHEME = "exact";
|
|
10
|
+
export const TRANSFER_WITH_AUTHORIZATION_ABI = [
|
|
11
|
+
{
|
|
12
|
+
name: "transferWithAuthorization",
|
|
13
|
+
type: "function",
|
|
14
|
+
stateMutability: "nonpayable",
|
|
15
|
+
inputs: [
|
|
16
|
+
{ name: "from", type: "address" },
|
|
17
|
+
{ name: "to", type: "address" },
|
|
18
|
+
{ name: "value", type: "uint256" },
|
|
19
|
+
{ name: "validAfter", type: "uint256" },
|
|
20
|
+
{ name: "validBefore", type: "uint256" },
|
|
21
|
+
{ name: "nonce", type: "bytes32" },
|
|
22
|
+
{ name: "v", type: "uint8" },
|
|
23
|
+
{ name: "r", type: "bytes32" },
|
|
24
|
+
{ name: "s", type: "bytes32" },
|
|
25
|
+
],
|
|
26
|
+
outputs: [],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "authorizationState",
|
|
30
|
+
type: "function",
|
|
31
|
+
stateMutability: "view",
|
|
32
|
+
inputs: [
|
|
33
|
+
{ name: "authorizer", type: "address" },
|
|
34
|
+
{ name: "nonce", type: "bytes32" },
|
|
35
|
+
],
|
|
36
|
+
outputs: [{ name: "", type: "bool" }],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "name",
|
|
40
|
+
type: "function",
|
|
41
|
+
stateMutability: "view",
|
|
42
|
+
inputs: [],
|
|
43
|
+
outputs: [{ name: "", type: "string" }],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "version",
|
|
47
|
+
type: "function",
|
|
48
|
+
stateMutability: "view",
|
|
49
|
+
inputs: [],
|
|
50
|
+
outputs: [{ name: "", type: "string" }],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "DOMAIN_SEPARATOR",
|
|
54
|
+
type: "function",
|
|
55
|
+
stateMutability: "view",
|
|
56
|
+
inputs: [],
|
|
57
|
+
outputs: [{ name: "", type: "bytes32" }],
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
export const EIP712_TYPES = {
|
|
61
|
+
TransferWithAuthorization: [
|
|
62
|
+
{ name: "from", type: "address" },
|
|
63
|
+
{ name: "to", type: "address" },
|
|
64
|
+
{ name: "value", type: "uint256" },
|
|
65
|
+
{ name: "validAfter", type: "uint256" },
|
|
66
|
+
{ name: "validBefore", type: "uint256" },
|
|
67
|
+
{ name: "nonce", type: "bytes32" },
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
export const x402ExactPayload = type({
|
|
71
|
+
signature: prefixedHexString,
|
|
72
|
+
authorization: {
|
|
73
|
+
from: prefixedHexString,
|
|
74
|
+
to: prefixedHexString,
|
|
75
|
+
value: "string",
|
|
76
|
+
validAfter: "string",
|
|
77
|
+
validBefore: "string",
|
|
78
|
+
nonce: prefixedHexString,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
export const eip712Domain = type({
|
|
82
|
+
"name?": "string",
|
|
83
|
+
"version?": "string",
|
|
84
|
+
"chainId?": "number",
|
|
85
|
+
"verifyingContract?": "string",
|
|
86
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type FacilitatorHandler } from "@faremeter/types/facilitator";
|
|
2
|
+
import type { Chain, Transport } from "viem";
|
|
3
|
+
import { type KnownX402Network, type AssetNameOrContractInfo } from "@faremeter/info/evm";
|
|
4
|
+
type CreateFacilitatorHandlerOpts = {
|
|
5
|
+
network?: KnownX402Network;
|
|
6
|
+
transport?: Transport;
|
|
7
|
+
};
|
|
8
|
+
export declare function createFacilitatorHandler(chain: Chain, privateKey: string, assetNameOrInfo: AssetNameOrContractInfo, opts?: CreateFacilitatorHandlerOpts): Promise<FacilitatorHandler>;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=facilitator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"facilitator.d.ts","sourceRoot":"","sources":["../../../src/exact/facilitator.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAGvE,OAAO,KAAK,EAAgB,KAAK,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAY3D,OAAO,EAEL,KAAK,gBAAgB,EAErB,KAAK,uBAAuB,EAC7B,MAAM,qBAAqB,CAAC;AA8B7B,KAAK,4BAA4B,GAAG;IAClC,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB,CAAC;AACF,wBAAsB,wBAAwB,CAC5C,KAAK,EAAE,KAAK,EACZ,UAAU,EAAE,MAAM,EAClB,eAAe,EAAE,uBAAuB,EACxC,IAAI,GAAE,4BAAiC,GACtC,OAAO,CAAC,kBAAkB,CAAC,CA+S7B"}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { isValidationError, caseInsensitiveLiteral } from "@faremeter/types";
|
|
2
|
+
import { isPrivateKey } from "@faremeter/types/evm";
|
|
3
|
+
import {} from "@faremeter/types/facilitator";
|
|
4
|
+
import { type } from "arktype";
|
|
5
|
+
import { createPublicClient, createWalletClient, http, verifyTypedData, encodeFunctionData, isAddress, } from "viem";
|
|
6
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
7
|
+
import { lookupX402Network, findAssetInfo, } from "@faremeter/info/evm";
|
|
8
|
+
import { X402_EXACT_SCHEME, TRANSFER_WITH_AUTHORIZATION_ABI, EIP712_TYPES, x402ExactPayload, } from "./constants.js";
|
|
9
|
+
import { generateDomain, generateForwarderDomain } from "./common.js";
|
|
10
|
+
function errorResponse(msg) {
|
|
11
|
+
return {
|
|
12
|
+
success: false,
|
|
13
|
+
error: msg,
|
|
14
|
+
txHash: null,
|
|
15
|
+
networkId: null,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const usedNonces = new Set();
|
|
19
|
+
function parseSignature(signature) {
|
|
20
|
+
const sig = signature.slice(2); // Remove 0x
|
|
21
|
+
const r = `0x${sig.slice(0, 64)}`;
|
|
22
|
+
const s = `0x${sig.slice(64, 128)}`;
|
|
23
|
+
const v = parseInt(sig.slice(128, 130), 16);
|
|
24
|
+
return { v, r, s };
|
|
25
|
+
}
|
|
26
|
+
export async function createFacilitatorHandler(chain, privateKey, assetNameOrInfo, opts = {}) {
|
|
27
|
+
if (!isPrivateKey(privateKey)) {
|
|
28
|
+
throw new Error(`Invalid private key: ${privateKey}`);
|
|
29
|
+
}
|
|
30
|
+
const network = opts.network ?? lookupX402Network(chain.id);
|
|
31
|
+
const chainId = chain.id;
|
|
32
|
+
const assetInfo = findAssetInfo(network, assetNameOrInfo);
|
|
33
|
+
const asset = assetInfo.address;
|
|
34
|
+
const useForwarder = assetInfo.forwarder !== undefined && assetInfo.forwarderName !== undefined;
|
|
35
|
+
if (!isAddress(asset)) {
|
|
36
|
+
throw new Error(`Invalid asset address: ${asset}`);
|
|
37
|
+
}
|
|
38
|
+
const transport = opts.transport ?? http(chain.rpcUrls.default.http[0]);
|
|
39
|
+
const account = privateKeyToAccount(privateKey);
|
|
40
|
+
const publicClient = createPublicClient({
|
|
41
|
+
chain,
|
|
42
|
+
transport,
|
|
43
|
+
});
|
|
44
|
+
const walletClient = createWalletClient({
|
|
45
|
+
account,
|
|
46
|
+
chain,
|
|
47
|
+
transport,
|
|
48
|
+
});
|
|
49
|
+
let domain;
|
|
50
|
+
if (useForwarder) {
|
|
51
|
+
if (!assetInfo.forwarder) {
|
|
52
|
+
throw new Error("Missing Forwarding Contract");
|
|
53
|
+
}
|
|
54
|
+
if (!assetInfo.forwarderVersion) {
|
|
55
|
+
throw new Error("Missing Forwarding Version");
|
|
56
|
+
}
|
|
57
|
+
if (!assetInfo.forwarderName) {
|
|
58
|
+
throw new Error("Missing Forwarding Name");
|
|
59
|
+
}
|
|
60
|
+
domain = generateForwarderDomain(chainId, {
|
|
61
|
+
version: assetInfo.forwarderVersion,
|
|
62
|
+
name: assetInfo.forwarderName,
|
|
63
|
+
verifyingContract: assetInfo.forwarder,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
domain = await generateDomain(publicClient, chainId, asset);
|
|
68
|
+
if (domain.name != assetInfo.contractName) {
|
|
69
|
+
throw new Error(`On chain contract name (${domain.name}) doesn't match configured asset name (${assetInfo.contractName})`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const checkTuple = type({
|
|
73
|
+
scheme: caseInsensitiveLiteral(X402_EXACT_SCHEME),
|
|
74
|
+
network: caseInsensitiveLiteral(network),
|
|
75
|
+
});
|
|
76
|
+
const checkTupleAndAsset = checkTuple.and({
|
|
77
|
+
asset: caseInsensitiveLiteral(asset),
|
|
78
|
+
});
|
|
79
|
+
const getSupported = () => {
|
|
80
|
+
return [
|
|
81
|
+
Promise.resolve({
|
|
82
|
+
x402Version: 1,
|
|
83
|
+
network,
|
|
84
|
+
scheme: X402_EXACT_SCHEME,
|
|
85
|
+
}),
|
|
86
|
+
];
|
|
87
|
+
};
|
|
88
|
+
const getRequirements = async (req) => {
|
|
89
|
+
return req
|
|
90
|
+
.filter((x) => !isValidationError(checkTupleAndAsset(x)))
|
|
91
|
+
.map((x) => ({
|
|
92
|
+
...x,
|
|
93
|
+
asset,
|
|
94
|
+
maxTimeoutSeconds: 300,
|
|
95
|
+
// Provide EIP-712 domain parameters for client signing
|
|
96
|
+
extra: {
|
|
97
|
+
name: useForwarder ? assetInfo.forwarderName : assetInfo.contractName,
|
|
98
|
+
version: useForwarder ? assetInfo.forwarderVersion : "2",
|
|
99
|
+
chainId,
|
|
100
|
+
verifyingContract: useForwarder ? assetInfo.forwarder : asset,
|
|
101
|
+
},
|
|
102
|
+
}));
|
|
103
|
+
};
|
|
104
|
+
const handleSettle = async (requirements, payment) => {
|
|
105
|
+
const tupleMatches = checkTuple(payment);
|
|
106
|
+
if (isValidationError(tupleMatches)) {
|
|
107
|
+
return null; // Not for us, let another handler try
|
|
108
|
+
}
|
|
109
|
+
// For the exact scheme with EIP-3009, validate the authorization payload
|
|
110
|
+
const payloadResult = x402ExactPayload(payment.payload);
|
|
111
|
+
if (payloadResult instanceof type.errors) {
|
|
112
|
+
return errorResponse(`Invalid payload: ${payloadResult.summary}`);
|
|
113
|
+
}
|
|
114
|
+
const { authorization, signature } = payloadResult;
|
|
115
|
+
// Check if the payment is to the correct address
|
|
116
|
+
if (authorization.to.toLowerCase() !== requirements.payTo.toLowerCase()) {
|
|
117
|
+
return errorResponse("Payment authorized to wrong address");
|
|
118
|
+
}
|
|
119
|
+
// Check if the amount matches
|
|
120
|
+
if (authorization.value !== requirements.maxAmountRequired) {
|
|
121
|
+
return errorResponse("Incorrect payment amount");
|
|
122
|
+
}
|
|
123
|
+
// Check if the authorization is still valid (time-wise)
|
|
124
|
+
const now = Math.floor(Date.now() / 1000);
|
|
125
|
+
const validAfter = parseInt(authorization.validAfter);
|
|
126
|
+
const validBefore = parseInt(authorization.validBefore);
|
|
127
|
+
if (now < validAfter) {
|
|
128
|
+
return errorResponse("Authorization not yet valid");
|
|
129
|
+
}
|
|
130
|
+
if (now > validBefore) {
|
|
131
|
+
return errorResponse("Authorization expired");
|
|
132
|
+
}
|
|
133
|
+
// Verify the from address is valid
|
|
134
|
+
if (!isAddress(authorization.from)) {
|
|
135
|
+
return errorResponse("Invalid from address");
|
|
136
|
+
}
|
|
137
|
+
// Check nonce hasn't been used (local check)
|
|
138
|
+
const nonceKey = `${authorization.from}-${authorization.nonce}`;
|
|
139
|
+
if (usedNonces.has(nonceKey)) {
|
|
140
|
+
return errorResponse("Nonce already used");
|
|
141
|
+
}
|
|
142
|
+
// Check on-chain nonce status
|
|
143
|
+
let onChainUsed;
|
|
144
|
+
try {
|
|
145
|
+
onChainUsed = await publicClient.readContract({
|
|
146
|
+
address: assetInfo.forwarder ?? asset,
|
|
147
|
+
abi: TRANSFER_WITH_AUTHORIZATION_ABI,
|
|
148
|
+
functionName: "authorizationState",
|
|
149
|
+
args: [authorization.from, authorization.nonce],
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
throw new Error("Failed to check authorization status", { cause: error });
|
|
154
|
+
}
|
|
155
|
+
if (onChainUsed) {
|
|
156
|
+
return errorResponse("Authorization already used on-chain");
|
|
157
|
+
}
|
|
158
|
+
let domain;
|
|
159
|
+
if (useForwarder) {
|
|
160
|
+
if (!assetInfo.forwarderVersion ||
|
|
161
|
+
!assetInfo.forwarderName ||
|
|
162
|
+
!assetInfo.forwarder) {
|
|
163
|
+
throw new Error("Secondary Forwardign Information Missing");
|
|
164
|
+
}
|
|
165
|
+
domain = generateForwarderDomain(chainId, {
|
|
166
|
+
version: assetInfo.forwarderVersion,
|
|
167
|
+
name: assetInfo.forwarderName,
|
|
168
|
+
verifyingContract: assetInfo.forwarder,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
domain = await generateDomain(publicClient, chainId, asset);
|
|
173
|
+
}
|
|
174
|
+
const types = EIP712_TYPES;
|
|
175
|
+
const message = {
|
|
176
|
+
from: authorization.from,
|
|
177
|
+
to: authorization.to,
|
|
178
|
+
value: BigInt(authorization.value),
|
|
179
|
+
validAfter: BigInt(validAfter),
|
|
180
|
+
validBefore: BigInt(validBefore),
|
|
181
|
+
nonce: authorization.nonce,
|
|
182
|
+
};
|
|
183
|
+
// Verify the signature
|
|
184
|
+
let isValidSignature;
|
|
185
|
+
try {
|
|
186
|
+
isValidSignature = await verifyTypedData({
|
|
187
|
+
address: authorization.from,
|
|
188
|
+
domain,
|
|
189
|
+
types,
|
|
190
|
+
primaryType: "TransferWithAuthorization",
|
|
191
|
+
message,
|
|
192
|
+
signature: signature,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
catch (cause) {
|
|
196
|
+
throw new Error("Signature verification failed", { cause });
|
|
197
|
+
}
|
|
198
|
+
if (!isValidSignature) {
|
|
199
|
+
return errorResponse("Invalid signature");
|
|
200
|
+
}
|
|
201
|
+
// Verify contract supports EIP-712
|
|
202
|
+
try {
|
|
203
|
+
await publicClient.readContract({
|
|
204
|
+
address: useForwarder ? domain.verifyingContract : asset,
|
|
205
|
+
abi: TRANSFER_WITH_AUTHORIZATION_ABI,
|
|
206
|
+
functionName: "DOMAIN_SEPARATOR",
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
catch (cause) {
|
|
210
|
+
throw new Error("Contract does not support EIP-712", { cause });
|
|
211
|
+
}
|
|
212
|
+
const acct = walletClient.account;
|
|
213
|
+
if (!acct || acct.type !== "local") {
|
|
214
|
+
return errorResponse("Wallet client is not configured with a local account");
|
|
215
|
+
}
|
|
216
|
+
const { v, r, s } = parseSignature(signature);
|
|
217
|
+
const data = encodeFunctionData({
|
|
218
|
+
abi: TRANSFER_WITH_AUTHORIZATION_ABI,
|
|
219
|
+
functionName: "transferWithAuthorization",
|
|
220
|
+
args: [
|
|
221
|
+
authorization.from,
|
|
222
|
+
authorization.to,
|
|
223
|
+
BigInt(authorization.value),
|
|
224
|
+
BigInt(validAfter),
|
|
225
|
+
BigInt(validBefore),
|
|
226
|
+
authorization.nonce,
|
|
227
|
+
v,
|
|
228
|
+
r,
|
|
229
|
+
s,
|
|
230
|
+
],
|
|
231
|
+
});
|
|
232
|
+
// Build and send the transaction
|
|
233
|
+
try {
|
|
234
|
+
const request = await walletClient.prepareTransactionRequest({
|
|
235
|
+
to: useForwarder ? domain.verifyingContract : asset,
|
|
236
|
+
data,
|
|
237
|
+
account: acct,
|
|
238
|
+
chain: undefined,
|
|
239
|
+
});
|
|
240
|
+
const serializedTransaction = await walletClient.signTransaction(request);
|
|
241
|
+
const txHash = await publicClient.sendRawTransaction({
|
|
242
|
+
serializedTransaction,
|
|
243
|
+
});
|
|
244
|
+
const receipt = await publicClient.waitForTransactionReceipt({
|
|
245
|
+
hash: txHash,
|
|
246
|
+
});
|
|
247
|
+
if (receipt.status !== "success") {
|
|
248
|
+
return errorResponse("Transaction failed");
|
|
249
|
+
}
|
|
250
|
+
usedNonces.add(nonceKey);
|
|
251
|
+
return {
|
|
252
|
+
success: true,
|
|
253
|
+
error: null,
|
|
254
|
+
txHash,
|
|
255
|
+
networkId: chainId.toString(),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
catch (cause) {
|
|
259
|
+
throw new Error("Transaction execution failed", { cause });
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
return {
|
|
263
|
+
getSupported,
|
|
264
|
+
getRequirements,
|
|
265
|
+
handleSettle,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createPaymentHandler } from "./client.js";
|
|
2
|
+
export { createFacilitatorHandler } from "./facilitator.js";
|
|
3
|
+
export { X402_EXACT_SCHEME, TRANSFER_WITH_AUTHORIZATION_ABI, EIP712_TYPES, x402ExactPayload, eip712Domain, } from "./constants.js";
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/exact/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EACL,iBAAiB,EACjB,+BAA+B,EAC/B,YAAY,EACZ,gBAAgB,EAChB,YAAY,GACb,MAAM,gBAAgB,CAAC"}
|