@faremeter/payment-evm 0.1.1

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.
@@ -0,0 +1,225 @@
1
+ import { isValidationError, } from "@faremeter/types";
2
+ import { type } from "arktype";
3
+ import { verifyTypedData, encodeFunctionData, isAddress } from "viem";
4
+ import { baseSepolia } from "viem/chains";
5
+ import { X402_EXACT_SCHEME, BASE_SEPOLIA_NETWORK, BASE_SEPOLIA_CHAIN_ID, USDC_BASE_SEPOLIA, TRANSFER_WITH_AUTHORIZATION_ABI, EIP712_TYPES, x402ExactPayload, } from "./constants.js";
6
+ function errorResponse(msg) {
7
+ return {
8
+ success: false,
9
+ error: msg,
10
+ txHash: null,
11
+ networkId: null,
12
+ };
13
+ }
14
+ const usedNonces = new Set();
15
+ function parseSignature(signature) {
16
+ const sig = signature.slice(2); // Remove 0x
17
+ const r = `0x${sig.slice(0, 64)}`;
18
+ const s = `0x${sig.slice(64, 128)}`;
19
+ const v = parseInt(sig.slice(128, 130), 16);
20
+ return { v, r, s };
21
+ }
22
+ export function createFacilitatorHandler(network, publicClient, walletClient, receivingAddress, asset = USDC_BASE_SEPOLIA) {
23
+ if (network !== BASE_SEPOLIA_NETWORK) {
24
+ throw new Error(`Unsupported network: ${network}. Only base-sepolia is supported.`);
25
+ }
26
+ if (!isAddress(asset)) {
27
+ throw new Error(`Invalid asset address: ${asset}`);
28
+ }
29
+ const checkTuple = type({
30
+ scheme: `'${X402_EXACT_SCHEME}'`,
31
+ network: `'${network}'`,
32
+ });
33
+ const checkTupleAndAsset = checkTuple.and({ asset: `'${asset}'` });
34
+ const getRequirements = async (req) => {
35
+ return req
36
+ .filter((x) => !isValidationError(checkTupleAndAsset(x)))
37
+ .map((x) => ({
38
+ ...x,
39
+ asset,
40
+ maxTimeoutSeconds: 300,
41
+ payTo: receivingAddress,
42
+ // Provide EIP-712 domain parameters for client signing
43
+ extra: {
44
+ name: "USDC",
45
+ version: "2",
46
+ chainId: BASE_SEPOLIA_CHAIN_ID,
47
+ verifyingContract: asset,
48
+ },
49
+ }));
50
+ };
51
+ const handleSettle = async (requirements, payment) => {
52
+ const tupleMatches = checkTuple(payment);
53
+ if (isValidationError(tupleMatches)) {
54
+ return null; // Not for us, let another handler try
55
+ }
56
+ // For the exact scheme with EIP-3009, validate the authorization payload
57
+ const payloadResult = x402ExactPayload(payment.payload);
58
+ if (payloadResult instanceof type.errors) {
59
+ return errorResponse(`Invalid payload: ${payloadResult.summary}`);
60
+ }
61
+ const { authorization, signature } = payloadResult;
62
+ // Check if the payment is to the correct address
63
+ if (authorization.to.toLowerCase() !== receivingAddress.toLowerCase()) {
64
+ return errorResponse("Payment authorized to wrong address");
65
+ }
66
+ // Check if the amount matches
67
+ if (authorization.value !== requirements.maxAmountRequired) {
68
+ return errorResponse("Incorrect payment amount");
69
+ }
70
+ // Check if the authorization is still valid (time-wise)
71
+ const now = Math.floor(Date.now() / 1000);
72
+ const validAfter = parseInt(authorization.validAfter);
73
+ const validBefore = parseInt(authorization.validBefore);
74
+ if (now < validAfter) {
75
+ return errorResponse("Authorization not yet valid");
76
+ }
77
+ if (now > validBefore) {
78
+ return errorResponse("Authorization expired");
79
+ }
80
+ // Verify the from address is valid
81
+ if (!isAddress(authorization.from)) {
82
+ return errorResponse("Invalid from address");
83
+ }
84
+ // Check nonce hasn't been used (local check)
85
+ const nonceKey = `${authorization.from}-${authorization.nonce}`;
86
+ if (usedNonces.has(nonceKey)) {
87
+ return errorResponse("Nonce already used");
88
+ }
89
+ // Check on-chain nonce status
90
+ let onChainUsed;
91
+ try {
92
+ onChainUsed = await publicClient.readContract({
93
+ address: asset,
94
+ abi: TRANSFER_WITH_AUTHORIZATION_ABI,
95
+ functionName: "authorizationState",
96
+ args: [authorization.from, authorization.nonce],
97
+ });
98
+ }
99
+ catch (error) {
100
+ return errorResponse(`Failed to check authorization status: ${error instanceof Error ? error.message : "Unknown error"}`);
101
+ }
102
+ if (onChainUsed) {
103
+ return errorResponse("Authorization already used on-chain");
104
+ }
105
+ // Read domain parameters from chain
106
+ let tokenName;
107
+ let tokenVersion;
108
+ let chainId;
109
+ try {
110
+ [tokenName, tokenVersion, chainId] = await Promise.all([
111
+ publicClient.readContract({
112
+ address: asset,
113
+ abi: TRANSFER_WITH_AUTHORIZATION_ABI,
114
+ functionName: "name",
115
+ }),
116
+ publicClient.readContract({
117
+ address: asset,
118
+ abi: TRANSFER_WITH_AUTHORIZATION_ABI,
119
+ functionName: "version",
120
+ }),
121
+ publicClient.getChainId(),
122
+ ]);
123
+ }
124
+ catch (error) {
125
+ return errorResponse(`Failed to read contract parameters: ${error instanceof Error ? error.message : "Unknown error"}`);
126
+ }
127
+ const domain = {
128
+ name: tokenName,
129
+ version: tokenVersion ?? "2",
130
+ chainId,
131
+ verifyingContract: asset,
132
+ };
133
+ const types = EIP712_TYPES;
134
+ const message = {
135
+ from: authorization.from,
136
+ to: authorization.to,
137
+ value: BigInt(authorization.value),
138
+ validAfter: BigInt(validAfter),
139
+ validBefore: BigInt(validBefore),
140
+ nonce: authorization.nonce,
141
+ };
142
+ // Verify the signature
143
+ let isValidSignature;
144
+ try {
145
+ isValidSignature = await verifyTypedData({
146
+ address: authorization.from,
147
+ domain,
148
+ types,
149
+ primaryType: "TransferWithAuthorization",
150
+ message,
151
+ signature: signature,
152
+ });
153
+ }
154
+ catch (error) {
155
+ return errorResponse(`Signature verification failed: ${error instanceof Error ? error.message : "Unknown error"}`);
156
+ }
157
+ if (!isValidSignature) {
158
+ return errorResponse("Invalid signature");
159
+ }
160
+ // Verify contract supports EIP-712
161
+ try {
162
+ await publicClient.readContract({
163
+ address: asset,
164
+ abi: TRANSFER_WITH_AUTHORIZATION_ABI,
165
+ functionName: "DOMAIN_SEPARATOR",
166
+ });
167
+ }
168
+ catch (error) {
169
+ return errorResponse(`Contract does not support EIP-712: ${error instanceof Error ? error.message : "Unknown error"}`);
170
+ }
171
+ const acct = walletClient.account;
172
+ if (!acct || acct.type !== "local") {
173
+ return errorResponse("Wallet client is not configured with a local account");
174
+ }
175
+ const { v, r, s } = parseSignature(signature);
176
+ const data = encodeFunctionData({
177
+ abi: TRANSFER_WITH_AUTHORIZATION_ABI,
178
+ functionName: "transferWithAuthorization",
179
+ args: [
180
+ authorization.from,
181
+ authorization.to,
182
+ BigInt(authorization.value),
183
+ BigInt(validAfter),
184
+ BigInt(validBefore),
185
+ authorization.nonce,
186
+ v,
187
+ r,
188
+ s,
189
+ ],
190
+ });
191
+ // Build and send the transaction
192
+ try {
193
+ const request = await walletClient.prepareTransactionRequest({
194
+ to: asset,
195
+ data,
196
+ account: acct,
197
+ chain: baseSepolia,
198
+ });
199
+ const serializedTransaction = await walletClient.signTransaction(request);
200
+ const txHash = await publicClient.sendRawTransaction({
201
+ serializedTransaction,
202
+ });
203
+ const receipt = await publicClient.waitForTransactionReceipt({
204
+ hash: txHash,
205
+ });
206
+ if (receipt.status !== "success") {
207
+ return errorResponse("Transaction failed");
208
+ }
209
+ usedNonces.add(nonceKey);
210
+ return {
211
+ success: true,
212
+ error: null,
213
+ txHash,
214
+ networkId: BASE_SEPOLIA_CHAIN_ID.toString(),
215
+ };
216
+ }
217
+ catch (error) {
218
+ return errorResponse(`Transaction execution failed: ${error instanceof Error ? error.message : "Unknown error"}`);
219
+ }
220
+ };
221
+ return {
222
+ getRequirements,
223
+ handleSettle,
224
+ };
225
+ }
@@ -0,0 +1,4 @@
1
+ export { createPaymentHandler } from "./client.js";
2
+ export { createFacilitatorHandler } from "./facilitator.js";
3
+ export { X402_EXACT_SCHEME, BASE_SEPOLIA_NETWORK, BASE_SEPOLIA_CHAIN_ID, USDC_BASE_SEPOLIA, 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/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,oBAAoB,EACpB,qBAAqB,EACrB,iBAAiB,EACjB,+BAA+B,EAC/B,YAAY,EACZ,gBAAgB,EAChB,YAAY,GACb,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { createPaymentHandler } from "./client.js";
2
+ export { createFacilitatorHandler } from "./facilitator.js";
3
+ export { X402_EXACT_SCHEME, BASE_SEPOLIA_NETWORK, BASE_SEPOLIA_CHAIN_ID, USDC_BASE_SEPOLIA, TRANSFER_WITH_AUTHORIZATION_ABI, EIP712_TYPES, x402ExactPayload, eip712Domain, } from "./constants.js";