@4mica/sdk 0.5.0 → 0.5.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 +251 -395
- package/package.json +36 -3
- package/.eslintrc.cjs +0 -29
- package/.github/workflows/ci.yml +0 -31
- package/.prettierignore +0 -3
- package/.prettierrc +0 -6
- package/eslint.config.mjs +0 -22
- package/src/abi/core4mica.json +0 -1605
- package/src/abi/erc20.json +0 -187
- package/src/auth.ts +0 -325
- package/src/bls.ts +0 -76
- package/src/client.ts +0 -347
- package/src/config.ts +0 -149
- package/src/contract.ts +0 -194
- package/src/errors.ts +0 -40
- package/src/guarantee.ts +0 -96
- package/src/index.ts +0 -12
- package/src/models.ts +0 -309
- package/src/rpc.ts +0 -225
- package/src/signing.ts +0 -330
- package/src/utils.ts +0 -75
- package/src/x402/index.ts +0 -192
- package/src/x402/models.ts +0 -94
- package/tests/auth.integration.test.ts +0 -97
- package/tests/auth.test.ts +0 -292
- package/tests/config.test.ts +0 -56
- package/tests/guarantee.test.ts +0 -26
- package/tests/rpc.test.ts +0 -76
- package/tests/signing.test.ts +0 -52
- package/tests/utils.test.ts +0 -35
- package/tests/x402.test.ts +0 -152
- package/tsconfig.build.json +0 -5
- package/tsconfig.json +0 -15
- package/vitest.config.ts +0 -12
package/src/signing.ts
DELETED
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
import { AbiCoder, Wallet, getBytes } from 'ethers';
|
|
2
|
-
import { SigningError } from './errors';
|
|
3
|
-
import { PaymentGuaranteeRequestClaims, PaymentSignature, SigningScheme } from './models';
|
|
4
|
-
import { ValidationError, normalizeAddress, normalizePrivateKey } from './utils';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* ClientEvmSigner - Used by x402 clients to sign payment authorizations
|
|
8
|
-
* This is typically a LocalAccount or wallet that holds private keys
|
|
9
|
-
* and can sign EIP-712 typed data for payment authorizations
|
|
10
|
-
*/
|
|
11
|
-
export type EvmSigner = {
|
|
12
|
-
readonly address: `0x${string}`;
|
|
13
|
-
|
|
14
|
-
signTypedData(message: {
|
|
15
|
-
domain: Record<string, unknown>;
|
|
16
|
-
types: Record<string, unknown>;
|
|
17
|
-
primaryType: string;
|
|
18
|
-
message: Record<string, unknown>;
|
|
19
|
-
}): Promise<`0x${string}`>;
|
|
20
|
-
|
|
21
|
-
signMessage(message: { message: string }): Promise<`0x${string}`>;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export function createLocalSigner(privateKey: string): EvmSigner {
|
|
25
|
-
privateKey = normalizePrivateKey(privateKey);
|
|
26
|
-
const wallet = new Wallet(privateKey);
|
|
27
|
-
return {
|
|
28
|
-
address: wallet.address as `0x${string}`,
|
|
29
|
-
signTypedData: async (message) => {
|
|
30
|
-
const signature = await wallet.signTypedData(
|
|
31
|
-
message.domain,
|
|
32
|
-
{ [message.primaryType]: message.types[message.primaryType] } as Record<
|
|
33
|
-
string,
|
|
34
|
-
Array<{ name: string; type: string }>
|
|
35
|
-
>,
|
|
36
|
-
message.message
|
|
37
|
-
);
|
|
38
|
-
return signature as `0x${string}`;
|
|
39
|
-
},
|
|
40
|
-
signMessage: async ({ message }) => {
|
|
41
|
-
const signature = await wallet.signMessage(message);
|
|
42
|
-
return signature as `0x${string}`;
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export class CorePublicParameters {
|
|
48
|
-
constructor(
|
|
49
|
-
public publicKey: Uint8Array,
|
|
50
|
-
public contractAddress: string,
|
|
51
|
-
public ethereumHttpRpcUrl: string,
|
|
52
|
-
public eip712Name: string,
|
|
53
|
-
public eip712Version: string,
|
|
54
|
-
public chainId: number
|
|
55
|
-
) {}
|
|
56
|
-
|
|
57
|
-
static fromRpc(payload: Record<string, unknown>): CorePublicParameters {
|
|
58
|
-
const pkRaw = payload.public_key ?? payload.publicKey;
|
|
59
|
-
const pk =
|
|
60
|
-
typeof pkRaw === 'string'
|
|
61
|
-
? getBytes(pkRaw)
|
|
62
|
-
: pkRaw instanceof Uint8Array
|
|
63
|
-
? pkRaw
|
|
64
|
-
: Array.isArray(pkRaw)
|
|
65
|
-
? Uint8Array.from(pkRaw as ArrayLike<number>)
|
|
66
|
-
: new Uint8Array();
|
|
67
|
-
return new CorePublicParameters(
|
|
68
|
-
pk,
|
|
69
|
-
String(payload.contract_address ?? payload.contractAddress ?? ''),
|
|
70
|
-
String(payload.ethereum_http_rpc_url ?? payload.ethereumHttpRpcUrl ?? ''),
|
|
71
|
-
(payload.eip712_name ?? payload.eip712Name ?? '4Mica') as string,
|
|
72
|
-
(payload.eip712_version ?? payload.eip712Version ?? '1') as string,
|
|
73
|
-
Number(payload.chain_id ?? payload.chainId)
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export const GUARANTEE_EIP712_TYPES = {
|
|
79
|
-
EIP712Domain: [
|
|
80
|
-
{ name: 'name', type: 'string' },
|
|
81
|
-
{ name: 'version', type: 'string' },
|
|
82
|
-
{ name: 'chainId', type: 'uint256' },
|
|
83
|
-
],
|
|
84
|
-
SolGuaranteeRequestClaimsV1: [
|
|
85
|
-
{ name: 'user', type: 'address' },
|
|
86
|
-
{ name: 'recipient', type: 'address' },
|
|
87
|
-
{ name: 'tabId', type: 'uint256' },
|
|
88
|
-
{ name: 'reqId', type: 'uint256' },
|
|
89
|
-
{ name: 'amount', type: 'uint256' },
|
|
90
|
-
{ name: 'asset', type: 'address' },
|
|
91
|
-
{ name: 'timestamp', type: 'uint64' },
|
|
92
|
-
],
|
|
93
|
-
} as const;
|
|
94
|
-
|
|
95
|
-
export type GuaranteeTypedData = {
|
|
96
|
-
types: typeof GUARANTEE_EIP712_TYPES;
|
|
97
|
-
primaryType: 'SolGuaranteeRequestClaimsV1';
|
|
98
|
-
domain: {
|
|
99
|
-
name: string;
|
|
100
|
-
version: string;
|
|
101
|
-
chainId: number;
|
|
102
|
-
};
|
|
103
|
-
message: {
|
|
104
|
-
user: string;
|
|
105
|
-
recipient: string;
|
|
106
|
-
tabId: bigint;
|
|
107
|
-
reqId: bigint;
|
|
108
|
-
amount: bigint;
|
|
109
|
-
asset: string;
|
|
110
|
-
timestamp: bigint;
|
|
111
|
-
};
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
export type GuaranteeTypedDataValidationOptions = {
|
|
115
|
-
expectedChainId?: number;
|
|
116
|
-
expectedSigner?: string;
|
|
117
|
-
expectedRecipient?: string;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
export type GuaranteeSigningContextOptions = {
|
|
121
|
-
signerAddress?: string;
|
|
122
|
-
signerChainId?: number;
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
126
|
-
Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
127
|
-
|
|
128
|
-
const fieldsMatch = (
|
|
129
|
-
actual: unknown,
|
|
130
|
-
expected: readonly { name: string; type: string }[]
|
|
131
|
-
): boolean => {
|
|
132
|
-
if (!Array.isArray(actual) || actual.length !== expected.length) return false;
|
|
133
|
-
const norm = (list: readonly { name: string; type: string }[]) =>
|
|
134
|
-
list.map((item) => `${item.name}:${item.type}`).sort();
|
|
135
|
-
const actualSet = new Set(norm(actual));
|
|
136
|
-
return norm(expected).every((key) => actualSet.has(key));
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const parseChainId = (value: unknown): number => {
|
|
140
|
-
if (typeof value === 'bigint') return Number(value);
|
|
141
|
-
if (typeof value === 'number') return value;
|
|
142
|
-
if (typeof value === 'string' && value.trim()) return Number(value);
|
|
143
|
-
return Number.NaN;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const ensureBigIntish = (value: unknown, label: string): void => {
|
|
147
|
-
try {
|
|
148
|
-
BigInt(value as bigint | number | string);
|
|
149
|
-
} catch {
|
|
150
|
-
throw new ValidationError(`${label} must be a numeric value`);
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
export function validateGuaranteeTypedData(
|
|
155
|
-
payload: {
|
|
156
|
-
domain: Record<string, unknown>;
|
|
157
|
-
types: Record<string, Array<{ name: string; type: string }>>;
|
|
158
|
-
message: Record<string, unknown>;
|
|
159
|
-
},
|
|
160
|
-
options: GuaranteeTypedDataValidationOptions = {}
|
|
161
|
-
): void {
|
|
162
|
-
if (!isRecord(payload)) {
|
|
163
|
-
throw new ValidationError('typed data payload is required');
|
|
164
|
-
}
|
|
165
|
-
if (!isRecord(payload.domain)) {
|
|
166
|
-
throw new ValidationError('domain is required');
|
|
167
|
-
}
|
|
168
|
-
if (!isRecord(payload.types)) {
|
|
169
|
-
throw new ValidationError('types are required');
|
|
170
|
-
}
|
|
171
|
-
if (!isRecord(payload.message)) {
|
|
172
|
-
throw new ValidationError('message is required');
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const types = payload.types;
|
|
176
|
-
if (
|
|
177
|
-
!fieldsMatch(
|
|
178
|
-
types.SolGuaranteeRequestClaimsV1,
|
|
179
|
-
GUARANTEE_EIP712_TYPES.SolGuaranteeRequestClaimsV1
|
|
180
|
-
)
|
|
181
|
-
) {
|
|
182
|
-
throw new ValidationError('Unexpected struct fields for SolGuaranteeRequestClaimsV1');
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const message = payload.message;
|
|
186
|
-
const requiredFields = ['user', 'recipient', 'tabId', 'reqId', 'amount', 'asset', 'timestamp'];
|
|
187
|
-
if (requiredFields.some((field) => !(field in message))) {
|
|
188
|
-
throw new ValidationError('message is missing required SolGuaranteeRequestClaimsV1 fields');
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const user = normalizeAddress(message.user as string);
|
|
192
|
-
const recipient = normalizeAddress(message.recipient as string);
|
|
193
|
-
normalizeAddress(message.asset as string);
|
|
194
|
-
|
|
195
|
-
if (options.expectedSigner) {
|
|
196
|
-
if (normalizeAddress(options.expectedSigner) !== user) {
|
|
197
|
-
throw new ValidationError('message.user must match signer address');
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
if (options.expectedRecipient) {
|
|
201
|
-
if (normalizeAddress(options.expectedRecipient) !== recipient) {
|
|
202
|
-
throw new ValidationError('message.recipient must match expected recipient');
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
ensureBigIntish(message.tabId, 'tabId');
|
|
207
|
-
ensureBigIntish(message.reqId, 'reqId');
|
|
208
|
-
ensureBigIntish(message.amount, 'amount');
|
|
209
|
-
ensureBigIntish(message.timestamp, 'timestamp');
|
|
210
|
-
|
|
211
|
-
if (options.expectedChainId !== undefined) {
|
|
212
|
-
const domainChainId = parseChainId(payload.domain.chainId);
|
|
213
|
-
if (!Number.isFinite(domainChainId)) {
|
|
214
|
-
throw new ValidationError('domain.chainId is required for typed data signatures');
|
|
215
|
-
}
|
|
216
|
-
if (Number(domainChainId) !== Number(options.expectedChainId)) {
|
|
217
|
-
throw new ValidationError(
|
|
218
|
-
`domain.chainId mismatch; expected ${options.expectedChainId}, got ${domainChainId}`
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export function validateGuaranteeSigningContext(
|
|
225
|
-
params: CorePublicParameters,
|
|
226
|
-
claims: PaymentGuaranteeRequestClaims,
|
|
227
|
-
options: GuaranteeSigningContextOptions = {}
|
|
228
|
-
): void {
|
|
229
|
-
if (options.signerAddress) {
|
|
230
|
-
const signer = normalizeAddress(options.signerAddress);
|
|
231
|
-
const user = normalizeAddress(claims.userAddress);
|
|
232
|
-
if (signer !== user) {
|
|
233
|
-
throw new ValidationError(
|
|
234
|
-
`address mismatch: signer ${options.signerAddress} != claims.user_address ${claims.userAddress}`
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (options.signerChainId !== undefined) {
|
|
240
|
-
const signerChainId = Number(options.signerChainId);
|
|
241
|
-
if (!Number.isFinite(signerChainId)) {
|
|
242
|
-
throw new ValidationError('signer chain id is invalid');
|
|
243
|
-
}
|
|
244
|
-
if (Number(params.chainId) !== signerChainId) {
|
|
245
|
-
throw new ValidationError(
|
|
246
|
-
`chain id mismatch: expected ${params.chainId}, got ${signerChainId}`
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
export function buildGuaranteeTypedData(
|
|
253
|
-
params: CorePublicParameters,
|
|
254
|
-
claims: PaymentGuaranteeRequestClaims
|
|
255
|
-
): GuaranteeTypedData {
|
|
256
|
-
return {
|
|
257
|
-
types: GUARANTEE_EIP712_TYPES,
|
|
258
|
-
primaryType: 'SolGuaranteeRequestClaimsV1',
|
|
259
|
-
domain: {
|
|
260
|
-
name: params.eip712Name,
|
|
261
|
-
version: params.eip712Version,
|
|
262
|
-
chainId: params.chainId,
|
|
263
|
-
},
|
|
264
|
-
message: {
|
|
265
|
-
user: claims.userAddress,
|
|
266
|
-
recipient: claims.recipientAddress,
|
|
267
|
-
tabId: claims.tabId,
|
|
268
|
-
reqId: claims.reqId,
|
|
269
|
-
amount: claims.amount,
|
|
270
|
-
asset: claims.assetAddress,
|
|
271
|
-
timestamp: BigInt(claims.timestamp),
|
|
272
|
-
},
|
|
273
|
-
} as const;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
export function encodeGuaranteeEip191(claims: PaymentGuaranteeRequestClaims): string {
|
|
277
|
-
const payload = AbiCoder.defaultAbiCoder().encode(
|
|
278
|
-
['address', 'address', 'uint256', 'uint256', 'uint256', 'address', 'uint64'],
|
|
279
|
-
[
|
|
280
|
-
claims.userAddress,
|
|
281
|
-
claims.recipientAddress,
|
|
282
|
-
claims.tabId,
|
|
283
|
-
claims.reqId,
|
|
284
|
-
claims.amount,
|
|
285
|
-
claims.assetAddress,
|
|
286
|
-
claims.timestamp,
|
|
287
|
-
]
|
|
288
|
-
);
|
|
289
|
-
return payload;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
export class PaymentSigner {
|
|
293
|
-
private signer: EvmSigner;
|
|
294
|
-
|
|
295
|
-
constructor(signer: EvmSigner) {
|
|
296
|
-
this.signer = signer;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
async signRequest(
|
|
300
|
-
params: CorePublicParameters,
|
|
301
|
-
claims: PaymentGuaranteeRequestClaims,
|
|
302
|
-
scheme: SigningScheme = SigningScheme.EIP712
|
|
303
|
-
): Promise<PaymentSignature> {
|
|
304
|
-
try {
|
|
305
|
-
validateGuaranteeSigningContext(params, claims, { signerAddress: this.signer.address });
|
|
306
|
-
if (scheme === SigningScheme.EIP712) {
|
|
307
|
-
const typed = buildGuaranteeTypedData(params, claims);
|
|
308
|
-
const signature = await this.signer.signTypedData({
|
|
309
|
-
domain: typed.domain,
|
|
310
|
-
types: { SolGuaranteeRequestClaimsV1: typed.types.SolGuaranteeRequestClaimsV1 },
|
|
311
|
-
primaryType: typed.primaryType,
|
|
312
|
-
message: typed.message,
|
|
313
|
-
});
|
|
314
|
-
return { signature, scheme };
|
|
315
|
-
}
|
|
316
|
-
if (scheme === SigningScheme.EIP191) {
|
|
317
|
-
const message = encodeGuaranteeEip191(claims);
|
|
318
|
-
const signature = await this.signer.signMessage({ message });
|
|
319
|
-
return { signature, scheme };
|
|
320
|
-
}
|
|
321
|
-
throw new SigningError(`unsupported signing scheme: ${scheme}`);
|
|
322
|
-
} catch (err: unknown) {
|
|
323
|
-
if (err instanceof ValidationError) {
|
|
324
|
-
throw new SigningError(err.message);
|
|
325
|
-
}
|
|
326
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
327
|
-
throw new SigningError(message);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
package/src/utils.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { getAddress, isAddress } from 'ethers';
|
|
2
|
-
|
|
3
|
-
export class ValidationError extends Error {
|
|
4
|
-
constructor(message: string) {
|
|
5
|
-
super(message);
|
|
6
|
-
this.name = 'ValidationError';
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function validateUrl(raw: string): string {
|
|
11
|
-
try {
|
|
12
|
-
const url = new URL(raw);
|
|
13
|
-
if (!url.protocol || !url.host) {
|
|
14
|
-
throw new Error('missing parts');
|
|
15
|
-
}
|
|
16
|
-
return raw;
|
|
17
|
-
} catch {
|
|
18
|
-
throw new ValidationError(`invalid URL: ${raw}`);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function normalizePrivateKey(raw: string): string {
|
|
23
|
-
const key = raw.startsWith('0x') ? raw.slice(2) : raw;
|
|
24
|
-
if (!/^[0-9a-fA-F]{64}$/.test(key)) {
|
|
25
|
-
throw new ValidationError('invalid private key (expected 32 byte hex)');
|
|
26
|
-
}
|
|
27
|
-
return `0x${key.toLowerCase()}`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function normalizeAddress(raw: string): string {
|
|
31
|
-
const candidate: string = String(raw);
|
|
32
|
-
if (isAddress(candidate)) {
|
|
33
|
-
return getAddress(candidate);
|
|
34
|
-
}
|
|
35
|
-
const lower: string = (candidate as string).toLowerCase();
|
|
36
|
-
if (isAddress(lower)) {
|
|
37
|
-
return getAddress(lower);
|
|
38
|
-
}
|
|
39
|
-
throw new ValidationError(`invalid address: ${raw}`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function parseNumericString(raw: string): bigint {
|
|
43
|
-
const text = raw.trim();
|
|
44
|
-
const n = BigInt(text);
|
|
45
|
-
if (n < 0n) {
|
|
46
|
-
throw new ValidationError('u256 cannot be negative');
|
|
47
|
-
}
|
|
48
|
-
return n;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function parseU256(value: number | bigint | string): bigint {
|
|
52
|
-
if (typeof value === 'number') {
|
|
53
|
-
if (!Number.isFinite(value)) {
|
|
54
|
-
throw new ValidationError('invalid integer');
|
|
55
|
-
}
|
|
56
|
-
if (value < 0) {
|
|
57
|
-
throw new ValidationError('u256 cannot be negative');
|
|
58
|
-
}
|
|
59
|
-
return BigInt(value);
|
|
60
|
-
}
|
|
61
|
-
if (typeof value === 'bigint') {
|
|
62
|
-
if (value < 0) {
|
|
63
|
-
throw new ValidationError('u256 cannot be negative');
|
|
64
|
-
}
|
|
65
|
-
return value;
|
|
66
|
-
}
|
|
67
|
-
if (typeof value === 'string') {
|
|
68
|
-
return parseNumericString(value);
|
|
69
|
-
}
|
|
70
|
-
throw new ValidationError(`unsupported numeric type: ${typeof value}`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function serializeU256(value: number | bigint | string): string {
|
|
74
|
-
return `0x${parseU256(value).toString(16)}`;
|
|
75
|
-
}
|
package/src/x402/index.ts
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import { PaymentGuaranteeRequestClaims, PaymentSignature, SigningScheme } from '../models';
|
|
2
|
-
import {
|
|
3
|
-
PaymentRequirementsV1,
|
|
4
|
-
TabResponse,
|
|
5
|
-
X402SignedPayment,
|
|
6
|
-
X402SettledPayment,
|
|
7
|
-
X402PaymentPayload,
|
|
8
|
-
PaymentRequirementsV2,
|
|
9
|
-
X402PaymentRequired,
|
|
10
|
-
X402PaymentEnvelopeV1,
|
|
11
|
-
X402PaymentEnvelopeV2,
|
|
12
|
-
X402ResourceInfo,
|
|
13
|
-
} from './models';
|
|
14
|
-
import { normalizeAddress, parseU256 } from '../utils';
|
|
15
|
-
import type { FetchFn } from '../rpc';
|
|
16
|
-
import { X402Error } from '../errors';
|
|
17
|
-
|
|
18
|
-
export * from './models';
|
|
19
|
-
|
|
20
|
-
export interface FlowSigner {
|
|
21
|
-
signPayment(
|
|
22
|
-
claims: PaymentGuaranteeRequestClaims,
|
|
23
|
-
scheme: SigningScheme
|
|
24
|
-
): Promise<PaymentSignature>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export class X402Flow {
|
|
28
|
-
private fetchFn: FetchFn;
|
|
29
|
-
|
|
30
|
-
constructor(
|
|
31
|
-
private signer: FlowSigner,
|
|
32
|
-
fetchFn: FetchFn = fetch
|
|
33
|
-
) {
|
|
34
|
-
this.fetchFn = fetchFn;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
static fromClient(client: { user: FlowSigner }): X402Flow {
|
|
38
|
-
return new X402Flow(client.user);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async signPayment(
|
|
42
|
-
paymentRequirements: PaymentRequirementsV1,
|
|
43
|
-
userAddress: string
|
|
44
|
-
): Promise<X402SignedPayment> {
|
|
45
|
-
X402Flow.validateScheme(paymentRequirements.scheme);
|
|
46
|
-
const tab = await this.requestTab(1, paymentRequirements, userAddress);
|
|
47
|
-
|
|
48
|
-
const claims = this.buildClaims(paymentRequirements, tab, userAddress);
|
|
49
|
-
const signature = await this.signer.signPayment(claims, SigningScheme.EIP712);
|
|
50
|
-
const paymentPayload = X402Flow.buildPaymentPayload(claims, signature);
|
|
51
|
-
|
|
52
|
-
const envelope: X402PaymentEnvelopeV1 = {
|
|
53
|
-
x402Version: 1,
|
|
54
|
-
scheme: paymentRequirements.scheme,
|
|
55
|
-
network: paymentRequirements.network,
|
|
56
|
-
payload: paymentPayload,
|
|
57
|
-
};
|
|
58
|
-
const header = Buffer.from(JSON.stringify(envelope)).toString('base64');
|
|
59
|
-
return { header, payload: paymentPayload, signature };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async signPaymentV2(
|
|
63
|
-
paymentRequired: X402PaymentRequired,
|
|
64
|
-
accepted: PaymentRequirementsV2,
|
|
65
|
-
userAddress: string
|
|
66
|
-
): Promise<X402SignedPayment> {
|
|
67
|
-
X402Flow.validateScheme(accepted.scheme);
|
|
68
|
-
const tab = await this.requestTab(2, accepted, userAddress, paymentRequired.resource);
|
|
69
|
-
|
|
70
|
-
const claims = this.buildClaims(accepted, tab, userAddress);
|
|
71
|
-
const signature = await this.signer.signPayment(claims, SigningScheme.EIP712);
|
|
72
|
-
const paymentPayload = X402Flow.buildPaymentPayload(claims, signature);
|
|
73
|
-
|
|
74
|
-
const envelope: X402PaymentEnvelopeV2 = {
|
|
75
|
-
x402Version: 2,
|
|
76
|
-
accepted: accepted,
|
|
77
|
-
payload: paymentPayload,
|
|
78
|
-
resource: paymentRequired.resource,
|
|
79
|
-
};
|
|
80
|
-
const header = Buffer.from(JSON.stringify(envelope)).toString('base64');
|
|
81
|
-
return { header, payload: paymentPayload, signature };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async settlePayment(
|
|
85
|
-
payment: X402SignedPayment,
|
|
86
|
-
paymentRequirements: PaymentRequirementsV1,
|
|
87
|
-
facilitatorUrl: string
|
|
88
|
-
): Promise<X402SettledPayment> {
|
|
89
|
-
const url = `${facilitatorUrl.replace(/\/$/, '')}/settle`;
|
|
90
|
-
const response = await this.fetchFn(url, {
|
|
91
|
-
method: 'POST',
|
|
92
|
-
headers: { 'content-type': 'application/json' },
|
|
93
|
-
body: JSON.stringify({
|
|
94
|
-
x402Version: 1,
|
|
95
|
-
paymentHeader: payment.header,
|
|
96
|
-
paymentRequirements,
|
|
97
|
-
}),
|
|
98
|
-
});
|
|
99
|
-
const data = await response.text();
|
|
100
|
-
if (!response.ok) {
|
|
101
|
-
throw new X402Error(`settlement failed with status ${response.status}: ${data}`);
|
|
102
|
-
}
|
|
103
|
-
const settlement = data ? JSON.parse(data) : {};
|
|
104
|
-
return { payment, settlement };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
protected async requestTab(
|
|
108
|
-
x402Version: number,
|
|
109
|
-
paymentRequirements: PaymentRequirementsV1 | PaymentRequirementsV2,
|
|
110
|
-
userAddress: string,
|
|
111
|
-
resource?: X402ResourceInfo
|
|
112
|
-
): Promise<TabResponse> {
|
|
113
|
-
const tabEndpoint = paymentRequirements.extra?.tabEndpoint;
|
|
114
|
-
if (!tabEndpoint || typeof tabEndpoint !== 'string') {
|
|
115
|
-
throw new X402Error('missing tabEndpoint in paymentRequirements.extra');
|
|
116
|
-
}
|
|
117
|
-
const resp = await this.fetchFn(tabEndpoint, {
|
|
118
|
-
method: 'POST',
|
|
119
|
-
headers: { 'content-type': 'application/json' },
|
|
120
|
-
body: JSON.stringify({
|
|
121
|
-
x402Version,
|
|
122
|
-
userAddress,
|
|
123
|
-
paymentRequirements,
|
|
124
|
-
resource,
|
|
125
|
-
}),
|
|
126
|
-
});
|
|
127
|
-
if (!resp.ok) {
|
|
128
|
-
const text = await resp.text();
|
|
129
|
-
throw new X402Error(`tab resolution failed: ${resp.status} ${text}`);
|
|
130
|
-
}
|
|
131
|
-
const body = await resp.json();
|
|
132
|
-
return {
|
|
133
|
-
tabId: body.tabId ?? body.tab_id,
|
|
134
|
-
userAddress: body.userAddress ?? body.user_address,
|
|
135
|
-
nextReqId: body.nextReqId ?? body.next_req_id ?? body.reqId ?? body.req_id,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
protected buildClaims(
|
|
140
|
-
requirements: PaymentRequirementsV1 | PaymentRequirementsV2,
|
|
141
|
-
tab: TabResponse,
|
|
142
|
-
userAddress: string
|
|
143
|
-
): PaymentGuaranteeRequestClaims {
|
|
144
|
-
const tabId = parseU256(tab.tabId);
|
|
145
|
-
const reqId =
|
|
146
|
-
tab.nextReqId !== undefined && tab.nextReqId !== null ? parseU256(tab.nextReqId) : 0n;
|
|
147
|
-
const amount = parseU256(
|
|
148
|
-
'maxAmountRequired' in requirements ? requirements.maxAmountRequired : requirements.amount
|
|
149
|
-
);
|
|
150
|
-
if (tab.userAddress.toLowerCase() !== userAddress.toLowerCase()) {
|
|
151
|
-
throw new X402Error(
|
|
152
|
-
`user mismatch in paymentRequirements: found ${tab.userAddress}, expected ${userAddress}`
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
156
|
-
return PaymentGuaranteeRequestClaims.new(
|
|
157
|
-
userAddress,
|
|
158
|
-
normalizeAddress(requirements.payTo),
|
|
159
|
-
tabId,
|
|
160
|
-
amount,
|
|
161
|
-
timestamp,
|
|
162
|
-
requirements.asset,
|
|
163
|
-
reqId
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
private static validateScheme(scheme: string): void {
|
|
168
|
-
if (!scheme.toLowerCase().includes('4mica')) {
|
|
169
|
-
throw new X402Error(`invalid scheme: ${scheme}`);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
private static buildPaymentPayload(
|
|
174
|
-
claims: PaymentGuaranteeRequestClaims,
|
|
175
|
-
signature: PaymentSignature
|
|
176
|
-
): X402PaymentPayload {
|
|
177
|
-
return {
|
|
178
|
-
claims: {
|
|
179
|
-
version: 'v1',
|
|
180
|
-
user_address: claims.userAddress,
|
|
181
|
-
recipient_address: claims.recipientAddress,
|
|
182
|
-
tab_id: `0x${claims.tabId.toString(16)}`,
|
|
183
|
-
req_id: `0x${claims.reqId.toString(16)}`,
|
|
184
|
-
amount: `0x${claims.amount.toString(16)}`,
|
|
185
|
-
asset_address: claims.assetAddress,
|
|
186
|
-
timestamp: claims.timestamp,
|
|
187
|
-
},
|
|
188
|
-
signature: signature.signature,
|
|
189
|
-
scheme: signature.scheme,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
}
|
package/src/x402/models.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { PaymentSignature, SigningScheme } from '../models';
|
|
2
|
-
|
|
3
|
-
export interface PaymentRequirementsV1 {
|
|
4
|
-
scheme: string;
|
|
5
|
-
network: string;
|
|
6
|
-
maxAmountRequired: string;
|
|
7
|
-
payTo: string;
|
|
8
|
-
asset: string;
|
|
9
|
-
resource?: string;
|
|
10
|
-
description?: string;
|
|
11
|
-
mimeType?: string;
|
|
12
|
-
outputSchema?: unknown;
|
|
13
|
-
maxTimeoutSeconds?: number;
|
|
14
|
-
extra?: PaymentRequirementsExtra;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface PaymentRequirementsV2 {
|
|
18
|
-
scheme: string;
|
|
19
|
-
network: string;
|
|
20
|
-
asset: string;
|
|
21
|
-
amount: string;
|
|
22
|
-
payTo: string;
|
|
23
|
-
maxTimeoutSeconds?: number;
|
|
24
|
-
extra?: PaymentRequirementsExtra;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export type PaymentRequirements = PaymentRequirementsV2;
|
|
28
|
-
|
|
29
|
-
export interface PaymentRequirementsExtra {
|
|
30
|
-
tabEndpoint?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface TabResponse {
|
|
34
|
-
tabId: string;
|
|
35
|
-
userAddress: string;
|
|
36
|
-
nextReqId?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface X402PaymentPayloadClaims {
|
|
40
|
-
version: string;
|
|
41
|
-
user_address: string;
|
|
42
|
-
recipient_address: string;
|
|
43
|
-
tab_id: string;
|
|
44
|
-
req_id: string;
|
|
45
|
-
amount: string;
|
|
46
|
-
timestamp: number;
|
|
47
|
-
asset_address: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface X402PaymentPayload {
|
|
51
|
-
claims: X402PaymentPayloadClaims;
|
|
52
|
-
/// 65-byte signature as 0x-prefixed hex
|
|
53
|
-
signature: string;
|
|
54
|
-
scheme: SigningScheme;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface X402PaymentEnvelopeV1 {
|
|
58
|
-
x402Version: number;
|
|
59
|
-
scheme: string;
|
|
60
|
-
network: string;
|
|
61
|
-
payload: X402PaymentPayload;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface X402ResourceInfo {
|
|
65
|
-
url: string;
|
|
66
|
-
description: string;
|
|
67
|
-
mimeType: string;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface X402PaymentEnvelopeV2 {
|
|
71
|
-
x402Version: number;
|
|
72
|
-
accepted: PaymentRequirementsV2;
|
|
73
|
-
payload: X402PaymentPayload;
|
|
74
|
-
resource: X402ResourceInfo;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface X402SignedPayment {
|
|
78
|
-
header: string;
|
|
79
|
-
payload: X402PaymentPayload;
|
|
80
|
-
signature: PaymentSignature;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface X402SettledPayment {
|
|
84
|
-
payment: X402SignedPayment;
|
|
85
|
-
settlement: unknown;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export interface X402PaymentRequired {
|
|
89
|
-
x402Version: number;
|
|
90
|
-
error?: string;
|
|
91
|
-
resource: X402ResourceInfo;
|
|
92
|
-
accepts: PaymentRequirementsV2[];
|
|
93
|
-
extensions?: Record<string, unknown>;
|
|
94
|
-
}
|