uvd-x402-sdk 2.0.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.
- package/LICENSE +21 -0
- package/README.md +782 -0
- package/dist/index-BrBqP1I8.d.ts +199 -0
- package/dist/index-D6Sr4ARD.d.mts +429 -0
- package/dist/index-D6Sr4ARD.d.ts +429 -0
- package/dist/index-DJ4Cvrev.d.mts +199 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1178 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1146 -0
- package/dist/index.mjs.map +1 -0
- package/dist/providers/evm/index.d.mts +84 -0
- package/dist/providers/evm/index.d.ts +84 -0
- package/dist/providers/evm/index.js +740 -0
- package/dist/providers/evm/index.js.map +1 -0
- package/dist/providers/evm/index.mjs +735 -0
- package/dist/providers/evm/index.mjs.map +1 -0
- package/dist/providers/near/index.d.mts +99 -0
- package/dist/providers/near/index.d.ts +99 -0
- package/dist/providers/near/index.js +483 -0
- package/dist/providers/near/index.js.map +1 -0
- package/dist/providers/near/index.mjs +478 -0
- package/dist/providers/near/index.mjs.map +1 -0
- package/dist/providers/solana/index.d.mts +115 -0
- package/dist/providers/solana/index.d.ts +115 -0
- package/dist/providers/solana/index.js +771 -0
- package/dist/providers/solana/index.js.map +1 -0
- package/dist/providers/solana/index.mjs +765 -0
- package/dist/providers/solana/index.mjs.map +1 -0
- package/dist/providers/stellar/index.d.mts +67 -0
- package/dist/providers/stellar/index.d.ts +67 -0
- package/dist/providers/stellar/index.js +306 -0
- package/dist/providers/stellar/index.js.map +1 -0
- package/dist/providers/stellar/index.mjs +301 -0
- package/dist/providers/stellar/index.mjs.map +1 -0
- package/dist/react/index.d.mts +73 -0
- package/dist/react/index.d.ts +73 -0
- package/dist/react/index.js +1218 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/index.mjs +1211 -0
- package/dist/react/index.mjs.map +1 -0
- package/dist/utils/index.d.mts +103 -0
- package/dist/utils/index.d.ts +103 -0
- package/dist/utils/index.js +575 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/index.mjs +562 -0
- package/dist/utils/index.mjs.map +1 -0
- package/package.json +149 -0
- package/src/chains/index.ts +539 -0
- package/src/client/X402Client.ts +663 -0
- package/src/client/index.ts +1 -0
- package/src/index.ts +166 -0
- package/src/providers/evm/index.ts +394 -0
- package/src/providers/near/index.ts +664 -0
- package/src/providers/solana/index.ts +489 -0
- package/src/providers/stellar/index.ts +376 -0
- package/src/react/index.tsx +417 -0
- package/src/types/index.ts +561 -0
- package/src/utils/index.ts +20 -0
- package/src/utils/x402.ts +295 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* uvd-x402-sdk
|
|
3
|
+
*
|
|
4
|
+
* x402 Payment SDK - Gasless crypto payments using the Ultravioleta facilitator.
|
|
5
|
+
*
|
|
6
|
+
* Supports 15 blockchain networks:
|
|
7
|
+
* - EVM (11): Base, Ethereum, Polygon, Arbitrum, Optimism, Avalanche, Celo, HyperEVM, Unichain, Monad, BSC(disabled)
|
|
8
|
+
* - SVM (2): Solana, Fogo
|
|
9
|
+
* - Stellar (1): Stellar
|
|
10
|
+
* - NEAR (1): NEAR Protocol
|
|
11
|
+
*
|
|
12
|
+
* Supports both x402 v1 and v2 protocols.
|
|
13
|
+
*
|
|
14
|
+
* @example Basic usage (EVM)
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { X402Client } from 'uvd-x402-sdk';
|
|
17
|
+
*
|
|
18
|
+
* const client = new X402Client({ defaultChain: 'base' });
|
|
19
|
+
*
|
|
20
|
+
* // Connect wallet
|
|
21
|
+
* await client.connect('base');
|
|
22
|
+
*
|
|
23
|
+
* // Create payment
|
|
24
|
+
* const result = await client.createPayment({
|
|
25
|
+
* recipient: '0x...',
|
|
26
|
+
* amount: '10.00',
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* // Use result.paymentHeader in X-PAYMENT HTTP header
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @example With SVM (Solana/Fogo)
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { SVMProvider } from 'uvd-x402-sdk/solana';
|
|
35
|
+
* import { getChainByName } from 'uvd-x402-sdk';
|
|
36
|
+
*
|
|
37
|
+
* const svm = new SVMProvider();
|
|
38
|
+
* const address = await svm.connect();
|
|
39
|
+
*
|
|
40
|
+
* // Solana payment
|
|
41
|
+
* const solanaConfig = getChainByName('solana')!;
|
|
42
|
+
* const payload = await svm.signPayment(paymentInfo, solanaConfig);
|
|
43
|
+
* const header = svm.encodePaymentHeader(payload, solanaConfig);
|
|
44
|
+
*
|
|
45
|
+
* // Fogo payment (same provider, different config)
|
|
46
|
+
* const fogoConfig = getChainByName('fogo')!;
|
|
47
|
+
* const fogoPayload = await svm.signPayment(paymentInfo, fogoConfig);
|
|
48
|
+
* const fogoHeader = svm.encodePaymentHeader(fogoPayload, fogoConfig);
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @example With NEAR
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { NEARProvider } from 'uvd-x402-sdk/near';
|
|
54
|
+
* import { getChainByName } from 'uvd-x402-sdk';
|
|
55
|
+
*
|
|
56
|
+
* const near = new NEARProvider();
|
|
57
|
+
* const accountId = await near.connect();
|
|
58
|
+
* const nearConfig = getChainByName('near')!;
|
|
59
|
+
* const payload = await near.signPayment(paymentInfo, nearConfig);
|
|
60
|
+
* const header = near.encodePaymentHeader(payload);
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @example With React
|
|
64
|
+
* ```tsx
|
|
65
|
+
* import { X402Provider, useX402, usePayment } from 'uvd-x402-sdk/react';
|
|
66
|
+
*
|
|
67
|
+
* function App() {
|
|
68
|
+
* return (
|
|
69
|
+
* <X402Provider>
|
|
70
|
+
* <PaymentButton />
|
|
71
|
+
* </X402Provider>
|
|
72
|
+
* );
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* @packageDocumentation
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
// Main client
|
|
80
|
+
export { X402Client } from './client';
|
|
81
|
+
|
|
82
|
+
// Chain configuration
|
|
83
|
+
export {
|
|
84
|
+
SUPPORTED_CHAINS,
|
|
85
|
+
DEFAULT_CHAIN,
|
|
86
|
+
DEFAULT_FACILITATOR_URL,
|
|
87
|
+
getChainById,
|
|
88
|
+
getChainByName,
|
|
89
|
+
isChainSupported,
|
|
90
|
+
getEnabledChains,
|
|
91
|
+
getChainsByNetworkType,
|
|
92
|
+
getEVMChainIds,
|
|
93
|
+
getSVMChains,
|
|
94
|
+
isSVMChain,
|
|
95
|
+
getNetworkType,
|
|
96
|
+
getExplorerTxUrl,
|
|
97
|
+
getExplorerAddressUrl,
|
|
98
|
+
} from './chains';
|
|
99
|
+
|
|
100
|
+
// x402 utilities
|
|
101
|
+
export {
|
|
102
|
+
detectX402Version,
|
|
103
|
+
chainToCAIP2,
|
|
104
|
+
caip2ToChain,
|
|
105
|
+
parseNetworkIdentifier,
|
|
106
|
+
encodeX402Header,
|
|
107
|
+
decodeX402Header,
|
|
108
|
+
createX402V1Header,
|
|
109
|
+
createX402V2Header,
|
|
110
|
+
createX402Header,
|
|
111
|
+
generatePaymentOptions,
|
|
112
|
+
isCAIP2Format,
|
|
113
|
+
convertX402Header,
|
|
114
|
+
} from './utils';
|
|
115
|
+
|
|
116
|
+
// Types
|
|
117
|
+
export type {
|
|
118
|
+
// Chain types
|
|
119
|
+
ChainConfig,
|
|
120
|
+
USDCConfig,
|
|
121
|
+
NativeCurrency,
|
|
122
|
+
NetworkType,
|
|
123
|
+
|
|
124
|
+
// Wallet types
|
|
125
|
+
WalletState,
|
|
126
|
+
WalletAdapter,
|
|
127
|
+
EIP712Domain,
|
|
128
|
+
EIP712Types,
|
|
129
|
+
|
|
130
|
+
// Payment types
|
|
131
|
+
PaymentInfo,
|
|
132
|
+
PaymentRequest,
|
|
133
|
+
PaymentResult,
|
|
134
|
+
PaymentPayload,
|
|
135
|
+
EVMPaymentPayload,
|
|
136
|
+
SolanaPaymentPayload,
|
|
137
|
+
StellarPaymentPayload,
|
|
138
|
+
NEARPaymentPayload,
|
|
139
|
+
|
|
140
|
+
// x402 header types (v1 and v2)
|
|
141
|
+
X402Version,
|
|
142
|
+
X402Header,
|
|
143
|
+
X402HeaderV1,
|
|
144
|
+
X402HeaderV2,
|
|
145
|
+
X402PaymentOption,
|
|
146
|
+
X402PayloadData,
|
|
147
|
+
X402EVMPayload,
|
|
148
|
+
X402SolanaPayload,
|
|
149
|
+
X402StellarPayload,
|
|
150
|
+
X402NEARPayload,
|
|
151
|
+
|
|
152
|
+
// Config types
|
|
153
|
+
X402ClientConfig,
|
|
154
|
+
MultiPaymentConfig,
|
|
155
|
+
NetworkBalance,
|
|
156
|
+
|
|
157
|
+
// Event types
|
|
158
|
+
X402Event,
|
|
159
|
+
X402EventData,
|
|
160
|
+
X402EventHandler,
|
|
161
|
+
|
|
162
|
+
// Error types
|
|
163
|
+
X402ErrorCode,
|
|
164
|
+
} from './types';
|
|
165
|
+
|
|
166
|
+
export { X402Error, DEFAULT_CONFIG, CAIP2_IDENTIFIERS, CAIP2_TO_CHAIN } from './types';
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* uvd-x402-sdk - EVM Provider
|
|
3
|
+
*
|
|
4
|
+
* Provides EVM wallet connection and payment creation.
|
|
5
|
+
* Uses EIP-712 typed data signing for ERC-3009 TransferWithAuthorization.
|
|
6
|
+
*
|
|
7
|
+
* Supports MetaMask, Rabby, and other injected EVM wallets.
|
|
8
|
+
* WalletConnect support is available for mobile wallets.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { EVMProvider } from 'uvd-x402-sdk/evm';
|
|
13
|
+
*
|
|
14
|
+
* const evm = new EVMProvider();
|
|
15
|
+
*
|
|
16
|
+
* // Connect to Base
|
|
17
|
+
* const address = await evm.connect('base');
|
|
18
|
+
*
|
|
19
|
+
* // Create payment
|
|
20
|
+
* const paymentHeader = await evm.signPayment(paymentInfo, chainConfig);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { ethers } from 'ethers';
|
|
25
|
+
import type {
|
|
26
|
+
ChainConfig,
|
|
27
|
+
PaymentInfo,
|
|
28
|
+
EVMPaymentPayload,
|
|
29
|
+
WalletAdapter,
|
|
30
|
+
} from '../../types';
|
|
31
|
+
import { X402Error } from '../../types';
|
|
32
|
+
import { getChainByName, getChainById } from '../../chains';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Ethereum provider interface
|
|
36
|
+
*/
|
|
37
|
+
interface EthereumProvider {
|
|
38
|
+
request: (args: { method: string; params?: unknown[] }) => Promise<unknown>;
|
|
39
|
+
on?: (event: string, handler: (...args: unknown[]) => void) => void;
|
|
40
|
+
removeListener?: (event: string, handler: (...args: unknown[]) => void) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* EVMProvider - Wallet adapter for EVM chains
|
|
45
|
+
*/
|
|
46
|
+
export class EVMProvider implements WalletAdapter {
|
|
47
|
+
readonly id = 'injected';
|
|
48
|
+
readonly name = 'EVM Wallet';
|
|
49
|
+
readonly networkType = 'evm' as const;
|
|
50
|
+
|
|
51
|
+
private provider: ethers.BrowserProvider | null = null;
|
|
52
|
+
private signer: ethers.Signer | null = null;
|
|
53
|
+
private address: string | null = null;
|
|
54
|
+
private chainId: number | null = null;
|
|
55
|
+
private chainName: string | null = null;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if an EVM wallet is available
|
|
59
|
+
*/
|
|
60
|
+
isAvailable(): boolean {
|
|
61
|
+
if (typeof window === 'undefined') return false;
|
|
62
|
+
return typeof (window as Window & { ethereum?: EthereumProvider }).ethereum !== 'undefined';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Connect to an EVM wallet
|
|
67
|
+
*/
|
|
68
|
+
async connect(chainName?: string): Promise<string> {
|
|
69
|
+
const targetChainName = chainName || 'base';
|
|
70
|
+
const chain = getChainByName(targetChainName);
|
|
71
|
+
|
|
72
|
+
if (!chain) {
|
|
73
|
+
throw new X402Error(`Unsupported chain: ${targetChainName}`, 'CHAIN_NOT_SUPPORTED');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (chain.networkType !== 'evm') {
|
|
77
|
+
throw new X402Error(`${targetChainName} is not an EVM chain`, 'CHAIN_NOT_SUPPORTED');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const ethereum = (window as Window & { ethereum?: EthereumProvider }).ethereum;
|
|
81
|
+
if (!ethereum) {
|
|
82
|
+
throw new X402Error(
|
|
83
|
+
'No Ethereum wallet found. Please install MetaMask or another EVM wallet.',
|
|
84
|
+
'WALLET_NOT_FOUND'
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
this.provider = new ethers.BrowserProvider(ethereum);
|
|
90
|
+
|
|
91
|
+
// Request account access
|
|
92
|
+
await this.provider.send('eth_requestAccounts', []);
|
|
93
|
+
|
|
94
|
+
// Switch to target chain
|
|
95
|
+
await this.switchChain(targetChainName);
|
|
96
|
+
|
|
97
|
+
// Get signer and address
|
|
98
|
+
this.signer = await this.provider.getSigner();
|
|
99
|
+
this.address = await this.signer.getAddress();
|
|
100
|
+
|
|
101
|
+
return this.address;
|
|
102
|
+
} catch (error: unknown) {
|
|
103
|
+
if (error instanceof Error) {
|
|
104
|
+
if (error.message.includes('User rejected') || (error as { code?: number }).code === 4001) {
|
|
105
|
+
throw new X402Error('Connection rejected by user', 'WALLET_CONNECTION_REJECTED');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
throw new X402Error(
|
|
109
|
+
`Failed to connect wallet: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
110
|
+
'UNKNOWN_ERROR',
|
|
111
|
+
error
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Disconnect from wallet
|
|
118
|
+
*/
|
|
119
|
+
async disconnect(): Promise<void> {
|
|
120
|
+
this.provider = null;
|
|
121
|
+
this.signer = null;
|
|
122
|
+
this.address = null;
|
|
123
|
+
this.chainId = null;
|
|
124
|
+
this.chainName = null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Switch to a different EVM chain
|
|
129
|
+
*/
|
|
130
|
+
async switchChain(chainName: string): Promise<void> {
|
|
131
|
+
const chain = getChainByName(chainName);
|
|
132
|
+
|
|
133
|
+
if (!chain) {
|
|
134
|
+
throw new X402Error(`Unsupported chain: ${chainName}`, 'CHAIN_NOT_SUPPORTED');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!this.provider) {
|
|
138
|
+
throw new X402Error('Wallet not connected', 'WALLET_NOT_CONNECTED');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await this.provider.send('wallet_switchEthereumChain', [{ chainId: chain.chainIdHex }]);
|
|
143
|
+
} catch (switchError: unknown) {
|
|
144
|
+
// Chain not added - try to add it
|
|
145
|
+
if ((switchError as { code?: number }).code === 4902) {
|
|
146
|
+
try {
|
|
147
|
+
await this.provider.send('wallet_addEthereumChain', [
|
|
148
|
+
{
|
|
149
|
+
chainId: chain.chainIdHex,
|
|
150
|
+
chainName: chain.displayName,
|
|
151
|
+
nativeCurrency: chain.nativeCurrency,
|
|
152
|
+
rpcUrls: [chain.rpcUrl],
|
|
153
|
+
blockExplorerUrls: [chain.explorerUrl],
|
|
154
|
+
},
|
|
155
|
+
]);
|
|
156
|
+
} catch (addError) {
|
|
157
|
+
throw new X402Error(
|
|
158
|
+
`Failed to add ${chain.displayName} network`,
|
|
159
|
+
'CHAIN_SWITCH_REJECTED',
|
|
160
|
+
addError
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
} else if ((switchError as { code?: number }).code === 4001) {
|
|
164
|
+
throw new X402Error('Network switch rejected by user', 'CHAIN_SWITCH_REJECTED');
|
|
165
|
+
} else {
|
|
166
|
+
throw new X402Error(
|
|
167
|
+
`Failed to switch to ${chain.displayName}`,
|
|
168
|
+
'CHAIN_SWITCH_REJECTED',
|
|
169
|
+
switchError
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this.chainId = chain.chainId;
|
|
175
|
+
this.chainName = chain.name;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get current address
|
|
180
|
+
*/
|
|
181
|
+
getAddress(): string | null {
|
|
182
|
+
return this.address;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get current chain ID
|
|
187
|
+
*/
|
|
188
|
+
getChainId(): number | null {
|
|
189
|
+
return this.chainId;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get current chain name
|
|
194
|
+
*/
|
|
195
|
+
getChainName(): string | null {
|
|
196
|
+
return this.chainName;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get USDC balance
|
|
201
|
+
*/
|
|
202
|
+
async getBalance(chainConfig: ChainConfig): Promise<string> {
|
|
203
|
+
if (!this.address) {
|
|
204
|
+
throw new X402Error('Wallet not connected', 'WALLET_NOT_CONNECTED');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Use public RPC for balance check
|
|
208
|
+
const publicProvider = new ethers.JsonRpcProvider(chainConfig.rpcUrl);
|
|
209
|
+
|
|
210
|
+
const usdcAbi = ['function balanceOf(address owner) view returns (uint256)'];
|
|
211
|
+
const usdcContract = new ethers.Contract(chainConfig.usdc.address, usdcAbi, publicProvider);
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const balance = await usdcContract.balanceOf(this.address);
|
|
215
|
+
const formatted = ethers.formatUnits(balance, chainConfig.usdc.decimals);
|
|
216
|
+
return parseFloat(formatted).toFixed(2);
|
|
217
|
+
} catch {
|
|
218
|
+
return '0.00';
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Create EVM payment (EIP-712 TransferWithAuthorization)
|
|
224
|
+
*/
|
|
225
|
+
async signPayment(paymentInfo: PaymentInfo, chainConfig: ChainConfig): Promise<string> {
|
|
226
|
+
if (!this.signer || !this.address) {
|
|
227
|
+
throw new X402Error('Wallet not connected', 'WALLET_NOT_CONNECTED');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Get recipient
|
|
231
|
+
const recipient = paymentInfo.recipients?.evm || paymentInfo.recipient;
|
|
232
|
+
|
|
233
|
+
// Generate random nonce
|
|
234
|
+
const nonceBytes = new Uint8Array(32);
|
|
235
|
+
if (typeof window !== 'undefined' && window.crypto) {
|
|
236
|
+
window.crypto.getRandomValues(nonceBytes);
|
|
237
|
+
} else {
|
|
238
|
+
for (let i = 0; i < 32; i++) {
|
|
239
|
+
nonceBytes[i] = Math.floor(Math.random() * 256);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const nonce = ethers.hexlify(nonceBytes);
|
|
243
|
+
|
|
244
|
+
// Set validity window
|
|
245
|
+
const validAfter = 0;
|
|
246
|
+
const validityWindowSeconds = chainConfig.name === 'base' ? 300 : 60;
|
|
247
|
+
const validBefore = Math.floor(Date.now() / 1000) + validityWindowSeconds;
|
|
248
|
+
|
|
249
|
+
// EIP-712 domain
|
|
250
|
+
const domain = {
|
|
251
|
+
name: chainConfig.usdc.name,
|
|
252
|
+
version: chainConfig.usdc.version,
|
|
253
|
+
chainId: chainConfig.chainId,
|
|
254
|
+
verifyingContract: chainConfig.usdc.address,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// EIP-712 types for TransferWithAuthorization (ERC-3009)
|
|
258
|
+
const types = {
|
|
259
|
+
TransferWithAuthorization: [
|
|
260
|
+
{ name: 'from', type: 'address' },
|
|
261
|
+
{ name: 'to', type: 'address' },
|
|
262
|
+
{ name: 'value', type: 'uint256' },
|
|
263
|
+
{ name: 'validAfter', type: 'uint256' },
|
|
264
|
+
{ name: 'validBefore', type: 'uint256' },
|
|
265
|
+
{ name: 'nonce', type: 'bytes32' },
|
|
266
|
+
],
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// Parse amount
|
|
270
|
+
const value = ethers.parseUnits(paymentInfo.amount, chainConfig.usdc.decimals);
|
|
271
|
+
const from = this.address;
|
|
272
|
+
const to = ethers.getAddress(recipient);
|
|
273
|
+
|
|
274
|
+
// Message to sign
|
|
275
|
+
const message = {
|
|
276
|
+
from,
|
|
277
|
+
to,
|
|
278
|
+
value,
|
|
279
|
+
validAfter,
|
|
280
|
+
validBefore,
|
|
281
|
+
nonce,
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// Sign the EIP-712 message
|
|
285
|
+
let signature: string;
|
|
286
|
+
try {
|
|
287
|
+
signature = await this.signer.signTypedData(domain, types, message);
|
|
288
|
+
} catch (error: unknown) {
|
|
289
|
+
if (error instanceof Error && (error.message.includes('User rejected') || (error as { code?: number }).code === 4001)) {
|
|
290
|
+
throw new X402Error('Signature rejected by user', 'SIGNATURE_REJECTED');
|
|
291
|
+
}
|
|
292
|
+
throw new X402Error(
|
|
293
|
+
`Failed to sign payment: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
294
|
+
'PAYMENT_FAILED',
|
|
295
|
+
error
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const sig = ethers.Signature.from(signature);
|
|
300
|
+
|
|
301
|
+
// Construct payload
|
|
302
|
+
const payload: EVMPaymentPayload = {
|
|
303
|
+
from,
|
|
304
|
+
to,
|
|
305
|
+
value: value.toString(),
|
|
306
|
+
validAfter,
|
|
307
|
+
validBefore,
|
|
308
|
+
nonce,
|
|
309
|
+
v: sig.v,
|
|
310
|
+
r: sig.r,
|
|
311
|
+
s: sig.s,
|
|
312
|
+
chainId: chainConfig.chainId,
|
|
313
|
+
token: chainConfig.usdc.address,
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
return JSON.stringify(payload);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Encode EVM payment as X-PAYMENT header
|
|
321
|
+
*/
|
|
322
|
+
encodePaymentHeader(paymentPayload: string, chainConfig: ChainConfig): string {
|
|
323
|
+
const payload = JSON.parse(paymentPayload) as EVMPaymentPayload;
|
|
324
|
+
|
|
325
|
+
// Reconstruct full signature
|
|
326
|
+
const fullSignature = payload.r + payload.s.slice(2) + payload.v.toString(16).padStart(2, '0');
|
|
327
|
+
|
|
328
|
+
const x402Payload = {
|
|
329
|
+
x402Version: 1,
|
|
330
|
+
scheme: 'exact',
|
|
331
|
+
network: chainConfig.name,
|
|
332
|
+
payload: {
|
|
333
|
+
signature: fullSignature,
|
|
334
|
+
authorization: {
|
|
335
|
+
from: payload.from,
|
|
336
|
+
to: payload.to,
|
|
337
|
+
value: payload.value,
|
|
338
|
+
validAfter: payload.validAfter.toString(),
|
|
339
|
+
validBefore: payload.validBefore.toString(),
|
|
340
|
+
nonce: payload.nonce,
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
return btoa(JSON.stringify(x402Payload));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Setup event listeners for wallet events
|
|
350
|
+
*/
|
|
351
|
+
setupEventListeners(
|
|
352
|
+
onAccountsChanged?: (accounts: string[]) => void,
|
|
353
|
+
onChainChanged?: (chainId: number) => void
|
|
354
|
+
): () => void {
|
|
355
|
+
const ethereum = (window as Window & { ethereum?: EthereumProvider }).ethereum;
|
|
356
|
+
if (!ethereum?.on || !ethereum?.removeListener) {
|
|
357
|
+
return () => {};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const handleAccountsChanged = (accounts: unknown) => {
|
|
361
|
+
if (Array.isArray(accounts)) {
|
|
362
|
+
if (accounts.length === 0) {
|
|
363
|
+
this.disconnect();
|
|
364
|
+
} else if (accounts[0] !== this.address) {
|
|
365
|
+
this.address = accounts[0] as string;
|
|
366
|
+
}
|
|
367
|
+
onAccountsChanged?.(accounts as string[]);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const handleChainChanged = (chainIdHex: unknown) => {
|
|
372
|
+
if (typeof chainIdHex === 'string') {
|
|
373
|
+
const chainId = parseInt(chainIdHex, 16);
|
|
374
|
+
const chain = getChainById(chainId);
|
|
375
|
+
if (chain) {
|
|
376
|
+
this.chainId = chainId;
|
|
377
|
+
this.chainName = chain.name;
|
|
378
|
+
}
|
|
379
|
+
onChainChanged?.(chainId);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
ethereum.on('accountsChanged', handleAccountsChanged);
|
|
384
|
+
ethereum.on('chainChanged', handleChainChanged);
|
|
385
|
+
|
|
386
|
+
// Return cleanup function
|
|
387
|
+
return () => {
|
|
388
|
+
ethereum.removeListener!('accountsChanged', handleAccountsChanged);
|
|
389
|
+
ethereum.removeListener!('chainChanged', handleChainChanged);
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export default EVMProvider;
|