moltspay 1.2.1 → 1.4.0
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 +292 -34
- package/dist/cdp/index.d.mts +4 -4
- package/dist/cdp/index.d.ts +4 -4
- package/dist/cdp/index.js +110 -30368
- package/dist/cdp/index.js.map +1 -1
- package/dist/cdp/index.mjs +94 -30360
- package/dist/cdp/index.mjs.map +1 -1
- package/dist/cdp-DeohBe1o.d.ts +66 -0
- package/dist/cdp-p_eHuQpb.d.mts +66 -0
- package/dist/chains/index.d.mts +9 -8
- package/dist/chains/index.d.ts +9 -8
- package/dist/chains/index.js +86 -0
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/index.mjs +86 -0
- package/dist/chains/index.mjs.map +1 -1
- package/dist/cli/index.js +2746 -290
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +2752 -282
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +60 -4
- package/dist/client/index.d.ts +60 -4
- package/dist/client/index.js +734 -43
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +732 -41
- package/dist/client/index.mjs.map +1 -1
- package/dist/facilitators/index.d.mts +220 -39
- package/dist/facilitators/index.d.ts +220 -39
- package/dist/facilitators/index.js +897 -1
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +902 -1
- package/dist/facilitators/index.mjs.map +1 -1
- package/dist/{index-DgJPZMBG.d.mts → index-D_2FkLwV.d.mts} +6 -2
- package/dist/{index-DgJPZMBG.d.ts → index-D_2FkLwV.d.ts} +6 -2
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2238 -30837
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2167 -30766
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.d.mts +30 -3
- package/dist/server/index.d.ts +30 -3
- package/dist/server/index.js +1345 -54
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +1355 -54
- package/dist/server/index.mjs.map +1 -1
- package/dist/verify/index.d.mts +1 -1
- package/dist/verify/index.d.ts +1 -1
- package/dist/verify/index.js +86 -0
- package/dist/verify/index.js.map +1 -1
- package/dist/verify/index.mjs +86 -0
- package/dist/verify/index.mjs.map +1 -1
- package/dist/wallet/index.d.mts +3 -3
- package/dist/wallet/index.d.ts +3 -3
- package/dist/wallet/index.js +86 -0
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs +86 -0
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +8 -2
- package/schemas/moltspay.services.schema.json +27 -132
|
@@ -224,7 +224,889 @@ var CDPFacilitator = class extends BaseFacilitator {
|
|
|
224
224
|
}
|
|
225
225
|
};
|
|
226
226
|
|
|
227
|
+
// src/chains/index.ts
|
|
228
|
+
var CHAINS = {
|
|
229
|
+
// ============ Mainnet ============
|
|
230
|
+
base: {
|
|
231
|
+
name: "Base",
|
|
232
|
+
chainId: 8453,
|
|
233
|
+
rpc: "https://mainnet.base.org",
|
|
234
|
+
tokens: {
|
|
235
|
+
USDC: {
|
|
236
|
+
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
237
|
+
decimals: 6,
|
|
238
|
+
symbol: "USDC",
|
|
239
|
+
eip712Name: "USD Coin"
|
|
240
|
+
// EIP-712 domain name
|
|
241
|
+
},
|
|
242
|
+
USDT: {
|
|
243
|
+
address: "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2",
|
|
244
|
+
decimals: 6,
|
|
245
|
+
symbol: "USDT",
|
|
246
|
+
eip712Name: "Tether USD"
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
250
|
+
// deprecated, for backward compat
|
|
251
|
+
explorer: "https://basescan.org/address/",
|
|
252
|
+
explorerTx: "https://basescan.org/tx/",
|
|
253
|
+
avgBlockTime: 2
|
|
254
|
+
},
|
|
255
|
+
polygon: {
|
|
256
|
+
name: "Polygon",
|
|
257
|
+
chainId: 137,
|
|
258
|
+
rpc: "https://polygon-bor-rpc.publicnode.com",
|
|
259
|
+
tokens: {
|
|
260
|
+
USDC: {
|
|
261
|
+
address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
262
|
+
decimals: 6,
|
|
263
|
+
symbol: "USDC",
|
|
264
|
+
eip712Name: "USD Coin"
|
|
265
|
+
},
|
|
266
|
+
USDT: {
|
|
267
|
+
address: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
|
|
268
|
+
decimals: 6,
|
|
269
|
+
symbol: "USDT",
|
|
270
|
+
eip712Name: "(PoS) Tether USD"
|
|
271
|
+
// Polygon uses this name
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
usdc: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
275
|
+
explorer: "https://polygonscan.com/address/",
|
|
276
|
+
explorerTx: "https://polygonscan.com/tx/",
|
|
277
|
+
avgBlockTime: 2
|
|
278
|
+
},
|
|
279
|
+
// ============ Testnet ============
|
|
280
|
+
base_sepolia: {
|
|
281
|
+
name: "Base Sepolia",
|
|
282
|
+
chainId: 84532,
|
|
283
|
+
rpc: "https://sepolia.base.org",
|
|
284
|
+
tokens: {
|
|
285
|
+
USDC: {
|
|
286
|
+
address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
287
|
+
decimals: 6,
|
|
288
|
+
symbol: "USDC",
|
|
289
|
+
eip712Name: "USDC"
|
|
290
|
+
// Testnet USDC uses 'USDC' not 'USD Coin'
|
|
291
|
+
},
|
|
292
|
+
USDT: {
|
|
293
|
+
address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
294
|
+
// Same as USDC on testnet (no official USDT)
|
|
295
|
+
decimals: 6,
|
|
296
|
+
symbol: "USDT",
|
|
297
|
+
eip712Name: "USDC"
|
|
298
|
+
// Uses same contract as USDC
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
302
|
+
explorer: "https://sepolia.basescan.org/address/",
|
|
303
|
+
explorerTx: "https://sepolia.basescan.org/tx/",
|
|
304
|
+
avgBlockTime: 2
|
|
305
|
+
},
|
|
306
|
+
// ============ Tempo Testnet (Moderato) ============
|
|
307
|
+
tempo_moderato: {
|
|
308
|
+
name: "Tempo Moderato",
|
|
309
|
+
chainId: 42431,
|
|
310
|
+
rpc: "https://rpc.moderato.tempo.xyz",
|
|
311
|
+
tokens: {
|
|
312
|
+
// TIP-20 stablecoins on Tempo testnet (from mppx SDK)
|
|
313
|
+
// Note: Tempo uses USD as native gas token, not ETH
|
|
314
|
+
USDC: {
|
|
315
|
+
address: "0x20c0000000000000000000000000000000000000",
|
|
316
|
+
// pathUSD - primary testnet stablecoin
|
|
317
|
+
decimals: 6,
|
|
318
|
+
symbol: "USDC",
|
|
319
|
+
eip712Name: "pathUSD"
|
|
320
|
+
},
|
|
321
|
+
USDT: {
|
|
322
|
+
address: "0x20c0000000000000000000000000000000000001",
|
|
323
|
+
// alphaUSD
|
|
324
|
+
decimals: 6,
|
|
325
|
+
symbol: "USDT",
|
|
326
|
+
eip712Name: "alphaUSD"
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
usdc: "0x20c0000000000000000000000000000000000000",
|
|
330
|
+
explorer: "https://explore.testnet.tempo.xyz/address/",
|
|
331
|
+
explorerTx: "https://explore.testnet.tempo.xyz/tx/",
|
|
332
|
+
avgBlockTime: 0.5
|
|
333
|
+
// ~500ms finality
|
|
334
|
+
},
|
|
335
|
+
// ============ BNB Chain Testnet ============
|
|
336
|
+
bnb_testnet: {
|
|
337
|
+
name: "BNB Testnet",
|
|
338
|
+
chainId: 97,
|
|
339
|
+
rpc: "https://data-seed-prebsc-1-s1.binance.org:8545",
|
|
340
|
+
tokens: {
|
|
341
|
+
// Note: BNB uses 18 decimals for stablecoins (unlike Base/Polygon which use 6)
|
|
342
|
+
// Using official Binance-Peg testnet tokens
|
|
343
|
+
USDC: {
|
|
344
|
+
address: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
345
|
+
// Testnet USDC
|
|
346
|
+
decimals: 18,
|
|
347
|
+
symbol: "USDC",
|
|
348
|
+
eip712Name: "USD Coin"
|
|
349
|
+
},
|
|
350
|
+
USDT: {
|
|
351
|
+
address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
|
|
352
|
+
// Testnet USDT
|
|
353
|
+
decimals: 18,
|
|
354
|
+
symbol: "USDT",
|
|
355
|
+
eip712Name: "Tether USD"
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
usdc: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
359
|
+
explorer: "https://testnet.bscscan.com/address/",
|
|
360
|
+
explorerTx: "https://testnet.bscscan.com/tx/",
|
|
361
|
+
avgBlockTime: 3,
|
|
362
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
363
|
+
requiresApproval: true
|
|
364
|
+
},
|
|
365
|
+
// ============ BNB Chain Mainnet ============
|
|
366
|
+
bnb: {
|
|
367
|
+
name: "BNB Smart Chain",
|
|
368
|
+
chainId: 56,
|
|
369
|
+
rpc: "https://bsc-dataseed.binance.org",
|
|
370
|
+
tokens: {
|
|
371
|
+
// Note: BNB uses 18 decimals for stablecoins
|
|
372
|
+
USDC: {
|
|
373
|
+
address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
374
|
+
decimals: 18,
|
|
375
|
+
symbol: "USDC",
|
|
376
|
+
eip712Name: "USD Coin"
|
|
377
|
+
},
|
|
378
|
+
USDT: {
|
|
379
|
+
address: "0x55d398326f99059fF775485246999027B3197955",
|
|
380
|
+
decimals: 18,
|
|
381
|
+
symbol: "USDT",
|
|
382
|
+
eip712Name: "Tether USD"
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
usdc: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
386
|
+
explorer: "https://bscscan.com/address/",
|
|
387
|
+
explorerTx: "https://bscscan.com/tx/",
|
|
388
|
+
avgBlockTime: 3,
|
|
389
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
390
|
+
requiresApproval: true
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// src/facilitators/tempo.ts
|
|
395
|
+
var TRANSFER_EVENT_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
396
|
+
var TempoFacilitator = class extends BaseFacilitator {
|
|
397
|
+
name = "tempo";
|
|
398
|
+
displayName = "Tempo Testnet";
|
|
399
|
+
supportedNetworks = ["eip155:42431"];
|
|
400
|
+
// Tempo Moderato
|
|
401
|
+
rpcUrl;
|
|
402
|
+
constructor() {
|
|
403
|
+
super();
|
|
404
|
+
this.rpcUrl = CHAINS.tempo_moderato.rpc;
|
|
405
|
+
}
|
|
406
|
+
async healthCheck() {
|
|
407
|
+
const start = Date.now();
|
|
408
|
+
try {
|
|
409
|
+
const response = await fetch(this.rpcUrl, {
|
|
410
|
+
method: "POST",
|
|
411
|
+
headers: { "Content-Type": "application/json" },
|
|
412
|
+
body: JSON.stringify({
|
|
413
|
+
jsonrpc: "2.0",
|
|
414
|
+
method: "eth_chainId",
|
|
415
|
+
params: [],
|
|
416
|
+
id: 1
|
|
417
|
+
})
|
|
418
|
+
});
|
|
419
|
+
const data = await response.json();
|
|
420
|
+
const chainId = parseInt(data.result, 16);
|
|
421
|
+
if (chainId !== 42431) {
|
|
422
|
+
return { healthy: false, error: `Wrong chainId: ${chainId}` };
|
|
423
|
+
}
|
|
424
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
425
|
+
} catch (error) {
|
|
426
|
+
return { healthy: false, error: String(error) };
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async verify(paymentPayload, requirements) {
|
|
430
|
+
try {
|
|
431
|
+
const tempoPayload = paymentPayload.payload;
|
|
432
|
+
if (!tempoPayload?.txHash) {
|
|
433
|
+
return { valid: false, error: "Missing txHash in payment payload" };
|
|
434
|
+
}
|
|
435
|
+
const receipt = await this.getTransactionReceipt(tempoPayload.txHash);
|
|
436
|
+
if (!receipt) {
|
|
437
|
+
return { valid: false, error: "Transaction not found" };
|
|
438
|
+
}
|
|
439
|
+
if (receipt.status !== "0x1") {
|
|
440
|
+
return { valid: false, error: "Transaction failed" };
|
|
441
|
+
}
|
|
442
|
+
const transferLog = receipt.logs.find(
|
|
443
|
+
(log) => log.topics[0] === TRANSFER_EVENT_TOPIC
|
|
444
|
+
);
|
|
445
|
+
if (!transferLog) {
|
|
446
|
+
return { valid: false, error: "No Transfer event found" };
|
|
447
|
+
}
|
|
448
|
+
const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
|
|
449
|
+
const expectedTo = requirements.payTo.toLowerCase();
|
|
450
|
+
if (toAddress !== expectedTo) {
|
|
451
|
+
return {
|
|
452
|
+
valid: false,
|
|
453
|
+
error: `Wrong recipient: ${toAddress}, expected ${expectedTo}`
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
const amount = BigInt(transferLog.data);
|
|
457
|
+
const expectedAmount = BigInt(requirements.amount);
|
|
458
|
+
if (amount < expectedAmount) {
|
|
459
|
+
return {
|
|
460
|
+
valid: false,
|
|
461
|
+
error: `Insufficient amount: ${amount}, expected ${expectedAmount}`
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const tokenAddress = transferLog.address.toLowerCase();
|
|
465
|
+
const expectedToken = requirements.asset.toLowerCase();
|
|
466
|
+
if (tokenAddress !== expectedToken) {
|
|
467
|
+
return {
|
|
468
|
+
valid: false,
|
|
469
|
+
error: `Wrong token: ${tokenAddress}, expected ${expectedToken}`
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
valid: true,
|
|
474
|
+
details: {
|
|
475
|
+
txHash: tempoPayload.txHash,
|
|
476
|
+
from: "0x" + transferLog.topics[1].slice(26),
|
|
477
|
+
to: toAddress,
|
|
478
|
+
amount: amount.toString(),
|
|
479
|
+
token: tokenAddress
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
} catch (error) {
|
|
483
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async settle(paymentPayload, requirements) {
|
|
487
|
+
const verifyResult = await this.verify(paymentPayload, requirements);
|
|
488
|
+
if (!verifyResult.valid) {
|
|
489
|
+
return { success: false, error: verifyResult.error };
|
|
490
|
+
}
|
|
491
|
+
const tempoPayload = paymentPayload.payload;
|
|
492
|
+
return {
|
|
493
|
+
success: true,
|
|
494
|
+
transaction: tempoPayload.txHash,
|
|
495
|
+
status: "settled"
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
async getTransactionReceipt(txHash) {
|
|
499
|
+
const response = await fetch(this.rpcUrl, {
|
|
500
|
+
method: "POST",
|
|
501
|
+
headers: { "Content-Type": "application/json" },
|
|
502
|
+
body: JSON.stringify({
|
|
503
|
+
jsonrpc: "2.0",
|
|
504
|
+
method: "eth_getTransactionReceipt",
|
|
505
|
+
params: [txHash],
|
|
506
|
+
id: 1
|
|
507
|
+
})
|
|
508
|
+
});
|
|
509
|
+
const data = await response.json();
|
|
510
|
+
return data.result;
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
// src/facilitators/bnb.ts
|
|
515
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
516
|
+
var TRANSFER_EVENT_TOPIC2 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
517
|
+
var EIP712_DOMAIN = {
|
|
518
|
+
name: "MoltsPay",
|
|
519
|
+
version: "1"
|
|
520
|
+
};
|
|
521
|
+
var INTENT_TYPES = {
|
|
522
|
+
PaymentIntent: [
|
|
523
|
+
{ name: "from", type: "address" },
|
|
524
|
+
{ name: "to", type: "address" },
|
|
525
|
+
{ name: "amount", type: "uint256" },
|
|
526
|
+
{ name: "token", type: "address" },
|
|
527
|
+
{ name: "service", type: "string" },
|
|
528
|
+
{ name: "nonce", type: "uint256" },
|
|
529
|
+
{ name: "deadline", type: "uint256" }
|
|
530
|
+
]
|
|
531
|
+
};
|
|
532
|
+
var BNBFacilitator = class extends BaseFacilitator {
|
|
533
|
+
name = "bnb";
|
|
534
|
+
displayName = "BNB Smart Chain";
|
|
535
|
+
supportedNetworks = ["eip155:56", "eip155:97"];
|
|
536
|
+
// Mainnet + Testnet
|
|
537
|
+
serverPrivateKey;
|
|
538
|
+
spenderAddress = null;
|
|
539
|
+
chainConfigs;
|
|
540
|
+
constructor(serverPrivateKey) {
|
|
541
|
+
super();
|
|
542
|
+
this.serverPrivateKey = serverPrivateKey || process.env.BNB_SERVER_PRIVATE_KEY || "";
|
|
543
|
+
if (this.serverPrivateKey) {
|
|
544
|
+
const key = this.serverPrivateKey.startsWith("0x") ? this.serverPrivateKey : `0x${this.serverPrivateKey}`;
|
|
545
|
+
const account = privateKeyToAccount(key);
|
|
546
|
+
this.spenderAddress = account.address;
|
|
547
|
+
}
|
|
548
|
+
this.chainConfigs = {
|
|
549
|
+
56: { rpc: CHAINS.bnb.rpc, chain: CHAINS.bnb },
|
|
550
|
+
97: { rpc: CHAINS.bnb_testnet.rpc, chain: CHAINS.bnb_testnet }
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
async healthCheck() {
|
|
554
|
+
const start = Date.now();
|
|
555
|
+
try {
|
|
556
|
+
const response = await fetch(this.chainConfigs[56].rpc, {
|
|
557
|
+
method: "POST",
|
|
558
|
+
headers: { "Content-Type": "application/json" },
|
|
559
|
+
body: JSON.stringify({
|
|
560
|
+
jsonrpc: "2.0",
|
|
561
|
+
method: "eth_chainId",
|
|
562
|
+
params: [],
|
|
563
|
+
id: 1
|
|
564
|
+
})
|
|
565
|
+
});
|
|
566
|
+
const data = await response.json();
|
|
567
|
+
const chainId = parseInt(data.result, 16);
|
|
568
|
+
if (chainId !== 56) {
|
|
569
|
+
return { healthy: false, error: `Wrong chainId: ${chainId}` };
|
|
570
|
+
}
|
|
571
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
572
|
+
} catch (error) {
|
|
573
|
+
return { healthy: false, error: String(error) };
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Verify a payment intent signature (before service execution)
|
|
578
|
+
*
|
|
579
|
+
* This verifies:
|
|
580
|
+
* 1. Signature is valid for the intent
|
|
581
|
+
* 2. Client has approved server wallet
|
|
582
|
+
* 3. Client has sufficient balance
|
|
583
|
+
* 4. Intent hasn't expired
|
|
584
|
+
*/
|
|
585
|
+
async verify(paymentPayload, requirements) {
|
|
586
|
+
try {
|
|
587
|
+
const bnbPayload = paymentPayload.payload;
|
|
588
|
+
if (!bnbPayload?.intent) {
|
|
589
|
+
return { valid: false, error: "Missing intent in payment payload" };
|
|
590
|
+
}
|
|
591
|
+
const { intent, chainId } = bnbPayload;
|
|
592
|
+
const config = this.chainConfigs[chainId];
|
|
593
|
+
if (!config) {
|
|
594
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
595
|
+
}
|
|
596
|
+
if (intent.deadline < Date.now()) {
|
|
597
|
+
return { valid: false, error: "Intent expired" };
|
|
598
|
+
}
|
|
599
|
+
const recoveredAddress = await this.recoverIntentSigner(intent, chainId);
|
|
600
|
+
if (recoveredAddress.toLowerCase() !== intent.from.toLowerCase()) {
|
|
601
|
+
return { valid: false, error: "Invalid signature" };
|
|
602
|
+
}
|
|
603
|
+
if (intent.to.toLowerCase() !== requirements.payTo.toLowerCase()) {
|
|
604
|
+
return { valid: false, error: `Wrong recipient: ${intent.to}` };
|
|
605
|
+
}
|
|
606
|
+
if (BigInt(intent.amount) < BigInt(requirements.amount)) {
|
|
607
|
+
return { valid: false, error: `Insufficient amount: ${intent.amount}` };
|
|
608
|
+
}
|
|
609
|
+
if (intent.token.toLowerCase() !== requirements.asset.toLowerCase()) {
|
|
610
|
+
return { valid: false, error: `Wrong token: ${intent.token}` };
|
|
611
|
+
}
|
|
612
|
+
const serverAddress = await this.getServerAddress();
|
|
613
|
+
const allowance = await this.getAllowance(intent.from, serverAddress, intent.token, config.rpc);
|
|
614
|
+
if (BigInt(allowance) < BigInt(intent.amount)) {
|
|
615
|
+
return { valid: false, error: "Insufficient allowance. Run: npx moltspay init --chain bnb" };
|
|
616
|
+
}
|
|
617
|
+
const balance = await this.getBalance(intent.from, intent.token, config.rpc);
|
|
618
|
+
if (BigInt(balance) < BigInt(intent.amount)) {
|
|
619
|
+
return { valid: false, error: "Insufficient balance" };
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
valid: true,
|
|
623
|
+
details: {
|
|
624
|
+
from: intent.from,
|
|
625
|
+
to: intent.to,
|
|
626
|
+
amount: intent.amount,
|
|
627
|
+
token: intent.token,
|
|
628
|
+
service: intent.service,
|
|
629
|
+
nonce: intent.nonce,
|
|
630
|
+
deadline: intent.deadline
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
} catch (error) {
|
|
634
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Settle a payment by executing transferFrom
|
|
639
|
+
*
|
|
640
|
+
* This is called AFTER the service has been successfully delivered.
|
|
641
|
+
* Server pays gas, transfers tokens from client to provider.
|
|
642
|
+
*/
|
|
643
|
+
async settle(paymentPayload, requirements) {
|
|
644
|
+
if (!this.serverPrivateKey) {
|
|
645
|
+
return { success: false, error: "Server wallet not configured (BNB_SERVER_PRIVATE_KEY)" };
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
const verifyResult = await this.verify(paymentPayload, requirements);
|
|
649
|
+
if (!verifyResult.valid) {
|
|
650
|
+
return { success: false, error: verifyResult.error };
|
|
651
|
+
}
|
|
652
|
+
const bnbPayload = paymentPayload.payload;
|
|
653
|
+
const { intent, chainId } = bnbPayload;
|
|
654
|
+
const config = this.chainConfigs[chainId];
|
|
655
|
+
const txHash = await this.executeTransferFrom(
|
|
656
|
+
intent.from,
|
|
657
|
+
intent.to,
|
|
658
|
+
intent.amount,
|
|
659
|
+
intent.token,
|
|
660
|
+
config.rpc
|
|
661
|
+
);
|
|
662
|
+
return {
|
|
663
|
+
success: true,
|
|
664
|
+
transaction: txHash,
|
|
665
|
+
status: "settled"
|
|
666
|
+
};
|
|
667
|
+
} catch (error) {
|
|
668
|
+
return { success: false, error: `Settlement failed: ${error}` };
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Check if client has approved the server wallet
|
|
673
|
+
*/
|
|
674
|
+
async checkApproval(clientAddress, token, chainId) {
|
|
675
|
+
const config = this.chainConfigs[chainId];
|
|
676
|
+
if (!config) {
|
|
677
|
+
throw new Error(`Unsupported chainId: ${chainId}`);
|
|
678
|
+
}
|
|
679
|
+
const serverAddress = await this.getServerAddress();
|
|
680
|
+
const allowance = await this.getAllowance(clientAddress, serverAddress, token, config.rpc);
|
|
681
|
+
const minAllowance = BigInt("1000000000000000000000");
|
|
682
|
+
return {
|
|
683
|
+
approved: BigInt(allowance) >= minAllowance,
|
|
684
|
+
allowance
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Verify a completed transaction (for checking past payments)
|
|
689
|
+
*/
|
|
690
|
+
async verifyTransaction(txHash, expected, chainId) {
|
|
691
|
+
const config = this.chainConfigs[chainId];
|
|
692
|
+
if (!config) {
|
|
693
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
694
|
+
}
|
|
695
|
+
try {
|
|
696
|
+
const receipt = await this.getTransactionReceipt(txHash, config.rpc);
|
|
697
|
+
if (!receipt) {
|
|
698
|
+
return { valid: false, error: "Transaction not found" };
|
|
699
|
+
}
|
|
700
|
+
if (receipt.status !== "0x1") {
|
|
701
|
+
return { valid: false, error: "Transaction failed" };
|
|
702
|
+
}
|
|
703
|
+
const transferLog = receipt.logs.find(
|
|
704
|
+
(log) => log.topics[0] === TRANSFER_EVENT_TOPIC2 && log.address.toLowerCase() === expected.token.toLowerCase()
|
|
705
|
+
);
|
|
706
|
+
if (!transferLog) {
|
|
707
|
+
return { valid: false, error: "No Transfer event found" };
|
|
708
|
+
}
|
|
709
|
+
const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
|
|
710
|
+
if (toAddress !== expected.to.toLowerCase()) {
|
|
711
|
+
return { valid: false, error: `Wrong recipient: ${toAddress}` };
|
|
712
|
+
}
|
|
713
|
+
const amount = BigInt(transferLog.data);
|
|
714
|
+
if (amount < BigInt(expected.amount)) {
|
|
715
|
+
return { valid: false, error: `Insufficient amount: ${amount}` };
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
valid: true,
|
|
719
|
+
details: {
|
|
720
|
+
txHash,
|
|
721
|
+
from: "0x" + transferLog.topics[1].slice(26),
|
|
722
|
+
to: toAddress,
|
|
723
|
+
amount: amount.toString(),
|
|
724
|
+
token: transferLog.address
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
} catch (error) {
|
|
728
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
// ==================== Private Methods ====================
|
|
732
|
+
/**
|
|
733
|
+
* Get the server's spender address (public, for 402 responses)
|
|
734
|
+
* Returns cached value computed at construction time.
|
|
735
|
+
*/
|
|
736
|
+
getSpenderAddress() {
|
|
737
|
+
return this.spenderAddress;
|
|
738
|
+
}
|
|
739
|
+
async getServerAddress() {
|
|
740
|
+
const { ethers } = await import("ethers");
|
|
741
|
+
const wallet = new ethers.Wallet(this.serverPrivateKey);
|
|
742
|
+
return wallet.address;
|
|
743
|
+
}
|
|
744
|
+
async recoverIntentSigner(intent, chainId) {
|
|
745
|
+
const { ethers } = await import("ethers");
|
|
746
|
+
const domain = {
|
|
747
|
+
...EIP712_DOMAIN,
|
|
748
|
+
chainId
|
|
749
|
+
};
|
|
750
|
+
const message = {
|
|
751
|
+
from: intent.from,
|
|
752
|
+
to: intent.to,
|
|
753
|
+
amount: intent.amount,
|
|
754
|
+
token: intent.token,
|
|
755
|
+
service: intent.service,
|
|
756
|
+
nonce: intent.nonce,
|
|
757
|
+
deadline: intent.deadline
|
|
758
|
+
};
|
|
759
|
+
const recoveredAddress = ethers.verifyTypedData(
|
|
760
|
+
domain,
|
|
761
|
+
INTENT_TYPES,
|
|
762
|
+
message,
|
|
763
|
+
intent.signature
|
|
764
|
+
);
|
|
765
|
+
return recoveredAddress;
|
|
766
|
+
}
|
|
767
|
+
async getAllowance(owner, spender, token, rpcUrl) {
|
|
768
|
+
const selector = "0xdd62ed3e";
|
|
769
|
+
const ownerPadded = owner.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
770
|
+
const spenderPadded = spender.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
771
|
+
const data = selector + ownerPadded + spenderPadded;
|
|
772
|
+
const response = await fetch(rpcUrl, {
|
|
773
|
+
method: "POST",
|
|
774
|
+
headers: { "Content-Type": "application/json" },
|
|
775
|
+
body: JSON.stringify({
|
|
776
|
+
jsonrpc: "2.0",
|
|
777
|
+
method: "eth_call",
|
|
778
|
+
params: [{ to: token, data }, "latest"],
|
|
779
|
+
id: 1
|
|
780
|
+
})
|
|
781
|
+
});
|
|
782
|
+
const result = await response.json();
|
|
783
|
+
return result.result || "0x0";
|
|
784
|
+
}
|
|
785
|
+
async getBalance(account, token, rpcUrl) {
|
|
786
|
+
const selector = "0x70a08231";
|
|
787
|
+
const accountPadded = account.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
788
|
+
const data = selector + accountPadded;
|
|
789
|
+
const response = await fetch(rpcUrl, {
|
|
790
|
+
method: "POST",
|
|
791
|
+
headers: { "Content-Type": "application/json" },
|
|
792
|
+
body: JSON.stringify({
|
|
793
|
+
jsonrpc: "2.0",
|
|
794
|
+
method: "eth_call",
|
|
795
|
+
params: [{ to: token, data }, "latest"],
|
|
796
|
+
id: 1
|
|
797
|
+
})
|
|
798
|
+
});
|
|
799
|
+
const result = await response.json();
|
|
800
|
+
return result.result || "0x0";
|
|
801
|
+
}
|
|
802
|
+
async executeTransferFrom(from, to, amount, token, rpcUrl) {
|
|
803
|
+
const { ethers } = await import("ethers");
|
|
804
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
805
|
+
const wallet = new ethers.Wallet(this.serverPrivateKey, provider);
|
|
806
|
+
const tokenContract = new ethers.Contract(token, [
|
|
807
|
+
"function transferFrom(address from, address to, uint256 amount) returns (bool)"
|
|
808
|
+
], wallet);
|
|
809
|
+
const tx = await tokenContract.transferFrom(from, to, amount);
|
|
810
|
+
const receipt = await tx.wait();
|
|
811
|
+
return receipt.hash;
|
|
812
|
+
}
|
|
813
|
+
async getTransactionReceipt(txHash, rpcUrl) {
|
|
814
|
+
const response = await fetch(rpcUrl, {
|
|
815
|
+
method: "POST",
|
|
816
|
+
headers: { "Content-Type": "application/json" },
|
|
817
|
+
body: JSON.stringify({
|
|
818
|
+
jsonrpc: "2.0",
|
|
819
|
+
method: "eth_getTransactionReceipt",
|
|
820
|
+
params: [txHash],
|
|
821
|
+
id: 1
|
|
822
|
+
})
|
|
823
|
+
});
|
|
824
|
+
const data = await response.json();
|
|
825
|
+
return data.result;
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
function createIntentTypedData(intent, chainId) {
|
|
829
|
+
return {
|
|
830
|
+
domain: {
|
|
831
|
+
...EIP712_DOMAIN,
|
|
832
|
+
chainId
|
|
833
|
+
},
|
|
834
|
+
types: INTENT_TYPES,
|
|
835
|
+
primaryType: "PaymentIntent",
|
|
836
|
+
message: {
|
|
837
|
+
from: intent.from,
|
|
838
|
+
to: intent.to,
|
|
839
|
+
amount: intent.amount,
|
|
840
|
+
token: intent.token,
|
|
841
|
+
service: intent.service,
|
|
842
|
+
nonce: intent.nonce,
|
|
843
|
+
deadline: intent.deadline
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// src/facilitators/solana.ts
|
|
849
|
+
import {
|
|
850
|
+
Connection as Connection2,
|
|
851
|
+
PublicKey as PublicKey2,
|
|
852
|
+
Transaction,
|
|
853
|
+
VersionedTransaction
|
|
854
|
+
} from "@solana/web3.js";
|
|
855
|
+
import {
|
|
856
|
+
getAssociatedTokenAddress,
|
|
857
|
+
createTransferCheckedInstruction,
|
|
858
|
+
getAccount,
|
|
859
|
+
createAssociatedTokenAccountInstruction
|
|
860
|
+
} from "@solana/spl-token";
|
|
861
|
+
|
|
862
|
+
// src/chains/solana.ts
|
|
863
|
+
import { Connection, PublicKey } from "@solana/web3.js";
|
|
864
|
+
var SOLANA_CHAINS = {
|
|
865
|
+
solana: {
|
|
866
|
+
name: "Solana Mainnet",
|
|
867
|
+
cluster: "mainnet-beta",
|
|
868
|
+
rpc: "https://api.mainnet-beta.solana.com",
|
|
869
|
+
explorer: "https://solscan.io/account/",
|
|
870
|
+
explorerTx: "https://solscan.io/tx/",
|
|
871
|
+
tokens: {
|
|
872
|
+
USDC: {
|
|
873
|
+
mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
874
|
+
// Circle official USDC
|
|
875
|
+
decimals: 6
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
},
|
|
879
|
+
solana_devnet: {
|
|
880
|
+
name: "Solana Devnet",
|
|
881
|
+
cluster: "devnet",
|
|
882
|
+
rpc: "https://api.devnet.solana.com",
|
|
883
|
+
explorer: "https://solscan.io/account/",
|
|
884
|
+
explorerTx: "https://solscan.io/tx/",
|
|
885
|
+
tokens: {
|
|
886
|
+
USDC: {
|
|
887
|
+
// Circle's devnet USDC (if not available, we'll deploy our own test token)
|
|
888
|
+
mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
889
|
+
decimals: 6
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
// src/facilitators/solana.ts
|
|
896
|
+
var SolanaFacilitator = class extends BaseFacilitator {
|
|
897
|
+
name = "solana";
|
|
898
|
+
displayName = "Solana Direct";
|
|
899
|
+
supportedNetworks = ["solana:mainnet", "solana:devnet"];
|
|
900
|
+
connections = /* @__PURE__ */ new Map();
|
|
901
|
+
feePayerKeypair;
|
|
902
|
+
constructor(config) {
|
|
903
|
+
super();
|
|
904
|
+
this.feePayerKeypair = config?.feePayerKeypair;
|
|
905
|
+
for (const [chain, config2] of Object.entries(SOLANA_CHAINS)) {
|
|
906
|
+
this.connections.set(
|
|
907
|
+
chain,
|
|
908
|
+
new Connection2(config2.rpc, "confirmed")
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
if (this.feePayerKeypair) {
|
|
912
|
+
console.log(`[SolanaFacilitator] Gasless mode enabled. Fee payer: ${this.feePayerKeypair.publicKey.toBase58()}`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Get fee payer public key (for gasless transactions)
|
|
917
|
+
*/
|
|
918
|
+
getFeePayerPubkey() {
|
|
919
|
+
return this.feePayerKeypair?.publicKey.toBase58() || null;
|
|
920
|
+
}
|
|
921
|
+
getConnection(chain) {
|
|
922
|
+
const conn = this.connections.get(chain);
|
|
923
|
+
if (!conn) {
|
|
924
|
+
throw new Error(`No connection for chain: ${chain}`);
|
|
925
|
+
}
|
|
926
|
+
return conn;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Convert our chain name to network identifier
|
|
930
|
+
*/
|
|
931
|
+
static chainToNetwork(chain) {
|
|
932
|
+
return chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Convert network identifier to chain name
|
|
936
|
+
*/
|
|
937
|
+
static networkToChain(network) {
|
|
938
|
+
if (network === "solana:mainnet") return "solana";
|
|
939
|
+
if (network === "solana:devnet") return "solana_devnet";
|
|
940
|
+
return null;
|
|
941
|
+
}
|
|
942
|
+
async healthCheck() {
|
|
943
|
+
const start = Date.now();
|
|
944
|
+
try {
|
|
945
|
+
const conn = this.getConnection("solana_devnet");
|
|
946
|
+
await conn.getSlot();
|
|
947
|
+
return {
|
|
948
|
+
healthy: true,
|
|
949
|
+
latencyMs: Date.now() - start
|
|
950
|
+
};
|
|
951
|
+
} catch (error) {
|
|
952
|
+
return {
|
|
953
|
+
healthy: false,
|
|
954
|
+
error: error.message
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Verify a Solana payment
|
|
960
|
+
*
|
|
961
|
+
* Checks:
|
|
962
|
+
* 1. Transaction is valid and properly signed
|
|
963
|
+
* 2. Transfer instruction matches expected amount and recipient
|
|
964
|
+
*/
|
|
965
|
+
async verify(paymentPayload, requirements) {
|
|
966
|
+
try {
|
|
967
|
+
const solanaPayload = paymentPayload.payload;
|
|
968
|
+
if (!solanaPayload || !solanaPayload.signedTransaction) {
|
|
969
|
+
return { valid: false, error: "Missing signed transaction" };
|
|
970
|
+
}
|
|
971
|
+
const chain = solanaPayload.chain || "solana_devnet";
|
|
972
|
+
const chainConfig = SOLANA_CHAINS[chain];
|
|
973
|
+
if (!chainConfig) {
|
|
974
|
+
return { valid: false, error: `Invalid chain: ${chain}` };
|
|
975
|
+
}
|
|
976
|
+
const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
|
|
977
|
+
let tx;
|
|
978
|
+
try {
|
|
979
|
+
tx = Transaction.from(txBuffer);
|
|
980
|
+
} catch {
|
|
981
|
+
tx = VersionedTransaction.deserialize(txBuffer);
|
|
982
|
+
}
|
|
983
|
+
if (tx instanceof Transaction) {
|
|
984
|
+
const hasAnySignature = tx.signatures.some(
|
|
985
|
+
(sig) => sig.signature && !sig.signature.every((b) => b === 0)
|
|
986
|
+
);
|
|
987
|
+
if (!hasAnySignature) {
|
|
988
|
+
return { valid: false, error: "Transaction not signed" };
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
const expectedAmount = BigInt(requirements.amount);
|
|
992
|
+
const expectedRecipient = new PublicKey2(requirements.payTo);
|
|
993
|
+
return {
|
|
994
|
+
valid: true,
|
|
995
|
+
details: {
|
|
996
|
+
chain,
|
|
997
|
+
sender: solanaPayload.sender,
|
|
998
|
+
recipient: requirements.payTo,
|
|
999
|
+
amount: requirements.amount
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
return { valid: false, error: error.message };
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Settle a Solana payment
|
|
1008
|
+
*
|
|
1009
|
+
* Submits the signed transaction to the network.
|
|
1010
|
+
* In gasless mode, adds fee payer signature before submitting.
|
|
1011
|
+
*/
|
|
1012
|
+
async settle(paymentPayload, requirements) {
|
|
1013
|
+
try {
|
|
1014
|
+
const solanaPayload = paymentPayload.payload;
|
|
1015
|
+
if (!solanaPayload || !solanaPayload.signedTransaction) {
|
|
1016
|
+
return { success: false, error: "Missing signed transaction" };
|
|
1017
|
+
}
|
|
1018
|
+
const chain = solanaPayload.chain || "solana_devnet";
|
|
1019
|
+
const connection = this.getConnection(chain);
|
|
1020
|
+
const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
|
|
1021
|
+
let txToSend;
|
|
1022
|
+
try {
|
|
1023
|
+
const tx = Transaction.from(txBuffer);
|
|
1024
|
+
if (this.feePayerKeypair && tx.feePayer) {
|
|
1025
|
+
const feePayerPubkey = this.feePayerKeypair.publicKey.toBase58();
|
|
1026
|
+
const txFeePayer = tx.feePayer.toBase58();
|
|
1027
|
+
if (txFeePayer === feePayerPubkey) {
|
|
1028
|
+
console.log(`[SolanaFacilitator] Gasless mode: adding fee payer signature`);
|
|
1029
|
+
tx.partialSign(this.feePayerKeypair);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
txToSend = tx.serialize();
|
|
1033
|
+
} catch (e) {
|
|
1034
|
+
txToSend = txBuffer;
|
|
1035
|
+
}
|
|
1036
|
+
const signature = await connection.sendRawTransaction(txToSend, {
|
|
1037
|
+
skipPreflight: false,
|
|
1038
|
+
preflightCommitment: "confirmed"
|
|
1039
|
+
});
|
|
1040
|
+
const confirmation = await connection.confirmTransaction(signature, "confirmed");
|
|
1041
|
+
if (confirmation.value.err) {
|
|
1042
|
+
return {
|
|
1043
|
+
success: false,
|
|
1044
|
+
error: `Transaction failed: ${JSON.stringify(confirmation.value.err)}`,
|
|
1045
|
+
transaction: signature
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
return {
|
|
1049
|
+
success: true,
|
|
1050
|
+
transaction: signature,
|
|
1051
|
+
status: "confirmed"
|
|
1052
|
+
};
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
return { success: false, error: error.message };
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
supportsNetwork(network) {
|
|
1058
|
+
return this.supportedNetworks.includes(network);
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
|
|
1062
|
+
const chainConfig = SOLANA_CHAINS[chain];
|
|
1063
|
+
const connection = new Connection2(chainConfig.rpc, "confirmed");
|
|
1064
|
+
const mint = new PublicKey2(chainConfig.tokens.USDC.mint);
|
|
1065
|
+
const actualFeePayer = feePayerPubkey || senderPubkey;
|
|
1066
|
+
const senderATA = await getAssociatedTokenAddress(mint, senderPubkey);
|
|
1067
|
+
const recipientATA = await getAssociatedTokenAddress(mint, recipientPubkey);
|
|
1068
|
+
const transaction = new Transaction();
|
|
1069
|
+
try {
|
|
1070
|
+
await getAccount(connection, recipientATA);
|
|
1071
|
+
} catch {
|
|
1072
|
+
transaction.add(
|
|
1073
|
+
createAssociatedTokenAccountInstruction(
|
|
1074
|
+
actualFeePayer,
|
|
1075
|
+
// payer (fee payer in gasless mode)
|
|
1076
|
+
recipientATA,
|
|
1077
|
+
// ata to create
|
|
1078
|
+
recipientPubkey,
|
|
1079
|
+
// owner
|
|
1080
|
+
mint
|
|
1081
|
+
// mint
|
|
1082
|
+
)
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
transaction.add(
|
|
1086
|
+
createTransferCheckedInstruction(
|
|
1087
|
+
senderATA,
|
|
1088
|
+
// source
|
|
1089
|
+
mint,
|
|
1090
|
+
// mint
|
|
1091
|
+
recipientATA,
|
|
1092
|
+
// destination
|
|
1093
|
+
senderPubkey,
|
|
1094
|
+
// owner (sender still authorizes the transfer)
|
|
1095
|
+
amount,
|
|
1096
|
+
// amount
|
|
1097
|
+
chainConfig.tokens.USDC.decimals
|
|
1098
|
+
// decimals
|
|
1099
|
+
)
|
|
1100
|
+
);
|
|
1101
|
+
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
|
|
1102
|
+
transaction.recentBlockhash = blockhash;
|
|
1103
|
+
transaction.feePayer = actualFeePayer;
|
|
1104
|
+
return transaction;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
227
1107
|
// src/facilitators/registry.ts
|
|
1108
|
+
import { Keypair as Keypair2 } from "@solana/web3.js";
|
|
1109
|
+
import bs58 from "bs58";
|
|
228
1110
|
var FacilitatorRegistry = class {
|
|
229
1111
|
factories = /* @__PURE__ */ new Map();
|
|
230
1112
|
instances = /* @__PURE__ */ new Map();
|
|
@@ -232,7 +1114,21 @@ var FacilitatorRegistry = class {
|
|
|
232
1114
|
roundRobinIndex = 0;
|
|
233
1115
|
constructor(selection) {
|
|
234
1116
|
this.registerFactory("cdp", (config) => new CDPFacilitator(config));
|
|
235
|
-
this.
|
|
1117
|
+
this.registerFactory("tempo", () => new TempoFacilitator());
|
|
1118
|
+
this.registerFactory("bnb", (config) => new BNBFacilitator(config?.serverPrivateKey));
|
|
1119
|
+
this.registerFactory("solana", (config) => {
|
|
1120
|
+
let feePayerKeypair;
|
|
1121
|
+
const feePayerKey = config?.feePayerPrivateKey || process.env.SOLANA_FEE_PAYER_KEY;
|
|
1122
|
+
if (feePayerKey) {
|
|
1123
|
+
try {
|
|
1124
|
+
feePayerKeypair = Keypair2.fromSecretKey(bs58.decode(feePayerKey));
|
|
1125
|
+
} catch (e) {
|
|
1126
|
+
console.warn(`[SolanaFacilitator] Invalid fee payer key: ${e.message}`);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return new SolanaFacilitator({ feePayerKeypair });
|
|
1130
|
+
});
|
|
1131
|
+
this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
|
|
236
1132
|
}
|
|
237
1133
|
/**
|
|
238
1134
|
* Register a new facilitator factory
|
|
@@ -458,10 +1354,15 @@ function createRegistry(selection) {
|
|
|
458
1354
|
return new FacilitatorRegistry(selection);
|
|
459
1355
|
}
|
|
460
1356
|
export {
|
|
1357
|
+
BNBFacilitator,
|
|
461
1358
|
BaseFacilitator,
|
|
462
1359
|
CDPFacilitator,
|
|
463
1360
|
FacilitatorRegistry,
|
|
1361
|
+
SolanaFacilitator,
|
|
1362
|
+
TempoFacilitator,
|
|
1363
|
+
createIntentTypedData,
|
|
464
1364
|
createRegistry,
|
|
1365
|
+
createSolanaPaymentTransaction,
|
|
465
1366
|
getDefaultRegistry
|
|
466
1367
|
};
|
|
467
1368
|
//# sourceMappingURL=index.mjs.map
|