moltspay 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +14 -0
- package/README.md +319 -89
- package/dist/cdp/index.d.mts +4 -4
- package/dist/cdp/index.d.ts +4 -4
- package/dist/cdp/index.js +57 -0
- package/dist/cdp/index.js.map +1 -1
- package/dist/cdp/index.mjs +57 -0
- package/dist/cdp/index.mjs.map +1 -1
- package/dist/chains/index.d.mts +9 -8
- package/dist/chains/index.d.ts +9 -8
- package/dist/chains/index.js +57 -0
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/index.mjs +57 -0
- package/dist/chains/index.mjs.map +1 -1
- package/dist/cli/index.js +2021 -285
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +2023 -277
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +39 -3
- package/dist/client/index.d.ts +39 -3
- package/dist/client/index.js +563 -37
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +571 -35
- package/dist/client/index.mjs.map +1 -1
- package/dist/facilitators/index.d.mts +220 -1
- package/dist/facilitators/index.d.ts +220 -1
- package/dist/facilitators/index.js +664 -1
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +670 -1
- package/dist/facilitators/index.mjs.map +1 -1
- package/dist/{index-On9ZaGDW.d.mts → index-D_2FkLwV.d.mts} +6 -2
- package/dist/{index-On9ZaGDW.d.ts → index-D_2FkLwV.d.ts} +6 -2
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1440 -153
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1448 -151
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.d.mts +13 -3
- package/dist/server/index.d.ts +13 -3
- package/dist/server/index.js +909 -54
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +919 -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 +57 -0
- package/dist/verify/index.js.map +1 -1
- package/dist/verify/index.mjs +57 -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 +57 -0
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs +57 -0
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +5 -2
- package/schemas/moltspay.services.schema.json +27 -132
package/dist/cli/index.js
CHANGED
|
@@ -37,16 +37,17 @@ var init_cjs_shims = __esm({
|
|
|
37
37
|
init_cjs_shims();
|
|
38
38
|
var import_crypto = require("crypto");
|
|
39
39
|
var import_commander = require("commander");
|
|
40
|
-
var
|
|
41
|
-
var
|
|
42
|
-
var
|
|
40
|
+
var import_os3 = require("os");
|
|
41
|
+
var import_path3 = require("path");
|
|
42
|
+
var import_fs5 = require("fs");
|
|
43
43
|
var import_child_process = require("child_process");
|
|
44
|
+
var import_ethers2 = require("ethers");
|
|
44
45
|
|
|
45
46
|
// src/client/index.ts
|
|
46
47
|
init_cjs_shims();
|
|
47
|
-
var
|
|
48
|
-
var
|
|
49
|
-
var
|
|
48
|
+
var import_fs2 = require("fs");
|
|
49
|
+
var import_os2 = require("os");
|
|
50
|
+
var import_path2 = require("path");
|
|
50
51
|
var import_ethers = require("ethers");
|
|
51
52
|
|
|
52
53
|
// src/chains/index.ts
|
|
@@ -157,6 +158,63 @@ var CHAINS = {
|
|
|
157
158
|
explorerTx: "https://explore.testnet.tempo.xyz/tx/",
|
|
158
159
|
avgBlockTime: 0.5
|
|
159
160
|
// ~500ms finality
|
|
161
|
+
},
|
|
162
|
+
// ============ BNB Chain Testnet ============
|
|
163
|
+
bnb_testnet: {
|
|
164
|
+
name: "BNB Testnet",
|
|
165
|
+
chainId: 97,
|
|
166
|
+
rpc: "https://data-seed-prebsc-1-s1.binance.org:8545",
|
|
167
|
+
tokens: {
|
|
168
|
+
// Note: BNB uses 18 decimals for stablecoins (unlike Base/Polygon which use 6)
|
|
169
|
+
// Using official Binance-Peg testnet tokens
|
|
170
|
+
USDC: {
|
|
171
|
+
address: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
172
|
+
// Testnet USDC
|
|
173
|
+
decimals: 18,
|
|
174
|
+
symbol: "USDC",
|
|
175
|
+
eip712Name: "USD Coin"
|
|
176
|
+
},
|
|
177
|
+
USDT: {
|
|
178
|
+
address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
|
|
179
|
+
// Testnet USDT
|
|
180
|
+
decimals: 18,
|
|
181
|
+
symbol: "USDT",
|
|
182
|
+
eip712Name: "Tether USD"
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
usdc: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
186
|
+
explorer: "https://testnet.bscscan.com/address/",
|
|
187
|
+
explorerTx: "https://testnet.bscscan.com/tx/",
|
|
188
|
+
avgBlockTime: 3,
|
|
189
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
190
|
+
requiresApproval: true
|
|
191
|
+
},
|
|
192
|
+
// ============ BNB Chain Mainnet ============
|
|
193
|
+
bnb: {
|
|
194
|
+
name: "BNB Smart Chain",
|
|
195
|
+
chainId: 56,
|
|
196
|
+
rpc: "https://bsc-dataseed.binance.org",
|
|
197
|
+
tokens: {
|
|
198
|
+
// Note: BNB uses 18 decimals for stablecoins
|
|
199
|
+
USDC: {
|
|
200
|
+
address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
201
|
+
decimals: 18,
|
|
202
|
+
symbol: "USDC",
|
|
203
|
+
eip712Name: "USD Coin"
|
|
204
|
+
},
|
|
205
|
+
USDT: {
|
|
206
|
+
address: "0x55d398326f99059fF775485246999027B3197955",
|
|
207
|
+
decimals: 18,
|
|
208
|
+
symbol: "USDT",
|
|
209
|
+
eip712Name: "Tether USD"
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
usdc: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
213
|
+
explorer: "https://bscscan.com/address/",
|
|
214
|
+
explorerTx: "https://bscscan.com/tx/",
|
|
215
|
+
avgBlockTime: 3,
|
|
216
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
217
|
+
requiresApproval: true
|
|
160
218
|
}
|
|
161
219
|
};
|
|
162
220
|
function getChain(name) {
|
|
@@ -167,6 +225,362 @@ function getChain(name) {
|
|
|
167
225
|
return config;
|
|
168
226
|
}
|
|
169
227
|
|
|
228
|
+
// src/wallet/solana.ts
|
|
229
|
+
init_cjs_shims();
|
|
230
|
+
var import_web32 = require("@solana/web3.js");
|
|
231
|
+
var import_spl_token = require("@solana/spl-token");
|
|
232
|
+
var import_fs = require("fs");
|
|
233
|
+
var import_path = require("path");
|
|
234
|
+
var import_os = require("os");
|
|
235
|
+
var import_bs58 = __toESM(require("bs58"));
|
|
236
|
+
|
|
237
|
+
// src/chains/solana.ts
|
|
238
|
+
init_cjs_shims();
|
|
239
|
+
var import_web3 = require("@solana/web3.js");
|
|
240
|
+
var SOLANA_CHAINS = {
|
|
241
|
+
solana: {
|
|
242
|
+
name: "Solana Mainnet",
|
|
243
|
+
cluster: "mainnet-beta",
|
|
244
|
+
rpc: "https://api.mainnet-beta.solana.com",
|
|
245
|
+
explorer: "https://solscan.io/account/",
|
|
246
|
+
explorerTx: "https://solscan.io/tx/",
|
|
247
|
+
tokens: {
|
|
248
|
+
USDC: {
|
|
249
|
+
mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
250
|
+
// Circle official USDC
|
|
251
|
+
decimals: 6
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
solana_devnet: {
|
|
256
|
+
name: "Solana Devnet",
|
|
257
|
+
cluster: "devnet",
|
|
258
|
+
rpc: "https://api.devnet.solana.com",
|
|
259
|
+
explorer: "https://solscan.io/account/",
|
|
260
|
+
explorerTx: "https://solscan.io/tx/",
|
|
261
|
+
tokens: {
|
|
262
|
+
USDC: {
|
|
263
|
+
// Circle's devnet USDC (if not available, we'll deploy our own test token)
|
|
264
|
+
mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
265
|
+
decimals: 6
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
function getSolanaConnection(chain) {
|
|
271
|
+
const config = SOLANA_CHAINS[chain];
|
|
272
|
+
return new import_web3.Connection(config.rpc, "confirmed");
|
|
273
|
+
}
|
|
274
|
+
function getUSDCMint(chain) {
|
|
275
|
+
return new import_web3.PublicKey(SOLANA_CHAINS[chain].tokens.USDC.mint);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/wallet/solana.ts
|
|
279
|
+
var DEFAULT_CONFIG_DIR = (0, import_path.join)((0, import_os.homedir)(), ".moltspay");
|
|
280
|
+
var SOLANA_WALLET_FILE = "wallet-solana.json";
|
|
281
|
+
function getSolanaWalletPath(configDir = DEFAULT_CONFIG_DIR) {
|
|
282
|
+
return (0, import_path.join)(configDir, SOLANA_WALLET_FILE);
|
|
283
|
+
}
|
|
284
|
+
function solanaWalletExists(configDir = DEFAULT_CONFIG_DIR) {
|
|
285
|
+
return (0, import_fs.existsSync)(getSolanaWalletPath(configDir));
|
|
286
|
+
}
|
|
287
|
+
function loadSolanaWallet(configDir = DEFAULT_CONFIG_DIR) {
|
|
288
|
+
const walletPath = getSolanaWalletPath(configDir);
|
|
289
|
+
if (!(0, import_fs.existsSync)(walletPath)) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
const data = JSON.parse((0, import_fs.readFileSync)(walletPath, "utf-8"));
|
|
294
|
+
const secretKey = import_bs58.default.decode(data.secretKey);
|
|
295
|
+
return import_web32.Keypair.fromSecretKey(secretKey);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.error("Failed to load Solana wallet:", error);
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function createSolanaWallet(configDir = DEFAULT_CONFIG_DIR) {
|
|
302
|
+
if (!(0, import_fs.existsSync)(configDir)) {
|
|
303
|
+
(0, import_fs.mkdirSync)(configDir, { recursive: true });
|
|
304
|
+
}
|
|
305
|
+
const keypair = import_web32.Keypair.generate();
|
|
306
|
+
const data = {
|
|
307
|
+
publicKey: keypair.publicKey.toBase58(),
|
|
308
|
+
secretKey: import_bs58.default.encode(keypair.secretKey),
|
|
309
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
310
|
+
};
|
|
311
|
+
const walletPath = getSolanaWalletPath(configDir);
|
|
312
|
+
(0, import_fs.writeFileSync)(walletPath, JSON.stringify(data, null, 2));
|
|
313
|
+
return keypair;
|
|
314
|
+
}
|
|
315
|
+
function getSolanaAddress(configDir = DEFAULT_CONFIG_DIR) {
|
|
316
|
+
const wallet = loadSolanaWallet(configDir);
|
|
317
|
+
return wallet?.publicKey.toBase58() || null;
|
|
318
|
+
}
|
|
319
|
+
async function getSolanaBalance(address, chain) {
|
|
320
|
+
const connection = getSolanaConnection(chain);
|
|
321
|
+
const pubkey = new import_web32.PublicKey(address);
|
|
322
|
+
const balance = await connection.getBalance(pubkey);
|
|
323
|
+
return balance / import_web32.LAMPORTS_PER_SOL;
|
|
324
|
+
}
|
|
325
|
+
async function getSolanaUSDCBalance(address, chain) {
|
|
326
|
+
const connection = getSolanaConnection(chain);
|
|
327
|
+
const owner = new import_web32.PublicKey(address);
|
|
328
|
+
const mint = getUSDCMint(chain);
|
|
329
|
+
try {
|
|
330
|
+
const ata = await (0, import_spl_token.getAssociatedTokenAddress)(mint, owner);
|
|
331
|
+
const account = await (0, import_spl_token.getAccount)(connection, ata);
|
|
332
|
+
return Number(account.amount) / 1e6;
|
|
333
|
+
} catch (error) {
|
|
334
|
+
if (error.name === "TokenAccountNotFoundError" || error.message?.includes("could not find account")) {
|
|
335
|
+
return 0;
|
|
336
|
+
}
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async function getSolanaBalances(address, chain) {
|
|
341
|
+
const [sol, usdc] = await Promise.all([
|
|
342
|
+
getSolanaBalance(address, chain),
|
|
343
|
+
getSolanaUSDCBalance(address, chain)
|
|
344
|
+
]);
|
|
345
|
+
return { sol, usdc };
|
|
346
|
+
}
|
|
347
|
+
function isValidSolanaAddress(address) {
|
|
348
|
+
try {
|
|
349
|
+
new import_web32.PublicKey(address);
|
|
350
|
+
return true;
|
|
351
|
+
} catch {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/facilitators/solana.ts
|
|
357
|
+
init_cjs_shims();
|
|
358
|
+
var import_web33 = require("@solana/web3.js");
|
|
359
|
+
var import_spl_token2 = require("@solana/spl-token");
|
|
360
|
+
|
|
361
|
+
// src/facilitators/interface.ts
|
|
362
|
+
init_cjs_shims();
|
|
363
|
+
var BaseFacilitator = class {
|
|
364
|
+
supportsNetwork(network) {
|
|
365
|
+
return this.supportedNetworks.includes(network);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// src/facilitators/solana.ts
|
|
370
|
+
var SolanaFacilitator = class extends BaseFacilitator {
|
|
371
|
+
name = "solana";
|
|
372
|
+
displayName = "Solana Direct";
|
|
373
|
+
supportedNetworks = ["solana:mainnet", "solana:devnet"];
|
|
374
|
+
connections = /* @__PURE__ */ new Map();
|
|
375
|
+
feePayerKeypair;
|
|
376
|
+
constructor(config) {
|
|
377
|
+
super();
|
|
378
|
+
this.feePayerKeypair = config?.feePayerKeypair;
|
|
379
|
+
for (const [chain, config2] of Object.entries(SOLANA_CHAINS)) {
|
|
380
|
+
this.connections.set(
|
|
381
|
+
chain,
|
|
382
|
+
new import_web33.Connection(config2.rpc, "confirmed")
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
if (this.feePayerKeypair) {
|
|
386
|
+
console.log(`[SolanaFacilitator] Gasless mode enabled. Fee payer: ${this.feePayerKeypair.publicKey.toBase58()}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Get fee payer public key (for gasless transactions)
|
|
391
|
+
*/
|
|
392
|
+
getFeePayerPubkey() {
|
|
393
|
+
return this.feePayerKeypair?.publicKey.toBase58() || null;
|
|
394
|
+
}
|
|
395
|
+
getConnection(chain) {
|
|
396
|
+
const conn = this.connections.get(chain);
|
|
397
|
+
if (!conn) {
|
|
398
|
+
throw new Error(`No connection for chain: ${chain}`);
|
|
399
|
+
}
|
|
400
|
+
return conn;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Convert our chain name to network identifier
|
|
404
|
+
*/
|
|
405
|
+
static chainToNetwork(chain) {
|
|
406
|
+
return chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Convert network identifier to chain name
|
|
410
|
+
*/
|
|
411
|
+
static networkToChain(network) {
|
|
412
|
+
if (network === "solana:mainnet") return "solana";
|
|
413
|
+
if (network === "solana:devnet") return "solana_devnet";
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
async healthCheck() {
|
|
417
|
+
const start = Date.now();
|
|
418
|
+
try {
|
|
419
|
+
const conn = this.getConnection("solana_devnet");
|
|
420
|
+
await conn.getSlot();
|
|
421
|
+
return {
|
|
422
|
+
healthy: true,
|
|
423
|
+
latencyMs: Date.now() - start
|
|
424
|
+
};
|
|
425
|
+
} catch (error) {
|
|
426
|
+
return {
|
|
427
|
+
healthy: false,
|
|
428
|
+
error: error.message
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Verify a Solana payment
|
|
434
|
+
*
|
|
435
|
+
* Checks:
|
|
436
|
+
* 1. Transaction is valid and properly signed
|
|
437
|
+
* 2. Transfer instruction matches expected amount and recipient
|
|
438
|
+
*/
|
|
439
|
+
async verify(paymentPayload, requirements) {
|
|
440
|
+
try {
|
|
441
|
+
const solanaPayload = paymentPayload.payload;
|
|
442
|
+
if (!solanaPayload || !solanaPayload.signedTransaction) {
|
|
443
|
+
return { valid: false, error: "Missing signed transaction" };
|
|
444
|
+
}
|
|
445
|
+
const chain = solanaPayload.chain || "solana_devnet";
|
|
446
|
+
const chainConfig = SOLANA_CHAINS[chain];
|
|
447
|
+
if (!chainConfig) {
|
|
448
|
+
return { valid: false, error: `Invalid chain: ${chain}` };
|
|
449
|
+
}
|
|
450
|
+
const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
|
|
451
|
+
let tx;
|
|
452
|
+
try {
|
|
453
|
+
tx = import_web33.Transaction.from(txBuffer);
|
|
454
|
+
} catch {
|
|
455
|
+
tx = import_web33.VersionedTransaction.deserialize(txBuffer);
|
|
456
|
+
}
|
|
457
|
+
if (tx instanceof import_web33.Transaction) {
|
|
458
|
+
const hasAnySignature = tx.signatures.some(
|
|
459
|
+
(sig) => sig.signature && !sig.signature.every((b) => b === 0)
|
|
460
|
+
);
|
|
461
|
+
if (!hasAnySignature) {
|
|
462
|
+
return { valid: false, error: "Transaction not signed" };
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const expectedAmount = BigInt(requirements.amount);
|
|
466
|
+
const expectedRecipient = new import_web33.PublicKey(requirements.payTo);
|
|
467
|
+
return {
|
|
468
|
+
valid: true,
|
|
469
|
+
details: {
|
|
470
|
+
chain,
|
|
471
|
+
sender: solanaPayload.sender,
|
|
472
|
+
recipient: requirements.payTo,
|
|
473
|
+
amount: requirements.amount
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
} catch (error) {
|
|
477
|
+
return { valid: false, error: error.message };
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Settle a Solana payment
|
|
482
|
+
*
|
|
483
|
+
* Submits the signed transaction to the network.
|
|
484
|
+
* In gasless mode, adds fee payer signature before submitting.
|
|
485
|
+
*/
|
|
486
|
+
async settle(paymentPayload, requirements) {
|
|
487
|
+
try {
|
|
488
|
+
const solanaPayload = paymentPayload.payload;
|
|
489
|
+
if (!solanaPayload || !solanaPayload.signedTransaction) {
|
|
490
|
+
return { success: false, error: "Missing signed transaction" };
|
|
491
|
+
}
|
|
492
|
+
const chain = solanaPayload.chain || "solana_devnet";
|
|
493
|
+
const connection = this.getConnection(chain);
|
|
494
|
+
const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
|
|
495
|
+
let txToSend;
|
|
496
|
+
try {
|
|
497
|
+
const tx = import_web33.Transaction.from(txBuffer);
|
|
498
|
+
if (this.feePayerKeypair && tx.feePayer) {
|
|
499
|
+
const feePayerPubkey = this.feePayerKeypair.publicKey.toBase58();
|
|
500
|
+
const txFeePayer = tx.feePayer.toBase58();
|
|
501
|
+
if (txFeePayer === feePayerPubkey) {
|
|
502
|
+
console.log(`[SolanaFacilitator] Gasless mode: adding fee payer signature`);
|
|
503
|
+
tx.partialSign(this.feePayerKeypair);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
txToSend = tx.serialize();
|
|
507
|
+
} catch (e) {
|
|
508
|
+
txToSend = txBuffer;
|
|
509
|
+
}
|
|
510
|
+
const signature = await connection.sendRawTransaction(txToSend, {
|
|
511
|
+
skipPreflight: false,
|
|
512
|
+
preflightCommitment: "confirmed"
|
|
513
|
+
});
|
|
514
|
+
const confirmation = await connection.confirmTransaction(signature, "confirmed");
|
|
515
|
+
if (confirmation.value.err) {
|
|
516
|
+
return {
|
|
517
|
+
success: false,
|
|
518
|
+
error: `Transaction failed: ${JSON.stringify(confirmation.value.err)}`,
|
|
519
|
+
transaction: signature
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
return {
|
|
523
|
+
success: true,
|
|
524
|
+
transaction: signature,
|
|
525
|
+
status: "confirmed"
|
|
526
|
+
};
|
|
527
|
+
} catch (error) {
|
|
528
|
+
return { success: false, error: error.message };
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
supportsNetwork(network) {
|
|
532
|
+
return this.supportedNetworks.includes(network);
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
|
|
536
|
+
const chainConfig = SOLANA_CHAINS[chain];
|
|
537
|
+
const connection = new import_web33.Connection(chainConfig.rpc, "confirmed");
|
|
538
|
+
const mint = new import_web33.PublicKey(chainConfig.tokens.USDC.mint);
|
|
539
|
+
const actualFeePayer = feePayerPubkey || senderPubkey;
|
|
540
|
+
const senderATA = await (0, import_spl_token2.getAssociatedTokenAddress)(mint, senderPubkey);
|
|
541
|
+
const recipientATA = await (0, import_spl_token2.getAssociatedTokenAddress)(mint, recipientPubkey);
|
|
542
|
+
const transaction = new import_web33.Transaction();
|
|
543
|
+
try {
|
|
544
|
+
await (0, import_spl_token2.getAccount)(connection, recipientATA);
|
|
545
|
+
} catch {
|
|
546
|
+
transaction.add(
|
|
547
|
+
(0, import_spl_token2.createAssociatedTokenAccountInstruction)(
|
|
548
|
+
actualFeePayer,
|
|
549
|
+
// payer (fee payer in gasless mode)
|
|
550
|
+
recipientATA,
|
|
551
|
+
// ata to create
|
|
552
|
+
recipientPubkey,
|
|
553
|
+
// owner
|
|
554
|
+
mint
|
|
555
|
+
// mint
|
|
556
|
+
)
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
transaction.add(
|
|
560
|
+
(0, import_spl_token2.createTransferCheckedInstruction)(
|
|
561
|
+
senderATA,
|
|
562
|
+
// source
|
|
563
|
+
mint,
|
|
564
|
+
// mint
|
|
565
|
+
recipientATA,
|
|
566
|
+
// destination
|
|
567
|
+
senderPubkey,
|
|
568
|
+
// owner (sender still authorizes the transfer)
|
|
569
|
+
amount,
|
|
570
|
+
// amount
|
|
571
|
+
chainConfig.tokens.USDC.decimals
|
|
572
|
+
// decimals
|
|
573
|
+
)
|
|
574
|
+
);
|
|
575
|
+
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
|
|
576
|
+
transaction.recentBlockhash = blockhash;
|
|
577
|
+
transaction.feePayer = actualFeePayer;
|
|
578
|
+
return transaction;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// src/client/index.ts
|
|
582
|
+
var import_web34 = require("@solana/web3.js");
|
|
583
|
+
|
|
170
584
|
// src/client/types.ts
|
|
171
585
|
init_cjs_shims();
|
|
172
586
|
|
|
@@ -189,7 +603,7 @@ var MoltsPayClient = class {
|
|
|
189
603
|
todaySpending = 0;
|
|
190
604
|
lastSpendingReset = 0;
|
|
191
605
|
constructor(options = {}) {
|
|
192
|
-
this.configDir = options.configDir || (0,
|
|
606
|
+
this.configDir = options.configDir || (0, import_path2.join)((0, import_os2.homedir)(), ".moltspay");
|
|
193
607
|
this.config = this.loadConfig();
|
|
194
608
|
this.walletData = this.loadWallet();
|
|
195
609
|
this.loadSpending();
|
|
@@ -209,6 +623,12 @@ var MoltsPayClient = class {
|
|
|
209
623
|
get address() {
|
|
210
624
|
return this.wallet?.address || null;
|
|
211
625
|
}
|
|
626
|
+
/**
|
|
627
|
+
* Get wallet instance (for direct operations like approvals)
|
|
628
|
+
*/
|
|
629
|
+
getWallet() {
|
|
630
|
+
return this.wallet;
|
|
631
|
+
}
|
|
212
632
|
/**
|
|
213
633
|
* Get current config
|
|
214
634
|
*/
|
|
@@ -262,11 +682,26 @@ var MoltsPayClient = class {
|
|
|
262
682
|
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
263
683
|
}
|
|
264
684
|
console.log(`[MoltsPay] Requesting service: ${service}`);
|
|
265
|
-
|
|
685
|
+
let executeUrl = `${serverUrl}/execute`;
|
|
686
|
+
try {
|
|
687
|
+
const services = await this.getServices(serverUrl);
|
|
688
|
+
const svc = services.services?.find((s) => s.id === service);
|
|
689
|
+
if (svc?.endpoint) {
|
|
690
|
+
executeUrl = `${serverUrl}${svc.endpoint}`;
|
|
691
|
+
console.log(`[MoltsPay] Using service endpoint: ${svc.endpoint}`);
|
|
692
|
+
}
|
|
693
|
+
} catch {
|
|
694
|
+
}
|
|
695
|
+
let requestBody;
|
|
696
|
+
if (options.rawData) {
|
|
697
|
+
requestBody = { service, ...params };
|
|
698
|
+
} else {
|
|
699
|
+
requestBody = { service, params };
|
|
700
|
+
}
|
|
266
701
|
if (options.chain) {
|
|
267
702
|
requestBody.chain = options.chain;
|
|
268
703
|
}
|
|
269
|
-
const initialRes = await fetch(
|
|
704
|
+
const initialRes = await fetch(executeUrl, {
|
|
270
705
|
method: "POST",
|
|
271
706
|
headers: { "Content-Type": "application/json" },
|
|
272
707
|
body: JSON.stringify(requestBody)
|
|
@@ -278,9 +713,14 @@ var MoltsPayClient = class {
|
|
|
278
713
|
}
|
|
279
714
|
throw new Error(data.error || "Unexpected response");
|
|
280
715
|
}
|
|
716
|
+
const wwwAuthHeader = initialRes.headers.get("www-authenticate");
|
|
281
717
|
const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
718
|
+
if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
|
|
719
|
+
console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
|
|
720
|
+
return await this.handleMPPPayment(executeUrl, service, params, wwwAuthHeader, options);
|
|
721
|
+
}
|
|
282
722
|
if (!paymentRequiredHeader) {
|
|
283
|
-
throw new Error("Missing x-payment-required
|
|
723
|
+
throw new Error("Missing payment header (x-payment-required or www-authenticate)");
|
|
284
724
|
}
|
|
285
725
|
let requirements;
|
|
286
726
|
try {
|
|
@@ -297,17 +737,22 @@ var MoltsPayClient = class {
|
|
|
297
737
|
throw new Error("Invalid x-payment-required header");
|
|
298
738
|
}
|
|
299
739
|
const networkToChainName = (network2) => {
|
|
740
|
+
if (network2 === "solana:mainnet") return "solana";
|
|
741
|
+
if (network2 === "solana:devnet") return "solana_devnet";
|
|
300
742
|
const match = network2.match(/^eip155:(\d+)$/);
|
|
301
743
|
if (!match) return null;
|
|
302
744
|
const chainId = parseInt(match[1]);
|
|
303
745
|
if (chainId === 8453) return "base";
|
|
304
746
|
if (chainId === 137) return "polygon";
|
|
305
747
|
if (chainId === 84532) return "base_sepolia";
|
|
748
|
+
if (chainId === 42431) return "tempo_moderato";
|
|
749
|
+
if (chainId === 56) return "bnb";
|
|
750
|
+
if (chainId === 97) return "bnb_testnet";
|
|
306
751
|
return null;
|
|
307
752
|
};
|
|
308
753
|
const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
|
|
309
|
-
let chainName;
|
|
310
754
|
const userSpecifiedChain = options.chain;
|
|
755
|
+
let selectedChain;
|
|
311
756
|
if (userSpecifiedChain) {
|
|
312
757
|
if (!serverChains.includes(userSpecifiedChain)) {
|
|
313
758
|
throw new Error(
|
|
@@ -315,17 +760,27 @@ var MoltsPayClient = class {
|
|
|
315
760
|
Server accepts: ${serverChains.join(", ")}`
|
|
316
761
|
);
|
|
317
762
|
}
|
|
318
|
-
|
|
763
|
+
selectedChain = userSpecifiedChain;
|
|
319
764
|
} else {
|
|
320
765
|
if (serverChains.length === 1 && serverChains[0] === "base") {
|
|
321
|
-
|
|
766
|
+
selectedChain = "base";
|
|
322
767
|
} else {
|
|
323
768
|
throw new Error(
|
|
324
769
|
`Server accepts: ${serverChains.join(", ")}
|
|
325
|
-
Please specify: --chain
|
|
770
|
+
Please specify: --chain <chain_name>`
|
|
326
771
|
);
|
|
327
772
|
}
|
|
328
773
|
}
|
|
774
|
+
if (selectedChain === "solana" || selectedChain === "solana_devnet") {
|
|
775
|
+
const solanaChain = selectedChain;
|
|
776
|
+
const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
777
|
+
const req2 = requirements.find((r) => r.network === network2);
|
|
778
|
+
if (!req2) {
|
|
779
|
+
throw new Error(`Failed to find payment requirement for ${selectedChain}`);
|
|
780
|
+
}
|
|
781
|
+
return await this.handleSolanaPayment(executeUrl, service, params, req2, solanaChain, options);
|
|
782
|
+
}
|
|
783
|
+
const chainName = selectedChain;
|
|
329
784
|
const chain = getChain(chainName);
|
|
330
785
|
const network = `eip155:${chain.chainId}`;
|
|
331
786
|
const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
|
|
@@ -360,6 +815,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
360
815
|
} else {
|
|
361
816
|
console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
|
|
362
817
|
}
|
|
818
|
+
if (chainName === "bnb" || chainName === "bnb_testnet") {
|
|
819
|
+
console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
|
|
820
|
+
const payTo2 = req.payTo || req.resource;
|
|
821
|
+
if (!payTo2) {
|
|
822
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
823
|
+
}
|
|
824
|
+
const bnbSpender = req.extra?.bnbSpender;
|
|
825
|
+
if (!bnbSpender) {
|
|
826
|
+
throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
|
|
827
|
+
}
|
|
828
|
+
return await this.handleBNBPayment(executeUrl, service, params, {
|
|
829
|
+
to: payTo2,
|
|
830
|
+
amount,
|
|
831
|
+
token,
|
|
832
|
+
chainName,
|
|
833
|
+
chain,
|
|
834
|
+
spender: bnbSpender
|
|
835
|
+
}, options);
|
|
836
|
+
}
|
|
363
837
|
const payTo = req.payTo || req.resource;
|
|
364
838
|
if (!payTo) {
|
|
365
839
|
throw new Error("Missing payTo address in payment requirements");
|
|
@@ -389,11 +863,11 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
389
863
|
};
|
|
390
864
|
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
391
865
|
console.log(`[MoltsPay] Sending request with payment...`);
|
|
392
|
-
const paidRequestBody = { service, params };
|
|
866
|
+
const paidRequestBody = options.rawData ? { service, ...params } : { service, params };
|
|
393
867
|
if (options.chain) {
|
|
394
868
|
paidRequestBody.chain = options.chain;
|
|
395
869
|
}
|
|
396
|
-
const paidRes = await fetch(
|
|
870
|
+
const paidRes = await fetch(executeUrl, {
|
|
397
871
|
method: "POST",
|
|
398
872
|
headers: {
|
|
399
873
|
"Content-Type": "application/json",
|
|
@@ -407,7 +881,304 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
407
881
|
}
|
|
408
882
|
this.recordSpending(amount);
|
|
409
883
|
console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
|
|
410
|
-
return result.result;
|
|
884
|
+
return result.result || result;
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Handle MPP (Machine Payments Protocol) payment flow
|
|
888
|
+
* Called when pay() detects WWW-Authenticate header in 402 response
|
|
889
|
+
*/
|
|
890
|
+
async handleMPPPayment(executeUrl, service, params, wwwAuthHeader, options = {}) {
|
|
891
|
+
const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
|
|
892
|
+
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
893
|
+
const { tempoModerato } = await import("viem/chains");
|
|
894
|
+
const { Actions } = await import("viem/tempo");
|
|
895
|
+
const privateKey = this.walletData.privateKey;
|
|
896
|
+
const account = privateKeyToAccount2(privateKey);
|
|
897
|
+
console.log(`[MoltsPay] Using MPP protocol on Tempo`);
|
|
898
|
+
console.log(`[MoltsPay] Account: ${account.address}`);
|
|
899
|
+
const parseAuthParam = (header, key) => {
|
|
900
|
+
const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
|
|
901
|
+
return match ? match[1] : null;
|
|
902
|
+
};
|
|
903
|
+
const challengeId = parseAuthParam(wwwAuthHeader, "id");
|
|
904
|
+
const method = parseAuthParam(wwwAuthHeader, "method");
|
|
905
|
+
const realm = parseAuthParam(wwwAuthHeader, "realm");
|
|
906
|
+
const requestB64 = parseAuthParam(wwwAuthHeader, "request");
|
|
907
|
+
if (method !== "tempo") {
|
|
908
|
+
throw new Error(`Unsupported payment method: ${method}`);
|
|
909
|
+
}
|
|
910
|
+
if (!requestB64) {
|
|
911
|
+
throw new Error("Missing request in WWW-Authenticate");
|
|
912
|
+
}
|
|
913
|
+
const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
|
|
914
|
+
const paymentRequest = JSON.parse(requestJson);
|
|
915
|
+
const { amount, currency, recipient, methodDetails } = paymentRequest;
|
|
916
|
+
const chainId = methodDetails?.chainId || 42431;
|
|
917
|
+
const amountDisplay = Number(amount) / 1e6;
|
|
918
|
+
console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
|
|
919
|
+
this.checkLimits(amountDisplay);
|
|
920
|
+
console.log(`[MoltsPay] Sending transaction on Tempo...`);
|
|
921
|
+
const tempoChain = { ...tempoModerato, feeToken: currency };
|
|
922
|
+
const publicClient = createPublicClient({
|
|
923
|
+
chain: tempoChain,
|
|
924
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
925
|
+
});
|
|
926
|
+
const walletClient = createWalletClient({
|
|
927
|
+
account,
|
|
928
|
+
chain: tempoChain,
|
|
929
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
930
|
+
});
|
|
931
|
+
const txHash = await Actions.token.transfer(walletClient, {
|
|
932
|
+
to: recipient,
|
|
933
|
+
amount: BigInt(amount),
|
|
934
|
+
token: currency
|
|
935
|
+
});
|
|
936
|
+
console.log(`[MoltsPay] Transaction: ${txHash}`);
|
|
937
|
+
await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
938
|
+
console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
|
|
939
|
+
const credential = {
|
|
940
|
+
challenge: {
|
|
941
|
+
id: challengeId,
|
|
942
|
+
realm,
|
|
943
|
+
method: "tempo",
|
|
944
|
+
intent: "charge",
|
|
945
|
+
request: paymentRequest
|
|
946
|
+
},
|
|
947
|
+
payload: { hash: txHash, type: "hash" },
|
|
948
|
+
source: `did:pkh:eip155:${chainId}:${account.address}`
|
|
949
|
+
};
|
|
950
|
+
const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
951
|
+
const retryBody = options.rawData ? { service, ...params, chain: "tempo_moderato" } : { service, params, chain: "tempo_moderato" };
|
|
952
|
+
const paidRes = await fetch(executeUrl, {
|
|
953
|
+
method: "POST",
|
|
954
|
+
headers: {
|
|
955
|
+
"Content-Type": "application/json",
|
|
956
|
+
"Authorization": `Payment ${credentialB64}`
|
|
957
|
+
},
|
|
958
|
+
body: JSON.stringify(retryBody)
|
|
959
|
+
});
|
|
960
|
+
const result = await paidRes.json();
|
|
961
|
+
if (!paidRes.ok) {
|
|
962
|
+
throw new Error(result.error || "Payment verification failed");
|
|
963
|
+
}
|
|
964
|
+
this.recordSpending(amountDisplay);
|
|
965
|
+
console.log(`[MoltsPay] Success!`);
|
|
966
|
+
return result.result || result;
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Handle BNB Chain payment flow (pre-approval + intent signature)
|
|
970
|
+
*
|
|
971
|
+
* Flow:
|
|
972
|
+
* 1. Check client has approved server wallet (done via `moltspay init`)
|
|
973
|
+
* 2. Sign EIP-712 payment intent (no gas, just signature)
|
|
974
|
+
* 3. Send intent to server
|
|
975
|
+
* 4. Server executes service
|
|
976
|
+
* 5. Server calls transferFrom if successful (pay-for-success)
|
|
977
|
+
*/
|
|
978
|
+
async handleBNBPayment(executeUrl, service, params, paymentDetails, options = {}) {
|
|
979
|
+
const { to, amount, token, chainName, chain, spender } = paymentDetails;
|
|
980
|
+
const tokenConfig = chain.tokens[token];
|
|
981
|
+
const provider = new import_ethers.ethers.JsonRpcProvider(chain.rpc);
|
|
982
|
+
const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
|
|
983
|
+
const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
|
|
984
|
+
if (allowance < amountWeiCheck) {
|
|
985
|
+
const nativeBalance = await provider.getBalance(this.wallet.address);
|
|
986
|
+
const minGasBalance = import_ethers.ethers.parseEther("0.0005");
|
|
987
|
+
if (nativeBalance < minGasBalance) {
|
|
988
|
+
const nativeBNB = parseFloat(import_ethers.ethers.formatEther(nativeBalance)).toFixed(4);
|
|
989
|
+
const isTestnet = chainName === "bnb_testnet";
|
|
990
|
+
if (isTestnet) {
|
|
991
|
+
throw new Error(
|
|
992
|
+
`\u274C Insufficient tBNB for approval transaction
|
|
993
|
+
|
|
994
|
+
Current tBNB: ${nativeBNB}
|
|
995
|
+
Required: ~0.001 tBNB
|
|
996
|
+
|
|
997
|
+
Get testnet tokens: npx moltspay faucet --chain bnb_testnet
|
|
998
|
+
(Gives USDC + tBNB for gas)`
|
|
999
|
+
);
|
|
1000
|
+
} else {
|
|
1001
|
+
throw new Error(
|
|
1002
|
+
`\u274C Insufficient BNB for approval transaction
|
|
1003
|
+
|
|
1004
|
+
Current BNB: ${nativeBNB}
|
|
1005
|
+
Required: ~0.001 BNB (~$0.60)
|
|
1006
|
+
|
|
1007
|
+
To get BNB:
|
|
1008
|
+
\u2022 Withdraw from Binance/exchange to your wallet
|
|
1009
|
+
\u2022 Most exchanges include BNB dust with withdrawals
|
|
1010
|
+
|
|
1011
|
+
After funding, run:
|
|
1012
|
+
npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
throw new Error(
|
|
1017
|
+
`Insufficient allowance for ${spender.slice(0, 10)}...
|
|
1018
|
+
Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
|
|
1022
|
+
const intent = {
|
|
1023
|
+
from: this.wallet.address,
|
|
1024
|
+
to,
|
|
1025
|
+
amount: amountWei,
|
|
1026
|
+
token: tokenConfig.address,
|
|
1027
|
+
service,
|
|
1028
|
+
nonce: Date.now(),
|
|
1029
|
+
// Use timestamp as nonce for simplicity
|
|
1030
|
+
deadline: Date.now() + 36e5
|
|
1031
|
+
// 1 hour
|
|
1032
|
+
};
|
|
1033
|
+
const domain = {
|
|
1034
|
+
name: "MoltsPay",
|
|
1035
|
+
version: "1",
|
|
1036
|
+
chainId: chain.chainId
|
|
1037
|
+
};
|
|
1038
|
+
const types = {
|
|
1039
|
+
PaymentIntent: [
|
|
1040
|
+
{ name: "from", type: "address" },
|
|
1041
|
+
{ name: "to", type: "address" },
|
|
1042
|
+
{ name: "amount", type: "uint256" },
|
|
1043
|
+
{ name: "token", type: "address" },
|
|
1044
|
+
{ name: "service", type: "string" },
|
|
1045
|
+
{ name: "nonce", type: "uint256" },
|
|
1046
|
+
{ name: "deadline", type: "uint256" }
|
|
1047
|
+
]
|
|
1048
|
+
};
|
|
1049
|
+
console.log(`[MoltsPay] Signing BNB payment intent...`);
|
|
1050
|
+
const signature = await this.wallet.signTypedData(domain, types, intent);
|
|
1051
|
+
const network = `eip155:${chain.chainId}`;
|
|
1052
|
+
const payload = {
|
|
1053
|
+
x402Version: 2,
|
|
1054
|
+
scheme: "exact",
|
|
1055
|
+
network,
|
|
1056
|
+
payload: {
|
|
1057
|
+
intent: {
|
|
1058
|
+
...intent,
|
|
1059
|
+
signature
|
|
1060
|
+
},
|
|
1061
|
+
chainId: chain.chainId
|
|
1062
|
+
},
|
|
1063
|
+
accepted: {
|
|
1064
|
+
scheme: "exact",
|
|
1065
|
+
network,
|
|
1066
|
+
asset: tokenConfig.address,
|
|
1067
|
+
amount: amountWei,
|
|
1068
|
+
payTo: to,
|
|
1069
|
+
maxTimeoutSeconds: 300
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
1073
|
+
console.log(`[MoltsPay] Sending BNB payment request...`);
|
|
1074
|
+
const bnbRequestBody = options.rawData ? { service, ...params, chain: chainName } : { service, params, chain: chainName };
|
|
1075
|
+
const paidRes = await fetch(executeUrl, {
|
|
1076
|
+
method: "POST",
|
|
1077
|
+
headers: {
|
|
1078
|
+
"Content-Type": "application/json",
|
|
1079
|
+
"X-Payment": paymentHeader
|
|
1080
|
+
},
|
|
1081
|
+
body: JSON.stringify(bnbRequestBody)
|
|
1082
|
+
});
|
|
1083
|
+
const result = await paidRes.json();
|
|
1084
|
+
if (!paidRes.ok) {
|
|
1085
|
+
throw new Error(result.error || "BNB payment failed");
|
|
1086
|
+
}
|
|
1087
|
+
this.recordSpending(amount);
|
|
1088
|
+
console.log(`[MoltsPay] Success! BNB payment settled.`);
|
|
1089
|
+
return result.result || result;
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Handle Solana payment flow
|
|
1093
|
+
*
|
|
1094
|
+
* Solana uses SPL token transfers with pay-for-success model:
|
|
1095
|
+
* 1. Client creates and signs a transfer transaction
|
|
1096
|
+
* 2. Server submits the transaction after service completes
|
|
1097
|
+
*/
|
|
1098
|
+
async handleSolanaPayment(executeUrl, service, params, requirements, chain, options = {}) {
|
|
1099
|
+
const solanaWallet = loadSolanaWallet(this.configDir);
|
|
1100
|
+
if (!solanaWallet) {
|
|
1101
|
+
throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
|
|
1102
|
+
}
|
|
1103
|
+
const amount = Number(requirements.amount);
|
|
1104
|
+
const amountUSDC = amount / 1e6;
|
|
1105
|
+
this.checkLimits(amountUSDC);
|
|
1106
|
+
console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
|
|
1107
|
+
if (!requirements.payTo) {
|
|
1108
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
1109
|
+
}
|
|
1110
|
+
const solanaFeePayer = requirements.extra?.solanaFeePayer;
|
|
1111
|
+
const feePayerPubkey = solanaFeePayer ? new import_web34.PublicKey(solanaFeePayer) : void 0;
|
|
1112
|
+
if (feePayerPubkey) {
|
|
1113
|
+
console.log(`[MoltsPay] Gasless mode: server pays fees`);
|
|
1114
|
+
}
|
|
1115
|
+
const recipientPubkey = new import_web34.PublicKey(requirements.payTo);
|
|
1116
|
+
const transaction = await createSolanaPaymentTransaction(
|
|
1117
|
+
solanaWallet.publicKey,
|
|
1118
|
+
recipientPubkey,
|
|
1119
|
+
BigInt(amount),
|
|
1120
|
+
chain,
|
|
1121
|
+
feePayerPubkey
|
|
1122
|
+
// Optional fee payer for gasless mode
|
|
1123
|
+
);
|
|
1124
|
+
if (feePayerPubkey) {
|
|
1125
|
+
transaction.partialSign(solanaWallet);
|
|
1126
|
+
} else {
|
|
1127
|
+
transaction.sign(solanaWallet);
|
|
1128
|
+
}
|
|
1129
|
+
const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
|
|
1130
|
+
console.log(`[MoltsPay] Transaction signed, sending to server...`);
|
|
1131
|
+
const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
1132
|
+
const payload = {
|
|
1133
|
+
x402Version: 2,
|
|
1134
|
+
scheme: "exact",
|
|
1135
|
+
network,
|
|
1136
|
+
payload: {
|
|
1137
|
+
signedTransaction: signedTx,
|
|
1138
|
+
sender: solanaWallet.publicKey.toBase58(),
|
|
1139
|
+
chain
|
|
1140
|
+
},
|
|
1141
|
+
accepted: {
|
|
1142
|
+
scheme: "exact",
|
|
1143
|
+
network,
|
|
1144
|
+
asset: requirements.asset,
|
|
1145
|
+
amount: requirements.amount,
|
|
1146
|
+
payTo: requirements.payTo,
|
|
1147
|
+
maxTimeoutSeconds: 300
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
1151
|
+
const solanaRequestBody = options.rawData ? { service, ...params, chain } : { service, params, chain };
|
|
1152
|
+
const paidRes = await fetch(executeUrl, {
|
|
1153
|
+
method: "POST",
|
|
1154
|
+
headers: {
|
|
1155
|
+
"Content-Type": "application/json",
|
|
1156
|
+
"X-Payment": paymentHeader
|
|
1157
|
+
},
|
|
1158
|
+
body: JSON.stringify(solanaRequestBody)
|
|
1159
|
+
});
|
|
1160
|
+
const result = await paidRes.json();
|
|
1161
|
+
if (!paidRes.ok) {
|
|
1162
|
+
throw new Error(result.error || "Solana payment failed");
|
|
1163
|
+
}
|
|
1164
|
+
this.recordSpending(amountUSDC);
|
|
1165
|
+
console.log(`[MoltsPay] Success! Solana payment settled.`);
|
|
1166
|
+
if (result.payment?.transaction) {
|
|
1167
|
+
const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
|
|
1168
|
+
console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
|
|
1169
|
+
}
|
|
1170
|
+
return result.result || result;
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Check ERC20 allowance for a spender
|
|
1174
|
+
*/
|
|
1175
|
+
async checkAllowance(tokenAddress, spender, provider) {
|
|
1176
|
+
const contract = new import_ethers.ethers.Contract(
|
|
1177
|
+
tokenAddress,
|
|
1178
|
+
["function allowance(address owner, address spender) view returns (uint256)"],
|
|
1179
|
+
provider
|
|
1180
|
+
);
|
|
1181
|
+
return await contract.allowance(this.wallet.address, spender);
|
|
411
1182
|
}
|
|
412
1183
|
/**
|
|
413
1184
|
* Sign EIP-3009 transferWithAuthorization (GASLESS)
|
|
@@ -479,26 +1250,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
479
1250
|
}
|
|
480
1251
|
// --- Config & Wallet Management ---
|
|
481
1252
|
loadConfig() {
|
|
482
|
-
const configPath = (0,
|
|
483
|
-
if ((0,
|
|
484
|
-
const content = (0,
|
|
1253
|
+
const configPath = (0, import_path2.join)(this.configDir, "config.json");
|
|
1254
|
+
if ((0, import_fs2.existsSync)(configPath)) {
|
|
1255
|
+
const content = (0, import_fs2.readFileSync)(configPath, "utf-8");
|
|
485
1256
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
486
1257
|
}
|
|
487
1258
|
return { ...DEFAULT_CONFIG };
|
|
488
1259
|
}
|
|
489
1260
|
saveConfig() {
|
|
490
|
-
(0,
|
|
491
|
-
const configPath = (0,
|
|
492
|
-
(0,
|
|
1261
|
+
(0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
|
|
1262
|
+
const configPath = (0, import_path2.join)(this.configDir, "config.json");
|
|
1263
|
+
(0, import_fs2.writeFileSync)(configPath, JSON.stringify(this.config, null, 2));
|
|
493
1264
|
}
|
|
494
1265
|
/**
|
|
495
1266
|
* Load spending data from disk
|
|
496
1267
|
*/
|
|
497
1268
|
loadSpending() {
|
|
498
|
-
const spendingPath = (0,
|
|
499
|
-
if ((0,
|
|
1269
|
+
const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
|
|
1270
|
+
if ((0, import_fs2.existsSync)(spendingPath)) {
|
|
500
1271
|
try {
|
|
501
|
-
const data = JSON.parse((0,
|
|
1272
|
+
const data = JSON.parse((0, import_fs2.readFileSync)(spendingPath, "utf-8"));
|
|
502
1273
|
const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
|
|
503
1274
|
if (data.date && data.date === today) {
|
|
504
1275
|
this.todaySpending = data.amount || 0;
|
|
@@ -517,29 +1288,29 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
517
1288
|
* Save spending data to disk
|
|
518
1289
|
*/
|
|
519
1290
|
saveSpending() {
|
|
520
|
-
(0,
|
|
521
|
-
const spendingPath = (0,
|
|
1291
|
+
(0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
|
|
1292
|
+
const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
|
|
522
1293
|
const data = {
|
|
523
1294
|
date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
|
|
524
1295
|
amount: this.todaySpending,
|
|
525
1296
|
updatedAt: Date.now()
|
|
526
1297
|
};
|
|
527
|
-
(0,
|
|
1298
|
+
(0, import_fs2.writeFileSync)(spendingPath, JSON.stringify(data, null, 2));
|
|
528
1299
|
}
|
|
529
1300
|
loadWallet() {
|
|
530
|
-
const walletPath = (0,
|
|
531
|
-
if ((0,
|
|
1301
|
+
const walletPath = (0, import_path2.join)(this.configDir, "wallet.json");
|
|
1302
|
+
if ((0, import_fs2.existsSync)(walletPath)) {
|
|
532
1303
|
try {
|
|
533
|
-
const stats = (0,
|
|
1304
|
+
const stats = (0, import_fs2.statSync)(walletPath);
|
|
534
1305
|
const mode = stats.mode & 511;
|
|
535
1306
|
if (mode !== 384) {
|
|
536
1307
|
console.warn(`[MoltsPay] WARNING: wallet.json has insecure permissions (${mode.toString(8)})`);
|
|
537
1308
|
console.warn(`[MoltsPay] Fixing permissions to 0600...`);
|
|
538
|
-
(0,
|
|
1309
|
+
(0, import_fs2.chmodSync)(walletPath, 384);
|
|
539
1310
|
}
|
|
540
1311
|
} catch (err) {
|
|
541
1312
|
}
|
|
542
|
-
const content = (0,
|
|
1313
|
+
const content = (0, import_fs2.readFileSync)(walletPath, "utf-8");
|
|
543
1314
|
return JSON.parse(content);
|
|
544
1315
|
}
|
|
545
1316
|
return null;
|
|
@@ -548,15 +1319,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
548
1319
|
* Initialize a new wallet (called by CLI)
|
|
549
1320
|
*/
|
|
550
1321
|
static init(configDir, options) {
|
|
551
|
-
(0,
|
|
1322
|
+
(0, import_fs2.mkdirSync)(configDir, { recursive: true });
|
|
552
1323
|
const wallet = import_ethers.Wallet.createRandom();
|
|
553
1324
|
const walletData = {
|
|
554
1325
|
address: wallet.address,
|
|
555
1326
|
privateKey: wallet.privateKey,
|
|
556
1327
|
createdAt: Date.now()
|
|
557
1328
|
};
|
|
558
|
-
const walletPath = (0,
|
|
559
|
-
(0,
|
|
1329
|
+
const walletPath = (0, import_path2.join)(configDir, "wallet.json");
|
|
1330
|
+
(0, import_fs2.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
560
1331
|
const config = {
|
|
561
1332
|
chain: options.chain,
|
|
562
1333
|
limits: {
|
|
@@ -564,8 +1335,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
564
1335
|
maxPerDay: options.maxPerDay
|
|
565
1336
|
}
|
|
566
1337
|
};
|
|
567
|
-
const configPath = (0,
|
|
568
|
-
(0,
|
|
1338
|
+
const configPath = (0, import_path2.join)(configDir, "config.json");
|
|
1339
|
+
(0, import_fs2.writeFileSync)(configPath, JSON.stringify(config, null, 2));
|
|
569
1340
|
return { address: wallet.address, configDir };
|
|
570
1341
|
}
|
|
571
1342
|
/**
|
|
@@ -601,7 +1372,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
601
1372
|
if (!this.wallet) {
|
|
602
1373
|
throw new Error("Client not initialized");
|
|
603
1374
|
}
|
|
604
|
-
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
|
|
1375
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
|
|
605
1376
|
const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
606
1377
|
const results = {};
|
|
607
1378
|
const tempoTokens = {
|
|
@@ -672,12 +1443,12 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
672
1443
|
if (!this.wallet || !this.walletData) {
|
|
673
1444
|
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
674
1445
|
}
|
|
675
|
-
const { privateKeyToAccount } = await import("viem/accounts");
|
|
1446
|
+
const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
|
|
676
1447
|
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
677
1448
|
const { tempoModerato } = await import("viem/chains");
|
|
678
1449
|
const { Actions } = await import("viem/tempo");
|
|
679
1450
|
const privateKey = this.walletData.privateKey;
|
|
680
|
-
const account =
|
|
1451
|
+
const account = privateKeyToAccount2(privateKey);
|
|
681
1452
|
console.log(`[MoltsPay] Making MPP request to: ${url}`);
|
|
682
1453
|
console.log(`[MoltsPay] Using account: ${account.address}`);
|
|
683
1454
|
const initResponse = await fetch(url, {
|
|
@@ -774,24 +1545,16 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
774
1545
|
|
|
775
1546
|
// src/server/index.ts
|
|
776
1547
|
init_cjs_shims();
|
|
777
|
-
var
|
|
1548
|
+
var import_fs4 = require("fs");
|
|
778
1549
|
var import_http = require("http");
|
|
779
1550
|
var path2 = __toESM(require("path"));
|
|
780
1551
|
|
|
781
1552
|
// src/facilitators/index.ts
|
|
782
1553
|
init_cjs_shims();
|
|
783
1554
|
|
|
784
|
-
// src/facilitators/interface.ts
|
|
785
|
-
init_cjs_shims();
|
|
786
|
-
var BaseFacilitator = class {
|
|
787
|
-
supportsNetwork(network) {
|
|
788
|
-
return this.supportedNetworks.includes(network);
|
|
789
|
-
}
|
|
790
|
-
};
|
|
791
|
-
|
|
792
1555
|
// src/facilitators/cdp.ts
|
|
793
1556
|
init_cjs_shims();
|
|
794
|
-
var
|
|
1557
|
+
var import_fs3 = require("fs");
|
|
795
1558
|
var path = __toESM(require("path"));
|
|
796
1559
|
var X402_VERSION2 = 2;
|
|
797
1560
|
var CDP_URL = "https://api.cdp.coinbase.com/platform/v2/x402";
|
|
@@ -802,9 +1565,9 @@ function loadEnvFile() {
|
|
|
802
1565
|
path.join(process.env.HOME || "", ".moltspay", ".env")
|
|
803
1566
|
];
|
|
804
1567
|
for (const envPath of envPaths) {
|
|
805
|
-
if ((0,
|
|
1568
|
+
if ((0, import_fs3.existsSync)(envPath)) {
|
|
806
1569
|
try {
|
|
807
|
-
const content = (0,
|
|
1570
|
+
const content = (0, import_fs3.readFileSync)(envPath, "utf-8");
|
|
808
1571
|
for (const line of content.split("\n")) {
|
|
809
1572
|
const trimmed = line.trim();
|
|
810
1573
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -1040,18 +1803,280 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
1040
1803
|
if (chainId !== 42431) {
|
|
1041
1804
|
return { healthy: false, error: `Wrong chainId: ${chainId}` };
|
|
1042
1805
|
}
|
|
1043
|
-
return { healthy: true, latencyMs: Date.now() - start };
|
|
1806
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
1807
|
+
} catch (error) {
|
|
1808
|
+
return { healthy: false, error: String(error) };
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
async verify(paymentPayload, requirements) {
|
|
1812
|
+
try {
|
|
1813
|
+
const tempoPayload = paymentPayload.payload;
|
|
1814
|
+
if (!tempoPayload?.txHash) {
|
|
1815
|
+
return { valid: false, error: "Missing txHash in payment payload" };
|
|
1816
|
+
}
|
|
1817
|
+
const receipt = await this.getTransactionReceipt(tempoPayload.txHash);
|
|
1818
|
+
if (!receipt) {
|
|
1819
|
+
return { valid: false, error: "Transaction not found" };
|
|
1820
|
+
}
|
|
1821
|
+
if (receipt.status !== "0x1") {
|
|
1822
|
+
return { valid: false, error: "Transaction failed" };
|
|
1823
|
+
}
|
|
1824
|
+
const transferLog = receipt.logs.find(
|
|
1825
|
+
(log) => log.topics[0] === TRANSFER_EVENT_TOPIC
|
|
1826
|
+
);
|
|
1827
|
+
if (!transferLog) {
|
|
1828
|
+
return { valid: false, error: "No Transfer event found" };
|
|
1829
|
+
}
|
|
1830
|
+
const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
|
|
1831
|
+
const expectedTo = requirements.payTo.toLowerCase();
|
|
1832
|
+
if (toAddress !== expectedTo) {
|
|
1833
|
+
return {
|
|
1834
|
+
valid: false,
|
|
1835
|
+
error: `Wrong recipient: ${toAddress}, expected ${expectedTo}`
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
const amount = BigInt(transferLog.data);
|
|
1839
|
+
const expectedAmount = BigInt(requirements.amount);
|
|
1840
|
+
if (amount < expectedAmount) {
|
|
1841
|
+
return {
|
|
1842
|
+
valid: false,
|
|
1843
|
+
error: `Insufficient amount: ${amount}, expected ${expectedAmount}`
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
const tokenAddress = transferLog.address.toLowerCase();
|
|
1847
|
+
const expectedToken = requirements.asset.toLowerCase();
|
|
1848
|
+
if (tokenAddress !== expectedToken) {
|
|
1849
|
+
return {
|
|
1850
|
+
valid: false,
|
|
1851
|
+
error: `Wrong token: ${tokenAddress}, expected ${expectedToken}`
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
return {
|
|
1855
|
+
valid: true,
|
|
1856
|
+
details: {
|
|
1857
|
+
txHash: tempoPayload.txHash,
|
|
1858
|
+
from: "0x" + transferLog.topics[1].slice(26),
|
|
1859
|
+
to: toAddress,
|
|
1860
|
+
amount: amount.toString(),
|
|
1861
|
+
token: tokenAddress
|
|
1862
|
+
}
|
|
1863
|
+
};
|
|
1864
|
+
} catch (error) {
|
|
1865
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
async settle(paymentPayload, requirements) {
|
|
1869
|
+
const verifyResult = await this.verify(paymentPayload, requirements);
|
|
1870
|
+
if (!verifyResult.valid) {
|
|
1871
|
+
return { success: false, error: verifyResult.error };
|
|
1872
|
+
}
|
|
1873
|
+
const tempoPayload = paymentPayload.payload;
|
|
1874
|
+
return {
|
|
1875
|
+
success: true,
|
|
1876
|
+
transaction: tempoPayload.txHash,
|
|
1877
|
+
status: "settled"
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
async getTransactionReceipt(txHash) {
|
|
1881
|
+
const response = await fetch(this.rpcUrl, {
|
|
1882
|
+
method: "POST",
|
|
1883
|
+
headers: { "Content-Type": "application/json" },
|
|
1884
|
+
body: JSON.stringify({
|
|
1885
|
+
jsonrpc: "2.0",
|
|
1886
|
+
method: "eth_getTransactionReceipt",
|
|
1887
|
+
params: [txHash],
|
|
1888
|
+
id: 1
|
|
1889
|
+
})
|
|
1890
|
+
});
|
|
1891
|
+
const data = await response.json();
|
|
1892
|
+
return data.result;
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
|
|
1896
|
+
// src/facilitators/bnb.ts
|
|
1897
|
+
init_cjs_shims();
|
|
1898
|
+
var import_accounts = require("viem/accounts");
|
|
1899
|
+
var TRANSFER_EVENT_TOPIC2 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
1900
|
+
var EIP712_DOMAIN = {
|
|
1901
|
+
name: "MoltsPay",
|
|
1902
|
+
version: "1"
|
|
1903
|
+
};
|
|
1904
|
+
var INTENT_TYPES = {
|
|
1905
|
+
PaymentIntent: [
|
|
1906
|
+
{ name: "from", type: "address" },
|
|
1907
|
+
{ name: "to", type: "address" },
|
|
1908
|
+
{ name: "amount", type: "uint256" },
|
|
1909
|
+
{ name: "token", type: "address" },
|
|
1910
|
+
{ name: "service", type: "string" },
|
|
1911
|
+
{ name: "nonce", type: "uint256" },
|
|
1912
|
+
{ name: "deadline", type: "uint256" }
|
|
1913
|
+
]
|
|
1914
|
+
};
|
|
1915
|
+
var BNBFacilitator = class extends BaseFacilitator {
|
|
1916
|
+
name = "bnb";
|
|
1917
|
+
displayName = "BNB Smart Chain";
|
|
1918
|
+
supportedNetworks = ["eip155:56", "eip155:97"];
|
|
1919
|
+
// Mainnet + Testnet
|
|
1920
|
+
serverPrivateKey;
|
|
1921
|
+
spenderAddress = null;
|
|
1922
|
+
chainConfigs;
|
|
1923
|
+
constructor(serverPrivateKey) {
|
|
1924
|
+
super();
|
|
1925
|
+
this.serverPrivateKey = serverPrivateKey || process.env.BNB_SERVER_PRIVATE_KEY || "";
|
|
1926
|
+
if (this.serverPrivateKey) {
|
|
1927
|
+
const key = this.serverPrivateKey.startsWith("0x") ? this.serverPrivateKey : `0x${this.serverPrivateKey}`;
|
|
1928
|
+
const account = (0, import_accounts.privateKeyToAccount)(key);
|
|
1929
|
+
this.spenderAddress = account.address;
|
|
1930
|
+
}
|
|
1931
|
+
this.chainConfigs = {
|
|
1932
|
+
56: { rpc: CHAINS.bnb.rpc, chain: CHAINS.bnb },
|
|
1933
|
+
97: { rpc: CHAINS.bnb_testnet.rpc, chain: CHAINS.bnb_testnet }
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
async healthCheck() {
|
|
1937
|
+
const start = Date.now();
|
|
1938
|
+
try {
|
|
1939
|
+
const response = await fetch(this.chainConfigs[56].rpc, {
|
|
1940
|
+
method: "POST",
|
|
1941
|
+
headers: { "Content-Type": "application/json" },
|
|
1942
|
+
body: JSON.stringify({
|
|
1943
|
+
jsonrpc: "2.0",
|
|
1944
|
+
method: "eth_chainId",
|
|
1945
|
+
params: [],
|
|
1946
|
+
id: 1
|
|
1947
|
+
})
|
|
1948
|
+
});
|
|
1949
|
+
const data = await response.json();
|
|
1950
|
+
const chainId = parseInt(data.result, 16);
|
|
1951
|
+
if (chainId !== 56) {
|
|
1952
|
+
return { healthy: false, error: `Wrong chainId: ${chainId}` };
|
|
1953
|
+
}
|
|
1954
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
1955
|
+
} catch (error) {
|
|
1956
|
+
return { healthy: false, error: String(error) };
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
/**
|
|
1960
|
+
* Verify a payment intent signature (before service execution)
|
|
1961
|
+
*
|
|
1962
|
+
* This verifies:
|
|
1963
|
+
* 1. Signature is valid for the intent
|
|
1964
|
+
* 2. Client has approved server wallet
|
|
1965
|
+
* 3. Client has sufficient balance
|
|
1966
|
+
* 4. Intent hasn't expired
|
|
1967
|
+
*/
|
|
1968
|
+
async verify(paymentPayload, requirements) {
|
|
1969
|
+
try {
|
|
1970
|
+
const bnbPayload = paymentPayload.payload;
|
|
1971
|
+
if (!bnbPayload?.intent) {
|
|
1972
|
+
return { valid: false, error: "Missing intent in payment payload" };
|
|
1973
|
+
}
|
|
1974
|
+
const { intent, chainId } = bnbPayload;
|
|
1975
|
+
const config = this.chainConfigs[chainId];
|
|
1976
|
+
if (!config) {
|
|
1977
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
1978
|
+
}
|
|
1979
|
+
if (intent.deadline < Date.now()) {
|
|
1980
|
+
return { valid: false, error: "Intent expired" };
|
|
1981
|
+
}
|
|
1982
|
+
const recoveredAddress = await this.recoverIntentSigner(intent, chainId);
|
|
1983
|
+
if (recoveredAddress.toLowerCase() !== intent.from.toLowerCase()) {
|
|
1984
|
+
return { valid: false, error: "Invalid signature" };
|
|
1985
|
+
}
|
|
1986
|
+
if (intent.to.toLowerCase() !== requirements.payTo.toLowerCase()) {
|
|
1987
|
+
return { valid: false, error: `Wrong recipient: ${intent.to}` };
|
|
1988
|
+
}
|
|
1989
|
+
if (BigInt(intent.amount) < BigInt(requirements.amount)) {
|
|
1990
|
+
return { valid: false, error: `Insufficient amount: ${intent.amount}` };
|
|
1991
|
+
}
|
|
1992
|
+
if (intent.token.toLowerCase() !== requirements.asset.toLowerCase()) {
|
|
1993
|
+
return { valid: false, error: `Wrong token: ${intent.token}` };
|
|
1994
|
+
}
|
|
1995
|
+
const serverAddress = await this.getServerAddress();
|
|
1996
|
+
const allowance = await this.getAllowance(intent.from, serverAddress, intent.token, config.rpc);
|
|
1997
|
+
if (BigInt(allowance) < BigInt(intent.amount)) {
|
|
1998
|
+
return { valid: false, error: "Insufficient allowance. Run: npx moltspay init --chain bnb" };
|
|
1999
|
+
}
|
|
2000
|
+
const balance = await this.getBalance(intent.from, intent.token, config.rpc);
|
|
2001
|
+
if (BigInt(balance) < BigInt(intent.amount)) {
|
|
2002
|
+
return { valid: false, error: "Insufficient balance" };
|
|
2003
|
+
}
|
|
2004
|
+
return {
|
|
2005
|
+
valid: true,
|
|
2006
|
+
details: {
|
|
2007
|
+
from: intent.from,
|
|
2008
|
+
to: intent.to,
|
|
2009
|
+
amount: intent.amount,
|
|
2010
|
+
token: intent.token,
|
|
2011
|
+
service: intent.service,
|
|
2012
|
+
nonce: intent.nonce,
|
|
2013
|
+
deadline: intent.deadline
|
|
2014
|
+
}
|
|
2015
|
+
};
|
|
2016
|
+
} catch (error) {
|
|
2017
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
/**
|
|
2021
|
+
* Settle a payment by executing transferFrom
|
|
2022
|
+
*
|
|
2023
|
+
* This is called AFTER the service has been successfully delivered.
|
|
2024
|
+
* Server pays gas, transfers tokens from client to provider.
|
|
2025
|
+
*/
|
|
2026
|
+
async settle(paymentPayload, requirements) {
|
|
2027
|
+
if (!this.serverPrivateKey) {
|
|
2028
|
+
return { success: false, error: "Server wallet not configured (BNB_SERVER_PRIVATE_KEY)" };
|
|
2029
|
+
}
|
|
2030
|
+
try {
|
|
2031
|
+
const verifyResult = await this.verify(paymentPayload, requirements);
|
|
2032
|
+
if (!verifyResult.valid) {
|
|
2033
|
+
return { success: false, error: verifyResult.error };
|
|
2034
|
+
}
|
|
2035
|
+
const bnbPayload = paymentPayload.payload;
|
|
2036
|
+
const { intent, chainId } = bnbPayload;
|
|
2037
|
+
const config = this.chainConfigs[chainId];
|
|
2038
|
+
const txHash = await this.executeTransferFrom(
|
|
2039
|
+
intent.from,
|
|
2040
|
+
intent.to,
|
|
2041
|
+
intent.amount,
|
|
2042
|
+
intent.token,
|
|
2043
|
+
config.rpc
|
|
2044
|
+
);
|
|
2045
|
+
return {
|
|
2046
|
+
success: true,
|
|
2047
|
+
transaction: txHash,
|
|
2048
|
+
status: "settled"
|
|
2049
|
+
};
|
|
1044
2050
|
} catch (error) {
|
|
1045
|
-
return {
|
|
2051
|
+
return { success: false, error: `Settlement failed: ${error}` };
|
|
1046
2052
|
}
|
|
1047
2053
|
}
|
|
1048
|
-
|
|
2054
|
+
/**
|
|
2055
|
+
* Check if client has approved the server wallet
|
|
2056
|
+
*/
|
|
2057
|
+
async checkApproval(clientAddress, token, chainId) {
|
|
2058
|
+
const config = this.chainConfigs[chainId];
|
|
2059
|
+
if (!config) {
|
|
2060
|
+
throw new Error(`Unsupported chainId: ${chainId}`);
|
|
2061
|
+
}
|
|
2062
|
+
const serverAddress = await this.getServerAddress();
|
|
2063
|
+
const allowance = await this.getAllowance(clientAddress, serverAddress, token, config.rpc);
|
|
2064
|
+
const minAllowance = BigInt("1000000000000000000000");
|
|
2065
|
+
return {
|
|
2066
|
+
approved: BigInt(allowance) >= minAllowance,
|
|
2067
|
+
allowance
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* Verify a completed transaction (for checking past payments)
|
|
2072
|
+
*/
|
|
2073
|
+
async verifyTransaction(txHash, expected, chainId) {
|
|
2074
|
+
const config = this.chainConfigs[chainId];
|
|
2075
|
+
if (!config) {
|
|
2076
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
2077
|
+
}
|
|
1049
2078
|
try {
|
|
1050
|
-
const
|
|
1051
|
-
if (!tempoPayload?.txHash) {
|
|
1052
|
-
return { valid: false, error: "Missing txHash in payment payload" };
|
|
1053
|
-
}
|
|
1054
|
-
const receipt = await this.getTransactionReceipt(tempoPayload.txHash);
|
|
2079
|
+
const receipt = await this.getTransactionReceipt(txHash, config.rpc);
|
|
1055
2080
|
if (!receipt) {
|
|
1056
2081
|
return { valid: false, error: "Transaction not found" };
|
|
1057
2082
|
}
|
|
@@ -1059,63 +2084,117 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
1059
2084
|
return { valid: false, error: "Transaction failed" };
|
|
1060
2085
|
}
|
|
1061
2086
|
const transferLog = receipt.logs.find(
|
|
1062
|
-
(log) => log.topics[0] ===
|
|
2087
|
+
(log) => log.topics[0] === TRANSFER_EVENT_TOPIC2 && log.address.toLowerCase() === expected.token.toLowerCase()
|
|
1063
2088
|
);
|
|
1064
2089
|
if (!transferLog) {
|
|
1065
2090
|
return { valid: false, error: "No Transfer event found" };
|
|
1066
2091
|
}
|
|
1067
2092
|
const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
return {
|
|
1071
|
-
valid: false,
|
|
1072
|
-
error: `Wrong recipient: ${toAddress}, expected ${expectedTo}`
|
|
1073
|
-
};
|
|
2093
|
+
if (toAddress !== expected.to.toLowerCase()) {
|
|
2094
|
+
return { valid: false, error: `Wrong recipient: ${toAddress}` };
|
|
1074
2095
|
}
|
|
1075
2096
|
const amount = BigInt(transferLog.data);
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
return {
|
|
1079
|
-
valid: false,
|
|
1080
|
-
error: `Insufficient amount: ${amount}, expected ${expectedAmount}`
|
|
1081
|
-
};
|
|
1082
|
-
}
|
|
1083
|
-
const tokenAddress = transferLog.address.toLowerCase();
|
|
1084
|
-
const expectedToken = requirements.asset.toLowerCase();
|
|
1085
|
-
if (tokenAddress !== expectedToken) {
|
|
1086
|
-
return {
|
|
1087
|
-
valid: false,
|
|
1088
|
-
error: `Wrong token: ${tokenAddress}, expected ${expectedToken}`
|
|
1089
|
-
};
|
|
2097
|
+
if (amount < BigInt(expected.amount)) {
|
|
2098
|
+
return { valid: false, error: `Insufficient amount: ${amount}` };
|
|
1090
2099
|
}
|
|
1091
2100
|
return {
|
|
1092
2101
|
valid: true,
|
|
1093
2102
|
details: {
|
|
1094
|
-
txHash
|
|
2103
|
+
txHash,
|
|
1095
2104
|
from: "0x" + transferLog.topics[1].slice(26),
|
|
1096
2105
|
to: toAddress,
|
|
1097
2106
|
amount: amount.toString(),
|
|
1098
|
-
token:
|
|
2107
|
+
token: transferLog.address
|
|
1099
2108
|
}
|
|
1100
2109
|
};
|
|
1101
2110
|
} catch (error) {
|
|
1102
2111
|
return { valid: false, error: `Verification failed: ${error}` };
|
|
1103
2112
|
}
|
|
1104
2113
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
return
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
2114
|
+
// ==================== Private Methods ====================
|
|
2115
|
+
/**
|
|
2116
|
+
* Get the server's spender address (public, for 402 responses)
|
|
2117
|
+
* Returns cached value computed at construction time.
|
|
2118
|
+
*/
|
|
2119
|
+
getSpenderAddress() {
|
|
2120
|
+
return this.spenderAddress;
|
|
2121
|
+
}
|
|
2122
|
+
async getServerAddress() {
|
|
2123
|
+
const { ethers: ethers3 } = await import("ethers");
|
|
2124
|
+
const wallet = new ethers3.Wallet(this.serverPrivateKey);
|
|
2125
|
+
return wallet.address;
|
|
2126
|
+
}
|
|
2127
|
+
async recoverIntentSigner(intent, chainId) {
|
|
2128
|
+
const { ethers: ethers3 } = await import("ethers");
|
|
2129
|
+
const domain = {
|
|
2130
|
+
...EIP712_DOMAIN,
|
|
2131
|
+
chainId
|
|
2132
|
+
};
|
|
2133
|
+
const message = {
|
|
2134
|
+
from: intent.from,
|
|
2135
|
+
to: intent.to,
|
|
2136
|
+
amount: intent.amount,
|
|
2137
|
+
token: intent.token,
|
|
2138
|
+
service: intent.service,
|
|
2139
|
+
nonce: intent.nonce,
|
|
2140
|
+
deadline: intent.deadline
|
|
1115
2141
|
};
|
|
2142
|
+
const recoveredAddress = ethers3.verifyTypedData(
|
|
2143
|
+
domain,
|
|
2144
|
+
INTENT_TYPES,
|
|
2145
|
+
message,
|
|
2146
|
+
intent.signature
|
|
2147
|
+
);
|
|
2148
|
+
return recoveredAddress;
|
|
1116
2149
|
}
|
|
1117
|
-
async
|
|
1118
|
-
const
|
|
2150
|
+
async getAllowance(owner, spender, token, rpcUrl) {
|
|
2151
|
+
const selector = "0xdd62ed3e";
|
|
2152
|
+
const ownerPadded = owner.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
2153
|
+
const spenderPadded = spender.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
2154
|
+
const data = selector + ownerPadded + spenderPadded;
|
|
2155
|
+
const response = await fetch(rpcUrl, {
|
|
2156
|
+
method: "POST",
|
|
2157
|
+
headers: { "Content-Type": "application/json" },
|
|
2158
|
+
body: JSON.stringify({
|
|
2159
|
+
jsonrpc: "2.0",
|
|
2160
|
+
method: "eth_call",
|
|
2161
|
+
params: [{ to: token, data }, "latest"],
|
|
2162
|
+
id: 1
|
|
2163
|
+
})
|
|
2164
|
+
});
|
|
2165
|
+
const result = await response.json();
|
|
2166
|
+
return result.result || "0x0";
|
|
2167
|
+
}
|
|
2168
|
+
async getBalance(account, token, rpcUrl) {
|
|
2169
|
+
const selector = "0x70a08231";
|
|
2170
|
+
const accountPadded = account.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
2171
|
+
const data = selector + accountPadded;
|
|
2172
|
+
const response = await fetch(rpcUrl, {
|
|
2173
|
+
method: "POST",
|
|
2174
|
+
headers: { "Content-Type": "application/json" },
|
|
2175
|
+
body: JSON.stringify({
|
|
2176
|
+
jsonrpc: "2.0",
|
|
2177
|
+
method: "eth_call",
|
|
2178
|
+
params: [{ to: token, data }, "latest"],
|
|
2179
|
+
id: 1
|
|
2180
|
+
})
|
|
2181
|
+
});
|
|
2182
|
+
const result = await response.json();
|
|
2183
|
+
return result.result || "0x0";
|
|
2184
|
+
}
|
|
2185
|
+
async executeTransferFrom(from, to, amount, token, rpcUrl) {
|
|
2186
|
+
const { ethers: ethers3 } = await import("ethers");
|
|
2187
|
+
const provider = new ethers3.JsonRpcProvider(rpcUrl);
|
|
2188
|
+
const wallet = new ethers3.Wallet(this.serverPrivateKey, provider);
|
|
2189
|
+
const tokenContract = new ethers3.Contract(token, [
|
|
2190
|
+
"function transferFrom(address from, address to, uint256 amount) returns (bool)"
|
|
2191
|
+
], wallet);
|
|
2192
|
+
const tx = await tokenContract.transferFrom(from, to, amount);
|
|
2193
|
+
const receipt = await tx.wait();
|
|
2194
|
+
return receipt.hash;
|
|
2195
|
+
}
|
|
2196
|
+
async getTransactionReceipt(txHash, rpcUrl) {
|
|
2197
|
+
const response = await fetch(rpcUrl, {
|
|
1119
2198
|
method: "POST",
|
|
1120
2199
|
headers: { "Content-Type": "application/json" },
|
|
1121
2200
|
body: JSON.stringify({
|
|
@@ -1132,6 +2211,8 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
1132
2211
|
|
|
1133
2212
|
// src/facilitators/registry.ts
|
|
1134
2213
|
init_cjs_shims();
|
|
2214
|
+
var import_web35 = require("@solana/web3.js");
|
|
2215
|
+
var import_bs582 = __toESM(require("bs58"));
|
|
1135
2216
|
var FacilitatorRegistry = class {
|
|
1136
2217
|
factories = /* @__PURE__ */ new Map();
|
|
1137
2218
|
instances = /* @__PURE__ */ new Map();
|
|
@@ -1140,7 +2221,20 @@ var FacilitatorRegistry = class {
|
|
|
1140
2221
|
constructor(selection) {
|
|
1141
2222
|
this.registerFactory("cdp", (config) => new CDPFacilitator(config));
|
|
1142
2223
|
this.registerFactory("tempo", () => new TempoFacilitator());
|
|
1143
|
-
this.
|
|
2224
|
+
this.registerFactory("bnb", (config) => new BNBFacilitator(config?.serverPrivateKey));
|
|
2225
|
+
this.registerFactory("solana", (config) => {
|
|
2226
|
+
let feePayerKeypair;
|
|
2227
|
+
const feePayerKey = config?.feePayerPrivateKey || process.env.SOLANA_FEE_PAYER_KEY;
|
|
2228
|
+
if (feePayerKey) {
|
|
2229
|
+
try {
|
|
2230
|
+
feePayerKeypair = import_web35.Keypair.fromSecretKey(import_bs582.default.decode(feePayerKey));
|
|
2231
|
+
} catch (e) {
|
|
2232
|
+
console.warn(`[SolanaFacilitator] Invalid fee payer key: ${e.message}`);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
return new SolanaFacilitator({ feePayerKeypair });
|
|
2236
|
+
});
|
|
2237
|
+
this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
|
|
1144
2238
|
}
|
|
1145
2239
|
/**
|
|
1146
2240
|
* Register a new facilitator factory
|
|
@@ -1387,14 +2481,40 @@ var TOKEN_ADDRESSES = {
|
|
|
1387
2481
|
// pathUSD
|
|
1388
2482
|
USDT: "0x20c0000000000000000000000000000000000001"
|
|
1389
2483
|
// alphaUSD
|
|
2484
|
+
},
|
|
2485
|
+
// BNB Smart Chain mainnet
|
|
2486
|
+
"eip155:56": {
|
|
2487
|
+
USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
2488
|
+
USDT: "0x55d398326f99059fF775485246999027B3197955"
|
|
2489
|
+
},
|
|
2490
|
+
// BNB Smart Chain testnet
|
|
2491
|
+
"eip155:97": {
|
|
2492
|
+
USDC: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
2493
|
+
USDT: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd"
|
|
2494
|
+
},
|
|
2495
|
+
// Solana networks use mint addresses (SPL tokens)
|
|
2496
|
+
"solana:mainnet": {
|
|
2497
|
+
USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
|
2498
|
+
// Circle USDC
|
|
2499
|
+
},
|
|
2500
|
+
"solana:devnet": {
|
|
2501
|
+
USDC: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"
|
|
2502
|
+
// Devnet USDC
|
|
1390
2503
|
}
|
|
1391
2504
|
};
|
|
1392
2505
|
var CHAIN_TO_NETWORK = {
|
|
1393
2506
|
"base": "eip155:8453",
|
|
1394
2507
|
"base_sepolia": "eip155:84532",
|
|
1395
2508
|
"polygon": "eip155:137",
|
|
1396
|
-
"tempo_moderato": "eip155:42431"
|
|
2509
|
+
"tempo_moderato": "eip155:42431",
|
|
2510
|
+
"bnb": "eip155:56",
|
|
2511
|
+
"bnb_testnet": "eip155:97",
|
|
2512
|
+
"solana": "solana:mainnet",
|
|
2513
|
+
"solana_devnet": "solana:devnet"
|
|
1397
2514
|
};
|
|
2515
|
+
function isSolanaNetwork(network) {
|
|
2516
|
+
return network.startsWith("solana:");
|
|
2517
|
+
}
|
|
1398
2518
|
var TOKEN_DOMAINS = {
|
|
1399
2519
|
// Base mainnet
|
|
1400
2520
|
"eip155:8453": {
|
|
@@ -1416,6 +2536,16 @@ var TOKEN_DOMAINS = {
|
|
|
1416
2536
|
"eip155:42431": {
|
|
1417
2537
|
USDC: { name: "pathUSD", version: "1" },
|
|
1418
2538
|
USDT: { name: "alphaUSD", version: "1" }
|
|
2539
|
+
},
|
|
2540
|
+
// BNB Smart Chain mainnet
|
|
2541
|
+
"eip155:56": {
|
|
2542
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
2543
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
2544
|
+
},
|
|
2545
|
+
// BNB Smart Chain testnet
|
|
2546
|
+
"eip155:97": {
|
|
2547
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
2548
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
1419
2549
|
}
|
|
1420
2550
|
};
|
|
1421
2551
|
function getTokenDomain(network, token) {
|
|
@@ -1431,9 +2561,9 @@ function loadEnvFile2() {
|
|
|
1431
2561
|
path2.join(process.env.HOME || "", ".moltspay", ".env")
|
|
1432
2562
|
];
|
|
1433
2563
|
for (const envPath of envPaths) {
|
|
1434
|
-
if ((0,
|
|
2564
|
+
if ((0, import_fs4.existsSync)(envPath)) {
|
|
1435
2565
|
try {
|
|
1436
|
-
const content = (0,
|
|
2566
|
+
const content = (0, import_fs4.readFileSync)(envPath, "utf-8");
|
|
1437
2567
|
for (const line of content.split("\n")) {
|
|
1438
2568
|
const trimmed = line.trim();
|
|
1439
2569
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -1464,7 +2594,7 @@ var MoltsPayServer = class {
|
|
|
1464
2594
|
useMainnet;
|
|
1465
2595
|
constructor(servicesPath, options = {}) {
|
|
1466
2596
|
loadEnvFile2();
|
|
1467
|
-
const content = (0,
|
|
2597
|
+
const content = (0, import_fs4.readFileSync)(servicesPath, "utf-8");
|
|
1468
2598
|
this.manifest = JSON.parse(content);
|
|
1469
2599
|
this.options = {
|
|
1470
2600
|
port: options.port || 3e3,
|
|
@@ -1473,7 +2603,7 @@ var MoltsPayServer = class {
|
|
|
1473
2603
|
};
|
|
1474
2604
|
this.useMainnet = process.env.USE_MAINNET?.toLowerCase() === "true";
|
|
1475
2605
|
this.networkId = this.useMainnet ? "eip155:8453" : "eip155:84532";
|
|
1476
|
-
const defaultFallback = ["tempo"];
|
|
2606
|
+
const defaultFallback = ["tempo", "bnb", "solana"];
|
|
1477
2607
|
const envFallback = process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean);
|
|
1478
2608
|
const facilitatorConfig = options.facilitators || {
|
|
1479
2609
|
primary: process.env.FACILITATOR_PRIMARY || "cdp",
|
|
@@ -1516,12 +2646,20 @@ var MoltsPayServer = class {
|
|
|
1516
2646
|
*/
|
|
1517
2647
|
getProviderChains() {
|
|
1518
2648
|
const provider = this.manifest.provider;
|
|
2649
|
+
const getWalletForChain = (chainName, explicitWallet) => {
|
|
2650
|
+
if (explicitWallet) return explicitWallet;
|
|
2651
|
+
if ((chainName === "solana" || chainName === "solana_devnet") && provider.solana_wallet) {
|
|
2652
|
+
return provider.solana_wallet;
|
|
2653
|
+
}
|
|
2654
|
+
return provider.wallet;
|
|
2655
|
+
};
|
|
1519
2656
|
if (provider.chains && provider.chains.length > 0) {
|
|
1520
2657
|
return provider.chains.map((c) => {
|
|
1521
2658
|
const chainName = typeof c === "string" ? c : c.chain;
|
|
2659
|
+
const explicitWallet = typeof c === "object" ? c.wallet : null;
|
|
1522
2660
|
return {
|
|
1523
2661
|
network: CHAIN_TO_NETWORK[chainName] || "eip155:8453",
|
|
1524
|
-
wallet: (
|
|
2662
|
+
wallet: getWalletForChain(chainName, explicitWallet || void 0),
|
|
1525
2663
|
tokens: (typeof c === "object" ? c.tokens : null) || ["USDC"]
|
|
1526
2664
|
};
|
|
1527
2665
|
});
|
|
@@ -1530,7 +2668,7 @@ var MoltsPayServer = class {
|
|
|
1530
2668
|
const network = CHAIN_TO_NETWORK[chain] || this.networkId;
|
|
1531
2669
|
return [{
|
|
1532
2670
|
network,
|
|
1533
|
-
wallet:
|
|
2671
|
+
wallet: getWalletForChain(chain),
|
|
1534
2672
|
tokens: ["USDC"]
|
|
1535
2673
|
}];
|
|
1536
2674
|
}
|
|
@@ -1601,7 +2739,8 @@ var MoltsPayServer = class {
|
|
|
1601
2739
|
}
|
|
1602
2740
|
const body = await this.readBody(req);
|
|
1603
2741
|
const paymentHeader = req.headers[PAYMENT_HEADER2];
|
|
1604
|
-
|
|
2742
|
+
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
2743
|
+
return await this.handleProxy(body, paymentHeader, authHeader, res);
|
|
1605
2744
|
}
|
|
1606
2745
|
const servicePath = url.pathname.replace(/^\//, "");
|
|
1607
2746
|
const skill = this.skills.get(servicePath);
|
|
@@ -1638,7 +2777,9 @@ var MoltsPayServer = class {
|
|
|
1638
2777
|
name: this.manifest.provider.name,
|
|
1639
2778
|
description: this.manifest.provider.description,
|
|
1640
2779
|
wallet: this.manifest.provider.wallet,
|
|
1641
|
-
chain: this.manifest.provider.chain || "base"
|
|
2780
|
+
chain: this.manifest.provider.chain || "base",
|
|
2781
|
+
solana_wallet: this.manifest.provider.solana_wallet,
|
|
2782
|
+
chains: this.manifest.provider.chains
|
|
1642
2783
|
},
|
|
1643
2784
|
services,
|
|
1644
2785
|
endpoints: {
|
|
@@ -1751,6 +2892,21 @@ var MoltsPayServer = class {
|
|
|
1751
2892
|
});
|
|
1752
2893
|
}
|
|
1753
2894
|
console.log(`[MoltsPay] Verified by ${verifyResult.facilitator}`);
|
|
2895
|
+
const isSolana = isSolanaNetwork(paymentNetwork);
|
|
2896
|
+
let settlement = null;
|
|
2897
|
+
if (isSolana) {
|
|
2898
|
+
console.log(`[MoltsPay] Solana detected - settling payment FIRST (blockhash expiry protection)`);
|
|
2899
|
+
try {
|
|
2900
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
2901
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
2902
|
+
} catch (err) {
|
|
2903
|
+
console.error("[MoltsPay] Solana settlement failed:", err.message);
|
|
2904
|
+
return this.sendJson(res, 402, {
|
|
2905
|
+
error: "Payment settlement failed",
|
|
2906
|
+
message: err.message
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
1754
2910
|
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
1755
2911
|
console.log(`[MoltsPay] Executing skill: ${service} (timeout: ${timeoutSeconds}s)`);
|
|
1756
2912
|
let result;
|
|
@@ -1765,16 +2921,19 @@ var MoltsPayServer = class {
|
|
|
1765
2921
|
console.error("[MoltsPay] Skill execution failed:", err.message);
|
|
1766
2922
|
return this.sendJson(res, 500, {
|
|
1767
2923
|
error: "Service execution failed",
|
|
1768
|
-
message: err.message
|
|
2924
|
+
message: err.message,
|
|
2925
|
+
paymentSettled: isSolana ? true : false,
|
|
2926
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
1769
2927
|
});
|
|
1770
2928
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
2929
|
+
if (!isSolana) {
|
|
2930
|
+
console.log(`[MoltsPay] Skill succeeded, settling payment...`);
|
|
2931
|
+
try {
|
|
2932
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
2933
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
2934
|
+
} catch (err) {
|
|
2935
|
+
console.error("[MoltsPay] Settlement failed:", err.message);
|
|
2936
|
+
}
|
|
1778
2937
|
}
|
|
1779
2938
|
const responseHeaders = {};
|
|
1780
2939
|
if (settlement?.success) {
|
|
@@ -2050,7 +3209,7 @@ var MoltsPayServer = class {
|
|
|
2050
3209
|
const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
|
|
2051
3210
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
2052
3211
|
const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
|
|
2053
|
-
|
|
3212
|
+
const requirements = {
|
|
2054
3213
|
scheme: "exact",
|
|
2055
3214
|
network: selectedNetwork,
|
|
2056
3215
|
asset: tokenAddress,
|
|
@@ -2059,6 +3218,27 @@ var MoltsPayServer = class {
|
|
|
2059
3218
|
maxTimeoutSeconds: 300,
|
|
2060
3219
|
extra: tokenDomain
|
|
2061
3220
|
};
|
|
3221
|
+
if (selectedNetwork === "solana:mainnet" || selectedNetwork === "solana:devnet") {
|
|
3222
|
+
const solanaFacilitator = this.registry.get("solana");
|
|
3223
|
+
const feePayerPubkey = solanaFacilitator?.getFeePayerPubkey?.();
|
|
3224
|
+
if (feePayerPubkey) {
|
|
3225
|
+
requirements.extra = {
|
|
3226
|
+
...requirements.extra || {},
|
|
3227
|
+
solanaFeePayer: feePayerPubkey
|
|
3228
|
+
};
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
if (selectedNetwork === "eip155:56" || selectedNetwork === "eip155:97") {
|
|
3232
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
3233
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
3234
|
+
if (spenderAddress) {
|
|
3235
|
+
requirements.extra = {
|
|
3236
|
+
...requirements.extra || {},
|
|
3237
|
+
bnbSpender: spenderAddress
|
|
3238
|
+
};
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
return requirements;
|
|
2062
3242
|
}
|
|
2063
3243
|
/**
|
|
2064
3244
|
* Detect which token is being used in the payment
|
|
@@ -2111,8 +3291,10 @@ var MoltsPayServer = class {
|
|
|
2111
3291
|
isProxyAllowed(clientIP) {
|
|
2112
3292
|
const allowedIPs = process.env.PROXY_ALLOWED_IPS?.split(",").map((ip) => ip.trim()) || [];
|
|
2113
3293
|
if (allowedIPs.length === 0) {
|
|
2114
|
-
|
|
2115
|
-
|
|
3294
|
+
return true;
|
|
3295
|
+
}
|
|
3296
|
+
if (allowedIPs.includes("*")) {
|
|
3297
|
+
return true;
|
|
2116
3298
|
}
|
|
2117
3299
|
const normalizedIP = clientIP === "::1" ? "127.0.0.1" : clientIP.replace("::ffff:", "");
|
|
2118
3300
|
const allowed = allowedIPs.includes(normalizedIP) || allowedIPs.includes(clientIP);
|
|
@@ -2124,31 +3306,42 @@ var MoltsPayServer = class {
|
|
|
2124
3306
|
/**
|
|
2125
3307
|
* POST /proxy - Handle payment for external services (moltspay-creators)
|
|
2126
3308
|
*
|
|
2127
|
-
* This endpoint allows other services to delegate x402 payment handling.
|
|
3309
|
+
* This endpoint allows other services to delegate x402/MPP payment handling.
|
|
2128
3310
|
* It does NOT execute any skill - just handles payment verification/settlement.
|
|
2129
3311
|
*
|
|
2130
3312
|
* Request body:
|
|
2131
3313
|
* { wallet, amount, currency, chain, memo, serviceId, description }
|
|
2132
3314
|
*
|
|
2133
|
-
*
|
|
2134
|
-
*
|
|
3315
|
+
* For x402 (base, polygon, base_sepolia):
|
|
3316
|
+
* Without X-Payment header: returns 402 with X-Payment-Required
|
|
3317
|
+
* With X-Payment header: verifies payment via CDP
|
|
3318
|
+
*
|
|
3319
|
+
* For MPP (tempo_moderato):
|
|
3320
|
+
* Without Authorization header: returns 402 with WWW-Authenticate
|
|
3321
|
+
* With Authorization: Payment header: verifies tx on Tempo chain
|
|
2135
3322
|
*/
|
|
2136
|
-
async handleProxy(body, paymentHeader, res) {
|
|
3323
|
+
async handleProxy(body, paymentHeader, authHeader, res) {
|
|
2137
3324
|
const { wallet, amount, currency, chain, memo, serviceId, description } = body;
|
|
2138
3325
|
if (!wallet || !amount) {
|
|
2139
3326
|
return this.sendJson(res, 400, { error: "Missing required fields: wallet, amount" });
|
|
2140
3327
|
}
|
|
2141
|
-
|
|
2142
|
-
|
|
3328
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
|
|
3329
|
+
if (chain && !supportedChains.includes(chain)) {
|
|
3330
|
+
return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
|
|
3331
|
+
}
|
|
3332
|
+
const isSolanaChain = chain === "solana" || chain === "solana_devnet";
|
|
3333
|
+
const isValidEvmAddress = /^0x[a-fA-F0-9]{40}$/.test(wallet);
|
|
3334
|
+
const isValidSolanaAddress2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(wallet);
|
|
3335
|
+
if (isSolanaChain && !isValidSolanaAddress2) {
|
|
3336
|
+
return this.sendJson(res, 400, { error: "Invalid Solana wallet address format" });
|
|
3337
|
+
}
|
|
3338
|
+
if (!isSolanaChain && !isValidEvmAddress) {
|
|
3339
|
+
return this.sendJson(res, 400, { error: "Invalid EVM wallet address format" });
|
|
2143
3340
|
}
|
|
2144
3341
|
const amountNum = parseFloat(amount);
|
|
2145
3342
|
if (isNaN(amountNum) || amountNum <= 0) {
|
|
2146
3343
|
return this.sendJson(res, 400, { error: "Invalid amount" });
|
|
2147
3344
|
}
|
|
2148
|
-
const supportedChains = ["base", "polygon", "base_sepolia"];
|
|
2149
|
-
if (chain && !supportedChains.includes(chain)) {
|
|
2150
|
-
return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
|
|
2151
|
-
}
|
|
2152
3345
|
const proxyConfig = {
|
|
2153
3346
|
id: serviceId || "proxy",
|
|
2154
3347
|
name: description || "Proxy Payment",
|
|
@@ -2160,6 +3353,9 @@ var MoltsPayServer = class {
|
|
|
2160
3353
|
input: {},
|
|
2161
3354
|
output: {}
|
|
2162
3355
|
};
|
|
3356
|
+
if (chain === "tempo_moderato") {
|
|
3357
|
+
return await this.handleProxyMPP(body, proxyConfig, authHeader, res);
|
|
3358
|
+
}
|
|
2163
3359
|
const requirements = this.buildProxyPaymentRequirements(proxyConfig, wallet, currency, chain);
|
|
2164
3360
|
if (!paymentHeader) {
|
|
2165
3361
|
return this.sendProxyPaymentRequired(proxyConfig, wallet, memo, chain, res);
|
|
@@ -2171,37 +3367,225 @@ var MoltsPayServer = class {
|
|
|
2171
3367
|
} catch {
|
|
2172
3368
|
return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
|
|
2173
3369
|
}
|
|
2174
|
-
if (payment.x402Version !== X402_VERSION3) {
|
|
2175
|
-
return this.sendJson(res, 402, { error: `Unsupported x402 version: ${payment.x402Version}` });
|
|
3370
|
+
if (payment.x402Version !== X402_VERSION3) {
|
|
3371
|
+
return this.sendJson(res, 402, { error: `Unsupported x402 version: ${payment.x402Version}` });
|
|
3372
|
+
}
|
|
3373
|
+
const scheme = payment.accepted?.scheme || payment.scheme;
|
|
3374
|
+
const network = payment.accepted?.network || payment.network;
|
|
3375
|
+
if (scheme !== "exact") {
|
|
3376
|
+
return this.sendJson(res, 402, { error: `Unsupported scheme: ${scheme}` });
|
|
3377
|
+
}
|
|
3378
|
+
const expectedNetwork = chain ? CHAIN_TO_NETWORK[chain] || this.networkId : this.networkId;
|
|
3379
|
+
if (network !== expectedNetwork) {
|
|
3380
|
+
return this.sendJson(res, 402, { error: `Network mismatch: expected ${expectedNetwork}, got ${network}` });
|
|
3381
|
+
}
|
|
3382
|
+
console.log(`[MoltsPay] /proxy: Verifying payment for ${wallet}...`);
|
|
3383
|
+
const verifyResult = await this.registry.verify(payment, requirements);
|
|
3384
|
+
if (!verifyResult.valid) {
|
|
3385
|
+
return this.sendJson(res, 402, {
|
|
3386
|
+
success: false,
|
|
3387
|
+
error: `Payment verification failed: ${verifyResult.error}`,
|
|
3388
|
+
facilitator: verifyResult.facilitator
|
|
3389
|
+
});
|
|
3390
|
+
}
|
|
3391
|
+
console.log(`[MoltsPay] /proxy: Verified by ${verifyResult.facilitator}`);
|
|
3392
|
+
const { execute, service, params } = body;
|
|
3393
|
+
if (execute && service) {
|
|
3394
|
+
const skill = this.skills.get(service);
|
|
3395
|
+
if (!skill) {
|
|
3396
|
+
console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
|
|
3397
|
+
return this.sendJson(res, 404, {
|
|
3398
|
+
success: false,
|
|
3399
|
+
paymentSettled: false,
|
|
3400
|
+
error: `Service not found: ${service}`
|
|
3401
|
+
});
|
|
3402
|
+
}
|
|
3403
|
+
const isSolana = isSolanaNetwork(network);
|
|
3404
|
+
let settlement2 = null;
|
|
3405
|
+
if (isSolana) {
|
|
3406
|
+
console.log(`[MoltsPay] /proxy: Solana detected - settling payment FIRST`);
|
|
3407
|
+
try {
|
|
3408
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
3409
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
3410
|
+
if (!settlement2.success) {
|
|
3411
|
+
console.error(`[MoltsPay] /proxy: Solana settlement failed: ${settlement2.error}`);
|
|
3412
|
+
return this.sendJson(res, 402, {
|
|
3413
|
+
success: false,
|
|
3414
|
+
paymentSettled: false,
|
|
3415
|
+
error: `Payment settlement failed: ${settlement2.error || "Unknown error"}`
|
|
3416
|
+
});
|
|
3417
|
+
}
|
|
3418
|
+
} catch (err) {
|
|
3419
|
+
console.error("[MoltsPay] /proxy: Solana settlement failed:", err.message);
|
|
3420
|
+
return this.sendJson(res, 402, {
|
|
3421
|
+
success: false,
|
|
3422
|
+
paymentSettled: false,
|
|
3423
|
+
error: `Payment settlement failed: ${err.message}`
|
|
3424
|
+
});
|
|
3425
|
+
}
|
|
3426
|
+
} else {
|
|
3427
|
+
console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
|
|
3428
|
+
}
|
|
3429
|
+
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
3430
|
+
let result;
|
|
3431
|
+
try {
|
|
3432
|
+
result = await Promise.race([
|
|
3433
|
+
skill.handler(params || {}),
|
|
3434
|
+
new Promise(
|
|
3435
|
+
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
3436
|
+
)
|
|
3437
|
+
]);
|
|
3438
|
+
console.log(`[MoltsPay] /proxy: Skill succeeded`);
|
|
3439
|
+
} catch (err) {
|
|
3440
|
+
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}`);
|
|
3441
|
+
return this.sendJson(res, 500, {
|
|
3442
|
+
success: false,
|
|
3443
|
+
paymentSettled: isSolana ? true : false,
|
|
3444
|
+
error: `Service execution failed: ${err.message}`,
|
|
3445
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
3446
|
+
});
|
|
3447
|
+
}
|
|
3448
|
+
if (!isSolana) {
|
|
3449
|
+
console.log(`[MoltsPay] /proxy: Settling payment...`);
|
|
3450
|
+
try {
|
|
3451
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
3452
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
3453
|
+
} catch (err) {
|
|
3454
|
+
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
3455
|
+
return this.sendJson(res, 200, {
|
|
3456
|
+
success: true,
|
|
3457
|
+
verified: true,
|
|
3458
|
+
settled: false,
|
|
3459
|
+
settlementError: err.message,
|
|
3460
|
+
from: payment.payload?.authorization?.from,
|
|
3461
|
+
paidTo: wallet,
|
|
3462
|
+
amount: amountNum,
|
|
3463
|
+
currency: currency || "USDC",
|
|
3464
|
+
memo,
|
|
3465
|
+
result
|
|
3466
|
+
});
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
return this.sendJson(res, 200, {
|
|
3470
|
+
success: true,
|
|
3471
|
+
verified: true,
|
|
3472
|
+
settled: settlement2?.success || false,
|
|
3473
|
+
txHash: settlement2?.transaction,
|
|
3474
|
+
from: payment.payload?.authorization?.from,
|
|
3475
|
+
paidTo: wallet,
|
|
3476
|
+
amount: amountNum,
|
|
3477
|
+
currency: currency || "USDC",
|
|
3478
|
+
facilitator: settlement2?.facilitator,
|
|
3479
|
+
memo,
|
|
3480
|
+
result
|
|
3481
|
+
});
|
|
3482
|
+
}
|
|
3483
|
+
console.log(`[MoltsPay] /proxy: Settling payment (no execution)...`);
|
|
3484
|
+
let settlement = null;
|
|
3485
|
+
try {
|
|
3486
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
3487
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
3488
|
+
} catch (err) {
|
|
3489
|
+
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
3490
|
+
return this.sendJson(res, 500, {
|
|
3491
|
+
success: false,
|
|
3492
|
+
error: `Settlement failed: ${err.message}`
|
|
3493
|
+
});
|
|
3494
|
+
}
|
|
3495
|
+
this.sendJson(res, 200, {
|
|
3496
|
+
success: true,
|
|
3497
|
+
verified: true,
|
|
3498
|
+
settled: settlement?.success || false,
|
|
3499
|
+
txHash: settlement?.transaction,
|
|
3500
|
+
from: payment.payload?.authorization?.from,
|
|
3501
|
+
// Buyer's wallet address
|
|
3502
|
+
paidTo: wallet,
|
|
3503
|
+
amount: amountNum,
|
|
3504
|
+
currency: currency || "USDC",
|
|
3505
|
+
facilitator: settlement?.facilitator,
|
|
3506
|
+
memo
|
|
3507
|
+
});
|
|
3508
|
+
}
|
|
3509
|
+
/**
|
|
3510
|
+
* Handle MPP payment flow for /proxy endpoint (tempo_moderato chain)
|
|
3511
|
+
*/
|
|
3512
|
+
async handleProxyMPP(body, config, authHeader, res) {
|
|
3513
|
+
const { wallet, amount, memo, serviceId } = body;
|
|
3514
|
+
const amountNum = parseFloat(amount);
|
|
3515
|
+
const amountInUnits = Math.floor(amountNum * 1e6).toString();
|
|
3516
|
+
if (!authHeader || !authHeader.toLowerCase().startsWith("payment ")) {
|
|
3517
|
+
const challengeId = this.generateChallengeId();
|
|
3518
|
+
const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
|
|
3519
|
+
const mppRequest = {
|
|
3520
|
+
amount: amountInUnits,
|
|
3521
|
+
currency: tokenAddress,
|
|
3522
|
+
methodDetails: {
|
|
3523
|
+
chainId: 42431,
|
|
3524
|
+
feePayer: true
|
|
3525
|
+
},
|
|
3526
|
+
recipient: wallet
|
|
3527
|
+
};
|
|
3528
|
+
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
3529
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
3530
|
+
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
3531
|
+
res.writeHead(402, {
|
|
3532
|
+
"Content-Type": "application/problem+json",
|
|
3533
|
+
[MPP_WWW_AUTH_HEADER]: wwwAuth
|
|
3534
|
+
});
|
|
3535
|
+
res.end(JSON.stringify({
|
|
3536
|
+
type: "https://paymentauth.org/problems/payment-required",
|
|
3537
|
+
title: "Payment Required",
|
|
3538
|
+
status: 402,
|
|
3539
|
+
detail: `Payment is required (${config.name}).`,
|
|
3540
|
+
service: serviceId || "proxy",
|
|
3541
|
+
price: amountNum,
|
|
3542
|
+
currency: "USDC"
|
|
3543
|
+
}, null, 2));
|
|
3544
|
+
return;
|
|
3545
|
+
}
|
|
3546
|
+
const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
|
|
3547
|
+
if (!credentialMatch) {
|
|
3548
|
+
return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
|
|
2176
3549
|
}
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
3550
|
+
let mppCredential;
|
|
3551
|
+
try {
|
|
3552
|
+
const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
3553
|
+
const decoded = Buffer.from(base64, "base64").toString("utf-8");
|
|
3554
|
+
mppCredential = JSON.parse(decoded);
|
|
3555
|
+
} catch (err) {
|
|
3556
|
+
console.error("[MoltsPay] /proxy MPP: Failed to parse credential:", err);
|
|
3557
|
+
return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
|
|
2181
3558
|
}
|
|
2182
|
-
|
|
2183
|
-
if (
|
|
2184
|
-
|
|
3559
|
+
let txHash;
|
|
3560
|
+
if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
|
|
3561
|
+
txHash = mppCredential.payload.hash;
|
|
3562
|
+
} else {
|
|
3563
|
+
return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
|
|
2185
3564
|
}
|
|
2186
|
-
console.log(`[MoltsPay] /proxy: Verifying
|
|
2187
|
-
const
|
|
2188
|
-
|
|
3565
|
+
console.log(`[MoltsPay] /proxy MPP: Verifying tx ${txHash} on Tempo...`);
|
|
3566
|
+
const requirements = this.buildPaymentRequirements(config, "eip155:42431", wallet, "USDC");
|
|
3567
|
+
const paymentPayload = {
|
|
3568
|
+
x402Version: X402_VERSION3,
|
|
3569
|
+
scheme: "exact",
|
|
3570
|
+
network: "eip155:42431",
|
|
3571
|
+
payload: { txHash, chainId: 42431 }
|
|
3572
|
+
};
|
|
3573
|
+
const verification = await this.registry.verify(paymentPayload, requirements);
|
|
3574
|
+
if (!verification.valid) {
|
|
2189
3575
|
return this.sendJson(res, 402, {
|
|
2190
|
-
|
|
2191
|
-
error: `Payment verification failed: ${verifyResult.error}`,
|
|
2192
|
-
facilitator: verifyResult.facilitator
|
|
3576
|
+
error: `Payment verification failed: ${verification.error}`
|
|
2193
3577
|
});
|
|
2194
3578
|
}
|
|
2195
|
-
console.log(`[MoltsPay] /proxy:
|
|
3579
|
+
console.log(`[MoltsPay] /proxy MPP: Payment verified by ${verification.facilitator}`);
|
|
2196
3580
|
const { execute, service, params } = body;
|
|
2197
3581
|
if (execute && service) {
|
|
2198
|
-
console.log(`[MoltsPay] /proxy: Executing skill
|
|
3582
|
+
console.log(`[MoltsPay] /proxy MPP: Executing skill: ${service}`);
|
|
2199
3583
|
const skill = this.skills.get(service);
|
|
2200
3584
|
if (!skill) {
|
|
2201
|
-
console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
|
|
2202
3585
|
return this.sendJson(res, 404, {
|
|
2203
3586
|
success: false,
|
|
2204
|
-
paymentSettled:
|
|
3587
|
+
paymentSettled: true,
|
|
3588
|
+
// Payment already happened on Tempo
|
|
2205
3589
|
error: `Service not found: ${service}`
|
|
2206
3590
|
});
|
|
2207
3591
|
}
|
|
@@ -2214,73 +3598,36 @@ var MoltsPayServer = class {
|
|
|
2214
3598
|
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
2215
3599
|
)
|
|
2216
3600
|
]);
|
|
2217
|
-
console.log(`[MoltsPay] /proxy: Skill succeeded, now settling payment...`);
|
|
2218
3601
|
} catch (err) {
|
|
2219
|
-
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}
|
|
3602
|
+
console.error(`[MoltsPay] /proxy MPP: Skill failed: ${err.message}`);
|
|
2220
3603
|
return this.sendJson(res, 500, {
|
|
2221
3604
|
success: false,
|
|
2222
|
-
paymentSettled:
|
|
3605
|
+
paymentSettled: true,
|
|
2223
3606
|
error: `Service execution failed: ${err.message}`
|
|
2224
3607
|
});
|
|
2225
3608
|
}
|
|
2226
|
-
let settlement2 = null;
|
|
2227
|
-
try {
|
|
2228
|
-
settlement2 = await this.registry.settle(payment, requirements);
|
|
2229
|
-
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
2230
|
-
} catch (err) {
|
|
2231
|
-
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
2232
|
-
return this.sendJson(res, 200, {
|
|
2233
|
-
success: true,
|
|
2234
|
-
verified: true,
|
|
2235
|
-
settled: false,
|
|
2236
|
-
settlementError: err.message,
|
|
2237
|
-
from: payment.payload?.authorization?.from,
|
|
2238
|
-
// Buyer's wallet address
|
|
2239
|
-
paidTo: wallet,
|
|
2240
|
-
amount: amountNum,
|
|
2241
|
-
currency: currency || "USDC",
|
|
2242
|
-
memo,
|
|
2243
|
-
result
|
|
2244
|
-
});
|
|
2245
|
-
}
|
|
2246
3609
|
return this.sendJson(res, 200, {
|
|
2247
3610
|
success: true,
|
|
2248
3611
|
verified: true,
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
from: payment.payload?.authorization?.from,
|
|
2252
|
-
// Buyer's wallet address
|
|
3612
|
+
txHash,
|
|
3613
|
+
chain: "tempo_moderato",
|
|
2253
3614
|
paidTo: wallet,
|
|
2254
3615
|
amount: amountNum,
|
|
2255
|
-
currency:
|
|
2256
|
-
facilitator:
|
|
3616
|
+
currency: "USDC",
|
|
3617
|
+
facilitator: verification.facilitator,
|
|
2257
3618
|
memo,
|
|
2258
3619
|
result
|
|
2259
3620
|
});
|
|
2260
3621
|
}
|
|
2261
|
-
console.log(`[MoltsPay] /proxy: Settling payment (no execution)...`);
|
|
2262
|
-
let settlement = null;
|
|
2263
|
-
try {
|
|
2264
|
-
settlement = await this.registry.settle(payment, requirements);
|
|
2265
|
-
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
2266
|
-
} catch (err) {
|
|
2267
|
-
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
2268
|
-
return this.sendJson(res, 500, {
|
|
2269
|
-
success: false,
|
|
2270
|
-
error: `Settlement failed: ${err.message}`
|
|
2271
|
-
});
|
|
2272
|
-
}
|
|
2273
3622
|
this.sendJson(res, 200, {
|
|
2274
3623
|
success: true,
|
|
2275
3624
|
verified: true,
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
from: payment.payload?.authorization?.from,
|
|
2279
|
-
// Buyer's wallet address
|
|
3625
|
+
txHash,
|
|
3626
|
+
chain: "tempo_moderato",
|
|
2280
3627
|
paidTo: wallet,
|
|
2281
3628
|
amount: amountNum,
|
|
2282
|
-
currency:
|
|
2283
|
-
facilitator:
|
|
3629
|
+
currency: "USDC",
|
|
3630
|
+
facilitator: verification.facilitator,
|
|
2284
3631
|
memo
|
|
2285
3632
|
});
|
|
2286
3633
|
}
|
|
@@ -2295,7 +3642,7 @@ var MoltsPayServer = class {
|
|
|
2295
3642
|
const tokenAddresses = TOKEN_ADDRESSES[networkId] || TOKEN_ADDRESSES[this.networkId] || {};
|
|
2296
3643
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
2297
3644
|
const tokenDomain = getTokenDomain(networkId, selectedToken);
|
|
2298
|
-
|
|
3645
|
+
const requirements = {
|
|
2299
3646
|
scheme: "exact",
|
|
2300
3647
|
network: networkId,
|
|
2301
3648
|
asset: tokenAddress,
|
|
@@ -2305,6 +3652,17 @@ var MoltsPayServer = class {
|
|
|
2305
3652
|
maxTimeoutSeconds: 300,
|
|
2306
3653
|
extra: tokenDomain
|
|
2307
3654
|
};
|
|
3655
|
+
if (networkId === "eip155:56" || networkId === "eip155:97") {
|
|
3656
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
3657
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
3658
|
+
if (spenderAddress) {
|
|
3659
|
+
requirements.extra = {
|
|
3660
|
+
...requirements.extra || {},
|
|
3661
|
+
bnbSpender: spenderAddress
|
|
3662
|
+
};
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
return requirements;
|
|
2308
3666
|
}
|
|
2309
3667
|
/**
|
|
2310
3668
|
* Return 402 with x402 payment requirements for proxy endpoint
|
|
@@ -2354,14 +3712,14 @@ if (!globalThis.crypto) {
|
|
|
2354
3712
|
}
|
|
2355
3713
|
function getVersion() {
|
|
2356
3714
|
const locations = [
|
|
2357
|
-
(0,
|
|
2358
|
-
(0,
|
|
2359
|
-
(0,
|
|
3715
|
+
(0, import_path3.join)(__dirname, "../../package.json"),
|
|
3716
|
+
(0, import_path3.join)(__dirname, "../package.json"),
|
|
3717
|
+
(0, import_path3.join)(process.cwd(), "node_modules/moltspay/package.json")
|
|
2360
3718
|
];
|
|
2361
3719
|
for (const loc of locations) {
|
|
2362
3720
|
try {
|
|
2363
|
-
if ((0,
|
|
2364
|
-
const pkg = JSON.parse((0,
|
|
3721
|
+
if ((0, import_fs5.existsSync)(loc)) {
|
|
3722
|
+
const pkg = JSON.parse((0, import_fs5.readFileSync)(loc, "utf-8"));
|
|
2365
3723
|
if (pkg.name === "moltspay") return pkg.version;
|
|
2366
3724
|
}
|
|
2367
3725
|
} catch {
|
|
@@ -2369,11 +3727,94 @@ function getVersion() {
|
|
|
2369
3727
|
}
|
|
2370
3728
|
return "0.0.0";
|
|
2371
3729
|
}
|
|
3730
|
+
var BNB_SPONSOR_KEY = process.env.MOLTSPAY_BNB_SPONSOR_KEY;
|
|
3731
|
+
var BNB_SPENDER_ADDRESS = process.env.MOLTSPAY_BNB_SPENDER || "0xEBB45208D806A0c73F9673E0c5713FF720DD6b79";
|
|
3732
|
+
var ERC20_APPROVE_ABI = [
|
|
3733
|
+
"function approve(address spender, uint256 amount) returns (bool)",
|
|
3734
|
+
"function allowance(address owner, address spender) view returns (uint256)"
|
|
3735
|
+
];
|
|
3736
|
+
async function setupBNBApprovals(client, chain, spenderAddress, sponsorGas = false) {
|
|
3737
|
+
const chainConfig = CHAINS[chain];
|
|
3738
|
+
const provider = new import_ethers2.ethers.JsonRpcProvider(chainConfig.rpc);
|
|
3739
|
+
const wallet = client.getWallet();
|
|
3740
|
+
if (!wallet) {
|
|
3741
|
+
console.log(" \u274C No wallet found");
|
|
3742
|
+
return;
|
|
3743
|
+
}
|
|
3744
|
+
const signer = wallet.connect(provider);
|
|
3745
|
+
console.log(` Spender: ${spenderAddress}`);
|
|
3746
|
+
let bnbBalance = await provider.getBalance(wallet.address);
|
|
3747
|
+
const minGasRequired = import_ethers2.ethers.parseEther("0.0005");
|
|
3748
|
+
if (bnbBalance < minGasRequired) {
|
|
3749
|
+
if (sponsorGas && BNB_SPONSOR_KEY) {
|
|
3750
|
+
console.log(" \u23F3 Sponsoring BNB gas for approvals...");
|
|
3751
|
+
try {
|
|
3752
|
+
const sponsorWallet = new import_ethers2.ethers.Wallet(BNB_SPONSOR_KEY, provider);
|
|
3753
|
+
const tx = await sponsorWallet.sendTransaction({
|
|
3754
|
+
to: wallet.address,
|
|
3755
|
+
value: import_ethers2.ethers.parseEther("0.001")
|
|
3756
|
+
});
|
|
3757
|
+
await tx.wait();
|
|
3758
|
+
console.log(` \u2705 Sponsored 0.001 BNB (tx: ${tx.hash.slice(0, 10)}...)`);
|
|
3759
|
+
bnbBalance = await provider.getBalance(wallet.address);
|
|
3760
|
+
} catch (err) {
|
|
3761
|
+
console.log(` \u26A0\uFE0F Gas sponsorship failed: ${err.message}`);
|
|
3762
|
+
console.log(` \u{1F4A1} Get testnet BNB: https://testnet.bnbchain.org/faucet-smart`);
|
|
3763
|
+
return;
|
|
3764
|
+
}
|
|
3765
|
+
} else {
|
|
3766
|
+
console.log(` \u26A0\uFE0F Need BNB for gas (~0.0005 BNB)`);
|
|
3767
|
+
console.log(` \u{1F4A1} Run: npx moltspay faucet --chain bnb_testnet`);
|
|
3768
|
+
console.log(` Then run: npx moltspay approve --chain ${chain} --spender ${spenderAddress}`);
|
|
3769
|
+
return;
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
for (const tokenSymbol of ["USDT", "USDC"]) {
|
|
3773
|
+
const tokenConfig = chainConfig.tokens[tokenSymbol];
|
|
3774
|
+
const tokenContract = new import_ethers2.ethers.Contract(tokenConfig.address, ERC20_APPROVE_ABI, signer);
|
|
3775
|
+
const allowance = await tokenContract.allowance(wallet.address, spenderAddress);
|
|
3776
|
+
if (allowance > 0n) {
|
|
3777
|
+
console.log(` \u2705 ${tokenSymbol}: already approved for ${spenderAddress.slice(0, 10)}...`);
|
|
3778
|
+
continue;
|
|
3779
|
+
}
|
|
3780
|
+
console.log(` \u23F3 Approving ${tokenSymbol}...`);
|
|
3781
|
+
try {
|
|
3782
|
+
const tx = await tokenContract.approve(spenderAddress, import_ethers2.ethers.MaxUint256);
|
|
3783
|
+
await tx.wait();
|
|
3784
|
+
console.log(` \u2705 ${tokenSymbol}: approved (tx: ${tx.hash.slice(0, 10)}...)`);
|
|
3785
|
+
} catch (err) {
|
|
3786
|
+
console.log(` \u274C ${tokenSymbol}: approval failed - ${err.message}`);
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
console.log("");
|
|
3790
|
+
}
|
|
3791
|
+
async function checkBNBApprovals(address, chain, configDir = DEFAULT_CONFIG_DIR2) {
|
|
3792
|
+
const chainConfig = CHAINS[chain];
|
|
3793
|
+
const provider = new import_ethers2.ethers.JsonRpcProvider(chainConfig.rpc);
|
|
3794
|
+
let spenderAddress = null;
|
|
3795
|
+
try {
|
|
3796
|
+
const walletPath = (0, import_path3.join)(configDir, "wallet.json");
|
|
3797
|
+
const walletData = JSON.parse((0, import_fs5.readFileSync)(walletPath, "utf-8"));
|
|
3798
|
+
spenderAddress = walletData.approvals?.[chain] || null;
|
|
3799
|
+
} catch {
|
|
3800
|
+
}
|
|
3801
|
+
const result = { usdt: false, usdc: false, spender: spenderAddress };
|
|
3802
|
+
if (!spenderAddress) {
|
|
3803
|
+
return result;
|
|
3804
|
+
}
|
|
3805
|
+
for (const tokenSymbol of ["USDT", "USDC"]) {
|
|
3806
|
+
const tokenConfig = chainConfig.tokens[tokenSymbol];
|
|
3807
|
+
const tokenContract = new import_ethers2.ethers.Contract(tokenConfig.address, ERC20_APPROVE_ABI, provider);
|
|
3808
|
+
const allowance = await tokenContract.allowance(address, spenderAddress);
|
|
3809
|
+
result[tokenSymbol.toLowerCase()] = allowance > 0n;
|
|
3810
|
+
}
|
|
3811
|
+
return result;
|
|
3812
|
+
}
|
|
2372
3813
|
var program = new import_commander.Command();
|
|
2373
|
-
var
|
|
2374
|
-
var PID_FILE = (0,
|
|
2375
|
-
if (!(0,
|
|
2376
|
-
(0,
|
|
3814
|
+
var DEFAULT_CONFIG_DIR2 = (0, import_path3.join)((0, import_os3.homedir)(), ".moltspay");
|
|
3815
|
+
var PID_FILE = (0, import_path3.join)(DEFAULT_CONFIG_DIR2, "server.pid");
|
|
3816
|
+
if (!(0, import_fs5.existsSync)(DEFAULT_CONFIG_DIR2)) {
|
|
3817
|
+
(0, import_fs5.mkdirSync)(DEFAULT_CONFIG_DIR2, { recursive: true });
|
|
2377
3818
|
}
|
|
2378
3819
|
function prompt(question) {
|
|
2379
3820
|
const rl = readline.createInterface({
|
|
@@ -2388,19 +3829,49 @@ function prompt(question) {
|
|
|
2388
3829
|
});
|
|
2389
3830
|
}
|
|
2390
3831
|
program.name("moltspay").description("MoltsPay - Payment infrastructure for AI Agents").version(getVersion());
|
|
2391
|
-
program.command("init").description("Initialize MoltsPay client (create wallet, set limits)").option("--chain <chain>", "Blockchain to use", "base").option("--max-per-tx <amount>", "Max amount per transaction").option("--max-per-day <amount>", "Max amount per day").option("--config-dir <dir>", "Config directory",
|
|
2392
|
-
console.log("\n\u{1F510} MoltsPay Client Setup\n");
|
|
2393
|
-
if ((0, import_fs4.existsSync)((0, import_path2.join)(options.configDir, "wallet.json"))) {
|
|
2394
|
-
console.log('\u26A0\uFE0F Already initialized. Use "moltspay config" to update settings.');
|
|
2395
|
-
console.log(` Config dir: ${options.configDir}`);
|
|
2396
|
-
return;
|
|
2397
|
-
}
|
|
3832
|
+
program.command("init").description("Initialize MoltsPay client (create wallet, set limits)").option("--chain <chain>", "Blockchain to use", "base").option("--max-per-tx <amount>", "Max amount per transaction").option("--max-per-day <amount>", "Max amount per day").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (options) => {
|
|
2398
3833
|
let chain = options.chain;
|
|
2399
|
-
const
|
|
3834
|
+
const supportedEVMChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
|
|
3835
|
+
const supportedSolanaChains = ["solana", "solana_devnet"];
|
|
3836
|
+
const supportedChains = [...supportedEVMChains, ...supportedSolanaChains];
|
|
2400
3837
|
if (!supportedChains.includes(chain)) {
|
|
2401
3838
|
console.error(`\u274C Unknown chain: ${chain}. Supported: ${supportedChains.join(", ")}`);
|
|
2402
3839
|
process.exit(1);
|
|
2403
3840
|
}
|
|
3841
|
+
if (supportedSolanaChains.includes(chain)) {
|
|
3842
|
+
console.log("\n\u{1F7E3} Solana Wallet Setup\n");
|
|
3843
|
+
if (solanaWalletExists(options.configDir)) {
|
|
3844
|
+
const existingAddress = getSolanaAddress(options.configDir);
|
|
3845
|
+
console.log(`\u26A0\uFE0F Solana wallet already exists: ${existingAddress}`);
|
|
3846
|
+
console.log(` Config dir: ${options.configDir}`);
|
|
3847
|
+
return;
|
|
3848
|
+
}
|
|
3849
|
+
console.log("Creating Solana wallet...");
|
|
3850
|
+
const keypair = createSolanaWallet(options.configDir);
|
|
3851
|
+
const address = keypair.publicKey.toBase58();
|
|
3852
|
+
console.log(`
|
|
3853
|
+
\u2705 Solana wallet created: ${address}`);
|
|
3854
|
+
console.log(`
|
|
3855
|
+
\u{1F4C1} Config saved to: ${(0, import_path3.join)(options.configDir, "wallet-solana.json")}`);
|
|
3856
|
+
console.log(`
|
|
3857
|
+
\u26A0\uFE0F IMPORTANT: Back up your wallet file!`);
|
|
3858
|
+
console.log(` This file contains your private key!
|
|
3859
|
+
`);
|
|
3860
|
+
if (chain === "solana_devnet") {
|
|
3861
|
+
console.log("\u{1F4A1} Get testnet tokens:");
|
|
3862
|
+
console.log(" npx moltspay faucet --chain solana_devnet\n");
|
|
3863
|
+
} else {
|
|
3864
|
+
console.log(`\u{1F4B0} Fund your wallet with USDC on Solana to start (gasless - no SOL needed).
|
|
3865
|
+
`);
|
|
3866
|
+
}
|
|
3867
|
+
return;
|
|
3868
|
+
}
|
|
3869
|
+
console.log("\n\u{1F510} MoltsPay Client Setup\n");
|
|
3870
|
+
if ((0, import_fs5.existsSync)((0, import_path3.join)(options.configDir, "wallet.json"))) {
|
|
3871
|
+
console.log('\u26A0\uFE0F EVM wallet already initialized. Use "moltspay config" to update settings.');
|
|
3872
|
+
console.log(` Config dir: ${options.configDir}`);
|
|
3873
|
+
return;
|
|
3874
|
+
}
|
|
2404
3875
|
let maxPerTx = options.maxPerTx ? parseFloat(options.maxPerTx) : null;
|
|
2405
3876
|
let maxPerDay = options.maxPerDay ? parseFloat(options.maxPerDay) : null;
|
|
2406
3877
|
if (!maxPerTx) {
|
|
@@ -2422,13 +3893,21 @@ program.command("init").description("Initialize MoltsPay client (create wallet,
|
|
|
2422
3893
|
console.log(`
|
|
2423
3894
|
\u{1F4C1} Config saved to: ${result.configDir}`);
|
|
2424
3895
|
console.log(`
|
|
2425
|
-
\u26A0\uFE0F IMPORTANT: Back up ${(0,
|
|
3896
|
+
\u26A0\uFE0F IMPORTANT: Back up ${(0, import_path3.join)(result.configDir, "wallet.json")}`);
|
|
2426
3897
|
console.log(` This file contains your private key!
|
|
2427
3898
|
`);
|
|
3899
|
+
if (chain === "bnb" || chain === "bnb_testnet") {
|
|
3900
|
+
console.log("\u{1F4CB} Setting up BNB chain approvals...\n");
|
|
3901
|
+
console.log(" \u2139\uFE0F Using default spender. For other services, run:");
|
|
3902
|
+
console.log(` npx moltspay approve --chain ${chain} --spender <address>
|
|
3903
|
+
`);
|
|
3904
|
+
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
3905
|
+
await setupBNBApprovals(client, chain, BNB_SPENDER_ADDRESS, true);
|
|
3906
|
+
}
|
|
2428
3907
|
console.log(`\u{1F4B0} Fund your wallet with USDC on ${chain} to start using services.
|
|
2429
3908
|
`);
|
|
2430
3909
|
});
|
|
2431
|
-
program.command("config").description("Update MoltsPay settings").option("--max-per-tx <amount>", "Max amount per transaction").option("--max-per-day <amount>", "Max amount per day").option("--config-dir <dir>", "Config directory",
|
|
3910
|
+
program.command("config").description("Update MoltsPay settings").option("--max-per-tx <amount>", "Max amount per transaction").option("--max-per-day <amount>", "Max amount per day").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (options) => {
|
|
2432
3911
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
2433
3912
|
if (!client.isInitialized) {
|
|
2434
3913
|
console.log("\u274C Not initialized. Run: npx moltspay init");
|
|
@@ -2463,25 +3942,40 @@ program.command("config").description("Update MoltsPay settings").option("--max-
|
|
|
2463
3942
|
}
|
|
2464
3943
|
}
|
|
2465
3944
|
});
|
|
2466
|
-
program.command("fund <amount>").description("Fund wallet with USDC via Coinbase (US debit card / Apple Pay)").option("--chain <chain>", "Chain to fund (base, polygon, or
|
|
3945
|
+
program.command("fund <amount>").description("Fund wallet with USDC via Coinbase (US debit card / Apple Pay)").option("--chain <chain>", "Chain to fund (base, polygon, solana, base_sepolia, bnb, or bnb_testnet)", "base").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (amountStr, options) => {
|
|
2467
3946
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
2468
|
-
if (!client.isInitialized) {
|
|
2469
|
-
console.log("\u274C Not initialized. Run: npx moltspay init");
|
|
2470
|
-
return;
|
|
2471
|
-
}
|
|
2472
3947
|
const amount = parseFloat(amountStr);
|
|
2473
3948
|
if (isNaN(amount) || amount < 5) {
|
|
2474
3949
|
console.log("\u274C Minimum $5.");
|
|
2475
3950
|
return;
|
|
2476
3951
|
}
|
|
2477
3952
|
const chain = options.chain?.toLowerCase() || "base";
|
|
2478
|
-
if (!["base", "polygon", "base_sepolia"].includes(chain)) {
|
|
2479
|
-
console.log("\u274C Invalid chain. Use: base, polygon, or
|
|
3953
|
+
if (!["base", "polygon", "base_sepolia", "solana", "bnb", "bnb_testnet"].includes(chain)) {
|
|
3954
|
+
console.log("\u274C Invalid chain. Use: base, polygon, solana, base_sepolia, bnb, or bnb_testnet");
|
|
2480
3955
|
return;
|
|
2481
3956
|
}
|
|
3957
|
+
let walletAddress;
|
|
3958
|
+
if (chain === "solana") {
|
|
3959
|
+
const solanaWallet = loadSolanaWallet(options.configDir || DEFAULT_CONFIG_DIR2);
|
|
3960
|
+
if (!solanaWallet) {
|
|
3961
|
+
console.log("\u274C No Solana wallet found. Run: npx moltspay init --chain solana");
|
|
3962
|
+
return;
|
|
3963
|
+
}
|
|
3964
|
+
walletAddress = getSolanaAddress(options.configDir || DEFAULT_CONFIG_DIR2) || "";
|
|
3965
|
+
if (!walletAddress) {
|
|
3966
|
+
console.log("\u274C Could not get Solana wallet address.");
|
|
3967
|
+
return;
|
|
3968
|
+
}
|
|
3969
|
+
} else {
|
|
3970
|
+
if (!client.isInitialized) {
|
|
3971
|
+
console.log("\u274C Not initialized. Run: npx moltspay init");
|
|
3972
|
+
return;
|
|
3973
|
+
}
|
|
3974
|
+
walletAddress = client.address;
|
|
3975
|
+
}
|
|
2482
3976
|
if (chain === "base_sepolia") {
|
|
2483
3977
|
console.log("\n\u{1F9EA} Testnet Funding\n");
|
|
2484
|
-
console.log(` Wallet: ${
|
|
3978
|
+
console.log(` Wallet: ${walletAddress}`);
|
|
2485
3979
|
console.log(` Chain: Base Sepolia (testnet)
|
|
2486
3980
|
`);
|
|
2487
3981
|
console.log("\u{1F4A1} Use the MoltsPay faucet to get free testnet USDC:\n");
|
|
@@ -2489,9 +3983,36 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
|
|
|
2489
3983
|
console.log(" Or get from Circle Faucet: https://faucet.circle.com/\n");
|
|
2490
3984
|
return;
|
|
2491
3985
|
}
|
|
3986
|
+
if (chain === "bnb_testnet") {
|
|
3987
|
+
console.log("\n\u{1F9EA} BNB Testnet Funding\n");
|
|
3988
|
+
console.log(` Wallet: ${walletAddress}`);
|
|
3989
|
+
console.log(` Chain: BNB Testnet
|
|
3990
|
+
`);
|
|
3991
|
+
console.log("\u{1F4A1} Use the MoltsPay faucet to get testnet USDC + tBNB:\n");
|
|
3992
|
+
console.log(" npx moltspay faucet --chain bnb_testnet\n");
|
|
3993
|
+
console.log(" This gives you:\n");
|
|
3994
|
+
console.log(" \u2022 1 USDC (testnet) for payments");
|
|
3995
|
+
console.log(" \u2022 0.001 tBNB for gas (first approval tx)\n");
|
|
3996
|
+
return;
|
|
3997
|
+
}
|
|
3998
|
+
if (chain === "bnb") {
|
|
3999
|
+
console.log("\n\u{1F4CB} BNB Chain Funding\n");
|
|
4000
|
+
console.log(` Wallet: ${walletAddress}
|
|
4001
|
+
`);
|
|
4002
|
+
console.log(" To use MoltsPay on BNB Chain, you need:\n");
|
|
4003
|
+
console.log(" 1. USDC for payments");
|
|
4004
|
+
console.log(" \u2192 Withdraw from Binance/exchange to your wallet address\n");
|
|
4005
|
+
console.log(" 2. Small amount of BNB for gas (~0.001 BNB / ~$0.60)");
|
|
4006
|
+
console.log(" \u2192 First approval transaction requires gas");
|
|
4007
|
+
console.log(" \u2192 After approval, all payments are gasless\n");
|
|
4008
|
+
console.log(" \u{1F4A1} Tip: Most exchanges include BNB dust when you withdraw to BNB Chain\n");
|
|
4009
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
4010
|
+
console.log(" After funding, check status: npx moltspay status\n");
|
|
4011
|
+
return;
|
|
4012
|
+
}
|
|
2492
4013
|
console.log("\n\u{1F4B3} Fund your agent wallet\n");
|
|
2493
|
-
console.log(` Wallet: ${
|
|
2494
|
-
console.log(` Chain: ${chain}`);
|
|
4014
|
+
console.log(` Wallet: ${walletAddress}`);
|
|
4015
|
+
console.log(` Chain: ${chain === "solana" ? "Solana" : chain}`);
|
|
2495
4016
|
console.log(` Amount: $${amount.toFixed(2)}
|
|
2496
4017
|
`);
|
|
2497
4018
|
try {
|
|
@@ -2500,7 +4021,7 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
|
|
|
2500
4021
|
method: "POST",
|
|
2501
4022
|
headers: { "Content-Type": "application/json" },
|
|
2502
4023
|
body: JSON.stringify({
|
|
2503
|
-
address:
|
|
4024
|
+
address: walletAddress,
|
|
2504
4025
|
amount,
|
|
2505
4026
|
chain
|
|
2506
4027
|
})
|
|
@@ -2518,11 +4039,92 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
|
|
|
2518
4039
|
console.log(`\u274C ${error.message}`);
|
|
2519
4040
|
}
|
|
2520
4041
|
});
|
|
2521
|
-
program.command("
|
|
4042
|
+
program.command("approve").description("Approve a spender address for BNB chain payments").requiredOption("--spender <address>", "Spender address to approve (from server 402 response)").option("--chain <chain>", "BNB chain (bnb or bnb_testnet)", "bnb_testnet").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (options) => {
|
|
4043
|
+
const chain = options.chain;
|
|
4044
|
+
if (chain !== "bnb" && chain !== "bnb_testnet") {
|
|
4045
|
+
console.log("\u274C approve command is only for BNB chains (bnb or bnb_testnet)");
|
|
4046
|
+
return;
|
|
4047
|
+
}
|
|
4048
|
+
if (!options.spender.match(/^0x[a-fA-F0-9]{40}$/)) {
|
|
4049
|
+
console.log("\u274C Invalid spender address format");
|
|
4050
|
+
return;
|
|
4051
|
+
}
|
|
4052
|
+
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
4053
|
+
if (!client.isInitialized) {
|
|
4054
|
+
console.log("\u274C Wallet not initialized. Run: npx moltspay init --chain " + chain);
|
|
4055
|
+
return;
|
|
4056
|
+
}
|
|
4057
|
+
console.log(`
|
|
4058
|
+
\u{1F510} Approving spender for ${chain}...
|
|
4059
|
+
`);
|
|
4060
|
+
await setupBNBApprovals(client, chain, options.spender, false);
|
|
4061
|
+
const walletPath = (0, import_path3.join)(options.configDir || DEFAULT_CONFIG_DIR2, "wallet.json");
|
|
4062
|
+
try {
|
|
4063
|
+
const walletData = JSON.parse((0, import_fs5.readFileSync)(walletPath, "utf-8"));
|
|
4064
|
+
walletData.approvals = walletData.approvals || {};
|
|
4065
|
+
walletData.approvals[chain] = options.spender;
|
|
4066
|
+
(0, import_fs5.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2));
|
|
4067
|
+
console.log(`\u2705 Approval complete! Spender saved for ${chain}.
|
|
4068
|
+
`);
|
|
4069
|
+
} catch (err) {
|
|
4070
|
+
console.log("\u2705 Approval complete!\n");
|
|
4071
|
+
console.log("\u26A0\uFE0F Could not save spender to wallet config");
|
|
4072
|
+
}
|
|
4073
|
+
});
|
|
4074
|
+
program.command("faucet").description("Request testnet tokens from faucet (Base Sepolia, Tempo Moderato, BNB Testnet, or Solana Devnet)").option("--chain <chain>", "Chain to get tokens on (base_sepolia, tempo_moderato, bnb_testnet, or solana_devnet)", "base_sepolia").option("--address <address>", "Wallet address (defaults to your wallet)").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (options) => {
|
|
2522
4075
|
let address = options.address;
|
|
2523
4076
|
const chain = options.chain?.toLowerCase() || "base_sepolia";
|
|
2524
|
-
if (!["base_sepolia", "tempo_moderato"].includes(chain)) {
|
|
2525
|
-
console.log("\u274C Invalid chain. Use: base_sepolia or
|
|
4077
|
+
if (!["base_sepolia", "tempo_moderato", "bnb_testnet", "solana_devnet"].includes(chain)) {
|
|
4078
|
+
console.log("\u274C Invalid chain. Use: base_sepolia, tempo_moderato, bnb_testnet, or solana_devnet");
|
|
4079
|
+
return;
|
|
4080
|
+
}
|
|
4081
|
+
if (chain === "solana_devnet") {
|
|
4082
|
+
if (!address) {
|
|
4083
|
+
address = getSolanaAddress(options.configDir);
|
|
4084
|
+
if (!address) {
|
|
4085
|
+
console.log("\u274C No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
|
|
4086
|
+
return;
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
if (!isValidSolanaAddress(address)) {
|
|
4090
|
+
console.log("\u274C Invalid Solana address");
|
|
4091
|
+
return;
|
|
4092
|
+
}
|
|
4093
|
+
console.log("\n\u{1F6B0} Solana Devnet Faucet (Gasless Mode)\n");
|
|
4094
|
+
console.log(` Address: ${address}
|
|
4095
|
+
`);
|
|
4096
|
+
let usdcSuccess = false;
|
|
4097
|
+
try {
|
|
4098
|
+
console.log(" \u23F3 Requesting 1 USDC from faucet...");
|
|
4099
|
+
const FAUCET_API = process.env.MOLTSPAY_FAUCET_API || "https://moltspay.com/api/v1/faucet";
|
|
4100
|
+
const response = await fetch(FAUCET_API, {
|
|
4101
|
+
method: "POST",
|
|
4102
|
+
headers: { "Content-Type": "application/json" },
|
|
4103
|
+
body: JSON.stringify({ address, chain: "solana_devnet" })
|
|
4104
|
+
});
|
|
4105
|
+
const result = await response.json();
|
|
4106
|
+
if (!response.ok) {
|
|
4107
|
+
console.log(` \u26A0\uFE0F USDC faucet: ${result.error || "Request failed"}`);
|
|
4108
|
+
if (result.hint) console.log(` ${result.hint}`);
|
|
4109
|
+
if (result.retry_after) console.log(` Retry after: ${result.retry_after}`);
|
|
4110
|
+
} else {
|
|
4111
|
+
console.log(` \u2705 Received ${result.amount} USDC!`);
|
|
4112
|
+
console.log(` Transaction: ${result.explorer}`);
|
|
4113
|
+
if (result.faucet_balance) {
|
|
4114
|
+
console.log(` Faucet balance: ${result.faucet_balance} USDC remaining`);
|
|
4115
|
+
}
|
|
4116
|
+
usdcSuccess = true;
|
|
4117
|
+
}
|
|
4118
|
+
} catch (error) {
|
|
4119
|
+
console.log(` \u26A0\uFE0F USDC faucet error: ${error.message}`);
|
|
4120
|
+
}
|
|
4121
|
+
console.log("");
|
|
4122
|
+
if (usdcSuccess) {
|
|
4123
|
+
console.log("\u{1F4A1} Check your balance:");
|
|
4124
|
+
console.log(" npx moltspay status\n");
|
|
4125
|
+
} else {
|
|
4126
|
+
console.log("\u274C Faucet request failed. Try again in a few minutes.\n");
|
|
4127
|
+
}
|
|
2526
4128
|
return;
|
|
2527
4129
|
}
|
|
2528
4130
|
if (!address) {
|
|
@@ -2570,6 +4172,46 @@ program.command("faucet").description("Request testnet tokens from faucet (Base
|
|
|
2570
4172
|
console.log(`\u274C ${error.message}`);
|
|
2571
4173
|
console.log("\n Try Tempo Wallet instead: https://wallet.tempo.xyz\n");
|
|
2572
4174
|
}
|
|
4175
|
+
} else if (chain === "bnb_testnet") {
|
|
4176
|
+
console.log(` Requesting 1 USDC on BNB Testnet...`);
|
|
4177
|
+
console.log(` Address: ${address}
|
|
4178
|
+
`);
|
|
4179
|
+
try {
|
|
4180
|
+
const FAUCET_API = process.env.MOLTSPAY_FAUCET_API || "https://moltspay.com/api/v1/faucet";
|
|
4181
|
+
const response = await fetch(FAUCET_API, {
|
|
4182
|
+
method: "POST",
|
|
4183
|
+
headers: { "Content-Type": "application/json" },
|
|
4184
|
+
body: JSON.stringify({ address, chain: "bnb_testnet" })
|
|
4185
|
+
});
|
|
4186
|
+
const result = await response.json();
|
|
4187
|
+
if (!response.ok) {
|
|
4188
|
+
console.log(`\u274C ${result.error || "Request failed"}`);
|
|
4189
|
+
if (result.hint) console.log(` ${result.hint}`);
|
|
4190
|
+
if (result.retry_after) console.log(` Retry after: ${result.retry_after}`);
|
|
4191
|
+
console.log("\n\u{1F4A1} Alternatively, get tokens manually:");
|
|
4192
|
+
console.log(` 1. Get test BNB: https://www.bnbchain.org/en/testnet-faucet`);
|
|
4193
|
+
console.log(` 2. Select "Peggy Tokens" -> USDC`);
|
|
4194
|
+
console.log(` 3. Enter: ${address}
|
|
4195
|
+
`);
|
|
4196
|
+
return;
|
|
4197
|
+
}
|
|
4198
|
+
console.log(`\u2705 Received ${result.amount} ${result.token || "USDC"} on ${result.chain_name || "BNB Testnet"}!
|
|
4199
|
+
`);
|
|
4200
|
+
console.log(` Transaction: ${result.explorer || `https://testnet.bscscan.com/tx/${result.transaction}`}`);
|
|
4201
|
+
if (result.faucet_balance) {
|
|
4202
|
+
console.log(` Faucet balance: ${result.faucet_balance} USDC`);
|
|
4203
|
+
}
|
|
4204
|
+
console.log("\n\u{1F4A1} Now you can test BNB payments:");
|
|
4205
|
+
console.log(` npx moltspay pay <service-url> <service-id> --chain bnb_testnet
|
|
4206
|
+
`);
|
|
4207
|
+
} catch (error) {
|
|
4208
|
+
console.log(`\u274C ${error.message}`);
|
|
4209
|
+
console.log("\n\u{1F4A1} Get tokens manually:");
|
|
4210
|
+
console.log(` 1. Get test BNB: https://www.bnbchain.org/en/testnet-faucet`);
|
|
4211
|
+
console.log(` 2. Select "Peggy Tokens" -> USDC`);
|
|
4212
|
+
console.log(` 3. Enter: ${address}
|
|
4213
|
+
`);
|
|
4214
|
+
}
|
|
2573
4215
|
} else {
|
|
2574
4216
|
console.log(` Requesting 1 USDC on Base Sepolia...`);
|
|
2575
4217
|
console.log(` Address: ${address}
|
|
@@ -2579,7 +4221,7 @@ program.command("faucet").description("Request testnet tokens from faucet (Base
|
|
|
2579
4221
|
const response = await fetch(FAUCET_API, {
|
|
2580
4222
|
method: "POST",
|
|
2581
4223
|
headers: { "Content-Type": "application/json" },
|
|
2582
|
-
body: JSON.stringify({ address })
|
|
4224
|
+
body: JSON.stringify({ address, chain: "base_sepolia" })
|
|
2583
4225
|
});
|
|
2584
4226
|
const result = await response.json();
|
|
2585
4227
|
if (!response.ok) {
|
|
@@ -2602,7 +4244,7 @@ program.command("faucet").description("Request testnet tokens from faucet (Base
|
|
|
2602
4244
|
}
|
|
2603
4245
|
}
|
|
2604
4246
|
});
|
|
2605
|
-
program.command("status").description("Show wallet status and balance").option("--config-dir <dir>", "Config directory",
|
|
4247
|
+
program.command("status").description("Show wallet status and balance").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).option("--json", "Output as JSON").action(async (options) => {
|
|
2606
4248
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
2607
4249
|
if (!client.isInitialized) {
|
|
2608
4250
|
if (options.json) {
|
|
@@ -2619,12 +4261,31 @@ program.command("status").description("Show wallet status and balance").option("
|
|
|
2619
4261
|
} catch (err) {
|
|
2620
4262
|
console.error("Warning: Could not fetch balances:", err.message);
|
|
2621
4263
|
}
|
|
4264
|
+
const solanaAddress = getSolanaAddress(options.configDir);
|
|
4265
|
+
let solanaBalances = {};
|
|
4266
|
+
if (solanaAddress) {
|
|
4267
|
+
try {
|
|
4268
|
+
solanaBalances.devnet = await getSolanaBalances(solanaAddress, "solana_devnet");
|
|
4269
|
+
} catch {
|
|
4270
|
+
}
|
|
4271
|
+
try {
|
|
4272
|
+
solanaBalances.mainnet = await getSolanaBalances(solanaAddress, "solana");
|
|
4273
|
+
} catch {
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
2622
4276
|
if (options.json) {
|
|
2623
|
-
|
|
4277
|
+
const output = {
|
|
2624
4278
|
address: client.address,
|
|
2625
4279
|
balances: allBalances,
|
|
2626
4280
|
limits: config.limits
|
|
2627
|
-
}
|
|
4281
|
+
};
|
|
4282
|
+
if (solanaAddress) {
|
|
4283
|
+
output.solana = {
|
|
4284
|
+
address: solanaAddress,
|
|
4285
|
+
balances: solanaBalances
|
|
4286
|
+
};
|
|
4287
|
+
}
|
|
4288
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2628
4289
|
} else {
|
|
2629
4290
|
console.log("\n\u{1F4CA} MoltsPay Wallet Status\n");
|
|
2630
4291
|
console.log(` Address: ${client.address}`);
|
|
@@ -2648,18 +4309,90 @@ program.command("status").description("Show wallet status and balance").option("
|
|
|
2648
4309
|
console.log(` alphaUSD: ${tempo.alphaUSD.toFixed(2)}`);
|
|
2649
4310
|
console.log(` betaUSD: ${tempo.betaUSD.toFixed(2)}`);
|
|
2650
4311
|
console.log(` thetaUSD: ${tempo.thetaUSD.toFixed(2)}`);
|
|
4312
|
+
} else if (chainName === "bnb" || chainName === "bnb_testnet") {
|
|
4313
|
+
const bnbBalance = balance.native;
|
|
4314
|
+
const bnbWarning = bnbBalance < 5e-4 ? " \u26A0\uFE0F Low gas" : "";
|
|
4315
|
+
console.log(` ${chainLabel.padEnd(14)} ${balance.usdc.toFixed(2)} USDC | ${balance.usdt.toFixed(2)} USDT | ${bnbBalance.toFixed(4)} BNB${bnbWarning}`);
|
|
2651
4316
|
} else {
|
|
2652
4317
|
console.log(` ${chainLabel.padEnd(14)} ${balance.usdc.toFixed(2)} USDC | ${balance.usdt.toFixed(2)} USDT`);
|
|
2653
4318
|
}
|
|
2654
4319
|
}
|
|
4320
|
+
const address = client.address;
|
|
4321
|
+
let bnbApprovalStatus = null;
|
|
4322
|
+
let bnbTestnetApprovalStatus = null;
|
|
4323
|
+
try {
|
|
4324
|
+
if (allBalances["bnb"]) {
|
|
4325
|
+
bnbApprovalStatus = await checkBNBApprovals(address, "bnb", options.configDir);
|
|
4326
|
+
}
|
|
4327
|
+
if (allBalances["bnb_testnet"]) {
|
|
4328
|
+
bnbTestnetApprovalStatus = await checkBNBApprovals(address, "bnb_testnet", options.configDir);
|
|
4329
|
+
}
|
|
4330
|
+
} catch {
|
|
4331
|
+
}
|
|
4332
|
+
if (bnbApprovalStatus || bnbTestnetApprovalStatus) {
|
|
4333
|
+
console.log("");
|
|
4334
|
+
console.log(" BNB Approvals (pay-for-success):");
|
|
4335
|
+
if (bnbApprovalStatus) {
|
|
4336
|
+
if (!bnbApprovalStatus.spender) {
|
|
4337
|
+
console.log(" BNB: \u26A0\uFE0F No spender configured");
|
|
4338
|
+
console.log(" \u2514\u2500 Run a payment first, or: npx moltspay approve --chain bnb --spender <address>");
|
|
4339
|
+
} else {
|
|
4340
|
+
const status = bnbApprovalStatus.usdt && bnbApprovalStatus.usdc ? "\u2705" : "\u26A0\uFE0F";
|
|
4341
|
+
const tokens = [
|
|
4342
|
+
bnbApprovalStatus.usdt ? "USDT\u2713" : "USDT\u2717",
|
|
4343
|
+
bnbApprovalStatus.usdc ? "USDC\u2713" : "USDC\u2717"
|
|
4344
|
+
].join(", ");
|
|
4345
|
+
console.log(` BNB: ${status} ${tokens}`);
|
|
4346
|
+
const bnbNative = allBalances["bnb"]?.native || 0;
|
|
4347
|
+
if (!bnbApprovalStatus.usdc && !bnbApprovalStatus.usdt && bnbNative < 5e-4) {
|
|
4348
|
+
console.log(" \u26A0\uFE0F Need ~0.001 BNB for first approval tx. Get from exchange.");
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4352
|
+
if (bnbTestnetApprovalStatus) {
|
|
4353
|
+
if (!bnbTestnetApprovalStatus.spender) {
|
|
4354
|
+
console.log(" BNB Testnet: \u26A0\uFE0F No spender configured");
|
|
4355
|
+
console.log(" \u2514\u2500 Run a payment first, or: npx moltspay approve --chain bnb_testnet --spender <address>");
|
|
4356
|
+
} else {
|
|
4357
|
+
const status = bnbTestnetApprovalStatus.usdt && bnbTestnetApprovalStatus.usdc ? "\u2705" : "\u26A0\uFE0F";
|
|
4358
|
+
const tokens = [
|
|
4359
|
+
bnbTestnetApprovalStatus.usdt ? "USDT\u2713" : "USDT\u2717",
|
|
4360
|
+
bnbTestnetApprovalStatus.usdc ? "USDC\u2713" : "USDC\u2717"
|
|
4361
|
+
].join(", ");
|
|
4362
|
+
console.log(` BNB Testnet: ${status} ${tokens}`);
|
|
4363
|
+
const tbnbNative = allBalances["bnb_testnet"]?.native || 0;
|
|
4364
|
+
if (!bnbTestnetApprovalStatus.usdc && !bnbTestnetApprovalStatus.usdt && tbnbNative < 5e-4) {
|
|
4365
|
+
console.log(" \u26A0\uFE0F Need tBNB for approval. Run: npx moltspay faucet --chain bnb_testnet");
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
}
|
|
2655
4370
|
console.log("");
|
|
2656
4371
|
console.log(" Spending Limits:");
|
|
2657
4372
|
console.log(` Per Transaction: $${config.limits.maxPerTx}`);
|
|
2658
4373
|
console.log(` Daily: $${config.limits.maxPerDay}`);
|
|
4374
|
+
const solanaAddress2 = getSolanaAddress(options.configDir);
|
|
4375
|
+
if (solanaAddress2) {
|
|
4376
|
+
console.log("");
|
|
4377
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
4378
|
+
console.log(` \u{1F7E3} Solana: ${solanaAddress2}`);
|
|
4379
|
+
try {
|
|
4380
|
+
const devnetBalances = await getSolanaBalances(solanaAddress2, "solana_devnet");
|
|
4381
|
+
console.log(` Devnet: ${devnetBalances.sol.toFixed(4)} SOL | ${devnetBalances.usdc.toFixed(2)} USDC`);
|
|
4382
|
+
} catch (err) {
|
|
4383
|
+
console.log(` Devnet: (unable to fetch)`);
|
|
4384
|
+
}
|
|
4385
|
+
try {
|
|
4386
|
+
const mainnetBalances = await getSolanaBalances(solanaAddress2, "solana");
|
|
4387
|
+
console.log(` Mainnet: ${mainnetBalances.sol.toFixed(4)} SOL | ${mainnetBalances.usdc.toFixed(2)} USDC`);
|
|
4388
|
+
} catch (err) {
|
|
4389
|
+
console.log(` Mainnet: (unable to fetch)`);
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
2659
4392
|
console.log("");
|
|
2660
4393
|
}
|
|
2661
4394
|
});
|
|
2662
|
-
program.command("list").description("List recent transactions").option("--days <n>", "Number of days to look back", "7").option("--chain <chain>", "Chain to query (base, polygon, base_sepolia, or all)", "all").option("--limit <n>", "Max transactions to show", "20").option("--config-dir <dir>", "Config directory",
|
|
4395
|
+
program.command("list").description("List recent transactions").option("--days <n>", "Number of days to look back", "7").option("--chain <chain>", "Chain to query (base, polygon, base_sepolia, or all)", "all").option("--limit <n>", "Max transactions to show", "20").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (options) => {
|
|
2663
4396
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
2664
4397
|
if (!client.isInitialized) {
|
|
2665
4398
|
console.log("\u274C Not initialized. Run: npx moltspay init");
|
|
@@ -2962,18 +4695,18 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
2962
4695
|
const handlers = /* @__PURE__ */ new Map();
|
|
2963
4696
|
let provider = null;
|
|
2964
4697
|
for (const inputPath of allPaths) {
|
|
2965
|
-
const resolvedPath = (0,
|
|
4698
|
+
const resolvedPath = (0, import_path3.resolve)(inputPath);
|
|
2966
4699
|
let manifestPath;
|
|
2967
4700
|
let skillDir;
|
|
2968
4701
|
let isSkillDir = false;
|
|
2969
|
-
if ((0,
|
|
2970
|
-
manifestPath = (0,
|
|
4702
|
+
if ((0, import_fs5.existsSync)((0, import_path3.join)(resolvedPath, "moltspay.services.json"))) {
|
|
4703
|
+
manifestPath = (0, import_path3.join)(resolvedPath, "moltspay.services.json");
|
|
2971
4704
|
skillDir = resolvedPath;
|
|
2972
4705
|
isSkillDir = true;
|
|
2973
|
-
} else if ((0,
|
|
4706
|
+
} else if ((0, import_fs5.existsSync)(resolvedPath) && resolvedPath.endsWith(".json")) {
|
|
2974
4707
|
manifestPath = resolvedPath;
|
|
2975
|
-
skillDir = (0,
|
|
2976
|
-
} else if ((0,
|
|
4708
|
+
skillDir = (0, import_path3.dirname)(resolvedPath);
|
|
4709
|
+
} else if ((0, import_fs5.existsSync)(resolvedPath)) {
|
|
2977
4710
|
console.error(`\u274C No moltspay.services.json found in: ${resolvedPath}`);
|
|
2978
4711
|
continue;
|
|
2979
4712
|
} else {
|
|
@@ -2982,25 +4715,25 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
2982
4715
|
}
|
|
2983
4716
|
console.log(`\u{1F4E6} Loading: ${manifestPath}`);
|
|
2984
4717
|
try {
|
|
2985
|
-
const manifestContent = JSON.parse((0,
|
|
4718
|
+
const manifestContent = JSON.parse((0, import_fs5.readFileSync)(manifestPath, "utf-8"));
|
|
2986
4719
|
if (!provider) {
|
|
2987
4720
|
provider = manifestContent.provider;
|
|
2988
4721
|
}
|
|
2989
4722
|
let skillModule = null;
|
|
2990
4723
|
if (isSkillDir) {
|
|
2991
4724
|
let entryPoint = "index.js";
|
|
2992
|
-
const pkgJsonPath = (0,
|
|
2993
|
-
if ((0,
|
|
4725
|
+
const pkgJsonPath = (0, import_path3.join)(skillDir, "package.json");
|
|
4726
|
+
if ((0, import_fs5.existsSync)(pkgJsonPath)) {
|
|
2994
4727
|
try {
|
|
2995
|
-
const pkgJson = JSON.parse((0,
|
|
4728
|
+
const pkgJson = JSON.parse((0, import_fs5.readFileSync)(pkgJsonPath, "utf-8"));
|
|
2996
4729
|
if (pkgJson.main) {
|
|
2997
4730
|
entryPoint = pkgJson.main;
|
|
2998
4731
|
}
|
|
2999
4732
|
} catch {
|
|
3000
4733
|
}
|
|
3001
4734
|
}
|
|
3002
|
-
const modulePath = (0,
|
|
3003
|
-
if ((0,
|
|
4735
|
+
const modulePath = (0, import_path3.join)(skillDir, entryPoint);
|
|
4736
|
+
if ((0, import_fs5.existsSync)(modulePath)) {
|
|
3004
4737
|
try {
|
|
3005
4738
|
skillModule = await import(modulePath);
|
|
3006
4739
|
console.log(` \u2705 Loaded module: ${modulePath}`);
|
|
@@ -3078,8 +4811,8 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
3078
4811
|
provider,
|
|
3079
4812
|
services: allServices
|
|
3080
4813
|
};
|
|
3081
|
-
const tempManifestPath = (0,
|
|
3082
|
-
(0,
|
|
4814
|
+
const tempManifestPath = (0, import_path3.join)(DEFAULT_CONFIG_DIR2, "combined-manifest.json");
|
|
4815
|
+
(0, import_fs5.writeFileSync)(tempManifestPath, JSON.stringify(combinedManifest, null, 2));
|
|
3083
4816
|
console.log(`
|
|
3084
4817
|
\u{1F4CB} Combined manifest: ${allServices.length} services`);
|
|
3085
4818
|
console.log(` Provider: ${provider.name}`);
|
|
@@ -3092,12 +4825,12 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
3092
4825
|
server.skill(serviceId, handler);
|
|
3093
4826
|
}
|
|
3094
4827
|
const pidData = { pid: process.pid, port, paths: allPaths };
|
|
3095
|
-
(0,
|
|
4828
|
+
(0, import_fs5.writeFileSync)(PID_FILE, JSON.stringify(pidData, null, 2));
|
|
3096
4829
|
server.listen(port);
|
|
3097
4830
|
const cleanup = () => {
|
|
3098
4831
|
try {
|
|
3099
|
-
if ((0,
|
|
3100
|
-
if ((0,
|
|
4832
|
+
if ((0, import_fs5.existsSync)(PID_FILE)) (0, import_fs5.unlinkSync)(PID_FILE);
|
|
4833
|
+
if ((0, import_fs5.existsSync)(tempManifestPath)) (0, import_fs5.unlinkSync)(tempManifestPath);
|
|
3101
4834
|
} catch {
|
|
3102
4835
|
}
|
|
3103
4836
|
};
|
|
@@ -3118,12 +4851,12 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
3118
4851
|
}
|
|
3119
4852
|
});
|
|
3120
4853
|
program.command("stop").description("Stop the running MoltsPay server").action(async () => {
|
|
3121
|
-
if (!(0,
|
|
4854
|
+
if (!(0, import_fs5.existsSync)(PID_FILE)) {
|
|
3122
4855
|
console.log("\u274C No running server found (no PID file)");
|
|
3123
4856
|
process.exit(1);
|
|
3124
4857
|
}
|
|
3125
4858
|
try {
|
|
3126
|
-
const pidData = JSON.parse((0,
|
|
4859
|
+
const pidData = JSON.parse((0, import_fs5.readFileSync)(PID_FILE, "utf-8"));
|
|
3127
4860
|
const { pid, port, manifest } = pidData;
|
|
3128
4861
|
console.log(`
|
|
3129
4862
|
\u{1F6D1} Stopping MoltsPay Server
|
|
@@ -3136,7 +4869,7 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
|
|
|
3136
4869
|
process.kill(pid, 0);
|
|
3137
4870
|
} catch {
|
|
3138
4871
|
console.log("\u26A0\uFE0F Process not running, cleaning up PID file...");
|
|
3139
|
-
(0,
|
|
4872
|
+
(0, import_fs5.unlinkSync)(PID_FILE);
|
|
3140
4873
|
process.exit(0);
|
|
3141
4874
|
}
|
|
3142
4875
|
process.kill(pid, "SIGTERM");
|
|
@@ -3148,8 +4881,8 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
|
|
|
3148
4881
|
process.kill(pid, "SIGKILL");
|
|
3149
4882
|
} catch {
|
|
3150
4883
|
}
|
|
3151
|
-
if ((0,
|
|
3152
|
-
(0,
|
|
4884
|
+
if ((0, import_fs5.existsSync)(PID_FILE)) {
|
|
4885
|
+
(0, import_fs5.unlinkSync)(PID_FILE);
|
|
3153
4886
|
}
|
|
3154
4887
|
console.log("\u2705 Server stopped\n");
|
|
3155
4888
|
} catch (err) {
|
|
@@ -3157,14 +4890,23 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
|
|
|
3157
4890
|
process.exit(1);
|
|
3158
4891
|
}
|
|
3159
4892
|
});
|
|
3160
|
-
program.command("pay <server> <service> [params]").description("Pay for a service and get the result").option("--prompt <text>", "Prompt for the service").option("--image <path>", "Image URL or local file path").option("--token <token>", "Token to pay with (USDC or USDT)", "USDC").option("--chain <chain>", "Chain to pay on (base, polygon, base_sepolia, or
|
|
4893
|
+
program.command("pay <server> <service> [params]").description("Pay for a service and get the result").option("--prompt <text>", "Prompt for the service").option("--image <path>", "Image URL or local file path").option("--data <json>", "Raw JSON data to send (for custom input formats)").option("--token <token>", "Token to pay with (USDC or USDT)", "USDC").option("--chain <chain>", "Chain to pay on (base, polygon, base_sepolia, tempo_moderato, solana, or solana_devnet).").option("--config-dir <dir>", "Config directory with wallet.json", DEFAULT_CONFIG_DIR2).option("--json", "Output raw JSON only").action(async (server, service, paramsJson, options) => {
|
|
3161
4894
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
3162
4895
|
if (!client.isInitialized) {
|
|
3163
4896
|
console.error("\u274C Wallet not initialized. Run: npx moltspay init");
|
|
3164
4897
|
process.exit(1);
|
|
3165
4898
|
}
|
|
3166
4899
|
let params = {};
|
|
3167
|
-
|
|
4900
|
+
let useRawData = false;
|
|
4901
|
+
if (options.data) {
|
|
4902
|
+
try {
|
|
4903
|
+
params = JSON.parse(options.data);
|
|
4904
|
+
useRawData = true;
|
|
4905
|
+
} catch {
|
|
4906
|
+
console.error("\u274C Invalid JSON in --data flag");
|
|
4907
|
+
process.exit(1);
|
|
4908
|
+
}
|
|
4909
|
+
} else if (paramsJson) {
|
|
3168
4910
|
try {
|
|
3169
4911
|
params = JSON.parse(paramsJson);
|
|
3170
4912
|
} catch {
|
|
@@ -3172,24 +4914,25 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
3172
4914
|
process.exit(1);
|
|
3173
4915
|
}
|
|
3174
4916
|
}
|
|
3175
|
-
if (options.prompt) params.prompt = options.prompt;
|
|
4917
|
+
if (!useRawData && options.prompt) params.prompt = options.prompt;
|
|
3176
4918
|
if (options.image) {
|
|
3177
4919
|
const imagePath = options.image;
|
|
3178
4920
|
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
|
3179
4921
|
params.image_url = imagePath;
|
|
3180
4922
|
} else {
|
|
3181
|
-
const filePath = (0,
|
|
3182
|
-
if (!(0,
|
|
4923
|
+
const filePath = (0, import_path3.resolve)(imagePath);
|
|
4924
|
+
if (!(0, import_fs5.existsSync)(filePath)) {
|
|
3183
4925
|
console.error(`\u274C Image file not found: ${filePath}`);
|
|
3184
4926
|
process.exit(1);
|
|
3185
4927
|
}
|
|
3186
|
-
const imageData = (0,
|
|
4928
|
+
const imageData = (0, import_fs5.readFileSync)(filePath);
|
|
3187
4929
|
params.image_base64 = imageData.toString("base64");
|
|
3188
4930
|
}
|
|
3189
4931
|
}
|
|
4932
|
+
const supportedPayChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
|
|
3190
4933
|
const chain = options.chain?.toLowerCase();
|
|
3191
|
-
if (chain && !
|
|
3192
|
-
console.error(`\u274C Unknown chain: ${chain}. Supported:
|
|
4934
|
+
if (chain && !supportedPayChains.includes(chain)) {
|
|
4935
|
+
console.error(`\u274C Unknown chain: ${chain}. Supported: ${supportedPayChains.join(", ")}`);
|
|
3193
4936
|
process.exit(1);
|
|
3194
4937
|
}
|
|
3195
4938
|
const imageDisplay = params.image_url || (params.image_base64 ? `[local file: ${options.image}]` : null);
|
|
@@ -3212,7 +4955,11 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
3212
4955
|
`);
|
|
3213
4956
|
console.log(` Server: ${server}`);
|
|
3214
4957
|
console.log(` Service: ${service}`);
|
|
3215
|
-
|
|
4958
|
+
if (useRawData) {
|
|
4959
|
+
console.log(` Data: ${JSON.stringify(params).slice(0, 50)}${JSON.stringify(params).length > 50 ? "..." : ""}`);
|
|
4960
|
+
} else {
|
|
4961
|
+
console.log(` Prompt: ${params.prompt}`);
|
|
4962
|
+
}
|
|
3216
4963
|
if (imageDisplay) console.log(` Image: ${imageDisplay}`);
|
|
3217
4964
|
console.log(` Chain: ${chain || "(auto)"}`);
|
|
3218
4965
|
console.log(` Token: ${token}`);
|
|
@@ -3220,22 +4967,11 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
3220
4967
|
console.log("");
|
|
3221
4968
|
}
|
|
3222
4969
|
try {
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
}
|
|
3229
|
-
const mppUrl = server.includes(service) ? server : `${server}/${service}`;
|
|
3230
|
-
result = await client.payWithMPP(mppUrl, {
|
|
3231
|
-
body: params
|
|
3232
|
-
});
|
|
3233
|
-
} else {
|
|
3234
|
-
result = await client.pay(server, service, params, {
|
|
3235
|
-
token,
|
|
3236
|
-
chain
|
|
3237
|
-
});
|
|
3238
|
-
}
|
|
4970
|
+
const result = await client.pay(server, service, params, {
|
|
4971
|
+
token,
|
|
4972
|
+
chain,
|
|
4973
|
+
rawData: useRawData
|
|
4974
|
+
});
|
|
3239
4975
|
if (options.json) {
|
|
3240
4976
|
console.log(JSON.stringify(result));
|
|
3241
4977
|
} else {
|
|
@@ -3253,11 +4989,11 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
3253
4989
|
}
|
|
3254
4990
|
});
|
|
3255
4991
|
program.command("validate <path>").description("Validate a moltspay.services.json file against the schema").action(async (inputPath) => {
|
|
3256
|
-
const resolvedPath = (0,
|
|
4992
|
+
const resolvedPath = (0, import_path3.resolve)(inputPath);
|
|
3257
4993
|
let manifestPath;
|
|
3258
|
-
if ((0,
|
|
3259
|
-
manifestPath = (0,
|
|
3260
|
-
} else if (resolvedPath.endsWith(".json") && (0,
|
|
4994
|
+
if ((0, import_fs5.existsSync)((0, import_path3.join)(resolvedPath, "moltspay.services.json"))) {
|
|
4995
|
+
manifestPath = (0, import_path3.join)(resolvedPath, "moltspay.services.json");
|
|
4996
|
+
} else if (resolvedPath.endsWith(".json") && (0, import_fs5.existsSync)(resolvedPath)) {
|
|
3261
4997
|
manifestPath = resolvedPath;
|
|
3262
4998
|
} else {
|
|
3263
4999
|
console.error(`\u274C Not found: ${resolvedPath}`);
|
|
@@ -3267,7 +5003,7 @@ program.command("validate <path>").description("Validate a moltspay.services.jso
|
|
|
3267
5003
|
\u{1F4CB} Validating: ${manifestPath}
|
|
3268
5004
|
`);
|
|
3269
5005
|
try {
|
|
3270
|
-
const content = JSON.parse((0,
|
|
5006
|
+
const content = JSON.parse((0, import_fs5.readFileSync)(manifestPath, "utf-8"));
|
|
3271
5007
|
const errors = [];
|
|
3272
5008
|
if (!content.provider) {
|
|
3273
5009
|
errors.push("Missing required field: provider");
|