@agentpay-ai/shared 0.1.1 → 0.1.2
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 +2 -2
- package/package.json +3 -2
- package/src/account-admin.ts +8 -0
- package/src/balance.ts +4 -2
- package/src/chains.ts +39 -8
- package/src/index.ts +1 -0
- package/src/invoice.ts +15 -11
- package/src/payment-intent.ts +5 -3
- package/src/tokens.ts +31 -21
- package/src/wallet-setup.ts +8 -1
- package/src/x402-bazaar.ts +273 -0
- package/src/x402.ts +250 -2
package/README.md
CHANGED
|
@@ -7,8 +7,8 @@ Use this package when building AgentPay-compatible tools that need the same vali
|
|
|
7
7
|
## Included Modules
|
|
8
8
|
|
|
9
9
|
- Approval phrase parsing and payment intent types.
|
|
10
|
-
-
|
|
11
|
-
- Wallet setup, invoice, x402, account admin, and payment tracking schemas.
|
|
10
|
+
- X Layer stablecoin metadata and balance helpers.
|
|
11
|
+
- Wallet setup, invoice, x402, x402 Bazaar discovery, account admin, and payment tracking schemas, including x402 `PAYMENT-RESPONSE` and `payment-identifier` proof helpers.
|
|
12
12
|
- Route calldata hashing helpers used by guarded contract calls.
|
|
13
13
|
|
|
14
14
|
## Example
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentpay-ai/shared",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts"
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"src/payment-tracking.ts",
|
|
18
18
|
"src/tokens.ts",
|
|
19
19
|
"src/wallet-setup.ts",
|
|
20
|
-
"src/x402.ts"
|
|
20
|
+
"src/x402.ts",
|
|
21
|
+
"src/x402-bazaar.ts"
|
|
21
22
|
],
|
|
22
23
|
"publishConfig": {
|
|
23
24
|
"access": "public"
|
package/src/account-admin.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
import { networkSelectionShape } from "./chains.ts";
|
|
3
4
|
import { evmAddressSchema } from "./payment-intent.ts";
|
|
4
5
|
|
|
5
6
|
const positiveIntegerStringSchema = z.string().regex(/^[1-9]\d*$/, "Expected a positive integer string");
|
|
@@ -7,33 +8,40 @@ const positiveIntegerStringSchema = z.string().regex(/^[1-9]\d*$/, "Expected a p
|
|
|
7
8
|
export const prepareAccountAdminTransactionInputSchema = z.discriminatedUnion("action", [
|
|
8
9
|
z.object({
|
|
9
10
|
action: z.literal("PAUSE"),
|
|
11
|
+
...networkSelectionShape,
|
|
10
12
|
}),
|
|
11
13
|
z.object({
|
|
12
14
|
action: z.literal("UNPAUSE"),
|
|
15
|
+
...networkSelectionShape,
|
|
13
16
|
}),
|
|
14
17
|
z.object({
|
|
15
18
|
action: z.literal("SET_EXECUTOR"),
|
|
16
19
|
newExecutorAddress: evmAddressSchema,
|
|
20
|
+
...networkSelectionShape,
|
|
17
21
|
}),
|
|
18
22
|
z.object({
|
|
19
23
|
action: z.literal("CANCEL_NONCE"),
|
|
20
24
|
nonce: positiveIntegerStringSchema,
|
|
25
|
+
...networkSelectionShape,
|
|
21
26
|
}),
|
|
22
27
|
z.object({
|
|
23
28
|
action: z.literal("SET_ALLOWED_TOKEN"),
|
|
24
29
|
tokenAddress: evmAddressSchema,
|
|
25
30
|
allowed: z.boolean(),
|
|
31
|
+
...networkSelectionShape,
|
|
26
32
|
}),
|
|
27
33
|
z.object({
|
|
28
34
|
action: z.literal("WITHDRAW_NATIVE"),
|
|
29
35
|
toAddress: evmAddressSchema,
|
|
30
36
|
amountAtomic: positiveIntegerStringSchema,
|
|
37
|
+
...networkSelectionShape,
|
|
31
38
|
}),
|
|
32
39
|
z.object({
|
|
33
40
|
action: z.literal("WITHDRAW_TOKEN"),
|
|
34
41
|
tokenAddress: evmAddressSchema,
|
|
35
42
|
toAddress: evmAddressSchema,
|
|
36
43
|
amountAtomic: positiveIntegerStringSchema,
|
|
44
|
+
...networkSelectionShape,
|
|
37
45
|
}),
|
|
38
46
|
]);
|
|
39
47
|
|
package/src/balance.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { networkSelectionShape } from "./chains.ts";
|
|
4
|
+
import { DEFAULT_STABLE_TOKEN_SYMBOLS, stableTokenSymbolSchema } from "./tokens.ts";
|
|
4
5
|
|
|
5
6
|
export const getBalanceInputSchema = z.object({
|
|
6
|
-
tokenSymbols: z.array(stableTokenSymbolSchema).min(1).default([...
|
|
7
|
+
tokenSymbols: z.array(stableTokenSymbolSchema).min(1).default([...DEFAULT_STABLE_TOKEN_SYMBOLS]),
|
|
8
|
+
...networkSelectionShape,
|
|
7
9
|
});
|
|
8
10
|
|
|
9
11
|
export type GetBalanceInput = z.input<typeof getBalanceInputSchema>;
|
package/src/chains.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
1
3
|
export const SUPPORTED_CHAINS = {
|
|
2
|
-
|
|
3
|
-
id:
|
|
4
|
-
name: "
|
|
4
|
+
196: {
|
|
5
|
+
id: 196,
|
|
6
|
+
name: "X Layer",
|
|
5
7
|
nativeCurrency: {
|
|
6
|
-
symbol: "
|
|
8
|
+
symbol: "OKB",
|
|
7
9
|
decimals: 18,
|
|
8
10
|
},
|
|
9
11
|
},
|
|
10
|
-
|
|
11
|
-
id:
|
|
12
|
-
name: "
|
|
12
|
+
1952: {
|
|
13
|
+
id: 1952,
|
|
14
|
+
name: "X Layer Testnet",
|
|
13
15
|
nativeCurrency: {
|
|
14
|
-
symbol: "
|
|
16
|
+
symbol: "OKB",
|
|
15
17
|
decimals: 18,
|
|
16
18
|
},
|
|
17
19
|
},
|
|
@@ -28,6 +30,35 @@ export const SUPPORTED_CHAINS = {
|
|
|
28
30
|
export type SupportedChainId = keyof typeof SUPPORTED_CHAINS;
|
|
29
31
|
export type NativeCurrency = (typeof SUPPORTED_CHAINS)[SupportedChainId]["nativeCurrency"];
|
|
30
32
|
|
|
33
|
+
export const X_LAYER_NETWORK_CHAIN_IDS = {
|
|
34
|
+
mainnet: 196,
|
|
35
|
+
testnet: 1952,
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
export const xLayerNetworkSchema = z.enum(["mainnet", "testnet"]);
|
|
39
|
+
export const xLayerHomeChainIdSchema = z.union([z.literal(196), z.literal(1952)]);
|
|
40
|
+
export const networkSelectionShape = {
|
|
41
|
+
network: xLayerNetworkSchema.optional(),
|
|
42
|
+
homeChainId: xLayerHomeChainIdSchema.optional(),
|
|
43
|
+
} as const;
|
|
44
|
+
|
|
45
|
+
export type XLayerNetwork = z.infer<typeof xLayerNetworkSchema>;
|
|
46
|
+
export type XLayerHomeChainId = z.infer<typeof xLayerHomeChainIdSchema>;
|
|
47
|
+
export type NetworkSelectionInput = {
|
|
48
|
+
network?: XLayerNetwork;
|
|
49
|
+
homeChainId?: XLayerHomeChainId;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export function resolveXLayerHomeChainId(input: NetworkSelectionInput, fallbackHomeChainId: XLayerHomeChainId = 196): XLayerHomeChainId {
|
|
53
|
+
const networkHomeChainId = input.network ? X_LAYER_NETWORK_CHAIN_IDS[input.network] : undefined;
|
|
54
|
+
|
|
55
|
+
if (networkHomeChainId !== undefined && input.homeChainId !== undefined && networkHomeChainId !== input.homeChainId) {
|
|
56
|
+
throw new Error(`Network ${input.network} maps to chain ${networkHomeChainId}, but homeChainId ${input.homeChainId} was provided.`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return input.homeChainId ?? networkHomeChainId ?? fallbackHomeChainId;
|
|
60
|
+
}
|
|
61
|
+
|
|
31
62
|
export function getChainName(chainId: number): string {
|
|
32
63
|
return SUPPORTED_CHAINS[chainId as SupportedChainId]?.name ?? `Chain ${chainId}`;
|
|
33
64
|
}
|
package/src/index.ts
CHANGED
package/src/invoice.ts
CHANGED
|
@@ -2,11 +2,11 @@ import { z } from "zod";
|
|
|
2
2
|
|
|
3
3
|
import { getChainName } from "./chains.ts";
|
|
4
4
|
import { preparePaymentInputSchema } from "./payment-intent.ts";
|
|
5
|
-
import { stableTokenSymbolSchema } from "./tokens.ts";
|
|
5
|
+
import { stableTokenSymbolSchema, type StableTokenSymbol } from "./tokens.ts";
|
|
6
6
|
|
|
7
7
|
export const parseInvoicePaymentInputSchema = z.object({
|
|
8
8
|
invoice: z.string().trim().min(1),
|
|
9
|
-
sourceTokenSymbol: stableTokenSymbolSchema.default("
|
|
9
|
+
sourceTokenSymbol: stableTokenSymbolSchema.default("USDT0"),
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
export type ParseInvoicePaymentInput = z.input<typeof parseInvoicePaymentInputSchema>;
|
|
@@ -16,10 +16,10 @@ export interface ParsedInvoicePayment {
|
|
|
16
16
|
recipientAddress: string;
|
|
17
17
|
destinationChainId: number;
|
|
18
18
|
destinationChain: string;
|
|
19
|
-
destinationTokenSymbol:
|
|
19
|
+
destinationTokenSymbol: StableTokenSymbol;
|
|
20
20
|
amountOut: string;
|
|
21
21
|
purpose: string;
|
|
22
|
-
sourceTokenSymbol:
|
|
22
|
+
sourceTokenSymbol: StableTokenSymbol;
|
|
23
23
|
paymentType: "INVOICE_PAYMENT";
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -101,30 +101,30 @@ function parseDestinationChainId(value: string | undefined): number | undefined
|
|
|
101
101
|
|
|
102
102
|
const normalized = normalizeKey(value);
|
|
103
103
|
const knownChains: Record<string, number> = {
|
|
104
|
+
xlayer: 196,
|
|
105
|
+
xlayermainnet: 196,
|
|
106
|
+
xlayertestnet: 1952,
|
|
104
107
|
base: 8453,
|
|
105
|
-
bnbchain: 56,
|
|
106
|
-
bnb: 56,
|
|
107
|
-
bsc: 56,
|
|
108
108
|
};
|
|
109
109
|
|
|
110
110
|
return knownChains[normalized];
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
function parseToken(value: string | undefined):
|
|
113
|
+
function parseToken(value: string | undefined): StableTokenSymbol | undefined {
|
|
114
114
|
if (!value) {
|
|
115
115
|
return undefined;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
return stableTokenSymbolSchema.parse(value
|
|
118
|
+
return stableTokenSymbolSchema.parse(normalizeTokenSymbol(value));
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
function parsePaymentFields(candidate: {
|
|
122
122
|
recipientAddress: string | undefined;
|
|
123
123
|
destinationChainId: number | undefined;
|
|
124
|
-
destinationTokenSymbol:
|
|
124
|
+
destinationTokenSymbol: StableTokenSymbol | undefined;
|
|
125
125
|
amountOut: string | undefined;
|
|
126
126
|
purpose: string | undefined;
|
|
127
|
-
sourceTokenSymbol:
|
|
127
|
+
sourceTokenSymbol: StableTokenSymbol | undefined;
|
|
128
128
|
}) {
|
|
129
129
|
const parsed = preparePaymentInputSchema.safeParse(candidate);
|
|
130
130
|
|
|
@@ -142,3 +142,7 @@ function invoicePurpose(invoiceId: string | undefined): string | undefined {
|
|
|
142
142
|
function normalizeKey(value: string): string {
|
|
143
143
|
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
144
144
|
}
|
|
145
|
+
|
|
146
|
+
function normalizeTokenSymbol(value: string): string {
|
|
147
|
+
return value.trim().toUpperCase().replace("USD₮0", "USDT0");
|
|
148
|
+
}
|
package/src/payment-intent.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
|
2
2
|
import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
|
-
import { getChainName } from "./chains.ts";
|
|
5
|
+
import { getChainName, networkSelectionShape } from "./chains.ts";
|
|
6
6
|
import { getStableTokenMetadata, stableTokenSymbolSchema } from "./tokens.ts";
|
|
7
7
|
|
|
8
8
|
const addressPattern = /^0x[a-fA-F0-9]{40}$/;
|
|
@@ -44,8 +44,9 @@ export const preparePaymentInputSchema = z.object({
|
|
|
44
44
|
destinationTokenSymbol: stableTokenSymbolSchema,
|
|
45
45
|
amountOut: positiveDecimalStringSchema,
|
|
46
46
|
purpose: z.string().trim().min(1).max(280),
|
|
47
|
-
sourceTokenSymbol: stableTokenSymbolSchema.default("
|
|
47
|
+
sourceTokenSymbol: stableTokenSymbolSchema.default("USDT0"),
|
|
48
48
|
paymentType: stablecoinPaymentTypeSchema.default("WALLET_PAYMENT"),
|
|
49
|
+
...networkSelectionShape,
|
|
49
50
|
});
|
|
50
51
|
|
|
51
52
|
export const quotePaymentRouteInputSchema = preparePaymentInputSchema.omit({ purpose: true, paymentType: true });
|
|
@@ -66,10 +67,11 @@ export type ExecutePaymentInput = z.infer<typeof executePaymentInputSchema>;
|
|
|
66
67
|
export const prepareContractCallInputSchema = z.object({
|
|
67
68
|
targetAddress: evmAddressSchema,
|
|
68
69
|
callData: hexDataSchema.refine((value) => value !== "0x", "Expected non-empty calldata"),
|
|
69
|
-
sourceTokenSymbol: stableTokenSymbolSchema.default("
|
|
70
|
+
sourceTokenSymbol: stableTokenSymbolSchema.default("USDT0"),
|
|
70
71
|
maxTokenSpend: positiveDecimalStringSchema,
|
|
71
72
|
maxNativeFee: z.string().regex(/^(?:0|[1-9]\d*)$/, "Expected a non-negative integer string").default("0"),
|
|
72
73
|
purpose: z.string().trim().min(1).max(280),
|
|
74
|
+
...networkSelectionShape,
|
|
73
75
|
});
|
|
74
76
|
|
|
75
77
|
export type PrepareContractCallInput = z.input<typeof prepareContractCallInputSchema>;
|
package/src/tokens.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
export const STABLE_TOKEN_SYMBOLS = ["USDC", "USDT"] as const;
|
|
3
|
+
export const STABLE_TOKEN_SYMBOLS = ["USDT0", "USDC", "USDT"] as const;
|
|
4
|
+
export const DEFAULT_STABLE_TOKEN_SYMBOLS = ["USDT0", "USDC"] as const satisfies readonly StableTokenSymbol[];
|
|
4
5
|
|
|
5
6
|
export const stableTokenSymbolSchema = z.enum(STABLE_TOKEN_SYMBOLS);
|
|
6
7
|
|
|
7
8
|
export type StableTokenSymbol = z.infer<typeof stableTokenSymbolSchema>;
|
|
8
9
|
|
|
9
10
|
export const STABLE_TOKEN_DECIMALS: Record<StableTokenSymbol, number> = {
|
|
11
|
+
USDT0: 6,
|
|
10
12
|
USDC: 6,
|
|
11
13
|
USDT: 6,
|
|
12
14
|
};
|
|
@@ -23,31 +25,25 @@ export type StableTokenMetadataOverrides = Partial<
|
|
|
23
25
|
|
|
24
26
|
let configuredStableTokenMetadataOverrides: StableTokenMetadataOverrides = {};
|
|
25
27
|
|
|
26
|
-
export const STABLE_TOKENS_BY_CHAIN: Record<number, Record<StableTokenSymbol, StableTokenMetadata
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
symbol: "
|
|
30
|
-
address: "
|
|
31
|
-
decimals:
|
|
32
|
-
},
|
|
33
|
-
USDT: {
|
|
34
|
-
symbol: "USDT",
|
|
35
|
-
address: "0x55d398326f99059fF775485246999027B3197955",
|
|
36
|
-
decimals: 18,
|
|
28
|
+
export const STABLE_TOKENS_BY_CHAIN: Record<number, Partial<Record<StableTokenSymbol, StableTokenMetadata>>> = {
|
|
29
|
+
196: {
|
|
30
|
+
USDT0: {
|
|
31
|
+
symbol: "USDT0",
|
|
32
|
+
address: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
|
|
33
|
+
decimals: 6,
|
|
37
34
|
},
|
|
38
|
-
},
|
|
39
|
-
97: {
|
|
40
35
|
USDC: {
|
|
41
36
|
symbol: "USDC",
|
|
42
|
-
address: "
|
|
37
|
+
address: "0x74b7F16337b8972027F6196A17a631aC6dE26d22",
|
|
43
38
|
decimals: 6,
|
|
44
39
|
},
|
|
45
40
|
USDT: {
|
|
46
41
|
symbol: "USDT",
|
|
47
|
-
address: "
|
|
48
|
-
decimals:
|
|
42
|
+
address: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
|
|
43
|
+
decimals: 6,
|
|
49
44
|
},
|
|
50
45
|
},
|
|
46
|
+
1952: {},
|
|
51
47
|
8453: {
|
|
52
48
|
USDC: {
|
|
53
49
|
symbol: "USDC",
|
|
@@ -77,15 +73,18 @@ export function getStableTokenDecimals(symbol: string): number {
|
|
|
77
73
|
export function getStableTokenMetadata(chainId: number, symbol: string): StableTokenMetadata {
|
|
78
74
|
const parsedSymbol = stableTokenSymbolSchema.parse(symbol);
|
|
79
75
|
const metadata = STABLE_TOKENS_BY_CHAIN[chainId]?.[parsedSymbol];
|
|
76
|
+
const override = configuredStableTokenMetadataOverrides[chainId]?.[parsedSymbol];
|
|
80
77
|
|
|
81
|
-
if (!metadata) {
|
|
78
|
+
if (!metadata && !override?.address) {
|
|
82
79
|
throw new Error(`Unsupported stable token ${parsedSymbol} on chain ${chainId}.`);
|
|
83
80
|
}
|
|
84
81
|
|
|
85
|
-
const override = configuredStableTokenMetadataOverrides[chainId]?.[parsedSymbol];
|
|
86
|
-
|
|
87
82
|
return {
|
|
88
|
-
...metadata
|
|
83
|
+
...(metadata ?? {
|
|
84
|
+
symbol: parsedSymbol,
|
|
85
|
+
address: override?.address ?? "",
|
|
86
|
+
decimals: STABLE_TOKEN_DECIMALS[parsedSymbol],
|
|
87
|
+
}),
|
|
89
88
|
...override,
|
|
90
89
|
symbol: parsedSymbol,
|
|
91
90
|
};
|
|
@@ -98,3 +97,14 @@ export function getStableTokenAddress(chainId: number, symbol: string): string {
|
|
|
98
97
|
export function getStableTokenDecimalsForChain(chainId: number, symbol: string): number {
|
|
99
98
|
return getStableTokenMetadata(chainId, symbol).decimals;
|
|
100
99
|
}
|
|
100
|
+
|
|
101
|
+
export function getSupportedStableTokenMetadataForChain(chainId: number): StableTokenMetadata[] {
|
|
102
|
+
const staticTokens = STABLE_TOKENS_BY_CHAIN[chainId] ?? {};
|
|
103
|
+
const overrideTokens = configuredStableTokenMetadataOverrides[chainId] ?? {};
|
|
104
|
+
const symbols = new Set<StableTokenSymbol>([
|
|
105
|
+
...(Object.keys(staticTokens) as StableTokenSymbol[]),
|
|
106
|
+
...(Object.keys(overrideTokens) as StableTokenSymbol[]),
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
return [...symbols].map((symbol) => getStableTokenMetadata(chainId, symbol));
|
|
110
|
+
}
|
package/src/wallet-setup.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
import { networkSelectionShape } from "./chains.ts";
|
|
3
4
|
import { evmAddressSchema } from "./payment-intent.ts";
|
|
4
5
|
|
|
5
6
|
export const setupIntentStatusSchema = z.enum(["PENDING", "SIGNED", "DEPLOYING", "COMPLETED", "EXPIRED", "FAILED"]);
|
|
@@ -8,6 +9,7 @@ export type SetupIntentStatus = z.infer<typeof setupIntentStatusSchema>;
|
|
|
8
9
|
|
|
9
10
|
export const prepareWalletCreationInputSchema = z.object({
|
|
10
11
|
ownerAddress: evmAddressSchema.optional(),
|
|
12
|
+
...networkSelectionShape,
|
|
11
13
|
});
|
|
12
14
|
|
|
13
15
|
export type PrepareWalletCreationInput = z.input<typeof prepareWalletCreationInputSchema>;
|
|
@@ -18,19 +20,23 @@ export const checkWalletCreationInputSchema = z.object({
|
|
|
18
20
|
|
|
19
21
|
export type CheckWalletCreationInput = z.infer<typeof checkWalletCreationInputSchema>;
|
|
20
22
|
|
|
21
|
-
export const getAgentWalletInputSchema = z.object({
|
|
23
|
+
export const getAgentWalletInputSchema = z.object({
|
|
24
|
+
...networkSelectionShape,
|
|
25
|
+
});
|
|
22
26
|
|
|
23
27
|
export type GetAgentWalletInput = z.infer<typeof getAgentWalletInputSchema>;
|
|
24
28
|
|
|
25
29
|
export const prepareRouteTargetAllowanceInputSchema = z.object({
|
|
26
30
|
routeTarget: evmAddressSchema,
|
|
27
31
|
allowed: z.boolean().default(true),
|
|
32
|
+
...networkSelectionShape,
|
|
28
33
|
});
|
|
29
34
|
|
|
30
35
|
export type PrepareRouteTargetAllowanceInput = z.input<typeof prepareRouteTargetAllowanceInputSchema>;
|
|
31
36
|
|
|
32
37
|
export const checkRouteTargetAllowanceInputSchema = z.object({
|
|
33
38
|
routeTarget: evmAddressSchema,
|
|
39
|
+
...networkSelectionShape,
|
|
34
40
|
});
|
|
35
41
|
|
|
36
42
|
export type CheckRouteTargetAllowanceInput = z.infer<typeof checkRouteTargetAllowanceInputSchema>;
|
|
@@ -56,4 +62,5 @@ export interface SetupIntentRecord {
|
|
|
56
62
|
errorCode?: string;
|
|
57
63
|
errorMessage?: string;
|
|
58
64
|
completedAt?: string;
|
|
65
|
+
homeChainId?: number;
|
|
59
66
|
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const positiveIntegerStringSchema = z.string().regex(/^[1-9]\d*$/, "Expected a positive integer string");
|
|
4
|
+
const httpMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).default("GET");
|
|
5
|
+
const bazaarResourceTypeSchema = z.enum(["http", "mcp"]);
|
|
6
|
+
const scalarParameterSchema = z.union([z.string(), z.number(), z.boolean()]);
|
|
7
|
+
|
|
8
|
+
const x402BazaarPaymentRequirementSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
scheme: z.string(),
|
|
11
|
+
network: z.string(),
|
|
12
|
+
amount: positiveIntegerStringSchema,
|
|
13
|
+
asset: z.string(),
|
|
14
|
+
payTo: z.string(),
|
|
15
|
+
maxTimeoutSeconds: z.number().int().positive(),
|
|
16
|
+
extra: z.record(z.string(), z.unknown()).optional(),
|
|
17
|
+
})
|
|
18
|
+
.passthrough();
|
|
19
|
+
|
|
20
|
+
export const x402BazaarResourceSchema = z
|
|
21
|
+
.object({
|
|
22
|
+
resource: z.string().url(),
|
|
23
|
+
type: bazaarResourceTypeSchema,
|
|
24
|
+
x402Version: z.literal(2),
|
|
25
|
+
accepts: z.array(x402BazaarPaymentRequirementSchema).min(1),
|
|
26
|
+
lastUpdated: z.string().optional(),
|
|
27
|
+
description: z.string().optional(),
|
|
28
|
+
mimeType: z.string().optional(),
|
|
29
|
+
serviceName: z.string().optional(),
|
|
30
|
+
tags: z.array(z.string()).optional(),
|
|
31
|
+
iconUrl: z.string().url().optional(),
|
|
32
|
+
extensions: z.record(z.string(), z.unknown()).optional(),
|
|
33
|
+
})
|
|
34
|
+
.passthrough();
|
|
35
|
+
|
|
36
|
+
export const searchX402ServicesInputSchema = z.object({
|
|
37
|
+
query: z.string().trim().min(1),
|
|
38
|
+
type: bazaarResourceTypeSchema.default("http"),
|
|
39
|
+
network: z.string().trim().min(1).optional(),
|
|
40
|
+
limit: z.number().int().min(1).max(20).default(5),
|
|
41
|
+
cursor: z.string().trim().min(1).optional(),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const prepareX402ServiceRequestInputSchema = z.object({
|
|
45
|
+
resource: x402BazaarResourceSchema,
|
|
46
|
+
parameters: z.record(z.string(), scalarParameterSchema).default({}),
|
|
47
|
+
headers: z.record(z.string(), z.string()).default({}),
|
|
48
|
+
body: z.unknown().optional(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export type X402BazaarResource = z.output<typeof x402BazaarResourceSchema>;
|
|
52
|
+
export type X402BazaarPaymentRequirement = z.output<typeof x402BazaarPaymentRequirementSchema>;
|
|
53
|
+
export type SearchX402ServicesInput = z.input<typeof searchX402ServicesInputSchema>;
|
|
54
|
+
export type ParsedSearchX402ServicesInput = z.output<typeof searchX402ServicesInputSchema>;
|
|
55
|
+
export type PrepareX402ServiceRequestInput = z.input<typeof prepareX402ServiceRequestInputSchema>;
|
|
56
|
+
export type ParsedPrepareX402ServiceRequestInput = z.output<typeof prepareX402ServiceRequestInputSchema>;
|
|
57
|
+
|
|
58
|
+
export interface X402BazaarPaymentRequiredObject extends Record<string, unknown> {
|
|
59
|
+
x402Version: 2;
|
|
60
|
+
resource: {
|
|
61
|
+
url: string;
|
|
62
|
+
description?: string;
|
|
63
|
+
serviceName?: string;
|
|
64
|
+
mimeType?: string;
|
|
65
|
+
};
|
|
66
|
+
accepts: X402BazaarPaymentRequirement[];
|
|
67
|
+
extensions?: Record<string, unknown>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface BuiltX402BazaarHttpRequest {
|
|
71
|
+
status: "REQUEST_READY" | "NEEDS_INPUT";
|
|
72
|
+
request?: {
|
|
73
|
+
url: string;
|
|
74
|
+
method: z.output<typeof httpMethodSchema>;
|
|
75
|
+
headers: Record<string, string>;
|
|
76
|
+
body?: string;
|
|
77
|
+
};
|
|
78
|
+
paymentRequired?: X402BazaarPaymentRequiredObject;
|
|
79
|
+
missingParameters: string[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function normalizeX402BazaarResource(rawResource: unknown): X402BazaarResource {
|
|
83
|
+
return x402BazaarResourceSchema.parse(rawResource);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function buildX402BazaarHttpRequest(request: {
|
|
87
|
+
resource: X402BazaarResource;
|
|
88
|
+
parameters: Record<string, string | number | boolean>;
|
|
89
|
+
headers: Record<string, string>;
|
|
90
|
+
body?: unknown;
|
|
91
|
+
}): BuiltX402BazaarHttpRequest {
|
|
92
|
+
if (request.resource.type !== "http") {
|
|
93
|
+
throw new Error("prepare_x402_service_request currently supports Bazaar HTTP resources only.");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const input = getBazaarInput(request.resource);
|
|
97
|
+
const method = httpMethodSchema.parse(asString(input.method)?.toUpperCase() ?? "GET");
|
|
98
|
+
const missingParameters = new Set<string>();
|
|
99
|
+
const usedParameters = new Set<string>();
|
|
100
|
+
const routeTemplate = asString(input.routeTemplate) ?? request.resource.resource;
|
|
101
|
+
const templatedPath = applyPathParameters(routeTemplate, request.parameters, usedParameters, missingParameters);
|
|
102
|
+
const url = new URL(resolveRouteTemplate(templatedPath, request.resource.resource));
|
|
103
|
+
const queryParams = asRecord(input.queryParams);
|
|
104
|
+
|
|
105
|
+
for (const key of Object.keys(queryParams ?? {})) {
|
|
106
|
+
usedParameters.add(key);
|
|
107
|
+
if (!Object.hasOwn(request.parameters, key)) {
|
|
108
|
+
missingParameters.add(key);
|
|
109
|
+
} else {
|
|
110
|
+
url.searchParams.set(key, String(request.parameters[key]));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const key of getJsonSchemaRequiredKeys(input)) {
|
|
115
|
+
if (!Object.hasOwn(request.parameters, key) && request.body === undefined) {
|
|
116
|
+
missingParameters.add(key);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (missingParameters.size > 0) {
|
|
121
|
+
return {
|
|
122
|
+
status: "NEEDS_INPUT",
|
|
123
|
+
missingParameters: [...missingParameters].sort(),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const headers = { ...request.headers };
|
|
128
|
+
const body = createRequestBody(method, request.body, request.parameters, usedParameters, headers);
|
|
129
|
+
const requestUrl = url.toString();
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
status: "REQUEST_READY",
|
|
133
|
+
request: omitUndefined({
|
|
134
|
+
url: requestUrl,
|
|
135
|
+
method,
|
|
136
|
+
headers,
|
|
137
|
+
body,
|
|
138
|
+
}) as BuiltX402BazaarHttpRequest["request"],
|
|
139
|
+
paymentRequired: createPaymentRequiredObject(request.resource, requestUrl),
|
|
140
|
+
missingParameters: [],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function getX402BazaarRequiredParameters(resource: X402BazaarResource): string[] {
|
|
145
|
+
const input = getBazaarInput(resource);
|
|
146
|
+
const routeTemplate = asString(input.routeTemplate) ?? resource.resource;
|
|
147
|
+
return [...new Set([...getPathParameterNames(routeTemplate), ...Object.keys(asRecord(input.queryParams) ?? {}), ...getJsonSchemaRequiredKeys(input)])].sort();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function getX402BazaarHttpMethod(resource: X402BazaarResource): string | undefined {
|
|
151
|
+
const input = getBazaarInput(resource);
|
|
152
|
+
return asString(input.method)?.toUpperCase();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function createPaymentRequiredObject(resource: X402BazaarResource, requestUrl: string): X402BazaarPaymentRequiredObject {
|
|
156
|
+
return omitUndefined({
|
|
157
|
+
x402Version: 2,
|
|
158
|
+
resource: omitUndefined({
|
|
159
|
+
url: requestUrl,
|
|
160
|
+
description: resource.description,
|
|
161
|
+
serviceName: resource.serviceName,
|
|
162
|
+
mimeType: resource.mimeType,
|
|
163
|
+
}),
|
|
164
|
+
accepts: resource.accepts,
|
|
165
|
+
extensions: resource.extensions,
|
|
166
|
+
}) as X402BazaarPaymentRequiredObject;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function createRequestBody(
|
|
170
|
+
method: z.output<typeof httpMethodSchema>,
|
|
171
|
+
rawBody: unknown,
|
|
172
|
+
parameters: Record<string, string | number | boolean>,
|
|
173
|
+
usedParameters: Set<string>,
|
|
174
|
+
headers: Record<string, string>,
|
|
175
|
+
): string | undefined {
|
|
176
|
+
if (method === "GET" || method === "DELETE") {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (rawBody !== undefined) {
|
|
181
|
+
return typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const unusedParameters = Object.fromEntries(
|
|
185
|
+
Object.entries(parameters).filter(([key]) => !usedParameters.has(key)),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
if (Object.keys(unusedParameters).length === 0) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!hasHeader(headers, "content-type")) {
|
|
193
|
+
headers["Content-Type"] = "application/json";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return JSON.stringify(unusedParameters);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function applyPathParameters(
|
|
200
|
+
routeTemplate: string,
|
|
201
|
+
parameters: Record<string, string | number | boolean>,
|
|
202
|
+
usedParameters: Set<string>,
|
|
203
|
+
missingParameters: Set<string>,
|
|
204
|
+
): string {
|
|
205
|
+
return routeTemplate
|
|
206
|
+
.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, key: string) =>
|
|
207
|
+
replacePathParameter(key, parameters, usedParameters, missingParameters),
|
|
208
|
+
)
|
|
209
|
+
.replace(/\[([A-Za-z_][A-Za-z0-9_]*)\]/g, (_match, key: string) =>
|
|
210
|
+
replacePathParameter(key, parameters, usedParameters, missingParameters),
|
|
211
|
+
)
|
|
212
|
+
.replace(/\/:([A-Za-z_][A-Za-z0-9_]*)/g, (_match, key: string) =>
|
|
213
|
+
`/${replacePathParameter(key, parameters, usedParameters, missingParameters)}`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function replacePathParameter(
|
|
218
|
+
key: string,
|
|
219
|
+
parameters: Record<string, string | number | boolean>,
|
|
220
|
+
usedParameters: Set<string>,
|
|
221
|
+
missingParameters: Set<string>,
|
|
222
|
+
): string {
|
|
223
|
+
usedParameters.add(key);
|
|
224
|
+
if (!Object.hasOwn(parameters, key)) {
|
|
225
|
+
missingParameters.add(key);
|
|
226
|
+
return `{${key}}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return encodeURIComponent(String(parameters[key]));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function getPathParameterNames(routeTemplate: string): string[] {
|
|
233
|
+
return [
|
|
234
|
+
...routeTemplate.matchAll(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g),
|
|
235
|
+
...routeTemplate.matchAll(/\[([A-Za-z_][A-Za-z0-9_]*)\]/g),
|
|
236
|
+
...routeTemplate.matchAll(/\/:([A-Za-z_][A-Za-z0-9_]*)/g),
|
|
237
|
+
].map((match) => match[1]!);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function resolveRouteTemplate(routeTemplate: string, resourceUrl: string): string {
|
|
241
|
+
return new URL(routeTemplate, resourceUrl).toString();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function getJsonSchemaRequiredKeys(input: Record<string, unknown>): string[] {
|
|
245
|
+
const inputSchema = asRecord(input.inputSchema);
|
|
246
|
+
const required = inputSchema?.required;
|
|
247
|
+
return Array.isArray(required) ? required.filter((key): key is string => typeof key === "string") : [];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function getBazaarInput(resource: X402BazaarResource): Record<string, unknown> {
|
|
251
|
+
const extensions = asRecord(resource.extensions);
|
|
252
|
+
const bazaar = asRecord(extensions?.bazaar);
|
|
253
|
+
const info = asRecord(bazaar?.info);
|
|
254
|
+
return asRecord(info?.input) ?? {};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
258
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
259
|
+
? (value as Record<string, unknown>)
|
|
260
|
+
: undefined;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function asString(value: unknown): string | undefined {
|
|
264
|
+
return typeof value === "string" ? value : undefined;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function hasHeader(headers: Record<string, string>, headerName: string): boolean {
|
|
268
|
+
return Object.keys(headers).some((name) => name.toLowerCase() === headerName.toLowerCase());
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function omitUndefined<T extends Record<string, unknown>>(record: T): Partial<T> {
|
|
272
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined)) as Partial<T>;
|
|
273
|
+
}
|
package/src/x402.ts
CHANGED
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
3
|
+
import { bytesToHex } from "@noble/hashes/utils.js";
|
|
2
4
|
|
|
3
5
|
import { getChainName } from "./chains.ts";
|
|
4
|
-
import { evmAddressSchema, preparePaymentInputSchema } from "./payment-intent.ts";
|
|
6
|
+
import { evmAddressSchema, preparePaymentInputSchema, type PaymentIntentRecord } from "./payment-intent.ts";
|
|
5
7
|
import { getStableTokenMetadata, STABLE_TOKEN_SYMBOLS, stableTokenSymbolSchema } from "./tokens.ts";
|
|
6
8
|
import type { StableTokenSymbol } from "./tokens.ts";
|
|
7
9
|
|
|
8
10
|
const positiveIntegerStringSchema = z.string().regex(/^[1-9]\d*$/, "Expected a positive integer string");
|
|
11
|
+
const PAYMENT_IDENTIFIER = "payment-identifier";
|
|
12
|
+
const paymentIdentifierSchema = z.string().regex(/^[A-Za-z0-9_-]{16,128}$/);
|
|
9
13
|
|
|
10
14
|
export const parseX402PaymentRequiredInputSchema = z.object({
|
|
11
15
|
paymentRequired: z.union([z.string().trim().min(1), z.record(z.string(), z.unknown())]),
|
|
12
|
-
sourceTokenSymbol: stableTokenSymbolSchema.default("
|
|
16
|
+
sourceTokenSymbol: stableTokenSymbolSchema.default("USDT0"),
|
|
13
17
|
});
|
|
14
18
|
|
|
15
19
|
export type ParseX402PaymentRequiredInput = z.input<typeof parseX402PaymentRequiredInputSchema>;
|
|
16
20
|
|
|
21
|
+
const retryX402HttpMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).default("GET");
|
|
22
|
+
|
|
23
|
+
export const retryX402RequestInputSchema = z.object({
|
|
24
|
+
paymentRequired: parseX402PaymentRequiredInputSchema.shape.paymentRequired,
|
|
25
|
+
paymentIntentId: z.string().trim().min(1),
|
|
26
|
+
request: z
|
|
27
|
+
.object({
|
|
28
|
+
url: z.string().url().optional(),
|
|
29
|
+
method: retryX402HttpMethodSchema,
|
|
30
|
+
headers: z.record(z.string(), z.string()).default({}),
|
|
31
|
+
body: z.string().optional(),
|
|
32
|
+
})
|
|
33
|
+
.default({ method: "GET", headers: {} }),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export type RetryX402RequestInput = z.input<typeof retryX402RequestInputSchema>;
|
|
37
|
+
export type ParsedRetryX402RequestInput = z.output<typeof retryX402RequestInputSchema>;
|
|
38
|
+
|
|
17
39
|
const x402ResourceInfoSchema = z
|
|
18
40
|
.object({
|
|
19
41
|
url: z.string().url(),
|
|
@@ -75,13 +97,51 @@ export interface ParsedX402PaymentRequired {
|
|
|
75
97
|
sourceTokenSymbol: StableTokenSymbol;
|
|
76
98
|
paymentType: "X402_PAYMENT";
|
|
77
99
|
};
|
|
100
|
+
extensions?: {
|
|
101
|
+
"payment-identifier"?: {
|
|
102
|
+
info: {
|
|
103
|
+
required: boolean;
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
};
|
|
78
107
|
standardX402SignatureRequired: true;
|
|
79
108
|
}
|
|
80
109
|
|
|
110
|
+
export interface AgentPayX402PaymentProof {
|
|
111
|
+
x402Version: 2;
|
|
112
|
+
scheme: "agentpay-receipt";
|
|
113
|
+
paymentIntentId: string;
|
|
114
|
+
paymentType: "X402_PAYMENT";
|
|
115
|
+
payer: string;
|
|
116
|
+
ownerAddress: string;
|
|
117
|
+
resourceUrl: string;
|
|
118
|
+
requirementHash: string;
|
|
119
|
+
network: string;
|
|
120
|
+
chainId: number;
|
|
121
|
+
asset: string;
|
|
122
|
+
payTo: string;
|
|
123
|
+
amount: string;
|
|
124
|
+
amountDecimal: string;
|
|
125
|
+
sourceTxHash: string;
|
|
126
|
+
destinationTxHash?: string;
|
|
127
|
+
settlementTxHash: string;
|
|
128
|
+
completedAt?: string;
|
|
129
|
+
paymentIdentifier?: string;
|
|
130
|
+
extensions?: {
|
|
131
|
+
"payment-identifier"?: {
|
|
132
|
+
info: {
|
|
133
|
+
required: boolean;
|
|
134
|
+
id: string;
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
81
140
|
export function parseX402PaymentRequired(rawInput: ParseX402PaymentRequiredInput): ParsedX402PaymentRequired {
|
|
82
141
|
const input = parseX402PaymentRequiredInputSchema.parse(rawInput);
|
|
83
142
|
const paymentRequired = x402PaymentRequiredSchema.parse(decodePaymentRequired(input.paymentRequired));
|
|
84
143
|
const selected = paymentRequired.accepts.map(toSupportedRequirement).find((requirement) => requirement !== null);
|
|
144
|
+
const paymentIdentifierExtension = parsePaymentIdentifierExtension(paymentRequired.extensions);
|
|
85
145
|
|
|
86
146
|
if (!selected) {
|
|
87
147
|
throw new Error("No AgentPay-supported x402 payment requirement was found.");
|
|
@@ -117,10 +177,84 @@ export function parseX402PaymentRequired(rawInput: ParseX402PaymentRequiredInput
|
|
|
117
177
|
sourceTokenSymbol: paymentInput.sourceTokenSymbol,
|
|
118
178
|
paymentType: "X402_PAYMENT",
|
|
119
179
|
},
|
|
180
|
+
...(paymentIdentifierExtension
|
|
181
|
+
? {
|
|
182
|
+
extensions: {
|
|
183
|
+
[PAYMENT_IDENTIFIER]: paymentIdentifierExtension,
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
: {}),
|
|
120
187
|
standardX402SignatureRequired: true,
|
|
121
188
|
};
|
|
122
189
|
}
|
|
123
190
|
|
|
191
|
+
export function createAgentPayX402PaymentHeader(request: {
|
|
192
|
+
parsed: ParsedX402PaymentRequired;
|
|
193
|
+
paymentIntent: PaymentIntentRecord;
|
|
194
|
+
}): string {
|
|
195
|
+
return encodeBase64UrlJson(createAgentPayX402PaymentProof(request));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function decodeAgentPayX402PaymentHeader(header: string): AgentPayX402PaymentProof {
|
|
199
|
+
const decoded = JSON.parse(Buffer.from(header, "base64url").toString("utf8")) as unknown;
|
|
200
|
+
return agentPayX402PaymentProofSchema.parse(decoded);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function createAgentPayX402PaymentProof(request: {
|
|
204
|
+
parsed: ParsedX402PaymentRequired;
|
|
205
|
+
paymentIntent: PaymentIntentRecord;
|
|
206
|
+
}): AgentPayX402PaymentProof {
|
|
207
|
+
const { parsed, paymentIntent } = request;
|
|
208
|
+
const selected = parsed.selectedRequirement;
|
|
209
|
+
|
|
210
|
+
validateCompletedX402Intent(parsed, paymentIntent);
|
|
211
|
+
|
|
212
|
+
const settlementTxHash = paymentIntent.destinationTxHash ?? paymentIntent.sourceTxHash;
|
|
213
|
+
const paymentIdentifierExtension = parsed.extensions?.[PAYMENT_IDENTIFIER];
|
|
214
|
+
const paymentIdentifier = paymentIdentifierExtension ? createPaymentIdentifier(paymentIntent.id) : undefined;
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
x402Version: 2,
|
|
218
|
+
scheme: "agentpay-receipt",
|
|
219
|
+
paymentIntentId: paymentIntent.id,
|
|
220
|
+
paymentType: "X402_PAYMENT",
|
|
221
|
+
payer: paymentIntent.accountAddress,
|
|
222
|
+
ownerAddress: paymentIntent.ownerAddress,
|
|
223
|
+
resourceUrl: parsed.resource.url,
|
|
224
|
+
requirementHash: createX402RequirementHash(parsed),
|
|
225
|
+
network: selected.network,
|
|
226
|
+
chainId: selected.chainId,
|
|
227
|
+
asset: selected.asset,
|
|
228
|
+
payTo: selected.payTo,
|
|
229
|
+
amount: selected.amountAtomic,
|
|
230
|
+
amountDecimal: selected.amount,
|
|
231
|
+
sourceTxHash: paymentIntent.sourceTxHash!,
|
|
232
|
+
...(paymentIntent.destinationTxHash ? { destinationTxHash: paymentIntent.destinationTxHash } : {}),
|
|
233
|
+
settlementTxHash: settlementTxHash!,
|
|
234
|
+
...(paymentIntent.completedAt ? { completedAt: paymentIntent.completedAt } : {}),
|
|
235
|
+
...(paymentIdentifier
|
|
236
|
+
? {
|
|
237
|
+
paymentIdentifier,
|
|
238
|
+
extensions: {
|
|
239
|
+
[PAYMENT_IDENTIFIER]: {
|
|
240
|
+
info: {
|
|
241
|
+
required: paymentIdentifierExtension!.info.required,
|
|
242
|
+
id: paymentIdentifier,
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
}
|
|
247
|
+
: {}),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function createX402RequirementHash(parsed: ParsedX402PaymentRequired): string {
|
|
252
|
+
return `0x${bytesToHex(keccak_256(new TextEncoder().encode(stableStringify({
|
|
253
|
+
resourceUrl: parsed.resource.url,
|
|
254
|
+
selectedRequirement: parsed.selectedRequirement,
|
|
255
|
+
}))))}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
124
258
|
function decodePaymentRequired(paymentRequired: string | Record<string, unknown>): unknown {
|
|
125
259
|
if (typeof paymentRequired !== "string") {
|
|
126
260
|
return paymentRequired;
|
|
@@ -132,6 +266,104 @@ function decodePaymentRequired(paymentRequired: string | Record<string, unknown>
|
|
|
132
266
|
return JSON.parse(json) as unknown;
|
|
133
267
|
}
|
|
134
268
|
|
|
269
|
+
const agentPayX402PaymentProofSchema = z.object({
|
|
270
|
+
x402Version: z.literal(2),
|
|
271
|
+
scheme: z.literal("agentpay-receipt"),
|
|
272
|
+
paymentIntentId: z.string().min(1),
|
|
273
|
+
paymentType: z.literal("X402_PAYMENT"),
|
|
274
|
+
payer: evmAddressSchema,
|
|
275
|
+
ownerAddress: evmAddressSchema,
|
|
276
|
+
resourceUrl: z.string().url(),
|
|
277
|
+
requirementHash: z.string().regex(/^0x[a-fA-F0-9]{64}$/),
|
|
278
|
+
network: z.string().min(1),
|
|
279
|
+
chainId: z.number().int().positive(),
|
|
280
|
+
asset: z.string().min(1),
|
|
281
|
+
payTo: evmAddressSchema,
|
|
282
|
+
amount: positiveIntegerStringSchema,
|
|
283
|
+
amountDecimal: z.string().min(1),
|
|
284
|
+
sourceTxHash: z.string().regex(/^0x[a-fA-F0-9]{64}$/),
|
|
285
|
+
destinationTxHash: z.string().regex(/^0x[a-fA-F0-9]{64}$/).optional(),
|
|
286
|
+
settlementTxHash: z.string().regex(/^0x[a-fA-F0-9]{64}$/),
|
|
287
|
+
completedAt: z.string().optional(),
|
|
288
|
+
paymentIdentifier: paymentIdentifierSchema.optional(),
|
|
289
|
+
extensions: z
|
|
290
|
+
.object({
|
|
291
|
+
"payment-identifier": z
|
|
292
|
+
.object({
|
|
293
|
+
info: z.object({
|
|
294
|
+
required: z.boolean(),
|
|
295
|
+
id: paymentIdentifierSchema,
|
|
296
|
+
}),
|
|
297
|
+
})
|
|
298
|
+
.optional(),
|
|
299
|
+
})
|
|
300
|
+
.optional(),
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
function validateCompletedX402Intent(parsed: ParsedX402PaymentRequired, paymentIntent: PaymentIntentRecord): void {
|
|
304
|
+
if (paymentIntent.status !== "COMPLETED") {
|
|
305
|
+
throw new Error(`Payment intent ${paymentIntent.id} must be COMPLETED before x402 proof can be generated.`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (paymentIntent.paymentType !== "X402_PAYMENT") {
|
|
309
|
+
throw new Error(`Payment intent ${paymentIntent.id} must be an X402_PAYMENT intent.`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!paymentIntent.sourceTxHash) {
|
|
313
|
+
throw new Error(`Payment intent ${paymentIntent.id} is missing a source transaction hash.`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const selected = parsed.selectedRequirement;
|
|
317
|
+
const matchesRequirement =
|
|
318
|
+
paymentIntent.destinationChainId === selected.chainId &&
|
|
319
|
+
paymentIntent.destinationTokenAddress.toLowerCase() === selected.asset.toLowerCase() &&
|
|
320
|
+
paymentIntent.destinationTokenSymbol === selected.tokenSymbol &&
|
|
321
|
+
paymentIntent.recipientAddress.toLowerCase() === selected.payTo.toLowerCase() &&
|
|
322
|
+
paymentIntent.amountOut === selected.amount;
|
|
323
|
+
|
|
324
|
+
if (!matchesRequirement) {
|
|
325
|
+
throw new Error(`Payment intent ${paymentIntent.id} does not match the x402 requirement.`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function encodeBase64UrlJson(value: unknown): string {
|
|
330
|
+
return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function parsePaymentIdentifierExtension(
|
|
334
|
+
extensions: Record<string, unknown> | undefined,
|
|
335
|
+
): ParsedX402PaymentRequired["extensions"] extends infer Extensions
|
|
336
|
+
? Extensions extends { "payment-identifier"?: infer Extension }
|
|
337
|
+
? Extension | undefined
|
|
338
|
+
: never
|
|
339
|
+
: never {
|
|
340
|
+
const extension = extensions?.[PAYMENT_IDENTIFIER];
|
|
341
|
+
|
|
342
|
+
if (extension === undefined) {
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!extension || typeof extension !== "object") {
|
|
347
|
+
throw new Error("Unsupported x402 payment-identifier extension.");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const info = (extension as { info?: unknown }).info;
|
|
351
|
+
|
|
352
|
+
if (!info || typeof info !== "object" || typeof (info as { required?: unknown }).required !== "boolean") {
|
|
353
|
+
throw new Error("Unsupported x402 payment-identifier extension.");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
info: {
|
|
358
|
+
required: (info as { required: boolean }).required,
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function createPaymentIdentifier(paymentIntentId: string): string {
|
|
364
|
+
return paymentIdentifierSchema.parse(paymentIntentId);
|
|
365
|
+
}
|
|
366
|
+
|
|
135
367
|
function toSupportedRequirement(requirement: z.infer<typeof x402PaymentRequirementSchema>):
|
|
136
368
|
| ParsedX402PaymentRequired["selectedRequirement"]
|
|
137
369
|
| null {
|
|
@@ -202,3 +434,19 @@ function createX402Purpose(resource: z.infer<typeof x402ResourceInfoSchema>): st
|
|
|
202
434
|
|
|
203
435
|
return `x402 payment for ${details}`.slice(0, 280);
|
|
204
436
|
}
|
|
437
|
+
|
|
438
|
+
function stableStringify(value: unknown): string {
|
|
439
|
+
if (Array.isArray(value)) {
|
|
440
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (value && typeof value === "object") {
|
|
444
|
+
return `{${Object.entries(value as Record<string, unknown>)
|
|
445
|
+
.filter(([, entryValue]) => entryValue !== undefined)
|
|
446
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
447
|
+
.map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`)
|
|
448
|
+
.join(",")}}`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return JSON.stringify(value);
|
|
452
|
+
}
|