moltspay 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +14 -0
- package/README.md +319 -89
- package/dist/cdp/index.d.mts +4 -4
- package/dist/cdp/index.d.ts +4 -4
- package/dist/cdp/index.js +57 -0
- package/dist/cdp/index.js.map +1 -1
- package/dist/cdp/index.mjs +57 -0
- package/dist/cdp/index.mjs.map +1 -1
- package/dist/chains/index.d.mts +9 -8
- package/dist/chains/index.d.ts +9 -8
- package/dist/chains/index.js +57 -0
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/index.mjs +57 -0
- package/dist/chains/index.mjs.map +1 -1
- package/dist/cli/index.js +2021 -285
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +2023 -277
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +39 -3
- package/dist/client/index.d.ts +39 -3
- package/dist/client/index.js +563 -37
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +571 -35
- package/dist/client/index.mjs.map +1 -1
- package/dist/facilitators/index.d.mts +220 -1
- package/dist/facilitators/index.d.ts +220 -1
- package/dist/facilitators/index.js +664 -1
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +670 -1
- package/dist/facilitators/index.mjs.map +1 -1
- package/dist/{index-On9ZaGDW.d.mts → index-D_2FkLwV.d.mts} +6 -2
- package/dist/{index-On9ZaGDW.d.ts → index-D_2FkLwV.d.ts} +6 -2
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1440 -153
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1448 -151
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.d.mts +13 -3
- package/dist/server/index.d.ts +13 -3
- package/dist/server/index.js +909 -54
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +919 -54
- package/dist/server/index.mjs.map +1 -1
- package/dist/verify/index.d.mts +1 -1
- package/dist/verify/index.d.ts +1 -1
- package/dist/verify/index.js +57 -0
- package/dist/verify/index.js.map +1 -1
- package/dist/verify/index.mjs +57 -0
- package/dist/verify/index.mjs.map +1 -1
- package/dist/wallet/index.d.mts +3 -3
- package/dist/wallet/index.d.ts +3 -3
- package/dist/wallet/index.js +57 -0
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs +57 -0
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +5 -2
- package/schemas/moltspay.services.schema.json +27 -132
package/dist/cli/index.mjs
CHANGED
|
@@ -21,16 +21,17 @@ var init_esm_shims = __esm({
|
|
|
21
21
|
init_esm_shims();
|
|
22
22
|
import { webcrypto } from "crypto";
|
|
23
23
|
import { Command } from "commander";
|
|
24
|
-
import { homedir as
|
|
25
|
-
import { join as
|
|
26
|
-
import { existsSync as
|
|
24
|
+
import { homedir as homedir3 } from "os";
|
|
25
|
+
import { join as join5, dirname, resolve } from "path";
|
|
26
|
+
import { existsSync as existsSync5, writeFileSync as writeFileSync3, readFileSync as readFileSync5, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
|
|
27
27
|
import { spawn } from "child_process";
|
|
28
|
+
import { ethers as ethers2 } from "ethers";
|
|
28
29
|
|
|
29
30
|
// src/client/index.ts
|
|
30
31
|
init_esm_shims();
|
|
31
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, chmodSync } from "fs";
|
|
32
|
-
import { homedir } from "os";
|
|
33
|
-
import { join } from "path";
|
|
32
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, statSync, chmodSync } from "fs";
|
|
33
|
+
import { homedir as homedir2 } from "os";
|
|
34
|
+
import { join as join2 } from "path";
|
|
34
35
|
import { Wallet, ethers } from "ethers";
|
|
35
36
|
|
|
36
37
|
// src/chains/index.ts
|
|
@@ -141,6 +142,63 @@ var CHAINS = {
|
|
|
141
142
|
explorerTx: "https://explore.testnet.tempo.xyz/tx/",
|
|
142
143
|
avgBlockTime: 0.5
|
|
143
144
|
// ~500ms finality
|
|
145
|
+
},
|
|
146
|
+
// ============ BNB Chain Testnet ============
|
|
147
|
+
bnb_testnet: {
|
|
148
|
+
name: "BNB Testnet",
|
|
149
|
+
chainId: 97,
|
|
150
|
+
rpc: "https://data-seed-prebsc-1-s1.binance.org:8545",
|
|
151
|
+
tokens: {
|
|
152
|
+
// Note: BNB uses 18 decimals for stablecoins (unlike Base/Polygon which use 6)
|
|
153
|
+
// Using official Binance-Peg testnet tokens
|
|
154
|
+
USDC: {
|
|
155
|
+
address: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
156
|
+
// Testnet USDC
|
|
157
|
+
decimals: 18,
|
|
158
|
+
symbol: "USDC",
|
|
159
|
+
eip712Name: "USD Coin"
|
|
160
|
+
},
|
|
161
|
+
USDT: {
|
|
162
|
+
address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
|
|
163
|
+
// Testnet USDT
|
|
164
|
+
decimals: 18,
|
|
165
|
+
symbol: "USDT",
|
|
166
|
+
eip712Name: "Tether USD"
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
usdc: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
170
|
+
explorer: "https://testnet.bscscan.com/address/",
|
|
171
|
+
explorerTx: "https://testnet.bscscan.com/tx/",
|
|
172
|
+
avgBlockTime: 3,
|
|
173
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
174
|
+
requiresApproval: true
|
|
175
|
+
},
|
|
176
|
+
// ============ BNB Chain Mainnet ============
|
|
177
|
+
bnb: {
|
|
178
|
+
name: "BNB Smart Chain",
|
|
179
|
+
chainId: 56,
|
|
180
|
+
rpc: "https://bsc-dataseed.binance.org",
|
|
181
|
+
tokens: {
|
|
182
|
+
// Note: BNB uses 18 decimals for stablecoins
|
|
183
|
+
USDC: {
|
|
184
|
+
address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
185
|
+
decimals: 18,
|
|
186
|
+
symbol: "USDC",
|
|
187
|
+
eip712Name: "USD Coin"
|
|
188
|
+
},
|
|
189
|
+
USDT: {
|
|
190
|
+
address: "0x55d398326f99059fF775485246999027B3197955",
|
|
191
|
+
decimals: 18,
|
|
192
|
+
symbol: "USDT",
|
|
193
|
+
eip712Name: "Tether USD"
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
usdc: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
197
|
+
explorer: "https://bscscan.com/address/",
|
|
198
|
+
explorerTx: "https://bscscan.com/tx/",
|
|
199
|
+
avgBlockTime: 3,
|
|
200
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
201
|
+
requiresApproval: true
|
|
144
202
|
}
|
|
145
203
|
};
|
|
146
204
|
function getChain(name) {
|
|
@@ -151,6 +209,372 @@ function getChain(name) {
|
|
|
151
209
|
return config;
|
|
152
210
|
}
|
|
153
211
|
|
|
212
|
+
// src/wallet/solana.ts
|
|
213
|
+
init_esm_shims();
|
|
214
|
+
import { Keypair, PublicKey as PublicKey2, LAMPORTS_PER_SOL } from "@solana/web3.js";
|
|
215
|
+
import { getAssociatedTokenAddress, getAccount } from "@solana/spl-token";
|
|
216
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
217
|
+
import { join } from "path";
|
|
218
|
+
import { homedir } from "os";
|
|
219
|
+
import bs58 from "bs58";
|
|
220
|
+
|
|
221
|
+
// src/chains/solana.ts
|
|
222
|
+
init_esm_shims();
|
|
223
|
+
import { Connection, PublicKey } from "@solana/web3.js";
|
|
224
|
+
var SOLANA_CHAINS = {
|
|
225
|
+
solana: {
|
|
226
|
+
name: "Solana Mainnet",
|
|
227
|
+
cluster: "mainnet-beta",
|
|
228
|
+
rpc: "https://api.mainnet-beta.solana.com",
|
|
229
|
+
explorer: "https://solscan.io/account/",
|
|
230
|
+
explorerTx: "https://solscan.io/tx/",
|
|
231
|
+
tokens: {
|
|
232
|
+
USDC: {
|
|
233
|
+
mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
234
|
+
// Circle official USDC
|
|
235
|
+
decimals: 6
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
solana_devnet: {
|
|
240
|
+
name: "Solana Devnet",
|
|
241
|
+
cluster: "devnet",
|
|
242
|
+
rpc: "https://api.devnet.solana.com",
|
|
243
|
+
explorer: "https://solscan.io/account/",
|
|
244
|
+
explorerTx: "https://solscan.io/tx/",
|
|
245
|
+
tokens: {
|
|
246
|
+
USDC: {
|
|
247
|
+
// Circle's devnet USDC (if not available, we'll deploy our own test token)
|
|
248
|
+
mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
249
|
+
decimals: 6
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
function getSolanaConnection(chain) {
|
|
255
|
+
const config = SOLANA_CHAINS[chain];
|
|
256
|
+
return new Connection(config.rpc, "confirmed");
|
|
257
|
+
}
|
|
258
|
+
function getUSDCMint(chain) {
|
|
259
|
+
return new PublicKey(SOLANA_CHAINS[chain].tokens.USDC.mint);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/wallet/solana.ts
|
|
263
|
+
var DEFAULT_CONFIG_DIR = join(homedir(), ".moltspay");
|
|
264
|
+
var SOLANA_WALLET_FILE = "wallet-solana.json";
|
|
265
|
+
function getSolanaWalletPath(configDir = DEFAULT_CONFIG_DIR) {
|
|
266
|
+
return join(configDir, SOLANA_WALLET_FILE);
|
|
267
|
+
}
|
|
268
|
+
function solanaWalletExists(configDir = DEFAULT_CONFIG_DIR) {
|
|
269
|
+
return existsSync(getSolanaWalletPath(configDir));
|
|
270
|
+
}
|
|
271
|
+
function loadSolanaWallet(configDir = DEFAULT_CONFIG_DIR) {
|
|
272
|
+
const walletPath = getSolanaWalletPath(configDir);
|
|
273
|
+
if (!existsSync(walletPath)) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const data = JSON.parse(readFileSync(walletPath, "utf-8"));
|
|
278
|
+
const secretKey = bs58.decode(data.secretKey);
|
|
279
|
+
return Keypair.fromSecretKey(secretKey);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error("Failed to load Solana wallet:", error);
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function createSolanaWallet(configDir = DEFAULT_CONFIG_DIR) {
|
|
286
|
+
if (!existsSync(configDir)) {
|
|
287
|
+
mkdirSync(configDir, { recursive: true });
|
|
288
|
+
}
|
|
289
|
+
const keypair = Keypair.generate();
|
|
290
|
+
const data = {
|
|
291
|
+
publicKey: keypair.publicKey.toBase58(),
|
|
292
|
+
secretKey: bs58.encode(keypair.secretKey),
|
|
293
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
294
|
+
};
|
|
295
|
+
const walletPath = getSolanaWalletPath(configDir);
|
|
296
|
+
writeFileSync(walletPath, JSON.stringify(data, null, 2));
|
|
297
|
+
return keypair;
|
|
298
|
+
}
|
|
299
|
+
function getSolanaAddress(configDir = DEFAULT_CONFIG_DIR) {
|
|
300
|
+
const wallet = loadSolanaWallet(configDir);
|
|
301
|
+
return wallet?.publicKey.toBase58() || null;
|
|
302
|
+
}
|
|
303
|
+
async function getSolanaBalance(address, chain) {
|
|
304
|
+
const connection = getSolanaConnection(chain);
|
|
305
|
+
const pubkey = new PublicKey2(address);
|
|
306
|
+
const balance = await connection.getBalance(pubkey);
|
|
307
|
+
return balance / LAMPORTS_PER_SOL;
|
|
308
|
+
}
|
|
309
|
+
async function getSolanaUSDCBalance(address, chain) {
|
|
310
|
+
const connection = getSolanaConnection(chain);
|
|
311
|
+
const owner = new PublicKey2(address);
|
|
312
|
+
const mint = getUSDCMint(chain);
|
|
313
|
+
try {
|
|
314
|
+
const ata = await getAssociatedTokenAddress(mint, owner);
|
|
315
|
+
const account = await getAccount(connection, ata);
|
|
316
|
+
return Number(account.amount) / 1e6;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (error.name === "TokenAccountNotFoundError" || error.message?.includes("could not find account")) {
|
|
319
|
+
return 0;
|
|
320
|
+
}
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function getSolanaBalances(address, chain) {
|
|
325
|
+
const [sol, usdc] = await Promise.all([
|
|
326
|
+
getSolanaBalance(address, chain),
|
|
327
|
+
getSolanaUSDCBalance(address, chain)
|
|
328
|
+
]);
|
|
329
|
+
return { sol, usdc };
|
|
330
|
+
}
|
|
331
|
+
function isValidSolanaAddress(address) {
|
|
332
|
+
try {
|
|
333
|
+
new PublicKey2(address);
|
|
334
|
+
return true;
|
|
335
|
+
} catch {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/facilitators/solana.ts
|
|
341
|
+
init_esm_shims();
|
|
342
|
+
import {
|
|
343
|
+
Connection as Connection3,
|
|
344
|
+
PublicKey as PublicKey3,
|
|
345
|
+
Transaction,
|
|
346
|
+
VersionedTransaction
|
|
347
|
+
} from "@solana/web3.js";
|
|
348
|
+
import {
|
|
349
|
+
getAssociatedTokenAddress as getAssociatedTokenAddress2,
|
|
350
|
+
createTransferCheckedInstruction,
|
|
351
|
+
getAccount as getAccount2,
|
|
352
|
+
createAssociatedTokenAccountInstruction
|
|
353
|
+
} from "@solana/spl-token";
|
|
354
|
+
|
|
355
|
+
// src/facilitators/interface.ts
|
|
356
|
+
init_esm_shims();
|
|
357
|
+
var BaseFacilitator = class {
|
|
358
|
+
supportsNetwork(network) {
|
|
359
|
+
return this.supportedNetworks.includes(network);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// src/facilitators/solana.ts
|
|
364
|
+
var SolanaFacilitator = class extends BaseFacilitator {
|
|
365
|
+
name = "solana";
|
|
366
|
+
displayName = "Solana Direct";
|
|
367
|
+
supportedNetworks = ["solana:mainnet", "solana:devnet"];
|
|
368
|
+
connections = /* @__PURE__ */ new Map();
|
|
369
|
+
feePayerKeypair;
|
|
370
|
+
constructor(config) {
|
|
371
|
+
super();
|
|
372
|
+
this.feePayerKeypair = config?.feePayerKeypair;
|
|
373
|
+
for (const [chain, config2] of Object.entries(SOLANA_CHAINS)) {
|
|
374
|
+
this.connections.set(
|
|
375
|
+
chain,
|
|
376
|
+
new Connection3(config2.rpc, "confirmed")
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
if (this.feePayerKeypair) {
|
|
380
|
+
console.log(`[SolanaFacilitator] Gasless mode enabled. Fee payer: ${this.feePayerKeypair.publicKey.toBase58()}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Get fee payer public key (for gasless transactions)
|
|
385
|
+
*/
|
|
386
|
+
getFeePayerPubkey() {
|
|
387
|
+
return this.feePayerKeypair?.publicKey.toBase58() || null;
|
|
388
|
+
}
|
|
389
|
+
getConnection(chain) {
|
|
390
|
+
const conn = this.connections.get(chain);
|
|
391
|
+
if (!conn) {
|
|
392
|
+
throw new Error(`No connection for chain: ${chain}`);
|
|
393
|
+
}
|
|
394
|
+
return conn;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Convert our chain name to network identifier
|
|
398
|
+
*/
|
|
399
|
+
static chainToNetwork(chain) {
|
|
400
|
+
return chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Convert network identifier to chain name
|
|
404
|
+
*/
|
|
405
|
+
static networkToChain(network) {
|
|
406
|
+
if (network === "solana:mainnet") return "solana";
|
|
407
|
+
if (network === "solana:devnet") return "solana_devnet";
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
async healthCheck() {
|
|
411
|
+
const start = Date.now();
|
|
412
|
+
try {
|
|
413
|
+
const conn = this.getConnection("solana_devnet");
|
|
414
|
+
await conn.getSlot();
|
|
415
|
+
return {
|
|
416
|
+
healthy: true,
|
|
417
|
+
latencyMs: Date.now() - start
|
|
418
|
+
};
|
|
419
|
+
} catch (error) {
|
|
420
|
+
return {
|
|
421
|
+
healthy: false,
|
|
422
|
+
error: error.message
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Verify a Solana payment
|
|
428
|
+
*
|
|
429
|
+
* Checks:
|
|
430
|
+
* 1. Transaction is valid and properly signed
|
|
431
|
+
* 2. Transfer instruction matches expected amount and recipient
|
|
432
|
+
*/
|
|
433
|
+
async verify(paymentPayload, requirements) {
|
|
434
|
+
try {
|
|
435
|
+
const solanaPayload = paymentPayload.payload;
|
|
436
|
+
if (!solanaPayload || !solanaPayload.signedTransaction) {
|
|
437
|
+
return { valid: false, error: "Missing signed transaction" };
|
|
438
|
+
}
|
|
439
|
+
const chain = solanaPayload.chain || "solana_devnet";
|
|
440
|
+
const chainConfig = SOLANA_CHAINS[chain];
|
|
441
|
+
if (!chainConfig) {
|
|
442
|
+
return { valid: false, error: `Invalid chain: ${chain}` };
|
|
443
|
+
}
|
|
444
|
+
const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
|
|
445
|
+
let tx;
|
|
446
|
+
try {
|
|
447
|
+
tx = Transaction.from(txBuffer);
|
|
448
|
+
} catch {
|
|
449
|
+
tx = VersionedTransaction.deserialize(txBuffer);
|
|
450
|
+
}
|
|
451
|
+
if (tx instanceof Transaction) {
|
|
452
|
+
const hasAnySignature = tx.signatures.some(
|
|
453
|
+
(sig) => sig.signature && !sig.signature.every((b) => b === 0)
|
|
454
|
+
);
|
|
455
|
+
if (!hasAnySignature) {
|
|
456
|
+
return { valid: false, error: "Transaction not signed" };
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const expectedAmount = BigInt(requirements.amount);
|
|
460
|
+
const expectedRecipient = new PublicKey3(requirements.payTo);
|
|
461
|
+
return {
|
|
462
|
+
valid: true,
|
|
463
|
+
details: {
|
|
464
|
+
chain,
|
|
465
|
+
sender: solanaPayload.sender,
|
|
466
|
+
recipient: requirements.payTo,
|
|
467
|
+
amount: requirements.amount
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
} catch (error) {
|
|
471
|
+
return { valid: false, error: error.message };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Settle a Solana payment
|
|
476
|
+
*
|
|
477
|
+
* Submits the signed transaction to the network.
|
|
478
|
+
* In gasless mode, adds fee payer signature before submitting.
|
|
479
|
+
*/
|
|
480
|
+
async settle(paymentPayload, requirements) {
|
|
481
|
+
try {
|
|
482
|
+
const solanaPayload = paymentPayload.payload;
|
|
483
|
+
if (!solanaPayload || !solanaPayload.signedTransaction) {
|
|
484
|
+
return { success: false, error: "Missing signed transaction" };
|
|
485
|
+
}
|
|
486
|
+
const chain = solanaPayload.chain || "solana_devnet";
|
|
487
|
+
const connection = this.getConnection(chain);
|
|
488
|
+
const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
|
|
489
|
+
let txToSend;
|
|
490
|
+
try {
|
|
491
|
+
const tx = Transaction.from(txBuffer);
|
|
492
|
+
if (this.feePayerKeypair && tx.feePayer) {
|
|
493
|
+
const feePayerPubkey = this.feePayerKeypair.publicKey.toBase58();
|
|
494
|
+
const txFeePayer = tx.feePayer.toBase58();
|
|
495
|
+
if (txFeePayer === feePayerPubkey) {
|
|
496
|
+
console.log(`[SolanaFacilitator] Gasless mode: adding fee payer signature`);
|
|
497
|
+
tx.partialSign(this.feePayerKeypair);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
txToSend = tx.serialize();
|
|
501
|
+
} catch (e) {
|
|
502
|
+
txToSend = txBuffer;
|
|
503
|
+
}
|
|
504
|
+
const signature = await connection.sendRawTransaction(txToSend, {
|
|
505
|
+
skipPreflight: false,
|
|
506
|
+
preflightCommitment: "confirmed"
|
|
507
|
+
});
|
|
508
|
+
const confirmation = await connection.confirmTransaction(signature, "confirmed");
|
|
509
|
+
if (confirmation.value.err) {
|
|
510
|
+
return {
|
|
511
|
+
success: false,
|
|
512
|
+
error: `Transaction failed: ${JSON.stringify(confirmation.value.err)}`,
|
|
513
|
+
transaction: signature
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
success: true,
|
|
518
|
+
transaction: signature,
|
|
519
|
+
status: "confirmed"
|
|
520
|
+
};
|
|
521
|
+
} catch (error) {
|
|
522
|
+
return { success: false, error: error.message };
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
supportsNetwork(network) {
|
|
526
|
+
return this.supportedNetworks.includes(network);
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
|
|
530
|
+
const chainConfig = SOLANA_CHAINS[chain];
|
|
531
|
+
const connection = new Connection3(chainConfig.rpc, "confirmed");
|
|
532
|
+
const mint = new PublicKey3(chainConfig.tokens.USDC.mint);
|
|
533
|
+
const actualFeePayer = feePayerPubkey || senderPubkey;
|
|
534
|
+
const senderATA = await getAssociatedTokenAddress2(mint, senderPubkey);
|
|
535
|
+
const recipientATA = await getAssociatedTokenAddress2(mint, recipientPubkey);
|
|
536
|
+
const transaction = new Transaction();
|
|
537
|
+
try {
|
|
538
|
+
await getAccount2(connection, recipientATA);
|
|
539
|
+
} catch {
|
|
540
|
+
transaction.add(
|
|
541
|
+
createAssociatedTokenAccountInstruction(
|
|
542
|
+
actualFeePayer,
|
|
543
|
+
// payer (fee payer in gasless mode)
|
|
544
|
+
recipientATA,
|
|
545
|
+
// ata to create
|
|
546
|
+
recipientPubkey,
|
|
547
|
+
// owner
|
|
548
|
+
mint
|
|
549
|
+
// mint
|
|
550
|
+
)
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
transaction.add(
|
|
554
|
+
createTransferCheckedInstruction(
|
|
555
|
+
senderATA,
|
|
556
|
+
// source
|
|
557
|
+
mint,
|
|
558
|
+
// mint
|
|
559
|
+
recipientATA,
|
|
560
|
+
// destination
|
|
561
|
+
senderPubkey,
|
|
562
|
+
// owner (sender still authorizes the transfer)
|
|
563
|
+
amount,
|
|
564
|
+
// amount
|
|
565
|
+
chainConfig.tokens.USDC.decimals
|
|
566
|
+
// decimals
|
|
567
|
+
)
|
|
568
|
+
);
|
|
569
|
+
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
|
|
570
|
+
transaction.recentBlockhash = blockhash;
|
|
571
|
+
transaction.feePayer = actualFeePayer;
|
|
572
|
+
return transaction;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/client/index.ts
|
|
576
|
+
import { PublicKey as PublicKey4 } from "@solana/web3.js";
|
|
577
|
+
|
|
154
578
|
// src/client/types.ts
|
|
155
579
|
init_esm_shims();
|
|
156
580
|
|
|
@@ -173,7 +597,7 @@ var MoltsPayClient = class {
|
|
|
173
597
|
todaySpending = 0;
|
|
174
598
|
lastSpendingReset = 0;
|
|
175
599
|
constructor(options = {}) {
|
|
176
|
-
this.configDir = options.configDir ||
|
|
600
|
+
this.configDir = options.configDir || join2(homedir2(), ".moltspay");
|
|
177
601
|
this.config = this.loadConfig();
|
|
178
602
|
this.walletData = this.loadWallet();
|
|
179
603
|
this.loadSpending();
|
|
@@ -193,6 +617,12 @@ var MoltsPayClient = class {
|
|
|
193
617
|
get address() {
|
|
194
618
|
return this.wallet?.address || null;
|
|
195
619
|
}
|
|
620
|
+
/**
|
|
621
|
+
* Get wallet instance (for direct operations like approvals)
|
|
622
|
+
*/
|
|
623
|
+
getWallet() {
|
|
624
|
+
return this.wallet;
|
|
625
|
+
}
|
|
196
626
|
/**
|
|
197
627
|
* Get current config
|
|
198
628
|
*/
|
|
@@ -246,11 +676,26 @@ var MoltsPayClient = class {
|
|
|
246
676
|
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
247
677
|
}
|
|
248
678
|
console.log(`[MoltsPay] Requesting service: ${service}`);
|
|
249
|
-
|
|
679
|
+
let executeUrl = `${serverUrl}/execute`;
|
|
680
|
+
try {
|
|
681
|
+
const services = await this.getServices(serverUrl);
|
|
682
|
+
const svc = services.services?.find((s) => s.id === service);
|
|
683
|
+
if (svc?.endpoint) {
|
|
684
|
+
executeUrl = `${serverUrl}${svc.endpoint}`;
|
|
685
|
+
console.log(`[MoltsPay] Using service endpoint: ${svc.endpoint}`);
|
|
686
|
+
}
|
|
687
|
+
} catch {
|
|
688
|
+
}
|
|
689
|
+
let requestBody;
|
|
690
|
+
if (options.rawData) {
|
|
691
|
+
requestBody = { service, ...params };
|
|
692
|
+
} else {
|
|
693
|
+
requestBody = { service, params };
|
|
694
|
+
}
|
|
250
695
|
if (options.chain) {
|
|
251
696
|
requestBody.chain = options.chain;
|
|
252
697
|
}
|
|
253
|
-
const initialRes = await fetch(
|
|
698
|
+
const initialRes = await fetch(executeUrl, {
|
|
254
699
|
method: "POST",
|
|
255
700
|
headers: { "Content-Type": "application/json" },
|
|
256
701
|
body: JSON.stringify(requestBody)
|
|
@@ -262,9 +707,14 @@ var MoltsPayClient = class {
|
|
|
262
707
|
}
|
|
263
708
|
throw new Error(data.error || "Unexpected response");
|
|
264
709
|
}
|
|
710
|
+
const wwwAuthHeader = initialRes.headers.get("www-authenticate");
|
|
265
711
|
const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
712
|
+
if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
|
|
713
|
+
console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
|
|
714
|
+
return await this.handleMPPPayment(executeUrl, service, params, wwwAuthHeader, options);
|
|
715
|
+
}
|
|
266
716
|
if (!paymentRequiredHeader) {
|
|
267
|
-
throw new Error("Missing x-payment-required
|
|
717
|
+
throw new Error("Missing payment header (x-payment-required or www-authenticate)");
|
|
268
718
|
}
|
|
269
719
|
let requirements;
|
|
270
720
|
try {
|
|
@@ -281,17 +731,22 @@ var MoltsPayClient = class {
|
|
|
281
731
|
throw new Error("Invalid x-payment-required header");
|
|
282
732
|
}
|
|
283
733
|
const networkToChainName = (network2) => {
|
|
734
|
+
if (network2 === "solana:mainnet") return "solana";
|
|
735
|
+
if (network2 === "solana:devnet") return "solana_devnet";
|
|
284
736
|
const match = network2.match(/^eip155:(\d+)$/);
|
|
285
737
|
if (!match) return null;
|
|
286
738
|
const chainId = parseInt(match[1]);
|
|
287
739
|
if (chainId === 8453) return "base";
|
|
288
740
|
if (chainId === 137) return "polygon";
|
|
289
741
|
if (chainId === 84532) return "base_sepolia";
|
|
742
|
+
if (chainId === 42431) return "tempo_moderato";
|
|
743
|
+
if (chainId === 56) return "bnb";
|
|
744
|
+
if (chainId === 97) return "bnb_testnet";
|
|
290
745
|
return null;
|
|
291
746
|
};
|
|
292
747
|
const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
|
|
293
|
-
let chainName;
|
|
294
748
|
const userSpecifiedChain = options.chain;
|
|
749
|
+
let selectedChain;
|
|
295
750
|
if (userSpecifiedChain) {
|
|
296
751
|
if (!serverChains.includes(userSpecifiedChain)) {
|
|
297
752
|
throw new Error(
|
|
@@ -299,17 +754,27 @@ var MoltsPayClient = class {
|
|
|
299
754
|
Server accepts: ${serverChains.join(", ")}`
|
|
300
755
|
);
|
|
301
756
|
}
|
|
302
|
-
|
|
757
|
+
selectedChain = userSpecifiedChain;
|
|
303
758
|
} else {
|
|
304
759
|
if (serverChains.length === 1 && serverChains[0] === "base") {
|
|
305
|
-
|
|
760
|
+
selectedChain = "base";
|
|
306
761
|
} else {
|
|
307
762
|
throw new Error(
|
|
308
763
|
`Server accepts: ${serverChains.join(", ")}
|
|
309
|
-
Please specify: --chain
|
|
764
|
+
Please specify: --chain <chain_name>`
|
|
310
765
|
);
|
|
311
766
|
}
|
|
312
767
|
}
|
|
768
|
+
if (selectedChain === "solana" || selectedChain === "solana_devnet") {
|
|
769
|
+
const solanaChain = selectedChain;
|
|
770
|
+
const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
771
|
+
const req2 = requirements.find((r) => r.network === network2);
|
|
772
|
+
if (!req2) {
|
|
773
|
+
throw new Error(`Failed to find payment requirement for ${selectedChain}`);
|
|
774
|
+
}
|
|
775
|
+
return await this.handleSolanaPayment(executeUrl, service, params, req2, solanaChain, options);
|
|
776
|
+
}
|
|
777
|
+
const chainName = selectedChain;
|
|
313
778
|
const chain = getChain(chainName);
|
|
314
779
|
const network = `eip155:${chain.chainId}`;
|
|
315
780
|
const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
|
|
@@ -344,6 +809,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
344
809
|
} else {
|
|
345
810
|
console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
|
|
346
811
|
}
|
|
812
|
+
if (chainName === "bnb" || chainName === "bnb_testnet") {
|
|
813
|
+
console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
|
|
814
|
+
const payTo2 = req.payTo || req.resource;
|
|
815
|
+
if (!payTo2) {
|
|
816
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
817
|
+
}
|
|
818
|
+
const bnbSpender = req.extra?.bnbSpender;
|
|
819
|
+
if (!bnbSpender) {
|
|
820
|
+
throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
|
|
821
|
+
}
|
|
822
|
+
return await this.handleBNBPayment(executeUrl, service, params, {
|
|
823
|
+
to: payTo2,
|
|
824
|
+
amount,
|
|
825
|
+
token,
|
|
826
|
+
chainName,
|
|
827
|
+
chain,
|
|
828
|
+
spender: bnbSpender
|
|
829
|
+
}, options);
|
|
830
|
+
}
|
|
347
831
|
const payTo = req.payTo || req.resource;
|
|
348
832
|
if (!payTo) {
|
|
349
833
|
throw new Error("Missing payTo address in payment requirements");
|
|
@@ -373,11 +857,11 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
373
857
|
};
|
|
374
858
|
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
375
859
|
console.log(`[MoltsPay] Sending request with payment...`);
|
|
376
|
-
const paidRequestBody = { service, params };
|
|
860
|
+
const paidRequestBody = options.rawData ? { service, ...params } : { service, params };
|
|
377
861
|
if (options.chain) {
|
|
378
862
|
paidRequestBody.chain = options.chain;
|
|
379
863
|
}
|
|
380
|
-
const paidRes = await fetch(
|
|
864
|
+
const paidRes = await fetch(executeUrl, {
|
|
381
865
|
method: "POST",
|
|
382
866
|
headers: {
|
|
383
867
|
"Content-Type": "application/json",
|
|
@@ -391,7 +875,304 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
391
875
|
}
|
|
392
876
|
this.recordSpending(amount);
|
|
393
877
|
console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
|
|
394
|
-
return result.result;
|
|
878
|
+
return result.result || result;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Handle MPP (Machine Payments Protocol) payment flow
|
|
882
|
+
* Called when pay() detects WWW-Authenticate header in 402 response
|
|
883
|
+
*/
|
|
884
|
+
async handleMPPPayment(executeUrl, service, params, wwwAuthHeader, options = {}) {
|
|
885
|
+
const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
|
|
886
|
+
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
887
|
+
const { tempoModerato } = await import("viem/chains");
|
|
888
|
+
const { Actions } = await import("viem/tempo");
|
|
889
|
+
const privateKey = this.walletData.privateKey;
|
|
890
|
+
const account = privateKeyToAccount2(privateKey);
|
|
891
|
+
console.log(`[MoltsPay] Using MPP protocol on Tempo`);
|
|
892
|
+
console.log(`[MoltsPay] Account: ${account.address}`);
|
|
893
|
+
const parseAuthParam = (header, key) => {
|
|
894
|
+
const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
|
|
895
|
+
return match ? match[1] : null;
|
|
896
|
+
};
|
|
897
|
+
const challengeId = parseAuthParam(wwwAuthHeader, "id");
|
|
898
|
+
const method = parseAuthParam(wwwAuthHeader, "method");
|
|
899
|
+
const realm = parseAuthParam(wwwAuthHeader, "realm");
|
|
900
|
+
const requestB64 = parseAuthParam(wwwAuthHeader, "request");
|
|
901
|
+
if (method !== "tempo") {
|
|
902
|
+
throw new Error(`Unsupported payment method: ${method}`);
|
|
903
|
+
}
|
|
904
|
+
if (!requestB64) {
|
|
905
|
+
throw new Error("Missing request in WWW-Authenticate");
|
|
906
|
+
}
|
|
907
|
+
const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
|
|
908
|
+
const paymentRequest = JSON.parse(requestJson);
|
|
909
|
+
const { amount, currency, recipient, methodDetails } = paymentRequest;
|
|
910
|
+
const chainId = methodDetails?.chainId || 42431;
|
|
911
|
+
const amountDisplay = Number(amount) / 1e6;
|
|
912
|
+
console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
|
|
913
|
+
this.checkLimits(amountDisplay);
|
|
914
|
+
console.log(`[MoltsPay] Sending transaction on Tempo...`);
|
|
915
|
+
const tempoChain = { ...tempoModerato, feeToken: currency };
|
|
916
|
+
const publicClient = createPublicClient({
|
|
917
|
+
chain: tempoChain,
|
|
918
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
919
|
+
});
|
|
920
|
+
const walletClient = createWalletClient({
|
|
921
|
+
account,
|
|
922
|
+
chain: tempoChain,
|
|
923
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
924
|
+
});
|
|
925
|
+
const txHash = await Actions.token.transfer(walletClient, {
|
|
926
|
+
to: recipient,
|
|
927
|
+
amount: BigInt(amount),
|
|
928
|
+
token: currency
|
|
929
|
+
});
|
|
930
|
+
console.log(`[MoltsPay] Transaction: ${txHash}`);
|
|
931
|
+
await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
932
|
+
console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
|
|
933
|
+
const credential = {
|
|
934
|
+
challenge: {
|
|
935
|
+
id: challengeId,
|
|
936
|
+
realm,
|
|
937
|
+
method: "tempo",
|
|
938
|
+
intent: "charge",
|
|
939
|
+
request: paymentRequest
|
|
940
|
+
},
|
|
941
|
+
payload: { hash: txHash, type: "hash" },
|
|
942
|
+
source: `did:pkh:eip155:${chainId}:${account.address}`
|
|
943
|
+
};
|
|
944
|
+
const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
945
|
+
const retryBody = options.rawData ? { service, ...params, chain: "tempo_moderato" } : { service, params, chain: "tempo_moderato" };
|
|
946
|
+
const paidRes = await fetch(executeUrl, {
|
|
947
|
+
method: "POST",
|
|
948
|
+
headers: {
|
|
949
|
+
"Content-Type": "application/json",
|
|
950
|
+
"Authorization": `Payment ${credentialB64}`
|
|
951
|
+
},
|
|
952
|
+
body: JSON.stringify(retryBody)
|
|
953
|
+
});
|
|
954
|
+
const result = await paidRes.json();
|
|
955
|
+
if (!paidRes.ok) {
|
|
956
|
+
throw new Error(result.error || "Payment verification failed");
|
|
957
|
+
}
|
|
958
|
+
this.recordSpending(amountDisplay);
|
|
959
|
+
console.log(`[MoltsPay] Success!`);
|
|
960
|
+
return result.result || result;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Handle BNB Chain payment flow (pre-approval + intent signature)
|
|
964
|
+
*
|
|
965
|
+
* Flow:
|
|
966
|
+
* 1. Check client has approved server wallet (done via `moltspay init`)
|
|
967
|
+
* 2. Sign EIP-712 payment intent (no gas, just signature)
|
|
968
|
+
* 3. Send intent to server
|
|
969
|
+
* 4. Server executes service
|
|
970
|
+
* 5. Server calls transferFrom if successful (pay-for-success)
|
|
971
|
+
*/
|
|
972
|
+
async handleBNBPayment(executeUrl, service, params, paymentDetails, options = {}) {
|
|
973
|
+
const { to, amount, token, chainName, chain, spender } = paymentDetails;
|
|
974
|
+
const tokenConfig = chain.tokens[token];
|
|
975
|
+
const provider = new ethers.JsonRpcProvider(chain.rpc);
|
|
976
|
+
const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
|
|
977
|
+
const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
|
|
978
|
+
if (allowance < amountWeiCheck) {
|
|
979
|
+
const nativeBalance = await provider.getBalance(this.wallet.address);
|
|
980
|
+
const minGasBalance = ethers.parseEther("0.0005");
|
|
981
|
+
if (nativeBalance < minGasBalance) {
|
|
982
|
+
const nativeBNB = parseFloat(ethers.formatEther(nativeBalance)).toFixed(4);
|
|
983
|
+
const isTestnet = chainName === "bnb_testnet";
|
|
984
|
+
if (isTestnet) {
|
|
985
|
+
throw new Error(
|
|
986
|
+
`\u274C Insufficient tBNB for approval transaction
|
|
987
|
+
|
|
988
|
+
Current tBNB: ${nativeBNB}
|
|
989
|
+
Required: ~0.001 tBNB
|
|
990
|
+
|
|
991
|
+
Get testnet tokens: npx moltspay faucet --chain bnb_testnet
|
|
992
|
+
(Gives USDC + tBNB for gas)`
|
|
993
|
+
);
|
|
994
|
+
} else {
|
|
995
|
+
throw new Error(
|
|
996
|
+
`\u274C Insufficient BNB for approval transaction
|
|
997
|
+
|
|
998
|
+
Current BNB: ${nativeBNB}
|
|
999
|
+
Required: ~0.001 BNB (~$0.60)
|
|
1000
|
+
|
|
1001
|
+
To get BNB:
|
|
1002
|
+
\u2022 Withdraw from Binance/exchange to your wallet
|
|
1003
|
+
\u2022 Most exchanges include BNB dust with withdrawals
|
|
1004
|
+
|
|
1005
|
+
After funding, run:
|
|
1006
|
+
npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
throw new Error(
|
|
1011
|
+
`Insufficient allowance for ${spender.slice(0, 10)}...
|
|
1012
|
+
Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
|
|
1016
|
+
const intent = {
|
|
1017
|
+
from: this.wallet.address,
|
|
1018
|
+
to,
|
|
1019
|
+
amount: amountWei,
|
|
1020
|
+
token: tokenConfig.address,
|
|
1021
|
+
service,
|
|
1022
|
+
nonce: Date.now(),
|
|
1023
|
+
// Use timestamp as nonce for simplicity
|
|
1024
|
+
deadline: Date.now() + 36e5
|
|
1025
|
+
// 1 hour
|
|
1026
|
+
};
|
|
1027
|
+
const domain = {
|
|
1028
|
+
name: "MoltsPay",
|
|
1029
|
+
version: "1",
|
|
1030
|
+
chainId: chain.chainId
|
|
1031
|
+
};
|
|
1032
|
+
const types = {
|
|
1033
|
+
PaymentIntent: [
|
|
1034
|
+
{ name: "from", type: "address" },
|
|
1035
|
+
{ name: "to", type: "address" },
|
|
1036
|
+
{ name: "amount", type: "uint256" },
|
|
1037
|
+
{ name: "token", type: "address" },
|
|
1038
|
+
{ name: "service", type: "string" },
|
|
1039
|
+
{ name: "nonce", type: "uint256" },
|
|
1040
|
+
{ name: "deadline", type: "uint256" }
|
|
1041
|
+
]
|
|
1042
|
+
};
|
|
1043
|
+
console.log(`[MoltsPay] Signing BNB payment intent...`);
|
|
1044
|
+
const signature = await this.wallet.signTypedData(domain, types, intent);
|
|
1045
|
+
const network = `eip155:${chain.chainId}`;
|
|
1046
|
+
const payload = {
|
|
1047
|
+
x402Version: 2,
|
|
1048
|
+
scheme: "exact",
|
|
1049
|
+
network,
|
|
1050
|
+
payload: {
|
|
1051
|
+
intent: {
|
|
1052
|
+
...intent,
|
|
1053
|
+
signature
|
|
1054
|
+
},
|
|
1055
|
+
chainId: chain.chainId
|
|
1056
|
+
},
|
|
1057
|
+
accepted: {
|
|
1058
|
+
scheme: "exact",
|
|
1059
|
+
network,
|
|
1060
|
+
asset: tokenConfig.address,
|
|
1061
|
+
amount: amountWei,
|
|
1062
|
+
payTo: to,
|
|
1063
|
+
maxTimeoutSeconds: 300
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
1067
|
+
console.log(`[MoltsPay] Sending BNB payment request...`);
|
|
1068
|
+
const bnbRequestBody = options.rawData ? { service, ...params, chain: chainName } : { service, params, chain: chainName };
|
|
1069
|
+
const paidRes = await fetch(executeUrl, {
|
|
1070
|
+
method: "POST",
|
|
1071
|
+
headers: {
|
|
1072
|
+
"Content-Type": "application/json",
|
|
1073
|
+
"X-Payment": paymentHeader
|
|
1074
|
+
},
|
|
1075
|
+
body: JSON.stringify(bnbRequestBody)
|
|
1076
|
+
});
|
|
1077
|
+
const result = await paidRes.json();
|
|
1078
|
+
if (!paidRes.ok) {
|
|
1079
|
+
throw new Error(result.error || "BNB payment failed");
|
|
1080
|
+
}
|
|
1081
|
+
this.recordSpending(amount);
|
|
1082
|
+
console.log(`[MoltsPay] Success! BNB payment settled.`);
|
|
1083
|
+
return result.result || result;
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Handle Solana payment flow
|
|
1087
|
+
*
|
|
1088
|
+
* Solana uses SPL token transfers with pay-for-success model:
|
|
1089
|
+
* 1. Client creates and signs a transfer transaction
|
|
1090
|
+
* 2. Server submits the transaction after service completes
|
|
1091
|
+
*/
|
|
1092
|
+
async handleSolanaPayment(executeUrl, service, params, requirements, chain, options = {}) {
|
|
1093
|
+
const solanaWallet = loadSolanaWallet(this.configDir);
|
|
1094
|
+
if (!solanaWallet) {
|
|
1095
|
+
throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
|
|
1096
|
+
}
|
|
1097
|
+
const amount = Number(requirements.amount);
|
|
1098
|
+
const amountUSDC = amount / 1e6;
|
|
1099
|
+
this.checkLimits(amountUSDC);
|
|
1100
|
+
console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
|
|
1101
|
+
if (!requirements.payTo) {
|
|
1102
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
1103
|
+
}
|
|
1104
|
+
const solanaFeePayer = requirements.extra?.solanaFeePayer;
|
|
1105
|
+
const feePayerPubkey = solanaFeePayer ? new PublicKey4(solanaFeePayer) : void 0;
|
|
1106
|
+
if (feePayerPubkey) {
|
|
1107
|
+
console.log(`[MoltsPay] Gasless mode: server pays fees`);
|
|
1108
|
+
}
|
|
1109
|
+
const recipientPubkey = new PublicKey4(requirements.payTo);
|
|
1110
|
+
const transaction = await createSolanaPaymentTransaction(
|
|
1111
|
+
solanaWallet.publicKey,
|
|
1112
|
+
recipientPubkey,
|
|
1113
|
+
BigInt(amount),
|
|
1114
|
+
chain,
|
|
1115
|
+
feePayerPubkey
|
|
1116
|
+
// Optional fee payer for gasless mode
|
|
1117
|
+
);
|
|
1118
|
+
if (feePayerPubkey) {
|
|
1119
|
+
transaction.partialSign(solanaWallet);
|
|
1120
|
+
} else {
|
|
1121
|
+
transaction.sign(solanaWallet);
|
|
1122
|
+
}
|
|
1123
|
+
const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
|
|
1124
|
+
console.log(`[MoltsPay] Transaction signed, sending to server...`);
|
|
1125
|
+
const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
1126
|
+
const payload = {
|
|
1127
|
+
x402Version: 2,
|
|
1128
|
+
scheme: "exact",
|
|
1129
|
+
network,
|
|
1130
|
+
payload: {
|
|
1131
|
+
signedTransaction: signedTx,
|
|
1132
|
+
sender: solanaWallet.publicKey.toBase58(),
|
|
1133
|
+
chain
|
|
1134
|
+
},
|
|
1135
|
+
accepted: {
|
|
1136
|
+
scheme: "exact",
|
|
1137
|
+
network,
|
|
1138
|
+
asset: requirements.asset,
|
|
1139
|
+
amount: requirements.amount,
|
|
1140
|
+
payTo: requirements.payTo,
|
|
1141
|
+
maxTimeoutSeconds: 300
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
1145
|
+
const solanaRequestBody = options.rawData ? { service, ...params, chain } : { service, params, chain };
|
|
1146
|
+
const paidRes = await fetch(executeUrl, {
|
|
1147
|
+
method: "POST",
|
|
1148
|
+
headers: {
|
|
1149
|
+
"Content-Type": "application/json",
|
|
1150
|
+
"X-Payment": paymentHeader
|
|
1151
|
+
},
|
|
1152
|
+
body: JSON.stringify(solanaRequestBody)
|
|
1153
|
+
});
|
|
1154
|
+
const result = await paidRes.json();
|
|
1155
|
+
if (!paidRes.ok) {
|
|
1156
|
+
throw new Error(result.error || "Solana payment failed");
|
|
1157
|
+
}
|
|
1158
|
+
this.recordSpending(amountUSDC);
|
|
1159
|
+
console.log(`[MoltsPay] Success! Solana payment settled.`);
|
|
1160
|
+
if (result.payment?.transaction) {
|
|
1161
|
+
const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
|
|
1162
|
+
console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
|
|
1163
|
+
}
|
|
1164
|
+
return result.result || result;
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Check ERC20 allowance for a spender
|
|
1168
|
+
*/
|
|
1169
|
+
async checkAllowance(tokenAddress, spender, provider) {
|
|
1170
|
+
const contract = new ethers.Contract(
|
|
1171
|
+
tokenAddress,
|
|
1172
|
+
["function allowance(address owner, address spender) view returns (uint256)"],
|
|
1173
|
+
provider
|
|
1174
|
+
);
|
|
1175
|
+
return await contract.allowance(this.wallet.address, spender);
|
|
395
1176
|
}
|
|
396
1177
|
/**
|
|
397
1178
|
* Sign EIP-3009 transferWithAuthorization (GASLESS)
|
|
@@ -463,26 +1244,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
463
1244
|
}
|
|
464
1245
|
// --- Config & Wallet Management ---
|
|
465
1246
|
loadConfig() {
|
|
466
|
-
const configPath =
|
|
467
|
-
if (
|
|
468
|
-
const content =
|
|
1247
|
+
const configPath = join2(this.configDir, "config.json");
|
|
1248
|
+
if (existsSync2(configPath)) {
|
|
1249
|
+
const content = readFileSync2(configPath, "utf-8");
|
|
469
1250
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
470
1251
|
}
|
|
471
1252
|
return { ...DEFAULT_CONFIG };
|
|
472
1253
|
}
|
|
473
1254
|
saveConfig() {
|
|
474
|
-
|
|
475
|
-
const configPath =
|
|
476
|
-
|
|
1255
|
+
mkdirSync2(this.configDir, { recursive: true });
|
|
1256
|
+
const configPath = join2(this.configDir, "config.json");
|
|
1257
|
+
writeFileSync2(configPath, JSON.stringify(this.config, null, 2));
|
|
477
1258
|
}
|
|
478
1259
|
/**
|
|
479
1260
|
* Load spending data from disk
|
|
480
1261
|
*/
|
|
481
1262
|
loadSpending() {
|
|
482
|
-
const spendingPath =
|
|
483
|
-
if (
|
|
1263
|
+
const spendingPath = join2(this.configDir, "spending.json");
|
|
1264
|
+
if (existsSync2(spendingPath)) {
|
|
484
1265
|
try {
|
|
485
|
-
const data = JSON.parse(
|
|
1266
|
+
const data = JSON.parse(readFileSync2(spendingPath, "utf-8"));
|
|
486
1267
|
const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
|
|
487
1268
|
if (data.date && data.date === today) {
|
|
488
1269
|
this.todaySpending = data.amount || 0;
|
|
@@ -501,18 +1282,18 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
501
1282
|
* Save spending data to disk
|
|
502
1283
|
*/
|
|
503
1284
|
saveSpending() {
|
|
504
|
-
|
|
505
|
-
const spendingPath =
|
|
1285
|
+
mkdirSync2(this.configDir, { recursive: true });
|
|
1286
|
+
const spendingPath = join2(this.configDir, "spending.json");
|
|
506
1287
|
const data = {
|
|
507
1288
|
date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
|
|
508
1289
|
amount: this.todaySpending,
|
|
509
1290
|
updatedAt: Date.now()
|
|
510
1291
|
};
|
|
511
|
-
|
|
1292
|
+
writeFileSync2(spendingPath, JSON.stringify(data, null, 2));
|
|
512
1293
|
}
|
|
513
1294
|
loadWallet() {
|
|
514
|
-
const walletPath =
|
|
515
|
-
if (
|
|
1295
|
+
const walletPath = join2(this.configDir, "wallet.json");
|
|
1296
|
+
if (existsSync2(walletPath)) {
|
|
516
1297
|
try {
|
|
517
1298
|
const stats = statSync(walletPath);
|
|
518
1299
|
const mode = stats.mode & 511;
|
|
@@ -523,7 +1304,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
523
1304
|
}
|
|
524
1305
|
} catch (err) {
|
|
525
1306
|
}
|
|
526
|
-
const content =
|
|
1307
|
+
const content = readFileSync2(walletPath, "utf-8");
|
|
527
1308
|
return JSON.parse(content);
|
|
528
1309
|
}
|
|
529
1310
|
return null;
|
|
@@ -532,15 +1313,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
532
1313
|
* Initialize a new wallet (called by CLI)
|
|
533
1314
|
*/
|
|
534
1315
|
static init(configDir, options) {
|
|
535
|
-
|
|
1316
|
+
mkdirSync2(configDir, { recursive: true });
|
|
536
1317
|
const wallet = Wallet.createRandom();
|
|
537
1318
|
const walletData = {
|
|
538
1319
|
address: wallet.address,
|
|
539
1320
|
privateKey: wallet.privateKey,
|
|
540
1321
|
createdAt: Date.now()
|
|
541
1322
|
};
|
|
542
|
-
const walletPath =
|
|
543
|
-
|
|
1323
|
+
const walletPath = join2(configDir, "wallet.json");
|
|
1324
|
+
writeFileSync2(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
544
1325
|
const config = {
|
|
545
1326
|
chain: options.chain,
|
|
546
1327
|
limits: {
|
|
@@ -548,8 +1329,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
548
1329
|
maxPerDay: options.maxPerDay
|
|
549
1330
|
}
|
|
550
1331
|
};
|
|
551
|
-
const configPath =
|
|
552
|
-
|
|
1332
|
+
const configPath = join2(configDir, "config.json");
|
|
1333
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2));
|
|
553
1334
|
return { address: wallet.address, configDir };
|
|
554
1335
|
}
|
|
555
1336
|
/**
|
|
@@ -585,7 +1366,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
585
1366
|
if (!this.wallet) {
|
|
586
1367
|
throw new Error("Client not initialized");
|
|
587
1368
|
}
|
|
588
|
-
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
|
|
1369
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
|
|
589
1370
|
const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
590
1371
|
const results = {};
|
|
591
1372
|
const tempoTokens = {
|
|
@@ -656,12 +1437,12 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
656
1437
|
if (!this.wallet || !this.walletData) {
|
|
657
1438
|
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
658
1439
|
}
|
|
659
|
-
const { privateKeyToAccount } = await import("viem/accounts");
|
|
1440
|
+
const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
|
|
660
1441
|
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
661
1442
|
const { tempoModerato } = await import("viem/chains");
|
|
662
1443
|
const { Actions } = await import("viem/tempo");
|
|
663
1444
|
const privateKey = this.walletData.privateKey;
|
|
664
|
-
const account =
|
|
1445
|
+
const account = privateKeyToAccount2(privateKey);
|
|
665
1446
|
console.log(`[MoltsPay] Making MPP request to: ${url}`);
|
|
666
1447
|
console.log(`[MoltsPay] Using account: ${account.address}`);
|
|
667
1448
|
const initResponse = await fetch(url, {
|
|
@@ -758,24 +1539,16 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
758
1539
|
|
|
759
1540
|
// src/server/index.ts
|
|
760
1541
|
init_esm_shims();
|
|
761
|
-
import { readFileSync as
|
|
1542
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
762
1543
|
import { createServer } from "http";
|
|
763
1544
|
import * as path3 from "path";
|
|
764
1545
|
|
|
765
1546
|
// src/facilitators/index.ts
|
|
766
1547
|
init_esm_shims();
|
|
767
1548
|
|
|
768
|
-
// src/facilitators/interface.ts
|
|
769
|
-
init_esm_shims();
|
|
770
|
-
var BaseFacilitator = class {
|
|
771
|
-
supportsNetwork(network) {
|
|
772
|
-
return this.supportedNetworks.includes(network);
|
|
773
|
-
}
|
|
774
|
-
};
|
|
775
|
-
|
|
776
1549
|
// src/facilitators/cdp.ts
|
|
777
1550
|
init_esm_shims();
|
|
778
|
-
import { readFileSync as
|
|
1551
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
|
|
779
1552
|
import * as path2 from "path";
|
|
780
1553
|
var X402_VERSION2 = 2;
|
|
781
1554
|
var CDP_URL = "https://api.cdp.coinbase.com/platform/v2/x402";
|
|
@@ -786,9 +1559,9 @@ function loadEnvFile() {
|
|
|
786
1559
|
path2.join(process.env.HOME || "", ".moltspay", ".env")
|
|
787
1560
|
];
|
|
788
1561
|
for (const envPath of envPaths) {
|
|
789
|
-
if (
|
|
1562
|
+
if (existsSync3(envPath)) {
|
|
790
1563
|
try {
|
|
791
|
-
const content =
|
|
1564
|
+
const content = readFileSync3(envPath, "utf-8");
|
|
792
1565
|
for (const line of content.split("\n")) {
|
|
793
1566
|
const trimmed = line.trim();
|
|
794
1567
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -1024,18 +1797,280 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
1024
1797
|
if (chainId !== 42431) {
|
|
1025
1798
|
return { healthy: false, error: `Wrong chainId: ${chainId}` };
|
|
1026
1799
|
}
|
|
1027
|
-
return { healthy: true, latencyMs: Date.now() - start };
|
|
1800
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
1801
|
+
} catch (error) {
|
|
1802
|
+
return { healthy: false, error: String(error) };
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
async verify(paymentPayload, requirements) {
|
|
1806
|
+
try {
|
|
1807
|
+
const tempoPayload = paymentPayload.payload;
|
|
1808
|
+
if (!tempoPayload?.txHash) {
|
|
1809
|
+
return { valid: false, error: "Missing txHash in payment payload" };
|
|
1810
|
+
}
|
|
1811
|
+
const receipt = await this.getTransactionReceipt(tempoPayload.txHash);
|
|
1812
|
+
if (!receipt) {
|
|
1813
|
+
return { valid: false, error: "Transaction not found" };
|
|
1814
|
+
}
|
|
1815
|
+
if (receipt.status !== "0x1") {
|
|
1816
|
+
return { valid: false, error: "Transaction failed" };
|
|
1817
|
+
}
|
|
1818
|
+
const transferLog = receipt.logs.find(
|
|
1819
|
+
(log) => log.topics[0] === TRANSFER_EVENT_TOPIC
|
|
1820
|
+
);
|
|
1821
|
+
if (!transferLog) {
|
|
1822
|
+
return { valid: false, error: "No Transfer event found" };
|
|
1823
|
+
}
|
|
1824
|
+
const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
|
|
1825
|
+
const expectedTo = requirements.payTo.toLowerCase();
|
|
1826
|
+
if (toAddress !== expectedTo) {
|
|
1827
|
+
return {
|
|
1828
|
+
valid: false,
|
|
1829
|
+
error: `Wrong recipient: ${toAddress}, expected ${expectedTo}`
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
const amount = BigInt(transferLog.data);
|
|
1833
|
+
const expectedAmount = BigInt(requirements.amount);
|
|
1834
|
+
if (amount < expectedAmount) {
|
|
1835
|
+
return {
|
|
1836
|
+
valid: false,
|
|
1837
|
+
error: `Insufficient amount: ${amount}, expected ${expectedAmount}`
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
const tokenAddress = transferLog.address.toLowerCase();
|
|
1841
|
+
const expectedToken = requirements.asset.toLowerCase();
|
|
1842
|
+
if (tokenAddress !== expectedToken) {
|
|
1843
|
+
return {
|
|
1844
|
+
valid: false,
|
|
1845
|
+
error: `Wrong token: ${tokenAddress}, expected ${expectedToken}`
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
return {
|
|
1849
|
+
valid: true,
|
|
1850
|
+
details: {
|
|
1851
|
+
txHash: tempoPayload.txHash,
|
|
1852
|
+
from: "0x" + transferLog.topics[1].slice(26),
|
|
1853
|
+
to: toAddress,
|
|
1854
|
+
amount: amount.toString(),
|
|
1855
|
+
token: tokenAddress
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
} catch (error) {
|
|
1859
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
async settle(paymentPayload, requirements) {
|
|
1863
|
+
const verifyResult = await this.verify(paymentPayload, requirements);
|
|
1864
|
+
if (!verifyResult.valid) {
|
|
1865
|
+
return { success: false, error: verifyResult.error };
|
|
1866
|
+
}
|
|
1867
|
+
const tempoPayload = paymentPayload.payload;
|
|
1868
|
+
return {
|
|
1869
|
+
success: true,
|
|
1870
|
+
transaction: tempoPayload.txHash,
|
|
1871
|
+
status: "settled"
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
async getTransactionReceipt(txHash) {
|
|
1875
|
+
const response = await fetch(this.rpcUrl, {
|
|
1876
|
+
method: "POST",
|
|
1877
|
+
headers: { "Content-Type": "application/json" },
|
|
1878
|
+
body: JSON.stringify({
|
|
1879
|
+
jsonrpc: "2.0",
|
|
1880
|
+
method: "eth_getTransactionReceipt",
|
|
1881
|
+
params: [txHash],
|
|
1882
|
+
id: 1
|
|
1883
|
+
})
|
|
1884
|
+
});
|
|
1885
|
+
const data = await response.json();
|
|
1886
|
+
return data.result;
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1889
|
+
|
|
1890
|
+
// src/facilitators/bnb.ts
|
|
1891
|
+
init_esm_shims();
|
|
1892
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
1893
|
+
var TRANSFER_EVENT_TOPIC2 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
1894
|
+
var EIP712_DOMAIN = {
|
|
1895
|
+
name: "MoltsPay",
|
|
1896
|
+
version: "1"
|
|
1897
|
+
};
|
|
1898
|
+
var INTENT_TYPES = {
|
|
1899
|
+
PaymentIntent: [
|
|
1900
|
+
{ name: "from", type: "address" },
|
|
1901
|
+
{ name: "to", type: "address" },
|
|
1902
|
+
{ name: "amount", type: "uint256" },
|
|
1903
|
+
{ name: "token", type: "address" },
|
|
1904
|
+
{ name: "service", type: "string" },
|
|
1905
|
+
{ name: "nonce", type: "uint256" },
|
|
1906
|
+
{ name: "deadline", type: "uint256" }
|
|
1907
|
+
]
|
|
1908
|
+
};
|
|
1909
|
+
var BNBFacilitator = class extends BaseFacilitator {
|
|
1910
|
+
name = "bnb";
|
|
1911
|
+
displayName = "BNB Smart Chain";
|
|
1912
|
+
supportedNetworks = ["eip155:56", "eip155:97"];
|
|
1913
|
+
// Mainnet + Testnet
|
|
1914
|
+
serverPrivateKey;
|
|
1915
|
+
spenderAddress = null;
|
|
1916
|
+
chainConfigs;
|
|
1917
|
+
constructor(serverPrivateKey) {
|
|
1918
|
+
super();
|
|
1919
|
+
this.serverPrivateKey = serverPrivateKey || process.env.BNB_SERVER_PRIVATE_KEY || "";
|
|
1920
|
+
if (this.serverPrivateKey) {
|
|
1921
|
+
const key = this.serverPrivateKey.startsWith("0x") ? this.serverPrivateKey : `0x${this.serverPrivateKey}`;
|
|
1922
|
+
const account = privateKeyToAccount(key);
|
|
1923
|
+
this.spenderAddress = account.address;
|
|
1924
|
+
}
|
|
1925
|
+
this.chainConfigs = {
|
|
1926
|
+
56: { rpc: CHAINS.bnb.rpc, chain: CHAINS.bnb },
|
|
1927
|
+
97: { rpc: CHAINS.bnb_testnet.rpc, chain: CHAINS.bnb_testnet }
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
async healthCheck() {
|
|
1931
|
+
const start = Date.now();
|
|
1932
|
+
try {
|
|
1933
|
+
const response = await fetch(this.chainConfigs[56].rpc, {
|
|
1934
|
+
method: "POST",
|
|
1935
|
+
headers: { "Content-Type": "application/json" },
|
|
1936
|
+
body: JSON.stringify({
|
|
1937
|
+
jsonrpc: "2.0",
|
|
1938
|
+
method: "eth_chainId",
|
|
1939
|
+
params: [],
|
|
1940
|
+
id: 1
|
|
1941
|
+
})
|
|
1942
|
+
});
|
|
1943
|
+
const data = await response.json();
|
|
1944
|
+
const chainId = parseInt(data.result, 16);
|
|
1945
|
+
if (chainId !== 56) {
|
|
1946
|
+
return { healthy: false, error: `Wrong chainId: ${chainId}` };
|
|
1947
|
+
}
|
|
1948
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
1949
|
+
} catch (error) {
|
|
1950
|
+
return { healthy: false, error: String(error) };
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
/**
|
|
1954
|
+
* Verify a payment intent signature (before service execution)
|
|
1955
|
+
*
|
|
1956
|
+
* This verifies:
|
|
1957
|
+
* 1. Signature is valid for the intent
|
|
1958
|
+
* 2. Client has approved server wallet
|
|
1959
|
+
* 3. Client has sufficient balance
|
|
1960
|
+
* 4. Intent hasn't expired
|
|
1961
|
+
*/
|
|
1962
|
+
async verify(paymentPayload, requirements) {
|
|
1963
|
+
try {
|
|
1964
|
+
const bnbPayload = paymentPayload.payload;
|
|
1965
|
+
if (!bnbPayload?.intent) {
|
|
1966
|
+
return { valid: false, error: "Missing intent in payment payload" };
|
|
1967
|
+
}
|
|
1968
|
+
const { intent, chainId } = bnbPayload;
|
|
1969
|
+
const config = this.chainConfigs[chainId];
|
|
1970
|
+
if (!config) {
|
|
1971
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
1972
|
+
}
|
|
1973
|
+
if (intent.deadline < Date.now()) {
|
|
1974
|
+
return { valid: false, error: "Intent expired" };
|
|
1975
|
+
}
|
|
1976
|
+
const recoveredAddress = await this.recoverIntentSigner(intent, chainId);
|
|
1977
|
+
if (recoveredAddress.toLowerCase() !== intent.from.toLowerCase()) {
|
|
1978
|
+
return { valid: false, error: "Invalid signature" };
|
|
1979
|
+
}
|
|
1980
|
+
if (intent.to.toLowerCase() !== requirements.payTo.toLowerCase()) {
|
|
1981
|
+
return { valid: false, error: `Wrong recipient: ${intent.to}` };
|
|
1982
|
+
}
|
|
1983
|
+
if (BigInt(intent.amount) < BigInt(requirements.amount)) {
|
|
1984
|
+
return { valid: false, error: `Insufficient amount: ${intent.amount}` };
|
|
1985
|
+
}
|
|
1986
|
+
if (intent.token.toLowerCase() !== requirements.asset.toLowerCase()) {
|
|
1987
|
+
return { valid: false, error: `Wrong token: ${intent.token}` };
|
|
1988
|
+
}
|
|
1989
|
+
const serverAddress = await this.getServerAddress();
|
|
1990
|
+
const allowance = await this.getAllowance(intent.from, serverAddress, intent.token, config.rpc);
|
|
1991
|
+
if (BigInt(allowance) < BigInt(intent.amount)) {
|
|
1992
|
+
return { valid: false, error: "Insufficient allowance. Run: npx moltspay init --chain bnb" };
|
|
1993
|
+
}
|
|
1994
|
+
const balance = await this.getBalance(intent.from, intent.token, config.rpc);
|
|
1995
|
+
if (BigInt(balance) < BigInt(intent.amount)) {
|
|
1996
|
+
return { valid: false, error: "Insufficient balance" };
|
|
1997
|
+
}
|
|
1998
|
+
return {
|
|
1999
|
+
valid: true,
|
|
2000
|
+
details: {
|
|
2001
|
+
from: intent.from,
|
|
2002
|
+
to: intent.to,
|
|
2003
|
+
amount: intent.amount,
|
|
2004
|
+
token: intent.token,
|
|
2005
|
+
service: intent.service,
|
|
2006
|
+
nonce: intent.nonce,
|
|
2007
|
+
deadline: intent.deadline
|
|
2008
|
+
}
|
|
2009
|
+
};
|
|
2010
|
+
} catch (error) {
|
|
2011
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
/**
|
|
2015
|
+
* Settle a payment by executing transferFrom
|
|
2016
|
+
*
|
|
2017
|
+
* This is called AFTER the service has been successfully delivered.
|
|
2018
|
+
* Server pays gas, transfers tokens from client to provider.
|
|
2019
|
+
*/
|
|
2020
|
+
async settle(paymentPayload, requirements) {
|
|
2021
|
+
if (!this.serverPrivateKey) {
|
|
2022
|
+
return { success: false, error: "Server wallet not configured (BNB_SERVER_PRIVATE_KEY)" };
|
|
2023
|
+
}
|
|
2024
|
+
try {
|
|
2025
|
+
const verifyResult = await this.verify(paymentPayload, requirements);
|
|
2026
|
+
if (!verifyResult.valid) {
|
|
2027
|
+
return { success: false, error: verifyResult.error };
|
|
2028
|
+
}
|
|
2029
|
+
const bnbPayload = paymentPayload.payload;
|
|
2030
|
+
const { intent, chainId } = bnbPayload;
|
|
2031
|
+
const config = this.chainConfigs[chainId];
|
|
2032
|
+
const txHash = await this.executeTransferFrom(
|
|
2033
|
+
intent.from,
|
|
2034
|
+
intent.to,
|
|
2035
|
+
intent.amount,
|
|
2036
|
+
intent.token,
|
|
2037
|
+
config.rpc
|
|
2038
|
+
);
|
|
2039
|
+
return {
|
|
2040
|
+
success: true,
|
|
2041
|
+
transaction: txHash,
|
|
2042
|
+
status: "settled"
|
|
2043
|
+
};
|
|
1028
2044
|
} catch (error) {
|
|
1029
|
-
return {
|
|
2045
|
+
return { success: false, error: `Settlement failed: ${error}` };
|
|
1030
2046
|
}
|
|
1031
2047
|
}
|
|
1032
|
-
|
|
2048
|
+
/**
|
|
2049
|
+
* Check if client has approved the server wallet
|
|
2050
|
+
*/
|
|
2051
|
+
async checkApproval(clientAddress, token, chainId) {
|
|
2052
|
+
const config = this.chainConfigs[chainId];
|
|
2053
|
+
if (!config) {
|
|
2054
|
+
throw new Error(`Unsupported chainId: ${chainId}`);
|
|
2055
|
+
}
|
|
2056
|
+
const serverAddress = await this.getServerAddress();
|
|
2057
|
+
const allowance = await this.getAllowance(clientAddress, serverAddress, token, config.rpc);
|
|
2058
|
+
const minAllowance = BigInt("1000000000000000000000");
|
|
2059
|
+
return {
|
|
2060
|
+
approved: BigInt(allowance) >= minAllowance,
|
|
2061
|
+
allowance
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
/**
|
|
2065
|
+
* Verify a completed transaction (for checking past payments)
|
|
2066
|
+
*/
|
|
2067
|
+
async verifyTransaction(txHash, expected, chainId) {
|
|
2068
|
+
const config = this.chainConfigs[chainId];
|
|
2069
|
+
if (!config) {
|
|
2070
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
2071
|
+
}
|
|
1033
2072
|
try {
|
|
1034
|
-
const
|
|
1035
|
-
if (!tempoPayload?.txHash) {
|
|
1036
|
-
return { valid: false, error: "Missing txHash in payment payload" };
|
|
1037
|
-
}
|
|
1038
|
-
const receipt = await this.getTransactionReceipt(tempoPayload.txHash);
|
|
2073
|
+
const receipt = await this.getTransactionReceipt(txHash, config.rpc);
|
|
1039
2074
|
if (!receipt) {
|
|
1040
2075
|
return { valid: false, error: "Transaction not found" };
|
|
1041
2076
|
}
|
|
@@ -1043,63 +2078,117 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
1043
2078
|
return { valid: false, error: "Transaction failed" };
|
|
1044
2079
|
}
|
|
1045
2080
|
const transferLog = receipt.logs.find(
|
|
1046
|
-
(log) => log.topics[0] ===
|
|
2081
|
+
(log) => log.topics[0] === TRANSFER_EVENT_TOPIC2 && log.address.toLowerCase() === expected.token.toLowerCase()
|
|
1047
2082
|
);
|
|
1048
2083
|
if (!transferLog) {
|
|
1049
2084
|
return { valid: false, error: "No Transfer event found" };
|
|
1050
2085
|
}
|
|
1051
2086
|
const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
return {
|
|
1055
|
-
valid: false,
|
|
1056
|
-
error: `Wrong recipient: ${toAddress}, expected ${expectedTo}`
|
|
1057
|
-
};
|
|
2087
|
+
if (toAddress !== expected.to.toLowerCase()) {
|
|
2088
|
+
return { valid: false, error: `Wrong recipient: ${toAddress}` };
|
|
1058
2089
|
}
|
|
1059
2090
|
const amount = BigInt(transferLog.data);
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
return {
|
|
1063
|
-
valid: false,
|
|
1064
|
-
error: `Insufficient amount: ${amount}, expected ${expectedAmount}`
|
|
1065
|
-
};
|
|
1066
|
-
}
|
|
1067
|
-
const tokenAddress = transferLog.address.toLowerCase();
|
|
1068
|
-
const expectedToken = requirements.asset.toLowerCase();
|
|
1069
|
-
if (tokenAddress !== expectedToken) {
|
|
1070
|
-
return {
|
|
1071
|
-
valid: false,
|
|
1072
|
-
error: `Wrong token: ${tokenAddress}, expected ${expectedToken}`
|
|
1073
|
-
};
|
|
2091
|
+
if (amount < BigInt(expected.amount)) {
|
|
2092
|
+
return { valid: false, error: `Insufficient amount: ${amount}` };
|
|
1074
2093
|
}
|
|
1075
2094
|
return {
|
|
1076
2095
|
valid: true,
|
|
1077
2096
|
details: {
|
|
1078
|
-
txHash
|
|
2097
|
+
txHash,
|
|
1079
2098
|
from: "0x" + transferLog.topics[1].slice(26),
|
|
1080
2099
|
to: toAddress,
|
|
1081
2100
|
amount: amount.toString(),
|
|
1082
|
-
token:
|
|
2101
|
+
token: transferLog.address
|
|
1083
2102
|
}
|
|
1084
2103
|
};
|
|
1085
2104
|
} catch (error) {
|
|
1086
2105
|
return { valid: false, error: `Verification failed: ${error}` };
|
|
1087
2106
|
}
|
|
1088
2107
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
return
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
2108
|
+
// ==================== Private Methods ====================
|
|
2109
|
+
/**
|
|
2110
|
+
* Get the server's spender address (public, for 402 responses)
|
|
2111
|
+
* Returns cached value computed at construction time.
|
|
2112
|
+
*/
|
|
2113
|
+
getSpenderAddress() {
|
|
2114
|
+
return this.spenderAddress;
|
|
2115
|
+
}
|
|
2116
|
+
async getServerAddress() {
|
|
2117
|
+
const { ethers: ethers3 } = await import("ethers");
|
|
2118
|
+
const wallet = new ethers3.Wallet(this.serverPrivateKey);
|
|
2119
|
+
return wallet.address;
|
|
2120
|
+
}
|
|
2121
|
+
async recoverIntentSigner(intent, chainId) {
|
|
2122
|
+
const { ethers: ethers3 } = await import("ethers");
|
|
2123
|
+
const domain = {
|
|
2124
|
+
...EIP712_DOMAIN,
|
|
2125
|
+
chainId
|
|
2126
|
+
};
|
|
2127
|
+
const message = {
|
|
2128
|
+
from: intent.from,
|
|
2129
|
+
to: intent.to,
|
|
2130
|
+
amount: intent.amount,
|
|
2131
|
+
token: intent.token,
|
|
2132
|
+
service: intent.service,
|
|
2133
|
+
nonce: intent.nonce,
|
|
2134
|
+
deadline: intent.deadline
|
|
1099
2135
|
};
|
|
2136
|
+
const recoveredAddress = ethers3.verifyTypedData(
|
|
2137
|
+
domain,
|
|
2138
|
+
INTENT_TYPES,
|
|
2139
|
+
message,
|
|
2140
|
+
intent.signature
|
|
2141
|
+
);
|
|
2142
|
+
return recoveredAddress;
|
|
1100
2143
|
}
|
|
1101
|
-
async
|
|
1102
|
-
const
|
|
2144
|
+
async getAllowance(owner, spender, token, rpcUrl) {
|
|
2145
|
+
const selector = "0xdd62ed3e";
|
|
2146
|
+
const ownerPadded = owner.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
2147
|
+
const spenderPadded = spender.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
2148
|
+
const data = selector + ownerPadded + spenderPadded;
|
|
2149
|
+
const response = await fetch(rpcUrl, {
|
|
2150
|
+
method: "POST",
|
|
2151
|
+
headers: { "Content-Type": "application/json" },
|
|
2152
|
+
body: JSON.stringify({
|
|
2153
|
+
jsonrpc: "2.0",
|
|
2154
|
+
method: "eth_call",
|
|
2155
|
+
params: [{ to: token, data }, "latest"],
|
|
2156
|
+
id: 1
|
|
2157
|
+
})
|
|
2158
|
+
});
|
|
2159
|
+
const result = await response.json();
|
|
2160
|
+
return result.result || "0x0";
|
|
2161
|
+
}
|
|
2162
|
+
async getBalance(account, token, rpcUrl) {
|
|
2163
|
+
const selector = "0x70a08231";
|
|
2164
|
+
const accountPadded = account.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
2165
|
+
const data = selector + accountPadded;
|
|
2166
|
+
const response = await fetch(rpcUrl, {
|
|
2167
|
+
method: "POST",
|
|
2168
|
+
headers: { "Content-Type": "application/json" },
|
|
2169
|
+
body: JSON.stringify({
|
|
2170
|
+
jsonrpc: "2.0",
|
|
2171
|
+
method: "eth_call",
|
|
2172
|
+
params: [{ to: token, data }, "latest"],
|
|
2173
|
+
id: 1
|
|
2174
|
+
})
|
|
2175
|
+
});
|
|
2176
|
+
const result = await response.json();
|
|
2177
|
+
return result.result || "0x0";
|
|
2178
|
+
}
|
|
2179
|
+
async executeTransferFrom(from, to, amount, token, rpcUrl) {
|
|
2180
|
+
const { ethers: ethers3 } = await import("ethers");
|
|
2181
|
+
const provider = new ethers3.JsonRpcProvider(rpcUrl);
|
|
2182
|
+
const wallet = new ethers3.Wallet(this.serverPrivateKey, provider);
|
|
2183
|
+
const tokenContract = new ethers3.Contract(token, [
|
|
2184
|
+
"function transferFrom(address from, address to, uint256 amount) returns (bool)"
|
|
2185
|
+
], wallet);
|
|
2186
|
+
const tx = await tokenContract.transferFrom(from, to, amount);
|
|
2187
|
+
const receipt = await tx.wait();
|
|
2188
|
+
return receipt.hash;
|
|
2189
|
+
}
|
|
2190
|
+
async getTransactionReceipt(txHash, rpcUrl) {
|
|
2191
|
+
const response = await fetch(rpcUrl, {
|
|
1103
2192
|
method: "POST",
|
|
1104
2193
|
headers: { "Content-Type": "application/json" },
|
|
1105
2194
|
body: JSON.stringify({
|
|
@@ -1116,6 +2205,8 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
1116
2205
|
|
|
1117
2206
|
// src/facilitators/registry.ts
|
|
1118
2207
|
init_esm_shims();
|
|
2208
|
+
import { Keypair as Keypair4 } from "@solana/web3.js";
|
|
2209
|
+
import bs582 from "bs58";
|
|
1119
2210
|
var FacilitatorRegistry = class {
|
|
1120
2211
|
factories = /* @__PURE__ */ new Map();
|
|
1121
2212
|
instances = /* @__PURE__ */ new Map();
|
|
@@ -1124,7 +2215,20 @@ var FacilitatorRegistry = class {
|
|
|
1124
2215
|
constructor(selection) {
|
|
1125
2216
|
this.registerFactory("cdp", (config) => new CDPFacilitator(config));
|
|
1126
2217
|
this.registerFactory("tempo", () => new TempoFacilitator());
|
|
1127
|
-
this.
|
|
2218
|
+
this.registerFactory("bnb", (config) => new BNBFacilitator(config?.serverPrivateKey));
|
|
2219
|
+
this.registerFactory("solana", (config) => {
|
|
2220
|
+
let feePayerKeypair;
|
|
2221
|
+
const feePayerKey = config?.feePayerPrivateKey || process.env.SOLANA_FEE_PAYER_KEY;
|
|
2222
|
+
if (feePayerKey) {
|
|
2223
|
+
try {
|
|
2224
|
+
feePayerKeypair = Keypair4.fromSecretKey(bs582.decode(feePayerKey));
|
|
2225
|
+
} catch (e) {
|
|
2226
|
+
console.warn(`[SolanaFacilitator] Invalid fee payer key: ${e.message}`);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
return new SolanaFacilitator({ feePayerKeypair });
|
|
2230
|
+
});
|
|
2231
|
+
this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
|
|
1128
2232
|
}
|
|
1129
2233
|
/**
|
|
1130
2234
|
* Register a new facilitator factory
|
|
@@ -1371,14 +2475,40 @@ var TOKEN_ADDRESSES = {
|
|
|
1371
2475
|
// pathUSD
|
|
1372
2476
|
USDT: "0x20c0000000000000000000000000000000000001"
|
|
1373
2477
|
// alphaUSD
|
|
2478
|
+
},
|
|
2479
|
+
// BNB Smart Chain mainnet
|
|
2480
|
+
"eip155:56": {
|
|
2481
|
+
USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
2482
|
+
USDT: "0x55d398326f99059fF775485246999027B3197955"
|
|
2483
|
+
},
|
|
2484
|
+
// BNB Smart Chain testnet
|
|
2485
|
+
"eip155:97": {
|
|
2486
|
+
USDC: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
2487
|
+
USDT: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd"
|
|
2488
|
+
},
|
|
2489
|
+
// Solana networks use mint addresses (SPL tokens)
|
|
2490
|
+
"solana:mainnet": {
|
|
2491
|
+
USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
|
2492
|
+
// Circle USDC
|
|
2493
|
+
},
|
|
2494
|
+
"solana:devnet": {
|
|
2495
|
+
USDC: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"
|
|
2496
|
+
// Devnet USDC
|
|
1374
2497
|
}
|
|
1375
2498
|
};
|
|
1376
2499
|
var CHAIN_TO_NETWORK = {
|
|
1377
2500
|
"base": "eip155:8453",
|
|
1378
2501
|
"base_sepolia": "eip155:84532",
|
|
1379
2502
|
"polygon": "eip155:137",
|
|
1380
|
-
"tempo_moderato": "eip155:42431"
|
|
2503
|
+
"tempo_moderato": "eip155:42431",
|
|
2504
|
+
"bnb": "eip155:56",
|
|
2505
|
+
"bnb_testnet": "eip155:97",
|
|
2506
|
+
"solana": "solana:mainnet",
|
|
2507
|
+
"solana_devnet": "solana:devnet"
|
|
1381
2508
|
};
|
|
2509
|
+
function isSolanaNetwork(network) {
|
|
2510
|
+
return network.startsWith("solana:");
|
|
2511
|
+
}
|
|
1382
2512
|
var TOKEN_DOMAINS = {
|
|
1383
2513
|
// Base mainnet
|
|
1384
2514
|
"eip155:8453": {
|
|
@@ -1400,6 +2530,16 @@ var TOKEN_DOMAINS = {
|
|
|
1400
2530
|
"eip155:42431": {
|
|
1401
2531
|
USDC: { name: "pathUSD", version: "1" },
|
|
1402
2532
|
USDT: { name: "alphaUSD", version: "1" }
|
|
2533
|
+
},
|
|
2534
|
+
// BNB Smart Chain mainnet
|
|
2535
|
+
"eip155:56": {
|
|
2536
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
2537
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
2538
|
+
},
|
|
2539
|
+
// BNB Smart Chain testnet
|
|
2540
|
+
"eip155:97": {
|
|
2541
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
2542
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
1403
2543
|
}
|
|
1404
2544
|
};
|
|
1405
2545
|
function getTokenDomain(network, token) {
|
|
@@ -1415,9 +2555,9 @@ function loadEnvFile2() {
|
|
|
1415
2555
|
path3.join(process.env.HOME || "", ".moltspay", ".env")
|
|
1416
2556
|
];
|
|
1417
2557
|
for (const envPath of envPaths) {
|
|
1418
|
-
if (
|
|
2558
|
+
if (existsSync4(envPath)) {
|
|
1419
2559
|
try {
|
|
1420
|
-
const content =
|
|
2560
|
+
const content = readFileSync4(envPath, "utf-8");
|
|
1421
2561
|
for (const line of content.split("\n")) {
|
|
1422
2562
|
const trimmed = line.trim();
|
|
1423
2563
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -1448,7 +2588,7 @@ var MoltsPayServer = class {
|
|
|
1448
2588
|
useMainnet;
|
|
1449
2589
|
constructor(servicesPath, options = {}) {
|
|
1450
2590
|
loadEnvFile2();
|
|
1451
|
-
const content =
|
|
2591
|
+
const content = readFileSync4(servicesPath, "utf-8");
|
|
1452
2592
|
this.manifest = JSON.parse(content);
|
|
1453
2593
|
this.options = {
|
|
1454
2594
|
port: options.port || 3e3,
|
|
@@ -1457,7 +2597,7 @@ var MoltsPayServer = class {
|
|
|
1457
2597
|
};
|
|
1458
2598
|
this.useMainnet = process.env.USE_MAINNET?.toLowerCase() === "true";
|
|
1459
2599
|
this.networkId = this.useMainnet ? "eip155:8453" : "eip155:84532";
|
|
1460
|
-
const defaultFallback = ["tempo"];
|
|
2600
|
+
const defaultFallback = ["tempo", "bnb", "solana"];
|
|
1461
2601
|
const envFallback = process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean);
|
|
1462
2602
|
const facilitatorConfig = options.facilitators || {
|
|
1463
2603
|
primary: process.env.FACILITATOR_PRIMARY || "cdp",
|
|
@@ -1500,12 +2640,20 @@ var MoltsPayServer = class {
|
|
|
1500
2640
|
*/
|
|
1501
2641
|
getProviderChains() {
|
|
1502
2642
|
const provider = this.manifest.provider;
|
|
2643
|
+
const getWalletForChain = (chainName, explicitWallet) => {
|
|
2644
|
+
if (explicitWallet) return explicitWallet;
|
|
2645
|
+
if ((chainName === "solana" || chainName === "solana_devnet") && provider.solana_wallet) {
|
|
2646
|
+
return provider.solana_wallet;
|
|
2647
|
+
}
|
|
2648
|
+
return provider.wallet;
|
|
2649
|
+
};
|
|
1503
2650
|
if (provider.chains && provider.chains.length > 0) {
|
|
1504
2651
|
return provider.chains.map((c) => {
|
|
1505
2652
|
const chainName = typeof c === "string" ? c : c.chain;
|
|
2653
|
+
const explicitWallet = typeof c === "object" ? c.wallet : null;
|
|
1506
2654
|
return {
|
|
1507
2655
|
network: CHAIN_TO_NETWORK[chainName] || "eip155:8453",
|
|
1508
|
-
wallet: (
|
|
2656
|
+
wallet: getWalletForChain(chainName, explicitWallet || void 0),
|
|
1509
2657
|
tokens: (typeof c === "object" ? c.tokens : null) || ["USDC"]
|
|
1510
2658
|
};
|
|
1511
2659
|
});
|
|
@@ -1514,7 +2662,7 @@ var MoltsPayServer = class {
|
|
|
1514
2662
|
const network = CHAIN_TO_NETWORK[chain] || this.networkId;
|
|
1515
2663
|
return [{
|
|
1516
2664
|
network,
|
|
1517
|
-
wallet:
|
|
2665
|
+
wallet: getWalletForChain(chain),
|
|
1518
2666
|
tokens: ["USDC"]
|
|
1519
2667
|
}];
|
|
1520
2668
|
}
|
|
@@ -1585,7 +2733,8 @@ var MoltsPayServer = class {
|
|
|
1585
2733
|
}
|
|
1586
2734
|
const body = await this.readBody(req);
|
|
1587
2735
|
const paymentHeader = req.headers[PAYMENT_HEADER2];
|
|
1588
|
-
|
|
2736
|
+
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
2737
|
+
return await this.handleProxy(body, paymentHeader, authHeader, res);
|
|
1589
2738
|
}
|
|
1590
2739
|
const servicePath = url.pathname.replace(/^\//, "");
|
|
1591
2740
|
const skill = this.skills.get(servicePath);
|
|
@@ -1622,7 +2771,9 @@ var MoltsPayServer = class {
|
|
|
1622
2771
|
name: this.manifest.provider.name,
|
|
1623
2772
|
description: this.manifest.provider.description,
|
|
1624
2773
|
wallet: this.manifest.provider.wallet,
|
|
1625
|
-
chain: this.manifest.provider.chain || "base"
|
|
2774
|
+
chain: this.manifest.provider.chain || "base",
|
|
2775
|
+
solana_wallet: this.manifest.provider.solana_wallet,
|
|
2776
|
+
chains: this.manifest.provider.chains
|
|
1626
2777
|
},
|
|
1627
2778
|
services,
|
|
1628
2779
|
endpoints: {
|
|
@@ -1735,6 +2886,21 @@ var MoltsPayServer = class {
|
|
|
1735
2886
|
});
|
|
1736
2887
|
}
|
|
1737
2888
|
console.log(`[MoltsPay] Verified by ${verifyResult.facilitator}`);
|
|
2889
|
+
const isSolana = isSolanaNetwork(paymentNetwork);
|
|
2890
|
+
let settlement = null;
|
|
2891
|
+
if (isSolana) {
|
|
2892
|
+
console.log(`[MoltsPay] Solana detected - settling payment FIRST (blockhash expiry protection)`);
|
|
2893
|
+
try {
|
|
2894
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
2895
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
2896
|
+
} catch (err) {
|
|
2897
|
+
console.error("[MoltsPay] Solana settlement failed:", err.message);
|
|
2898
|
+
return this.sendJson(res, 402, {
|
|
2899
|
+
error: "Payment settlement failed",
|
|
2900
|
+
message: err.message
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
1738
2904
|
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
1739
2905
|
console.log(`[MoltsPay] Executing skill: ${service} (timeout: ${timeoutSeconds}s)`);
|
|
1740
2906
|
let result;
|
|
@@ -1749,16 +2915,19 @@ var MoltsPayServer = class {
|
|
|
1749
2915
|
console.error("[MoltsPay] Skill execution failed:", err.message);
|
|
1750
2916
|
return this.sendJson(res, 500, {
|
|
1751
2917
|
error: "Service execution failed",
|
|
1752
|
-
message: err.message
|
|
2918
|
+
message: err.message,
|
|
2919
|
+
paymentSettled: isSolana ? true : false,
|
|
2920
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
1753
2921
|
});
|
|
1754
2922
|
}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
2923
|
+
if (!isSolana) {
|
|
2924
|
+
console.log(`[MoltsPay] Skill succeeded, settling payment...`);
|
|
2925
|
+
try {
|
|
2926
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
2927
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
2928
|
+
} catch (err) {
|
|
2929
|
+
console.error("[MoltsPay] Settlement failed:", err.message);
|
|
2930
|
+
}
|
|
1762
2931
|
}
|
|
1763
2932
|
const responseHeaders = {};
|
|
1764
2933
|
if (settlement?.success) {
|
|
@@ -2034,7 +3203,7 @@ var MoltsPayServer = class {
|
|
|
2034
3203
|
const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
|
|
2035
3204
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
2036
3205
|
const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
|
|
2037
|
-
|
|
3206
|
+
const requirements = {
|
|
2038
3207
|
scheme: "exact",
|
|
2039
3208
|
network: selectedNetwork,
|
|
2040
3209
|
asset: tokenAddress,
|
|
@@ -2043,6 +3212,27 @@ var MoltsPayServer = class {
|
|
|
2043
3212
|
maxTimeoutSeconds: 300,
|
|
2044
3213
|
extra: tokenDomain
|
|
2045
3214
|
};
|
|
3215
|
+
if (selectedNetwork === "solana:mainnet" || selectedNetwork === "solana:devnet") {
|
|
3216
|
+
const solanaFacilitator = this.registry.get("solana");
|
|
3217
|
+
const feePayerPubkey = solanaFacilitator?.getFeePayerPubkey?.();
|
|
3218
|
+
if (feePayerPubkey) {
|
|
3219
|
+
requirements.extra = {
|
|
3220
|
+
...requirements.extra || {},
|
|
3221
|
+
solanaFeePayer: feePayerPubkey
|
|
3222
|
+
};
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
if (selectedNetwork === "eip155:56" || selectedNetwork === "eip155:97") {
|
|
3226
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
3227
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
3228
|
+
if (spenderAddress) {
|
|
3229
|
+
requirements.extra = {
|
|
3230
|
+
...requirements.extra || {},
|
|
3231
|
+
bnbSpender: spenderAddress
|
|
3232
|
+
};
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
return requirements;
|
|
2046
3236
|
}
|
|
2047
3237
|
/**
|
|
2048
3238
|
* Detect which token is being used in the payment
|
|
@@ -2095,8 +3285,10 @@ var MoltsPayServer = class {
|
|
|
2095
3285
|
isProxyAllowed(clientIP) {
|
|
2096
3286
|
const allowedIPs = process.env.PROXY_ALLOWED_IPS?.split(",").map((ip) => ip.trim()) || [];
|
|
2097
3287
|
if (allowedIPs.length === 0) {
|
|
2098
|
-
|
|
2099
|
-
|
|
3288
|
+
return true;
|
|
3289
|
+
}
|
|
3290
|
+
if (allowedIPs.includes("*")) {
|
|
3291
|
+
return true;
|
|
2100
3292
|
}
|
|
2101
3293
|
const normalizedIP = clientIP === "::1" ? "127.0.0.1" : clientIP.replace("::ffff:", "");
|
|
2102
3294
|
const allowed = allowedIPs.includes(normalizedIP) || allowedIPs.includes(clientIP);
|
|
@@ -2108,31 +3300,42 @@ var MoltsPayServer = class {
|
|
|
2108
3300
|
/**
|
|
2109
3301
|
* POST /proxy - Handle payment for external services (moltspay-creators)
|
|
2110
3302
|
*
|
|
2111
|
-
* This endpoint allows other services to delegate x402 payment handling.
|
|
3303
|
+
* This endpoint allows other services to delegate x402/MPP payment handling.
|
|
2112
3304
|
* It does NOT execute any skill - just handles payment verification/settlement.
|
|
2113
3305
|
*
|
|
2114
3306
|
* Request body:
|
|
2115
3307
|
* { wallet, amount, currency, chain, memo, serviceId, description }
|
|
2116
3308
|
*
|
|
2117
|
-
*
|
|
2118
|
-
*
|
|
3309
|
+
* For x402 (base, polygon, base_sepolia):
|
|
3310
|
+
* Without X-Payment header: returns 402 with X-Payment-Required
|
|
3311
|
+
* With X-Payment header: verifies payment via CDP
|
|
3312
|
+
*
|
|
3313
|
+
* For MPP (tempo_moderato):
|
|
3314
|
+
* Without Authorization header: returns 402 with WWW-Authenticate
|
|
3315
|
+
* With Authorization: Payment header: verifies tx on Tempo chain
|
|
2119
3316
|
*/
|
|
2120
|
-
async handleProxy(body, paymentHeader, res) {
|
|
3317
|
+
async handleProxy(body, paymentHeader, authHeader, res) {
|
|
2121
3318
|
const { wallet, amount, currency, chain, memo, serviceId, description } = body;
|
|
2122
3319
|
if (!wallet || !amount) {
|
|
2123
3320
|
return this.sendJson(res, 400, { error: "Missing required fields: wallet, amount" });
|
|
2124
3321
|
}
|
|
2125
|
-
|
|
2126
|
-
|
|
3322
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
|
|
3323
|
+
if (chain && !supportedChains.includes(chain)) {
|
|
3324
|
+
return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
|
|
3325
|
+
}
|
|
3326
|
+
const isSolanaChain = chain === "solana" || chain === "solana_devnet";
|
|
3327
|
+
const isValidEvmAddress = /^0x[a-fA-F0-9]{40}$/.test(wallet);
|
|
3328
|
+
const isValidSolanaAddress2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(wallet);
|
|
3329
|
+
if (isSolanaChain && !isValidSolanaAddress2) {
|
|
3330
|
+
return this.sendJson(res, 400, { error: "Invalid Solana wallet address format" });
|
|
3331
|
+
}
|
|
3332
|
+
if (!isSolanaChain && !isValidEvmAddress) {
|
|
3333
|
+
return this.sendJson(res, 400, { error: "Invalid EVM wallet address format" });
|
|
2127
3334
|
}
|
|
2128
3335
|
const amountNum = parseFloat(amount);
|
|
2129
3336
|
if (isNaN(amountNum) || amountNum <= 0) {
|
|
2130
3337
|
return this.sendJson(res, 400, { error: "Invalid amount" });
|
|
2131
3338
|
}
|
|
2132
|
-
const supportedChains = ["base", "polygon", "base_sepolia"];
|
|
2133
|
-
if (chain && !supportedChains.includes(chain)) {
|
|
2134
|
-
return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
|
|
2135
|
-
}
|
|
2136
3339
|
const proxyConfig = {
|
|
2137
3340
|
id: serviceId || "proxy",
|
|
2138
3341
|
name: description || "Proxy Payment",
|
|
@@ -2144,6 +3347,9 @@ var MoltsPayServer = class {
|
|
|
2144
3347
|
input: {},
|
|
2145
3348
|
output: {}
|
|
2146
3349
|
};
|
|
3350
|
+
if (chain === "tempo_moderato") {
|
|
3351
|
+
return await this.handleProxyMPP(body, proxyConfig, authHeader, res);
|
|
3352
|
+
}
|
|
2147
3353
|
const requirements = this.buildProxyPaymentRequirements(proxyConfig, wallet, currency, chain);
|
|
2148
3354
|
if (!paymentHeader) {
|
|
2149
3355
|
return this.sendProxyPaymentRequired(proxyConfig, wallet, memo, chain, res);
|
|
@@ -2155,37 +3361,225 @@ var MoltsPayServer = class {
|
|
|
2155
3361
|
} catch {
|
|
2156
3362
|
return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
|
|
2157
3363
|
}
|
|
2158
|
-
if (payment.x402Version !== X402_VERSION3) {
|
|
2159
|
-
return this.sendJson(res, 402, { error: `Unsupported x402 version: ${payment.x402Version}` });
|
|
3364
|
+
if (payment.x402Version !== X402_VERSION3) {
|
|
3365
|
+
return this.sendJson(res, 402, { error: `Unsupported x402 version: ${payment.x402Version}` });
|
|
3366
|
+
}
|
|
3367
|
+
const scheme = payment.accepted?.scheme || payment.scheme;
|
|
3368
|
+
const network = payment.accepted?.network || payment.network;
|
|
3369
|
+
if (scheme !== "exact") {
|
|
3370
|
+
return this.sendJson(res, 402, { error: `Unsupported scheme: ${scheme}` });
|
|
3371
|
+
}
|
|
3372
|
+
const expectedNetwork = chain ? CHAIN_TO_NETWORK[chain] || this.networkId : this.networkId;
|
|
3373
|
+
if (network !== expectedNetwork) {
|
|
3374
|
+
return this.sendJson(res, 402, { error: `Network mismatch: expected ${expectedNetwork}, got ${network}` });
|
|
3375
|
+
}
|
|
3376
|
+
console.log(`[MoltsPay] /proxy: Verifying payment for ${wallet}...`);
|
|
3377
|
+
const verifyResult = await this.registry.verify(payment, requirements);
|
|
3378
|
+
if (!verifyResult.valid) {
|
|
3379
|
+
return this.sendJson(res, 402, {
|
|
3380
|
+
success: false,
|
|
3381
|
+
error: `Payment verification failed: ${verifyResult.error}`,
|
|
3382
|
+
facilitator: verifyResult.facilitator
|
|
3383
|
+
});
|
|
3384
|
+
}
|
|
3385
|
+
console.log(`[MoltsPay] /proxy: Verified by ${verifyResult.facilitator}`);
|
|
3386
|
+
const { execute, service, params } = body;
|
|
3387
|
+
if (execute && service) {
|
|
3388
|
+
const skill = this.skills.get(service);
|
|
3389
|
+
if (!skill) {
|
|
3390
|
+
console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
|
|
3391
|
+
return this.sendJson(res, 404, {
|
|
3392
|
+
success: false,
|
|
3393
|
+
paymentSettled: false,
|
|
3394
|
+
error: `Service not found: ${service}`
|
|
3395
|
+
});
|
|
3396
|
+
}
|
|
3397
|
+
const isSolana = isSolanaNetwork(network);
|
|
3398
|
+
let settlement2 = null;
|
|
3399
|
+
if (isSolana) {
|
|
3400
|
+
console.log(`[MoltsPay] /proxy: Solana detected - settling payment FIRST`);
|
|
3401
|
+
try {
|
|
3402
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
3403
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
3404
|
+
if (!settlement2.success) {
|
|
3405
|
+
console.error(`[MoltsPay] /proxy: Solana settlement failed: ${settlement2.error}`);
|
|
3406
|
+
return this.sendJson(res, 402, {
|
|
3407
|
+
success: false,
|
|
3408
|
+
paymentSettled: false,
|
|
3409
|
+
error: `Payment settlement failed: ${settlement2.error || "Unknown error"}`
|
|
3410
|
+
});
|
|
3411
|
+
}
|
|
3412
|
+
} catch (err) {
|
|
3413
|
+
console.error("[MoltsPay] /proxy: Solana settlement failed:", err.message);
|
|
3414
|
+
return this.sendJson(res, 402, {
|
|
3415
|
+
success: false,
|
|
3416
|
+
paymentSettled: false,
|
|
3417
|
+
error: `Payment settlement failed: ${err.message}`
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
} else {
|
|
3421
|
+
console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
|
|
3422
|
+
}
|
|
3423
|
+
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
3424
|
+
let result;
|
|
3425
|
+
try {
|
|
3426
|
+
result = await Promise.race([
|
|
3427
|
+
skill.handler(params || {}),
|
|
3428
|
+
new Promise(
|
|
3429
|
+
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
3430
|
+
)
|
|
3431
|
+
]);
|
|
3432
|
+
console.log(`[MoltsPay] /proxy: Skill succeeded`);
|
|
3433
|
+
} catch (err) {
|
|
3434
|
+
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}`);
|
|
3435
|
+
return this.sendJson(res, 500, {
|
|
3436
|
+
success: false,
|
|
3437
|
+
paymentSettled: isSolana ? true : false,
|
|
3438
|
+
error: `Service execution failed: ${err.message}`,
|
|
3439
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
3440
|
+
});
|
|
3441
|
+
}
|
|
3442
|
+
if (!isSolana) {
|
|
3443
|
+
console.log(`[MoltsPay] /proxy: Settling payment...`);
|
|
3444
|
+
try {
|
|
3445
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
3446
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
3447
|
+
} catch (err) {
|
|
3448
|
+
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
3449
|
+
return this.sendJson(res, 200, {
|
|
3450
|
+
success: true,
|
|
3451
|
+
verified: true,
|
|
3452
|
+
settled: false,
|
|
3453
|
+
settlementError: err.message,
|
|
3454
|
+
from: payment.payload?.authorization?.from,
|
|
3455
|
+
paidTo: wallet,
|
|
3456
|
+
amount: amountNum,
|
|
3457
|
+
currency: currency || "USDC",
|
|
3458
|
+
memo,
|
|
3459
|
+
result
|
|
3460
|
+
});
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
return this.sendJson(res, 200, {
|
|
3464
|
+
success: true,
|
|
3465
|
+
verified: true,
|
|
3466
|
+
settled: settlement2?.success || false,
|
|
3467
|
+
txHash: settlement2?.transaction,
|
|
3468
|
+
from: payment.payload?.authorization?.from,
|
|
3469
|
+
paidTo: wallet,
|
|
3470
|
+
amount: amountNum,
|
|
3471
|
+
currency: currency || "USDC",
|
|
3472
|
+
facilitator: settlement2?.facilitator,
|
|
3473
|
+
memo,
|
|
3474
|
+
result
|
|
3475
|
+
});
|
|
3476
|
+
}
|
|
3477
|
+
console.log(`[MoltsPay] /proxy: Settling payment (no execution)...`);
|
|
3478
|
+
let settlement = null;
|
|
3479
|
+
try {
|
|
3480
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
3481
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
3482
|
+
} catch (err) {
|
|
3483
|
+
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
3484
|
+
return this.sendJson(res, 500, {
|
|
3485
|
+
success: false,
|
|
3486
|
+
error: `Settlement failed: ${err.message}`
|
|
3487
|
+
});
|
|
3488
|
+
}
|
|
3489
|
+
this.sendJson(res, 200, {
|
|
3490
|
+
success: true,
|
|
3491
|
+
verified: true,
|
|
3492
|
+
settled: settlement?.success || false,
|
|
3493
|
+
txHash: settlement?.transaction,
|
|
3494
|
+
from: payment.payload?.authorization?.from,
|
|
3495
|
+
// Buyer's wallet address
|
|
3496
|
+
paidTo: wallet,
|
|
3497
|
+
amount: amountNum,
|
|
3498
|
+
currency: currency || "USDC",
|
|
3499
|
+
facilitator: settlement?.facilitator,
|
|
3500
|
+
memo
|
|
3501
|
+
});
|
|
3502
|
+
}
|
|
3503
|
+
/**
|
|
3504
|
+
* Handle MPP payment flow for /proxy endpoint (tempo_moderato chain)
|
|
3505
|
+
*/
|
|
3506
|
+
async handleProxyMPP(body, config, authHeader, res) {
|
|
3507
|
+
const { wallet, amount, memo, serviceId } = body;
|
|
3508
|
+
const amountNum = parseFloat(amount);
|
|
3509
|
+
const amountInUnits = Math.floor(amountNum * 1e6).toString();
|
|
3510
|
+
if (!authHeader || !authHeader.toLowerCase().startsWith("payment ")) {
|
|
3511
|
+
const challengeId = this.generateChallengeId();
|
|
3512
|
+
const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
|
|
3513
|
+
const mppRequest = {
|
|
3514
|
+
amount: amountInUnits,
|
|
3515
|
+
currency: tokenAddress,
|
|
3516
|
+
methodDetails: {
|
|
3517
|
+
chainId: 42431,
|
|
3518
|
+
feePayer: true
|
|
3519
|
+
},
|
|
3520
|
+
recipient: wallet
|
|
3521
|
+
};
|
|
3522
|
+
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
3523
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
3524
|
+
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
3525
|
+
res.writeHead(402, {
|
|
3526
|
+
"Content-Type": "application/problem+json",
|
|
3527
|
+
[MPP_WWW_AUTH_HEADER]: wwwAuth
|
|
3528
|
+
});
|
|
3529
|
+
res.end(JSON.stringify({
|
|
3530
|
+
type: "https://paymentauth.org/problems/payment-required",
|
|
3531
|
+
title: "Payment Required",
|
|
3532
|
+
status: 402,
|
|
3533
|
+
detail: `Payment is required (${config.name}).`,
|
|
3534
|
+
service: serviceId || "proxy",
|
|
3535
|
+
price: amountNum,
|
|
3536
|
+
currency: "USDC"
|
|
3537
|
+
}, null, 2));
|
|
3538
|
+
return;
|
|
3539
|
+
}
|
|
3540
|
+
const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
|
|
3541
|
+
if (!credentialMatch) {
|
|
3542
|
+
return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
|
|
2160
3543
|
}
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
3544
|
+
let mppCredential;
|
|
3545
|
+
try {
|
|
3546
|
+
const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
3547
|
+
const decoded = Buffer.from(base64, "base64").toString("utf-8");
|
|
3548
|
+
mppCredential = JSON.parse(decoded);
|
|
3549
|
+
} catch (err) {
|
|
3550
|
+
console.error("[MoltsPay] /proxy MPP: Failed to parse credential:", err);
|
|
3551
|
+
return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
|
|
2165
3552
|
}
|
|
2166
|
-
|
|
2167
|
-
if (
|
|
2168
|
-
|
|
3553
|
+
let txHash;
|
|
3554
|
+
if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
|
|
3555
|
+
txHash = mppCredential.payload.hash;
|
|
3556
|
+
} else {
|
|
3557
|
+
return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
|
|
2169
3558
|
}
|
|
2170
|
-
console.log(`[MoltsPay] /proxy: Verifying
|
|
2171
|
-
const
|
|
2172
|
-
|
|
3559
|
+
console.log(`[MoltsPay] /proxy MPP: Verifying tx ${txHash} on Tempo...`);
|
|
3560
|
+
const requirements = this.buildPaymentRequirements(config, "eip155:42431", wallet, "USDC");
|
|
3561
|
+
const paymentPayload = {
|
|
3562
|
+
x402Version: X402_VERSION3,
|
|
3563
|
+
scheme: "exact",
|
|
3564
|
+
network: "eip155:42431",
|
|
3565
|
+
payload: { txHash, chainId: 42431 }
|
|
3566
|
+
};
|
|
3567
|
+
const verification = await this.registry.verify(paymentPayload, requirements);
|
|
3568
|
+
if (!verification.valid) {
|
|
2173
3569
|
return this.sendJson(res, 402, {
|
|
2174
|
-
|
|
2175
|
-
error: `Payment verification failed: ${verifyResult.error}`,
|
|
2176
|
-
facilitator: verifyResult.facilitator
|
|
3570
|
+
error: `Payment verification failed: ${verification.error}`
|
|
2177
3571
|
});
|
|
2178
3572
|
}
|
|
2179
|
-
console.log(`[MoltsPay] /proxy:
|
|
3573
|
+
console.log(`[MoltsPay] /proxy MPP: Payment verified by ${verification.facilitator}`);
|
|
2180
3574
|
const { execute, service, params } = body;
|
|
2181
3575
|
if (execute && service) {
|
|
2182
|
-
console.log(`[MoltsPay] /proxy: Executing skill
|
|
3576
|
+
console.log(`[MoltsPay] /proxy MPP: Executing skill: ${service}`);
|
|
2183
3577
|
const skill = this.skills.get(service);
|
|
2184
3578
|
if (!skill) {
|
|
2185
|
-
console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
|
|
2186
3579
|
return this.sendJson(res, 404, {
|
|
2187
3580
|
success: false,
|
|
2188
|
-
paymentSettled:
|
|
3581
|
+
paymentSettled: true,
|
|
3582
|
+
// Payment already happened on Tempo
|
|
2189
3583
|
error: `Service not found: ${service}`
|
|
2190
3584
|
});
|
|
2191
3585
|
}
|
|
@@ -2198,73 +3592,36 @@ var MoltsPayServer = class {
|
|
|
2198
3592
|
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
2199
3593
|
)
|
|
2200
3594
|
]);
|
|
2201
|
-
console.log(`[MoltsPay] /proxy: Skill succeeded, now settling payment...`);
|
|
2202
3595
|
} catch (err) {
|
|
2203
|
-
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}
|
|
3596
|
+
console.error(`[MoltsPay] /proxy MPP: Skill failed: ${err.message}`);
|
|
2204
3597
|
return this.sendJson(res, 500, {
|
|
2205
3598
|
success: false,
|
|
2206
|
-
paymentSettled:
|
|
3599
|
+
paymentSettled: true,
|
|
2207
3600
|
error: `Service execution failed: ${err.message}`
|
|
2208
3601
|
});
|
|
2209
3602
|
}
|
|
2210
|
-
let settlement2 = null;
|
|
2211
|
-
try {
|
|
2212
|
-
settlement2 = await this.registry.settle(payment, requirements);
|
|
2213
|
-
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
2214
|
-
} catch (err) {
|
|
2215
|
-
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
2216
|
-
return this.sendJson(res, 200, {
|
|
2217
|
-
success: true,
|
|
2218
|
-
verified: true,
|
|
2219
|
-
settled: false,
|
|
2220
|
-
settlementError: err.message,
|
|
2221
|
-
from: payment.payload?.authorization?.from,
|
|
2222
|
-
// Buyer's wallet address
|
|
2223
|
-
paidTo: wallet,
|
|
2224
|
-
amount: amountNum,
|
|
2225
|
-
currency: currency || "USDC",
|
|
2226
|
-
memo,
|
|
2227
|
-
result
|
|
2228
|
-
});
|
|
2229
|
-
}
|
|
2230
3603
|
return this.sendJson(res, 200, {
|
|
2231
3604
|
success: true,
|
|
2232
3605
|
verified: true,
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
from: payment.payload?.authorization?.from,
|
|
2236
|
-
// Buyer's wallet address
|
|
3606
|
+
txHash,
|
|
3607
|
+
chain: "tempo_moderato",
|
|
2237
3608
|
paidTo: wallet,
|
|
2238
3609
|
amount: amountNum,
|
|
2239
|
-
currency:
|
|
2240
|
-
facilitator:
|
|
3610
|
+
currency: "USDC",
|
|
3611
|
+
facilitator: verification.facilitator,
|
|
2241
3612
|
memo,
|
|
2242
3613
|
result
|
|
2243
3614
|
});
|
|
2244
3615
|
}
|
|
2245
|
-
console.log(`[MoltsPay] /proxy: Settling payment (no execution)...`);
|
|
2246
|
-
let settlement = null;
|
|
2247
|
-
try {
|
|
2248
|
-
settlement = await this.registry.settle(payment, requirements);
|
|
2249
|
-
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
2250
|
-
} catch (err) {
|
|
2251
|
-
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
2252
|
-
return this.sendJson(res, 500, {
|
|
2253
|
-
success: false,
|
|
2254
|
-
error: `Settlement failed: ${err.message}`
|
|
2255
|
-
});
|
|
2256
|
-
}
|
|
2257
3616
|
this.sendJson(res, 200, {
|
|
2258
3617
|
success: true,
|
|
2259
3618
|
verified: true,
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
from: payment.payload?.authorization?.from,
|
|
2263
|
-
// Buyer's wallet address
|
|
3619
|
+
txHash,
|
|
3620
|
+
chain: "tempo_moderato",
|
|
2264
3621
|
paidTo: wallet,
|
|
2265
3622
|
amount: amountNum,
|
|
2266
|
-
currency:
|
|
2267
|
-
facilitator:
|
|
3623
|
+
currency: "USDC",
|
|
3624
|
+
facilitator: verification.facilitator,
|
|
2268
3625
|
memo
|
|
2269
3626
|
});
|
|
2270
3627
|
}
|
|
@@ -2279,7 +3636,7 @@ var MoltsPayServer = class {
|
|
|
2279
3636
|
const tokenAddresses = TOKEN_ADDRESSES[networkId] || TOKEN_ADDRESSES[this.networkId] || {};
|
|
2280
3637
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
2281
3638
|
const tokenDomain = getTokenDomain(networkId, selectedToken);
|
|
2282
|
-
|
|
3639
|
+
const requirements = {
|
|
2283
3640
|
scheme: "exact",
|
|
2284
3641
|
network: networkId,
|
|
2285
3642
|
asset: tokenAddress,
|
|
@@ -2289,6 +3646,17 @@ var MoltsPayServer = class {
|
|
|
2289
3646
|
maxTimeoutSeconds: 300,
|
|
2290
3647
|
extra: tokenDomain
|
|
2291
3648
|
};
|
|
3649
|
+
if (networkId === "eip155:56" || networkId === "eip155:97") {
|
|
3650
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
3651
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
3652
|
+
if (spenderAddress) {
|
|
3653
|
+
requirements.extra = {
|
|
3654
|
+
...requirements.extra || {},
|
|
3655
|
+
bnbSpender: spenderAddress
|
|
3656
|
+
};
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
return requirements;
|
|
2292
3660
|
}
|
|
2293
3661
|
/**
|
|
2294
3662
|
* Return 402 with x402 payment requirements for proxy endpoint
|
|
@@ -2338,14 +3706,14 @@ if (!globalThis.crypto) {
|
|
|
2338
3706
|
}
|
|
2339
3707
|
function getVersion() {
|
|
2340
3708
|
const locations = [
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
3709
|
+
join5(__dirname, "../../package.json"),
|
|
3710
|
+
join5(__dirname, "../package.json"),
|
|
3711
|
+
join5(process.cwd(), "node_modules/moltspay/package.json")
|
|
2344
3712
|
];
|
|
2345
3713
|
for (const loc of locations) {
|
|
2346
3714
|
try {
|
|
2347
|
-
if (
|
|
2348
|
-
const pkg = JSON.parse(
|
|
3715
|
+
if (existsSync5(loc)) {
|
|
3716
|
+
const pkg = JSON.parse(readFileSync5(loc, "utf-8"));
|
|
2349
3717
|
if (pkg.name === "moltspay") return pkg.version;
|
|
2350
3718
|
}
|
|
2351
3719
|
} catch {
|
|
@@ -2353,11 +3721,94 @@ function getVersion() {
|
|
|
2353
3721
|
}
|
|
2354
3722
|
return "0.0.0";
|
|
2355
3723
|
}
|
|
3724
|
+
var BNB_SPONSOR_KEY = process.env.MOLTSPAY_BNB_SPONSOR_KEY;
|
|
3725
|
+
var BNB_SPENDER_ADDRESS = process.env.MOLTSPAY_BNB_SPENDER || "0xEBB45208D806A0c73F9673E0c5713FF720DD6b79";
|
|
3726
|
+
var ERC20_APPROVE_ABI = [
|
|
3727
|
+
"function approve(address spender, uint256 amount) returns (bool)",
|
|
3728
|
+
"function allowance(address owner, address spender) view returns (uint256)"
|
|
3729
|
+
];
|
|
3730
|
+
async function setupBNBApprovals(client, chain, spenderAddress, sponsorGas = false) {
|
|
3731
|
+
const chainConfig = CHAINS[chain];
|
|
3732
|
+
const provider = new ethers2.JsonRpcProvider(chainConfig.rpc);
|
|
3733
|
+
const wallet = client.getWallet();
|
|
3734
|
+
if (!wallet) {
|
|
3735
|
+
console.log(" \u274C No wallet found");
|
|
3736
|
+
return;
|
|
3737
|
+
}
|
|
3738
|
+
const signer = wallet.connect(provider);
|
|
3739
|
+
console.log(` Spender: ${spenderAddress}`);
|
|
3740
|
+
let bnbBalance = await provider.getBalance(wallet.address);
|
|
3741
|
+
const minGasRequired = ethers2.parseEther("0.0005");
|
|
3742
|
+
if (bnbBalance < minGasRequired) {
|
|
3743
|
+
if (sponsorGas && BNB_SPONSOR_KEY) {
|
|
3744
|
+
console.log(" \u23F3 Sponsoring BNB gas for approvals...");
|
|
3745
|
+
try {
|
|
3746
|
+
const sponsorWallet = new ethers2.Wallet(BNB_SPONSOR_KEY, provider);
|
|
3747
|
+
const tx = await sponsorWallet.sendTransaction({
|
|
3748
|
+
to: wallet.address,
|
|
3749
|
+
value: ethers2.parseEther("0.001")
|
|
3750
|
+
});
|
|
3751
|
+
await tx.wait();
|
|
3752
|
+
console.log(` \u2705 Sponsored 0.001 BNB (tx: ${tx.hash.slice(0, 10)}...)`);
|
|
3753
|
+
bnbBalance = await provider.getBalance(wallet.address);
|
|
3754
|
+
} catch (err) {
|
|
3755
|
+
console.log(` \u26A0\uFE0F Gas sponsorship failed: ${err.message}`);
|
|
3756
|
+
console.log(` \u{1F4A1} Get testnet BNB: https://testnet.bnbchain.org/faucet-smart`);
|
|
3757
|
+
return;
|
|
3758
|
+
}
|
|
3759
|
+
} else {
|
|
3760
|
+
console.log(` \u26A0\uFE0F Need BNB for gas (~0.0005 BNB)`);
|
|
3761
|
+
console.log(` \u{1F4A1} Run: npx moltspay faucet --chain bnb_testnet`);
|
|
3762
|
+
console.log(` Then run: npx moltspay approve --chain ${chain} --spender ${spenderAddress}`);
|
|
3763
|
+
return;
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
for (const tokenSymbol of ["USDT", "USDC"]) {
|
|
3767
|
+
const tokenConfig = chainConfig.tokens[tokenSymbol];
|
|
3768
|
+
const tokenContract = new ethers2.Contract(tokenConfig.address, ERC20_APPROVE_ABI, signer);
|
|
3769
|
+
const allowance = await tokenContract.allowance(wallet.address, spenderAddress);
|
|
3770
|
+
if (allowance > 0n) {
|
|
3771
|
+
console.log(` \u2705 ${tokenSymbol}: already approved for ${spenderAddress.slice(0, 10)}...`);
|
|
3772
|
+
continue;
|
|
3773
|
+
}
|
|
3774
|
+
console.log(` \u23F3 Approving ${tokenSymbol}...`);
|
|
3775
|
+
try {
|
|
3776
|
+
const tx = await tokenContract.approve(spenderAddress, ethers2.MaxUint256);
|
|
3777
|
+
await tx.wait();
|
|
3778
|
+
console.log(` \u2705 ${tokenSymbol}: approved (tx: ${tx.hash.slice(0, 10)}...)`);
|
|
3779
|
+
} catch (err) {
|
|
3780
|
+
console.log(` \u274C ${tokenSymbol}: approval failed - ${err.message}`);
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
console.log("");
|
|
3784
|
+
}
|
|
3785
|
+
async function checkBNBApprovals(address, chain, configDir = DEFAULT_CONFIG_DIR2) {
|
|
3786
|
+
const chainConfig = CHAINS[chain];
|
|
3787
|
+
const provider = new ethers2.JsonRpcProvider(chainConfig.rpc);
|
|
3788
|
+
let spenderAddress = null;
|
|
3789
|
+
try {
|
|
3790
|
+
const walletPath = join5(configDir, "wallet.json");
|
|
3791
|
+
const walletData = JSON.parse(readFileSync5(walletPath, "utf-8"));
|
|
3792
|
+
spenderAddress = walletData.approvals?.[chain] || null;
|
|
3793
|
+
} catch {
|
|
3794
|
+
}
|
|
3795
|
+
const result = { usdt: false, usdc: false, spender: spenderAddress };
|
|
3796
|
+
if (!spenderAddress) {
|
|
3797
|
+
return result;
|
|
3798
|
+
}
|
|
3799
|
+
for (const tokenSymbol of ["USDT", "USDC"]) {
|
|
3800
|
+
const tokenConfig = chainConfig.tokens[tokenSymbol];
|
|
3801
|
+
const tokenContract = new ethers2.Contract(tokenConfig.address, ERC20_APPROVE_ABI, provider);
|
|
3802
|
+
const allowance = await tokenContract.allowance(address, spenderAddress);
|
|
3803
|
+
result[tokenSymbol.toLowerCase()] = allowance > 0n;
|
|
3804
|
+
}
|
|
3805
|
+
return result;
|
|
3806
|
+
}
|
|
2356
3807
|
var program = new Command();
|
|
2357
|
-
var
|
|
2358
|
-
var PID_FILE =
|
|
2359
|
-
if (!
|
|
2360
|
-
|
|
3808
|
+
var DEFAULT_CONFIG_DIR2 = join5(homedir3(), ".moltspay");
|
|
3809
|
+
var PID_FILE = join5(DEFAULT_CONFIG_DIR2, "server.pid");
|
|
3810
|
+
if (!existsSync5(DEFAULT_CONFIG_DIR2)) {
|
|
3811
|
+
mkdirSync3(DEFAULT_CONFIG_DIR2, { recursive: true });
|
|
2361
3812
|
}
|
|
2362
3813
|
function prompt(question) {
|
|
2363
3814
|
const rl = readline.createInterface({
|
|
@@ -2372,19 +3823,49 @@ function prompt(question) {
|
|
|
2372
3823
|
});
|
|
2373
3824
|
}
|
|
2374
3825
|
program.name("moltspay").description("MoltsPay - Payment infrastructure for AI Agents").version(getVersion());
|
|
2375
|
-
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",
|
|
2376
|
-
console.log("\n\u{1F510} MoltsPay Client Setup\n");
|
|
2377
|
-
if (existsSync4(join4(options.configDir, "wallet.json"))) {
|
|
2378
|
-
console.log('\u26A0\uFE0F Already initialized. Use "moltspay config" to update settings.');
|
|
2379
|
-
console.log(` Config dir: ${options.configDir}`);
|
|
2380
|
-
return;
|
|
2381
|
-
}
|
|
3826
|
+
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) => {
|
|
2382
3827
|
let chain = options.chain;
|
|
2383
|
-
const
|
|
3828
|
+
const supportedEVMChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
|
|
3829
|
+
const supportedSolanaChains = ["solana", "solana_devnet"];
|
|
3830
|
+
const supportedChains = [...supportedEVMChains, ...supportedSolanaChains];
|
|
2384
3831
|
if (!supportedChains.includes(chain)) {
|
|
2385
3832
|
console.error(`\u274C Unknown chain: ${chain}. Supported: ${supportedChains.join(", ")}`);
|
|
2386
3833
|
process.exit(1);
|
|
2387
3834
|
}
|
|
3835
|
+
if (supportedSolanaChains.includes(chain)) {
|
|
3836
|
+
console.log("\n\u{1F7E3} Solana Wallet Setup\n");
|
|
3837
|
+
if (solanaWalletExists(options.configDir)) {
|
|
3838
|
+
const existingAddress = getSolanaAddress(options.configDir);
|
|
3839
|
+
console.log(`\u26A0\uFE0F Solana wallet already exists: ${existingAddress}`);
|
|
3840
|
+
console.log(` Config dir: ${options.configDir}`);
|
|
3841
|
+
return;
|
|
3842
|
+
}
|
|
3843
|
+
console.log("Creating Solana wallet...");
|
|
3844
|
+
const keypair = createSolanaWallet(options.configDir);
|
|
3845
|
+
const address = keypair.publicKey.toBase58();
|
|
3846
|
+
console.log(`
|
|
3847
|
+
\u2705 Solana wallet created: ${address}`);
|
|
3848
|
+
console.log(`
|
|
3849
|
+
\u{1F4C1} Config saved to: ${join5(options.configDir, "wallet-solana.json")}`);
|
|
3850
|
+
console.log(`
|
|
3851
|
+
\u26A0\uFE0F IMPORTANT: Back up your wallet file!`);
|
|
3852
|
+
console.log(` This file contains your private key!
|
|
3853
|
+
`);
|
|
3854
|
+
if (chain === "solana_devnet") {
|
|
3855
|
+
console.log("\u{1F4A1} Get testnet tokens:");
|
|
3856
|
+
console.log(" npx moltspay faucet --chain solana_devnet\n");
|
|
3857
|
+
} else {
|
|
3858
|
+
console.log(`\u{1F4B0} Fund your wallet with USDC on Solana to start (gasless - no SOL needed).
|
|
3859
|
+
`);
|
|
3860
|
+
}
|
|
3861
|
+
return;
|
|
3862
|
+
}
|
|
3863
|
+
console.log("\n\u{1F510} MoltsPay Client Setup\n");
|
|
3864
|
+
if (existsSync5(join5(options.configDir, "wallet.json"))) {
|
|
3865
|
+
console.log('\u26A0\uFE0F EVM wallet already initialized. Use "moltspay config" to update settings.');
|
|
3866
|
+
console.log(` Config dir: ${options.configDir}`);
|
|
3867
|
+
return;
|
|
3868
|
+
}
|
|
2388
3869
|
let maxPerTx = options.maxPerTx ? parseFloat(options.maxPerTx) : null;
|
|
2389
3870
|
let maxPerDay = options.maxPerDay ? parseFloat(options.maxPerDay) : null;
|
|
2390
3871
|
if (!maxPerTx) {
|
|
@@ -2406,13 +3887,21 @@ program.command("init").description("Initialize MoltsPay client (create wallet,
|
|
|
2406
3887
|
console.log(`
|
|
2407
3888
|
\u{1F4C1} Config saved to: ${result.configDir}`);
|
|
2408
3889
|
console.log(`
|
|
2409
|
-
\u26A0\uFE0F IMPORTANT: Back up ${
|
|
3890
|
+
\u26A0\uFE0F IMPORTANT: Back up ${join5(result.configDir, "wallet.json")}`);
|
|
2410
3891
|
console.log(` This file contains your private key!
|
|
2411
3892
|
`);
|
|
3893
|
+
if (chain === "bnb" || chain === "bnb_testnet") {
|
|
3894
|
+
console.log("\u{1F4CB} Setting up BNB chain approvals...\n");
|
|
3895
|
+
console.log(" \u2139\uFE0F Using default spender. For other services, run:");
|
|
3896
|
+
console.log(` npx moltspay approve --chain ${chain} --spender <address>
|
|
3897
|
+
`);
|
|
3898
|
+
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
3899
|
+
await setupBNBApprovals(client, chain, BNB_SPENDER_ADDRESS, true);
|
|
3900
|
+
}
|
|
2412
3901
|
console.log(`\u{1F4B0} Fund your wallet with USDC on ${chain} to start using services.
|
|
2413
3902
|
`);
|
|
2414
3903
|
});
|
|
2415
|
-
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",
|
|
3904
|
+
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) => {
|
|
2416
3905
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
2417
3906
|
if (!client.isInitialized) {
|
|
2418
3907
|
console.log("\u274C Not initialized. Run: npx moltspay init");
|
|
@@ -2447,25 +3936,40 @@ program.command("config").description("Update MoltsPay settings").option("--max-
|
|
|
2447
3936
|
}
|
|
2448
3937
|
}
|
|
2449
3938
|
});
|
|
2450
|
-
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
|
|
3939
|
+
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) => {
|
|
2451
3940
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
2452
|
-
if (!client.isInitialized) {
|
|
2453
|
-
console.log("\u274C Not initialized. Run: npx moltspay init");
|
|
2454
|
-
return;
|
|
2455
|
-
}
|
|
2456
3941
|
const amount = parseFloat(amountStr);
|
|
2457
3942
|
if (isNaN(amount) || amount < 5) {
|
|
2458
3943
|
console.log("\u274C Minimum $5.");
|
|
2459
3944
|
return;
|
|
2460
3945
|
}
|
|
2461
3946
|
const chain = options.chain?.toLowerCase() || "base";
|
|
2462
|
-
if (!["base", "polygon", "base_sepolia"].includes(chain)) {
|
|
2463
|
-
console.log("\u274C Invalid chain. Use: base, polygon, or
|
|
3947
|
+
if (!["base", "polygon", "base_sepolia", "solana", "bnb", "bnb_testnet"].includes(chain)) {
|
|
3948
|
+
console.log("\u274C Invalid chain. Use: base, polygon, solana, base_sepolia, bnb, or bnb_testnet");
|
|
2464
3949
|
return;
|
|
2465
3950
|
}
|
|
3951
|
+
let walletAddress;
|
|
3952
|
+
if (chain === "solana") {
|
|
3953
|
+
const solanaWallet = loadSolanaWallet(options.configDir || DEFAULT_CONFIG_DIR2);
|
|
3954
|
+
if (!solanaWallet) {
|
|
3955
|
+
console.log("\u274C No Solana wallet found. Run: npx moltspay init --chain solana");
|
|
3956
|
+
return;
|
|
3957
|
+
}
|
|
3958
|
+
walletAddress = getSolanaAddress(options.configDir || DEFAULT_CONFIG_DIR2) || "";
|
|
3959
|
+
if (!walletAddress) {
|
|
3960
|
+
console.log("\u274C Could not get Solana wallet address.");
|
|
3961
|
+
return;
|
|
3962
|
+
}
|
|
3963
|
+
} else {
|
|
3964
|
+
if (!client.isInitialized) {
|
|
3965
|
+
console.log("\u274C Not initialized. Run: npx moltspay init");
|
|
3966
|
+
return;
|
|
3967
|
+
}
|
|
3968
|
+
walletAddress = client.address;
|
|
3969
|
+
}
|
|
2466
3970
|
if (chain === "base_sepolia") {
|
|
2467
3971
|
console.log("\n\u{1F9EA} Testnet Funding\n");
|
|
2468
|
-
console.log(` Wallet: ${
|
|
3972
|
+
console.log(` Wallet: ${walletAddress}`);
|
|
2469
3973
|
console.log(` Chain: Base Sepolia (testnet)
|
|
2470
3974
|
`);
|
|
2471
3975
|
console.log("\u{1F4A1} Use the MoltsPay faucet to get free testnet USDC:\n");
|
|
@@ -2473,9 +3977,36 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
|
|
|
2473
3977
|
console.log(" Or get from Circle Faucet: https://faucet.circle.com/\n");
|
|
2474
3978
|
return;
|
|
2475
3979
|
}
|
|
3980
|
+
if (chain === "bnb_testnet") {
|
|
3981
|
+
console.log("\n\u{1F9EA} BNB Testnet Funding\n");
|
|
3982
|
+
console.log(` Wallet: ${walletAddress}`);
|
|
3983
|
+
console.log(` Chain: BNB Testnet
|
|
3984
|
+
`);
|
|
3985
|
+
console.log("\u{1F4A1} Use the MoltsPay faucet to get testnet USDC + tBNB:\n");
|
|
3986
|
+
console.log(" npx moltspay faucet --chain bnb_testnet\n");
|
|
3987
|
+
console.log(" This gives you:\n");
|
|
3988
|
+
console.log(" \u2022 1 USDC (testnet) for payments");
|
|
3989
|
+
console.log(" \u2022 0.001 tBNB for gas (first approval tx)\n");
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3992
|
+
if (chain === "bnb") {
|
|
3993
|
+
console.log("\n\u{1F4CB} BNB Chain Funding\n");
|
|
3994
|
+
console.log(` Wallet: ${walletAddress}
|
|
3995
|
+
`);
|
|
3996
|
+
console.log(" To use MoltsPay on BNB Chain, you need:\n");
|
|
3997
|
+
console.log(" 1. USDC for payments");
|
|
3998
|
+
console.log(" \u2192 Withdraw from Binance/exchange to your wallet address\n");
|
|
3999
|
+
console.log(" 2. Small amount of BNB for gas (~0.001 BNB / ~$0.60)");
|
|
4000
|
+
console.log(" \u2192 First approval transaction requires gas");
|
|
4001
|
+
console.log(" \u2192 After approval, all payments are gasless\n");
|
|
4002
|
+
console.log(" \u{1F4A1} Tip: Most exchanges include BNB dust when you withdraw to BNB Chain\n");
|
|
4003
|
+
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");
|
|
4004
|
+
console.log(" After funding, check status: npx moltspay status\n");
|
|
4005
|
+
return;
|
|
4006
|
+
}
|
|
2476
4007
|
console.log("\n\u{1F4B3} Fund your agent wallet\n");
|
|
2477
|
-
console.log(` Wallet: ${
|
|
2478
|
-
console.log(` Chain: ${chain}`);
|
|
4008
|
+
console.log(` Wallet: ${walletAddress}`);
|
|
4009
|
+
console.log(` Chain: ${chain === "solana" ? "Solana" : chain}`);
|
|
2479
4010
|
console.log(` Amount: $${amount.toFixed(2)}
|
|
2480
4011
|
`);
|
|
2481
4012
|
try {
|
|
@@ -2484,7 +4015,7 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
|
|
|
2484
4015
|
method: "POST",
|
|
2485
4016
|
headers: { "Content-Type": "application/json" },
|
|
2486
4017
|
body: JSON.stringify({
|
|
2487
|
-
address:
|
|
4018
|
+
address: walletAddress,
|
|
2488
4019
|
amount,
|
|
2489
4020
|
chain
|
|
2490
4021
|
})
|
|
@@ -2502,11 +4033,92 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
|
|
|
2502
4033
|
console.log(`\u274C ${error.message}`);
|
|
2503
4034
|
}
|
|
2504
4035
|
});
|
|
2505
|
-
program.command("
|
|
4036
|
+
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) => {
|
|
4037
|
+
const chain = options.chain;
|
|
4038
|
+
if (chain !== "bnb" && chain !== "bnb_testnet") {
|
|
4039
|
+
console.log("\u274C approve command is only for BNB chains (bnb or bnb_testnet)");
|
|
4040
|
+
return;
|
|
4041
|
+
}
|
|
4042
|
+
if (!options.spender.match(/^0x[a-fA-F0-9]{40}$/)) {
|
|
4043
|
+
console.log("\u274C Invalid spender address format");
|
|
4044
|
+
return;
|
|
4045
|
+
}
|
|
4046
|
+
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
4047
|
+
if (!client.isInitialized) {
|
|
4048
|
+
console.log("\u274C Wallet not initialized. Run: npx moltspay init --chain " + chain);
|
|
4049
|
+
return;
|
|
4050
|
+
}
|
|
4051
|
+
console.log(`
|
|
4052
|
+
\u{1F510} Approving spender for ${chain}...
|
|
4053
|
+
`);
|
|
4054
|
+
await setupBNBApprovals(client, chain, options.spender, false);
|
|
4055
|
+
const walletPath = join5(options.configDir || DEFAULT_CONFIG_DIR2, "wallet.json");
|
|
4056
|
+
try {
|
|
4057
|
+
const walletData = JSON.parse(readFileSync5(walletPath, "utf-8"));
|
|
4058
|
+
walletData.approvals = walletData.approvals || {};
|
|
4059
|
+
walletData.approvals[chain] = options.spender;
|
|
4060
|
+
writeFileSync3(walletPath, JSON.stringify(walletData, null, 2));
|
|
4061
|
+
console.log(`\u2705 Approval complete! Spender saved for ${chain}.
|
|
4062
|
+
`);
|
|
4063
|
+
} catch (err) {
|
|
4064
|
+
console.log("\u2705 Approval complete!\n");
|
|
4065
|
+
console.log("\u26A0\uFE0F Could not save spender to wallet config");
|
|
4066
|
+
}
|
|
4067
|
+
});
|
|
4068
|
+
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) => {
|
|
2506
4069
|
let address = options.address;
|
|
2507
4070
|
const chain = options.chain?.toLowerCase() || "base_sepolia";
|
|
2508
|
-
if (!["base_sepolia", "tempo_moderato"].includes(chain)) {
|
|
2509
|
-
console.log("\u274C Invalid chain. Use: base_sepolia or
|
|
4071
|
+
if (!["base_sepolia", "tempo_moderato", "bnb_testnet", "solana_devnet"].includes(chain)) {
|
|
4072
|
+
console.log("\u274C Invalid chain. Use: base_sepolia, tempo_moderato, bnb_testnet, or solana_devnet");
|
|
4073
|
+
return;
|
|
4074
|
+
}
|
|
4075
|
+
if (chain === "solana_devnet") {
|
|
4076
|
+
if (!address) {
|
|
4077
|
+
address = getSolanaAddress(options.configDir);
|
|
4078
|
+
if (!address) {
|
|
4079
|
+
console.log("\u274C No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
|
|
4080
|
+
return;
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
if (!isValidSolanaAddress(address)) {
|
|
4084
|
+
console.log("\u274C Invalid Solana address");
|
|
4085
|
+
return;
|
|
4086
|
+
}
|
|
4087
|
+
console.log("\n\u{1F6B0} Solana Devnet Faucet (Gasless Mode)\n");
|
|
4088
|
+
console.log(` Address: ${address}
|
|
4089
|
+
`);
|
|
4090
|
+
let usdcSuccess = false;
|
|
4091
|
+
try {
|
|
4092
|
+
console.log(" \u23F3 Requesting 1 USDC from faucet...");
|
|
4093
|
+
const FAUCET_API = process.env.MOLTSPAY_FAUCET_API || "https://moltspay.com/api/v1/faucet";
|
|
4094
|
+
const response = await fetch(FAUCET_API, {
|
|
4095
|
+
method: "POST",
|
|
4096
|
+
headers: { "Content-Type": "application/json" },
|
|
4097
|
+
body: JSON.stringify({ address, chain: "solana_devnet" })
|
|
4098
|
+
});
|
|
4099
|
+
const result = await response.json();
|
|
4100
|
+
if (!response.ok) {
|
|
4101
|
+
console.log(` \u26A0\uFE0F USDC faucet: ${result.error || "Request failed"}`);
|
|
4102
|
+
if (result.hint) console.log(` ${result.hint}`);
|
|
4103
|
+
if (result.retry_after) console.log(` Retry after: ${result.retry_after}`);
|
|
4104
|
+
} else {
|
|
4105
|
+
console.log(` \u2705 Received ${result.amount} USDC!`);
|
|
4106
|
+
console.log(` Transaction: ${result.explorer}`);
|
|
4107
|
+
if (result.faucet_balance) {
|
|
4108
|
+
console.log(` Faucet balance: ${result.faucet_balance} USDC remaining`);
|
|
4109
|
+
}
|
|
4110
|
+
usdcSuccess = true;
|
|
4111
|
+
}
|
|
4112
|
+
} catch (error) {
|
|
4113
|
+
console.log(` \u26A0\uFE0F USDC faucet error: ${error.message}`);
|
|
4114
|
+
}
|
|
4115
|
+
console.log("");
|
|
4116
|
+
if (usdcSuccess) {
|
|
4117
|
+
console.log("\u{1F4A1} Check your balance:");
|
|
4118
|
+
console.log(" npx moltspay status\n");
|
|
4119
|
+
} else {
|
|
4120
|
+
console.log("\u274C Faucet request failed. Try again in a few minutes.\n");
|
|
4121
|
+
}
|
|
2510
4122
|
return;
|
|
2511
4123
|
}
|
|
2512
4124
|
if (!address) {
|
|
@@ -2554,6 +4166,46 @@ program.command("faucet").description("Request testnet tokens from faucet (Base
|
|
|
2554
4166
|
console.log(`\u274C ${error.message}`);
|
|
2555
4167
|
console.log("\n Try Tempo Wallet instead: https://wallet.tempo.xyz\n");
|
|
2556
4168
|
}
|
|
4169
|
+
} else if (chain === "bnb_testnet") {
|
|
4170
|
+
console.log(` Requesting 1 USDC on BNB Testnet...`);
|
|
4171
|
+
console.log(` Address: ${address}
|
|
4172
|
+
`);
|
|
4173
|
+
try {
|
|
4174
|
+
const FAUCET_API = process.env.MOLTSPAY_FAUCET_API || "https://moltspay.com/api/v1/faucet";
|
|
4175
|
+
const response = await fetch(FAUCET_API, {
|
|
4176
|
+
method: "POST",
|
|
4177
|
+
headers: { "Content-Type": "application/json" },
|
|
4178
|
+
body: JSON.stringify({ address, chain: "bnb_testnet" })
|
|
4179
|
+
});
|
|
4180
|
+
const result = await response.json();
|
|
4181
|
+
if (!response.ok) {
|
|
4182
|
+
console.log(`\u274C ${result.error || "Request failed"}`);
|
|
4183
|
+
if (result.hint) console.log(` ${result.hint}`);
|
|
4184
|
+
if (result.retry_after) console.log(` Retry after: ${result.retry_after}`);
|
|
4185
|
+
console.log("\n\u{1F4A1} Alternatively, get tokens manually:");
|
|
4186
|
+
console.log(` 1. Get test BNB: https://www.bnbchain.org/en/testnet-faucet`);
|
|
4187
|
+
console.log(` 2. Select "Peggy Tokens" -> USDC`);
|
|
4188
|
+
console.log(` 3. Enter: ${address}
|
|
4189
|
+
`);
|
|
4190
|
+
return;
|
|
4191
|
+
}
|
|
4192
|
+
console.log(`\u2705 Received ${result.amount} ${result.token || "USDC"} on ${result.chain_name || "BNB Testnet"}!
|
|
4193
|
+
`);
|
|
4194
|
+
console.log(` Transaction: ${result.explorer || `https://testnet.bscscan.com/tx/${result.transaction}`}`);
|
|
4195
|
+
if (result.faucet_balance) {
|
|
4196
|
+
console.log(` Faucet balance: ${result.faucet_balance} USDC`);
|
|
4197
|
+
}
|
|
4198
|
+
console.log("\n\u{1F4A1} Now you can test BNB payments:");
|
|
4199
|
+
console.log(` npx moltspay pay <service-url> <service-id> --chain bnb_testnet
|
|
4200
|
+
`);
|
|
4201
|
+
} catch (error) {
|
|
4202
|
+
console.log(`\u274C ${error.message}`);
|
|
4203
|
+
console.log("\n\u{1F4A1} Get tokens manually:");
|
|
4204
|
+
console.log(` 1. Get test BNB: https://www.bnbchain.org/en/testnet-faucet`);
|
|
4205
|
+
console.log(` 2. Select "Peggy Tokens" -> USDC`);
|
|
4206
|
+
console.log(` 3. Enter: ${address}
|
|
4207
|
+
`);
|
|
4208
|
+
}
|
|
2557
4209
|
} else {
|
|
2558
4210
|
console.log(` Requesting 1 USDC on Base Sepolia...`);
|
|
2559
4211
|
console.log(` Address: ${address}
|
|
@@ -2563,7 +4215,7 @@ program.command("faucet").description("Request testnet tokens from faucet (Base
|
|
|
2563
4215
|
const response = await fetch(FAUCET_API, {
|
|
2564
4216
|
method: "POST",
|
|
2565
4217
|
headers: { "Content-Type": "application/json" },
|
|
2566
|
-
body: JSON.stringify({ address })
|
|
4218
|
+
body: JSON.stringify({ address, chain: "base_sepolia" })
|
|
2567
4219
|
});
|
|
2568
4220
|
const result = await response.json();
|
|
2569
4221
|
if (!response.ok) {
|
|
@@ -2586,7 +4238,7 @@ program.command("faucet").description("Request testnet tokens from faucet (Base
|
|
|
2586
4238
|
}
|
|
2587
4239
|
}
|
|
2588
4240
|
});
|
|
2589
|
-
program.command("status").description("Show wallet status and balance").option("--config-dir <dir>", "Config directory",
|
|
4241
|
+
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) => {
|
|
2590
4242
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
2591
4243
|
if (!client.isInitialized) {
|
|
2592
4244
|
if (options.json) {
|
|
@@ -2603,12 +4255,31 @@ program.command("status").description("Show wallet status and balance").option("
|
|
|
2603
4255
|
} catch (err) {
|
|
2604
4256
|
console.error("Warning: Could not fetch balances:", err.message);
|
|
2605
4257
|
}
|
|
4258
|
+
const solanaAddress = getSolanaAddress(options.configDir);
|
|
4259
|
+
let solanaBalances = {};
|
|
4260
|
+
if (solanaAddress) {
|
|
4261
|
+
try {
|
|
4262
|
+
solanaBalances.devnet = await getSolanaBalances(solanaAddress, "solana_devnet");
|
|
4263
|
+
} catch {
|
|
4264
|
+
}
|
|
4265
|
+
try {
|
|
4266
|
+
solanaBalances.mainnet = await getSolanaBalances(solanaAddress, "solana");
|
|
4267
|
+
} catch {
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
2606
4270
|
if (options.json) {
|
|
2607
|
-
|
|
4271
|
+
const output = {
|
|
2608
4272
|
address: client.address,
|
|
2609
4273
|
balances: allBalances,
|
|
2610
4274
|
limits: config.limits
|
|
2611
|
-
}
|
|
4275
|
+
};
|
|
4276
|
+
if (solanaAddress) {
|
|
4277
|
+
output.solana = {
|
|
4278
|
+
address: solanaAddress,
|
|
4279
|
+
balances: solanaBalances
|
|
4280
|
+
};
|
|
4281
|
+
}
|
|
4282
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2612
4283
|
} else {
|
|
2613
4284
|
console.log("\n\u{1F4CA} MoltsPay Wallet Status\n");
|
|
2614
4285
|
console.log(` Address: ${client.address}`);
|
|
@@ -2632,18 +4303,90 @@ program.command("status").description("Show wallet status and balance").option("
|
|
|
2632
4303
|
console.log(` alphaUSD: ${tempo.alphaUSD.toFixed(2)}`);
|
|
2633
4304
|
console.log(` betaUSD: ${tempo.betaUSD.toFixed(2)}`);
|
|
2634
4305
|
console.log(` thetaUSD: ${tempo.thetaUSD.toFixed(2)}`);
|
|
4306
|
+
} else if (chainName === "bnb" || chainName === "bnb_testnet") {
|
|
4307
|
+
const bnbBalance = balance.native;
|
|
4308
|
+
const bnbWarning = bnbBalance < 5e-4 ? " \u26A0\uFE0F Low gas" : "";
|
|
4309
|
+
console.log(` ${chainLabel.padEnd(14)} ${balance.usdc.toFixed(2)} USDC | ${balance.usdt.toFixed(2)} USDT | ${bnbBalance.toFixed(4)} BNB${bnbWarning}`);
|
|
2635
4310
|
} else {
|
|
2636
4311
|
console.log(` ${chainLabel.padEnd(14)} ${balance.usdc.toFixed(2)} USDC | ${balance.usdt.toFixed(2)} USDT`);
|
|
2637
4312
|
}
|
|
2638
4313
|
}
|
|
4314
|
+
const address = client.address;
|
|
4315
|
+
let bnbApprovalStatus = null;
|
|
4316
|
+
let bnbTestnetApprovalStatus = null;
|
|
4317
|
+
try {
|
|
4318
|
+
if (allBalances["bnb"]) {
|
|
4319
|
+
bnbApprovalStatus = await checkBNBApprovals(address, "bnb", options.configDir);
|
|
4320
|
+
}
|
|
4321
|
+
if (allBalances["bnb_testnet"]) {
|
|
4322
|
+
bnbTestnetApprovalStatus = await checkBNBApprovals(address, "bnb_testnet", options.configDir);
|
|
4323
|
+
}
|
|
4324
|
+
} catch {
|
|
4325
|
+
}
|
|
4326
|
+
if (bnbApprovalStatus || bnbTestnetApprovalStatus) {
|
|
4327
|
+
console.log("");
|
|
4328
|
+
console.log(" BNB Approvals (pay-for-success):");
|
|
4329
|
+
if (bnbApprovalStatus) {
|
|
4330
|
+
if (!bnbApprovalStatus.spender) {
|
|
4331
|
+
console.log(" BNB: \u26A0\uFE0F No spender configured");
|
|
4332
|
+
console.log(" \u2514\u2500 Run a payment first, or: npx moltspay approve --chain bnb --spender <address>");
|
|
4333
|
+
} else {
|
|
4334
|
+
const status = bnbApprovalStatus.usdt && bnbApprovalStatus.usdc ? "\u2705" : "\u26A0\uFE0F";
|
|
4335
|
+
const tokens = [
|
|
4336
|
+
bnbApprovalStatus.usdt ? "USDT\u2713" : "USDT\u2717",
|
|
4337
|
+
bnbApprovalStatus.usdc ? "USDC\u2713" : "USDC\u2717"
|
|
4338
|
+
].join(", ");
|
|
4339
|
+
console.log(` BNB: ${status} ${tokens}`);
|
|
4340
|
+
const bnbNative = allBalances["bnb"]?.native || 0;
|
|
4341
|
+
if (!bnbApprovalStatus.usdc && !bnbApprovalStatus.usdt && bnbNative < 5e-4) {
|
|
4342
|
+
console.log(" \u26A0\uFE0F Need ~0.001 BNB for first approval tx. Get from exchange.");
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4346
|
+
if (bnbTestnetApprovalStatus) {
|
|
4347
|
+
if (!bnbTestnetApprovalStatus.spender) {
|
|
4348
|
+
console.log(" BNB Testnet: \u26A0\uFE0F No spender configured");
|
|
4349
|
+
console.log(" \u2514\u2500 Run a payment first, or: npx moltspay approve --chain bnb_testnet --spender <address>");
|
|
4350
|
+
} else {
|
|
4351
|
+
const status = bnbTestnetApprovalStatus.usdt && bnbTestnetApprovalStatus.usdc ? "\u2705" : "\u26A0\uFE0F";
|
|
4352
|
+
const tokens = [
|
|
4353
|
+
bnbTestnetApprovalStatus.usdt ? "USDT\u2713" : "USDT\u2717",
|
|
4354
|
+
bnbTestnetApprovalStatus.usdc ? "USDC\u2713" : "USDC\u2717"
|
|
4355
|
+
].join(", ");
|
|
4356
|
+
console.log(` BNB Testnet: ${status} ${tokens}`);
|
|
4357
|
+
const tbnbNative = allBalances["bnb_testnet"]?.native || 0;
|
|
4358
|
+
if (!bnbTestnetApprovalStatus.usdc && !bnbTestnetApprovalStatus.usdt && tbnbNative < 5e-4) {
|
|
4359
|
+
console.log(" \u26A0\uFE0F Need tBNB for approval. Run: npx moltspay faucet --chain bnb_testnet");
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
}
|
|
2639
4364
|
console.log("");
|
|
2640
4365
|
console.log(" Spending Limits:");
|
|
2641
4366
|
console.log(` Per Transaction: $${config.limits.maxPerTx}`);
|
|
2642
4367
|
console.log(` Daily: $${config.limits.maxPerDay}`);
|
|
4368
|
+
const solanaAddress2 = getSolanaAddress(options.configDir);
|
|
4369
|
+
if (solanaAddress2) {
|
|
4370
|
+
console.log("");
|
|
4371
|
+
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");
|
|
4372
|
+
console.log(` \u{1F7E3} Solana: ${solanaAddress2}`);
|
|
4373
|
+
try {
|
|
4374
|
+
const devnetBalances = await getSolanaBalances(solanaAddress2, "solana_devnet");
|
|
4375
|
+
console.log(` Devnet: ${devnetBalances.sol.toFixed(4)} SOL | ${devnetBalances.usdc.toFixed(2)} USDC`);
|
|
4376
|
+
} catch (err) {
|
|
4377
|
+
console.log(` Devnet: (unable to fetch)`);
|
|
4378
|
+
}
|
|
4379
|
+
try {
|
|
4380
|
+
const mainnetBalances = await getSolanaBalances(solanaAddress2, "solana");
|
|
4381
|
+
console.log(` Mainnet: ${mainnetBalances.sol.toFixed(4)} SOL | ${mainnetBalances.usdc.toFixed(2)} USDC`);
|
|
4382
|
+
} catch (err) {
|
|
4383
|
+
console.log(` Mainnet: (unable to fetch)`);
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
2643
4386
|
console.log("");
|
|
2644
4387
|
}
|
|
2645
4388
|
});
|
|
2646
|
-
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",
|
|
4389
|
+
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) => {
|
|
2647
4390
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
2648
4391
|
if (!client.isInitialized) {
|
|
2649
4392
|
console.log("\u274C Not initialized. Run: npx moltspay init");
|
|
@@ -2950,14 +4693,14 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
2950
4693
|
let manifestPath;
|
|
2951
4694
|
let skillDir;
|
|
2952
4695
|
let isSkillDir = false;
|
|
2953
|
-
if (
|
|
2954
|
-
manifestPath =
|
|
4696
|
+
if (existsSync5(join5(resolvedPath, "moltspay.services.json"))) {
|
|
4697
|
+
manifestPath = join5(resolvedPath, "moltspay.services.json");
|
|
2955
4698
|
skillDir = resolvedPath;
|
|
2956
4699
|
isSkillDir = true;
|
|
2957
|
-
} else if (
|
|
4700
|
+
} else if (existsSync5(resolvedPath) && resolvedPath.endsWith(".json")) {
|
|
2958
4701
|
manifestPath = resolvedPath;
|
|
2959
4702
|
skillDir = dirname(resolvedPath);
|
|
2960
|
-
} else if (
|
|
4703
|
+
} else if (existsSync5(resolvedPath)) {
|
|
2961
4704
|
console.error(`\u274C No moltspay.services.json found in: ${resolvedPath}`);
|
|
2962
4705
|
continue;
|
|
2963
4706
|
} else {
|
|
@@ -2966,25 +4709,25 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
2966
4709
|
}
|
|
2967
4710
|
console.log(`\u{1F4E6} Loading: ${manifestPath}`);
|
|
2968
4711
|
try {
|
|
2969
|
-
const manifestContent = JSON.parse(
|
|
4712
|
+
const manifestContent = JSON.parse(readFileSync5(manifestPath, "utf-8"));
|
|
2970
4713
|
if (!provider) {
|
|
2971
4714
|
provider = manifestContent.provider;
|
|
2972
4715
|
}
|
|
2973
4716
|
let skillModule = null;
|
|
2974
4717
|
if (isSkillDir) {
|
|
2975
4718
|
let entryPoint = "index.js";
|
|
2976
|
-
const pkgJsonPath =
|
|
2977
|
-
if (
|
|
4719
|
+
const pkgJsonPath = join5(skillDir, "package.json");
|
|
4720
|
+
if (existsSync5(pkgJsonPath)) {
|
|
2978
4721
|
try {
|
|
2979
|
-
const pkgJson = JSON.parse(
|
|
4722
|
+
const pkgJson = JSON.parse(readFileSync5(pkgJsonPath, "utf-8"));
|
|
2980
4723
|
if (pkgJson.main) {
|
|
2981
4724
|
entryPoint = pkgJson.main;
|
|
2982
4725
|
}
|
|
2983
4726
|
} catch {
|
|
2984
4727
|
}
|
|
2985
4728
|
}
|
|
2986
|
-
const modulePath =
|
|
2987
|
-
if (
|
|
4729
|
+
const modulePath = join5(skillDir, entryPoint);
|
|
4730
|
+
if (existsSync5(modulePath)) {
|
|
2988
4731
|
try {
|
|
2989
4732
|
skillModule = await import(modulePath);
|
|
2990
4733
|
console.log(` \u2705 Loaded module: ${modulePath}`);
|
|
@@ -3062,8 +4805,8 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
3062
4805
|
provider,
|
|
3063
4806
|
services: allServices
|
|
3064
4807
|
};
|
|
3065
|
-
const tempManifestPath =
|
|
3066
|
-
|
|
4808
|
+
const tempManifestPath = join5(DEFAULT_CONFIG_DIR2, "combined-manifest.json");
|
|
4809
|
+
writeFileSync3(tempManifestPath, JSON.stringify(combinedManifest, null, 2));
|
|
3067
4810
|
console.log(`
|
|
3068
4811
|
\u{1F4CB} Combined manifest: ${allServices.length} services`);
|
|
3069
4812
|
console.log(` Provider: ${provider.name}`);
|
|
@@ -3076,12 +4819,12 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
3076
4819
|
server.skill(serviceId, handler);
|
|
3077
4820
|
}
|
|
3078
4821
|
const pidData = { pid: process.pid, port, paths: allPaths };
|
|
3079
|
-
|
|
4822
|
+
writeFileSync3(PID_FILE, JSON.stringify(pidData, null, 2));
|
|
3080
4823
|
server.listen(port);
|
|
3081
4824
|
const cleanup = () => {
|
|
3082
4825
|
try {
|
|
3083
|
-
if (
|
|
3084
|
-
if (
|
|
4826
|
+
if (existsSync5(PID_FILE)) unlinkSync(PID_FILE);
|
|
4827
|
+
if (existsSync5(tempManifestPath)) unlinkSync(tempManifestPath);
|
|
3085
4828
|
} catch {
|
|
3086
4829
|
}
|
|
3087
4830
|
};
|
|
@@ -3102,12 +4845,12 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
3102
4845
|
}
|
|
3103
4846
|
});
|
|
3104
4847
|
program.command("stop").description("Stop the running MoltsPay server").action(async () => {
|
|
3105
|
-
if (!
|
|
4848
|
+
if (!existsSync5(PID_FILE)) {
|
|
3106
4849
|
console.log("\u274C No running server found (no PID file)");
|
|
3107
4850
|
process.exit(1);
|
|
3108
4851
|
}
|
|
3109
4852
|
try {
|
|
3110
|
-
const pidData = JSON.parse(
|
|
4853
|
+
const pidData = JSON.parse(readFileSync5(PID_FILE, "utf-8"));
|
|
3111
4854
|
const { pid, port, manifest } = pidData;
|
|
3112
4855
|
console.log(`
|
|
3113
4856
|
\u{1F6D1} Stopping MoltsPay Server
|
|
@@ -3132,7 +4875,7 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
|
|
|
3132
4875
|
process.kill(pid, "SIGKILL");
|
|
3133
4876
|
} catch {
|
|
3134
4877
|
}
|
|
3135
|
-
if (
|
|
4878
|
+
if (existsSync5(PID_FILE)) {
|
|
3136
4879
|
unlinkSync(PID_FILE);
|
|
3137
4880
|
}
|
|
3138
4881
|
console.log("\u2705 Server stopped\n");
|
|
@@ -3141,14 +4884,23 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
|
|
|
3141
4884
|
process.exit(1);
|
|
3142
4885
|
}
|
|
3143
4886
|
});
|
|
3144
|
-
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
|
|
4887
|
+
program.command("pay <server> <service> [params]").description("Pay for a service and get the result").option("--prompt <text>", "Prompt for the service").option("--image <path>", "Image URL or local file path").option("--data <json>", "Raw JSON data to send (for custom input formats)").option("--token <token>", "Token to pay with (USDC or USDT)", "USDC").option("--chain <chain>", "Chain to pay on (base, polygon, base_sepolia, tempo_moderato, solana, or solana_devnet).").option("--config-dir <dir>", "Config directory with wallet.json", DEFAULT_CONFIG_DIR2).option("--json", "Output raw JSON only").action(async (server, service, paramsJson, options) => {
|
|
3145
4888
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
3146
4889
|
if (!client.isInitialized) {
|
|
3147
4890
|
console.error("\u274C Wallet not initialized. Run: npx moltspay init");
|
|
3148
4891
|
process.exit(1);
|
|
3149
4892
|
}
|
|
3150
4893
|
let params = {};
|
|
3151
|
-
|
|
4894
|
+
let useRawData = false;
|
|
4895
|
+
if (options.data) {
|
|
4896
|
+
try {
|
|
4897
|
+
params = JSON.parse(options.data);
|
|
4898
|
+
useRawData = true;
|
|
4899
|
+
} catch {
|
|
4900
|
+
console.error("\u274C Invalid JSON in --data flag");
|
|
4901
|
+
process.exit(1);
|
|
4902
|
+
}
|
|
4903
|
+
} else if (paramsJson) {
|
|
3152
4904
|
try {
|
|
3153
4905
|
params = JSON.parse(paramsJson);
|
|
3154
4906
|
} catch {
|
|
@@ -3156,24 +4908,25 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
3156
4908
|
process.exit(1);
|
|
3157
4909
|
}
|
|
3158
4910
|
}
|
|
3159
|
-
if (options.prompt) params.prompt = options.prompt;
|
|
4911
|
+
if (!useRawData && options.prompt) params.prompt = options.prompt;
|
|
3160
4912
|
if (options.image) {
|
|
3161
4913
|
const imagePath = options.image;
|
|
3162
4914
|
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
|
3163
4915
|
params.image_url = imagePath;
|
|
3164
4916
|
} else {
|
|
3165
4917
|
const filePath = resolve(imagePath);
|
|
3166
|
-
if (!
|
|
4918
|
+
if (!existsSync5(filePath)) {
|
|
3167
4919
|
console.error(`\u274C Image file not found: ${filePath}`);
|
|
3168
4920
|
process.exit(1);
|
|
3169
4921
|
}
|
|
3170
|
-
const imageData =
|
|
4922
|
+
const imageData = readFileSync5(filePath);
|
|
3171
4923
|
params.image_base64 = imageData.toString("base64");
|
|
3172
4924
|
}
|
|
3173
4925
|
}
|
|
4926
|
+
const supportedPayChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
|
|
3174
4927
|
const chain = options.chain?.toLowerCase();
|
|
3175
|
-
if (chain && !
|
|
3176
|
-
console.error(`\u274C Unknown chain: ${chain}. Supported:
|
|
4928
|
+
if (chain && !supportedPayChains.includes(chain)) {
|
|
4929
|
+
console.error(`\u274C Unknown chain: ${chain}. Supported: ${supportedPayChains.join(", ")}`);
|
|
3177
4930
|
process.exit(1);
|
|
3178
4931
|
}
|
|
3179
4932
|
const imageDisplay = params.image_url || (params.image_base64 ? `[local file: ${options.image}]` : null);
|
|
@@ -3196,7 +4949,11 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
3196
4949
|
`);
|
|
3197
4950
|
console.log(` Server: ${server}`);
|
|
3198
4951
|
console.log(` Service: ${service}`);
|
|
3199
|
-
|
|
4952
|
+
if (useRawData) {
|
|
4953
|
+
console.log(` Data: ${JSON.stringify(params).slice(0, 50)}${JSON.stringify(params).length > 50 ? "..." : ""}`);
|
|
4954
|
+
} else {
|
|
4955
|
+
console.log(` Prompt: ${params.prompt}`);
|
|
4956
|
+
}
|
|
3200
4957
|
if (imageDisplay) console.log(` Image: ${imageDisplay}`);
|
|
3201
4958
|
console.log(` Chain: ${chain || "(auto)"}`);
|
|
3202
4959
|
console.log(` Token: ${token}`);
|
|
@@ -3204,22 +4961,11 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
3204
4961
|
console.log("");
|
|
3205
4962
|
}
|
|
3206
4963
|
try {
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
}
|
|
3213
|
-
const mppUrl = server.includes(service) ? server : `${server}/${service}`;
|
|
3214
|
-
result = await client.payWithMPP(mppUrl, {
|
|
3215
|
-
body: params
|
|
3216
|
-
});
|
|
3217
|
-
} else {
|
|
3218
|
-
result = await client.pay(server, service, params, {
|
|
3219
|
-
token,
|
|
3220
|
-
chain
|
|
3221
|
-
});
|
|
3222
|
-
}
|
|
4964
|
+
const result = await client.pay(server, service, params, {
|
|
4965
|
+
token,
|
|
4966
|
+
chain,
|
|
4967
|
+
rawData: useRawData
|
|
4968
|
+
});
|
|
3223
4969
|
if (options.json) {
|
|
3224
4970
|
console.log(JSON.stringify(result));
|
|
3225
4971
|
} else {
|
|
@@ -3239,9 +4985,9 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
3239
4985
|
program.command("validate <path>").description("Validate a moltspay.services.json file against the schema").action(async (inputPath) => {
|
|
3240
4986
|
const resolvedPath = resolve(inputPath);
|
|
3241
4987
|
let manifestPath;
|
|
3242
|
-
if (
|
|
3243
|
-
manifestPath =
|
|
3244
|
-
} else if (resolvedPath.endsWith(".json") &&
|
|
4988
|
+
if (existsSync5(join5(resolvedPath, "moltspay.services.json"))) {
|
|
4989
|
+
manifestPath = join5(resolvedPath, "moltspay.services.json");
|
|
4990
|
+
} else if (resolvedPath.endsWith(".json") && existsSync5(resolvedPath)) {
|
|
3245
4991
|
manifestPath = resolvedPath;
|
|
3246
4992
|
} else {
|
|
3247
4993
|
console.error(`\u274C Not found: ${resolvedPath}`);
|
|
@@ -3251,7 +4997,7 @@ program.command("validate <path>").description("Validate a moltspay.services.jso
|
|
|
3251
4997
|
\u{1F4CB} Validating: ${manifestPath}
|
|
3252
4998
|
`);
|
|
3253
4999
|
try {
|
|
3254
|
-
const content = JSON.parse(
|
|
5000
|
+
const content = JSON.parse(readFileSync5(manifestPath, "utf-8"));
|
|
3255
5001
|
const errors = [];
|
|
3256
5002
|
if (!content.provider) {
|
|
3257
5003
|
errors.push("Missing required field: provider");
|