Skip to content

Commit 8d17685

Browse files
[SDK] Refactor payment processing to use facilitator.accepts (#8344)
1 parent 8ef418c commit 8d17685

File tree

6 files changed

+181
-263
lines changed

6 files changed

+181
-263
lines changed
Lines changed: 14 additions & 228 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,18 @@
11
import type { Abi } from "abitype";
22
import { toFunctionSelector } from "viem/utils";
3-
import { ChainIdToNetwork, type Money, moneySchema } from "x402/types";
43
import { getCachedChain } from "../chains/utils.js";
54
import type { ThirdwebClient } from "../client/client.js";
65
import { resolveContractAbi } from "../contract/actions/resolve-abi.js";
76
import { getContract } from "../contract/contract.js";
87
import { isPermitSupported } from "../extensions/erc20/__generated__/IERC20Permit/write/permit.js";
98
import { isTransferWithAuthorizationSupported } from "../extensions/erc20/__generated__/USDC/write/transferWithAuthorization.js";
10-
import { getAddress } from "../utils/address.js";
11-
import { toUnits } from "../utils/units.js";
129
import { decodePayment } from "./encode.js";
13-
import type { ThirdwebX402Facilitator } from "./facilitator.js";
1410
import {
15-
networkToChainId,
11+
networkToCaip2ChainId,
1612
type RequestedPaymentPayload,
1713
type RequestedPaymentRequirements,
1814
} from "./schemas.js";
1915
import {
20-
type DefaultAsset,
2116
type ERC20TokenAmount,
2217
type PaymentArgs,
2318
type PaymentRequiredResult,
@@ -50,95 +45,24 @@ export async function decodePaymentRequest(
5045
method,
5146
paymentData,
5247
} = args;
53-
const {
54-
description,
55-
mimeType,
56-
maxTimeoutSeconds,
57-
inputSchema,
58-
outputSchema,
59-
errorMessages,
60-
discoverable,
61-
} = routeConfig;
48+
const { errorMessages } = routeConfig;
6249

63-
let chainId: number;
64-
try {
65-
chainId = networkToChainId(network);
66-
} catch (error) {
67-
return {
68-
status: 402,
69-
responseHeaders: { "Content-Type": "application/json" },
70-
responseBody: {
71-
x402Version,
72-
error:
73-
error instanceof Error
74-
? error.message
75-
: `Invalid network: ${network}`,
76-
accepts: [],
77-
},
78-
};
79-
}
80-
81-
const atomicAmountForAsset = await processPriceToAtomicAmount(
50+
const paymentRequirementsResult = await facilitator.accepts({
51+
resourceUrl,
52+
method,
53+
network,
8254
price,
83-
chainId,
84-
facilitator,
85-
);
86-
if ("error" in atomicAmountForAsset) {
87-
return {
88-
status: 402,
89-
responseHeaders: { "Content-Type": "application/json" },
90-
responseBody: {
91-
x402Version,
92-
error: atomicAmountForAsset.error,
93-
accepts: [],
94-
},
95-
};
96-
}
97-
const { maxAmountRequired, asset } = atomicAmountForAsset;
98-
99-
const paymentRequirements: RequestedPaymentRequirements[] = [];
100-
101-
const mappedNetwork = ChainIdToNetwork[chainId];
102-
paymentRequirements.push({
103-
scheme: "exact",
104-
network: mappedNetwork ? mappedNetwork : `eip155:${chainId}`,
105-
maxAmountRequired,
106-
resource: resourceUrl,
107-
description: description ?? "",
108-
mimeType: mimeType ?? "application/json",
109-
payTo: getAddress(facilitator.address), // always pay to the facilitator address first
110-
maxTimeoutSeconds: maxTimeoutSeconds ?? 86400,
111-
asset: getAddress(asset.address),
112-
outputSchema: {
113-
input: {
114-
type: "http",
115-
method,
116-
discoverable: discoverable ?? true,
117-
...inputSchema,
118-
},
119-
output: outputSchema,
120-
},
121-
extra: {
122-
recipientAddress: payTo, // input payTo is the final recipient address
123-
...((asset as ERC20TokenAmount["asset"]).eip712 ?? {}),
124-
},
55+
routeConfig,
56+
payTo,
12557
});
12658

127-
// Check for payment header
59+
// Check for payment header, if none, return the payment requirements
12860
if (!paymentData) {
129-
return {
130-
status: 402,
131-
responseHeaders: {
132-
"Content-Type": "application/json",
133-
},
134-
responseBody: {
135-
x402Version,
136-
error: errorMessages?.paymentRequired || "X-PAYMENT header is required",
137-
accepts: paymentRequirements,
138-
},
139-
};
61+
return paymentRequirementsResult;
14062
}
14163

64+
const paymentRequirements = paymentRequirementsResult.responseBody.accepts;
65+
14266
// decode b64 payment
14367
let decodedPayment: RequestedPaymentPayload;
14468
try {
@@ -163,8 +87,8 @@ export async function decodePaymentRequest(
16387
const selectedPaymentRequirements = paymentRequirements.find(
16488
(value) =>
16589
value.scheme === decodedPayment.scheme &&
166-
networkToChainId(value.network) ===
167-
networkToChainId(decodedPayment.network),
90+
networkToCaip2ChainId(value.network) ===
91+
networkToCaip2ChainId(decodedPayment.network),
16892
);
16993
if (!selectedPaymentRequirements) {
17094
return {
@@ -190,86 +114,6 @@ export async function decodePaymentRequest(
190114
};
191115
}
192116

193-
/**
194-
* Parses the amount from the given price
195-
*
196-
* @param price - The price to parse
197-
* @param network - The network to get the default asset for
198-
* @returns The parsed amount or an error message
199-
*/
200-
async function processPriceToAtomicAmount(
201-
price: Money | ERC20TokenAmount,
202-
chainId: number,
203-
facilitator: ThirdwebX402Facilitator,
204-
): Promise<
205-
{ maxAmountRequired: string; asset: DefaultAsset } | { error: string }
206-
> {
207-
// Handle USDC amount (string) or token amount (ERC20TokenAmount)
208-
let maxAmountRequired: string;
209-
let asset: DefaultAsset;
210-
211-
if (typeof price === "string" || typeof price === "number") {
212-
// USDC amount in dollars
213-
const parsedAmount = moneySchema.safeParse(price);
214-
if (!parsedAmount.success) {
215-
return {
216-
error: `Invalid price (price: ${price}). Must be in the form "$3.10", 0.10, "0.001", ${parsedAmount.error}`,
217-
};
218-
}
219-
const parsedUsdAmount = parsedAmount.data;
220-
const defaultAsset = await getDefaultAsset(chainId, facilitator);
221-
if (!defaultAsset) {
222-
return {
223-
error: `Unable to get default asset on chain ${chainId}. Please specify an asset in the payment requirements.`,
224-
};
225-
}
226-
asset = defaultAsset;
227-
maxAmountRequired = toUnits(
228-
parsedUsdAmount.toString(),
229-
defaultAsset.decimals,
230-
).toString();
231-
} else {
232-
// Token amount in atomic units
233-
maxAmountRequired = price.amount;
234-
const tokenExtras = await getOrDetectTokenExtras({
235-
facilitator,
236-
partialAsset: price.asset,
237-
chainId,
238-
});
239-
if (!tokenExtras) {
240-
return {
241-
error: `Unable to find token information for ${price.asset.address} on chain ${chainId}. Please specify the asset decimals and eip712 information in the asset options.`,
242-
};
243-
}
244-
asset = {
245-
address: price.asset.address,
246-
decimals: tokenExtras.decimals,
247-
eip712: {
248-
name: tokenExtras.name,
249-
version: tokenExtras.version,
250-
primaryType: tokenExtras.primaryType,
251-
},
252-
};
253-
}
254-
255-
return {
256-
maxAmountRequired,
257-
asset,
258-
};
259-
}
260-
261-
async function getDefaultAsset(
262-
chainId: number,
263-
facilitator: ThirdwebX402Facilitator,
264-
): Promise<DefaultAsset | undefined> {
265-
const supportedAssets = await facilitator.supported();
266-
const matchingAsset = supportedAssets.kinds.find(
267-
(supported) => networkToChainId(supported.network) === chainId,
268-
);
269-
const assetConfig = matchingAsset?.extra?.defaultAsset as DefaultAsset;
270-
return assetConfig;
271-
}
272-
273117
export async function getSupportedSignatureType(args: {
274118
client: ThirdwebClient;
275119
asset: string;
@@ -309,61 +153,3 @@ export async function getSupportedSignatureType(args: {
309153
}
310154
return undefined;
311155
}
312-
313-
async function getOrDetectTokenExtras(args: {
314-
facilitator: ThirdwebX402Facilitator;
315-
partialAsset: ERC20TokenAmount["asset"];
316-
chainId: number;
317-
}): Promise<
318-
| {
319-
name: string;
320-
version: string;
321-
decimals: number;
322-
primaryType: SupportedSignatureType;
323-
}
324-
| undefined
325-
> {
326-
const { facilitator, partialAsset, chainId } = args;
327-
if (
328-
partialAsset.eip712?.name &&
329-
partialAsset.eip712?.version &&
330-
partialAsset.decimals !== undefined
331-
) {
332-
return {
333-
name: partialAsset.eip712.name,
334-
version: partialAsset.eip712.version,
335-
decimals: partialAsset.decimals,
336-
primaryType: partialAsset.eip712.primaryType,
337-
};
338-
}
339-
// read from facilitator
340-
const response = await facilitator
341-
.supported({
342-
chainId,
343-
tokenAddress: partialAsset.address,
344-
})
345-
.catch(() => {
346-
return {
347-
kinds: [],
348-
};
349-
});
350-
351-
const exactScheme = response.kinds?.find((kind) => kind.scheme === "exact");
352-
if (!exactScheme) {
353-
return undefined;
354-
}
355-
const supportedAsset = exactScheme.extra?.supportedAssets?.find(
356-
(asset) =>
357-
asset.address.toLowerCase() === partialAsset.address.toLowerCase(),
358-
);
359-
if (!supportedAsset) {
360-
return undefined;
361-
}
362-
363-
return {
364-
name: supportedAsset.eip712.name,
365-
version: supportedAsset.eip712.version,
366-
decimals: supportedAsset.decimals,
367-
primaryType: supportedAsset.eip712.primaryType as SupportedSignatureType,
368-
};
369-
}

packages/thirdweb/src/x402/facilitator.ts

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import type { VerifyResponse } from "x402/types";
22
import type { ThirdwebClient } from "../client/client.js";
33
import { stringify } from "../utils/json.js";
44
import { withCache } from "../utils/promise/withCache.js";
5-
import type {
6-
FacilitatorSettleResponse,
7-
FacilitatorSupportedResponse,
8-
FacilitatorVerifyResponse,
9-
RequestedPaymentPayload,
10-
RequestedPaymentRequirements,
5+
import {
6+
type FacilitatorSettleResponse,
7+
type FacilitatorSupportedResponse,
8+
type FacilitatorVerifyResponse,
9+
networkToCaip2ChainId,
10+
type RequestedPaymentPayload,
11+
type RequestedPaymentRequirements,
1112
} from "./schemas.js";
13+
import type { PaymentArgs, PaymentRequiredResult } from "./types.js";
1214

1315
export type WaitUntil = "simulated" | "submitted" | "confirmed";
1416

@@ -46,6 +48,9 @@ export type ThirdwebX402Facilitator = {
4648
chainId: number;
4749
tokenAddress?: string;
4850
}) => Promise<FacilitatorSupportedResponse>;
51+
accepts: (
52+
args: Omit<PaymentArgs, "facilitator">,
53+
) => Promise<PaymentRequiredResult>;
4954
};
5055

5156
const DEFAULT_BASE_URL = "https://api.thirdweb.com/v1/payments/x402";
@@ -256,6 +261,42 @@ export function facilitator(
256261
},
257262
);
258263
},
264+
265+
async accepts(
266+
args: Omit<PaymentArgs, "facilitator">,
267+
): Promise<PaymentRequiredResult> {
268+
const url = config.baseUrl ?? DEFAULT_BASE_URL;
269+
let headers = { "Content-Type": "application/json" };
270+
const authHeaders = await facilitator.createAuthHeaders();
271+
headers = { ...headers, ...authHeaders.verify }; // same as verify
272+
const caip2ChainId = networkToCaip2ChainId(args.network);
273+
const res = await fetch(`${url}/accepts`, {
274+
method: "POST",
275+
headers,
276+
body: stringify({
277+
resourceUrl: args.resourceUrl,
278+
method: args.method,
279+
network: caip2ChainId,
280+
price: args.price,
281+
routeConfig: args.routeConfig,
282+
serverWalletAddress: facilitator.address,
283+
recipientAddress: args.payTo,
284+
}),
285+
});
286+
if (res.status !== 402) {
287+
throw new Error(
288+
`Failed to construct payment requirements: ${res.statusText} - ${await res.text()}`,
289+
);
290+
}
291+
return {
292+
status: res.status as 402,
293+
responseBody:
294+
(await res.json()) as PaymentRequiredResult["responseBody"],
295+
responseHeaders: {
296+
"Content-Type": "application/json",
297+
},
298+
};
299+
},
259300
};
260301

261302
return facilitator;

0 commit comments

Comments
 (0)