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
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* uvd-x402-sdk - NEAR Provider
|
|
3
|
+
*
|
|
4
|
+
* Provides NEAR wallet connection and payment creation via MyNearWallet or Meteor.
|
|
5
|
+
* Uses NEP-366 meta-transactions where the facilitator pays all gas fees.
|
|
6
|
+
*
|
|
7
|
+
* NEP-366 Flow:
|
|
8
|
+
* 1. User creates a DelegateAction (ft_transfer on USDC contract)
|
|
9
|
+
* 2. User signs the DelegateAction with their ED25519 key
|
|
10
|
+
* 3. SignedDelegateAction is sent to facilitator
|
|
11
|
+
* 4. Facilitator wraps it and submits to NEAR, paying all gas
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { NEARProvider } from 'uvd-x402-sdk/near';
|
|
16
|
+
* import { getChainByName } from 'uvd-x402-sdk';
|
|
17
|
+
*
|
|
18
|
+
* const near = new NEARProvider();
|
|
19
|
+
*
|
|
20
|
+
* // Connect
|
|
21
|
+
* const accountId = await near.connect();
|
|
22
|
+
*
|
|
23
|
+
* // Create payment
|
|
24
|
+
* const chainConfig = getChainByName('near')!;
|
|
25
|
+
* const paymentPayload = await near.signPayment(paymentInfo, chainConfig);
|
|
26
|
+
* const header = near.encodePaymentHeader(paymentPayload);
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import type {
|
|
31
|
+
ChainConfig,
|
|
32
|
+
PaymentInfo,
|
|
33
|
+
NEARPaymentPayload,
|
|
34
|
+
WalletAdapter,
|
|
35
|
+
} from '../../types';
|
|
36
|
+
import { X402Error } from '../../types';
|
|
37
|
+
|
|
38
|
+
// NEAR configuration
|
|
39
|
+
const NEAR_CONFIG = {
|
|
40
|
+
networkId: 'mainnet',
|
|
41
|
+
nodeUrl: 'https://rpc.mainnet.near.org',
|
|
42
|
+
walletUrl: 'https://wallet.mainnet.near.org',
|
|
43
|
+
helperUrl: 'https://helper.mainnet.near.org',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// NEP-366 prefix: (2^30 + 366) as u32 little-endian
|
|
47
|
+
const NEP366_PREFIX = new Uint8Array([0x6e, 0x01, 0x00, 0x40]);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Simple Borsh serializer for NEAR transactions
|
|
51
|
+
*/
|
|
52
|
+
class BorshSerializer {
|
|
53
|
+
private buffer: number[] = [];
|
|
54
|
+
|
|
55
|
+
writeU8(value: number): void {
|
|
56
|
+
this.buffer.push(value & 0xff);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
writeU32(value: number): void {
|
|
60
|
+
this.buffer.push(value & 0xff);
|
|
61
|
+
this.buffer.push((value >> 8) & 0xff);
|
|
62
|
+
this.buffer.push((value >> 16) & 0xff);
|
|
63
|
+
this.buffer.push((value >> 24) & 0xff);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
writeU64(value: bigint | number): void {
|
|
67
|
+
const val = BigInt(value);
|
|
68
|
+
for (let i = 0; i < 8; i++) {
|
|
69
|
+
this.buffer.push(Number((val >> BigInt(i * 8)) & BigInt(0xff)));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
writeU128(value: bigint | number): void {
|
|
74
|
+
const val = BigInt(value);
|
|
75
|
+
for (let i = 0; i < 16; i++) {
|
|
76
|
+
this.buffer.push(Number((val >> BigInt(i * 8)) & BigInt(0xff)));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
writeString(value: string): void {
|
|
81
|
+
const encoded = new TextEncoder().encode(value);
|
|
82
|
+
this.writeU32(encoded.length);
|
|
83
|
+
this.buffer.push(...encoded);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
writeFixedBytes(data: Uint8Array): void {
|
|
87
|
+
this.buffer.push(...data);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
writeBytes(data: Uint8Array): void {
|
|
91
|
+
this.writeU32(data.length);
|
|
92
|
+
this.buffer.push(...data);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getBytes(): Uint8Array {
|
|
96
|
+
return new Uint8Array(this.buffer);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Serialize a NonDelegateAction for ft_transfer (NEP-366)
|
|
102
|
+
*/
|
|
103
|
+
function serializeNonDelegateAction(
|
|
104
|
+
receiverId: string,
|
|
105
|
+
amount: bigint,
|
|
106
|
+
memo?: string
|
|
107
|
+
): Uint8Array {
|
|
108
|
+
const args: Record<string, string> = {
|
|
109
|
+
receiver_id: receiverId,
|
|
110
|
+
amount: amount.toString(),
|
|
111
|
+
};
|
|
112
|
+
if (memo) {
|
|
113
|
+
args.memo = memo;
|
|
114
|
+
}
|
|
115
|
+
const argsJson = new TextEncoder().encode(JSON.stringify(args));
|
|
116
|
+
|
|
117
|
+
const ser = new BorshSerializer();
|
|
118
|
+
ser.writeU8(2); // FunctionCall action type
|
|
119
|
+
ser.writeString('ft_transfer');
|
|
120
|
+
ser.writeBytes(argsJson);
|
|
121
|
+
ser.writeU64(BigInt(30_000_000_000_000)); // 30 TGas
|
|
122
|
+
ser.writeU128(BigInt(1)); // 1 yoctoNEAR deposit (required for ft_transfer)
|
|
123
|
+
return ser.getBytes();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Serialize a DelegateAction for NEP-366 meta-transactions
|
|
128
|
+
*/
|
|
129
|
+
function serializeDelegateAction(
|
|
130
|
+
senderId: string,
|
|
131
|
+
receiverId: string,
|
|
132
|
+
actionsBytes: Uint8Array,
|
|
133
|
+
nonce: bigint,
|
|
134
|
+
maxBlockHeight: bigint,
|
|
135
|
+
publicKeyBytes: Uint8Array
|
|
136
|
+
): Uint8Array {
|
|
137
|
+
const ser = new BorshSerializer();
|
|
138
|
+
ser.writeString(senderId);
|
|
139
|
+
ser.writeString(receiverId);
|
|
140
|
+
ser.writeU32(1); // 1 action
|
|
141
|
+
ser.writeFixedBytes(actionsBytes);
|
|
142
|
+
ser.writeU64(nonce);
|
|
143
|
+
ser.writeU64(maxBlockHeight);
|
|
144
|
+
ser.writeU8(0); // ED25519 key type
|
|
145
|
+
ser.writeFixedBytes(publicKeyBytes);
|
|
146
|
+
return ser.getBytes();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Serialize a SignedDelegateAction for NEP-366
|
|
151
|
+
*/
|
|
152
|
+
function serializeSignedDelegateAction(
|
|
153
|
+
delegateActionBytes: Uint8Array,
|
|
154
|
+
signatureBytes: Uint8Array
|
|
155
|
+
): Uint8Array {
|
|
156
|
+
const ser = new BorshSerializer();
|
|
157
|
+
ser.writeFixedBytes(delegateActionBytes);
|
|
158
|
+
ser.writeU8(0); // ED25519 signature type
|
|
159
|
+
ser.writeFixedBytes(signatureBytes);
|
|
160
|
+
return ser.getBytes();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* SHA-256 hash function
|
|
165
|
+
*/
|
|
166
|
+
async function sha256(data: Uint8Array): Promise<Uint8Array> {
|
|
167
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data as BufferSource);
|
|
168
|
+
return new Uint8Array(hashBuffer);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* NEAR RPC call
|
|
173
|
+
*/
|
|
174
|
+
async function nearRpcCall<T>(
|
|
175
|
+
rpcUrl: string,
|
|
176
|
+
method: string,
|
|
177
|
+
params: Record<string, unknown>
|
|
178
|
+
): Promise<T> {
|
|
179
|
+
const response = await fetch(rpcUrl, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: { 'Content-Type': 'application/json' },
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
jsonrpc: '2.0',
|
|
184
|
+
id: 'dontcare',
|
|
185
|
+
method,
|
|
186
|
+
params,
|
|
187
|
+
}),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const data = await response.json();
|
|
191
|
+
if (data.error) {
|
|
192
|
+
throw new Error(`NEAR RPC error: ${JSON.stringify(data.error)}`);
|
|
193
|
+
}
|
|
194
|
+
return data.result as T;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* NEAR wallet selector interface
|
|
199
|
+
*/
|
|
200
|
+
interface NEARWalletSelector {
|
|
201
|
+
isSignedIn(): boolean;
|
|
202
|
+
getAccountId(): string | null;
|
|
203
|
+
signIn(options?: { contractId?: string }): Promise<void>;
|
|
204
|
+
signOut(): Promise<void>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* MyNearWallet interface
|
|
209
|
+
*/
|
|
210
|
+
interface MyNearWallet {
|
|
211
|
+
isInstalled?: () => boolean;
|
|
212
|
+
signIn?: (options?: { contractId?: string }) => Promise<{ accountId: string }>;
|
|
213
|
+
signOut?: () => Promise<void>;
|
|
214
|
+
getAccountId?: () => string | null;
|
|
215
|
+
signMessage?: (params: {
|
|
216
|
+
message: Uint8Array;
|
|
217
|
+
recipient: string;
|
|
218
|
+
nonce: Uint8Array;
|
|
219
|
+
}) => Promise<{ signature: Uint8Array; publicKey: string }>;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* NEARProvider - Wallet adapter for NEAR Protocol via MyNearWallet/Meteor
|
|
224
|
+
*/
|
|
225
|
+
export class NEARProvider implements WalletAdapter {
|
|
226
|
+
readonly id = 'near-wallet';
|
|
227
|
+
readonly name = 'NEAR Wallet';
|
|
228
|
+
readonly networkType = 'near' as const;
|
|
229
|
+
|
|
230
|
+
private accountId: string | null = null;
|
|
231
|
+
private publicKey: Uint8Array | null = null;
|
|
232
|
+
private rpcUrl: string = NEAR_CONFIG.nodeUrl;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Check if NEAR wallet is available
|
|
236
|
+
*/
|
|
237
|
+
isAvailable(): boolean {
|
|
238
|
+
if (typeof window === 'undefined') return false;
|
|
239
|
+
// Check for NEAR wallet selector or injected wallet
|
|
240
|
+
const win = window as Window & {
|
|
241
|
+
nearWalletSelector?: NEARWalletSelector;
|
|
242
|
+
myNearWallet?: MyNearWallet;
|
|
243
|
+
near?: { wallet?: NEARWalletSelector };
|
|
244
|
+
};
|
|
245
|
+
return !!(
|
|
246
|
+
win.nearWalletSelector ||
|
|
247
|
+
win.myNearWallet?.isInstalled?.() ||
|
|
248
|
+
win.near?.wallet
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Connect to NEAR wallet
|
|
254
|
+
*/
|
|
255
|
+
async connect(): Promise<string> {
|
|
256
|
+
// Try to get wallet from window
|
|
257
|
+
const win = window as Window & {
|
|
258
|
+
nearWalletSelector?: NEARWalletSelector;
|
|
259
|
+
myNearWallet?: MyNearWallet;
|
|
260
|
+
near?: { wallet?: NEARWalletSelector };
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
// Try NEAR wallet selector first
|
|
265
|
+
if (win.nearWalletSelector) {
|
|
266
|
+
if (!win.nearWalletSelector.isSignedIn()) {
|
|
267
|
+
await win.nearWalletSelector.signIn();
|
|
268
|
+
}
|
|
269
|
+
const accountId = win.nearWalletSelector.getAccountId();
|
|
270
|
+
if (!accountId) {
|
|
271
|
+
throw new X402Error('Failed to get NEAR account ID', 'WALLET_CONNECTION_REJECTED');
|
|
272
|
+
}
|
|
273
|
+
this.accountId = accountId;
|
|
274
|
+
await this.fetchPublicKey();
|
|
275
|
+
return accountId;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Try MyNearWallet
|
|
279
|
+
if (win.myNearWallet?.signIn) {
|
|
280
|
+
const result = await win.myNearWallet.signIn();
|
|
281
|
+
this.accountId = result.accountId;
|
|
282
|
+
await this.fetchPublicKey();
|
|
283
|
+
return result.accountId;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Try legacy near.wallet
|
|
287
|
+
if (win.near?.wallet) {
|
|
288
|
+
if (!win.near.wallet.isSignedIn()) {
|
|
289
|
+
await win.near.wallet.signIn();
|
|
290
|
+
}
|
|
291
|
+
const accountId = win.near.wallet.getAccountId();
|
|
292
|
+
if (!accountId) {
|
|
293
|
+
throw new X402Error('Failed to get NEAR account ID', 'WALLET_CONNECTION_REJECTED');
|
|
294
|
+
}
|
|
295
|
+
this.accountId = accountId;
|
|
296
|
+
await this.fetchPublicKey();
|
|
297
|
+
return accountId;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
throw new X402Error(
|
|
301
|
+
'No NEAR wallet found. Please install MyNearWallet or Meteor.',
|
|
302
|
+
'WALLET_NOT_FOUND'
|
|
303
|
+
);
|
|
304
|
+
} catch (error: unknown) {
|
|
305
|
+
if (error instanceof X402Error) throw error;
|
|
306
|
+
|
|
307
|
+
if (error instanceof Error) {
|
|
308
|
+
if (error.message.includes('User rejected') || error.message.includes('cancelled')) {
|
|
309
|
+
throw new X402Error('Connection rejected by user', 'WALLET_CONNECTION_REJECTED');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
throw new X402Error(
|
|
314
|
+
`Failed to connect NEAR wallet: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
315
|
+
'UNKNOWN_ERROR',
|
|
316
|
+
error
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Disconnect from NEAR wallet
|
|
323
|
+
*/
|
|
324
|
+
async disconnect(): Promise<void> {
|
|
325
|
+
const win = window as Window & {
|
|
326
|
+
nearWalletSelector?: NEARWalletSelector;
|
|
327
|
+
myNearWallet?: MyNearWallet;
|
|
328
|
+
near?: { wallet?: NEARWalletSelector };
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
if (win.nearWalletSelector) {
|
|
333
|
+
await win.nearWalletSelector.signOut();
|
|
334
|
+
} else if (win.myNearWallet?.signOut) {
|
|
335
|
+
await win.myNearWallet.signOut();
|
|
336
|
+
} else if (win.near?.wallet) {
|
|
337
|
+
await win.near.wallet.signOut();
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
// Ignore disconnect errors
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
this.accountId = null;
|
|
344
|
+
this.publicKey = null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Get current account ID
|
|
349
|
+
*/
|
|
350
|
+
getAddress(): string | null {
|
|
351
|
+
return this.accountId;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Get USDC balance on NEAR
|
|
356
|
+
*/
|
|
357
|
+
async getBalance(chainConfig: ChainConfig): Promise<string> {
|
|
358
|
+
if (!this.accountId) {
|
|
359
|
+
throw new X402Error('Wallet not connected', 'WALLET_NOT_CONNECTED');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
const args = JSON.stringify({ account_id: this.accountId });
|
|
364
|
+
const argsBase64 = btoa(args);
|
|
365
|
+
|
|
366
|
+
const result = await nearRpcCall<{ result: number[] }>(
|
|
367
|
+
chainConfig.rpcUrl || this.rpcUrl,
|
|
368
|
+
'query',
|
|
369
|
+
{
|
|
370
|
+
request_type: 'call_function',
|
|
371
|
+
finality: 'final',
|
|
372
|
+
account_id: chainConfig.usdc.address,
|
|
373
|
+
method_name: 'ft_balance_of',
|
|
374
|
+
args_base64: argsBase64,
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
const resultBytes = new Uint8Array(result.result);
|
|
379
|
+
const balanceStr = new TextDecoder().decode(resultBytes).replace(/"/g, '');
|
|
380
|
+
const balance = Number(balanceStr) / Math.pow(10, chainConfig.usdc.decimals);
|
|
381
|
+
|
|
382
|
+
return balance.toFixed(2);
|
|
383
|
+
} catch {
|
|
384
|
+
return '0.00';
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Create NEAR payment (NEP-366 SignedDelegateAction)
|
|
390
|
+
*
|
|
391
|
+
* User signs a DelegateAction that authorizes ft_transfer.
|
|
392
|
+
* Facilitator wraps this and submits to NEAR, paying all gas fees.
|
|
393
|
+
*/
|
|
394
|
+
async signPayment(paymentInfo: PaymentInfo, chainConfig: ChainConfig): Promise<string> {
|
|
395
|
+
if (!this.accountId || !this.publicKey) {
|
|
396
|
+
throw new X402Error('Wallet not connected', 'WALLET_NOT_CONNECTED');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Get recipient
|
|
400
|
+
const recipient = paymentInfo.recipients?.near || paymentInfo.recipient;
|
|
401
|
+
|
|
402
|
+
// Parse amount (6 decimals for USDC)
|
|
403
|
+
const amount = BigInt(Math.floor(parseFloat(paymentInfo.amount) * 1_000_000));
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
// Get access key nonce
|
|
407
|
+
const accessKey = await this.getAccessKeyNonce();
|
|
408
|
+
const nonce = BigInt(accessKey.nonce) + BigInt(1);
|
|
409
|
+
|
|
410
|
+
// Get current block height
|
|
411
|
+
const block = await this.getLatestBlock();
|
|
412
|
+
const maxBlockHeight = BigInt(block.header.height) + BigInt(1000);
|
|
413
|
+
|
|
414
|
+
// Serialize ft_transfer action
|
|
415
|
+
const actionBytes = serializeNonDelegateAction(
|
|
416
|
+
recipient,
|
|
417
|
+
amount,
|
|
418
|
+
'x402 payment via uvd-x402-sdk'
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
// Serialize DelegateAction
|
|
422
|
+
const delegateActionBytes = serializeDelegateAction(
|
|
423
|
+
this.accountId,
|
|
424
|
+
chainConfig.usdc.address, // USDC contract
|
|
425
|
+
actionBytes,
|
|
426
|
+
nonce,
|
|
427
|
+
maxBlockHeight,
|
|
428
|
+
this.publicKey
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
// Create hash for signing (NEP-366 prefix + delegateAction)
|
|
432
|
+
const hashInput = new Uint8Array(NEP366_PREFIX.length + delegateActionBytes.length);
|
|
433
|
+
hashInput.set(NEP366_PREFIX, 0);
|
|
434
|
+
hashInput.set(delegateActionBytes, NEP366_PREFIX.length);
|
|
435
|
+
const delegateHash = await sha256(hashInput);
|
|
436
|
+
|
|
437
|
+
// Sign the hash
|
|
438
|
+
const signature = await this.signMessage(delegateHash);
|
|
439
|
+
|
|
440
|
+
// Serialize SignedDelegateAction
|
|
441
|
+
const signedDelegateActionBytes = serializeSignedDelegateAction(
|
|
442
|
+
delegateActionBytes,
|
|
443
|
+
signature
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
// Base64 encode
|
|
447
|
+
const signedDelegateB64 = btoa(
|
|
448
|
+
String.fromCharCode(...signedDelegateActionBytes)
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
const payload: NEARPaymentPayload = {
|
|
452
|
+
signedDelegateAction: signedDelegateB64,
|
|
453
|
+
network: 'near',
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
return JSON.stringify(payload);
|
|
457
|
+
} catch (error: unknown) {
|
|
458
|
+
if (error instanceof X402Error) throw error;
|
|
459
|
+
|
|
460
|
+
if (error instanceof Error) {
|
|
461
|
+
if (error.message.includes('User rejected') || error.message.includes('cancelled')) {
|
|
462
|
+
throw new X402Error('Signature rejected by user', 'SIGNATURE_REJECTED');
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
throw new X402Error(
|
|
467
|
+
`Failed to create NEAR payment: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
468
|
+
'PAYMENT_FAILED',
|
|
469
|
+
error
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Encode NEAR payment as X-PAYMENT header
|
|
476
|
+
*/
|
|
477
|
+
encodePaymentHeader(paymentPayload: string): string {
|
|
478
|
+
const payload = JSON.parse(paymentPayload) as NEARPaymentPayload;
|
|
479
|
+
|
|
480
|
+
const x402Payload = {
|
|
481
|
+
x402Version: 1,
|
|
482
|
+
scheme: 'exact',
|
|
483
|
+
network: 'near',
|
|
484
|
+
payload: {
|
|
485
|
+
signedDelegateAction: payload.signedDelegateAction,
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
return btoa(JSON.stringify(x402Payload));
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Private helpers
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Fetch public key from NEAR RPC
|
|
496
|
+
*/
|
|
497
|
+
private async fetchPublicKey(): Promise<void> {
|
|
498
|
+
if (!this.accountId) return;
|
|
499
|
+
|
|
500
|
+
try {
|
|
501
|
+
const result = await nearRpcCall<{ keys: Array<{ public_key: string }> }>(
|
|
502
|
+
this.rpcUrl,
|
|
503
|
+
'query',
|
|
504
|
+
{
|
|
505
|
+
request_type: 'view_access_key_list',
|
|
506
|
+
finality: 'final',
|
|
507
|
+
account_id: this.accountId,
|
|
508
|
+
}
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
if (result.keys && result.keys.length > 0) {
|
|
512
|
+
// Get first full access key
|
|
513
|
+
const keyStr = result.keys[0].public_key;
|
|
514
|
+
// Remove ed25519: prefix and decode base58
|
|
515
|
+
const keyB58 = keyStr.replace('ed25519:', '');
|
|
516
|
+
this.publicKey = this.base58Decode(keyB58);
|
|
517
|
+
}
|
|
518
|
+
} catch {
|
|
519
|
+
// Will be set during signing if not available
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Get access key nonce from NEAR RPC
|
|
525
|
+
*/
|
|
526
|
+
private async getAccessKeyNonce(): Promise<{ nonce: number }> {
|
|
527
|
+
if (!this.accountId || !this.publicKey) {
|
|
528
|
+
throw new Error('Account not connected');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const publicKeyB58 = this.base58Encode(this.publicKey);
|
|
532
|
+
|
|
533
|
+
const result = await nearRpcCall<{ nonce: number }>(
|
|
534
|
+
this.rpcUrl,
|
|
535
|
+
'query',
|
|
536
|
+
{
|
|
537
|
+
request_type: 'view_access_key',
|
|
538
|
+
finality: 'final',
|
|
539
|
+
account_id: this.accountId,
|
|
540
|
+
public_key: `ed25519:${publicKeyB58}`,
|
|
541
|
+
}
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
return result;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Get latest block from NEAR RPC
|
|
549
|
+
*/
|
|
550
|
+
private async getLatestBlock(): Promise<{ header: { height: number } }> {
|
|
551
|
+
return nearRpcCall(this.rpcUrl, 'block', { finality: 'final' });
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Sign a message using the connected wallet
|
|
556
|
+
*/
|
|
557
|
+
private async signMessage(message: Uint8Array): Promise<Uint8Array> {
|
|
558
|
+
const win = window as Window & {
|
|
559
|
+
nearWalletSelector?: NEARWalletSelector;
|
|
560
|
+
myNearWallet?: MyNearWallet;
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
// Try MyNearWallet signMessage
|
|
564
|
+
if (win.myNearWallet?.signMessage) {
|
|
565
|
+
const result = await win.myNearWallet.signMessage({
|
|
566
|
+
message,
|
|
567
|
+
recipient: 'uvd-x402-sdk',
|
|
568
|
+
nonce: crypto.getRandomValues(new Uint8Array(32)),
|
|
569
|
+
});
|
|
570
|
+
return result.signature;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// For other wallets, we need to use the @near-js/crypto library
|
|
574
|
+
// This will be handled by the wallet selector
|
|
575
|
+
throw new X402Error(
|
|
576
|
+
'Signing not supported. Please use MyNearWallet or install @near-wallet-selector/core',
|
|
577
|
+
'PAYMENT_FAILED'
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Base58 decode (NEAR style)
|
|
583
|
+
*/
|
|
584
|
+
private base58Decode(str: string): Uint8Array {
|
|
585
|
+
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
586
|
+
const ALPHABET_MAP: Record<string, number> = {};
|
|
587
|
+
for (let i = 0; i < ALPHABET.length; i++) {
|
|
588
|
+
ALPHABET_MAP[ALPHABET[i]] = i;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
let bytes: number[] = [0];
|
|
592
|
+
for (let i = 0; i < str.length; i++) {
|
|
593
|
+
const value = ALPHABET_MAP[str[i]];
|
|
594
|
+
if (value === undefined) {
|
|
595
|
+
throw new Error(`Invalid base58 character: ${str[i]}`);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
for (let j = 0; j < bytes.length; j++) {
|
|
599
|
+
bytes[j] *= 58;
|
|
600
|
+
}
|
|
601
|
+
bytes[0] += value;
|
|
602
|
+
|
|
603
|
+
let carry = 0;
|
|
604
|
+
for (let j = 0; j < bytes.length; j++) {
|
|
605
|
+
bytes[j] += carry;
|
|
606
|
+
carry = bytes[j] >> 8;
|
|
607
|
+
bytes[j] &= 0xff;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
while (carry) {
|
|
611
|
+
bytes.push(carry & 0xff);
|
|
612
|
+
carry >>= 8;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Handle leading zeros
|
|
617
|
+
for (let i = 0; i < str.length && str[i] === '1'; i++) {
|
|
618
|
+
bytes.push(0);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return new Uint8Array(bytes.reverse());
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Base58 encode (NEAR style)
|
|
626
|
+
*/
|
|
627
|
+
private base58Encode(bytes: Uint8Array): string {
|
|
628
|
+
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
629
|
+
|
|
630
|
+
let digits = [0];
|
|
631
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
632
|
+
for (let j = 0; j < digits.length; j++) {
|
|
633
|
+
digits[j] <<= 8;
|
|
634
|
+
}
|
|
635
|
+
digits[0] += bytes[i];
|
|
636
|
+
|
|
637
|
+
let carry = 0;
|
|
638
|
+
for (let j = 0; j < digits.length; j++) {
|
|
639
|
+
digits[j] += carry;
|
|
640
|
+
carry = (digits[j] / 58) | 0;
|
|
641
|
+
digits[j] %= 58;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
while (carry) {
|
|
645
|
+
digits.push(carry % 58);
|
|
646
|
+
carry = (carry / 58) | 0;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Handle leading zeros
|
|
651
|
+
let str = '';
|
|
652
|
+
for (let i = 0; i < bytes.length && bytes[i] === 0; i++) {
|
|
653
|
+
str += '1';
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
657
|
+
str += ALPHABET[digits[i]];
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return str;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export default NEARProvider;
|