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
package/dist/server/index.mjs
CHANGED
|
@@ -229,7 +229,825 @@ var CDPFacilitator = class extends BaseFacilitator {
|
|
|
229
229
|
}
|
|
230
230
|
};
|
|
231
231
|
|
|
232
|
+
// src/chains/index.ts
|
|
233
|
+
var CHAINS = {
|
|
234
|
+
// ============ Mainnet ============
|
|
235
|
+
base: {
|
|
236
|
+
name: "Base",
|
|
237
|
+
chainId: 8453,
|
|
238
|
+
rpc: "https://mainnet.base.org",
|
|
239
|
+
tokens: {
|
|
240
|
+
USDC: {
|
|
241
|
+
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
242
|
+
decimals: 6,
|
|
243
|
+
symbol: "USDC",
|
|
244
|
+
eip712Name: "USD Coin"
|
|
245
|
+
// EIP-712 domain name
|
|
246
|
+
},
|
|
247
|
+
USDT: {
|
|
248
|
+
address: "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2",
|
|
249
|
+
decimals: 6,
|
|
250
|
+
symbol: "USDT",
|
|
251
|
+
eip712Name: "Tether USD"
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
255
|
+
// deprecated, for backward compat
|
|
256
|
+
explorer: "https://basescan.org/address/",
|
|
257
|
+
explorerTx: "https://basescan.org/tx/",
|
|
258
|
+
avgBlockTime: 2
|
|
259
|
+
},
|
|
260
|
+
polygon: {
|
|
261
|
+
name: "Polygon",
|
|
262
|
+
chainId: 137,
|
|
263
|
+
rpc: "https://polygon-bor-rpc.publicnode.com",
|
|
264
|
+
tokens: {
|
|
265
|
+
USDC: {
|
|
266
|
+
address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
267
|
+
decimals: 6,
|
|
268
|
+
symbol: "USDC",
|
|
269
|
+
eip712Name: "USD Coin"
|
|
270
|
+
},
|
|
271
|
+
USDT: {
|
|
272
|
+
address: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
|
|
273
|
+
decimals: 6,
|
|
274
|
+
symbol: "USDT",
|
|
275
|
+
eip712Name: "(PoS) Tether USD"
|
|
276
|
+
// Polygon uses this name
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
usdc: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
280
|
+
explorer: "https://polygonscan.com/address/",
|
|
281
|
+
explorerTx: "https://polygonscan.com/tx/",
|
|
282
|
+
avgBlockTime: 2
|
|
283
|
+
},
|
|
284
|
+
// ============ Testnet ============
|
|
285
|
+
base_sepolia: {
|
|
286
|
+
name: "Base Sepolia",
|
|
287
|
+
chainId: 84532,
|
|
288
|
+
rpc: "https://sepolia.base.org",
|
|
289
|
+
tokens: {
|
|
290
|
+
USDC: {
|
|
291
|
+
address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
292
|
+
decimals: 6,
|
|
293
|
+
symbol: "USDC",
|
|
294
|
+
eip712Name: "USDC"
|
|
295
|
+
// Testnet USDC uses 'USDC' not 'USD Coin'
|
|
296
|
+
},
|
|
297
|
+
USDT: {
|
|
298
|
+
address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
299
|
+
// Same as USDC on testnet (no official USDT)
|
|
300
|
+
decimals: 6,
|
|
301
|
+
symbol: "USDT",
|
|
302
|
+
eip712Name: "USDC"
|
|
303
|
+
// Uses same contract as USDC
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
307
|
+
explorer: "https://sepolia.basescan.org/address/",
|
|
308
|
+
explorerTx: "https://sepolia.basescan.org/tx/",
|
|
309
|
+
avgBlockTime: 2
|
|
310
|
+
},
|
|
311
|
+
// ============ Tempo Testnet (Moderato) ============
|
|
312
|
+
tempo_moderato: {
|
|
313
|
+
name: "Tempo Moderato",
|
|
314
|
+
chainId: 42431,
|
|
315
|
+
rpc: "https://rpc.moderato.tempo.xyz",
|
|
316
|
+
tokens: {
|
|
317
|
+
// TIP-20 stablecoins on Tempo testnet (from mppx SDK)
|
|
318
|
+
// Note: Tempo uses USD as native gas token, not ETH
|
|
319
|
+
USDC: {
|
|
320
|
+
address: "0x20c0000000000000000000000000000000000000",
|
|
321
|
+
// pathUSD - primary testnet stablecoin
|
|
322
|
+
decimals: 6,
|
|
323
|
+
symbol: "USDC",
|
|
324
|
+
eip712Name: "pathUSD"
|
|
325
|
+
},
|
|
326
|
+
USDT: {
|
|
327
|
+
address: "0x20c0000000000000000000000000000000000001",
|
|
328
|
+
// alphaUSD
|
|
329
|
+
decimals: 6,
|
|
330
|
+
symbol: "USDT",
|
|
331
|
+
eip712Name: "alphaUSD"
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
usdc: "0x20c0000000000000000000000000000000000000",
|
|
335
|
+
explorer: "https://explore.testnet.tempo.xyz/address/",
|
|
336
|
+
explorerTx: "https://explore.testnet.tempo.xyz/tx/",
|
|
337
|
+
avgBlockTime: 0.5
|
|
338
|
+
// ~500ms finality
|
|
339
|
+
},
|
|
340
|
+
// ============ BNB Chain Testnet ============
|
|
341
|
+
bnb_testnet: {
|
|
342
|
+
name: "BNB Testnet",
|
|
343
|
+
chainId: 97,
|
|
344
|
+
rpc: "https://data-seed-prebsc-1-s1.binance.org:8545",
|
|
345
|
+
tokens: {
|
|
346
|
+
// Note: BNB uses 18 decimals for stablecoins (unlike Base/Polygon which use 6)
|
|
347
|
+
// Using official Binance-Peg testnet tokens
|
|
348
|
+
USDC: {
|
|
349
|
+
address: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
350
|
+
// Testnet USDC
|
|
351
|
+
decimals: 18,
|
|
352
|
+
symbol: "USDC",
|
|
353
|
+
eip712Name: "USD Coin"
|
|
354
|
+
},
|
|
355
|
+
USDT: {
|
|
356
|
+
address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
|
|
357
|
+
// Testnet USDT
|
|
358
|
+
decimals: 18,
|
|
359
|
+
symbol: "USDT",
|
|
360
|
+
eip712Name: "Tether USD"
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
usdc: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
364
|
+
explorer: "https://testnet.bscscan.com/address/",
|
|
365
|
+
explorerTx: "https://testnet.bscscan.com/tx/",
|
|
366
|
+
avgBlockTime: 3,
|
|
367
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
368
|
+
requiresApproval: true
|
|
369
|
+
},
|
|
370
|
+
// ============ BNB Chain Mainnet ============
|
|
371
|
+
bnb: {
|
|
372
|
+
name: "BNB Smart Chain",
|
|
373
|
+
chainId: 56,
|
|
374
|
+
rpc: "https://bsc-dataseed.binance.org",
|
|
375
|
+
tokens: {
|
|
376
|
+
// Note: BNB uses 18 decimals for stablecoins
|
|
377
|
+
USDC: {
|
|
378
|
+
address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
379
|
+
decimals: 18,
|
|
380
|
+
symbol: "USDC",
|
|
381
|
+
eip712Name: "USD Coin"
|
|
382
|
+
},
|
|
383
|
+
USDT: {
|
|
384
|
+
address: "0x55d398326f99059fF775485246999027B3197955",
|
|
385
|
+
decimals: 18,
|
|
386
|
+
symbol: "USDT",
|
|
387
|
+
eip712Name: "Tether USD"
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
usdc: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
391
|
+
explorer: "https://bscscan.com/address/",
|
|
392
|
+
explorerTx: "https://bscscan.com/tx/",
|
|
393
|
+
avgBlockTime: 3,
|
|
394
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
395
|
+
requiresApproval: true
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// src/facilitators/tempo.ts
|
|
400
|
+
var TRANSFER_EVENT_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
401
|
+
var TempoFacilitator = class extends BaseFacilitator {
|
|
402
|
+
name = "tempo";
|
|
403
|
+
displayName = "Tempo Testnet";
|
|
404
|
+
supportedNetworks = ["eip155:42431"];
|
|
405
|
+
// Tempo Moderato
|
|
406
|
+
rpcUrl;
|
|
407
|
+
constructor() {
|
|
408
|
+
super();
|
|
409
|
+
this.rpcUrl = CHAINS.tempo_moderato.rpc;
|
|
410
|
+
}
|
|
411
|
+
async healthCheck() {
|
|
412
|
+
const start = Date.now();
|
|
413
|
+
try {
|
|
414
|
+
const response = await fetch(this.rpcUrl, {
|
|
415
|
+
method: "POST",
|
|
416
|
+
headers: { "Content-Type": "application/json" },
|
|
417
|
+
body: JSON.stringify({
|
|
418
|
+
jsonrpc: "2.0",
|
|
419
|
+
method: "eth_chainId",
|
|
420
|
+
params: [],
|
|
421
|
+
id: 1
|
|
422
|
+
})
|
|
423
|
+
});
|
|
424
|
+
const data = await response.json();
|
|
425
|
+
const chainId = parseInt(data.result, 16);
|
|
426
|
+
if (chainId !== 42431) {
|
|
427
|
+
return { healthy: false, error: `Wrong chainId: ${chainId}` };
|
|
428
|
+
}
|
|
429
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
430
|
+
} catch (error) {
|
|
431
|
+
return { healthy: false, error: String(error) };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async verify(paymentPayload, requirements) {
|
|
435
|
+
try {
|
|
436
|
+
const tempoPayload = paymentPayload.payload;
|
|
437
|
+
if (!tempoPayload?.txHash) {
|
|
438
|
+
return { valid: false, error: "Missing txHash in payment payload" };
|
|
439
|
+
}
|
|
440
|
+
const receipt = await this.getTransactionReceipt(tempoPayload.txHash);
|
|
441
|
+
if (!receipt) {
|
|
442
|
+
return { valid: false, error: "Transaction not found" };
|
|
443
|
+
}
|
|
444
|
+
if (receipt.status !== "0x1") {
|
|
445
|
+
return { valid: false, error: "Transaction failed" };
|
|
446
|
+
}
|
|
447
|
+
const transferLog = receipt.logs.find(
|
|
448
|
+
(log) => log.topics[0] === TRANSFER_EVENT_TOPIC
|
|
449
|
+
);
|
|
450
|
+
if (!transferLog) {
|
|
451
|
+
return { valid: false, error: "No Transfer event found" };
|
|
452
|
+
}
|
|
453
|
+
const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
|
|
454
|
+
const expectedTo = requirements.payTo.toLowerCase();
|
|
455
|
+
if (toAddress !== expectedTo) {
|
|
456
|
+
return {
|
|
457
|
+
valid: false,
|
|
458
|
+
error: `Wrong recipient: ${toAddress}, expected ${expectedTo}`
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
const amount = BigInt(transferLog.data);
|
|
462
|
+
const expectedAmount = BigInt(requirements.amount);
|
|
463
|
+
if (amount < expectedAmount) {
|
|
464
|
+
return {
|
|
465
|
+
valid: false,
|
|
466
|
+
error: `Insufficient amount: ${amount}, expected ${expectedAmount}`
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
const tokenAddress = transferLog.address.toLowerCase();
|
|
470
|
+
const expectedToken = requirements.asset.toLowerCase();
|
|
471
|
+
if (tokenAddress !== expectedToken) {
|
|
472
|
+
return {
|
|
473
|
+
valid: false,
|
|
474
|
+
error: `Wrong token: ${tokenAddress}, expected ${expectedToken}`
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
valid: true,
|
|
479
|
+
details: {
|
|
480
|
+
txHash: tempoPayload.txHash,
|
|
481
|
+
from: "0x" + transferLog.topics[1].slice(26),
|
|
482
|
+
to: toAddress,
|
|
483
|
+
amount: amount.toString(),
|
|
484
|
+
token: tokenAddress
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
} catch (error) {
|
|
488
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async settle(paymentPayload, requirements) {
|
|
492
|
+
const verifyResult = await this.verify(paymentPayload, requirements);
|
|
493
|
+
if (!verifyResult.valid) {
|
|
494
|
+
return { success: false, error: verifyResult.error };
|
|
495
|
+
}
|
|
496
|
+
const tempoPayload = paymentPayload.payload;
|
|
497
|
+
return {
|
|
498
|
+
success: true,
|
|
499
|
+
transaction: tempoPayload.txHash,
|
|
500
|
+
status: "settled"
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
async getTransactionReceipt(txHash) {
|
|
504
|
+
const response = await fetch(this.rpcUrl, {
|
|
505
|
+
method: "POST",
|
|
506
|
+
headers: { "Content-Type": "application/json" },
|
|
507
|
+
body: JSON.stringify({
|
|
508
|
+
jsonrpc: "2.0",
|
|
509
|
+
method: "eth_getTransactionReceipt",
|
|
510
|
+
params: [txHash],
|
|
511
|
+
id: 1
|
|
512
|
+
})
|
|
513
|
+
});
|
|
514
|
+
const data = await response.json();
|
|
515
|
+
return data.result;
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// src/facilitators/bnb.ts
|
|
520
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
521
|
+
var TRANSFER_EVENT_TOPIC2 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
522
|
+
var EIP712_DOMAIN = {
|
|
523
|
+
name: "MoltsPay",
|
|
524
|
+
version: "1"
|
|
525
|
+
};
|
|
526
|
+
var INTENT_TYPES = {
|
|
527
|
+
PaymentIntent: [
|
|
528
|
+
{ name: "from", type: "address" },
|
|
529
|
+
{ name: "to", type: "address" },
|
|
530
|
+
{ name: "amount", type: "uint256" },
|
|
531
|
+
{ name: "token", type: "address" },
|
|
532
|
+
{ name: "service", type: "string" },
|
|
533
|
+
{ name: "nonce", type: "uint256" },
|
|
534
|
+
{ name: "deadline", type: "uint256" }
|
|
535
|
+
]
|
|
536
|
+
};
|
|
537
|
+
var BNBFacilitator = class extends BaseFacilitator {
|
|
538
|
+
name = "bnb";
|
|
539
|
+
displayName = "BNB Smart Chain";
|
|
540
|
+
supportedNetworks = ["eip155:56", "eip155:97"];
|
|
541
|
+
// Mainnet + Testnet
|
|
542
|
+
serverPrivateKey;
|
|
543
|
+
spenderAddress = null;
|
|
544
|
+
chainConfigs;
|
|
545
|
+
constructor(serverPrivateKey) {
|
|
546
|
+
super();
|
|
547
|
+
this.serverPrivateKey = serverPrivateKey || process.env.BNB_SERVER_PRIVATE_KEY || "";
|
|
548
|
+
if (this.serverPrivateKey) {
|
|
549
|
+
const key = this.serverPrivateKey.startsWith("0x") ? this.serverPrivateKey : `0x${this.serverPrivateKey}`;
|
|
550
|
+
const account = privateKeyToAccount(key);
|
|
551
|
+
this.spenderAddress = account.address;
|
|
552
|
+
}
|
|
553
|
+
this.chainConfigs = {
|
|
554
|
+
56: { rpc: CHAINS.bnb.rpc, chain: CHAINS.bnb },
|
|
555
|
+
97: { rpc: CHAINS.bnb_testnet.rpc, chain: CHAINS.bnb_testnet }
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
async healthCheck() {
|
|
559
|
+
const start = Date.now();
|
|
560
|
+
try {
|
|
561
|
+
const response = await fetch(this.chainConfigs[56].rpc, {
|
|
562
|
+
method: "POST",
|
|
563
|
+
headers: { "Content-Type": "application/json" },
|
|
564
|
+
body: JSON.stringify({
|
|
565
|
+
jsonrpc: "2.0",
|
|
566
|
+
method: "eth_chainId",
|
|
567
|
+
params: [],
|
|
568
|
+
id: 1
|
|
569
|
+
})
|
|
570
|
+
});
|
|
571
|
+
const data = await response.json();
|
|
572
|
+
const chainId = parseInt(data.result, 16);
|
|
573
|
+
if (chainId !== 56) {
|
|
574
|
+
return { healthy: false, error: `Wrong chainId: ${chainId}` };
|
|
575
|
+
}
|
|
576
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
577
|
+
} catch (error) {
|
|
578
|
+
return { healthy: false, error: String(error) };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Verify a payment intent signature (before service execution)
|
|
583
|
+
*
|
|
584
|
+
* This verifies:
|
|
585
|
+
* 1. Signature is valid for the intent
|
|
586
|
+
* 2. Client has approved server wallet
|
|
587
|
+
* 3. Client has sufficient balance
|
|
588
|
+
* 4. Intent hasn't expired
|
|
589
|
+
*/
|
|
590
|
+
async verify(paymentPayload, requirements) {
|
|
591
|
+
try {
|
|
592
|
+
const bnbPayload = paymentPayload.payload;
|
|
593
|
+
if (!bnbPayload?.intent) {
|
|
594
|
+
return { valid: false, error: "Missing intent in payment payload" };
|
|
595
|
+
}
|
|
596
|
+
const { intent, chainId } = bnbPayload;
|
|
597
|
+
const config = this.chainConfigs[chainId];
|
|
598
|
+
if (!config) {
|
|
599
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
600
|
+
}
|
|
601
|
+
if (intent.deadline < Date.now()) {
|
|
602
|
+
return { valid: false, error: "Intent expired" };
|
|
603
|
+
}
|
|
604
|
+
const recoveredAddress = await this.recoverIntentSigner(intent, chainId);
|
|
605
|
+
if (recoveredAddress.toLowerCase() !== intent.from.toLowerCase()) {
|
|
606
|
+
return { valid: false, error: "Invalid signature" };
|
|
607
|
+
}
|
|
608
|
+
if (intent.to.toLowerCase() !== requirements.payTo.toLowerCase()) {
|
|
609
|
+
return { valid: false, error: `Wrong recipient: ${intent.to}` };
|
|
610
|
+
}
|
|
611
|
+
if (BigInt(intent.amount) < BigInt(requirements.amount)) {
|
|
612
|
+
return { valid: false, error: `Insufficient amount: ${intent.amount}` };
|
|
613
|
+
}
|
|
614
|
+
if (intent.token.toLowerCase() !== requirements.asset.toLowerCase()) {
|
|
615
|
+
return { valid: false, error: `Wrong token: ${intent.token}` };
|
|
616
|
+
}
|
|
617
|
+
const serverAddress = await this.getServerAddress();
|
|
618
|
+
const allowance = await this.getAllowance(intent.from, serverAddress, intent.token, config.rpc);
|
|
619
|
+
if (BigInt(allowance) < BigInt(intent.amount)) {
|
|
620
|
+
return { valid: false, error: "Insufficient allowance. Run: npx moltspay init --chain bnb" };
|
|
621
|
+
}
|
|
622
|
+
const balance = await this.getBalance(intent.from, intent.token, config.rpc);
|
|
623
|
+
if (BigInt(balance) < BigInt(intent.amount)) {
|
|
624
|
+
return { valid: false, error: "Insufficient balance" };
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
valid: true,
|
|
628
|
+
details: {
|
|
629
|
+
from: intent.from,
|
|
630
|
+
to: intent.to,
|
|
631
|
+
amount: intent.amount,
|
|
632
|
+
token: intent.token,
|
|
633
|
+
service: intent.service,
|
|
634
|
+
nonce: intent.nonce,
|
|
635
|
+
deadline: intent.deadline
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
} catch (error) {
|
|
639
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Settle a payment by executing transferFrom
|
|
644
|
+
*
|
|
645
|
+
* This is called AFTER the service has been successfully delivered.
|
|
646
|
+
* Server pays gas, transfers tokens from client to provider.
|
|
647
|
+
*/
|
|
648
|
+
async settle(paymentPayload, requirements) {
|
|
649
|
+
if (!this.serverPrivateKey) {
|
|
650
|
+
return { success: false, error: "Server wallet not configured (BNB_SERVER_PRIVATE_KEY)" };
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
const verifyResult = await this.verify(paymentPayload, requirements);
|
|
654
|
+
if (!verifyResult.valid) {
|
|
655
|
+
return { success: false, error: verifyResult.error };
|
|
656
|
+
}
|
|
657
|
+
const bnbPayload = paymentPayload.payload;
|
|
658
|
+
const { intent, chainId } = bnbPayload;
|
|
659
|
+
const config = this.chainConfigs[chainId];
|
|
660
|
+
const txHash = await this.executeTransferFrom(
|
|
661
|
+
intent.from,
|
|
662
|
+
intent.to,
|
|
663
|
+
intent.amount,
|
|
664
|
+
intent.token,
|
|
665
|
+
config.rpc
|
|
666
|
+
);
|
|
667
|
+
return {
|
|
668
|
+
success: true,
|
|
669
|
+
transaction: txHash,
|
|
670
|
+
status: "settled"
|
|
671
|
+
};
|
|
672
|
+
} catch (error) {
|
|
673
|
+
return { success: false, error: `Settlement failed: ${error}` };
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Check if client has approved the server wallet
|
|
678
|
+
*/
|
|
679
|
+
async checkApproval(clientAddress, token, chainId) {
|
|
680
|
+
const config = this.chainConfigs[chainId];
|
|
681
|
+
if (!config) {
|
|
682
|
+
throw new Error(`Unsupported chainId: ${chainId}`);
|
|
683
|
+
}
|
|
684
|
+
const serverAddress = await this.getServerAddress();
|
|
685
|
+
const allowance = await this.getAllowance(clientAddress, serverAddress, token, config.rpc);
|
|
686
|
+
const minAllowance = BigInt("1000000000000000000000");
|
|
687
|
+
return {
|
|
688
|
+
approved: BigInt(allowance) >= minAllowance,
|
|
689
|
+
allowance
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Verify a completed transaction (for checking past payments)
|
|
694
|
+
*/
|
|
695
|
+
async verifyTransaction(txHash, expected, chainId) {
|
|
696
|
+
const config = this.chainConfigs[chainId];
|
|
697
|
+
if (!config) {
|
|
698
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
const receipt = await this.getTransactionReceipt(txHash, config.rpc);
|
|
702
|
+
if (!receipt) {
|
|
703
|
+
return { valid: false, error: "Transaction not found" };
|
|
704
|
+
}
|
|
705
|
+
if (receipt.status !== "0x1") {
|
|
706
|
+
return { valid: false, error: "Transaction failed" };
|
|
707
|
+
}
|
|
708
|
+
const transferLog = receipt.logs.find(
|
|
709
|
+
(log) => log.topics[0] === TRANSFER_EVENT_TOPIC2 && log.address.toLowerCase() === expected.token.toLowerCase()
|
|
710
|
+
);
|
|
711
|
+
if (!transferLog) {
|
|
712
|
+
return { valid: false, error: "No Transfer event found" };
|
|
713
|
+
}
|
|
714
|
+
const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
|
|
715
|
+
if (toAddress !== expected.to.toLowerCase()) {
|
|
716
|
+
return { valid: false, error: `Wrong recipient: ${toAddress}` };
|
|
717
|
+
}
|
|
718
|
+
const amount = BigInt(transferLog.data);
|
|
719
|
+
if (amount < BigInt(expected.amount)) {
|
|
720
|
+
return { valid: false, error: `Insufficient amount: ${amount}` };
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
valid: true,
|
|
724
|
+
details: {
|
|
725
|
+
txHash,
|
|
726
|
+
from: "0x" + transferLog.topics[1].slice(26),
|
|
727
|
+
to: toAddress,
|
|
728
|
+
amount: amount.toString(),
|
|
729
|
+
token: transferLog.address
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
} catch (error) {
|
|
733
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
// ==================== Private Methods ====================
|
|
737
|
+
/**
|
|
738
|
+
* Get the server's spender address (public, for 402 responses)
|
|
739
|
+
* Returns cached value computed at construction time.
|
|
740
|
+
*/
|
|
741
|
+
getSpenderAddress() {
|
|
742
|
+
return this.spenderAddress;
|
|
743
|
+
}
|
|
744
|
+
async getServerAddress() {
|
|
745
|
+
const { ethers } = await import("ethers");
|
|
746
|
+
const wallet = new ethers.Wallet(this.serverPrivateKey);
|
|
747
|
+
return wallet.address;
|
|
748
|
+
}
|
|
749
|
+
async recoverIntentSigner(intent, chainId) {
|
|
750
|
+
const { ethers } = await import("ethers");
|
|
751
|
+
const domain = {
|
|
752
|
+
...EIP712_DOMAIN,
|
|
753
|
+
chainId
|
|
754
|
+
};
|
|
755
|
+
const message = {
|
|
756
|
+
from: intent.from,
|
|
757
|
+
to: intent.to,
|
|
758
|
+
amount: intent.amount,
|
|
759
|
+
token: intent.token,
|
|
760
|
+
service: intent.service,
|
|
761
|
+
nonce: intent.nonce,
|
|
762
|
+
deadline: intent.deadline
|
|
763
|
+
};
|
|
764
|
+
const recoveredAddress = ethers.verifyTypedData(
|
|
765
|
+
domain,
|
|
766
|
+
INTENT_TYPES,
|
|
767
|
+
message,
|
|
768
|
+
intent.signature
|
|
769
|
+
);
|
|
770
|
+
return recoveredAddress;
|
|
771
|
+
}
|
|
772
|
+
async getAllowance(owner, spender, token, rpcUrl) {
|
|
773
|
+
const selector = "0xdd62ed3e";
|
|
774
|
+
const ownerPadded = owner.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
775
|
+
const spenderPadded = spender.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
776
|
+
const data = selector + ownerPadded + spenderPadded;
|
|
777
|
+
const response = await fetch(rpcUrl, {
|
|
778
|
+
method: "POST",
|
|
779
|
+
headers: { "Content-Type": "application/json" },
|
|
780
|
+
body: JSON.stringify({
|
|
781
|
+
jsonrpc: "2.0",
|
|
782
|
+
method: "eth_call",
|
|
783
|
+
params: [{ to: token, data }, "latest"],
|
|
784
|
+
id: 1
|
|
785
|
+
})
|
|
786
|
+
});
|
|
787
|
+
const result = await response.json();
|
|
788
|
+
return result.result || "0x0";
|
|
789
|
+
}
|
|
790
|
+
async getBalance(account, token, rpcUrl) {
|
|
791
|
+
const selector = "0x70a08231";
|
|
792
|
+
const accountPadded = account.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
793
|
+
const data = selector + accountPadded;
|
|
794
|
+
const response = await fetch(rpcUrl, {
|
|
795
|
+
method: "POST",
|
|
796
|
+
headers: { "Content-Type": "application/json" },
|
|
797
|
+
body: JSON.stringify({
|
|
798
|
+
jsonrpc: "2.0",
|
|
799
|
+
method: "eth_call",
|
|
800
|
+
params: [{ to: token, data }, "latest"],
|
|
801
|
+
id: 1
|
|
802
|
+
})
|
|
803
|
+
});
|
|
804
|
+
const result = await response.json();
|
|
805
|
+
return result.result || "0x0";
|
|
806
|
+
}
|
|
807
|
+
async executeTransferFrom(from, to, amount, token, rpcUrl) {
|
|
808
|
+
const { ethers } = await import("ethers");
|
|
809
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
810
|
+
const wallet = new ethers.Wallet(this.serverPrivateKey, provider);
|
|
811
|
+
const tokenContract = new ethers.Contract(token, [
|
|
812
|
+
"function transferFrom(address from, address to, uint256 amount) returns (bool)"
|
|
813
|
+
], wallet);
|
|
814
|
+
const tx = await tokenContract.transferFrom(from, to, amount);
|
|
815
|
+
const receipt = await tx.wait();
|
|
816
|
+
return receipt.hash;
|
|
817
|
+
}
|
|
818
|
+
async getTransactionReceipt(txHash, rpcUrl) {
|
|
819
|
+
const response = await fetch(rpcUrl, {
|
|
820
|
+
method: "POST",
|
|
821
|
+
headers: { "Content-Type": "application/json" },
|
|
822
|
+
body: JSON.stringify({
|
|
823
|
+
jsonrpc: "2.0",
|
|
824
|
+
method: "eth_getTransactionReceipt",
|
|
825
|
+
params: [txHash],
|
|
826
|
+
id: 1
|
|
827
|
+
})
|
|
828
|
+
});
|
|
829
|
+
const data = await response.json();
|
|
830
|
+
return data.result;
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
// src/facilitators/solana.ts
|
|
835
|
+
import {
|
|
836
|
+
Connection as Connection2,
|
|
837
|
+
PublicKey as PublicKey2,
|
|
838
|
+
Transaction,
|
|
839
|
+
VersionedTransaction
|
|
840
|
+
} from "@solana/web3.js";
|
|
841
|
+
import {
|
|
842
|
+
getAssociatedTokenAddress,
|
|
843
|
+
createTransferCheckedInstruction,
|
|
844
|
+
getAccount,
|
|
845
|
+
createAssociatedTokenAccountInstruction
|
|
846
|
+
} from "@solana/spl-token";
|
|
847
|
+
|
|
848
|
+
// src/chains/solana.ts
|
|
849
|
+
import { Connection, PublicKey } from "@solana/web3.js";
|
|
850
|
+
var SOLANA_CHAINS = {
|
|
851
|
+
solana: {
|
|
852
|
+
name: "Solana Mainnet",
|
|
853
|
+
cluster: "mainnet-beta",
|
|
854
|
+
rpc: "https://api.mainnet-beta.solana.com",
|
|
855
|
+
explorer: "https://solscan.io/account/",
|
|
856
|
+
explorerTx: "https://solscan.io/tx/",
|
|
857
|
+
tokens: {
|
|
858
|
+
USDC: {
|
|
859
|
+
mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
860
|
+
// Circle official USDC
|
|
861
|
+
decimals: 6
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
solana_devnet: {
|
|
866
|
+
name: "Solana Devnet",
|
|
867
|
+
cluster: "devnet",
|
|
868
|
+
rpc: "https://api.devnet.solana.com",
|
|
869
|
+
explorer: "https://solscan.io/account/",
|
|
870
|
+
explorerTx: "https://solscan.io/tx/",
|
|
871
|
+
tokens: {
|
|
872
|
+
USDC: {
|
|
873
|
+
// Circle's devnet USDC (if not available, we'll deploy our own test token)
|
|
874
|
+
mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
875
|
+
decimals: 6
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
// src/facilitators/solana.ts
|
|
882
|
+
var SolanaFacilitator = class extends BaseFacilitator {
|
|
883
|
+
name = "solana";
|
|
884
|
+
displayName = "Solana Direct";
|
|
885
|
+
supportedNetworks = ["solana:mainnet", "solana:devnet"];
|
|
886
|
+
connections = /* @__PURE__ */ new Map();
|
|
887
|
+
feePayerKeypair;
|
|
888
|
+
constructor(config) {
|
|
889
|
+
super();
|
|
890
|
+
this.feePayerKeypair = config?.feePayerKeypair;
|
|
891
|
+
for (const [chain, config2] of Object.entries(SOLANA_CHAINS)) {
|
|
892
|
+
this.connections.set(
|
|
893
|
+
chain,
|
|
894
|
+
new Connection2(config2.rpc, "confirmed")
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
if (this.feePayerKeypair) {
|
|
898
|
+
console.log(`[SolanaFacilitator] Gasless mode enabled. Fee payer: ${this.feePayerKeypair.publicKey.toBase58()}`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Get fee payer public key (for gasless transactions)
|
|
903
|
+
*/
|
|
904
|
+
getFeePayerPubkey() {
|
|
905
|
+
return this.feePayerKeypair?.publicKey.toBase58() || null;
|
|
906
|
+
}
|
|
907
|
+
getConnection(chain) {
|
|
908
|
+
const conn = this.connections.get(chain);
|
|
909
|
+
if (!conn) {
|
|
910
|
+
throw new Error(`No connection for chain: ${chain}`);
|
|
911
|
+
}
|
|
912
|
+
return conn;
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Convert our chain name to network identifier
|
|
916
|
+
*/
|
|
917
|
+
static chainToNetwork(chain) {
|
|
918
|
+
return chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Convert network identifier to chain name
|
|
922
|
+
*/
|
|
923
|
+
static networkToChain(network) {
|
|
924
|
+
if (network === "solana:mainnet") return "solana";
|
|
925
|
+
if (network === "solana:devnet") return "solana_devnet";
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
async healthCheck() {
|
|
929
|
+
const start = Date.now();
|
|
930
|
+
try {
|
|
931
|
+
const conn = this.getConnection("solana_devnet");
|
|
932
|
+
await conn.getSlot();
|
|
933
|
+
return {
|
|
934
|
+
healthy: true,
|
|
935
|
+
latencyMs: Date.now() - start
|
|
936
|
+
};
|
|
937
|
+
} catch (error) {
|
|
938
|
+
return {
|
|
939
|
+
healthy: false,
|
|
940
|
+
error: error.message
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Verify a Solana payment
|
|
946
|
+
*
|
|
947
|
+
* Checks:
|
|
948
|
+
* 1. Transaction is valid and properly signed
|
|
949
|
+
* 2. Transfer instruction matches expected amount and recipient
|
|
950
|
+
*/
|
|
951
|
+
async verify(paymentPayload, requirements) {
|
|
952
|
+
try {
|
|
953
|
+
const solanaPayload = paymentPayload.payload;
|
|
954
|
+
if (!solanaPayload || !solanaPayload.signedTransaction) {
|
|
955
|
+
return { valid: false, error: "Missing signed transaction" };
|
|
956
|
+
}
|
|
957
|
+
const chain = solanaPayload.chain || "solana_devnet";
|
|
958
|
+
const chainConfig = SOLANA_CHAINS[chain];
|
|
959
|
+
if (!chainConfig) {
|
|
960
|
+
return { valid: false, error: `Invalid chain: ${chain}` };
|
|
961
|
+
}
|
|
962
|
+
const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
|
|
963
|
+
let tx;
|
|
964
|
+
try {
|
|
965
|
+
tx = Transaction.from(txBuffer);
|
|
966
|
+
} catch {
|
|
967
|
+
tx = VersionedTransaction.deserialize(txBuffer);
|
|
968
|
+
}
|
|
969
|
+
if (tx instanceof Transaction) {
|
|
970
|
+
const hasAnySignature = tx.signatures.some(
|
|
971
|
+
(sig) => sig.signature && !sig.signature.every((b) => b === 0)
|
|
972
|
+
);
|
|
973
|
+
if (!hasAnySignature) {
|
|
974
|
+
return { valid: false, error: "Transaction not signed" };
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
const expectedAmount = BigInt(requirements.amount);
|
|
978
|
+
const expectedRecipient = new PublicKey2(requirements.payTo);
|
|
979
|
+
return {
|
|
980
|
+
valid: true,
|
|
981
|
+
details: {
|
|
982
|
+
chain,
|
|
983
|
+
sender: solanaPayload.sender,
|
|
984
|
+
recipient: requirements.payTo,
|
|
985
|
+
amount: requirements.amount
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
} catch (error) {
|
|
989
|
+
return { valid: false, error: error.message };
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Settle a Solana payment
|
|
994
|
+
*
|
|
995
|
+
* Submits the signed transaction to the network.
|
|
996
|
+
* In gasless mode, adds fee payer signature before submitting.
|
|
997
|
+
*/
|
|
998
|
+
async settle(paymentPayload, requirements) {
|
|
999
|
+
try {
|
|
1000
|
+
const solanaPayload = paymentPayload.payload;
|
|
1001
|
+
if (!solanaPayload || !solanaPayload.signedTransaction) {
|
|
1002
|
+
return { success: false, error: "Missing signed transaction" };
|
|
1003
|
+
}
|
|
1004
|
+
const chain = solanaPayload.chain || "solana_devnet";
|
|
1005
|
+
const connection = this.getConnection(chain);
|
|
1006
|
+
const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
|
|
1007
|
+
let txToSend;
|
|
1008
|
+
try {
|
|
1009
|
+
const tx = Transaction.from(txBuffer);
|
|
1010
|
+
if (this.feePayerKeypair && tx.feePayer) {
|
|
1011
|
+
const feePayerPubkey = this.feePayerKeypair.publicKey.toBase58();
|
|
1012
|
+
const txFeePayer = tx.feePayer.toBase58();
|
|
1013
|
+
if (txFeePayer === feePayerPubkey) {
|
|
1014
|
+
console.log(`[SolanaFacilitator] Gasless mode: adding fee payer signature`);
|
|
1015
|
+
tx.partialSign(this.feePayerKeypair);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
txToSend = tx.serialize();
|
|
1019
|
+
} catch (e) {
|
|
1020
|
+
txToSend = txBuffer;
|
|
1021
|
+
}
|
|
1022
|
+
const signature = await connection.sendRawTransaction(txToSend, {
|
|
1023
|
+
skipPreflight: false,
|
|
1024
|
+
preflightCommitment: "confirmed"
|
|
1025
|
+
});
|
|
1026
|
+
const confirmation = await connection.confirmTransaction(signature, "confirmed");
|
|
1027
|
+
if (confirmation.value.err) {
|
|
1028
|
+
return {
|
|
1029
|
+
success: false,
|
|
1030
|
+
error: `Transaction failed: ${JSON.stringify(confirmation.value.err)}`,
|
|
1031
|
+
transaction: signature
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
return {
|
|
1035
|
+
success: true,
|
|
1036
|
+
transaction: signature,
|
|
1037
|
+
status: "confirmed"
|
|
1038
|
+
};
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
return { success: false, error: error.message };
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
supportsNetwork(network) {
|
|
1044
|
+
return this.supportedNetworks.includes(network);
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
|
|
232
1048
|
// src/facilitators/registry.ts
|
|
1049
|
+
import { Keypair as Keypair2 } from "@solana/web3.js";
|
|
1050
|
+
import bs58 from "bs58";
|
|
233
1051
|
var FacilitatorRegistry = class {
|
|
234
1052
|
factories = /* @__PURE__ */ new Map();
|
|
235
1053
|
instances = /* @__PURE__ */ new Map();
|
|
@@ -237,7 +1055,21 @@ var FacilitatorRegistry = class {
|
|
|
237
1055
|
roundRobinIndex = 0;
|
|
238
1056
|
constructor(selection) {
|
|
239
1057
|
this.registerFactory("cdp", (config) => new CDPFacilitator(config));
|
|
240
|
-
this.
|
|
1058
|
+
this.registerFactory("tempo", () => new TempoFacilitator());
|
|
1059
|
+
this.registerFactory("bnb", (config) => new BNBFacilitator(config?.serverPrivateKey));
|
|
1060
|
+
this.registerFactory("solana", (config) => {
|
|
1061
|
+
let feePayerKeypair;
|
|
1062
|
+
const feePayerKey = config?.feePayerPrivateKey || process.env.SOLANA_FEE_PAYER_KEY;
|
|
1063
|
+
if (feePayerKey) {
|
|
1064
|
+
try {
|
|
1065
|
+
feePayerKeypair = Keypair2.fromSecretKey(bs58.decode(feePayerKey));
|
|
1066
|
+
} catch (e) {
|
|
1067
|
+
console.warn(`[SolanaFacilitator] Invalid fee payer key: ${e.message}`);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
return new SolanaFacilitator({ feePayerKeypair });
|
|
1071
|
+
});
|
|
1072
|
+
this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
|
|
241
1073
|
}
|
|
242
1074
|
/**
|
|
243
1075
|
* Register a new facilitator factory
|
|
@@ -458,6 +1290,9 @@ var X402_VERSION2 = 2;
|
|
|
458
1290
|
var PAYMENT_REQUIRED_HEADER = "x-payment-required";
|
|
459
1291
|
var PAYMENT_HEADER = "x-payment";
|
|
460
1292
|
var PAYMENT_RESPONSE_HEADER = "x-payment-response";
|
|
1293
|
+
var MPP_AUTH_HEADER = "authorization";
|
|
1294
|
+
var MPP_WWW_AUTH_HEADER = "www-authenticate";
|
|
1295
|
+
var MPP_RECEIPT_HEADER = "payment-receipt";
|
|
461
1296
|
var TOKEN_ADDRESSES = {
|
|
462
1297
|
"eip155:8453": {
|
|
463
1298
|
USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
@@ -471,13 +1306,47 @@ var TOKEN_ADDRESSES = {
|
|
|
471
1306
|
"eip155:137": {
|
|
472
1307
|
USDC: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
473
1308
|
USDT: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F"
|
|
1309
|
+
},
|
|
1310
|
+
"eip155:42431": {
|
|
1311
|
+
// Tempo Moderato testnet - TIP-20 stablecoins
|
|
1312
|
+
USDC: "0x20c0000000000000000000000000000000000000",
|
|
1313
|
+
// pathUSD
|
|
1314
|
+
USDT: "0x20c0000000000000000000000000000000000001"
|
|
1315
|
+
// alphaUSD
|
|
1316
|
+
},
|
|
1317
|
+
// BNB Smart Chain mainnet
|
|
1318
|
+
"eip155:56": {
|
|
1319
|
+
USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
1320
|
+
USDT: "0x55d398326f99059fF775485246999027B3197955"
|
|
1321
|
+
},
|
|
1322
|
+
// BNB Smart Chain testnet
|
|
1323
|
+
"eip155:97": {
|
|
1324
|
+
USDC: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
1325
|
+
USDT: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd"
|
|
1326
|
+
},
|
|
1327
|
+
// Solana networks use mint addresses (SPL tokens)
|
|
1328
|
+
"solana:mainnet": {
|
|
1329
|
+
USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
|
1330
|
+
// Circle USDC
|
|
1331
|
+
},
|
|
1332
|
+
"solana:devnet": {
|
|
1333
|
+
USDC: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"
|
|
1334
|
+
// Devnet USDC
|
|
474
1335
|
}
|
|
475
1336
|
};
|
|
476
1337
|
var CHAIN_TO_NETWORK = {
|
|
477
1338
|
"base": "eip155:8453",
|
|
478
1339
|
"base_sepolia": "eip155:84532",
|
|
479
|
-
"polygon": "eip155:137"
|
|
1340
|
+
"polygon": "eip155:137",
|
|
1341
|
+
"tempo_moderato": "eip155:42431",
|
|
1342
|
+
"bnb": "eip155:56",
|
|
1343
|
+
"bnb_testnet": "eip155:97",
|
|
1344
|
+
"solana": "solana:mainnet",
|
|
1345
|
+
"solana_devnet": "solana:devnet"
|
|
480
1346
|
};
|
|
1347
|
+
function isSolanaNetwork(network) {
|
|
1348
|
+
return network.startsWith("solana:");
|
|
1349
|
+
}
|
|
481
1350
|
var TOKEN_DOMAINS = {
|
|
482
1351
|
// Base mainnet
|
|
483
1352
|
"eip155:8453": {
|
|
@@ -494,6 +1363,21 @@ var TOKEN_DOMAINS = {
|
|
|
494
1363
|
"eip155:137": {
|
|
495
1364
|
USDC: { name: "USD Coin", version: "2" },
|
|
496
1365
|
USDT: { name: "(PoS) Tether USD", version: "2" }
|
|
1366
|
+
},
|
|
1367
|
+
// Tempo Moderato testnet - TIP-20 stablecoins
|
|
1368
|
+
"eip155:42431": {
|
|
1369
|
+
USDC: { name: "pathUSD", version: "1" },
|
|
1370
|
+
USDT: { name: "alphaUSD", version: "1" }
|
|
1371
|
+
},
|
|
1372
|
+
// BNB Smart Chain mainnet
|
|
1373
|
+
"eip155:56": {
|
|
1374
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
1375
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
1376
|
+
},
|
|
1377
|
+
// BNB Smart Chain testnet
|
|
1378
|
+
"eip155:97": {
|
|
1379
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
1380
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
497
1381
|
}
|
|
498
1382
|
};
|
|
499
1383
|
function getTokenDomain(network, token) {
|
|
@@ -551,9 +1435,11 @@ var MoltsPayServer = class {
|
|
|
551
1435
|
};
|
|
552
1436
|
this.useMainnet = process.env.USE_MAINNET?.toLowerCase() === "true";
|
|
553
1437
|
this.networkId = this.useMainnet ? "eip155:8453" : "eip155:84532";
|
|
1438
|
+
const defaultFallback = ["tempo", "bnb", "solana"];
|
|
1439
|
+
const envFallback = process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean);
|
|
554
1440
|
const facilitatorConfig = options.facilitators || {
|
|
555
1441
|
primary: process.env.FACILITATOR_PRIMARY || "cdp",
|
|
556
|
-
fallback:
|
|
1442
|
+
fallback: envFallback || defaultFallback,
|
|
557
1443
|
strategy: process.env.FACILITATOR_STRATEGY || "failover",
|
|
558
1444
|
config: {
|
|
559
1445
|
cdp: { useMainnet: this.useMainnet }
|
|
@@ -592,12 +1478,20 @@ var MoltsPayServer = class {
|
|
|
592
1478
|
*/
|
|
593
1479
|
getProviderChains() {
|
|
594
1480
|
const provider = this.manifest.provider;
|
|
1481
|
+
const getWalletForChain = (chainName, explicitWallet) => {
|
|
1482
|
+
if (explicitWallet) return explicitWallet;
|
|
1483
|
+
if ((chainName === "solana" || chainName === "solana_devnet") && provider.solana_wallet) {
|
|
1484
|
+
return provider.solana_wallet;
|
|
1485
|
+
}
|
|
1486
|
+
return provider.wallet;
|
|
1487
|
+
};
|
|
595
1488
|
if (provider.chains && provider.chains.length > 0) {
|
|
596
1489
|
return provider.chains.map((c) => {
|
|
597
1490
|
const chainName = typeof c === "string" ? c : c.chain;
|
|
1491
|
+
const explicitWallet = typeof c === "object" ? c.wallet : null;
|
|
598
1492
|
return {
|
|
599
1493
|
network: CHAIN_TO_NETWORK[chainName] || "eip155:8453",
|
|
600
|
-
wallet: (
|
|
1494
|
+
wallet: getWalletForChain(chainName, explicitWallet || void 0),
|
|
601
1495
|
tokens: (typeof c === "object" ? c.tokens : null) || ["USDC"]
|
|
602
1496
|
};
|
|
603
1497
|
});
|
|
@@ -606,7 +1500,7 @@ var MoltsPayServer = class {
|
|
|
606
1500
|
const network = CHAIN_TO_NETWORK[chain] || this.networkId;
|
|
607
1501
|
return [{
|
|
608
1502
|
network,
|
|
609
|
-
wallet:
|
|
1503
|
+
wallet: getWalletForChain(chain),
|
|
610
1504
|
tokens: ["USDC"]
|
|
611
1505
|
}];
|
|
612
1506
|
}
|
|
@@ -647,8 +1541,8 @@ var MoltsPayServer = class {
|
|
|
647
1541
|
async handleRequest(req, res) {
|
|
648
1542
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
649
1543
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
650
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment");
|
|
651
|
-
res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response");
|
|
1544
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization");
|
|
1545
|
+
res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt");
|
|
652
1546
|
if (req.method === "OPTIONS") {
|
|
653
1547
|
res.writeHead(204);
|
|
654
1548
|
res.end();
|
|
@@ -677,7 +1571,16 @@ var MoltsPayServer = class {
|
|
|
677
1571
|
}
|
|
678
1572
|
const body = await this.readBody(req);
|
|
679
1573
|
const paymentHeader = req.headers[PAYMENT_HEADER];
|
|
680
|
-
|
|
1574
|
+
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
1575
|
+
return await this.handleProxy(body, paymentHeader, authHeader, res);
|
|
1576
|
+
}
|
|
1577
|
+
const servicePath = url.pathname.replace(/^\//, "");
|
|
1578
|
+
const skill = this.skills.get(servicePath);
|
|
1579
|
+
if (skill && (req.method === "POST" || req.method === "GET")) {
|
|
1580
|
+
const body = req.method === "POST" ? await this.readBody(req) : {};
|
|
1581
|
+
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
1582
|
+
const x402Header = req.headers[PAYMENT_HEADER];
|
|
1583
|
+
return await this.handleMPPRequest(skill, body, authHeader, x402Header, res);
|
|
681
1584
|
}
|
|
682
1585
|
this.sendJson(res, 404, { error: "Not found" });
|
|
683
1586
|
} catch (err) {
|
|
@@ -706,7 +1609,9 @@ var MoltsPayServer = class {
|
|
|
706
1609
|
name: this.manifest.provider.name,
|
|
707
1610
|
description: this.manifest.provider.description,
|
|
708
1611
|
wallet: this.manifest.provider.wallet,
|
|
709
|
-
chain: this.manifest.provider.chain || "base"
|
|
1612
|
+
chain: this.manifest.provider.chain || "base",
|
|
1613
|
+
solana_wallet: this.manifest.provider.solana_wallet,
|
|
1614
|
+
chains: this.manifest.provider.chains
|
|
710
1615
|
},
|
|
711
1616
|
services,
|
|
712
1617
|
endpoints: {
|
|
@@ -819,6 +1724,21 @@ var MoltsPayServer = class {
|
|
|
819
1724
|
});
|
|
820
1725
|
}
|
|
821
1726
|
console.log(`[MoltsPay] Verified by ${verifyResult.facilitator}`);
|
|
1727
|
+
const isSolana = isSolanaNetwork(paymentNetwork);
|
|
1728
|
+
let settlement = null;
|
|
1729
|
+
if (isSolana) {
|
|
1730
|
+
console.log(`[MoltsPay] Solana detected - settling payment FIRST (blockhash expiry protection)`);
|
|
1731
|
+
try {
|
|
1732
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
1733
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
1734
|
+
} catch (err) {
|
|
1735
|
+
console.error("[MoltsPay] Solana settlement failed:", err.message);
|
|
1736
|
+
return this.sendJson(res, 402, {
|
|
1737
|
+
error: "Payment settlement failed",
|
|
1738
|
+
message: err.message
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
822
1742
|
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
823
1743
|
console.log(`[MoltsPay] Executing skill: ${service} (timeout: ${timeoutSeconds}s)`);
|
|
824
1744
|
let result;
|
|
@@ -833,16 +1753,19 @@ var MoltsPayServer = class {
|
|
|
833
1753
|
console.error("[MoltsPay] Skill execution failed:", err.message);
|
|
834
1754
|
return this.sendJson(res, 500, {
|
|
835
1755
|
error: "Service execution failed",
|
|
836
|
-
message: err.message
|
|
1756
|
+
message: err.message,
|
|
1757
|
+
paymentSettled: isSolana ? true : false,
|
|
1758
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
837
1759
|
});
|
|
838
1760
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1761
|
+
if (!isSolana) {
|
|
1762
|
+
console.log(`[MoltsPay] Skill succeeded, settling payment...`);
|
|
1763
|
+
try {
|
|
1764
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
1765
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
1766
|
+
} catch (err) {
|
|
1767
|
+
console.error("[MoltsPay] Settlement failed:", err.message);
|
|
1768
|
+
}
|
|
846
1769
|
}
|
|
847
1770
|
const responseHeaders = {};
|
|
848
1771
|
if (settlement?.success) {
|
|
@@ -862,6 +1785,187 @@ var MoltsPayServer = class {
|
|
|
862
1785
|
payment: settlement?.success ? { transaction: settlement.transaction, status: "settled", facilitator: settlement.facilitator } : { status: "pending" }
|
|
863
1786
|
}, responseHeaders);
|
|
864
1787
|
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Handle MPP (Machine Payments Protocol) request
|
|
1790
|
+
* Supports both x402 and MPP protocols on service endpoints
|
|
1791
|
+
*/
|
|
1792
|
+
async handleMPPRequest(skill, body, authHeader, x402Header, res) {
|
|
1793
|
+
const config = skill.config;
|
|
1794
|
+
const params = body || {};
|
|
1795
|
+
if (x402Header) {
|
|
1796
|
+
return await this.handleExecute({ service: config.id, params }, x402Header, res);
|
|
1797
|
+
}
|
|
1798
|
+
if (authHeader && authHeader.toLowerCase().startsWith("payment ")) {
|
|
1799
|
+
return await this.handleMPPPayment(skill, params, authHeader, res);
|
|
1800
|
+
}
|
|
1801
|
+
return this.sendMPPPaymentRequired(config, res);
|
|
1802
|
+
}
|
|
1803
|
+
/**
|
|
1804
|
+
* Handle MPP payment verification and service execution
|
|
1805
|
+
*/
|
|
1806
|
+
async handleMPPPayment(skill, params, authHeader, res) {
|
|
1807
|
+
const config = skill.config;
|
|
1808
|
+
const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
|
|
1809
|
+
if (!credentialMatch) {
|
|
1810
|
+
return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
|
|
1811
|
+
}
|
|
1812
|
+
let mppCredential;
|
|
1813
|
+
try {
|
|
1814
|
+
const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
1815
|
+
const decoded = Buffer.from(base64, "base64").toString("utf-8");
|
|
1816
|
+
mppCredential = JSON.parse(decoded);
|
|
1817
|
+
} catch (err) {
|
|
1818
|
+
console.error("[MoltsPay] Failed to parse MPP credential:", err);
|
|
1819
|
+
return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
|
|
1820
|
+
}
|
|
1821
|
+
let txHash;
|
|
1822
|
+
if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
|
|
1823
|
+
txHash = mppCredential.payload.hash;
|
|
1824
|
+
} else if (mppCredential.payload?.type === "transaction") {
|
|
1825
|
+
return this.sendJson(res, 400, {
|
|
1826
|
+
error: "Transaction type not supported. Please use push mode (hash type)."
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
if (!txHash) {
|
|
1830
|
+
return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
|
|
1831
|
+
}
|
|
1832
|
+
let chainId = mppCredential.challenge?.request?.methodDetails?.chainId;
|
|
1833
|
+
if (!chainId && mppCredential.source) {
|
|
1834
|
+
const chainMatch = mppCredential.source.match(/eip155:(\d+)/);
|
|
1835
|
+
if (chainMatch) chainId = parseInt(chainMatch[1], 10);
|
|
1836
|
+
}
|
|
1837
|
+
chainId = chainId || 42431;
|
|
1838
|
+
const network = `eip155:${chainId}`;
|
|
1839
|
+
if (!this.isNetworkAccepted(network)) {
|
|
1840
|
+
return this.sendJson(res, 402, {
|
|
1841
|
+
error: `Network not accepted: ${network}`
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
const requirements = this.buildPaymentRequirements(
|
|
1845
|
+
config,
|
|
1846
|
+
network,
|
|
1847
|
+
this.getWalletForNetwork(network),
|
|
1848
|
+
"USDC"
|
|
1849
|
+
);
|
|
1850
|
+
const paymentPayload = {
|
|
1851
|
+
x402Version: X402_VERSION2,
|
|
1852
|
+
scheme: "exact",
|
|
1853
|
+
network,
|
|
1854
|
+
payload: {
|
|
1855
|
+
txHash,
|
|
1856
|
+
chainId
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
console.log(`[MoltsPay] Verifying MPP payment: txHash=${txHash}, chainId=${chainId}`);
|
|
1860
|
+
const verification = await this.registry.verify(paymentPayload, requirements);
|
|
1861
|
+
if (!verification.valid) {
|
|
1862
|
+
return this.sendJson(res, 402, {
|
|
1863
|
+
error: `Payment verification failed: ${verification.error}`
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
console.log(`[MoltsPay] Payment verified! Executing service: ${config.id}`);
|
|
1867
|
+
let result;
|
|
1868
|
+
try {
|
|
1869
|
+
result = await skill.handler(params);
|
|
1870
|
+
} catch (err) {
|
|
1871
|
+
console.error(`[MoltsPay] Skill execution error:`, err);
|
|
1872
|
+
return this.sendJson(res, 500, {
|
|
1873
|
+
error: `Service execution failed: ${err.message}`
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
const receipt = {
|
|
1877
|
+
success: true,
|
|
1878
|
+
txHash,
|
|
1879
|
+
network,
|
|
1880
|
+
facilitator: verification.facilitator
|
|
1881
|
+
};
|
|
1882
|
+
const receiptEncoded = Buffer.from(JSON.stringify(receipt)).toString("base64");
|
|
1883
|
+
res.writeHead(200, {
|
|
1884
|
+
"Content-Type": "application/json",
|
|
1885
|
+
[MPP_RECEIPT_HEADER]: receiptEncoded
|
|
1886
|
+
});
|
|
1887
|
+
res.end(JSON.stringify({
|
|
1888
|
+
success: true,
|
|
1889
|
+
result,
|
|
1890
|
+
payment: {
|
|
1891
|
+
txHash,
|
|
1892
|
+
status: "verified",
|
|
1893
|
+
facilitator: verification.facilitator
|
|
1894
|
+
}
|
|
1895
|
+
}, null, 2));
|
|
1896
|
+
}
|
|
1897
|
+
/**
|
|
1898
|
+
* Return 402 with both x402 and MPP payment requirements
|
|
1899
|
+
*/
|
|
1900
|
+
sendMPPPaymentRequired(config, res) {
|
|
1901
|
+
const acceptedTokens = getAcceptedCurrencies(config);
|
|
1902
|
+
const providerChains = this.getProviderChains();
|
|
1903
|
+
const accepts = [];
|
|
1904
|
+
for (const chainConfig of providerChains) {
|
|
1905
|
+
for (const token of acceptedTokens) {
|
|
1906
|
+
if (chainConfig.tokens.includes(token)) {
|
|
1907
|
+
accepts.push(this.buildPaymentRequirements(config, chainConfig.network, chainConfig.wallet, token));
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
const x402PaymentRequired = {
|
|
1912
|
+
x402Version: X402_VERSION2,
|
|
1913
|
+
accepts,
|
|
1914
|
+
acceptedCurrencies: acceptedTokens,
|
|
1915
|
+
resource: {
|
|
1916
|
+
url: `/${config.id}`,
|
|
1917
|
+
description: `${config.name} - $${config.price} ${config.currency}`
|
|
1918
|
+
}
|
|
1919
|
+
};
|
|
1920
|
+
const x402Encoded = Buffer.from(JSON.stringify(x402PaymentRequired)).toString("base64");
|
|
1921
|
+
const tempoChain = providerChains.find((c) => c.network === "eip155:42431");
|
|
1922
|
+
let mppWwwAuth = "";
|
|
1923
|
+
if (tempoChain) {
|
|
1924
|
+
const challengeId = this.generateChallengeId();
|
|
1925
|
+
const amountInUnits = Math.floor(config.price * 1e6).toString();
|
|
1926
|
+
const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
|
|
1927
|
+
const mppRequest = {
|
|
1928
|
+
amount: amountInUnits,
|
|
1929
|
+
currency: tokenAddress,
|
|
1930
|
+
methodDetails: {
|
|
1931
|
+
chainId: 42431,
|
|
1932
|
+
feePayer: true
|
|
1933
|
+
},
|
|
1934
|
+
recipient: tempoChain.wallet
|
|
1935
|
+
};
|
|
1936
|
+
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
1937
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
1938
|
+
mppWwwAuth = `Payment id="${challengeId}", realm="${this.manifest.provider.name}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
1939
|
+
}
|
|
1940
|
+
const headers = {
|
|
1941
|
+
"Content-Type": "application/problem+json",
|
|
1942
|
+
[PAYMENT_REQUIRED_HEADER]: x402Encoded
|
|
1943
|
+
};
|
|
1944
|
+
if (mppWwwAuth) {
|
|
1945
|
+
headers[MPP_WWW_AUTH_HEADER] = mppWwwAuth;
|
|
1946
|
+
}
|
|
1947
|
+
res.writeHead(402, headers);
|
|
1948
|
+
res.end(JSON.stringify({
|
|
1949
|
+
type: "https://paymentauth.org/problems/payment-required",
|
|
1950
|
+
title: "Payment Required",
|
|
1951
|
+
status: 402,
|
|
1952
|
+
detail: `Payment is required (${config.name}).`,
|
|
1953
|
+
service: config.id,
|
|
1954
|
+
price: config.price,
|
|
1955
|
+
currency: config.currency,
|
|
1956
|
+
acceptedCurrencies: acceptedTokens
|
|
1957
|
+
}, null, 2));
|
|
1958
|
+
}
|
|
1959
|
+
/**
|
|
1960
|
+
* Generate a unique challenge ID for MPP
|
|
1961
|
+
*/
|
|
1962
|
+
generateChallengeId() {
|
|
1963
|
+
const bytes = new Uint8Array(24);
|
|
1964
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1965
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
1966
|
+
}
|
|
1967
|
+
return Buffer.from(bytes).toString("base64url");
|
|
1968
|
+
}
|
|
865
1969
|
/**
|
|
866
1970
|
* Return 402 with x402 payment requirements (v2 format)
|
|
867
1971
|
* Includes requirements for all chains and all accepted currencies
|
|
@@ -937,7 +2041,7 @@ var MoltsPayServer = class {
|
|
|
937
2041
|
const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
|
|
938
2042
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
939
2043
|
const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
|
|
940
|
-
|
|
2044
|
+
const requirements = {
|
|
941
2045
|
scheme: "exact",
|
|
942
2046
|
network: selectedNetwork,
|
|
943
2047
|
asset: tokenAddress,
|
|
@@ -946,6 +2050,27 @@ var MoltsPayServer = class {
|
|
|
946
2050
|
maxTimeoutSeconds: 300,
|
|
947
2051
|
extra: tokenDomain
|
|
948
2052
|
};
|
|
2053
|
+
if (selectedNetwork === "solana:mainnet" || selectedNetwork === "solana:devnet") {
|
|
2054
|
+
const solanaFacilitator = this.registry.get("solana");
|
|
2055
|
+
const feePayerPubkey = solanaFacilitator?.getFeePayerPubkey?.();
|
|
2056
|
+
if (feePayerPubkey) {
|
|
2057
|
+
requirements.extra = {
|
|
2058
|
+
...requirements.extra || {},
|
|
2059
|
+
solanaFeePayer: feePayerPubkey
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
if (selectedNetwork === "eip155:56" || selectedNetwork === "eip155:97") {
|
|
2064
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
2065
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
2066
|
+
if (spenderAddress) {
|
|
2067
|
+
requirements.extra = {
|
|
2068
|
+
...requirements.extra || {},
|
|
2069
|
+
bnbSpender: spenderAddress
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
return requirements;
|
|
949
2074
|
}
|
|
950
2075
|
/**
|
|
951
2076
|
* Detect which token is being used in the payment
|
|
@@ -1011,31 +2136,42 @@ var MoltsPayServer = class {
|
|
|
1011
2136
|
/**
|
|
1012
2137
|
* POST /proxy - Handle payment for external services (moltspay-creators)
|
|
1013
2138
|
*
|
|
1014
|
-
* This endpoint allows other services to delegate x402 payment handling.
|
|
2139
|
+
* This endpoint allows other services to delegate x402/MPP payment handling.
|
|
1015
2140
|
* It does NOT execute any skill - just handles payment verification/settlement.
|
|
1016
2141
|
*
|
|
1017
2142
|
* Request body:
|
|
1018
2143
|
* { wallet, amount, currency, chain, memo, serviceId, description }
|
|
1019
2144
|
*
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
2145
|
+
* For x402 (base, polygon, base_sepolia):
|
|
2146
|
+
* Without X-Payment header: returns 402 with X-Payment-Required
|
|
2147
|
+
* With X-Payment header: verifies payment via CDP
|
|
2148
|
+
*
|
|
2149
|
+
* For MPP (tempo_moderato):
|
|
2150
|
+
* Without Authorization header: returns 402 with WWW-Authenticate
|
|
2151
|
+
* With Authorization: Payment header: verifies tx on Tempo chain
|
|
1022
2152
|
*/
|
|
1023
|
-
async handleProxy(body, paymentHeader, res) {
|
|
2153
|
+
async handleProxy(body, paymentHeader, authHeader, res) {
|
|
1024
2154
|
const { wallet, amount, currency, chain, memo, serviceId, description } = body;
|
|
1025
2155
|
if (!wallet || !amount) {
|
|
1026
2156
|
return this.sendJson(res, 400, { error: "Missing required fields: wallet, amount" });
|
|
1027
2157
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
2158
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
|
|
2159
|
+
if (chain && !supportedChains.includes(chain)) {
|
|
2160
|
+
return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
|
|
2161
|
+
}
|
|
2162
|
+
const isSolanaChain = chain === "solana" || chain === "solana_devnet";
|
|
2163
|
+
const isValidEvmAddress = /^0x[a-fA-F0-9]{40}$/.test(wallet);
|
|
2164
|
+
const isValidSolanaAddress = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(wallet);
|
|
2165
|
+
if (isSolanaChain && !isValidSolanaAddress) {
|
|
2166
|
+
return this.sendJson(res, 400, { error: "Invalid Solana wallet address format" });
|
|
2167
|
+
}
|
|
2168
|
+
if (!isSolanaChain && !isValidEvmAddress) {
|
|
2169
|
+
return this.sendJson(res, 400, { error: "Invalid EVM wallet address format" });
|
|
1030
2170
|
}
|
|
1031
2171
|
const amountNum = parseFloat(amount);
|
|
1032
2172
|
if (isNaN(amountNum) || amountNum <= 0) {
|
|
1033
2173
|
return this.sendJson(res, 400, { error: "Invalid amount" });
|
|
1034
2174
|
}
|
|
1035
|
-
const supportedChains = ["base", "polygon", "base_sepolia"];
|
|
1036
|
-
if (chain && !supportedChains.includes(chain)) {
|
|
1037
|
-
return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
|
|
1038
|
-
}
|
|
1039
2175
|
const proxyConfig = {
|
|
1040
2176
|
id: serviceId || "proxy",
|
|
1041
2177
|
name: description || "Proxy Payment",
|
|
@@ -1047,6 +2183,9 @@ var MoltsPayServer = class {
|
|
|
1047
2183
|
input: {},
|
|
1048
2184
|
output: {}
|
|
1049
2185
|
};
|
|
2186
|
+
if (chain === "tempo_moderato") {
|
|
2187
|
+
return await this.handleProxyMPP(body, proxyConfig, authHeader, res);
|
|
2188
|
+
}
|
|
1050
2189
|
const requirements = this.buildProxyPaymentRequirements(proxyConfig, wallet, currency, chain);
|
|
1051
2190
|
if (!paymentHeader) {
|
|
1052
2191
|
return this.sendProxyPaymentRequired(proxyConfig, wallet, memo, chain, res);
|
|
@@ -1082,7 +2221,6 @@ var MoltsPayServer = class {
|
|
|
1082
2221
|
console.log(`[MoltsPay] /proxy: Verified by ${verifyResult.facilitator}`);
|
|
1083
2222
|
const { execute, service, params } = body;
|
|
1084
2223
|
if (execute && service) {
|
|
1085
|
-
console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
|
|
1086
2224
|
const skill = this.skills.get(service);
|
|
1087
2225
|
if (!skill) {
|
|
1088
2226
|
console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
|
|
@@ -1092,6 +2230,32 @@ var MoltsPayServer = class {
|
|
|
1092
2230
|
error: `Service not found: ${service}`
|
|
1093
2231
|
});
|
|
1094
2232
|
}
|
|
2233
|
+
const isSolana = isSolanaNetwork(network);
|
|
2234
|
+
let settlement2 = null;
|
|
2235
|
+
if (isSolana) {
|
|
2236
|
+
console.log(`[MoltsPay] /proxy: Solana detected - settling payment FIRST`);
|
|
2237
|
+
try {
|
|
2238
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
2239
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
2240
|
+
if (!settlement2.success) {
|
|
2241
|
+
console.error(`[MoltsPay] /proxy: Solana settlement failed: ${settlement2.error}`);
|
|
2242
|
+
return this.sendJson(res, 402, {
|
|
2243
|
+
success: false,
|
|
2244
|
+
paymentSettled: false,
|
|
2245
|
+
error: `Payment settlement failed: ${settlement2.error || "Unknown error"}`
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
} catch (err) {
|
|
2249
|
+
console.error("[MoltsPay] /proxy: Solana settlement failed:", err.message);
|
|
2250
|
+
return this.sendJson(res, 402, {
|
|
2251
|
+
success: false,
|
|
2252
|
+
paymentSettled: false,
|
|
2253
|
+
error: `Payment settlement failed: ${err.message}`
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
} else {
|
|
2257
|
+
console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
|
|
2258
|
+
}
|
|
1095
2259
|
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
1096
2260
|
let result;
|
|
1097
2261
|
try {
|
|
@@ -1101,34 +2265,36 @@ var MoltsPayServer = class {
|
|
|
1101
2265
|
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
1102
2266
|
)
|
|
1103
2267
|
]);
|
|
1104
|
-
console.log(`[MoltsPay] /proxy: Skill succeeded
|
|
2268
|
+
console.log(`[MoltsPay] /proxy: Skill succeeded`);
|
|
1105
2269
|
} catch (err) {
|
|
1106
|
-
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}
|
|
2270
|
+
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}`);
|
|
1107
2271
|
return this.sendJson(res, 500, {
|
|
1108
2272
|
success: false,
|
|
1109
|
-
paymentSettled: false,
|
|
1110
|
-
error: `Service execution failed: ${err.message}
|
|
2273
|
+
paymentSettled: isSolana ? true : false,
|
|
2274
|
+
error: `Service execution failed: ${err.message}`,
|
|
2275
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
1111
2276
|
});
|
|
1112
2277
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
2278
|
+
if (!isSolana) {
|
|
2279
|
+
console.log(`[MoltsPay] /proxy: Settling payment...`);
|
|
2280
|
+
try {
|
|
2281
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
2282
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
2283
|
+
} catch (err) {
|
|
2284
|
+
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
2285
|
+
return this.sendJson(res, 200, {
|
|
2286
|
+
success: true,
|
|
2287
|
+
verified: true,
|
|
2288
|
+
settled: false,
|
|
2289
|
+
settlementError: err.message,
|
|
2290
|
+
from: payment.payload?.authorization?.from,
|
|
2291
|
+
paidTo: wallet,
|
|
2292
|
+
amount: amountNum,
|
|
2293
|
+
currency: currency || "USDC",
|
|
2294
|
+
memo,
|
|
2295
|
+
result
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
1132
2298
|
}
|
|
1133
2299
|
return this.sendJson(res, 200, {
|
|
1134
2300
|
success: true,
|
|
@@ -1136,7 +2302,6 @@ var MoltsPayServer = class {
|
|
|
1136
2302
|
settled: settlement2?.success || false,
|
|
1137
2303
|
txHash: settlement2?.transaction,
|
|
1138
2304
|
from: payment.payload?.authorization?.from,
|
|
1139
|
-
// Buyer's wallet address
|
|
1140
2305
|
paidTo: wallet,
|
|
1141
2306
|
amount: amountNum,
|
|
1142
2307
|
currency: currency || "USDC",
|
|
@@ -1171,6 +2336,131 @@ var MoltsPayServer = class {
|
|
|
1171
2336
|
memo
|
|
1172
2337
|
});
|
|
1173
2338
|
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Handle MPP payment flow for /proxy endpoint (tempo_moderato chain)
|
|
2341
|
+
*/
|
|
2342
|
+
async handleProxyMPP(body, config, authHeader, res) {
|
|
2343
|
+
const { wallet, amount, memo, serviceId } = body;
|
|
2344
|
+
const amountNum = parseFloat(amount);
|
|
2345
|
+
const amountInUnits = Math.floor(amountNum * 1e6).toString();
|
|
2346
|
+
if (!authHeader || !authHeader.toLowerCase().startsWith("payment ")) {
|
|
2347
|
+
const challengeId = this.generateChallengeId();
|
|
2348
|
+
const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
|
|
2349
|
+
const mppRequest = {
|
|
2350
|
+
amount: amountInUnits,
|
|
2351
|
+
currency: tokenAddress,
|
|
2352
|
+
methodDetails: {
|
|
2353
|
+
chainId: 42431,
|
|
2354
|
+
feePayer: true
|
|
2355
|
+
},
|
|
2356
|
+
recipient: wallet
|
|
2357
|
+
};
|
|
2358
|
+
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
2359
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
2360
|
+
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
2361
|
+
res.writeHead(402, {
|
|
2362
|
+
"Content-Type": "application/problem+json",
|
|
2363
|
+
[MPP_WWW_AUTH_HEADER]: wwwAuth
|
|
2364
|
+
});
|
|
2365
|
+
res.end(JSON.stringify({
|
|
2366
|
+
type: "https://paymentauth.org/problems/payment-required",
|
|
2367
|
+
title: "Payment Required",
|
|
2368
|
+
status: 402,
|
|
2369
|
+
detail: `Payment is required (${config.name}).`,
|
|
2370
|
+
service: serviceId || "proxy",
|
|
2371
|
+
price: amountNum,
|
|
2372
|
+
currency: "USDC"
|
|
2373
|
+
}, null, 2));
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
|
|
2377
|
+
if (!credentialMatch) {
|
|
2378
|
+
return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
|
|
2379
|
+
}
|
|
2380
|
+
let mppCredential;
|
|
2381
|
+
try {
|
|
2382
|
+
const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
2383
|
+
const decoded = Buffer.from(base64, "base64").toString("utf-8");
|
|
2384
|
+
mppCredential = JSON.parse(decoded);
|
|
2385
|
+
} catch (err) {
|
|
2386
|
+
console.error("[MoltsPay] /proxy MPP: Failed to parse credential:", err);
|
|
2387
|
+
return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
|
|
2388
|
+
}
|
|
2389
|
+
let txHash;
|
|
2390
|
+
if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
|
|
2391
|
+
txHash = mppCredential.payload.hash;
|
|
2392
|
+
} else {
|
|
2393
|
+
return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
|
|
2394
|
+
}
|
|
2395
|
+
console.log(`[MoltsPay] /proxy MPP: Verifying tx ${txHash} on Tempo...`);
|
|
2396
|
+
const requirements = this.buildPaymentRequirements(config, "eip155:42431", wallet, "USDC");
|
|
2397
|
+
const paymentPayload = {
|
|
2398
|
+
x402Version: X402_VERSION2,
|
|
2399
|
+
scheme: "exact",
|
|
2400
|
+
network: "eip155:42431",
|
|
2401
|
+
payload: { txHash, chainId: 42431 }
|
|
2402
|
+
};
|
|
2403
|
+
const verification = await this.registry.verify(paymentPayload, requirements);
|
|
2404
|
+
if (!verification.valid) {
|
|
2405
|
+
return this.sendJson(res, 402, {
|
|
2406
|
+
error: `Payment verification failed: ${verification.error}`
|
|
2407
|
+
});
|
|
2408
|
+
}
|
|
2409
|
+
console.log(`[MoltsPay] /proxy MPP: Payment verified by ${verification.facilitator}`);
|
|
2410
|
+
const { execute, service, params } = body;
|
|
2411
|
+
if (execute && service) {
|
|
2412
|
+
console.log(`[MoltsPay] /proxy MPP: Executing skill: ${service}`);
|
|
2413
|
+
const skill = this.skills.get(service);
|
|
2414
|
+
if (!skill) {
|
|
2415
|
+
return this.sendJson(res, 404, {
|
|
2416
|
+
success: false,
|
|
2417
|
+
paymentSettled: true,
|
|
2418
|
+
// Payment already happened on Tempo
|
|
2419
|
+
error: `Service not found: ${service}`
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
2423
|
+
let result;
|
|
2424
|
+
try {
|
|
2425
|
+
result = await Promise.race([
|
|
2426
|
+
skill.handler(params || {}),
|
|
2427
|
+
new Promise(
|
|
2428
|
+
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
2429
|
+
)
|
|
2430
|
+
]);
|
|
2431
|
+
} catch (err) {
|
|
2432
|
+
console.error(`[MoltsPay] /proxy MPP: Skill failed: ${err.message}`);
|
|
2433
|
+
return this.sendJson(res, 500, {
|
|
2434
|
+
success: false,
|
|
2435
|
+
paymentSettled: true,
|
|
2436
|
+
error: `Service execution failed: ${err.message}`
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
2439
|
+
return this.sendJson(res, 200, {
|
|
2440
|
+
success: true,
|
|
2441
|
+
verified: true,
|
|
2442
|
+
txHash,
|
|
2443
|
+
chain: "tempo_moderato",
|
|
2444
|
+
paidTo: wallet,
|
|
2445
|
+
amount: amountNum,
|
|
2446
|
+
currency: "USDC",
|
|
2447
|
+
facilitator: verification.facilitator,
|
|
2448
|
+
memo,
|
|
2449
|
+
result
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
this.sendJson(res, 200, {
|
|
2453
|
+
success: true,
|
|
2454
|
+
verified: true,
|
|
2455
|
+
txHash,
|
|
2456
|
+
chain: "tempo_moderato",
|
|
2457
|
+
paidTo: wallet,
|
|
2458
|
+
amount: amountNum,
|
|
2459
|
+
currency: "USDC",
|
|
2460
|
+
facilitator: verification.facilitator,
|
|
2461
|
+
memo
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
1174
2464
|
/**
|
|
1175
2465
|
* Build payment requirements for proxy endpoint (uses provided wallet)
|
|
1176
2466
|
*/
|
|
@@ -1182,7 +2472,7 @@ var MoltsPayServer = class {
|
|
|
1182
2472
|
const tokenAddresses = TOKEN_ADDRESSES[networkId] || TOKEN_ADDRESSES[this.networkId] || {};
|
|
1183
2473
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
1184
2474
|
const tokenDomain = getTokenDomain(networkId, selectedToken);
|
|
1185
|
-
|
|
2475
|
+
const requirements = {
|
|
1186
2476
|
scheme: "exact",
|
|
1187
2477
|
network: networkId,
|
|
1188
2478
|
asset: tokenAddress,
|
|
@@ -1192,6 +2482,17 @@ var MoltsPayServer = class {
|
|
|
1192
2482
|
maxTimeoutSeconds: 300,
|
|
1193
2483
|
extra: tokenDomain
|
|
1194
2484
|
};
|
|
2485
|
+
if (networkId === "eip155:56" || networkId === "eip155:97") {
|
|
2486
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
2487
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
2488
|
+
if (spenderAddress) {
|
|
2489
|
+
requirements.extra = {
|
|
2490
|
+
...requirements.extra || {},
|
|
2491
|
+
bnbSpender: spenderAddress
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
return requirements;
|
|
1195
2496
|
}
|
|
1196
2497
|
/**
|
|
1197
2498
|
* Return 402 with x402 payment requirements for proxy endpoint
|