moltspay 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -38
- 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 +1975 -273
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1977 -265
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +36 -3
- package/dist/client/index.d.ts +36 -3
- package/dist/client/index.js +540 -32
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +548 -30
- 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 +1413 -146
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1421 -144
- 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 +905 -52
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +915 -52
- 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 +4 -1
- 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
|
*/
|
|
@@ -278,9 +698,14 @@ var MoltsPayClient = class {
|
|
|
278
698
|
}
|
|
279
699
|
throw new Error(data.error || "Unexpected response");
|
|
280
700
|
}
|
|
701
|
+
const wwwAuthHeader = initialRes.headers.get("www-authenticate");
|
|
281
702
|
const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
703
|
+
if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
|
|
704
|
+
console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
|
|
705
|
+
return await this.handleMPPPayment(serverUrl, service, params, wwwAuthHeader);
|
|
706
|
+
}
|
|
282
707
|
if (!paymentRequiredHeader) {
|
|
283
|
-
throw new Error("Missing x-payment-required
|
|
708
|
+
throw new Error("Missing payment header (x-payment-required or www-authenticate)");
|
|
284
709
|
}
|
|
285
710
|
let requirements;
|
|
286
711
|
try {
|
|
@@ -297,17 +722,22 @@ var MoltsPayClient = class {
|
|
|
297
722
|
throw new Error("Invalid x-payment-required header");
|
|
298
723
|
}
|
|
299
724
|
const networkToChainName = (network2) => {
|
|
725
|
+
if (network2 === "solana:mainnet") return "solana";
|
|
726
|
+
if (network2 === "solana:devnet") return "solana_devnet";
|
|
300
727
|
const match = network2.match(/^eip155:(\d+)$/);
|
|
301
728
|
if (!match) return null;
|
|
302
729
|
const chainId = parseInt(match[1]);
|
|
303
730
|
if (chainId === 8453) return "base";
|
|
304
731
|
if (chainId === 137) return "polygon";
|
|
305
732
|
if (chainId === 84532) return "base_sepolia";
|
|
733
|
+
if (chainId === 42431) return "tempo_moderato";
|
|
734
|
+
if (chainId === 56) return "bnb";
|
|
735
|
+
if (chainId === 97) return "bnb_testnet";
|
|
306
736
|
return null;
|
|
307
737
|
};
|
|
308
738
|
const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
|
|
309
|
-
let chainName;
|
|
310
739
|
const userSpecifiedChain = options.chain;
|
|
740
|
+
let selectedChain;
|
|
311
741
|
if (userSpecifiedChain) {
|
|
312
742
|
if (!serverChains.includes(userSpecifiedChain)) {
|
|
313
743
|
throw new Error(
|
|
@@ -315,17 +745,27 @@ var MoltsPayClient = class {
|
|
|
315
745
|
Server accepts: ${serverChains.join(", ")}`
|
|
316
746
|
);
|
|
317
747
|
}
|
|
318
|
-
|
|
748
|
+
selectedChain = userSpecifiedChain;
|
|
319
749
|
} else {
|
|
320
750
|
if (serverChains.length === 1 && serverChains[0] === "base") {
|
|
321
|
-
|
|
751
|
+
selectedChain = "base";
|
|
322
752
|
} else {
|
|
323
753
|
throw new Error(
|
|
324
754
|
`Server accepts: ${serverChains.join(", ")}
|
|
325
|
-
Please specify: --chain
|
|
755
|
+
Please specify: --chain <chain_name>`
|
|
326
756
|
);
|
|
327
757
|
}
|
|
328
758
|
}
|
|
759
|
+
if (selectedChain === "solana" || selectedChain === "solana_devnet") {
|
|
760
|
+
const solanaChain = selectedChain;
|
|
761
|
+
const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
762
|
+
const req2 = requirements.find((r) => r.network === network2);
|
|
763
|
+
if (!req2) {
|
|
764
|
+
throw new Error(`Failed to find payment requirement for ${selectedChain}`);
|
|
765
|
+
}
|
|
766
|
+
return await this.handleSolanaPayment(serverUrl, service, params, req2, solanaChain);
|
|
767
|
+
}
|
|
768
|
+
const chainName = selectedChain;
|
|
329
769
|
const chain = getChain(chainName);
|
|
330
770
|
const network = `eip155:${chain.chainId}`;
|
|
331
771
|
const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
|
|
@@ -360,6 +800,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
360
800
|
} else {
|
|
361
801
|
console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
|
|
362
802
|
}
|
|
803
|
+
if (chainName === "bnb" || chainName === "bnb_testnet") {
|
|
804
|
+
console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
|
|
805
|
+
const payTo2 = req.payTo || req.resource;
|
|
806
|
+
if (!payTo2) {
|
|
807
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
808
|
+
}
|
|
809
|
+
const bnbSpender = req.extra?.bnbSpender;
|
|
810
|
+
if (!bnbSpender) {
|
|
811
|
+
throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
|
|
812
|
+
}
|
|
813
|
+
return await this.handleBNBPayment(serverUrl, service, params, {
|
|
814
|
+
to: payTo2,
|
|
815
|
+
amount,
|
|
816
|
+
token,
|
|
817
|
+
chainName,
|
|
818
|
+
chain,
|
|
819
|
+
spender: bnbSpender
|
|
820
|
+
});
|
|
821
|
+
}
|
|
363
822
|
const payTo = req.payTo || req.resource;
|
|
364
823
|
if (!payTo) {
|
|
365
824
|
throw new Error("Missing payTo address in payment requirements");
|
|
@@ -409,6 +868,300 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
409
868
|
console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
|
|
410
869
|
return result.result;
|
|
411
870
|
}
|
|
871
|
+
/**
|
|
872
|
+
* Handle MPP (Machine Payments Protocol) payment flow
|
|
873
|
+
* Called when pay() detects WWW-Authenticate header in 402 response
|
|
874
|
+
*/
|
|
875
|
+
async handleMPPPayment(serverUrl, service, params, wwwAuthHeader) {
|
|
876
|
+
const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
|
|
877
|
+
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
878
|
+
const { tempoModerato } = await import("viem/chains");
|
|
879
|
+
const { Actions } = await import("viem/tempo");
|
|
880
|
+
const privateKey = this.walletData.privateKey;
|
|
881
|
+
const account = privateKeyToAccount2(privateKey);
|
|
882
|
+
console.log(`[MoltsPay] Using MPP protocol on Tempo`);
|
|
883
|
+
console.log(`[MoltsPay] Account: ${account.address}`);
|
|
884
|
+
const parseAuthParam = (header, key) => {
|
|
885
|
+
const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
|
|
886
|
+
return match ? match[1] : null;
|
|
887
|
+
};
|
|
888
|
+
const challengeId = parseAuthParam(wwwAuthHeader, "id");
|
|
889
|
+
const method = parseAuthParam(wwwAuthHeader, "method");
|
|
890
|
+
const realm = parseAuthParam(wwwAuthHeader, "realm");
|
|
891
|
+
const requestB64 = parseAuthParam(wwwAuthHeader, "request");
|
|
892
|
+
if (method !== "tempo") {
|
|
893
|
+
throw new Error(`Unsupported payment method: ${method}`);
|
|
894
|
+
}
|
|
895
|
+
if (!requestB64) {
|
|
896
|
+
throw new Error("Missing request in WWW-Authenticate");
|
|
897
|
+
}
|
|
898
|
+
const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
|
|
899
|
+
const paymentRequest = JSON.parse(requestJson);
|
|
900
|
+
const { amount, currency, recipient, methodDetails } = paymentRequest;
|
|
901
|
+
const chainId = methodDetails?.chainId || 42431;
|
|
902
|
+
const amountDisplay = Number(amount) / 1e6;
|
|
903
|
+
console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
|
|
904
|
+
this.checkLimits(amountDisplay);
|
|
905
|
+
console.log(`[MoltsPay] Sending transaction on Tempo...`);
|
|
906
|
+
const tempoChain = { ...tempoModerato, feeToken: currency };
|
|
907
|
+
const publicClient = createPublicClient({
|
|
908
|
+
chain: tempoChain,
|
|
909
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
910
|
+
});
|
|
911
|
+
const walletClient = createWalletClient({
|
|
912
|
+
account,
|
|
913
|
+
chain: tempoChain,
|
|
914
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
915
|
+
});
|
|
916
|
+
const txHash = await Actions.token.transfer(walletClient, {
|
|
917
|
+
to: recipient,
|
|
918
|
+
amount: BigInt(amount),
|
|
919
|
+
token: currency
|
|
920
|
+
});
|
|
921
|
+
console.log(`[MoltsPay] Transaction: ${txHash}`);
|
|
922
|
+
await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
923
|
+
console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
|
|
924
|
+
const credential = {
|
|
925
|
+
challenge: {
|
|
926
|
+
id: challengeId,
|
|
927
|
+
realm,
|
|
928
|
+
method: "tempo",
|
|
929
|
+
intent: "charge",
|
|
930
|
+
request: paymentRequest
|
|
931
|
+
},
|
|
932
|
+
payload: { hash: txHash, type: "hash" },
|
|
933
|
+
source: `did:pkh:eip155:${chainId}:${account.address}`
|
|
934
|
+
};
|
|
935
|
+
const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
936
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
937
|
+
method: "POST",
|
|
938
|
+
headers: {
|
|
939
|
+
"Content-Type": "application/json",
|
|
940
|
+
"Authorization": `Payment ${credentialB64}`
|
|
941
|
+
},
|
|
942
|
+
body: JSON.stringify({ service, params, chain: "tempo_moderato" })
|
|
943
|
+
});
|
|
944
|
+
const result = await paidRes.json();
|
|
945
|
+
if (!paidRes.ok) {
|
|
946
|
+
throw new Error(result.error || "Payment verification failed");
|
|
947
|
+
}
|
|
948
|
+
this.recordSpending(amountDisplay);
|
|
949
|
+
console.log(`[MoltsPay] Success!`);
|
|
950
|
+
return result.result || result;
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Handle BNB Chain payment flow (pre-approval + intent signature)
|
|
954
|
+
*
|
|
955
|
+
* Flow:
|
|
956
|
+
* 1. Check client has approved server wallet (done via `moltspay init`)
|
|
957
|
+
* 2. Sign EIP-712 payment intent (no gas, just signature)
|
|
958
|
+
* 3. Send intent to server
|
|
959
|
+
* 4. Server executes service
|
|
960
|
+
* 5. Server calls transferFrom if successful (pay-for-success)
|
|
961
|
+
*/
|
|
962
|
+
async handleBNBPayment(serverUrl, service, params, paymentDetails) {
|
|
963
|
+
const { to, amount, token, chainName, chain, spender } = paymentDetails;
|
|
964
|
+
const tokenConfig = chain.tokens[token];
|
|
965
|
+
const provider = new import_ethers.ethers.JsonRpcProvider(chain.rpc);
|
|
966
|
+
const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
|
|
967
|
+
const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
|
|
968
|
+
if (allowance < amountWeiCheck) {
|
|
969
|
+
const nativeBalance = await provider.getBalance(this.wallet.address);
|
|
970
|
+
const minGasBalance = import_ethers.ethers.parseEther("0.0005");
|
|
971
|
+
if (nativeBalance < minGasBalance) {
|
|
972
|
+
const nativeBNB = parseFloat(import_ethers.ethers.formatEther(nativeBalance)).toFixed(4);
|
|
973
|
+
const isTestnet = chainName === "bnb_testnet";
|
|
974
|
+
if (isTestnet) {
|
|
975
|
+
throw new Error(
|
|
976
|
+
`\u274C Insufficient tBNB for approval transaction
|
|
977
|
+
|
|
978
|
+
Current tBNB: ${nativeBNB}
|
|
979
|
+
Required: ~0.001 tBNB
|
|
980
|
+
|
|
981
|
+
Get testnet tokens: npx moltspay faucet --chain bnb_testnet
|
|
982
|
+
(Gives USDC + tBNB for gas)`
|
|
983
|
+
);
|
|
984
|
+
} else {
|
|
985
|
+
throw new Error(
|
|
986
|
+
`\u274C Insufficient BNB for approval transaction
|
|
987
|
+
|
|
988
|
+
Current BNB: ${nativeBNB}
|
|
989
|
+
Required: ~0.001 BNB (~$0.60)
|
|
990
|
+
|
|
991
|
+
To get BNB:
|
|
992
|
+
\u2022 Withdraw from Binance/exchange to your wallet
|
|
993
|
+
\u2022 Most exchanges include BNB dust with withdrawals
|
|
994
|
+
|
|
995
|
+
After funding, run:
|
|
996
|
+
npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
`Insufficient allowance for ${spender.slice(0, 10)}...
|
|
1002
|
+
Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
|
|
1006
|
+
const intent = {
|
|
1007
|
+
from: this.wallet.address,
|
|
1008
|
+
to,
|
|
1009
|
+
amount: amountWei,
|
|
1010
|
+
token: tokenConfig.address,
|
|
1011
|
+
service,
|
|
1012
|
+
nonce: Date.now(),
|
|
1013
|
+
// Use timestamp as nonce for simplicity
|
|
1014
|
+
deadline: Date.now() + 36e5
|
|
1015
|
+
// 1 hour
|
|
1016
|
+
};
|
|
1017
|
+
const domain = {
|
|
1018
|
+
name: "MoltsPay",
|
|
1019
|
+
version: "1",
|
|
1020
|
+
chainId: chain.chainId
|
|
1021
|
+
};
|
|
1022
|
+
const types = {
|
|
1023
|
+
PaymentIntent: [
|
|
1024
|
+
{ name: "from", type: "address" },
|
|
1025
|
+
{ name: "to", type: "address" },
|
|
1026
|
+
{ name: "amount", type: "uint256" },
|
|
1027
|
+
{ name: "token", type: "address" },
|
|
1028
|
+
{ name: "service", type: "string" },
|
|
1029
|
+
{ name: "nonce", type: "uint256" },
|
|
1030
|
+
{ name: "deadline", type: "uint256" }
|
|
1031
|
+
]
|
|
1032
|
+
};
|
|
1033
|
+
console.log(`[MoltsPay] Signing BNB payment intent...`);
|
|
1034
|
+
const signature = await this.wallet.signTypedData(domain, types, intent);
|
|
1035
|
+
const network = `eip155:${chain.chainId}`;
|
|
1036
|
+
const payload = {
|
|
1037
|
+
x402Version: 2,
|
|
1038
|
+
scheme: "exact",
|
|
1039
|
+
network,
|
|
1040
|
+
payload: {
|
|
1041
|
+
intent: {
|
|
1042
|
+
...intent,
|
|
1043
|
+
signature
|
|
1044
|
+
},
|
|
1045
|
+
chainId: chain.chainId
|
|
1046
|
+
},
|
|
1047
|
+
accepted: {
|
|
1048
|
+
scheme: "exact",
|
|
1049
|
+
network,
|
|
1050
|
+
asset: tokenConfig.address,
|
|
1051
|
+
amount: amountWei,
|
|
1052
|
+
payTo: to,
|
|
1053
|
+
maxTimeoutSeconds: 300
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
1057
|
+
console.log(`[MoltsPay] Sending BNB payment request...`);
|
|
1058
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
1059
|
+
method: "POST",
|
|
1060
|
+
headers: {
|
|
1061
|
+
"Content-Type": "application/json",
|
|
1062
|
+
"X-Payment": paymentHeader
|
|
1063
|
+
},
|
|
1064
|
+
body: JSON.stringify({ service, params, chain: chainName })
|
|
1065
|
+
});
|
|
1066
|
+
const result = await paidRes.json();
|
|
1067
|
+
if (!paidRes.ok) {
|
|
1068
|
+
throw new Error(result.error || "BNB payment failed");
|
|
1069
|
+
}
|
|
1070
|
+
this.recordSpending(amount);
|
|
1071
|
+
console.log(`[MoltsPay] Success! BNB payment settled.`);
|
|
1072
|
+
return result.result || result;
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Handle Solana payment flow
|
|
1076
|
+
*
|
|
1077
|
+
* Solana uses SPL token transfers with pay-for-success model:
|
|
1078
|
+
* 1. Client creates and signs a transfer transaction
|
|
1079
|
+
* 2. Server submits the transaction after service completes
|
|
1080
|
+
*/
|
|
1081
|
+
async handleSolanaPayment(serverUrl, service, params, requirements, chain) {
|
|
1082
|
+
const solanaWallet = loadSolanaWallet(this.configDir);
|
|
1083
|
+
if (!solanaWallet) {
|
|
1084
|
+
throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
|
|
1085
|
+
}
|
|
1086
|
+
const amount = Number(requirements.amount);
|
|
1087
|
+
const amountUSDC = amount / 1e6;
|
|
1088
|
+
this.checkLimits(amountUSDC);
|
|
1089
|
+
console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
|
|
1090
|
+
if (!requirements.payTo) {
|
|
1091
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
1092
|
+
}
|
|
1093
|
+
const solanaFeePayer = requirements.extra?.solanaFeePayer;
|
|
1094
|
+
const feePayerPubkey = solanaFeePayer ? new import_web34.PublicKey(solanaFeePayer) : void 0;
|
|
1095
|
+
if (feePayerPubkey) {
|
|
1096
|
+
console.log(`[MoltsPay] Gasless mode: server pays fees`);
|
|
1097
|
+
}
|
|
1098
|
+
const recipientPubkey = new import_web34.PublicKey(requirements.payTo);
|
|
1099
|
+
const transaction = await createSolanaPaymentTransaction(
|
|
1100
|
+
solanaWallet.publicKey,
|
|
1101
|
+
recipientPubkey,
|
|
1102
|
+
BigInt(amount),
|
|
1103
|
+
chain,
|
|
1104
|
+
feePayerPubkey
|
|
1105
|
+
// Optional fee payer for gasless mode
|
|
1106
|
+
);
|
|
1107
|
+
if (feePayerPubkey) {
|
|
1108
|
+
transaction.partialSign(solanaWallet);
|
|
1109
|
+
} else {
|
|
1110
|
+
transaction.sign(solanaWallet);
|
|
1111
|
+
}
|
|
1112
|
+
const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
|
|
1113
|
+
console.log(`[MoltsPay] Transaction signed, sending to server...`);
|
|
1114
|
+
const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
1115
|
+
const payload = {
|
|
1116
|
+
x402Version: 2,
|
|
1117
|
+
scheme: "exact",
|
|
1118
|
+
network,
|
|
1119
|
+
payload: {
|
|
1120
|
+
signedTransaction: signedTx,
|
|
1121
|
+
sender: solanaWallet.publicKey.toBase58(),
|
|
1122
|
+
chain
|
|
1123
|
+
},
|
|
1124
|
+
accepted: {
|
|
1125
|
+
scheme: "exact",
|
|
1126
|
+
network,
|
|
1127
|
+
asset: requirements.asset,
|
|
1128
|
+
amount: requirements.amount,
|
|
1129
|
+
payTo: requirements.payTo,
|
|
1130
|
+
maxTimeoutSeconds: 300
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
1134
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
1135
|
+
method: "POST",
|
|
1136
|
+
headers: {
|
|
1137
|
+
"Content-Type": "application/json",
|
|
1138
|
+
"X-Payment": paymentHeader
|
|
1139
|
+
},
|
|
1140
|
+
body: JSON.stringify({ service, params, chain })
|
|
1141
|
+
});
|
|
1142
|
+
const result = await paidRes.json();
|
|
1143
|
+
if (!paidRes.ok) {
|
|
1144
|
+
throw new Error(result.error || "Solana payment failed");
|
|
1145
|
+
}
|
|
1146
|
+
this.recordSpending(amountUSDC);
|
|
1147
|
+
console.log(`[MoltsPay] Success! Solana payment settled.`);
|
|
1148
|
+
if (result.payment?.transaction) {
|
|
1149
|
+
const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
|
|
1150
|
+
console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
|
|
1151
|
+
}
|
|
1152
|
+
return result.result || result;
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Check ERC20 allowance for a spender
|
|
1156
|
+
*/
|
|
1157
|
+
async checkAllowance(tokenAddress, spender, provider) {
|
|
1158
|
+
const contract = new import_ethers.ethers.Contract(
|
|
1159
|
+
tokenAddress,
|
|
1160
|
+
["function allowance(address owner, address spender) view returns (uint256)"],
|
|
1161
|
+
provider
|
|
1162
|
+
);
|
|
1163
|
+
return await contract.allowance(this.wallet.address, spender);
|
|
1164
|
+
}
|
|
412
1165
|
/**
|
|
413
1166
|
* Sign EIP-3009 transferWithAuthorization (GASLESS)
|
|
414
1167
|
* This only signs - no on-chain transaction, no gas needed.
|
|
@@ -479,26 +1232,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
479
1232
|
}
|
|
480
1233
|
// --- Config & Wallet Management ---
|
|
481
1234
|
loadConfig() {
|
|
482
|
-
const configPath = (0,
|
|
483
|
-
if ((0,
|
|
484
|
-
const content = (0,
|
|
1235
|
+
const configPath = (0, import_path2.join)(this.configDir, "config.json");
|
|
1236
|
+
if ((0, import_fs2.existsSync)(configPath)) {
|
|
1237
|
+
const content = (0, import_fs2.readFileSync)(configPath, "utf-8");
|
|
485
1238
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
486
1239
|
}
|
|
487
1240
|
return { ...DEFAULT_CONFIG };
|
|
488
1241
|
}
|
|
489
1242
|
saveConfig() {
|
|
490
|
-
(0,
|
|
491
|
-
const configPath = (0,
|
|
492
|
-
(0,
|
|
1243
|
+
(0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
|
|
1244
|
+
const configPath = (0, import_path2.join)(this.configDir, "config.json");
|
|
1245
|
+
(0, import_fs2.writeFileSync)(configPath, JSON.stringify(this.config, null, 2));
|
|
493
1246
|
}
|
|
494
1247
|
/**
|
|
495
1248
|
* Load spending data from disk
|
|
496
1249
|
*/
|
|
497
1250
|
loadSpending() {
|
|
498
|
-
const spendingPath = (0,
|
|
499
|
-
if ((0,
|
|
1251
|
+
const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
|
|
1252
|
+
if ((0, import_fs2.existsSync)(spendingPath)) {
|
|
500
1253
|
try {
|
|
501
|
-
const data = JSON.parse((0,
|
|
1254
|
+
const data = JSON.parse((0, import_fs2.readFileSync)(spendingPath, "utf-8"));
|
|
502
1255
|
const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
|
|
503
1256
|
if (data.date && data.date === today) {
|
|
504
1257
|
this.todaySpending = data.amount || 0;
|
|
@@ -517,29 +1270,29 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
517
1270
|
* Save spending data to disk
|
|
518
1271
|
*/
|
|
519
1272
|
saveSpending() {
|
|
520
|
-
(0,
|
|
521
|
-
const spendingPath = (0,
|
|
1273
|
+
(0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
|
|
1274
|
+
const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
|
|
522
1275
|
const data = {
|
|
523
1276
|
date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
|
|
524
1277
|
amount: this.todaySpending,
|
|
525
1278
|
updatedAt: Date.now()
|
|
526
1279
|
};
|
|
527
|
-
(0,
|
|
1280
|
+
(0, import_fs2.writeFileSync)(spendingPath, JSON.stringify(data, null, 2));
|
|
528
1281
|
}
|
|
529
1282
|
loadWallet() {
|
|
530
|
-
const walletPath = (0,
|
|
531
|
-
if ((0,
|
|
1283
|
+
const walletPath = (0, import_path2.join)(this.configDir, "wallet.json");
|
|
1284
|
+
if ((0, import_fs2.existsSync)(walletPath)) {
|
|
532
1285
|
try {
|
|
533
|
-
const stats = (0,
|
|
1286
|
+
const stats = (0, import_fs2.statSync)(walletPath);
|
|
534
1287
|
const mode = stats.mode & 511;
|
|
535
1288
|
if (mode !== 384) {
|
|
536
1289
|
console.warn(`[MoltsPay] WARNING: wallet.json has insecure permissions (${mode.toString(8)})`);
|
|
537
1290
|
console.warn(`[MoltsPay] Fixing permissions to 0600...`);
|
|
538
|
-
(0,
|
|
1291
|
+
(0, import_fs2.chmodSync)(walletPath, 384);
|
|
539
1292
|
}
|
|
540
1293
|
} catch (err) {
|
|
541
1294
|
}
|
|
542
|
-
const content = (0,
|
|
1295
|
+
const content = (0, import_fs2.readFileSync)(walletPath, "utf-8");
|
|
543
1296
|
return JSON.parse(content);
|
|
544
1297
|
}
|
|
545
1298
|
return null;
|
|
@@ -548,15 +1301,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
548
1301
|
* Initialize a new wallet (called by CLI)
|
|
549
1302
|
*/
|
|
550
1303
|
static init(configDir, options) {
|
|
551
|
-
(0,
|
|
1304
|
+
(0, import_fs2.mkdirSync)(configDir, { recursive: true });
|
|
552
1305
|
const wallet = import_ethers.Wallet.createRandom();
|
|
553
1306
|
const walletData = {
|
|
554
1307
|
address: wallet.address,
|
|
555
1308
|
privateKey: wallet.privateKey,
|
|
556
1309
|
createdAt: Date.now()
|
|
557
1310
|
};
|
|
558
|
-
const walletPath = (0,
|
|
559
|
-
(0,
|
|
1311
|
+
const walletPath = (0, import_path2.join)(configDir, "wallet.json");
|
|
1312
|
+
(0, import_fs2.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
560
1313
|
const config = {
|
|
561
1314
|
chain: options.chain,
|
|
562
1315
|
limits: {
|
|
@@ -564,8 +1317,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
564
1317
|
maxPerDay: options.maxPerDay
|
|
565
1318
|
}
|
|
566
1319
|
};
|
|
567
|
-
const configPath = (0,
|
|
568
|
-
(0,
|
|
1320
|
+
const configPath = (0, import_path2.join)(configDir, "config.json");
|
|
1321
|
+
(0, import_fs2.writeFileSync)(configPath, JSON.stringify(config, null, 2));
|
|
569
1322
|
return { address: wallet.address, configDir };
|
|
570
1323
|
}
|
|
571
1324
|
/**
|
|
@@ -601,7 +1354,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
601
1354
|
if (!this.wallet) {
|
|
602
1355
|
throw new Error("Client not initialized");
|
|
603
1356
|
}
|
|
604
|
-
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
|
|
1357
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
|
|
605
1358
|
const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
606
1359
|
const results = {};
|
|
607
1360
|
const tempoTokens = {
|
|
@@ -672,12 +1425,12 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
672
1425
|
if (!this.wallet || !this.walletData) {
|
|
673
1426
|
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
674
1427
|
}
|
|
675
|
-
const { privateKeyToAccount } = await import("viem/accounts");
|
|
1428
|
+
const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
|
|
676
1429
|
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
677
1430
|
const { tempoModerato } = await import("viem/chains");
|
|
678
1431
|
const { Actions } = await import("viem/tempo");
|
|
679
1432
|
const privateKey = this.walletData.privateKey;
|
|
680
|
-
const account =
|
|
1433
|
+
const account = privateKeyToAccount2(privateKey);
|
|
681
1434
|
console.log(`[MoltsPay] Making MPP request to: ${url}`);
|
|
682
1435
|
console.log(`[MoltsPay] Using account: ${account.address}`);
|
|
683
1436
|
const initResponse = await fetch(url, {
|
|
@@ -774,24 +1527,16 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
774
1527
|
|
|
775
1528
|
// src/server/index.ts
|
|
776
1529
|
init_cjs_shims();
|
|
777
|
-
var
|
|
1530
|
+
var import_fs4 = require("fs");
|
|
778
1531
|
var import_http = require("http");
|
|
779
1532
|
var path2 = __toESM(require("path"));
|
|
780
1533
|
|
|
781
1534
|
// src/facilitators/index.ts
|
|
782
1535
|
init_cjs_shims();
|
|
783
1536
|
|
|
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
1537
|
// src/facilitators/cdp.ts
|
|
793
1538
|
init_cjs_shims();
|
|
794
|
-
var
|
|
1539
|
+
var import_fs3 = require("fs");
|
|
795
1540
|
var path = __toESM(require("path"));
|
|
796
1541
|
var X402_VERSION2 = 2;
|
|
797
1542
|
var CDP_URL = "https://api.cdp.coinbase.com/platform/v2/x402";
|
|
@@ -802,9 +1547,9 @@ function loadEnvFile() {
|
|
|
802
1547
|
path.join(process.env.HOME || "", ".moltspay", ".env")
|
|
803
1548
|
];
|
|
804
1549
|
for (const envPath of envPaths) {
|
|
805
|
-
if ((0,
|
|
1550
|
+
if ((0, import_fs3.existsSync)(envPath)) {
|
|
806
1551
|
try {
|
|
807
|
-
const content = (0,
|
|
1552
|
+
const content = (0, import_fs3.readFileSync)(envPath, "utf-8");
|
|
808
1553
|
for (const line of content.split("\n")) {
|
|
809
1554
|
const trimmed = line.trim();
|
|
810
1555
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -1042,16 +1787,278 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
1042
1787
|
}
|
|
1043
1788
|
return { healthy: true, latencyMs: Date.now() - start };
|
|
1044
1789
|
} catch (error) {
|
|
1045
|
-
return { healthy: false, error: String(error) };
|
|
1790
|
+
return { healthy: false, error: String(error) };
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
async verify(paymentPayload, requirements) {
|
|
1794
|
+
try {
|
|
1795
|
+
const tempoPayload = paymentPayload.payload;
|
|
1796
|
+
if (!tempoPayload?.txHash) {
|
|
1797
|
+
return { valid: false, error: "Missing txHash in payment payload" };
|
|
1798
|
+
}
|
|
1799
|
+
const receipt = await this.getTransactionReceipt(tempoPayload.txHash);
|
|
1800
|
+
if (!receipt) {
|
|
1801
|
+
return { valid: false, error: "Transaction not found" };
|
|
1802
|
+
}
|
|
1803
|
+
if (receipt.status !== "0x1") {
|
|
1804
|
+
return { valid: false, error: "Transaction failed" };
|
|
1805
|
+
}
|
|
1806
|
+
const transferLog = receipt.logs.find(
|
|
1807
|
+
(log) => log.topics[0] === TRANSFER_EVENT_TOPIC
|
|
1808
|
+
);
|
|
1809
|
+
if (!transferLog) {
|
|
1810
|
+
return { valid: false, error: "No Transfer event found" };
|
|
1811
|
+
}
|
|
1812
|
+
const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
|
|
1813
|
+
const expectedTo = requirements.payTo.toLowerCase();
|
|
1814
|
+
if (toAddress !== expectedTo) {
|
|
1815
|
+
return {
|
|
1816
|
+
valid: false,
|
|
1817
|
+
error: `Wrong recipient: ${toAddress}, expected ${expectedTo}`
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
const amount = BigInt(transferLog.data);
|
|
1821
|
+
const expectedAmount = BigInt(requirements.amount);
|
|
1822
|
+
if (amount < expectedAmount) {
|
|
1823
|
+
return {
|
|
1824
|
+
valid: false,
|
|
1825
|
+
error: `Insufficient amount: ${amount}, expected ${expectedAmount}`
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
const tokenAddress = transferLog.address.toLowerCase();
|
|
1829
|
+
const expectedToken = requirements.asset.toLowerCase();
|
|
1830
|
+
if (tokenAddress !== expectedToken) {
|
|
1831
|
+
return {
|
|
1832
|
+
valid: false,
|
|
1833
|
+
error: `Wrong token: ${tokenAddress}, expected ${expectedToken}`
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
return {
|
|
1837
|
+
valid: true,
|
|
1838
|
+
details: {
|
|
1839
|
+
txHash: tempoPayload.txHash,
|
|
1840
|
+
from: "0x" + transferLog.topics[1].slice(26),
|
|
1841
|
+
to: toAddress,
|
|
1842
|
+
amount: amount.toString(),
|
|
1843
|
+
token: tokenAddress
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
} catch (error) {
|
|
1847
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
async settle(paymentPayload, requirements) {
|
|
1851
|
+
const verifyResult = await this.verify(paymentPayload, requirements);
|
|
1852
|
+
if (!verifyResult.valid) {
|
|
1853
|
+
return { success: false, error: verifyResult.error };
|
|
1854
|
+
}
|
|
1855
|
+
const tempoPayload = paymentPayload.payload;
|
|
1856
|
+
return {
|
|
1857
|
+
success: true,
|
|
1858
|
+
transaction: tempoPayload.txHash,
|
|
1859
|
+
status: "settled"
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
async getTransactionReceipt(txHash) {
|
|
1863
|
+
const response = await fetch(this.rpcUrl, {
|
|
1864
|
+
method: "POST",
|
|
1865
|
+
headers: { "Content-Type": "application/json" },
|
|
1866
|
+
body: JSON.stringify({
|
|
1867
|
+
jsonrpc: "2.0",
|
|
1868
|
+
method: "eth_getTransactionReceipt",
|
|
1869
|
+
params: [txHash],
|
|
1870
|
+
id: 1
|
|
1871
|
+
})
|
|
1872
|
+
});
|
|
1873
|
+
const data = await response.json();
|
|
1874
|
+
return data.result;
|
|
1875
|
+
}
|
|
1876
|
+
};
|
|
1877
|
+
|
|
1878
|
+
// src/facilitators/bnb.ts
|
|
1879
|
+
init_cjs_shims();
|
|
1880
|
+
var import_accounts = require("viem/accounts");
|
|
1881
|
+
var TRANSFER_EVENT_TOPIC2 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
1882
|
+
var EIP712_DOMAIN = {
|
|
1883
|
+
name: "MoltsPay",
|
|
1884
|
+
version: "1"
|
|
1885
|
+
};
|
|
1886
|
+
var INTENT_TYPES = {
|
|
1887
|
+
PaymentIntent: [
|
|
1888
|
+
{ name: "from", type: "address" },
|
|
1889
|
+
{ name: "to", type: "address" },
|
|
1890
|
+
{ name: "amount", type: "uint256" },
|
|
1891
|
+
{ name: "token", type: "address" },
|
|
1892
|
+
{ name: "service", type: "string" },
|
|
1893
|
+
{ name: "nonce", type: "uint256" },
|
|
1894
|
+
{ name: "deadline", type: "uint256" }
|
|
1895
|
+
]
|
|
1896
|
+
};
|
|
1897
|
+
var BNBFacilitator = class extends BaseFacilitator {
|
|
1898
|
+
name = "bnb";
|
|
1899
|
+
displayName = "BNB Smart Chain";
|
|
1900
|
+
supportedNetworks = ["eip155:56", "eip155:97"];
|
|
1901
|
+
// Mainnet + Testnet
|
|
1902
|
+
serverPrivateKey;
|
|
1903
|
+
spenderAddress = null;
|
|
1904
|
+
chainConfigs;
|
|
1905
|
+
constructor(serverPrivateKey) {
|
|
1906
|
+
super();
|
|
1907
|
+
this.serverPrivateKey = serverPrivateKey || process.env.BNB_SERVER_PRIVATE_KEY || "";
|
|
1908
|
+
if (this.serverPrivateKey) {
|
|
1909
|
+
const key = this.serverPrivateKey.startsWith("0x") ? this.serverPrivateKey : `0x${this.serverPrivateKey}`;
|
|
1910
|
+
const account = (0, import_accounts.privateKeyToAccount)(key);
|
|
1911
|
+
this.spenderAddress = account.address;
|
|
1912
|
+
}
|
|
1913
|
+
this.chainConfigs = {
|
|
1914
|
+
56: { rpc: CHAINS.bnb.rpc, chain: CHAINS.bnb },
|
|
1915
|
+
97: { rpc: CHAINS.bnb_testnet.rpc, chain: CHAINS.bnb_testnet }
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
async healthCheck() {
|
|
1919
|
+
const start = Date.now();
|
|
1920
|
+
try {
|
|
1921
|
+
const response = await fetch(this.chainConfigs[56].rpc, {
|
|
1922
|
+
method: "POST",
|
|
1923
|
+
headers: { "Content-Type": "application/json" },
|
|
1924
|
+
body: JSON.stringify({
|
|
1925
|
+
jsonrpc: "2.0",
|
|
1926
|
+
method: "eth_chainId",
|
|
1927
|
+
params: [],
|
|
1928
|
+
id: 1
|
|
1929
|
+
})
|
|
1930
|
+
});
|
|
1931
|
+
const data = await response.json();
|
|
1932
|
+
const chainId = parseInt(data.result, 16);
|
|
1933
|
+
if (chainId !== 56) {
|
|
1934
|
+
return { healthy: false, error: `Wrong chainId: ${chainId}` };
|
|
1935
|
+
}
|
|
1936
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
1937
|
+
} catch (error) {
|
|
1938
|
+
return { healthy: false, error: String(error) };
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Verify a payment intent signature (before service execution)
|
|
1943
|
+
*
|
|
1944
|
+
* This verifies:
|
|
1945
|
+
* 1. Signature is valid for the intent
|
|
1946
|
+
* 2. Client has approved server wallet
|
|
1947
|
+
* 3. Client has sufficient balance
|
|
1948
|
+
* 4. Intent hasn't expired
|
|
1949
|
+
*/
|
|
1950
|
+
async verify(paymentPayload, requirements) {
|
|
1951
|
+
try {
|
|
1952
|
+
const bnbPayload = paymentPayload.payload;
|
|
1953
|
+
if (!bnbPayload?.intent) {
|
|
1954
|
+
return { valid: false, error: "Missing intent in payment payload" };
|
|
1955
|
+
}
|
|
1956
|
+
const { intent, chainId } = bnbPayload;
|
|
1957
|
+
const config = this.chainConfigs[chainId];
|
|
1958
|
+
if (!config) {
|
|
1959
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
1960
|
+
}
|
|
1961
|
+
if (intent.deadline < Date.now()) {
|
|
1962
|
+
return { valid: false, error: "Intent expired" };
|
|
1963
|
+
}
|
|
1964
|
+
const recoveredAddress = await this.recoverIntentSigner(intent, chainId);
|
|
1965
|
+
if (recoveredAddress.toLowerCase() !== intent.from.toLowerCase()) {
|
|
1966
|
+
return { valid: false, error: "Invalid signature" };
|
|
1967
|
+
}
|
|
1968
|
+
if (intent.to.toLowerCase() !== requirements.payTo.toLowerCase()) {
|
|
1969
|
+
return { valid: false, error: `Wrong recipient: ${intent.to}` };
|
|
1970
|
+
}
|
|
1971
|
+
if (BigInt(intent.amount) < BigInt(requirements.amount)) {
|
|
1972
|
+
return { valid: false, error: `Insufficient amount: ${intent.amount}` };
|
|
1973
|
+
}
|
|
1974
|
+
if (intent.token.toLowerCase() !== requirements.asset.toLowerCase()) {
|
|
1975
|
+
return { valid: false, error: `Wrong token: ${intent.token}` };
|
|
1976
|
+
}
|
|
1977
|
+
const serverAddress = await this.getServerAddress();
|
|
1978
|
+
const allowance = await this.getAllowance(intent.from, serverAddress, intent.token, config.rpc);
|
|
1979
|
+
if (BigInt(allowance) < BigInt(intent.amount)) {
|
|
1980
|
+
return { valid: false, error: "Insufficient allowance. Run: npx moltspay init --chain bnb" };
|
|
1981
|
+
}
|
|
1982
|
+
const balance = await this.getBalance(intent.from, intent.token, config.rpc);
|
|
1983
|
+
if (BigInt(balance) < BigInt(intent.amount)) {
|
|
1984
|
+
return { valid: false, error: "Insufficient balance" };
|
|
1985
|
+
}
|
|
1986
|
+
return {
|
|
1987
|
+
valid: true,
|
|
1988
|
+
details: {
|
|
1989
|
+
from: intent.from,
|
|
1990
|
+
to: intent.to,
|
|
1991
|
+
amount: intent.amount,
|
|
1992
|
+
token: intent.token,
|
|
1993
|
+
service: intent.service,
|
|
1994
|
+
nonce: intent.nonce,
|
|
1995
|
+
deadline: intent.deadline
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
} catch (error) {
|
|
1999
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
1046
2000
|
}
|
|
1047
2001
|
}
|
|
1048
|
-
|
|
2002
|
+
/**
|
|
2003
|
+
* Settle a payment by executing transferFrom
|
|
2004
|
+
*
|
|
2005
|
+
* This is called AFTER the service has been successfully delivered.
|
|
2006
|
+
* Server pays gas, transfers tokens from client to provider.
|
|
2007
|
+
*/
|
|
2008
|
+
async settle(paymentPayload, requirements) {
|
|
2009
|
+
if (!this.serverPrivateKey) {
|
|
2010
|
+
return { success: false, error: "Server wallet not configured (BNB_SERVER_PRIVATE_KEY)" };
|
|
2011
|
+
}
|
|
1049
2012
|
try {
|
|
1050
|
-
const
|
|
1051
|
-
if (!
|
|
1052
|
-
return {
|
|
2013
|
+
const verifyResult = await this.verify(paymentPayload, requirements);
|
|
2014
|
+
if (!verifyResult.valid) {
|
|
2015
|
+
return { success: false, error: verifyResult.error };
|
|
1053
2016
|
}
|
|
1054
|
-
const
|
|
2017
|
+
const bnbPayload = paymentPayload.payload;
|
|
2018
|
+
const { intent, chainId } = bnbPayload;
|
|
2019
|
+
const config = this.chainConfigs[chainId];
|
|
2020
|
+
const txHash = await this.executeTransferFrom(
|
|
2021
|
+
intent.from,
|
|
2022
|
+
intent.to,
|
|
2023
|
+
intent.amount,
|
|
2024
|
+
intent.token,
|
|
2025
|
+
config.rpc
|
|
2026
|
+
);
|
|
2027
|
+
return {
|
|
2028
|
+
success: true,
|
|
2029
|
+
transaction: txHash,
|
|
2030
|
+
status: "settled"
|
|
2031
|
+
};
|
|
2032
|
+
} catch (error) {
|
|
2033
|
+
return { success: false, error: `Settlement failed: ${error}` };
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
/**
|
|
2037
|
+
* Check if client has approved the server wallet
|
|
2038
|
+
*/
|
|
2039
|
+
async checkApproval(clientAddress, token, chainId) {
|
|
2040
|
+
const config = this.chainConfigs[chainId];
|
|
2041
|
+
if (!config) {
|
|
2042
|
+
throw new Error(`Unsupported chainId: ${chainId}`);
|
|
2043
|
+
}
|
|
2044
|
+
const serverAddress = await this.getServerAddress();
|
|
2045
|
+
const allowance = await this.getAllowance(clientAddress, serverAddress, token, config.rpc);
|
|
2046
|
+
const minAllowance = BigInt("1000000000000000000000");
|
|
2047
|
+
return {
|
|
2048
|
+
approved: BigInt(allowance) >= minAllowance,
|
|
2049
|
+
allowance
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Verify a completed transaction (for checking past payments)
|
|
2054
|
+
*/
|
|
2055
|
+
async verifyTransaction(txHash, expected, chainId) {
|
|
2056
|
+
const config = this.chainConfigs[chainId];
|
|
2057
|
+
if (!config) {
|
|
2058
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
2059
|
+
}
|
|
2060
|
+
try {
|
|
2061
|
+
const receipt = await this.getTransactionReceipt(txHash, config.rpc);
|
|
1055
2062
|
if (!receipt) {
|
|
1056
2063
|
return { valid: false, error: "Transaction not found" };
|
|
1057
2064
|
}
|
|
@@ -1059,63 +2066,117 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
1059
2066
|
return { valid: false, error: "Transaction failed" };
|
|
1060
2067
|
}
|
|
1061
2068
|
const transferLog = receipt.logs.find(
|
|
1062
|
-
(log) => log.topics[0] ===
|
|
2069
|
+
(log) => log.topics[0] === TRANSFER_EVENT_TOPIC2 && log.address.toLowerCase() === expected.token.toLowerCase()
|
|
1063
2070
|
);
|
|
1064
2071
|
if (!transferLog) {
|
|
1065
2072
|
return { valid: false, error: "No Transfer event found" };
|
|
1066
2073
|
}
|
|
1067
2074
|
const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
return {
|
|
1071
|
-
valid: false,
|
|
1072
|
-
error: `Wrong recipient: ${toAddress}, expected ${expectedTo}`
|
|
1073
|
-
};
|
|
2075
|
+
if (toAddress !== expected.to.toLowerCase()) {
|
|
2076
|
+
return { valid: false, error: `Wrong recipient: ${toAddress}` };
|
|
1074
2077
|
}
|
|
1075
2078
|
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
|
-
};
|
|
2079
|
+
if (amount < BigInt(expected.amount)) {
|
|
2080
|
+
return { valid: false, error: `Insufficient amount: ${amount}` };
|
|
1090
2081
|
}
|
|
1091
2082
|
return {
|
|
1092
2083
|
valid: true,
|
|
1093
2084
|
details: {
|
|
1094
|
-
txHash
|
|
2085
|
+
txHash,
|
|
1095
2086
|
from: "0x" + transferLog.topics[1].slice(26),
|
|
1096
2087
|
to: toAddress,
|
|
1097
2088
|
amount: amount.toString(),
|
|
1098
|
-
token:
|
|
2089
|
+
token: transferLog.address
|
|
1099
2090
|
}
|
|
1100
2091
|
};
|
|
1101
2092
|
} catch (error) {
|
|
1102
2093
|
return { valid: false, error: `Verification failed: ${error}` };
|
|
1103
2094
|
}
|
|
1104
2095
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
return
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
2096
|
+
// ==================== Private Methods ====================
|
|
2097
|
+
/**
|
|
2098
|
+
* Get the server's spender address (public, for 402 responses)
|
|
2099
|
+
* Returns cached value computed at construction time.
|
|
2100
|
+
*/
|
|
2101
|
+
getSpenderAddress() {
|
|
2102
|
+
return this.spenderAddress;
|
|
2103
|
+
}
|
|
2104
|
+
async getServerAddress() {
|
|
2105
|
+
const { ethers: ethers3 } = await import("ethers");
|
|
2106
|
+
const wallet = new ethers3.Wallet(this.serverPrivateKey);
|
|
2107
|
+
return wallet.address;
|
|
2108
|
+
}
|
|
2109
|
+
async recoverIntentSigner(intent, chainId) {
|
|
2110
|
+
const { ethers: ethers3 } = await import("ethers");
|
|
2111
|
+
const domain = {
|
|
2112
|
+
...EIP712_DOMAIN,
|
|
2113
|
+
chainId
|
|
1115
2114
|
};
|
|
2115
|
+
const message = {
|
|
2116
|
+
from: intent.from,
|
|
2117
|
+
to: intent.to,
|
|
2118
|
+
amount: intent.amount,
|
|
2119
|
+
token: intent.token,
|
|
2120
|
+
service: intent.service,
|
|
2121
|
+
nonce: intent.nonce,
|
|
2122
|
+
deadline: intent.deadline
|
|
2123
|
+
};
|
|
2124
|
+
const recoveredAddress = ethers3.verifyTypedData(
|
|
2125
|
+
domain,
|
|
2126
|
+
INTENT_TYPES,
|
|
2127
|
+
message,
|
|
2128
|
+
intent.signature
|
|
2129
|
+
);
|
|
2130
|
+
return recoveredAddress;
|
|
1116
2131
|
}
|
|
1117
|
-
async
|
|
1118
|
-
const
|
|
2132
|
+
async getAllowance(owner, spender, token, rpcUrl) {
|
|
2133
|
+
const selector = "0xdd62ed3e";
|
|
2134
|
+
const ownerPadded = owner.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
2135
|
+
const spenderPadded = spender.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
2136
|
+
const data = selector + ownerPadded + spenderPadded;
|
|
2137
|
+
const response = await fetch(rpcUrl, {
|
|
2138
|
+
method: "POST",
|
|
2139
|
+
headers: { "Content-Type": "application/json" },
|
|
2140
|
+
body: JSON.stringify({
|
|
2141
|
+
jsonrpc: "2.0",
|
|
2142
|
+
method: "eth_call",
|
|
2143
|
+
params: [{ to: token, data }, "latest"],
|
|
2144
|
+
id: 1
|
|
2145
|
+
})
|
|
2146
|
+
});
|
|
2147
|
+
const result = await response.json();
|
|
2148
|
+
return result.result || "0x0";
|
|
2149
|
+
}
|
|
2150
|
+
async getBalance(account, token, rpcUrl) {
|
|
2151
|
+
const selector = "0x70a08231";
|
|
2152
|
+
const accountPadded = account.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
2153
|
+
const data = selector + accountPadded;
|
|
2154
|
+
const response = await fetch(rpcUrl, {
|
|
2155
|
+
method: "POST",
|
|
2156
|
+
headers: { "Content-Type": "application/json" },
|
|
2157
|
+
body: JSON.stringify({
|
|
2158
|
+
jsonrpc: "2.0",
|
|
2159
|
+
method: "eth_call",
|
|
2160
|
+
params: [{ to: token, data }, "latest"],
|
|
2161
|
+
id: 1
|
|
2162
|
+
})
|
|
2163
|
+
});
|
|
2164
|
+
const result = await response.json();
|
|
2165
|
+
return result.result || "0x0";
|
|
2166
|
+
}
|
|
2167
|
+
async executeTransferFrom(from, to, amount, token, rpcUrl) {
|
|
2168
|
+
const { ethers: ethers3 } = await import("ethers");
|
|
2169
|
+
const provider = new ethers3.JsonRpcProvider(rpcUrl);
|
|
2170
|
+
const wallet = new ethers3.Wallet(this.serverPrivateKey, provider);
|
|
2171
|
+
const tokenContract = new ethers3.Contract(token, [
|
|
2172
|
+
"function transferFrom(address from, address to, uint256 amount) returns (bool)"
|
|
2173
|
+
], wallet);
|
|
2174
|
+
const tx = await tokenContract.transferFrom(from, to, amount);
|
|
2175
|
+
const receipt = await tx.wait();
|
|
2176
|
+
return receipt.hash;
|
|
2177
|
+
}
|
|
2178
|
+
async getTransactionReceipt(txHash, rpcUrl) {
|
|
2179
|
+
const response = await fetch(rpcUrl, {
|
|
1119
2180
|
method: "POST",
|
|
1120
2181
|
headers: { "Content-Type": "application/json" },
|
|
1121
2182
|
body: JSON.stringify({
|
|
@@ -1132,6 +2193,8 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
1132
2193
|
|
|
1133
2194
|
// src/facilitators/registry.ts
|
|
1134
2195
|
init_cjs_shims();
|
|
2196
|
+
var import_web35 = require("@solana/web3.js");
|
|
2197
|
+
var import_bs582 = __toESM(require("bs58"));
|
|
1135
2198
|
var FacilitatorRegistry = class {
|
|
1136
2199
|
factories = /* @__PURE__ */ new Map();
|
|
1137
2200
|
instances = /* @__PURE__ */ new Map();
|
|
@@ -1140,7 +2203,20 @@ var FacilitatorRegistry = class {
|
|
|
1140
2203
|
constructor(selection) {
|
|
1141
2204
|
this.registerFactory("cdp", (config) => new CDPFacilitator(config));
|
|
1142
2205
|
this.registerFactory("tempo", () => new TempoFacilitator());
|
|
1143
|
-
this.
|
|
2206
|
+
this.registerFactory("bnb", (config) => new BNBFacilitator(config?.serverPrivateKey));
|
|
2207
|
+
this.registerFactory("solana", (config) => {
|
|
2208
|
+
let feePayerKeypair;
|
|
2209
|
+
const feePayerKey = config?.feePayerPrivateKey || process.env.SOLANA_FEE_PAYER_KEY;
|
|
2210
|
+
if (feePayerKey) {
|
|
2211
|
+
try {
|
|
2212
|
+
feePayerKeypair = import_web35.Keypair.fromSecretKey(import_bs582.default.decode(feePayerKey));
|
|
2213
|
+
} catch (e) {
|
|
2214
|
+
console.warn(`[SolanaFacilitator] Invalid fee payer key: ${e.message}`);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
return new SolanaFacilitator({ feePayerKeypair });
|
|
2218
|
+
});
|
|
2219
|
+
this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
|
|
1144
2220
|
}
|
|
1145
2221
|
/**
|
|
1146
2222
|
* Register a new facilitator factory
|
|
@@ -1387,14 +2463,40 @@ var TOKEN_ADDRESSES = {
|
|
|
1387
2463
|
// pathUSD
|
|
1388
2464
|
USDT: "0x20c0000000000000000000000000000000000001"
|
|
1389
2465
|
// alphaUSD
|
|
2466
|
+
},
|
|
2467
|
+
// BNB Smart Chain mainnet
|
|
2468
|
+
"eip155:56": {
|
|
2469
|
+
USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
2470
|
+
USDT: "0x55d398326f99059fF775485246999027B3197955"
|
|
2471
|
+
},
|
|
2472
|
+
// BNB Smart Chain testnet
|
|
2473
|
+
"eip155:97": {
|
|
2474
|
+
USDC: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
2475
|
+
USDT: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd"
|
|
2476
|
+
},
|
|
2477
|
+
// Solana networks use mint addresses (SPL tokens)
|
|
2478
|
+
"solana:mainnet": {
|
|
2479
|
+
USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
|
2480
|
+
// Circle USDC
|
|
2481
|
+
},
|
|
2482
|
+
"solana:devnet": {
|
|
2483
|
+
USDC: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"
|
|
2484
|
+
// Devnet USDC
|
|
1390
2485
|
}
|
|
1391
2486
|
};
|
|
1392
2487
|
var CHAIN_TO_NETWORK = {
|
|
1393
2488
|
"base": "eip155:8453",
|
|
1394
2489
|
"base_sepolia": "eip155:84532",
|
|
1395
2490
|
"polygon": "eip155:137",
|
|
1396
|
-
"tempo_moderato": "eip155:42431"
|
|
2491
|
+
"tempo_moderato": "eip155:42431",
|
|
2492
|
+
"bnb": "eip155:56",
|
|
2493
|
+
"bnb_testnet": "eip155:97",
|
|
2494
|
+
"solana": "solana:mainnet",
|
|
2495
|
+
"solana_devnet": "solana:devnet"
|
|
1397
2496
|
};
|
|
2497
|
+
function isSolanaNetwork(network) {
|
|
2498
|
+
return network.startsWith("solana:");
|
|
2499
|
+
}
|
|
1398
2500
|
var TOKEN_DOMAINS = {
|
|
1399
2501
|
// Base mainnet
|
|
1400
2502
|
"eip155:8453": {
|
|
@@ -1416,6 +2518,16 @@ var TOKEN_DOMAINS = {
|
|
|
1416
2518
|
"eip155:42431": {
|
|
1417
2519
|
USDC: { name: "pathUSD", version: "1" },
|
|
1418
2520
|
USDT: { name: "alphaUSD", version: "1" }
|
|
2521
|
+
},
|
|
2522
|
+
// BNB Smart Chain mainnet
|
|
2523
|
+
"eip155:56": {
|
|
2524
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
2525
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
2526
|
+
},
|
|
2527
|
+
// BNB Smart Chain testnet
|
|
2528
|
+
"eip155:97": {
|
|
2529
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
2530
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
1419
2531
|
}
|
|
1420
2532
|
};
|
|
1421
2533
|
function getTokenDomain(network, token) {
|
|
@@ -1431,9 +2543,9 @@ function loadEnvFile2() {
|
|
|
1431
2543
|
path2.join(process.env.HOME || "", ".moltspay", ".env")
|
|
1432
2544
|
];
|
|
1433
2545
|
for (const envPath of envPaths) {
|
|
1434
|
-
if ((0,
|
|
2546
|
+
if ((0, import_fs4.existsSync)(envPath)) {
|
|
1435
2547
|
try {
|
|
1436
|
-
const content = (0,
|
|
2548
|
+
const content = (0, import_fs4.readFileSync)(envPath, "utf-8");
|
|
1437
2549
|
for (const line of content.split("\n")) {
|
|
1438
2550
|
const trimmed = line.trim();
|
|
1439
2551
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -1464,7 +2576,7 @@ var MoltsPayServer = class {
|
|
|
1464
2576
|
useMainnet;
|
|
1465
2577
|
constructor(servicesPath, options = {}) {
|
|
1466
2578
|
loadEnvFile2();
|
|
1467
|
-
const content = (0,
|
|
2579
|
+
const content = (0, import_fs4.readFileSync)(servicesPath, "utf-8");
|
|
1468
2580
|
this.manifest = JSON.parse(content);
|
|
1469
2581
|
this.options = {
|
|
1470
2582
|
port: options.port || 3e3,
|
|
@@ -1473,7 +2585,7 @@ var MoltsPayServer = class {
|
|
|
1473
2585
|
};
|
|
1474
2586
|
this.useMainnet = process.env.USE_MAINNET?.toLowerCase() === "true";
|
|
1475
2587
|
this.networkId = this.useMainnet ? "eip155:8453" : "eip155:84532";
|
|
1476
|
-
const defaultFallback = ["tempo"];
|
|
2588
|
+
const defaultFallback = ["tempo", "bnb", "solana"];
|
|
1477
2589
|
const envFallback = process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean);
|
|
1478
2590
|
const facilitatorConfig = options.facilitators || {
|
|
1479
2591
|
primary: process.env.FACILITATOR_PRIMARY || "cdp",
|
|
@@ -1516,12 +2628,20 @@ var MoltsPayServer = class {
|
|
|
1516
2628
|
*/
|
|
1517
2629
|
getProviderChains() {
|
|
1518
2630
|
const provider = this.manifest.provider;
|
|
2631
|
+
const getWalletForChain = (chainName, explicitWallet) => {
|
|
2632
|
+
if (explicitWallet) return explicitWallet;
|
|
2633
|
+
if ((chainName === "solana" || chainName === "solana_devnet") && provider.solana_wallet) {
|
|
2634
|
+
return provider.solana_wallet;
|
|
2635
|
+
}
|
|
2636
|
+
return provider.wallet;
|
|
2637
|
+
};
|
|
1519
2638
|
if (provider.chains && provider.chains.length > 0) {
|
|
1520
2639
|
return provider.chains.map((c) => {
|
|
1521
2640
|
const chainName = typeof c === "string" ? c : c.chain;
|
|
2641
|
+
const explicitWallet = typeof c === "object" ? c.wallet : null;
|
|
1522
2642
|
return {
|
|
1523
2643
|
network: CHAIN_TO_NETWORK[chainName] || "eip155:8453",
|
|
1524
|
-
wallet: (
|
|
2644
|
+
wallet: getWalletForChain(chainName, explicitWallet || void 0),
|
|
1525
2645
|
tokens: (typeof c === "object" ? c.tokens : null) || ["USDC"]
|
|
1526
2646
|
};
|
|
1527
2647
|
});
|
|
@@ -1530,7 +2650,7 @@ var MoltsPayServer = class {
|
|
|
1530
2650
|
const network = CHAIN_TO_NETWORK[chain] || this.networkId;
|
|
1531
2651
|
return [{
|
|
1532
2652
|
network,
|
|
1533
|
-
wallet:
|
|
2653
|
+
wallet: getWalletForChain(chain),
|
|
1534
2654
|
tokens: ["USDC"]
|
|
1535
2655
|
}];
|
|
1536
2656
|
}
|
|
@@ -1601,7 +2721,8 @@ var MoltsPayServer = class {
|
|
|
1601
2721
|
}
|
|
1602
2722
|
const body = await this.readBody(req);
|
|
1603
2723
|
const paymentHeader = req.headers[PAYMENT_HEADER2];
|
|
1604
|
-
|
|
2724
|
+
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
2725
|
+
return await this.handleProxy(body, paymentHeader, authHeader, res);
|
|
1605
2726
|
}
|
|
1606
2727
|
const servicePath = url.pathname.replace(/^\//, "");
|
|
1607
2728
|
const skill = this.skills.get(servicePath);
|
|
@@ -1638,7 +2759,9 @@ var MoltsPayServer = class {
|
|
|
1638
2759
|
name: this.manifest.provider.name,
|
|
1639
2760
|
description: this.manifest.provider.description,
|
|
1640
2761
|
wallet: this.manifest.provider.wallet,
|
|
1641
|
-
chain: this.manifest.provider.chain || "base"
|
|
2762
|
+
chain: this.manifest.provider.chain || "base",
|
|
2763
|
+
solana_wallet: this.manifest.provider.solana_wallet,
|
|
2764
|
+
chains: this.manifest.provider.chains
|
|
1642
2765
|
},
|
|
1643
2766
|
services,
|
|
1644
2767
|
endpoints: {
|
|
@@ -1751,6 +2874,21 @@ var MoltsPayServer = class {
|
|
|
1751
2874
|
});
|
|
1752
2875
|
}
|
|
1753
2876
|
console.log(`[MoltsPay] Verified by ${verifyResult.facilitator}`);
|
|
2877
|
+
const isSolana = isSolanaNetwork(paymentNetwork);
|
|
2878
|
+
let settlement = null;
|
|
2879
|
+
if (isSolana) {
|
|
2880
|
+
console.log(`[MoltsPay] Solana detected - settling payment FIRST (blockhash expiry protection)`);
|
|
2881
|
+
try {
|
|
2882
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
2883
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
2884
|
+
} catch (err) {
|
|
2885
|
+
console.error("[MoltsPay] Solana settlement failed:", err.message);
|
|
2886
|
+
return this.sendJson(res, 402, {
|
|
2887
|
+
error: "Payment settlement failed",
|
|
2888
|
+
message: err.message
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
1754
2892
|
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
1755
2893
|
console.log(`[MoltsPay] Executing skill: ${service} (timeout: ${timeoutSeconds}s)`);
|
|
1756
2894
|
let result;
|
|
@@ -1765,16 +2903,19 @@ var MoltsPayServer = class {
|
|
|
1765
2903
|
console.error("[MoltsPay] Skill execution failed:", err.message);
|
|
1766
2904
|
return this.sendJson(res, 500, {
|
|
1767
2905
|
error: "Service execution failed",
|
|
1768
|
-
message: err.message
|
|
2906
|
+
message: err.message,
|
|
2907
|
+
paymentSettled: isSolana ? true : false,
|
|
2908
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
1769
2909
|
});
|
|
1770
2910
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
2911
|
+
if (!isSolana) {
|
|
2912
|
+
console.log(`[MoltsPay] Skill succeeded, settling payment...`);
|
|
2913
|
+
try {
|
|
2914
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
2915
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
2916
|
+
} catch (err) {
|
|
2917
|
+
console.error("[MoltsPay] Settlement failed:", err.message);
|
|
2918
|
+
}
|
|
1778
2919
|
}
|
|
1779
2920
|
const responseHeaders = {};
|
|
1780
2921
|
if (settlement?.success) {
|
|
@@ -2050,7 +3191,7 @@ var MoltsPayServer = class {
|
|
|
2050
3191
|
const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
|
|
2051
3192
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
2052
3193
|
const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
|
|
2053
|
-
|
|
3194
|
+
const requirements = {
|
|
2054
3195
|
scheme: "exact",
|
|
2055
3196
|
network: selectedNetwork,
|
|
2056
3197
|
asset: tokenAddress,
|
|
@@ -2059,6 +3200,27 @@ var MoltsPayServer = class {
|
|
|
2059
3200
|
maxTimeoutSeconds: 300,
|
|
2060
3201
|
extra: tokenDomain
|
|
2061
3202
|
};
|
|
3203
|
+
if (selectedNetwork === "solana:mainnet" || selectedNetwork === "solana:devnet") {
|
|
3204
|
+
const solanaFacilitator = this.registry.get("solana");
|
|
3205
|
+
const feePayerPubkey = solanaFacilitator?.getFeePayerPubkey?.();
|
|
3206
|
+
if (feePayerPubkey) {
|
|
3207
|
+
requirements.extra = {
|
|
3208
|
+
...requirements.extra || {},
|
|
3209
|
+
solanaFeePayer: feePayerPubkey
|
|
3210
|
+
};
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
if (selectedNetwork === "eip155:56" || selectedNetwork === "eip155:97") {
|
|
3214
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
3215
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
3216
|
+
if (spenderAddress) {
|
|
3217
|
+
requirements.extra = {
|
|
3218
|
+
...requirements.extra || {},
|
|
3219
|
+
bnbSpender: spenderAddress
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
return requirements;
|
|
2062
3224
|
}
|
|
2063
3225
|
/**
|
|
2064
3226
|
* Detect which token is being used in the payment
|
|
@@ -2124,31 +3286,42 @@ var MoltsPayServer = class {
|
|
|
2124
3286
|
/**
|
|
2125
3287
|
* POST /proxy - Handle payment for external services (moltspay-creators)
|
|
2126
3288
|
*
|
|
2127
|
-
* This endpoint allows other services to delegate x402 payment handling.
|
|
3289
|
+
* This endpoint allows other services to delegate x402/MPP payment handling.
|
|
2128
3290
|
* It does NOT execute any skill - just handles payment verification/settlement.
|
|
2129
3291
|
*
|
|
2130
3292
|
* Request body:
|
|
2131
3293
|
* { wallet, amount, currency, chain, memo, serviceId, description }
|
|
2132
3294
|
*
|
|
2133
|
-
*
|
|
2134
|
-
*
|
|
3295
|
+
* For x402 (base, polygon, base_sepolia):
|
|
3296
|
+
* Without X-Payment header: returns 402 with X-Payment-Required
|
|
3297
|
+
* With X-Payment header: verifies payment via CDP
|
|
3298
|
+
*
|
|
3299
|
+
* For MPP (tempo_moderato):
|
|
3300
|
+
* Without Authorization header: returns 402 with WWW-Authenticate
|
|
3301
|
+
* With Authorization: Payment header: verifies tx on Tempo chain
|
|
2135
3302
|
*/
|
|
2136
|
-
async handleProxy(body, paymentHeader, res) {
|
|
3303
|
+
async handleProxy(body, paymentHeader, authHeader, res) {
|
|
2137
3304
|
const { wallet, amount, currency, chain, memo, serviceId, description } = body;
|
|
2138
3305
|
if (!wallet || !amount) {
|
|
2139
3306
|
return this.sendJson(res, 400, { error: "Missing required fields: wallet, amount" });
|
|
2140
3307
|
}
|
|
2141
|
-
|
|
2142
|
-
|
|
3308
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
|
|
3309
|
+
if (chain && !supportedChains.includes(chain)) {
|
|
3310
|
+
return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
|
|
3311
|
+
}
|
|
3312
|
+
const isSolanaChain = chain === "solana" || chain === "solana_devnet";
|
|
3313
|
+
const isValidEvmAddress = /^0x[a-fA-F0-9]{40}$/.test(wallet);
|
|
3314
|
+
const isValidSolanaAddress2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(wallet);
|
|
3315
|
+
if (isSolanaChain && !isValidSolanaAddress2) {
|
|
3316
|
+
return this.sendJson(res, 400, { error: "Invalid Solana wallet address format" });
|
|
3317
|
+
}
|
|
3318
|
+
if (!isSolanaChain && !isValidEvmAddress) {
|
|
3319
|
+
return this.sendJson(res, 400, { error: "Invalid EVM wallet address format" });
|
|
2143
3320
|
}
|
|
2144
3321
|
const amountNum = parseFloat(amount);
|
|
2145
3322
|
if (isNaN(amountNum) || amountNum <= 0) {
|
|
2146
3323
|
return this.sendJson(res, 400, { error: "Invalid amount" });
|
|
2147
3324
|
}
|
|
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
3325
|
const proxyConfig = {
|
|
2153
3326
|
id: serviceId || "proxy",
|
|
2154
3327
|
name: description || "Proxy Payment",
|
|
@@ -2160,6 +3333,9 @@ var MoltsPayServer = class {
|
|
|
2160
3333
|
input: {},
|
|
2161
3334
|
output: {}
|
|
2162
3335
|
};
|
|
3336
|
+
if (chain === "tempo_moderato") {
|
|
3337
|
+
return await this.handleProxyMPP(body, proxyConfig, authHeader, res);
|
|
3338
|
+
}
|
|
2163
3339
|
const requirements = this.buildProxyPaymentRequirements(proxyConfig, wallet, currency, chain);
|
|
2164
3340
|
if (!paymentHeader) {
|
|
2165
3341
|
return this.sendProxyPaymentRequired(proxyConfig, wallet, memo, chain, res);
|
|
@@ -2171,37 +3347,225 @@ var MoltsPayServer = class {
|
|
|
2171
3347
|
} catch {
|
|
2172
3348
|
return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
|
|
2173
3349
|
}
|
|
2174
|
-
if (payment.x402Version !== X402_VERSION3) {
|
|
2175
|
-
return this.sendJson(res, 402, { error: `Unsupported x402 version: ${payment.x402Version}` });
|
|
3350
|
+
if (payment.x402Version !== X402_VERSION3) {
|
|
3351
|
+
return this.sendJson(res, 402, { error: `Unsupported x402 version: ${payment.x402Version}` });
|
|
3352
|
+
}
|
|
3353
|
+
const scheme = payment.accepted?.scheme || payment.scheme;
|
|
3354
|
+
const network = payment.accepted?.network || payment.network;
|
|
3355
|
+
if (scheme !== "exact") {
|
|
3356
|
+
return this.sendJson(res, 402, { error: `Unsupported scheme: ${scheme}` });
|
|
3357
|
+
}
|
|
3358
|
+
const expectedNetwork = chain ? CHAIN_TO_NETWORK[chain] || this.networkId : this.networkId;
|
|
3359
|
+
if (network !== expectedNetwork) {
|
|
3360
|
+
return this.sendJson(res, 402, { error: `Network mismatch: expected ${expectedNetwork}, got ${network}` });
|
|
3361
|
+
}
|
|
3362
|
+
console.log(`[MoltsPay] /proxy: Verifying payment for ${wallet}...`);
|
|
3363
|
+
const verifyResult = await this.registry.verify(payment, requirements);
|
|
3364
|
+
if (!verifyResult.valid) {
|
|
3365
|
+
return this.sendJson(res, 402, {
|
|
3366
|
+
success: false,
|
|
3367
|
+
error: `Payment verification failed: ${verifyResult.error}`,
|
|
3368
|
+
facilitator: verifyResult.facilitator
|
|
3369
|
+
});
|
|
3370
|
+
}
|
|
3371
|
+
console.log(`[MoltsPay] /proxy: Verified by ${verifyResult.facilitator}`);
|
|
3372
|
+
const { execute, service, params } = body;
|
|
3373
|
+
if (execute && service) {
|
|
3374
|
+
const skill = this.skills.get(service);
|
|
3375
|
+
if (!skill) {
|
|
3376
|
+
console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
|
|
3377
|
+
return this.sendJson(res, 404, {
|
|
3378
|
+
success: false,
|
|
3379
|
+
paymentSettled: false,
|
|
3380
|
+
error: `Service not found: ${service}`
|
|
3381
|
+
});
|
|
3382
|
+
}
|
|
3383
|
+
const isSolana = isSolanaNetwork(network);
|
|
3384
|
+
let settlement2 = null;
|
|
3385
|
+
if (isSolana) {
|
|
3386
|
+
console.log(`[MoltsPay] /proxy: Solana detected - settling payment FIRST`);
|
|
3387
|
+
try {
|
|
3388
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
3389
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
3390
|
+
if (!settlement2.success) {
|
|
3391
|
+
console.error(`[MoltsPay] /proxy: Solana settlement failed: ${settlement2.error}`);
|
|
3392
|
+
return this.sendJson(res, 402, {
|
|
3393
|
+
success: false,
|
|
3394
|
+
paymentSettled: false,
|
|
3395
|
+
error: `Payment settlement failed: ${settlement2.error || "Unknown error"}`
|
|
3396
|
+
});
|
|
3397
|
+
}
|
|
3398
|
+
} catch (err) {
|
|
3399
|
+
console.error("[MoltsPay] /proxy: Solana settlement failed:", err.message);
|
|
3400
|
+
return this.sendJson(res, 402, {
|
|
3401
|
+
success: false,
|
|
3402
|
+
paymentSettled: false,
|
|
3403
|
+
error: `Payment settlement failed: ${err.message}`
|
|
3404
|
+
});
|
|
3405
|
+
}
|
|
3406
|
+
} else {
|
|
3407
|
+
console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
|
|
3408
|
+
}
|
|
3409
|
+
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
3410
|
+
let result;
|
|
3411
|
+
try {
|
|
3412
|
+
result = await Promise.race([
|
|
3413
|
+
skill.handler(params || {}),
|
|
3414
|
+
new Promise(
|
|
3415
|
+
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
3416
|
+
)
|
|
3417
|
+
]);
|
|
3418
|
+
console.log(`[MoltsPay] /proxy: Skill succeeded`);
|
|
3419
|
+
} catch (err) {
|
|
3420
|
+
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}`);
|
|
3421
|
+
return this.sendJson(res, 500, {
|
|
3422
|
+
success: false,
|
|
3423
|
+
paymentSettled: isSolana ? true : false,
|
|
3424
|
+
error: `Service execution failed: ${err.message}`,
|
|
3425
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
3426
|
+
});
|
|
3427
|
+
}
|
|
3428
|
+
if (!isSolana) {
|
|
3429
|
+
console.log(`[MoltsPay] /proxy: Settling payment...`);
|
|
3430
|
+
try {
|
|
3431
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
3432
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
3433
|
+
} catch (err) {
|
|
3434
|
+
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
3435
|
+
return this.sendJson(res, 200, {
|
|
3436
|
+
success: true,
|
|
3437
|
+
verified: true,
|
|
3438
|
+
settled: false,
|
|
3439
|
+
settlementError: err.message,
|
|
3440
|
+
from: payment.payload?.authorization?.from,
|
|
3441
|
+
paidTo: wallet,
|
|
3442
|
+
amount: amountNum,
|
|
3443
|
+
currency: currency || "USDC",
|
|
3444
|
+
memo,
|
|
3445
|
+
result
|
|
3446
|
+
});
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
return this.sendJson(res, 200, {
|
|
3450
|
+
success: true,
|
|
3451
|
+
verified: true,
|
|
3452
|
+
settled: settlement2?.success || false,
|
|
3453
|
+
txHash: settlement2?.transaction,
|
|
3454
|
+
from: payment.payload?.authorization?.from,
|
|
3455
|
+
paidTo: wallet,
|
|
3456
|
+
amount: amountNum,
|
|
3457
|
+
currency: currency || "USDC",
|
|
3458
|
+
facilitator: settlement2?.facilitator,
|
|
3459
|
+
memo,
|
|
3460
|
+
result
|
|
3461
|
+
});
|
|
3462
|
+
}
|
|
3463
|
+
console.log(`[MoltsPay] /proxy: Settling payment (no execution)...`);
|
|
3464
|
+
let settlement = null;
|
|
3465
|
+
try {
|
|
3466
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
3467
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
3468
|
+
} catch (err) {
|
|
3469
|
+
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
3470
|
+
return this.sendJson(res, 500, {
|
|
3471
|
+
success: false,
|
|
3472
|
+
error: `Settlement failed: ${err.message}`
|
|
3473
|
+
});
|
|
3474
|
+
}
|
|
3475
|
+
this.sendJson(res, 200, {
|
|
3476
|
+
success: true,
|
|
3477
|
+
verified: true,
|
|
3478
|
+
settled: settlement?.success || false,
|
|
3479
|
+
txHash: settlement?.transaction,
|
|
3480
|
+
from: payment.payload?.authorization?.from,
|
|
3481
|
+
// Buyer's wallet address
|
|
3482
|
+
paidTo: wallet,
|
|
3483
|
+
amount: amountNum,
|
|
3484
|
+
currency: currency || "USDC",
|
|
3485
|
+
facilitator: settlement?.facilitator,
|
|
3486
|
+
memo
|
|
3487
|
+
});
|
|
3488
|
+
}
|
|
3489
|
+
/**
|
|
3490
|
+
* Handle MPP payment flow for /proxy endpoint (tempo_moderato chain)
|
|
3491
|
+
*/
|
|
3492
|
+
async handleProxyMPP(body, config, authHeader, res) {
|
|
3493
|
+
const { wallet, amount, memo, serviceId } = body;
|
|
3494
|
+
const amountNum = parseFloat(amount);
|
|
3495
|
+
const amountInUnits = Math.floor(amountNum * 1e6).toString();
|
|
3496
|
+
if (!authHeader || !authHeader.toLowerCase().startsWith("payment ")) {
|
|
3497
|
+
const challengeId = this.generateChallengeId();
|
|
3498
|
+
const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
|
|
3499
|
+
const mppRequest = {
|
|
3500
|
+
amount: amountInUnits,
|
|
3501
|
+
currency: tokenAddress,
|
|
3502
|
+
methodDetails: {
|
|
3503
|
+
chainId: 42431,
|
|
3504
|
+
feePayer: true
|
|
3505
|
+
},
|
|
3506
|
+
recipient: wallet
|
|
3507
|
+
};
|
|
3508
|
+
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
3509
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
3510
|
+
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
3511
|
+
res.writeHead(402, {
|
|
3512
|
+
"Content-Type": "application/problem+json",
|
|
3513
|
+
[MPP_WWW_AUTH_HEADER]: wwwAuth
|
|
3514
|
+
});
|
|
3515
|
+
res.end(JSON.stringify({
|
|
3516
|
+
type: "https://paymentauth.org/problems/payment-required",
|
|
3517
|
+
title: "Payment Required",
|
|
3518
|
+
status: 402,
|
|
3519
|
+
detail: `Payment is required (${config.name}).`,
|
|
3520
|
+
service: serviceId || "proxy",
|
|
3521
|
+
price: amountNum,
|
|
3522
|
+
currency: "USDC"
|
|
3523
|
+
}, null, 2));
|
|
3524
|
+
return;
|
|
3525
|
+
}
|
|
3526
|
+
const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
|
|
3527
|
+
if (!credentialMatch) {
|
|
3528
|
+
return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
|
|
2176
3529
|
}
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
3530
|
+
let mppCredential;
|
|
3531
|
+
try {
|
|
3532
|
+
const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
3533
|
+
const decoded = Buffer.from(base64, "base64").toString("utf-8");
|
|
3534
|
+
mppCredential = JSON.parse(decoded);
|
|
3535
|
+
} catch (err) {
|
|
3536
|
+
console.error("[MoltsPay] /proxy MPP: Failed to parse credential:", err);
|
|
3537
|
+
return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
|
|
2181
3538
|
}
|
|
2182
|
-
|
|
2183
|
-
if (
|
|
2184
|
-
|
|
3539
|
+
let txHash;
|
|
3540
|
+
if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
|
|
3541
|
+
txHash = mppCredential.payload.hash;
|
|
3542
|
+
} else {
|
|
3543
|
+
return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
|
|
2185
3544
|
}
|
|
2186
|
-
console.log(`[MoltsPay] /proxy: Verifying
|
|
2187
|
-
const
|
|
2188
|
-
|
|
3545
|
+
console.log(`[MoltsPay] /proxy MPP: Verifying tx ${txHash} on Tempo...`);
|
|
3546
|
+
const requirements = this.buildPaymentRequirements(config, "eip155:42431", wallet, "USDC");
|
|
3547
|
+
const paymentPayload = {
|
|
3548
|
+
x402Version: X402_VERSION3,
|
|
3549
|
+
scheme: "exact",
|
|
3550
|
+
network: "eip155:42431",
|
|
3551
|
+
payload: { txHash, chainId: 42431 }
|
|
3552
|
+
};
|
|
3553
|
+
const verification = await this.registry.verify(paymentPayload, requirements);
|
|
3554
|
+
if (!verification.valid) {
|
|
2189
3555
|
return this.sendJson(res, 402, {
|
|
2190
|
-
|
|
2191
|
-
error: `Payment verification failed: ${verifyResult.error}`,
|
|
2192
|
-
facilitator: verifyResult.facilitator
|
|
3556
|
+
error: `Payment verification failed: ${verification.error}`
|
|
2193
3557
|
});
|
|
2194
3558
|
}
|
|
2195
|
-
console.log(`[MoltsPay] /proxy:
|
|
3559
|
+
console.log(`[MoltsPay] /proxy MPP: Payment verified by ${verification.facilitator}`);
|
|
2196
3560
|
const { execute, service, params } = body;
|
|
2197
3561
|
if (execute && service) {
|
|
2198
|
-
console.log(`[MoltsPay] /proxy: Executing skill
|
|
3562
|
+
console.log(`[MoltsPay] /proxy MPP: Executing skill: ${service}`);
|
|
2199
3563
|
const skill = this.skills.get(service);
|
|
2200
3564
|
if (!skill) {
|
|
2201
|
-
console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
|
|
2202
3565
|
return this.sendJson(res, 404, {
|
|
2203
3566
|
success: false,
|
|
2204
|
-
paymentSettled:
|
|
3567
|
+
paymentSettled: true,
|
|
3568
|
+
// Payment already happened on Tempo
|
|
2205
3569
|
error: `Service not found: ${service}`
|
|
2206
3570
|
});
|
|
2207
3571
|
}
|
|
@@ -2214,73 +3578,36 @@ var MoltsPayServer = class {
|
|
|
2214
3578
|
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
2215
3579
|
)
|
|
2216
3580
|
]);
|
|
2217
|
-
console.log(`[MoltsPay] /proxy: Skill succeeded, now settling payment...`);
|
|
2218
3581
|
} catch (err) {
|
|
2219
|
-
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}
|
|
3582
|
+
console.error(`[MoltsPay] /proxy MPP: Skill failed: ${err.message}`);
|
|
2220
3583
|
return this.sendJson(res, 500, {
|
|
2221
3584
|
success: false,
|
|
2222
|
-
paymentSettled:
|
|
3585
|
+
paymentSettled: true,
|
|
2223
3586
|
error: `Service execution failed: ${err.message}`
|
|
2224
3587
|
});
|
|
2225
3588
|
}
|
|
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
3589
|
return this.sendJson(res, 200, {
|
|
2247
3590
|
success: true,
|
|
2248
3591
|
verified: true,
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
from: payment.payload?.authorization?.from,
|
|
2252
|
-
// Buyer's wallet address
|
|
3592
|
+
txHash,
|
|
3593
|
+
chain: "tempo_moderato",
|
|
2253
3594
|
paidTo: wallet,
|
|
2254
3595
|
amount: amountNum,
|
|
2255
|
-
currency:
|
|
2256
|
-
facilitator:
|
|
3596
|
+
currency: "USDC",
|
|
3597
|
+
facilitator: verification.facilitator,
|
|
2257
3598
|
memo,
|
|
2258
3599
|
result
|
|
2259
3600
|
});
|
|
2260
3601
|
}
|
|
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
3602
|
this.sendJson(res, 200, {
|
|
2274
3603
|
success: true,
|
|
2275
3604
|
verified: true,
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
from: payment.payload?.authorization?.from,
|
|
2279
|
-
// Buyer's wallet address
|
|
3605
|
+
txHash,
|
|
3606
|
+
chain: "tempo_moderato",
|
|
2280
3607
|
paidTo: wallet,
|
|
2281
3608
|
amount: amountNum,
|
|
2282
|
-
currency:
|
|
2283
|
-
facilitator:
|
|
3609
|
+
currency: "USDC",
|
|
3610
|
+
facilitator: verification.facilitator,
|
|
2284
3611
|
memo
|
|
2285
3612
|
});
|
|
2286
3613
|
}
|
|
@@ -2295,7 +3622,7 @@ var MoltsPayServer = class {
|
|
|
2295
3622
|
const tokenAddresses = TOKEN_ADDRESSES[networkId] || TOKEN_ADDRESSES[this.networkId] || {};
|
|
2296
3623
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
2297
3624
|
const tokenDomain = getTokenDomain(networkId, selectedToken);
|
|
2298
|
-
|
|
3625
|
+
const requirements = {
|
|
2299
3626
|
scheme: "exact",
|
|
2300
3627
|
network: networkId,
|
|
2301
3628
|
asset: tokenAddress,
|
|
@@ -2305,6 +3632,17 @@ var MoltsPayServer = class {
|
|
|
2305
3632
|
maxTimeoutSeconds: 300,
|
|
2306
3633
|
extra: tokenDomain
|
|
2307
3634
|
};
|
|
3635
|
+
if (networkId === "eip155:56" || networkId === "eip155:97") {
|
|
3636
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
3637
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
3638
|
+
if (spenderAddress) {
|
|
3639
|
+
requirements.extra = {
|
|
3640
|
+
...requirements.extra || {},
|
|
3641
|
+
bnbSpender: spenderAddress
|
|
3642
|
+
};
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
return requirements;
|
|
2308
3646
|
}
|
|
2309
3647
|
/**
|
|
2310
3648
|
* Return 402 with x402 payment requirements for proxy endpoint
|
|
@@ -2354,14 +3692,14 @@ if (!globalThis.crypto) {
|
|
|
2354
3692
|
}
|
|
2355
3693
|
function getVersion() {
|
|
2356
3694
|
const locations = [
|
|
2357
|
-
(0,
|
|
2358
|
-
(0,
|
|
2359
|
-
(0,
|
|
3695
|
+
(0, import_path3.join)(__dirname, "../../package.json"),
|
|
3696
|
+
(0, import_path3.join)(__dirname, "../package.json"),
|
|
3697
|
+
(0, import_path3.join)(process.cwd(), "node_modules/moltspay/package.json")
|
|
2360
3698
|
];
|
|
2361
3699
|
for (const loc of locations) {
|
|
2362
3700
|
try {
|
|
2363
|
-
if ((0,
|
|
2364
|
-
const pkg = JSON.parse((0,
|
|
3701
|
+
if ((0, import_fs5.existsSync)(loc)) {
|
|
3702
|
+
const pkg = JSON.parse((0, import_fs5.readFileSync)(loc, "utf-8"));
|
|
2365
3703
|
if (pkg.name === "moltspay") return pkg.version;
|
|
2366
3704
|
}
|
|
2367
3705
|
} catch {
|
|
@@ -2369,11 +3707,94 @@ function getVersion() {
|
|
|
2369
3707
|
}
|
|
2370
3708
|
return "0.0.0";
|
|
2371
3709
|
}
|
|
3710
|
+
var BNB_SPONSOR_KEY = process.env.MOLTSPAY_BNB_SPONSOR_KEY;
|
|
3711
|
+
var BNB_SPENDER_ADDRESS = process.env.MOLTSPAY_BNB_SPENDER || "0xEBB45208D806A0c73F9673E0c5713FF720DD6b79";
|
|
3712
|
+
var ERC20_APPROVE_ABI = [
|
|
3713
|
+
"function approve(address spender, uint256 amount) returns (bool)",
|
|
3714
|
+
"function allowance(address owner, address spender) view returns (uint256)"
|
|
3715
|
+
];
|
|
3716
|
+
async function setupBNBApprovals(client, chain, spenderAddress, sponsorGas = false) {
|
|
3717
|
+
const chainConfig = CHAINS[chain];
|
|
3718
|
+
const provider = new import_ethers2.ethers.JsonRpcProvider(chainConfig.rpc);
|
|
3719
|
+
const wallet = client.getWallet();
|
|
3720
|
+
if (!wallet) {
|
|
3721
|
+
console.log(" \u274C No wallet found");
|
|
3722
|
+
return;
|
|
3723
|
+
}
|
|
3724
|
+
const signer = wallet.connect(provider);
|
|
3725
|
+
console.log(` Spender: ${spenderAddress}`);
|
|
3726
|
+
let bnbBalance = await provider.getBalance(wallet.address);
|
|
3727
|
+
const minGasRequired = import_ethers2.ethers.parseEther("0.0005");
|
|
3728
|
+
if (bnbBalance < minGasRequired) {
|
|
3729
|
+
if (sponsorGas && BNB_SPONSOR_KEY) {
|
|
3730
|
+
console.log(" \u23F3 Sponsoring BNB gas for approvals...");
|
|
3731
|
+
try {
|
|
3732
|
+
const sponsorWallet = new import_ethers2.ethers.Wallet(BNB_SPONSOR_KEY, provider);
|
|
3733
|
+
const tx = await sponsorWallet.sendTransaction({
|
|
3734
|
+
to: wallet.address,
|
|
3735
|
+
value: import_ethers2.ethers.parseEther("0.001")
|
|
3736
|
+
});
|
|
3737
|
+
await tx.wait();
|
|
3738
|
+
console.log(` \u2705 Sponsored 0.001 BNB (tx: ${tx.hash.slice(0, 10)}...)`);
|
|
3739
|
+
bnbBalance = await provider.getBalance(wallet.address);
|
|
3740
|
+
} catch (err) {
|
|
3741
|
+
console.log(` \u26A0\uFE0F Gas sponsorship failed: ${err.message}`);
|
|
3742
|
+
console.log(` \u{1F4A1} Get testnet BNB: https://testnet.bnbchain.org/faucet-smart`);
|
|
3743
|
+
return;
|
|
3744
|
+
}
|
|
3745
|
+
} else {
|
|
3746
|
+
console.log(` \u26A0\uFE0F Need BNB for gas (~0.0005 BNB)`);
|
|
3747
|
+
console.log(` \u{1F4A1} Run: npx moltspay faucet --chain bnb_testnet`);
|
|
3748
|
+
console.log(` Then run: npx moltspay approve --chain ${chain} --spender ${spenderAddress}`);
|
|
3749
|
+
return;
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
for (const tokenSymbol of ["USDT", "USDC"]) {
|
|
3753
|
+
const tokenConfig = chainConfig.tokens[tokenSymbol];
|
|
3754
|
+
const tokenContract = new import_ethers2.ethers.Contract(tokenConfig.address, ERC20_APPROVE_ABI, signer);
|
|
3755
|
+
const allowance = await tokenContract.allowance(wallet.address, spenderAddress);
|
|
3756
|
+
if (allowance > 0n) {
|
|
3757
|
+
console.log(` \u2705 ${tokenSymbol}: already approved for ${spenderAddress.slice(0, 10)}...`);
|
|
3758
|
+
continue;
|
|
3759
|
+
}
|
|
3760
|
+
console.log(` \u23F3 Approving ${tokenSymbol}...`);
|
|
3761
|
+
try {
|
|
3762
|
+
const tx = await tokenContract.approve(spenderAddress, import_ethers2.ethers.MaxUint256);
|
|
3763
|
+
await tx.wait();
|
|
3764
|
+
console.log(` \u2705 ${tokenSymbol}: approved (tx: ${tx.hash.slice(0, 10)}...)`);
|
|
3765
|
+
} catch (err) {
|
|
3766
|
+
console.log(` \u274C ${tokenSymbol}: approval failed - ${err.message}`);
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
console.log("");
|
|
3770
|
+
}
|
|
3771
|
+
async function checkBNBApprovals(address, chain, configDir = DEFAULT_CONFIG_DIR2) {
|
|
3772
|
+
const chainConfig = CHAINS[chain];
|
|
3773
|
+
const provider = new import_ethers2.ethers.JsonRpcProvider(chainConfig.rpc);
|
|
3774
|
+
let spenderAddress = null;
|
|
3775
|
+
try {
|
|
3776
|
+
const walletPath = (0, import_path3.join)(configDir, "wallet.json");
|
|
3777
|
+
const walletData = JSON.parse((0, import_fs5.readFileSync)(walletPath, "utf-8"));
|
|
3778
|
+
spenderAddress = walletData.approvals?.[chain] || null;
|
|
3779
|
+
} catch {
|
|
3780
|
+
}
|
|
3781
|
+
const result = { usdt: false, usdc: false, spender: spenderAddress };
|
|
3782
|
+
if (!spenderAddress) {
|
|
3783
|
+
return result;
|
|
3784
|
+
}
|
|
3785
|
+
for (const tokenSymbol of ["USDT", "USDC"]) {
|
|
3786
|
+
const tokenConfig = chainConfig.tokens[tokenSymbol];
|
|
3787
|
+
const tokenContract = new import_ethers2.ethers.Contract(tokenConfig.address, ERC20_APPROVE_ABI, provider);
|
|
3788
|
+
const allowance = await tokenContract.allowance(address, spenderAddress);
|
|
3789
|
+
result[tokenSymbol.toLowerCase()] = allowance > 0n;
|
|
3790
|
+
}
|
|
3791
|
+
return result;
|
|
3792
|
+
}
|
|
2372
3793
|
var program = new import_commander.Command();
|
|
2373
|
-
var
|
|
2374
|
-
var PID_FILE = (0,
|
|
2375
|
-
if (!(0,
|
|
2376
|
-
(0,
|
|
3794
|
+
var DEFAULT_CONFIG_DIR2 = (0, import_path3.join)((0, import_os3.homedir)(), ".moltspay");
|
|
3795
|
+
var PID_FILE = (0, import_path3.join)(DEFAULT_CONFIG_DIR2, "server.pid");
|
|
3796
|
+
if (!(0, import_fs5.existsSync)(DEFAULT_CONFIG_DIR2)) {
|
|
3797
|
+
(0, import_fs5.mkdirSync)(DEFAULT_CONFIG_DIR2, { recursive: true });
|
|
2377
3798
|
}
|
|
2378
3799
|
function prompt(question) {
|
|
2379
3800
|
const rl = readline.createInterface({
|
|
@@ -2388,19 +3809,49 @@ function prompt(question) {
|
|
|
2388
3809
|
});
|
|
2389
3810
|
}
|
|
2390
3811
|
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
|
-
}
|
|
3812
|
+
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
3813
|
let chain = options.chain;
|
|
2399
|
-
const
|
|
3814
|
+
const supportedEVMChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
|
|
3815
|
+
const supportedSolanaChains = ["solana", "solana_devnet"];
|
|
3816
|
+
const supportedChains = [...supportedEVMChains, ...supportedSolanaChains];
|
|
2400
3817
|
if (!supportedChains.includes(chain)) {
|
|
2401
3818
|
console.error(`\u274C Unknown chain: ${chain}. Supported: ${supportedChains.join(", ")}`);
|
|
2402
3819
|
process.exit(1);
|
|
2403
3820
|
}
|
|
3821
|
+
if (supportedSolanaChains.includes(chain)) {
|
|
3822
|
+
console.log("\n\u{1F7E3} Solana Wallet Setup\n");
|
|
3823
|
+
if (solanaWalletExists(options.configDir)) {
|
|
3824
|
+
const existingAddress = getSolanaAddress(options.configDir);
|
|
3825
|
+
console.log(`\u26A0\uFE0F Solana wallet already exists: ${existingAddress}`);
|
|
3826
|
+
console.log(` Config dir: ${options.configDir}`);
|
|
3827
|
+
return;
|
|
3828
|
+
}
|
|
3829
|
+
console.log("Creating Solana wallet...");
|
|
3830
|
+
const keypair = createSolanaWallet(options.configDir);
|
|
3831
|
+
const address = keypair.publicKey.toBase58();
|
|
3832
|
+
console.log(`
|
|
3833
|
+
\u2705 Solana wallet created: ${address}`);
|
|
3834
|
+
console.log(`
|
|
3835
|
+
\u{1F4C1} Config saved to: ${(0, import_path3.join)(options.configDir, "wallet-solana.json")}`);
|
|
3836
|
+
console.log(`
|
|
3837
|
+
\u26A0\uFE0F IMPORTANT: Back up your wallet file!`);
|
|
3838
|
+
console.log(` This file contains your private key!
|
|
3839
|
+
`);
|
|
3840
|
+
if (chain === "solana_devnet") {
|
|
3841
|
+
console.log("\u{1F4A1} Get testnet tokens:");
|
|
3842
|
+
console.log(" npx moltspay faucet --chain solana_devnet\n");
|
|
3843
|
+
} else {
|
|
3844
|
+
console.log(`\u{1F4B0} Fund your wallet with USDC on Solana to start (gasless - no SOL needed).
|
|
3845
|
+
`);
|
|
3846
|
+
}
|
|
3847
|
+
return;
|
|
3848
|
+
}
|
|
3849
|
+
console.log("\n\u{1F510} MoltsPay Client Setup\n");
|
|
3850
|
+
if ((0, import_fs5.existsSync)((0, import_path3.join)(options.configDir, "wallet.json"))) {
|
|
3851
|
+
console.log('\u26A0\uFE0F EVM wallet already initialized. Use "moltspay config" to update settings.');
|
|
3852
|
+
console.log(` Config dir: ${options.configDir}`);
|
|
3853
|
+
return;
|
|
3854
|
+
}
|
|
2404
3855
|
let maxPerTx = options.maxPerTx ? parseFloat(options.maxPerTx) : null;
|
|
2405
3856
|
let maxPerDay = options.maxPerDay ? parseFloat(options.maxPerDay) : null;
|
|
2406
3857
|
if (!maxPerTx) {
|
|
@@ -2422,13 +3873,21 @@ program.command("init").description("Initialize MoltsPay client (create wallet,
|
|
|
2422
3873
|
console.log(`
|
|
2423
3874
|
\u{1F4C1} Config saved to: ${result.configDir}`);
|
|
2424
3875
|
console.log(`
|
|
2425
|
-
\u26A0\uFE0F IMPORTANT: Back up ${(0,
|
|
3876
|
+
\u26A0\uFE0F IMPORTANT: Back up ${(0, import_path3.join)(result.configDir, "wallet.json")}`);
|
|
2426
3877
|
console.log(` This file contains your private key!
|
|
2427
3878
|
`);
|
|
3879
|
+
if (chain === "bnb" || chain === "bnb_testnet") {
|
|
3880
|
+
console.log("\u{1F4CB} Setting up BNB chain approvals...\n");
|
|
3881
|
+
console.log(" \u2139\uFE0F Using default spender. For other services, run:");
|
|
3882
|
+
console.log(` npx moltspay approve --chain ${chain} --spender <address>
|
|
3883
|
+
`);
|
|
3884
|
+
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
3885
|
+
await setupBNBApprovals(client, chain, BNB_SPENDER_ADDRESS, true);
|
|
3886
|
+
}
|
|
2428
3887
|
console.log(`\u{1F4B0} Fund your wallet with USDC on ${chain} to start using services.
|
|
2429
3888
|
`);
|
|
2430
3889
|
});
|
|
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",
|
|
3890
|
+
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
3891
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
2433
3892
|
if (!client.isInitialized) {
|
|
2434
3893
|
console.log("\u274C Not initialized. Run: npx moltspay init");
|
|
@@ -2463,25 +3922,40 @@ program.command("config").description("Update MoltsPay settings").option("--max-
|
|
|
2463
3922
|
}
|
|
2464
3923
|
}
|
|
2465
3924
|
});
|
|
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
|
|
3925
|
+
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
3926
|
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
3927
|
const amount = parseFloat(amountStr);
|
|
2473
3928
|
if (isNaN(amount) || amount < 5) {
|
|
2474
3929
|
console.log("\u274C Minimum $5.");
|
|
2475
3930
|
return;
|
|
2476
3931
|
}
|
|
2477
3932
|
const chain = options.chain?.toLowerCase() || "base";
|
|
2478
|
-
if (!["base", "polygon", "base_sepolia"].includes(chain)) {
|
|
2479
|
-
console.log("\u274C Invalid chain. Use: base, polygon, or
|
|
3933
|
+
if (!["base", "polygon", "base_sepolia", "solana", "bnb", "bnb_testnet"].includes(chain)) {
|
|
3934
|
+
console.log("\u274C Invalid chain. Use: base, polygon, solana, base_sepolia, bnb, or bnb_testnet");
|
|
2480
3935
|
return;
|
|
2481
3936
|
}
|
|
3937
|
+
let walletAddress;
|
|
3938
|
+
if (chain === "solana") {
|
|
3939
|
+
const solanaWallet = loadSolanaWallet(options.configDir || DEFAULT_CONFIG_DIR2);
|
|
3940
|
+
if (!solanaWallet) {
|
|
3941
|
+
console.log("\u274C No Solana wallet found. Run: npx moltspay init --chain solana");
|
|
3942
|
+
return;
|
|
3943
|
+
}
|
|
3944
|
+
walletAddress = getSolanaAddress(options.configDir || DEFAULT_CONFIG_DIR2) || "";
|
|
3945
|
+
if (!walletAddress) {
|
|
3946
|
+
console.log("\u274C Could not get Solana wallet address.");
|
|
3947
|
+
return;
|
|
3948
|
+
}
|
|
3949
|
+
} else {
|
|
3950
|
+
if (!client.isInitialized) {
|
|
3951
|
+
console.log("\u274C Not initialized. Run: npx moltspay init");
|
|
3952
|
+
return;
|
|
3953
|
+
}
|
|
3954
|
+
walletAddress = client.address;
|
|
3955
|
+
}
|
|
2482
3956
|
if (chain === "base_sepolia") {
|
|
2483
3957
|
console.log("\n\u{1F9EA} Testnet Funding\n");
|
|
2484
|
-
console.log(` Wallet: ${
|
|
3958
|
+
console.log(` Wallet: ${walletAddress}`);
|
|
2485
3959
|
console.log(` Chain: Base Sepolia (testnet)
|
|
2486
3960
|
`);
|
|
2487
3961
|
console.log("\u{1F4A1} Use the MoltsPay faucet to get free testnet USDC:\n");
|
|
@@ -2489,9 +3963,36 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
|
|
|
2489
3963
|
console.log(" Or get from Circle Faucet: https://faucet.circle.com/\n");
|
|
2490
3964
|
return;
|
|
2491
3965
|
}
|
|
3966
|
+
if (chain === "bnb_testnet") {
|
|
3967
|
+
console.log("\n\u{1F9EA} BNB Testnet Funding\n");
|
|
3968
|
+
console.log(` Wallet: ${walletAddress}`);
|
|
3969
|
+
console.log(` Chain: BNB Testnet
|
|
3970
|
+
`);
|
|
3971
|
+
console.log("\u{1F4A1} Use the MoltsPay faucet to get testnet USDC + tBNB:\n");
|
|
3972
|
+
console.log(" npx moltspay faucet --chain bnb_testnet\n");
|
|
3973
|
+
console.log(" This gives you:\n");
|
|
3974
|
+
console.log(" \u2022 1 USDC (testnet) for payments");
|
|
3975
|
+
console.log(" \u2022 0.001 tBNB for gas (first approval tx)\n");
|
|
3976
|
+
return;
|
|
3977
|
+
}
|
|
3978
|
+
if (chain === "bnb") {
|
|
3979
|
+
console.log("\n\u{1F4CB} BNB Chain Funding\n");
|
|
3980
|
+
console.log(` Wallet: ${walletAddress}
|
|
3981
|
+
`);
|
|
3982
|
+
console.log(" To use MoltsPay on BNB Chain, you need:\n");
|
|
3983
|
+
console.log(" 1. USDC for payments");
|
|
3984
|
+
console.log(" \u2192 Withdraw from Binance/exchange to your wallet address\n");
|
|
3985
|
+
console.log(" 2. Small amount of BNB for gas (~0.001 BNB / ~$0.60)");
|
|
3986
|
+
console.log(" \u2192 First approval transaction requires gas");
|
|
3987
|
+
console.log(" \u2192 After approval, all payments are gasless\n");
|
|
3988
|
+
console.log(" \u{1F4A1} Tip: Most exchanges include BNB dust when you withdraw to BNB Chain\n");
|
|
3989
|
+
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");
|
|
3990
|
+
console.log(" After funding, check status: npx moltspay status\n");
|
|
3991
|
+
return;
|
|
3992
|
+
}
|
|
2492
3993
|
console.log("\n\u{1F4B3} Fund your agent wallet\n");
|
|
2493
|
-
console.log(` Wallet: ${
|
|
2494
|
-
console.log(` Chain: ${chain}`);
|
|
3994
|
+
console.log(` Wallet: ${walletAddress}`);
|
|
3995
|
+
console.log(` Chain: ${chain === "solana" ? "Solana" : chain}`);
|
|
2495
3996
|
console.log(` Amount: $${amount.toFixed(2)}
|
|
2496
3997
|
`);
|
|
2497
3998
|
try {
|
|
@@ -2500,7 +4001,7 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
|
|
|
2500
4001
|
method: "POST",
|
|
2501
4002
|
headers: { "Content-Type": "application/json" },
|
|
2502
4003
|
body: JSON.stringify({
|
|
2503
|
-
address:
|
|
4004
|
+
address: walletAddress,
|
|
2504
4005
|
amount,
|
|
2505
4006
|
chain
|
|
2506
4007
|
})
|
|
@@ -2518,11 +4019,92 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
|
|
|
2518
4019
|
console.log(`\u274C ${error.message}`);
|
|
2519
4020
|
}
|
|
2520
4021
|
});
|
|
2521
|
-
program.command("
|
|
4022
|
+
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) => {
|
|
4023
|
+
const chain = options.chain;
|
|
4024
|
+
if (chain !== "bnb" && chain !== "bnb_testnet") {
|
|
4025
|
+
console.log("\u274C approve command is only for BNB chains (bnb or bnb_testnet)");
|
|
4026
|
+
return;
|
|
4027
|
+
}
|
|
4028
|
+
if (!options.spender.match(/^0x[a-fA-F0-9]{40}$/)) {
|
|
4029
|
+
console.log("\u274C Invalid spender address format");
|
|
4030
|
+
return;
|
|
4031
|
+
}
|
|
4032
|
+
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
4033
|
+
if (!client.isInitialized) {
|
|
4034
|
+
console.log("\u274C Wallet not initialized. Run: npx moltspay init --chain " + chain);
|
|
4035
|
+
return;
|
|
4036
|
+
}
|
|
4037
|
+
console.log(`
|
|
4038
|
+
\u{1F510} Approving spender for ${chain}...
|
|
4039
|
+
`);
|
|
4040
|
+
await setupBNBApprovals(client, chain, options.spender, false);
|
|
4041
|
+
const walletPath = (0, import_path3.join)(options.configDir || DEFAULT_CONFIG_DIR2, "wallet.json");
|
|
4042
|
+
try {
|
|
4043
|
+
const walletData = JSON.parse((0, import_fs5.readFileSync)(walletPath, "utf-8"));
|
|
4044
|
+
walletData.approvals = walletData.approvals || {};
|
|
4045
|
+
walletData.approvals[chain] = options.spender;
|
|
4046
|
+
(0, import_fs5.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2));
|
|
4047
|
+
console.log(`\u2705 Approval complete! Spender saved for ${chain}.
|
|
4048
|
+
`);
|
|
4049
|
+
} catch (err) {
|
|
4050
|
+
console.log("\u2705 Approval complete!\n");
|
|
4051
|
+
console.log("\u26A0\uFE0F Could not save spender to wallet config");
|
|
4052
|
+
}
|
|
4053
|
+
});
|
|
4054
|
+
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
4055
|
let address = options.address;
|
|
2523
4056
|
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
|
|
4057
|
+
if (!["base_sepolia", "tempo_moderato", "bnb_testnet", "solana_devnet"].includes(chain)) {
|
|
4058
|
+
console.log("\u274C Invalid chain. Use: base_sepolia, tempo_moderato, bnb_testnet, or solana_devnet");
|
|
4059
|
+
return;
|
|
4060
|
+
}
|
|
4061
|
+
if (chain === "solana_devnet") {
|
|
4062
|
+
if (!address) {
|
|
4063
|
+
address = getSolanaAddress(options.configDir);
|
|
4064
|
+
if (!address) {
|
|
4065
|
+
console.log("\u274C No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
|
|
4066
|
+
return;
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
if (!isValidSolanaAddress(address)) {
|
|
4070
|
+
console.log("\u274C Invalid Solana address");
|
|
4071
|
+
return;
|
|
4072
|
+
}
|
|
4073
|
+
console.log("\n\u{1F6B0} Solana Devnet Faucet (Gasless Mode)\n");
|
|
4074
|
+
console.log(` Address: ${address}
|
|
4075
|
+
`);
|
|
4076
|
+
let usdcSuccess = false;
|
|
4077
|
+
try {
|
|
4078
|
+
console.log(" \u23F3 Requesting 1 USDC from faucet...");
|
|
4079
|
+
const FAUCET_API = process.env.MOLTSPAY_FAUCET_API || "https://moltspay.com/api/v1/faucet";
|
|
4080
|
+
const response = await fetch(FAUCET_API, {
|
|
4081
|
+
method: "POST",
|
|
4082
|
+
headers: { "Content-Type": "application/json" },
|
|
4083
|
+
body: JSON.stringify({ address, chain: "solana_devnet" })
|
|
4084
|
+
});
|
|
4085
|
+
const result = await response.json();
|
|
4086
|
+
if (!response.ok) {
|
|
4087
|
+
console.log(` \u26A0\uFE0F USDC faucet: ${result.error || "Request failed"}`);
|
|
4088
|
+
if (result.hint) console.log(` ${result.hint}`);
|
|
4089
|
+
if (result.retry_after) console.log(` Retry after: ${result.retry_after}`);
|
|
4090
|
+
} else {
|
|
4091
|
+
console.log(` \u2705 Received ${result.amount} USDC!`);
|
|
4092
|
+
console.log(` Transaction: ${result.explorer}`);
|
|
4093
|
+
if (result.faucet_balance) {
|
|
4094
|
+
console.log(` Faucet balance: ${result.faucet_balance} USDC remaining`);
|
|
4095
|
+
}
|
|
4096
|
+
usdcSuccess = true;
|
|
4097
|
+
}
|
|
4098
|
+
} catch (error) {
|
|
4099
|
+
console.log(` \u26A0\uFE0F USDC faucet error: ${error.message}`);
|
|
4100
|
+
}
|
|
4101
|
+
console.log("");
|
|
4102
|
+
if (usdcSuccess) {
|
|
4103
|
+
console.log("\u{1F4A1} Check your balance:");
|
|
4104
|
+
console.log(" npx moltspay status\n");
|
|
4105
|
+
} else {
|
|
4106
|
+
console.log("\u274C Faucet request failed. Try again in a few minutes.\n");
|
|
4107
|
+
}
|
|
2526
4108
|
return;
|
|
2527
4109
|
}
|
|
2528
4110
|
if (!address) {
|
|
@@ -2570,6 +4152,46 @@ program.command("faucet").description("Request testnet tokens from faucet (Base
|
|
|
2570
4152
|
console.log(`\u274C ${error.message}`);
|
|
2571
4153
|
console.log("\n Try Tempo Wallet instead: https://wallet.tempo.xyz\n");
|
|
2572
4154
|
}
|
|
4155
|
+
} else if (chain === "bnb_testnet") {
|
|
4156
|
+
console.log(` Requesting 1 USDC on BNB Testnet...`);
|
|
4157
|
+
console.log(` Address: ${address}
|
|
4158
|
+
`);
|
|
4159
|
+
try {
|
|
4160
|
+
const FAUCET_API = process.env.MOLTSPAY_FAUCET_API || "https://moltspay.com/api/v1/faucet";
|
|
4161
|
+
const response = await fetch(FAUCET_API, {
|
|
4162
|
+
method: "POST",
|
|
4163
|
+
headers: { "Content-Type": "application/json" },
|
|
4164
|
+
body: JSON.stringify({ address, chain: "bnb_testnet" })
|
|
4165
|
+
});
|
|
4166
|
+
const result = await response.json();
|
|
4167
|
+
if (!response.ok) {
|
|
4168
|
+
console.log(`\u274C ${result.error || "Request failed"}`);
|
|
4169
|
+
if (result.hint) console.log(` ${result.hint}`);
|
|
4170
|
+
if (result.retry_after) console.log(` Retry after: ${result.retry_after}`);
|
|
4171
|
+
console.log("\n\u{1F4A1} Alternatively, get tokens manually:");
|
|
4172
|
+
console.log(` 1. Get test BNB: https://www.bnbchain.org/en/testnet-faucet`);
|
|
4173
|
+
console.log(` 2. Select "Peggy Tokens" -> USDC`);
|
|
4174
|
+
console.log(` 3. Enter: ${address}
|
|
4175
|
+
`);
|
|
4176
|
+
return;
|
|
4177
|
+
}
|
|
4178
|
+
console.log(`\u2705 Received ${result.amount} ${result.token || "USDC"} on ${result.chain_name || "BNB Testnet"}!
|
|
4179
|
+
`);
|
|
4180
|
+
console.log(` Transaction: ${result.explorer || `https://testnet.bscscan.com/tx/${result.transaction}`}`);
|
|
4181
|
+
if (result.faucet_balance) {
|
|
4182
|
+
console.log(` Faucet balance: ${result.faucet_balance} USDC`);
|
|
4183
|
+
}
|
|
4184
|
+
console.log("\n\u{1F4A1} Now you can test BNB payments:");
|
|
4185
|
+
console.log(` npx moltspay pay <service-url> <service-id> --chain bnb_testnet
|
|
4186
|
+
`);
|
|
4187
|
+
} catch (error) {
|
|
4188
|
+
console.log(`\u274C ${error.message}`);
|
|
4189
|
+
console.log("\n\u{1F4A1} Get tokens manually:");
|
|
4190
|
+
console.log(` 1. Get test BNB: https://www.bnbchain.org/en/testnet-faucet`);
|
|
4191
|
+
console.log(` 2. Select "Peggy Tokens" -> USDC`);
|
|
4192
|
+
console.log(` 3. Enter: ${address}
|
|
4193
|
+
`);
|
|
4194
|
+
}
|
|
2573
4195
|
} else {
|
|
2574
4196
|
console.log(` Requesting 1 USDC on Base Sepolia...`);
|
|
2575
4197
|
console.log(` Address: ${address}
|
|
@@ -2579,7 +4201,7 @@ program.command("faucet").description("Request testnet tokens from faucet (Base
|
|
|
2579
4201
|
const response = await fetch(FAUCET_API, {
|
|
2580
4202
|
method: "POST",
|
|
2581
4203
|
headers: { "Content-Type": "application/json" },
|
|
2582
|
-
body: JSON.stringify({ address })
|
|
4204
|
+
body: JSON.stringify({ address, chain: "base_sepolia" })
|
|
2583
4205
|
});
|
|
2584
4206
|
const result = await response.json();
|
|
2585
4207
|
if (!response.ok) {
|
|
@@ -2602,7 +4224,7 @@ program.command("faucet").description("Request testnet tokens from faucet (Base
|
|
|
2602
4224
|
}
|
|
2603
4225
|
}
|
|
2604
4226
|
});
|
|
2605
|
-
program.command("status").description("Show wallet status and balance").option("--config-dir <dir>", "Config directory",
|
|
4227
|
+
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
4228
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
2607
4229
|
if (!client.isInitialized) {
|
|
2608
4230
|
if (options.json) {
|
|
@@ -2619,12 +4241,31 @@ program.command("status").description("Show wallet status and balance").option("
|
|
|
2619
4241
|
} catch (err) {
|
|
2620
4242
|
console.error("Warning: Could not fetch balances:", err.message);
|
|
2621
4243
|
}
|
|
4244
|
+
const solanaAddress = getSolanaAddress(options.configDir);
|
|
4245
|
+
let solanaBalances = {};
|
|
4246
|
+
if (solanaAddress) {
|
|
4247
|
+
try {
|
|
4248
|
+
solanaBalances.devnet = await getSolanaBalances(solanaAddress, "solana_devnet");
|
|
4249
|
+
} catch {
|
|
4250
|
+
}
|
|
4251
|
+
try {
|
|
4252
|
+
solanaBalances.mainnet = await getSolanaBalances(solanaAddress, "solana");
|
|
4253
|
+
} catch {
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
2622
4256
|
if (options.json) {
|
|
2623
|
-
|
|
4257
|
+
const output = {
|
|
2624
4258
|
address: client.address,
|
|
2625
4259
|
balances: allBalances,
|
|
2626
4260
|
limits: config.limits
|
|
2627
|
-
}
|
|
4261
|
+
};
|
|
4262
|
+
if (solanaAddress) {
|
|
4263
|
+
output.solana = {
|
|
4264
|
+
address: solanaAddress,
|
|
4265
|
+
balances: solanaBalances
|
|
4266
|
+
};
|
|
4267
|
+
}
|
|
4268
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2628
4269
|
} else {
|
|
2629
4270
|
console.log("\n\u{1F4CA} MoltsPay Wallet Status\n");
|
|
2630
4271
|
console.log(` Address: ${client.address}`);
|
|
@@ -2648,18 +4289,90 @@ program.command("status").description("Show wallet status and balance").option("
|
|
|
2648
4289
|
console.log(` alphaUSD: ${tempo.alphaUSD.toFixed(2)}`);
|
|
2649
4290
|
console.log(` betaUSD: ${tempo.betaUSD.toFixed(2)}`);
|
|
2650
4291
|
console.log(` thetaUSD: ${tempo.thetaUSD.toFixed(2)}`);
|
|
4292
|
+
} else if (chainName === "bnb" || chainName === "bnb_testnet") {
|
|
4293
|
+
const bnbBalance = balance.native;
|
|
4294
|
+
const bnbWarning = bnbBalance < 5e-4 ? " \u26A0\uFE0F Low gas" : "";
|
|
4295
|
+
console.log(` ${chainLabel.padEnd(14)} ${balance.usdc.toFixed(2)} USDC | ${balance.usdt.toFixed(2)} USDT | ${bnbBalance.toFixed(4)} BNB${bnbWarning}`);
|
|
2651
4296
|
} else {
|
|
2652
4297
|
console.log(` ${chainLabel.padEnd(14)} ${balance.usdc.toFixed(2)} USDC | ${balance.usdt.toFixed(2)} USDT`);
|
|
2653
4298
|
}
|
|
2654
4299
|
}
|
|
4300
|
+
const address = client.address;
|
|
4301
|
+
let bnbApprovalStatus = null;
|
|
4302
|
+
let bnbTestnetApprovalStatus = null;
|
|
4303
|
+
try {
|
|
4304
|
+
if (allBalances["bnb"]) {
|
|
4305
|
+
bnbApprovalStatus = await checkBNBApprovals(address, "bnb", options.configDir);
|
|
4306
|
+
}
|
|
4307
|
+
if (allBalances["bnb_testnet"]) {
|
|
4308
|
+
bnbTestnetApprovalStatus = await checkBNBApprovals(address, "bnb_testnet", options.configDir);
|
|
4309
|
+
}
|
|
4310
|
+
} catch {
|
|
4311
|
+
}
|
|
4312
|
+
if (bnbApprovalStatus || bnbTestnetApprovalStatus) {
|
|
4313
|
+
console.log("");
|
|
4314
|
+
console.log(" BNB Approvals (pay-for-success):");
|
|
4315
|
+
if (bnbApprovalStatus) {
|
|
4316
|
+
if (!bnbApprovalStatus.spender) {
|
|
4317
|
+
console.log(" BNB: \u26A0\uFE0F No spender configured");
|
|
4318
|
+
console.log(" \u2514\u2500 Run a payment first, or: npx moltspay approve --chain bnb --spender <address>");
|
|
4319
|
+
} else {
|
|
4320
|
+
const status = bnbApprovalStatus.usdt && bnbApprovalStatus.usdc ? "\u2705" : "\u26A0\uFE0F";
|
|
4321
|
+
const tokens = [
|
|
4322
|
+
bnbApprovalStatus.usdt ? "USDT\u2713" : "USDT\u2717",
|
|
4323
|
+
bnbApprovalStatus.usdc ? "USDC\u2713" : "USDC\u2717"
|
|
4324
|
+
].join(", ");
|
|
4325
|
+
console.log(` BNB: ${status} ${tokens}`);
|
|
4326
|
+
const bnbNative = allBalances["bnb"]?.native || 0;
|
|
4327
|
+
if (!bnbApprovalStatus.usdc && !bnbApprovalStatus.usdt && bnbNative < 5e-4) {
|
|
4328
|
+
console.log(" \u26A0\uFE0F Need ~0.001 BNB for first approval tx. Get from exchange.");
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
if (bnbTestnetApprovalStatus) {
|
|
4333
|
+
if (!bnbTestnetApprovalStatus.spender) {
|
|
4334
|
+
console.log(" BNB Testnet: \u26A0\uFE0F No spender configured");
|
|
4335
|
+
console.log(" \u2514\u2500 Run a payment first, or: npx moltspay approve --chain bnb_testnet --spender <address>");
|
|
4336
|
+
} else {
|
|
4337
|
+
const status = bnbTestnetApprovalStatus.usdt && bnbTestnetApprovalStatus.usdc ? "\u2705" : "\u26A0\uFE0F";
|
|
4338
|
+
const tokens = [
|
|
4339
|
+
bnbTestnetApprovalStatus.usdt ? "USDT\u2713" : "USDT\u2717",
|
|
4340
|
+
bnbTestnetApprovalStatus.usdc ? "USDC\u2713" : "USDC\u2717"
|
|
4341
|
+
].join(", ");
|
|
4342
|
+
console.log(` BNB Testnet: ${status} ${tokens}`);
|
|
4343
|
+
const tbnbNative = allBalances["bnb_testnet"]?.native || 0;
|
|
4344
|
+
if (!bnbTestnetApprovalStatus.usdc && !bnbTestnetApprovalStatus.usdt && tbnbNative < 5e-4) {
|
|
4345
|
+
console.log(" \u26A0\uFE0F Need tBNB for approval. Run: npx moltspay faucet --chain bnb_testnet");
|
|
4346
|
+
}
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
2655
4350
|
console.log("");
|
|
2656
4351
|
console.log(" Spending Limits:");
|
|
2657
4352
|
console.log(` Per Transaction: $${config.limits.maxPerTx}`);
|
|
2658
4353
|
console.log(` Daily: $${config.limits.maxPerDay}`);
|
|
4354
|
+
const solanaAddress2 = getSolanaAddress(options.configDir);
|
|
4355
|
+
if (solanaAddress2) {
|
|
4356
|
+
console.log("");
|
|
4357
|
+
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");
|
|
4358
|
+
console.log(` \u{1F7E3} Solana: ${solanaAddress2}`);
|
|
4359
|
+
try {
|
|
4360
|
+
const devnetBalances = await getSolanaBalances(solanaAddress2, "solana_devnet");
|
|
4361
|
+
console.log(` Devnet: ${devnetBalances.sol.toFixed(4)} SOL | ${devnetBalances.usdc.toFixed(2)} USDC`);
|
|
4362
|
+
} catch (err) {
|
|
4363
|
+
console.log(` Devnet: (unable to fetch)`);
|
|
4364
|
+
}
|
|
4365
|
+
try {
|
|
4366
|
+
const mainnetBalances = await getSolanaBalances(solanaAddress2, "solana");
|
|
4367
|
+
console.log(` Mainnet: ${mainnetBalances.sol.toFixed(4)} SOL | ${mainnetBalances.usdc.toFixed(2)} USDC`);
|
|
4368
|
+
} catch (err) {
|
|
4369
|
+
console.log(` Mainnet: (unable to fetch)`);
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
2659
4372
|
console.log("");
|
|
2660
4373
|
}
|
|
2661
4374
|
});
|
|
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",
|
|
4375
|
+
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
4376
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
2664
4377
|
if (!client.isInitialized) {
|
|
2665
4378
|
console.log("\u274C Not initialized. Run: npx moltspay init");
|
|
@@ -2962,18 +4675,18 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
2962
4675
|
const handlers = /* @__PURE__ */ new Map();
|
|
2963
4676
|
let provider = null;
|
|
2964
4677
|
for (const inputPath of allPaths) {
|
|
2965
|
-
const resolvedPath = (0,
|
|
4678
|
+
const resolvedPath = (0, import_path3.resolve)(inputPath);
|
|
2966
4679
|
let manifestPath;
|
|
2967
4680
|
let skillDir;
|
|
2968
4681
|
let isSkillDir = false;
|
|
2969
|
-
if ((0,
|
|
2970
|
-
manifestPath = (0,
|
|
4682
|
+
if ((0, import_fs5.existsSync)((0, import_path3.join)(resolvedPath, "moltspay.services.json"))) {
|
|
4683
|
+
manifestPath = (0, import_path3.join)(resolvedPath, "moltspay.services.json");
|
|
2971
4684
|
skillDir = resolvedPath;
|
|
2972
4685
|
isSkillDir = true;
|
|
2973
|
-
} else if ((0,
|
|
4686
|
+
} else if ((0, import_fs5.existsSync)(resolvedPath) && resolvedPath.endsWith(".json")) {
|
|
2974
4687
|
manifestPath = resolvedPath;
|
|
2975
|
-
skillDir = (0,
|
|
2976
|
-
} else if ((0,
|
|
4688
|
+
skillDir = (0, import_path3.dirname)(resolvedPath);
|
|
4689
|
+
} else if ((0, import_fs5.existsSync)(resolvedPath)) {
|
|
2977
4690
|
console.error(`\u274C No moltspay.services.json found in: ${resolvedPath}`);
|
|
2978
4691
|
continue;
|
|
2979
4692
|
} else {
|
|
@@ -2982,25 +4695,25 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
2982
4695
|
}
|
|
2983
4696
|
console.log(`\u{1F4E6} Loading: ${manifestPath}`);
|
|
2984
4697
|
try {
|
|
2985
|
-
const manifestContent = JSON.parse((0,
|
|
4698
|
+
const manifestContent = JSON.parse((0, import_fs5.readFileSync)(manifestPath, "utf-8"));
|
|
2986
4699
|
if (!provider) {
|
|
2987
4700
|
provider = manifestContent.provider;
|
|
2988
4701
|
}
|
|
2989
4702
|
let skillModule = null;
|
|
2990
4703
|
if (isSkillDir) {
|
|
2991
4704
|
let entryPoint = "index.js";
|
|
2992
|
-
const pkgJsonPath = (0,
|
|
2993
|
-
if ((0,
|
|
4705
|
+
const pkgJsonPath = (0, import_path3.join)(skillDir, "package.json");
|
|
4706
|
+
if ((0, import_fs5.existsSync)(pkgJsonPath)) {
|
|
2994
4707
|
try {
|
|
2995
|
-
const pkgJson = JSON.parse((0,
|
|
4708
|
+
const pkgJson = JSON.parse((0, import_fs5.readFileSync)(pkgJsonPath, "utf-8"));
|
|
2996
4709
|
if (pkgJson.main) {
|
|
2997
4710
|
entryPoint = pkgJson.main;
|
|
2998
4711
|
}
|
|
2999
4712
|
} catch {
|
|
3000
4713
|
}
|
|
3001
4714
|
}
|
|
3002
|
-
const modulePath = (0,
|
|
3003
|
-
if ((0,
|
|
4715
|
+
const modulePath = (0, import_path3.join)(skillDir, entryPoint);
|
|
4716
|
+
if ((0, import_fs5.existsSync)(modulePath)) {
|
|
3004
4717
|
try {
|
|
3005
4718
|
skillModule = await import(modulePath);
|
|
3006
4719
|
console.log(` \u2705 Loaded module: ${modulePath}`);
|
|
@@ -3078,8 +4791,8 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
3078
4791
|
provider,
|
|
3079
4792
|
services: allServices
|
|
3080
4793
|
};
|
|
3081
|
-
const tempManifestPath = (0,
|
|
3082
|
-
(0,
|
|
4794
|
+
const tempManifestPath = (0, import_path3.join)(DEFAULT_CONFIG_DIR2, "combined-manifest.json");
|
|
4795
|
+
(0, import_fs5.writeFileSync)(tempManifestPath, JSON.stringify(combinedManifest, null, 2));
|
|
3083
4796
|
console.log(`
|
|
3084
4797
|
\u{1F4CB} Combined manifest: ${allServices.length} services`);
|
|
3085
4798
|
console.log(` Provider: ${provider.name}`);
|
|
@@ -3092,12 +4805,12 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
3092
4805
|
server.skill(serviceId, handler);
|
|
3093
4806
|
}
|
|
3094
4807
|
const pidData = { pid: process.pid, port, paths: allPaths };
|
|
3095
|
-
(0,
|
|
4808
|
+
(0, import_fs5.writeFileSync)(PID_FILE, JSON.stringify(pidData, null, 2));
|
|
3096
4809
|
server.listen(port);
|
|
3097
4810
|
const cleanup = () => {
|
|
3098
4811
|
try {
|
|
3099
|
-
if ((0,
|
|
3100
|
-
if ((0,
|
|
4812
|
+
if ((0, import_fs5.existsSync)(PID_FILE)) (0, import_fs5.unlinkSync)(PID_FILE);
|
|
4813
|
+
if ((0, import_fs5.existsSync)(tempManifestPath)) (0, import_fs5.unlinkSync)(tempManifestPath);
|
|
3101
4814
|
} catch {
|
|
3102
4815
|
}
|
|
3103
4816
|
};
|
|
@@ -3118,12 +4831,12 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
3118
4831
|
}
|
|
3119
4832
|
});
|
|
3120
4833
|
program.command("stop").description("Stop the running MoltsPay server").action(async () => {
|
|
3121
|
-
if (!(0,
|
|
4834
|
+
if (!(0, import_fs5.existsSync)(PID_FILE)) {
|
|
3122
4835
|
console.log("\u274C No running server found (no PID file)");
|
|
3123
4836
|
process.exit(1);
|
|
3124
4837
|
}
|
|
3125
4838
|
try {
|
|
3126
|
-
const pidData = JSON.parse((0,
|
|
4839
|
+
const pidData = JSON.parse((0, import_fs5.readFileSync)(PID_FILE, "utf-8"));
|
|
3127
4840
|
const { pid, port, manifest } = pidData;
|
|
3128
4841
|
console.log(`
|
|
3129
4842
|
\u{1F6D1} Stopping MoltsPay Server
|
|
@@ -3136,7 +4849,7 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
|
|
|
3136
4849
|
process.kill(pid, 0);
|
|
3137
4850
|
} catch {
|
|
3138
4851
|
console.log("\u26A0\uFE0F Process not running, cleaning up PID file...");
|
|
3139
|
-
(0,
|
|
4852
|
+
(0, import_fs5.unlinkSync)(PID_FILE);
|
|
3140
4853
|
process.exit(0);
|
|
3141
4854
|
}
|
|
3142
4855
|
process.kill(pid, "SIGTERM");
|
|
@@ -3148,8 +4861,8 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
|
|
|
3148
4861
|
process.kill(pid, "SIGKILL");
|
|
3149
4862
|
} catch {
|
|
3150
4863
|
}
|
|
3151
|
-
if ((0,
|
|
3152
|
-
(0,
|
|
4864
|
+
if ((0, import_fs5.existsSync)(PID_FILE)) {
|
|
4865
|
+
(0, import_fs5.unlinkSync)(PID_FILE);
|
|
3153
4866
|
}
|
|
3154
4867
|
console.log("\u2705 Server stopped\n");
|
|
3155
4868
|
} catch (err) {
|
|
@@ -3157,7 +4870,7 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
|
|
|
3157
4870
|
process.exit(1);
|
|
3158
4871
|
}
|
|
3159
4872
|
});
|
|
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
|
|
4873
|
+
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, 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
4874
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
3162
4875
|
if (!client.isInitialized) {
|
|
3163
4876
|
console.error("\u274C Wallet not initialized. Run: npx moltspay init");
|
|
@@ -3178,18 +4891,19 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
3178
4891
|
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
|
3179
4892
|
params.image_url = imagePath;
|
|
3180
4893
|
} else {
|
|
3181
|
-
const filePath = (0,
|
|
3182
|
-
if (!(0,
|
|
4894
|
+
const filePath = (0, import_path3.resolve)(imagePath);
|
|
4895
|
+
if (!(0, import_fs5.existsSync)(filePath)) {
|
|
3183
4896
|
console.error(`\u274C Image file not found: ${filePath}`);
|
|
3184
4897
|
process.exit(1);
|
|
3185
4898
|
}
|
|
3186
|
-
const imageData = (0,
|
|
4899
|
+
const imageData = (0, import_fs5.readFileSync)(filePath);
|
|
3187
4900
|
params.image_base64 = imageData.toString("base64");
|
|
3188
4901
|
}
|
|
3189
4902
|
}
|
|
4903
|
+
const supportedPayChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
|
|
3190
4904
|
const chain = options.chain?.toLowerCase();
|
|
3191
|
-
if (chain && !
|
|
3192
|
-
console.error(`\u274C Unknown chain: ${chain}. Supported:
|
|
4905
|
+
if (chain && !supportedPayChains.includes(chain)) {
|
|
4906
|
+
console.error(`\u274C Unknown chain: ${chain}. Supported: ${supportedPayChains.join(", ")}`);
|
|
3193
4907
|
process.exit(1);
|
|
3194
4908
|
}
|
|
3195
4909
|
const imageDisplay = params.image_url || (params.image_base64 ? `[local file: ${options.image}]` : null);
|
|
@@ -3220,22 +4934,10 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
3220
4934
|
console.log("");
|
|
3221
4935
|
}
|
|
3222
4936
|
try {
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
console.log("");
|
|
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
|
-
}
|
|
4937
|
+
const result = await client.pay(server, service, params, {
|
|
4938
|
+
token,
|
|
4939
|
+
chain
|
|
4940
|
+
});
|
|
3239
4941
|
if (options.json) {
|
|
3240
4942
|
console.log(JSON.stringify(result));
|
|
3241
4943
|
} else {
|
|
@@ -3253,11 +4955,11 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
3253
4955
|
}
|
|
3254
4956
|
});
|
|
3255
4957
|
program.command("validate <path>").description("Validate a moltspay.services.json file against the schema").action(async (inputPath) => {
|
|
3256
|
-
const resolvedPath = (0,
|
|
4958
|
+
const resolvedPath = (0, import_path3.resolve)(inputPath);
|
|
3257
4959
|
let manifestPath;
|
|
3258
|
-
if ((0,
|
|
3259
|
-
manifestPath = (0,
|
|
3260
|
-
} else if (resolvedPath.endsWith(".json") && (0,
|
|
4960
|
+
if ((0, import_fs5.existsSync)((0, import_path3.join)(resolvedPath, "moltspay.services.json"))) {
|
|
4961
|
+
manifestPath = (0, import_path3.join)(resolvedPath, "moltspay.services.json");
|
|
4962
|
+
} else if (resolvedPath.endsWith(".json") && (0, import_fs5.existsSync)(resolvedPath)) {
|
|
3261
4963
|
manifestPath = resolvedPath;
|
|
3262
4964
|
} else {
|
|
3263
4965
|
console.error(`\u274C Not found: ${resolvedPath}`);
|
|
@@ -3267,7 +4969,7 @@ program.command("validate <path>").description("Validate a moltspay.services.jso
|
|
|
3267
4969
|
\u{1F4CB} Validating: ${manifestPath}
|
|
3268
4970
|
`);
|
|
3269
4971
|
try {
|
|
3270
|
-
const content = JSON.parse((0,
|
|
4972
|
+
const content = JSON.parse((0, import_fs5.readFileSync)(manifestPath, "utf-8"));
|
|
3271
4973
|
const errors = [];
|
|
3272
4974
|
if (!content.provider) {
|
|
3273
4975
|
errors.push("Missing required field: provider");
|