moltspay 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -38
- package/dist/cdp/index.d.mts +4 -4
- package/dist/cdp/index.d.ts +4 -4
- package/dist/cdp/index.js +57 -0
- package/dist/cdp/index.js.map +1 -1
- package/dist/cdp/index.mjs +57 -0
- package/dist/cdp/index.mjs.map +1 -1
- package/dist/chains/index.d.mts +9 -8
- package/dist/chains/index.d.ts +9 -8
- package/dist/chains/index.js +57 -0
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/index.mjs +57 -0
- package/dist/chains/index.mjs.map +1 -1
- package/dist/cli/index.js +1975 -273
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1977 -265
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +36 -3
- package/dist/client/index.d.ts +36 -3
- package/dist/client/index.js +540 -32
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +548 -30
- package/dist/client/index.mjs.map +1 -1
- package/dist/facilitators/index.d.mts +220 -1
- package/dist/facilitators/index.d.ts +220 -1
- package/dist/facilitators/index.js +664 -1
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +670 -1
- package/dist/facilitators/index.mjs.map +1 -1
- package/dist/{index-On9ZaGDW.d.mts → index-D_2FkLwV.d.mts} +6 -2
- package/dist/{index-On9ZaGDW.d.ts → index-D_2FkLwV.d.ts} +6 -2
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1413 -146
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1421 -144
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.d.mts +13 -3
- package/dist/server/index.d.ts +13 -3
- package/dist/server/index.js +905 -52
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +915 -52
- package/dist/server/index.mjs.map +1 -1
- package/dist/verify/index.d.mts +1 -1
- package/dist/verify/index.d.ts +1 -1
- package/dist/verify/index.js +57 -0
- package/dist/verify/index.js.map +1 -1
- package/dist/verify/index.mjs +57 -0
- package/dist/verify/index.mjs.map +1 -1
- package/dist/wallet/index.d.mts +3 -3
- package/dist/wallet/index.d.ts +3 -3
- package/dist/wallet/index.js +57 -0
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs +57 -0
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +4 -1
- package/schemas/moltspay.services.schema.json +27 -132
package/dist/cli/index.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
|
*/
|
|
@@ -262,9 +692,14 @@ var MoltsPayClient = class {
|
|
|
262
692
|
}
|
|
263
693
|
throw new Error(data.error || "Unexpected response");
|
|
264
694
|
}
|
|
695
|
+
const wwwAuthHeader = initialRes.headers.get("www-authenticate");
|
|
265
696
|
const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
697
|
+
if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
|
|
698
|
+
console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
|
|
699
|
+
return await this.handleMPPPayment(serverUrl, service, params, wwwAuthHeader);
|
|
700
|
+
}
|
|
266
701
|
if (!paymentRequiredHeader) {
|
|
267
|
-
throw new Error("Missing x-payment-required
|
|
702
|
+
throw new Error("Missing payment header (x-payment-required or www-authenticate)");
|
|
268
703
|
}
|
|
269
704
|
let requirements;
|
|
270
705
|
try {
|
|
@@ -281,17 +716,22 @@ var MoltsPayClient = class {
|
|
|
281
716
|
throw new Error("Invalid x-payment-required header");
|
|
282
717
|
}
|
|
283
718
|
const networkToChainName = (network2) => {
|
|
719
|
+
if (network2 === "solana:mainnet") return "solana";
|
|
720
|
+
if (network2 === "solana:devnet") return "solana_devnet";
|
|
284
721
|
const match = network2.match(/^eip155:(\d+)$/);
|
|
285
722
|
if (!match) return null;
|
|
286
723
|
const chainId = parseInt(match[1]);
|
|
287
724
|
if (chainId === 8453) return "base";
|
|
288
725
|
if (chainId === 137) return "polygon";
|
|
289
726
|
if (chainId === 84532) return "base_sepolia";
|
|
727
|
+
if (chainId === 42431) return "tempo_moderato";
|
|
728
|
+
if (chainId === 56) return "bnb";
|
|
729
|
+
if (chainId === 97) return "bnb_testnet";
|
|
290
730
|
return null;
|
|
291
731
|
};
|
|
292
732
|
const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
|
|
293
|
-
let chainName;
|
|
294
733
|
const userSpecifiedChain = options.chain;
|
|
734
|
+
let selectedChain;
|
|
295
735
|
if (userSpecifiedChain) {
|
|
296
736
|
if (!serverChains.includes(userSpecifiedChain)) {
|
|
297
737
|
throw new Error(
|
|
@@ -299,17 +739,27 @@ var MoltsPayClient = class {
|
|
|
299
739
|
Server accepts: ${serverChains.join(", ")}`
|
|
300
740
|
);
|
|
301
741
|
}
|
|
302
|
-
|
|
742
|
+
selectedChain = userSpecifiedChain;
|
|
303
743
|
} else {
|
|
304
744
|
if (serverChains.length === 1 && serverChains[0] === "base") {
|
|
305
|
-
|
|
745
|
+
selectedChain = "base";
|
|
306
746
|
} else {
|
|
307
747
|
throw new Error(
|
|
308
748
|
`Server accepts: ${serverChains.join(", ")}
|
|
309
|
-
Please specify: --chain
|
|
749
|
+
Please specify: --chain <chain_name>`
|
|
310
750
|
);
|
|
311
751
|
}
|
|
312
752
|
}
|
|
753
|
+
if (selectedChain === "solana" || selectedChain === "solana_devnet") {
|
|
754
|
+
const solanaChain = selectedChain;
|
|
755
|
+
const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
756
|
+
const req2 = requirements.find((r) => r.network === network2);
|
|
757
|
+
if (!req2) {
|
|
758
|
+
throw new Error(`Failed to find payment requirement for ${selectedChain}`);
|
|
759
|
+
}
|
|
760
|
+
return await this.handleSolanaPayment(serverUrl, service, params, req2, solanaChain);
|
|
761
|
+
}
|
|
762
|
+
const chainName = selectedChain;
|
|
313
763
|
const chain = getChain(chainName);
|
|
314
764
|
const network = `eip155:${chain.chainId}`;
|
|
315
765
|
const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
|
|
@@ -344,6 +794,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
344
794
|
} else {
|
|
345
795
|
console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
|
|
346
796
|
}
|
|
797
|
+
if (chainName === "bnb" || chainName === "bnb_testnet") {
|
|
798
|
+
console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
|
|
799
|
+
const payTo2 = req.payTo || req.resource;
|
|
800
|
+
if (!payTo2) {
|
|
801
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
802
|
+
}
|
|
803
|
+
const bnbSpender = req.extra?.bnbSpender;
|
|
804
|
+
if (!bnbSpender) {
|
|
805
|
+
throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
|
|
806
|
+
}
|
|
807
|
+
return await this.handleBNBPayment(serverUrl, service, params, {
|
|
808
|
+
to: payTo2,
|
|
809
|
+
amount,
|
|
810
|
+
token,
|
|
811
|
+
chainName,
|
|
812
|
+
chain,
|
|
813
|
+
spender: bnbSpender
|
|
814
|
+
});
|
|
815
|
+
}
|
|
347
816
|
const payTo = req.payTo || req.resource;
|
|
348
817
|
if (!payTo) {
|
|
349
818
|
throw new Error("Missing payTo address in payment requirements");
|
|
@@ -393,6 +862,300 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
393
862
|
console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
|
|
394
863
|
return result.result;
|
|
395
864
|
}
|
|
865
|
+
/**
|
|
866
|
+
* Handle MPP (Machine Payments Protocol) payment flow
|
|
867
|
+
* Called when pay() detects WWW-Authenticate header in 402 response
|
|
868
|
+
*/
|
|
869
|
+
async handleMPPPayment(serverUrl, service, params, wwwAuthHeader) {
|
|
870
|
+
const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
|
|
871
|
+
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
872
|
+
const { tempoModerato } = await import("viem/chains");
|
|
873
|
+
const { Actions } = await import("viem/tempo");
|
|
874
|
+
const privateKey = this.walletData.privateKey;
|
|
875
|
+
const account = privateKeyToAccount2(privateKey);
|
|
876
|
+
console.log(`[MoltsPay] Using MPP protocol on Tempo`);
|
|
877
|
+
console.log(`[MoltsPay] Account: ${account.address}`);
|
|
878
|
+
const parseAuthParam = (header, key) => {
|
|
879
|
+
const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
|
|
880
|
+
return match ? match[1] : null;
|
|
881
|
+
};
|
|
882
|
+
const challengeId = parseAuthParam(wwwAuthHeader, "id");
|
|
883
|
+
const method = parseAuthParam(wwwAuthHeader, "method");
|
|
884
|
+
const realm = parseAuthParam(wwwAuthHeader, "realm");
|
|
885
|
+
const requestB64 = parseAuthParam(wwwAuthHeader, "request");
|
|
886
|
+
if (method !== "tempo") {
|
|
887
|
+
throw new Error(`Unsupported payment method: ${method}`);
|
|
888
|
+
}
|
|
889
|
+
if (!requestB64) {
|
|
890
|
+
throw new Error("Missing request in WWW-Authenticate");
|
|
891
|
+
}
|
|
892
|
+
const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
|
|
893
|
+
const paymentRequest = JSON.parse(requestJson);
|
|
894
|
+
const { amount, currency, recipient, methodDetails } = paymentRequest;
|
|
895
|
+
const chainId = methodDetails?.chainId || 42431;
|
|
896
|
+
const amountDisplay = Number(amount) / 1e6;
|
|
897
|
+
console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
|
|
898
|
+
this.checkLimits(amountDisplay);
|
|
899
|
+
console.log(`[MoltsPay] Sending transaction on Tempo...`);
|
|
900
|
+
const tempoChain = { ...tempoModerato, feeToken: currency };
|
|
901
|
+
const publicClient = createPublicClient({
|
|
902
|
+
chain: tempoChain,
|
|
903
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
904
|
+
});
|
|
905
|
+
const walletClient = createWalletClient({
|
|
906
|
+
account,
|
|
907
|
+
chain: tempoChain,
|
|
908
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
909
|
+
});
|
|
910
|
+
const txHash = await Actions.token.transfer(walletClient, {
|
|
911
|
+
to: recipient,
|
|
912
|
+
amount: BigInt(amount),
|
|
913
|
+
token: currency
|
|
914
|
+
});
|
|
915
|
+
console.log(`[MoltsPay] Transaction: ${txHash}`);
|
|
916
|
+
await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
917
|
+
console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
|
|
918
|
+
const credential = {
|
|
919
|
+
challenge: {
|
|
920
|
+
id: challengeId,
|
|
921
|
+
realm,
|
|
922
|
+
method: "tempo",
|
|
923
|
+
intent: "charge",
|
|
924
|
+
request: paymentRequest
|
|
925
|
+
},
|
|
926
|
+
payload: { hash: txHash, type: "hash" },
|
|
927
|
+
source: `did:pkh:eip155:${chainId}:${account.address}`
|
|
928
|
+
};
|
|
929
|
+
const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
930
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
931
|
+
method: "POST",
|
|
932
|
+
headers: {
|
|
933
|
+
"Content-Type": "application/json",
|
|
934
|
+
"Authorization": `Payment ${credentialB64}`
|
|
935
|
+
},
|
|
936
|
+
body: JSON.stringify({ service, params, chain: "tempo_moderato" })
|
|
937
|
+
});
|
|
938
|
+
const result = await paidRes.json();
|
|
939
|
+
if (!paidRes.ok) {
|
|
940
|
+
throw new Error(result.error || "Payment verification failed");
|
|
941
|
+
}
|
|
942
|
+
this.recordSpending(amountDisplay);
|
|
943
|
+
console.log(`[MoltsPay] Success!`);
|
|
944
|
+
return result.result || result;
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Handle BNB Chain payment flow (pre-approval + intent signature)
|
|
948
|
+
*
|
|
949
|
+
* Flow:
|
|
950
|
+
* 1. Check client has approved server wallet (done via `moltspay init`)
|
|
951
|
+
* 2. Sign EIP-712 payment intent (no gas, just signature)
|
|
952
|
+
* 3. Send intent to server
|
|
953
|
+
* 4. Server executes service
|
|
954
|
+
* 5. Server calls transferFrom if successful (pay-for-success)
|
|
955
|
+
*/
|
|
956
|
+
async handleBNBPayment(serverUrl, service, params, paymentDetails) {
|
|
957
|
+
const { to, amount, token, chainName, chain, spender } = paymentDetails;
|
|
958
|
+
const tokenConfig = chain.tokens[token];
|
|
959
|
+
const provider = new ethers.JsonRpcProvider(chain.rpc);
|
|
960
|
+
const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
|
|
961
|
+
const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
|
|
962
|
+
if (allowance < amountWeiCheck) {
|
|
963
|
+
const nativeBalance = await provider.getBalance(this.wallet.address);
|
|
964
|
+
const minGasBalance = ethers.parseEther("0.0005");
|
|
965
|
+
if (nativeBalance < minGasBalance) {
|
|
966
|
+
const nativeBNB = parseFloat(ethers.formatEther(nativeBalance)).toFixed(4);
|
|
967
|
+
const isTestnet = chainName === "bnb_testnet";
|
|
968
|
+
if (isTestnet) {
|
|
969
|
+
throw new Error(
|
|
970
|
+
`\u274C Insufficient tBNB for approval transaction
|
|
971
|
+
|
|
972
|
+
Current tBNB: ${nativeBNB}
|
|
973
|
+
Required: ~0.001 tBNB
|
|
974
|
+
|
|
975
|
+
Get testnet tokens: npx moltspay faucet --chain bnb_testnet
|
|
976
|
+
(Gives USDC + tBNB for gas)`
|
|
977
|
+
);
|
|
978
|
+
} else {
|
|
979
|
+
throw new Error(
|
|
980
|
+
`\u274C Insufficient BNB for approval transaction
|
|
981
|
+
|
|
982
|
+
Current BNB: ${nativeBNB}
|
|
983
|
+
Required: ~0.001 BNB (~$0.60)
|
|
984
|
+
|
|
985
|
+
To get BNB:
|
|
986
|
+
\u2022 Withdraw from Binance/exchange to your wallet
|
|
987
|
+
\u2022 Most exchanges include BNB dust with withdrawals
|
|
988
|
+
|
|
989
|
+
After funding, run:
|
|
990
|
+
npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
throw new Error(
|
|
995
|
+
`Insufficient allowance for ${spender.slice(0, 10)}...
|
|
996
|
+
Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
|
|
1000
|
+
const intent = {
|
|
1001
|
+
from: this.wallet.address,
|
|
1002
|
+
to,
|
|
1003
|
+
amount: amountWei,
|
|
1004
|
+
token: tokenConfig.address,
|
|
1005
|
+
service,
|
|
1006
|
+
nonce: Date.now(),
|
|
1007
|
+
// Use timestamp as nonce for simplicity
|
|
1008
|
+
deadline: Date.now() + 36e5
|
|
1009
|
+
// 1 hour
|
|
1010
|
+
};
|
|
1011
|
+
const domain = {
|
|
1012
|
+
name: "MoltsPay",
|
|
1013
|
+
version: "1",
|
|
1014
|
+
chainId: chain.chainId
|
|
1015
|
+
};
|
|
1016
|
+
const types = {
|
|
1017
|
+
PaymentIntent: [
|
|
1018
|
+
{ name: "from", type: "address" },
|
|
1019
|
+
{ name: "to", type: "address" },
|
|
1020
|
+
{ name: "amount", type: "uint256" },
|
|
1021
|
+
{ name: "token", type: "address" },
|
|
1022
|
+
{ name: "service", type: "string" },
|
|
1023
|
+
{ name: "nonce", type: "uint256" },
|
|
1024
|
+
{ name: "deadline", type: "uint256" }
|
|
1025
|
+
]
|
|
1026
|
+
};
|
|
1027
|
+
console.log(`[MoltsPay] Signing BNB payment intent...`);
|
|
1028
|
+
const signature = await this.wallet.signTypedData(domain, types, intent);
|
|
1029
|
+
const network = `eip155:${chain.chainId}`;
|
|
1030
|
+
const payload = {
|
|
1031
|
+
x402Version: 2,
|
|
1032
|
+
scheme: "exact",
|
|
1033
|
+
network,
|
|
1034
|
+
payload: {
|
|
1035
|
+
intent: {
|
|
1036
|
+
...intent,
|
|
1037
|
+
signature
|
|
1038
|
+
},
|
|
1039
|
+
chainId: chain.chainId
|
|
1040
|
+
},
|
|
1041
|
+
accepted: {
|
|
1042
|
+
scheme: "exact",
|
|
1043
|
+
network,
|
|
1044
|
+
asset: tokenConfig.address,
|
|
1045
|
+
amount: amountWei,
|
|
1046
|
+
payTo: to,
|
|
1047
|
+
maxTimeoutSeconds: 300
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
1051
|
+
console.log(`[MoltsPay] Sending BNB payment request...`);
|
|
1052
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
1053
|
+
method: "POST",
|
|
1054
|
+
headers: {
|
|
1055
|
+
"Content-Type": "application/json",
|
|
1056
|
+
"X-Payment": paymentHeader
|
|
1057
|
+
},
|
|
1058
|
+
body: JSON.stringify({ service, params, chain: chainName })
|
|
1059
|
+
});
|
|
1060
|
+
const result = await paidRes.json();
|
|
1061
|
+
if (!paidRes.ok) {
|
|
1062
|
+
throw new Error(result.error || "BNB payment failed");
|
|
1063
|
+
}
|
|
1064
|
+
this.recordSpending(amount);
|
|
1065
|
+
console.log(`[MoltsPay] Success! BNB payment settled.`);
|
|
1066
|
+
return result.result || result;
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Handle Solana payment flow
|
|
1070
|
+
*
|
|
1071
|
+
* Solana uses SPL token transfers with pay-for-success model:
|
|
1072
|
+
* 1. Client creates and signs a transfer transaction
|
|
1073
|
+
* 2. Server submits the transaction after service completes
|
|
1074
|
+
*/
|
|
1075
|
+
async handleSolanaPayment(serverUrl, service, params, requirements, chain) {
|
|
1076
|
+
const solanaWallet = loadSolanaWallet(this.configDir);
|
|
1077
|
+
if (!solanaWallet) {
|
|
1078
|
+
throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
|
|
1079
|
+
}
|
|
1080
|
+
const amount = Number(requirements.amount);
|
|
1081
|
+
const amountUSDC = amount / 1e6;
|
|
1082
|
+
this.checkLimits(amountUSDC);
|
|
1083
|
+
console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
|
|
1084
|
+
if (!requirements.payTo) {
|
|
1085
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
1086
|
+
}
|
|
1087
|
+
const solanaFeePayer = requirements.extra?.solanaFeePayer;
|
|
1088
|
+
const feePayerPubkey = solanaFeePayer ? new PublicKey4(solanaFeePayer) : void 0;
|
|
1089
|
+
if (feePayerPubkey) {
|
|
1090
|
+
console.log(`[MoltsPay] Gasless mode: server pays fees`);
|
|
1091
|
+
}
|
|
1092
|
+
const recipientPubkey = new PublicKey4(requirements.payTo);
|
|
1093
|
+
const transaction = await createSolanaPaymentTransaction(
|
|
1094
|
+
solanaWallet.publicKey,
|
|
1095
|
+
recipientPubkey,
|
|
1096
|
+
BigInt(amount),
|
|
1097
|
+
chain,
|
|
1098
|
+
feePayerPubkey
|
|
1099
|
+
// Optional fee payer for gasless mode
|
|
1100
|
+
);
|
|
1101
|
+
if (feePayerPubkey) {
|
|
1102
|
+
transaction.partialSign(solanaWallet);
|
|
1103
|
+
} else {
|
|
1104
|
+
transaction.sign(solanaWallet);
|
|
1105
|
+
}
|
|
1106
|
+
const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
|
|
1107
|
+
console.log(`[MoltsPay] Transaction signed, sending to server...`);
|
|
1108
|
+
const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
1109
|
+
const payload = {
|
|
1110
|
+
x402Version: 2,
|
|
1111
|
+
scheme: "exact",
|
|
1112
|
+
network,
|
|
1113
|
+
payload: {
|
|
1114
|
+
signedTransaction: signedTx,
|
|
1115
|
+
sender: solanaWallet.publicKey.toBase58(),
|
|
1116
|
+
chain
|
|
1117
|
+
},
|
|
1118
|
+
accepted: {
|
|
1119
|
+
scheme: "exact",
|
|
1120
|
+
network,
|
|
1121
|
+
asset: requirements.asset,
|
|
1122
|
+
amount: requirements.amount,
|
|
1123
|
+
payTo: requirements.payTo,
|
|
1124
|
+
maxTimeoutSeconds: 300
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
1128
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
1129
|
+
method: "POST",
|
|
1130
|
+
headers: {
|
|
1131
|
+
"Content-Type": "application/json",
|
|
1132
|
+
"X-Payment": paymentHeader
|
|
1133
|
+
},
|
|
1134
|
+
body: JSON.stringify({ service, params, chain })
|
|
1135
|
+
});
|
|
1136
|
+
const result = await paidRes.json();
|
|
1137
|
+
if (!paidRes.ok) {
|
|
1138
|
+
throw new Error(result.error || "Solana payment failed");
|
|
1139
|
+
}
|
|
1140
|
+
this.recordSpending(amountUSDC);
|
|
1141
|
+
console.log(`[MoltsPay] Success! Solana payment settled.`);
|
|
1142
|
+
if (result.payment?.transaction) {
|
|
1143
|
+
const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
|
|
1144
|
+
console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
|
|
1145
|
+
}
|
|
1146
|
+
return result.result || result;
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Check ERC20 allowance for a spender
|
|
1150
|
+
*/
|
|
1151
|
+
async checkAllowance(tokenAddress, spender, provider) {
|
|
1152
|
+
const contract = new ethers.Contract(
|
|
1153
|
+
tokenAddress,
|
|
1154
|
+
["function allowance(address owner, address spender) view returns (uint256)"],
|
|
1155
|
+
provider
|
|
1156
|
+
);
|
|
1157
|
+
return await contract.allowance(this.wallet.address, spender);
|
|
1158
|
+
}
|
|
396
1159
|
/**
|
|
397
1160
|
* Sign EIP-3009 transferWithAuthorization (GASLESS)
|
|
398
1161
|
* This only signs - no on-chain transaction, no gas needed.
|
|
@@ -463,26 +1226,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
463
1226
|
}
|
|
464
1227
|
// --- Config & Wallet Management ---
|
|
465
1228
|
loadConfig() {
|
|
466
|
-
const configPath =
|
|
467
|
-
if (
|
|
468
|
-
const content =
|
|
1229
|
+
const configPath = join2(this.configDir, "config.json");
|
|
1230
|
+
if (existsSync2(configPath)) {
|
|
1231
|
+
const content = readFileSync2(configPath, "utf-8");
|
|
469
1232
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
470
1233
|
}
|
|
471
1234
|
return { ...DEFAULT_CONFIG };
|
|
472
1235
|
}
|
|
473
1236
|
saveConfig() {
|
|
474
|
-
|
|
475
|
-
const configPath =
|
|
476
|
-
|
|
1237
|
+
mkdirSync2(this.configDir, { recursive: true });
|
|
1238
|
+
const configPath = join2(this.configDir, "config.json");
|
|
1239
|
+
writeFileSync2(configPath, JSON.stringify(this.config, null, 2));
|
|
477
1240
|
}
|
|
478
1241
|
/**
|
|
479
1242
|
* Load spending data from disk
|
|
480
1243
|
*/
|
|
481
1244
|
loadSpending() {
|
|
482
|
-
const spendingPath =
|
|
483
|
-
if (
|
|
1245
|
+
const spendingPath = join2(this.configDir, "spending.json");
|
|
1246
|
+
if (existsSync2(spendingPath)) {
|
|
484
1247
|
try {
|
|
485
|
-
const data = JSON.parse(
|
|
1248
|
+
const data = JSON.parse(readFileSync2(spendingPath, "utf-8"));
|
|
486
1249
|
const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
|
|
487
1250
|
if (data.date && data.date === today) {
|
|
488
1251
|
this.todaySpending = data.amount || 0;
|
|
@@ -501,18 +1264,18 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
501
1264
|
* Save spending data to disk
|
|
502
1265
|
*/
|
|
503
1266
|
saveSpending() {
|
|
504
|
-
|
|
505
|
-
const spendingPath =
|
|
1267
|
+
mkdirSync2(this.configDir, { recursive: true });
|
|
1268
|
+
const spendingPath = join2(this.configDir, "spending.json");
|
|
506
1269
|
const data = {
|
|
507
1270
|
date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
|
|
508
1271
|
amount: this.todaySpending,
|
|
509
1272
|
updatedAt: Date.now()
|
|
510
1273
|
};
|
|
511
|
-
|
|
1274
|
+
writeFileSync2(spendingPath, JSON.stringify(data, null, 2));
|
|
512
1275
|
}
|
|
513
1276
|
loadWallet() {
|
|
514
|
-
const walletPath =
|
|
515
|
-
if (
|
|
1277
|
+
const walletPath = join2(this.configDir, "wallet.json");
|
|
1278
|
+
if (existsSync2(walletPath)) {
|
|
516
1279
|
try {
|
|
517
1280
|
const stats = statSync(walletPath);
|
|
518
1281
|
const mode = stats.mode & 511;
|
|
@@ -523,7 +1286,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
523
1286
|
}
|
|
524
1287
|
} catch (err) {
|
|
525
1288
|
}
|
|
526
|
-
const content =
|
|
1289
|
+
const content = readFileSync2(walletPath, "utf-8");
|
|
527
1290
|
return JSON.parse(content);
|
|
528
1291
|
}
|
|
529
1292
|
return null;
|
|
@@ -532,15 +1295,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
532
1295
|
* Initialize a new wallet (called by CLI)
|
|
533
1296
|
*/
|
|
534
1297
|
static init(configDir, options) {
|
|
535
|
-
|
|
1298
|
+
mkdirSync2(configDir, { recursive: true });
|
|
536
1299
|
const wallet = Wallet.createRandom();
|
|
537
1300
|
const walletData = {
|
|
538
1301
|
address: wallet.address,
|
|
539
1302
|
privateKey: wallet.privateKey,
|
|
540
1303
|
createdAt: Date.now()
|
|
541
1304
|
};
|
|
542
|
-
const walletPath =
|
|
543
|
-
|
|
1305
|
+
const walletPath = join2(configDir, "wallet.json");
|
|
1306
|
+
writeFileSync2(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
544
1307
|
const config = {
|
|
545
1308
|
chain: options.chain,
|
|
546
1309
|
limits: {
|
|
@@ -548,8 +1311,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
548
1311
|
maxPerDay: options.maxPerDay
|
|
549
1312
|
}
|
|
550
1313
|
};
|
|
551
|
-
const configPath =
|
|
552
|
-
|
|
1314
|
+
const configPath = join2(configDir, "config.json");
|
|
1315
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2));
|
|
553
1316
|
return { address: wallet.address, configDir };
|
|
554
1317
|
}
|
|
555
1318
|
/**
|
|
@@ -585,7 +1348,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
585
1348
|
if (!this.wallet) {
|
|
586
1349
|
throw new Error("Client not initialized");
|
|
587
1350
|
}
|
|
588
|
-
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
|
|
1351
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
|
|
589
1352
|
const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
590
1353
|
const results = {};
|
|
591
1354
|
const tempoTokens = {
|
|
@@ -656,12 +1419,12 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
656
1419
|
if (!this.wallet || !this.walletData) {
|
|
657
1420
|
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
658
1421
|
}
|
|
659
|
-
const { privateKeyToAccount } = await import("viem/accounts");
|
|
1422
|
+
const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
|
|
660
1423
|
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
661
1424
|
const { tempoModerato } = await import("viem/chains");
|
|
662
1425
|
const { Actions } = await import("viem/tempo");
|
|
663
1426
|
const privateKey = this.walletData.privateKey;
|
|
664
|
-
const account =
|
|
1427
|
+
const account = privateKeyToAccount2(privateKey);
|
|
665
1428
|
console.log(`[MoltsPay] Making MPP request to: ${url}`);
|
|
666
1429
|
console.log(`[MoltsPay] Using account: ${account.address}`);
|
|
667
1430
|
const initResponse = await fetch(url, {
|
|
@@ -758,24 +1521,16 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
758
1521
|
|
|
759
1522
|
// src/server/index.ts
|
|
760
1523
|
init_esm_shims();
|
|
761
|
-
import { readFileSync as
|
|
1524
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
762
1525
|
import { createServer } from "http";
|
|
763
1526
|
import * as path3 from "path";
|
|
764
1527
|
|
|
765
1528
|
// src/facilitators/index.ts
|
|
766
1529
|
init_esm_shims();
|
|
767
1530
|
|
|
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
1531
|
// src/facilitators/cdp.ts
|
|
777
1532
|
init_esm_shims();
|
|
778
|
-
import { readFileSync as
|
|
1533
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
|
|
779
1534
|
import * as path2 from "path";
|
|
780
1535
|
var X402_VERSION2 = 2;
|
|
781
1536
|
var CDP_URL = "https://api.cdp.coinbase.com/platform/v2/x402";
|
|
@@ -786,9 +1541,9 @@ function loadEnvFile() {
|
|
|
786
1541
|
path2.join(process.env.HOME || "", ".moltspay", ".env")
|
|
787
1542
|
];
|
|
788
1543
|
for (const envPath of envPaths) {
|
|
789
|
-
if (
|
|
1544
|
+
if (existsSync3(envPath)) {
|
|
790
1545
|
try {
|
|
791
|
-
const content =
|
|
1546
|
+
const content = readFileSync3(envPath, "utf-8");
|
|
792
1547
|
for (const line of content.split("\n")) {
|
|
793
1548
|
const trimmed = line.trim();
|
|
794
1549
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -1026,16 +1781,278 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
1026
1781
|
}
|
|
1027
1782
|
return { healthy: true, latencyMs: Date.now() - start };
|
|
1028
1783
|
} catch (error) {
|
|
1029
|
-
return { healthy: false, error: String(error) };
|
|
1784
|
+
return { healthy: false, error: String(error) };
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
async verify(paymentPayload, requirements) {
|
|
1788
|
+
try {
|
|
1789
|
+
const tempoPayload = paymentPayload.payload;
|
|
1790
|
+
if (!tempoPayload?.txHash) {
|
|
1791
|
+
return { valid: false, error: "Missing txHash in payment payload" };
|
|
1792
|
+
}
|
|
1793
|
+
const receipt = await this.getTransactionReceipt(tempoPayload.txHash);
|
|
1794
|
+
if (!receipt) {
|
|
1795
|
+
return { valid: false, error: "Transaction not found" };
|
|
1796
|
+
}
|
|
1797
|
+
if (receipt.status !== "0x1") {
|
|
1798
|
+
return { valid: false, error: "Transaction failed" };
|
|
1799
|
+
}
|
|
1800
|
+
const transferLog = receipt.logs.find(
|
|
1801
|
+
(log) => log.topics[0] === TRANSFER_EVENT_TOPIC
|
|
1802
|
+
);
|
|
1803
|
+
if (!transferLog) {
|
|
1804
|
+
return { valid: false, error: "No Transfer event found" };
|
|
1805
|
+
}
|
|
1806
|
+
const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
|
|
1807
|
+
const expectedTo = requirements.payTo.toLowerCase();
|
|
1808
|
+
if (toAddress !== expectedTo) {
|
|
1809
|
+
return {
|
|
1810
|
+
valid: false,
|
|
1811
|
+
error: `Wrong recipient: ${toAddress}, expected ${expectedTo}`
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
const amount = BigInt(transferLog.data);
|
|
1815
|
+
const expectedAmount = BigInt(requirements.amount);
|
|
1816
|
+
if (amount < expectedAmount) {
|
|
1817
|
+
return {
|
|
1818
|
+
valid: false,
|
|
1819
|
+
error: `Insufficient amount: ${amount}, expected ${expectedAmount}`
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
const tokenAddress = transferLog.address.toLowerCase();
|
|
1823
|
+
const expectedToken = requirements.asset.toLowerCase();
|
|
1824
|
+
if (tokenAddress !== expectedToken) {
|
|
1825
|
+
return {
|
|
1826
|
+
valid: false,
|
|
1827
|
+
error: `Wrong token: ${tokenAddress}, expected ${expectedToken}`
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
return {
|
|
1831
|
+
valid: true,
|
|
1832
|
+
details: {
|
|
1833
|
+
txHash: tempoPayload.txHash,
|
|
1834
|
+
from: "0x" + transferLog.topics[1].slice(26),
|
|
1835
|
+
to: toAddress,
|
|
1836
|
+
amount: amount.toString(),
|
|
1837
|
+
token: tokenAddress
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
async settle(paymentPayload, requirements) {
|
|
1845
|
+
const verifyResult = await this.verify(paymentPayload, requirements);
|
|
1846
|
+
if (!verifyResult.valid) {
|
|
1847
|
+
return { success: false, error: verifyResult.error };
|
|
1848
|
+
}
|
|
1849
|
+
const tempoPayload = paymentPayload.payload;
|
|
1850
|
+
return {
|
|
1851
|
+
success: true,
|
|
1852
|
+
transaction: tempoPayload.txHash,
|
|
1853
|
+
status: "settled"
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
async getTransactionReceipt(txHash) {
|
|
1857
|
+
const response = await fetch(this.rpcUrl, {
|
|
1858
|
+
method: "POST",
|
|
1859
|
+
headers: { "Content-Type": "application/json" },
|
|
1860
|
+
body: JSON.stringify({
|
|
1861
|
+
jsonrpc: "2.0",
|
|
1862
|
+
method: "eth_getTransactionReceipt",
|
|
1863
|
+
params: [txHash],
|
|
1864
|
+
id: 1
|
|
1865
|
+
})
|
|
1866
|
+
});
|
|
1867
|
+
const data = await response.json();
|
|
1868
|
+
return data.result;
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
|
|
1872
|
+
// src/facilitators/bnb.ts
|
|
1873
|
+
init_esm_shims();
|
|
1874
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
1875
|
+
var TRANSFER_EVENT_TOPIC2 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
1876
|
+
var EIP712_DOMAIN = {
|
|
1877
|
+
name: "MoltsPay",
|
|
1878
|
+
version: "1"
|
|
1879
|
+
};
|
|
1880
|
+
var INTENT_TYPES = {
|
|
1881
|
+
PaymentIntent: [
|
|
1882
|
+
{ name: "from", type: "address" },
|
|
1883
|
+
{ name: "to", type: "address" },
|
|
1884
|
+
{ name: "amount", type: "uint256" },
|
|
1885
|
+
{ name: "token", type: "address" },
|
|
1886
|
+
{ name: "service", type: "string" },
|
|
1887
|
+
{ name: "nonce", type: "uint256" },
|
|
1888
|
+
{ name: "deadline", type: "uint256" }
|
|
1889
|
+
]
|
|
1890
|
+
};
|
|
1891
|
+
var BNBFacilitator = class extends BaseFacilitator {
|
|
1892
|
+
name = "bnb";
|
|
1893
|
+
displayName = "BNB Smart Chain";
|
|
1894
|
+
supportedNetworks = ["eip155:56", "eip155:97"];
|
|
1895
|
+
// Mainnet + Testnet
|
|
1896
|
+
serverPrivateKey;
|
|
1897
|
+
spenderAddress = null;
|
|
1898
|
+
chainConfigs;
|
|
1899
|
+
constructor(serverPrivateKey) {
|
|
1900
|
+
super();
|
|
1901
|
+
this.serverPrivateKey = serverPrivateKey || process.env.BNB_SERVER_PRIVATE_KEY || "";
|
|
1902
|
+
if (this.serverPrivateKey) {
|
|
1903
|
+
const key = this.serverPrivateKey.startsWith("0x") ? this.serverPrivateKey : `0x${this.serverPrivateKey}`;
|
|
1904
|
+
const account = privateKeyToAccount(key);
|
|
1905
|
+
this.spenderAddress = account.address;
|
|
1906
|
+
}
|
|
1907
|
+
this.chainConfigs = {
|
|
1908
|
+
56: { rpc: CHAINS.bnb.rpc, chain: CHAINS.bnb },
|
|
1909
|
+
97: { rpc: CHAINS.bnb_testnet.rpc, chain: CHAINS.bnb_testnet }
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
async healthCheck() {
|
|
1913
|
+
const start = Date.now();
|
|
1914
|
+
try {
|
|
1915
|
+
const response = await fetch(this.chainConfigs[56].rpc, {
|
|
1916
|
+
method: "POST",
|
|
1917
|
+
headers: { "Content-Type": "application/json" },
|
|
1918
|
+
body: JSON.stringify({
|
|
1919
|
+
jsonrpc: "2.0",
|
|
1920
|
+
method: "eth_chainId",
|
|
1921
|
+
params: [],
|
|
1922
|
+
id: 1
|
|
1923
|
+
})
|
|
1924
|
+
});
|
|
1925
|
+
const data = await response.json();
|
|
1926
|
+
const chainId = parseInt(data.result, 16);
|
|
1927
|
+
if (chainId !== 56) {
|
|
1928
|
+
return { healthy: false, error: `Wrong chainId: ${chainId}` };
|
|
1929
|
+
}
|
|
1930
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
1931
|
+
} catch (error) {
|
|
1932
|
+
return { healthy: false, error: String(error) };
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Verify a payment intent signature (before service execution)
|
|
1937
|
+
*
|
|
1938
|
+
* This verifies:
|
|
1939
|
+
* 1. Signature is valid for the intent
|
|
1940
|
+
* 2. Client has approved server wallet
|
|
1941
|
+
* 3. Client has sufficient balance
|
|
1942
|
+
* 4. Intent hasn't expired
|
|
1943
|
+
*/
|
|
1944
|
+
async verify(paymentPayload, requirements) {
|
|
1945
|
+
try {
|
|
1946
|
+
const bnbPayload = paymentPayload.payload;
|
|
1947
|
+
if (!bnbPayload?.intent) {
|
|
1948
|
+
return { valid: false, error: "Missing intent in payment payload" };
|
|
1949
|
+
}
|
|
1950
|
+
const { intent, chainId } = bnbPayload;
|
|
1951
|
+
const config = this.chainConfigs[chainId];
|
|
1952
|
+
if (!config) {
|
|
1953
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
1954
|
+
}
|
|
1955
|
+
if (intent.deadline < Date.now()) {
|
|
1956
|
+
return { valid: false, error: "Intent expired" };
|
|
1957
|
+
}
|
|
1958
|
+
const recoveredAddress = await this.recoverIntentSigner(intent, chainId);
|
|
1959
|
+
if (recoveredAddress.toLowerCase() !== intent.from.toLowerCase()) {
|
|
1960
|
+
return { valid: false, error: "Invalid signature" };
|
|
1961
|
+
}
|
|
1962
|
+
if (intent.to.toLowerCase() !== requirements.payTo.toLowerCase()) {
|
|
1963
|
+
return { valid: false, error: `Wrong recipient: ${intent.to}` };
|
|
1964
|
+
}
|
|
1965
|
+
if (BigInt(intent.amount) < BigInt(requirements.amount)) {
|
|
1966
|
+
return { valid: false, error: `Insufficient amount: ${intent.amount}` };
|
|
1967
|
+
}
|
|
1968
|
+
if (intent.token.toLowerCase() !== requirements.asset.toLowerCase()) {
|
|
1969
|
+
return { valid: false, error: `Wrong token: ${intent.token}` };
|
|
1970
|
+
}
|
|
1971
|
+
const serverAddress = await this.getServerAddress();
|
|
1972
|
+
const allowance = await this.getAllowance(intent.from, serverAddress, intent.token, config.rpc);
|
|
1973
|
+
if (BigInt(allowance) < BigInt(intent.amount)) {
|
|
1974
|
+
return { valid: false, error: "Insufficient allowance. Run: npx moltspay init --chain bnb" };
|
|
1975
|
+
}
|
|
1976
|
+
const balance = await this.getBalance(intent.from, intent.token, config.rpc);
|
|
1977
|
+
if (BigInt(balance) < BigInt(intent.amount)) {
|
|
1978
|
+
return { valid: false, error: "Insufficient balance" };
|
|
1979
|
+
}
|
|
1980
|
+
return {
|
|
1981
|
+
valid: true,
|
|
1982
|
+
details: {
|
|
1983
|
+
from: intent.from,
|
|
1984
|
+
to: intent.to,
|
|
1985
|
+
amount: intent.amount,
|
|
1986
|
+
token: intent.token,
|
|
1987
|
+
service: intent.service,
|
|
1988
|
+
nonce: intent.nonce,
|
|
1989
|
+
deadline: intent.deadline
|
|
1990
|
+
}
|
|
1991
|
+
};
|
|
1992
|
+
} catch (error) {
|
|
1993
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
1030
1994
|
}
|
|
1031
1995
|
}
|
|
1032
|
-
|
|
1996
|
+
/**
|
|
1997
|
+
* Settle a payment by executing transferFrom
|
|
1998
|
+
*
|
|
1999
|
+
* This is called AFTER the service has been successfully delivered.
|
|
2000
|
+
* Server pays gas, transfers tokens from client to provider.
|
|
2001
|
+
*/
|
|
2002
|
+
async settle(paymentPayload, requirements) {
|
|
2003
|
+
if (!this.serverPrivateKey) {
|
|
2004
|
+
return { success: false, error: "Server wallet not configured (BNB_SERVER_PRIVATE_KEY)" };
|
|
2005
|
+
}
|
|
1033
2006
|
try {
|
|
1034
|
-
const
|
|
1035
|
-
if (!
|
|
1036
|
-
return {
|
|
2007
|
+
const verifyResult = await this.verify(paymentPayload, requirements);
|
|
2008
|
+
if (!verifyResult.valid) {
|
|
2009
|
+
return { success: false, error: verifyResult.error };
|
|
1037
2010
|
}
|
|
1038
|
-
const
|
|
2011
|
+
const bnbPayload = paymentPayload.payload;
|
|
2012
|
+
const { intent, chainId } = bnbPayload;
|
|
2013
|
+
const config = this.chainConfigs[chainId];
|
|
2014
|
+
const txHash = await this.executeTransferFrom(
|
|
2015
|
+
intent.from,
|
|
2016
|
+
intent.to,
|
|
2017
|
+
intent.amount,
|
|
2018
|
+
intent.token,
|
|
2019
|
+
config.rpc
|
|
2020
|
+
);
|
|
2021
|
+
return {
|
|
2022
|
+
success: true,
|
|
2023
|
+
transaction: txHash,
|
|
2024
|
+
status: "settled"
|
|
2025
|
+
};
|
|
2026
|
+
} catch (error) {
|
|
2027
|
+
return { success: false, error: `Settlement failed: ${error}` };
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Check if client has approved the server wallet
|
|
2032
|
+
*/
|
|
2033
|
+
async checkApproval(clientAddress, token, chainId) {
|
|
2034
|
+
const config = this.chainConfigs[chainId];
|
|
2035
|
+
if (!config) {
|
|
2036
|
+
throw new Error(`Unsupported chainId: ${chainId}`);
|
|
2037
|
+
}
|
|
2038
|
+
const serverAddress = await this.getServerAddress();
|
|
2039
|
+
const allowance = await this.getAllowance(clientAddress, serverAddress, token, config.rpc);
|
|
2040
|
+
const minAllowance = BigInt("1000000000000000000000");
|
|
2041
|
+
return {
|
|
2042
|
+
approved: BigInt(allowance) >= minAllowance,
|
|
2043
|
+
allowance
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Verify a completed transaction (for checking past payments)
|
|
2048
|
+
*/
|
|
2049
|
+
async verifyTransaction(txHash, expected, chainId) {
|
|
2050
|
+
const config = this.chainConfigs[chainId];
|
|
2051
|
+
if (!config) {
|
|
2052
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
2053
|
+
}
|
|
2054
|
+
try {
|
|
2055
|
+
const receipt = await this.getTransactionReceipt(txHash, config.rpc);
|
|
1039
2056
|
if (!receipt) {
|
|
1040
2057
|
return { valid: false, error: "Transaction not found" };
|
|
1041
2058
|
}
|
|
@@ -1043,63 +2060,117 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
1043
2060
|
return { valid: false, error: "Transaction failed" };
|
|
1044
2061
|
}
|
|
1045
2062
|
const transferLog = receipt.logs.find(
|
|
1046
|
-
(log) => log.topics[0] ===
|
|
2063
|
+
(log) => log.topics[0] === TRANSFER_EVENT_TOPIC2 && log.address.toLowerCase() === expected.token.toLowerCase()
|
|
1047
2064
|
);
|
|
1048
2065
|
if (!transferLog) {
|
|
1049
2066
|
return { valid: false, error: "No Transfer event found" };
|
|
1050
2067
|
}
|
|
1051
2068
|
const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
return {
|
|
1055
|
-
valid: false,
|
|
1056
|
-
error: `Wrong recipient: ${toAddress}, expected ${expectedTo}`
|
|
1057
|
-
};
|
|
2069
|
+
if (toAddress !== expected.to.toLowerCase()) {
|
|
2070
|
+
return { valid: false, error: `Wrong recipient: ${toAddress}` };
|
|
1058
2071
|
}
|
|
1059
2072
|
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
|
-
};
|
|
2073
|
+
if (amount < BigInt(expected.amount)) {
|
|
2074
|
+
return { valid: false, error: `Insufficient amount: ${amount}` };
|
|
1074
2075
|
}
|
|
1075
2076
|
return {
|
|
1076
2077
|
valid: true,
|
|
1077
2078
|
details: {
|
|
1078
|
-
txHash
|
|
2079
|
+
txHash,
|
|
1079
2080
|
from: "0x" + transferLog.topics[1].slice(26),
|
|
1080
2081
|
to: toAddress,
|
|
1081
2082
|
amount: amount.toString(),
|
|
1082
|
-
token:
|
|
2083
|
+
token: transferLog.address
|
|
1083
2084
|
}
|
|
1084
2085
|
};
|
|
1085
2086
|
} catch (error) {
|
|
1086
2087
|
return { valid: false, error: `Verification failed: ${error}` };
|
|
1087
2088
|
}
|
|
1088
2089
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
return
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
2090
|
+
// ==================== Private Methods ====================
|
|
2091
|
+
/**
|
|
2092
|
+
* Get the server's spender address (public, for 402 responses)
|
|
2093
|
+
* Returns cached value computed at construction time.
|
|
2094
|
+
*/
|
|
2095
|
+
getSpenderAddress() {
|
|
2096
|
+
return this.spenderAddress;
|
|
2097
|
+
}
|
|
2098
|
+
async getServerAddress() {
|
|
2099
|
+
const { ethers: ethers3 } = await import("ethers");
|
|
2100
|
+
const wallet = new ethers3.Wallet(this.serverPrivateKey);
|
|
2101
|
+
return wallet.address;
|
|
2102
|
+
}
|
|
2103
|
+
async recoverIntentSigner(intent, chainId) {
|
|
2104
|
+
const { ethers: ethers3 } = await import("ethers");
|
|
2105
|
+
const domain = {
|
|
2106
|
+
...EIP712_DOMAIN,
|
|
2107
|
+
chainId
|
|
1099
2108
|
};
|
|
2109
|
+
const message = {
|
|
2110
|
+
from: intent.from,
|
|
2111
|
+
to: intent.to,
|
|
2112
|
+
amount: intent.amount,
|
|
2113
|
+
token: intent.token,
|
|
2114
|
+
service: intent.service,
|
|
2115
|
+
nonce: intent.nonce,
|
|
2116
|
+
deadline: intent.deadline
|
|
2117
|
+
};
|
|
2118
|
+
const recoveredAddress = ethers3.verifyTypedData(
|
|
2119
|
+
domain,
|
|
2120
|
+
INTENT_TYPES,
|
|
2121
|
+
message,
|
|
2122
|
+
intent.signature
|
|
2123
|
+
);
|
|
2124
|
+
return recoveredAddress;
|
|
1100
2125
|
}
|
|
1101
|
-
async
|
|
1102
|
-
const
|
|
2126
|
+
async getAllowance(owner, spender, token, rpcUrl) {
|
|
2127
|
+
const selector = "0xdd62ed3e";
|
|
2128
|
+
const ownerPadded = owner.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
2129
|
+
const spenderPadded = spender.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
2130
|
+
const data = selector + ownerPadded + spenderPadded;
|
|
2131
|
+
const response = await fetch(rpcUrl, {
|
|
2132
|
+
method: "POST",
|
|
2133
|
+
headers: { "Content-Type": "application/json" },
|
|
2134
|
+
body: JSON.stringify({
|
|
2135
|
+
jsonrpc: "2.0",
|
|
2136
|
+
method: "eth_call",
|
|
2137
|
+
params: [{ to: token, data }, "latest"],
|
|
2138
|
+
id: 1
|
|
2139
|
+
})
|
|
2140
|
+
});
|
|
2141
|
+
const result = await response.json();
|
|
2142
|
+
return result.result || "0x0";
|
|
2143
|
+
}
|
|
2144
|
+
async getBalance(account, token, rpcUrl) {
|
|
2145
|
+
const selector = "0x70a08231";
|
|
2146
|
+
const accountPadded = account.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
2147
|
+
const data = selector + accountPadded;
|
|
2148
|
+
const response = await fetch(rpcUrl, {
|
|
2149
|
+
method: "POST",
|
|
2150
|
+
headers: { "Content-Type": "application/json" },
|
|
2151
|
+
body: JSON.stringify({
|
|
2152
|
+
jsonrpc: "2.0",
|
|
2153
|
+
method: "eth_call",
|
|
2154
|
+
params: [{ to: token, data }, "latest"],
|
|
2155
|
+
id: 1
|
|
2156
|
+
})
|
|
2157
|
+
});
|
|
2158
|
+
const result = await response.json();
|
|
2159
|
+
return result.result || "0x0";
|
|
2160
|
+
}
|
|
2161
|
+
async executeTransferFrom(from, to, amount, token, rpcUrl) {
|
|
2162
|
+
const { ethers: ethers3 } = await import("ethers");
|
|
2163
|
+
const provider = new ethers3.JsonRpcProvider(rpcUrl);
|
|
2164
|
+
const wallet = new ethers3.Wallet(this.serverPrivateKey, provider);
|
|
2165
|
+
const tokenContract = new ethers3.Contract(token, [
|
|
2166
|
+
"function transferFrom(address from, address to, uint256 amount) returns (bool)"
|
|
2167
|
+
], wallet);
|
|
2168
|
+
const tx = await tokenContract.transferFrom(from, to, amount);
|
|
2169
|
+
const receipt = await tx.wait();
|
|
2170
|
+
return receipt.hash;
|
|
2171
|
+
}
|
|
2172
|
+
async getTransactionReceipt(txHash, rpcUrl) {
|
|
2173
|
+
const response = await fetch(rpcUrl, {
|
|
1103
2174
|
method: "POST",
|
|
1104
2175
|
headers: { "Content-Type": "application/json" },
|
|
1105
2176
|
body: JSON.stringify({
|
|
@@ -1116,6 +2187,8 @@ var TempoFacilitator = class extends BaseFacilitator {
|
|
|
1116
2187
|
|
|
1117
2188
|
// src/facilitators/registry.ts
|
|
1118
2189
|
init_esm_shims();
|
|
2190
|
+
import { Keypair as Keypair4 } from "@solana/web3.js";
|
|
2191
|
+
import bs582 from "bs58";
|
|
1119
2192
|
var FacilitatorRegistry = class {
|
|
1120
2193
|
factories = /* @__PURE__ */ new Map();
|
|
1121
2194
|
instances = /* @__PURE__ */ new Map();
|
|
@@ -1124,7 +2197,20 @@ var FacilitatorRegistry = class {
|
|
|
1124
2197
|
constructor(selection) {
|
|
1125
2198
|
this.registerFactory("cdp", (config) => new CDPFacilitator(config));
|
|
1126
2199
|
this.registerFactory("tempo", () => new TempoFacilitator());
|
|
1127
|
-
this.
|
|
2200
|
+
this.registerFactory("bnb", (config) => new BNBFacilitator(config?.serverPrivateKey));
|
|
2201
|
+
this.registerFactory("solana", (config) => {
|
|
2202
|
+
let feePayerKeypair;
|
|
2203
|
+
const feePayerKey = config?.feePayerPrivateKey || process.env.SOLANA_FEE_PAYER_KEY;
|
|
2204
|
+
if (feePayerKey) {
|
|
2205
|
+
try {
|
|
2206
|
+
feePayerKeypair = Keypair4.fromSecretKey(bs582.decode(feePayerKey));
|
|
2207
|
+
} catch (e) {
|
|
2208
|
+
console.warn(`[SolanaFacilitator] Invalid fee payer key: ${e.message}`);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
return new SolanaFacilitator({ feePayerKeypair });
|
|
2212
|
+
});
|
|
2213
|
+
this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
|
|
1128
2214
|
}
|
|
1129
2215
|
/**
|
|
1130
2216
|
* Register a new facilitator factory
|
|
@@ -1371,14 +2457,40 @@ var TOKEN_ADDRESSES = {
|
|
|
1371
2457
|
// pathUSD
|
|
1372
2458
|
USDT: "0x20c0000000000000000000000000000000000001"
|
|
1373
2459
|
// alphaUSD
|
|
2460
|
+
},
|
|
2461
|
+
// BNB Smart Chain mainnet
|
|
2462
|
+
"eip155:56": {
|
|
2463
|
+
USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
2464
|
+
USDT: "0x55d398326f99059fF775485246999027B3197955"
|
|
2465
|
+
},
|
|
2466
|
+
// BNB Smart Chain testnet
|
|
2467
|
+
"eip155:97": {
|
|
2468
|
+
USDC: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
2469
|
+
USDT: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd"
|
|
2470
|
+
},
|
|
2471
|
+
// Solana networks use mint addresses (SPL tokens)
|
|
2472
|
+
"solana:mainnet": {
|
|
2473
|
+
USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
|
2474
|
+
// Circle USDC
|
|
2475
|
+
},
|
|
2476
|
+
"solana:devnet": {
|
|
2477
|
+
USDC: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"
|
|
2478
|
+
// Devnet USDC
|
|
1374
2479
|
}
|
|
1375
2480
|
};
|
|
1376
2481
|
var CHAIN_TO_NETWORK = {
|
|
1377
2482
|
"base": "eip155:8453",
|
|
1378
2483
|
"base_sepolia": "eip155:84532",
|
|
1379
2484
|
"polygon": "eip155:137",
|
|
1380
|
-
"tempo_moderato": "eip155:42431"
|
|
2485
|
+
"tempo_moderato": "eip155:42431",
|
|
2486
|
+
"bnb": "eip155:56",
|
|
2487
|
+
"bnb_testnet": "eip155:97",
|
|
2488
|
+
"solana": "solana:mainnet",
|
|
2489
|
+
"solana_devnet": "solana:devnet"
|
|
1381
2490
|
};
|
|
2491
|
+
function isSolanaNetwork(network) {
|
|
2492
|
+
return network.startsWith("solana:");
|
|
2493
|
+
}
|
|
1382
2494
|
var TOKEN_DOMAINS = {
|
|
1383
2495
|
// Base mainnet
|
|
1384
2496
|
"eip155:8453": {
|
|
@@ -1400,6 +2512,16 @@ var TOKEN_DOMAINS = {
|
|
|
1400
2512
|
"eip155:42431": {
|
|
1401
2513
|
USDC: { name: "pathUSD", version: "1" },
|
|
1402
2514
|
USDT: { name: "alphaUSD", version: "1" }
|
|
2515
|
+
},
|
|
2516
|
+
// BNB Smart Chain mainnet
|
|
2517
|
+
"eip155:56": {
|
|
2518
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
2519
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
2520
|
+
},
|
|
2521
|
+
// BNB Smart Chain testnet
|
|
2522
|
+
"eip155:97": {
|
|
2523
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
2524
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
1403
2525
|
}
|
|
1404
2526
|
};
|
|
1405
2527
|
function getTokenDomain(network, token) {
|
|
@@ -1415,9 +2537,9 @@ function loadEnvFile2() {
|
|
|
1415
2537
|
path3.join(process.env.HOME || "", ".moltspay", ".env")
|
|
1416
2538
|
];
|
|
1417
2539
|
for (const envPath of envPaths) {
|
|
1418
|
-
if (
|
|
2540
|
+
if (existsSync4(envPath)) {
|
|
1419
2541
|
try {
|
|
1420
|
-
const content =
|
|
2542
|
+
const content = readFileSync4(envPath, "utf-8");
|
|
1421
2543
|
for (const line of content.split("\n")) {
|
|
1422
2544
|
const trimmed = line.trim();
|
|
1423
2545
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -1448,7 +2570,7 @@ var MoltsPayServer = class {
|
|
|
1448
2570
|
useMainnet;
|
|
1449
2571
|
constructor(servicesPath, options = {}) {
|
|
1450
2572
|
loadEnvFile2();
|
|
1451
|
-
const content =
|
|
2573
|
+
const content = readFileSync4(servicesPath, "utf-8");
|
|
1452
2574
|
this.manifest = JSON.parse(content);
|
|
1453
2575
|
this.options = {
|
|
1454
2576
|
port: options.port || 3e3,
|
|
@@ -1457,7 +2579,7 @@ var MoltsPayServer = class {
|
|
|
1457
2579
|
};
|
|
1458
2580
|
this.useMainnet = process.env.USE_MAINNET?.toLowerCase() === "true";
|
|
1459
2581
|
this.networkId = this.useMainnet ? "eip155:8453" : "eip155:84532";
|
|
1460
|
-
const defaultFallback = ["tempo"];
|
|
2582
|
+
const defaultFallback = ["tempo", "bnb", "solana"];
|
|
1461
2583
|
const envFallback = process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean);
|
|
1462
2584
|
const facilitatorConfig = options.facilitators || {
|
|
1463
2585
|
primary: process.env.FACILITATOR_PRIMARY || "cdp",
|
|
@@ -1500,12 +2622,20 @@ var MoltsPayServer = class {
|
|
|
1500
2622
|
*/
|
|
1501
2623
|
getProviderChains() {
|
|
1502
2624
|
const provider = this.manifest.provider;
|
|
2625
|
+
const getWalletForChain = (chainName, explicitWallet) => {
|
|
2626
|
+
if (explicitWallet) return explicitWallet;
|
|
2627
|
+
if ((chainName === "solana" || chainName === "solana_devnet") && provider.solana_wallet) {
|
|
2628
|
+
return provider.solana_wallet;
|
|
2629
|
+
}
|
|
2630
|
+
return provider.wallet;
|
|
2631
|
+
};
|
|
1503
2632
|
if (provider.chains && provider.chains.length > 0) {
|
|
1504
2633
|
return provider.chains.map((c) => {
|
|
1505
2634
|
const chainName = typeof c === "string" ? c : c.chain;
|
|
2635
|
+
const explicitWallet = typeof c === "object" ? c.wallet : null;
|
|
1506
2636
|
return {
|
|
1507
2637
|
network: CHAIN_TO_NETWORK[chainName] || "eip155:8453",
|
|
1508
|
-
wallet: (
|
|
2638
|
+
wallet: getWalletForChain(chainName, explicitWallet || void 0),
|
|
1509
2639
|
tokens: (typeof c === "object" ? c.tokens : null) || ["USDC"]
|
|
1510
2640
|
};
|
|
1511
2641
|
});
|
|
@@ -1514,7 +2644,7 @@ var MoltsPayServer = class {
|
|
|
1514
2644
|
const network = CHAIN_TO_NETWORK[chain] || this.networkId;
|
|
1515
2645
|
return [{
|
|
1516
2646
|
network,
|
|
1517
|
-
wallet:
|
|
2647
|
+
wallet: getWalletForChain(chain),
|
|
1518
2648
|
tokens: ["USDC"]
|
|
1519
2649
|
}];
|
|
1520
2650
|
}
|
|
@@ -1585,7 +2715,8 @@ var MoltsPayServer = class {
|
|
|
1585
2715
|
}
|
|
1586
2716
|
const body = await this.readBody(req);
|
|
1587
2717
|
const paymentHeader = req.headers[PAYMENT_HEADER2];
|
|
1588
|
-
|
|
2718
|
+
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
2719
|
+
return await this.handleProxy(body, paymentHeader, authHeader, res);
|
|
1589
2720
|
}
|
|
1590
2721
|
const servicePath = url.pathname.replace(/^\//, "");
|
|
1591
2722
|
const skill = this.skills.get(servicePath);
|
|
@@ -1622,7 +2753,9 @@ var MoltsPayServer = class {
|
|
|
1622
2753
|
name: this.manifest.provider.name,
|
|
1623
2754
|
description: this.manifest.provider.description,
|
|
1624
2755
|
wallet: this.manifest.provider.wallet,
|
|
1625
|
-
chain: this.manifest.provider.chain || "base"
|
|
2756
|
+
chain: this.manifest.provider.chain || "base",
|
|
2757
|
+
solana_wallet: this.manifest.provider.solana_wallet,
|
|
2758
|
+
chains: this.manifest.provider.chains
|
|
1626
2759
|
},
|
|
1627
2760
|
services,
|
|
1628
2761
|
endpoints: {
|
|
@@ -1735,6 +2868,21 @@ var MoltsPayServer = class {
|
|
|
1735
2868
|
});
|
|
1736
2869
|
}
|
|
1737
2870
|
console.log(`[MoltsPay] Verified by ${verifyResult.facilitator}`);
|
|
2871
|
+
const isSolana = isSolanaNetwork(paymentNetwork);
|
|
2872
|
+
let settlement = null;
|
|
2873
|
+
if (isSolana) {
|
|
2874
|
+
console.log(`[MoltsPay] Solana detected - settling payment FIRST (blockhash expiry protection)`);
|
|
2875
|
+
try {
|
|
2876
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
2877
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
2878
|
+
} catch (err) {
|
|
2879
|
+
console.error("[MoltsPay] Solana settlement failed:", err.message);
|
|
2880
|
+
return this.sendJson(res, 402, {
|
|
2881
|
+
error: "Payment settlement failed",
|
|
2882
|
+
message: err.message
|
|
2883
|
+
});
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
1738
2886
|
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
1739
2887
|
console.log(`[MoltsPay] Executing skill: ${service} (timeout: ${timeoutSeconds}s)`);
|
|
1740
2888
|
let result;
|
|
@@ -1749,16 +2897,19 @@ var MoltsPayServer = class {
|
|
|
1749
2897
|
console.error("[MoltsPay] Skill execution failed:", err.message);
|
|
1750
2898
|
return this.sendJson(res, 500, {
|
|
1751
2899
|
error: "Service execution failed",
|
|
1752
|
-
message: err.message
|
|
2900
|
+
message: err.message,
|
|
2901
|
+
paymentSettled: isSolana ? true : false,
|
|
2902
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
1753
2903
|
});
|
|
1754
2904
|
}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
2905
|
+
if (!isSolana) {
|
|
2906
|
+
console.log(`[MoltsPay] Skill succeeded, settling payment...`);
|
|
2907
|
+
try {
|
|
2908
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
2909
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
2910
|
+
} catch (err) {
|
|
2911
|
+
console.error("[MoltsPay] Settlement failed:", err.message);
|
|
2912
|
+
}
|
|
1762
2913
|
}
|
|
1763
2914
|
const responseHeaders = {};
|
|
1764
2915
|
if (settlement?.success) {
|
|
@@ -2034,7 +3185,7 @@ var MoltsPayServer = class {
|
|
|
2034
3185
|
const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
|
|
2035
3186
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
2036
3187
|
const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
|
|
2037
|
-
|
|
3188
|
+
const requirements = {
|
|
2038
3189
|
scheme: "exact",
|
|
2039
3190
|
network: selectedNetwork,
|
|
2040
3191
|
asset: tokenAddress,
|
|
@@ -2043,6 +3194,27 @@ var MoltsPayServer = class {
|
|
|
2043
3194
|
maxTimeoutSeconds: 300,
|
|
2044
3195
|
extra: tokenDomain
|
|
2045
3196
|
};
|
|
3197
|
+
if (selectedNetwork === "solana:mainnet" || selectedNetwork === "solana:devnet") {
|
|
3198
|
+
const solanaFacilitator = this.registry.get("solana");
|
|
3199
|
+
const feePayerPubkey = solanaFacilitator?.getFeePayerPubkey?.();
|
|
3200
|
+
if (feePayerPubkey) {
|
|
3201
|
+
requirements.extra = {
|
|
3202
|
+
...requirements.extra || {},
|
|
3203
|
+
solanaFeePayer: feePayerPubkey
|
|
3204
|
+
};
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
if (selectedNetwork === "eip155:56" || selectedNetwork === "eip155:97") {
|
|
3208
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
3209
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
3210
|
+
if (spenderAddress) {
|
|
3211
|
+
requirements.extra = {
|
|
3212
|
+
...requirements.extra || {},
|
|
3213
|
+
bnbSpender: spenderAddress
|
|
3214
|
+
};
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
return requirements;
|
|
2046
3218
|
}
|
|
2047
3219
|
/**
|
|
2048
3220
|
* Detect which token is being used in the payment
|
|
@@ -2108,31 +3280,42 @@ var MoltsPayServer = class {
|
|
|
2108
3280
|
/**
|
|
2109
3281
|
* POST /proxy - Handle payment for external services (moltspay-creators)
|
|
2110
3282
|
*
|
|
2111
|
-
* This endpoint allows other services to delegate x402 payment handling.
|
|
3283
|
+
* This endpoint allows other services to delegate x402/MPP payment handling.
|
|
2112
3284
|
* It does NOT execute any skill - just handles payment verification/settlement.
|
|
2113
3285
|
*
|
|
2114
3286
|
* Request body:
|
|
2115
3287
|
* { wallet, amount, currency, chain, memo, serviceId, description }
|
|
2116
3288
|
*
|
|
2117
|
-
*
|
|
2118
|
-
*
|
|
3289
|
+
* For x402 (base, polygon, base_sepolia):
|
|
3290
|
+
* Without X-Payment header: returns 402 with X-Payment-Required
|
|
3291
|
+
* With X-Payment header: verifies payment via CDP
|
|
3292
|
+
*
|
|
3293
|
+
* For MPP (tempo_moderato):
|
|
3294
|
+
* Without Authorization header: returns 402 with WWW-Authenticate
|
|
3295
|
+
* With Authorization: Payment header: verifies tx on Tempo chain
|
|
2119
3296
|
*/
|
|
2120
|
-
async handleProxy(body, paymentHeader, res) {
|
|
3297
|
+
async handleProxy(body, paymentHeader, authHeader, res) {
|
|
2121
3298
|
const { wallet, amount, currency, chain, memo, serviceId, description } = body;
|
|
2122
3299
|
if (!wallet || !amount) {
|
|
2123
3300
|
return this.sendJson(res, 400, { error: "Missing required fields: wallet, amount" });
|
|
2124
3301
|
}
|
|
2125
|
-
|
|
2126
|
-
|
|
3302
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
|
|
3303
|
+
if (chain && !supportedChains.includes(chain)) {
|
|
3304
|
+
return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
|
|
3305
|
+
}
|
|
3306
|
+
const isSolanaChain = chain === "solana" || chain === "solana_devnet";
|
|
3307
|
+
const isValidEvmAddress = /^0x[a-fA-F0-9]{40}$/.test(wallet);
|
|
3308
|
+
const isValidSolanaAddress2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(wallet);
|
|
3309
|
+
if (isSolanaChain && !isValidSolanaAddress2) {
|
|
3310
|
+
return this.sendJson(res, 400, { error: "Invalid Solana wallet address format" });
|
|
3311
|
+
}
|
|
3312
|
+
if (!isSolanaChain && !isValidEvmAddress) {
|
|
3313
|
+
return this.sendJson(res, 400, { error: "Invalid EVM wallet address format" });
|
|
2127
3314
|
}
|
|
2128
3315
|
const amountNum = parseFloat(amount);
|
|
2129
3316
|
if (isNaN(amountNum) || amountNum <= 0) {
|
|
2130
3317
|
return this.sendJson(res, 400, { error: "Invalid amount" });
|
|
2131
3318
|
}
|
|
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
3319
|
const proxyConfig = {
|
|
2137
3320
|
id: serviceId || "proxy",
|
|
2138
3321
|
name: description || "Proxy Payment",
|
|
@@ -2144,6 +3327,9 @@ var MoltsPayServer = class {
|
|
|
2144
3327
|
input: {},
|
|
2145
3328
|
output: {}
|
|
2146
3329
|
};
|
|
3330
|
+
if (chain === "tempo_moderato") {
|
|
3331
|
+
return await this.handleProxyMPP(body, proxyConfig, authHeader, res);
|
|
3332
|
+
}
|
|
2147
3333
|
const requirements = this.buildProxyPaymentRequirements(proxyConfig, wallet, currency, chain);
|
|
2148
3334
|
if (!paymentHeader) {
|
|
2149
3335
|
return this.sendProxyPaymentRequired(proxyConfig, wallet, memo, chain, res);
|
|
@@ -2155,37 +3341,225 @@ var MoltsPayServer = class {
|
|
|
2155
3341
|
} catch {
|
|
2156
3342
|
return this.sendJson(res, 400, { error: "Invalid X-Payment header" });
|
|
2157
3343
|
}
|
|
2158
|
-
if (payment.x402Version !== X402_VERSION3) {
|
|
2159
|
-
return this.sendJson(res, 402, { error: `Unsupported x402 version: ${payment.x402Version}` });
|
|
3344
|
+
if (payment.x402Version !== X402_VERSION3) {
|
|
3345
|
+
return this.sendJson(res, 402, { error: `Unsupported x402 version: ${payment.x402Version}` });
|
|
3346
|
+
}
|
|
3347
|
+
const scheme = payment.accepted?.scheme || payment.scheme;
|
|
3348
|
+
const network = payment.accepted?.network || payment.network;
|
|
3349
|
+
if (scheme !== "exact") {
|
|
3350
|
+
return this.sendJson(res, 402, { error: `Unsupported scheme: ${scheme}` });
|
|
3351
|
+
}
|
|
3352
|
+
const expectedNetwork = chain ? CHAIN_TO_NETWORK[chain] || this.networkId : this.networkId;
|
|
3353
|
+
if (network !== expectedNetwork) {
|
|
3354
|
+
return this.sendJson(res, 402, { error: `Network mismatch: expected ${expectedNetwork}, got ${network}` });
|
|
3355
|
+
}
|
|
3356
|
+
console.log(`[MoltsPay] /proxy: Verifying payment for ${wallet}...`);
|
|
3357
|
+
const verifyResult = await this.registry.verify(payment, requirements);
|
|
3358
|
+
if (!verifyResult.valid) {
|
|
3359
|
+
return this.sendJson(res, 402, {
|
|
3360
|
+
success: false,
|
|
3361
|
+
error: `Payment verification failed: ${verifyResult.error}`,
|
|
3362
|
+
facilitator: verifyResult.facilitator
|
|
3363
|
+
});
|
|
3364
|
+
}
|
|
3365
|
+
console.log(`[MoltsPay] /proxy: Verified by ${verifyResult.facilitator}`);
|
|
3366
|
+
const { execute, service, params } = body;
|
|
3367
|
+
if (execute && service) {
|
|
3368
|
+
const skill = this.skills.get(service);
|
|
3369
|
+
if (!skill) {
|
|
3370
|
+
console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
|
|
3371
|
+
return this.sendJson(res, 404, {
|
|
3372
|
+
success: false,
|
|
3373
|
+
paymentSettled: false,
|
|
3374
|
+
error: `Service not found: ${service}`
|
|
3375
|
+
});
|
|
3376
|
+
}
|
|
3377
|
+
const isSolana = isSolanaNetwork(network);
|
|
3378
|
+
let settlement2 = null;
|
|
3379
|
+
if (isSolana) {
|
|
3380
|
+
console.log(`[MoltsPay] /proxy: Solana detected - settling payment FIRST`);
|
|
3381
|
+
try {
|
|
3382
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
3383
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
3384
|
+
if (!settlement2.success) {
|
|
3385
|
+
console.error(`[MoltsPay] /proxy: Solana settlement failed: ${settlement2.error}`);
|
|
3386
|
+
return this.sendJson(res, 402, {
|
|
3387
|
+
success: false,
|
|
3388
|
+
paymentSettled: false,
|
|
3389
|
+
error: `Payment settlement failed: ${settlement2.error || "Unknown error"}`
|
|
3390
|
+
});
|
|
3391
|
+
}
|
|
3392
|
+
} catch (err) {
|
|
3393
|
+
console.error("[MoltsPay] /proxy: Solana settlement failed:", err.message);
|
|
3394
|
+
return this.sendJson(res, 402, {
|
|
3395
|
+
success: false,
|
|
3396
|
+
paymentSettled: false,
|
|
3397
|
+
error: `Payment settlement failed: ${err.message}`
|
|
3398
|
+
});
|
|
3399
|
+
}
|
|
3400
|
+
} else {
|
|
3401
|
+
console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
|
|
3402
|
+
}
|
|
3403
|
+
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
3404
|
+
let result;
|
|
3405
|
+
try {
|
|
3406
|
+
result = await Promise.race([
|
|
3407
|
+
skill.handler(params || {}),
|
|
3408
|
+
new Promise(
|
|
3409
|
+
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
3410
|
+
)
|
|
3411
|
+
]);
|
|
3412
|
+
console.log(`[MoltsPay] /proxy: Skill succeeded`);
|
|
3413
|
+
} catch (err) {
|
|
3414
|
+
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}`);
|
|
3415
|
+
return this.sendJson(res, 500, {
|
|
3416
|
+
success: false,
|
|
3417
|
+
paymentSettled: isSolana ? true : false,
|
|
3418
|
+
error: `Service execution failed: ${err.message}`,
|
|
3419
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
3420
|
+
});
|
|
3421
|
+
}
|
|
3422
|
+
if (!isSolana) {
|
|
3423
|
+
console.log(`[MoltsPay] /proxy: Settling payment...`);
|
|
3424
|
+
try {
|
|
3425
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
3426
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
3427
|
+
} catch (err) {
|
|
3428
|
+
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
3429
|
+
return this.sendJson(res, 200, {
|
|
3430
|
+
success: true,
|
|
3431
|
+
verified: true,
|
|
3432
|
+
settled: false,
|
|
3433
|
+
settlementError: err.message,
|
|
3434
|
+
from: payment.payload?.authorization?.from,
|
|
3435
|
+
paidTo: wallet,
|
|
3436
|
+
amount: amountNum,
|
|
3437
|
+
currency: currency || "USDC",
|
|
3438
|
+
memo,
|
|
3439
|
+
result
|
|
3440
|
+
});
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
return this.sendJson(res, 200, {
|
|
3444
|
+
success: true,
|
|
3445
|
+
verified: true,
|
|
3446
|
+
settled: settlement2?.success || false,
|
|
3447
|
+
txHash: settlement2?.transaction,
|
|
3448
|
+
from: payment.payload?.authorization?.from,
|
|
3449
|
+
paidTo: wallet,
|
|
3450
|
+
amount: amountNum,
|
|
3451
|
+
currency: currency || "USDC",
|
|
3452
|
+
facilitator: settlement2?.facilitator,
|
|
3453
|
+
memo,
|
|
3454
|
+
result
|
|
3455
|
+
});
|
|
3456
|
+
}
|
|
3457
|
+
console.log(`[MoltsPay] /proxy: Settling payment (no execution)...`);
|
|
3458
|
+
let settlement = null;
|
|
3459
|
+
try {
|
|
3460
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
3461
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
3462
|
+
} catch (err) {
|
|
3463
|
+
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
3464
|
+
return this.sendJson(res, 500, {
|
|
3465
|
+
success: false,
|
|
3466
|
+
error: `Settlement failed: ${err.message}`
|
|
3467
|
+
});
|
|
3468
|
+
}
|
|
3469
|
+
this.sendJson(res, 200, {
|
|
3470
|
+
success: true,
|
|
3471
|
+
verified: true,
|
|
3472
|
+
settled: settlement?.success || false,
|
|
3473
|
+
txHash: settlement?.transaction,
|
|
3474
|
+
from: payment.payload?.authorization?.from,
|
|
3475
|
+
// Buyer's wallet address
|
|
3476
|
+
paidTo: wallet,
|
|
3477
|
+
amount: amountNum,
|
|
3478
|
+
currency: currency || "USDC",
|
|
3479
|
+
facilitator: settlement?.facilitator,
|
|
3480
|
+
memo
|
|
3481
|
+
});
|
|
3482
|
+
}
|
|
3483
|
+
/**
|
|
3484
|
+
* Handle MPP payment flow for /proxy endpoint (tempo_moderato chain)
|
|
3485
|
+
*/
|
|
3486
|
+
async handleProxyMPP(body, config, authHeader, res) {
|
|
3487
|
+
const { wallet, amount, memo, serviceId } = body;
|
|
3488
|
+
const amountNum = parseFloat(amount);
|
|
3489
|
+
const amountInUnits = Math.floor(amountNum * 1e6).toString();
|
|
3490
|
+
if (!authHeader || !authHeader.toLowerCase().startsWith("payment ")) {
|
|
3491
|
+
const challengeId = this.generateChallengeId();
|
|
3492
|
+
const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
|
|
3493
|
+
const mppRequest = {
|
|
3494
|
+
amount: amountInUnits,
|
|
3495
|
+
currency: tokenAddress,
|
|
3496
|
+
methodDetails: {
|
|
3497
|
+
chainId: 42431,
|
|
3498
|
+
feePayer: true
|
|
3499
|
+
},
|
|
3500
|
+
recipient: wallet
|
|
3501
|
+
};
|
|
3502
|
+
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
3503
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
3504
|
+
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
3505
|
+
res.writeHead(402, {
|
|
3506
|
+
"Content-Type": "application/problem+json",
|
|
3507
|
+
[MPP_WWW_AUTH_HEADER]: wwwAuth
|
|
3508
|
+
});
|
|
3509
|
+
res.end(JSON.stringify({
|
|
3510
|
+
type: "https://paymentauth.org/problems/payment-required",
|
|
3511
|
+
title: "Payment Required",
|
|
3512
|
+
status: 402,
|
|
3513
|
+
detail: `Payment is required (${config.name}).`,
|
|
3514
|
+
service: serviceId || "proxy",
|
|
3515
|
+
price: amountNum,
|
|
3516
|
+
currency: "USDC"
|
|
3517
|
+
}, null, 2));
|
|
3518
|
+
return;
|
|
3519
|
+
}
|
|
3520
|
+
const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
|
|
3521
|
+
if (!credentialMatch) {
|
|
3522
|
+
return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
|
|
2160
3523
|
}
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
3524
|
+
let mppCredential;
|
|
3525
|
+
try {
|
|
3526
|
+
const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
3527
|
+
const decoded = Buffer.from(base64, "base64").toString("utf-8");
|
|
3528
|
+
mppCredential = JSON.parse(decoded);
|
|
3529
|
+
} catch (err) {
|
|
3530
|
+
console.error("[MoltsPay] /proxy MPP: Failed to parse credential:", err);
|
|
3531
|
+
return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
|
|
2165
3532
|
}
|
|
2166
|
-
|
|
2167
|
-
if (
|
|
2168
|
-
|
|
3533
|
+
let txHash;
|
|
3534
|
+
if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
|
|
3535
|
+
txHash = mppCredential.payload.hash;
|
|
3536
|
+
} else {
|
|
3537
|
+
return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
|
|
2169
3538
|
}
|
|
2170
|
-
console.log(`[MoltsPay] /proxy: Verifying
|
|
2171
|
-
const
|
|
2172
|
-
|
|
3539
|
+
console.log(`[MoltsPay] /proxy MPP: Verifying tx ${txHash} on Tempo...`);
|
|
3540
|
+
const requirements = this.buildPaymentRequirements(config, "eip155:42431", wallet, "USDC");
|
|
3541
|
+
const paymentPayload = {
|
|
3542
|
+
x402Version: X402_VERSION3,
|
|
3543
|
+
scheme: "exact",
|
|
3544
|
+
network: "eip155:42431",
|
|
3545
|
+
payload: { txHash, chainId: 42431 }
|
|
3546
|
+
};
|
|
3547
|
+
const verification = await this.registry.verify(paymentPayload, requirements);
|
|
3548
|
+
if (!verification.valid) {
|
|
2173
3549
|
return this.sendJson(res, 402, {
|
|
2174
|
-
|
|
2175
|
-
error: `Payment verification failed: ${verifyResult.error}`,
|
|
2176
|
-
facilitator: verifyResult.facilitator
|
|
3550
|
+
error: `Payment verification failed: ${verification.error}`
|
|
2177
3551
|
});
|
|
2178
3552
|
}
|
|
2179
|
-
console.log(`[MoltsPay] /proxy:
|
|
3553
|
+
console.log(`[MoltsPay] /proxy MPP: Payment verified by ${verification.facilitator}`);
|
|
2180
3554
|
const { execute, service, params } = body;
|
|
2181
3555
|
if (execute && service) {
|
|
2182
|
-
console.log(`[MoltsPay] /proxy: Executing skill
|
|
3556
|
+
console.log(`[MoltsPay] /proxy MPP: Executing skill: ${service}`);
|
|
2183
3557
|
const skill = this.skills.get(service);
|
|
2184
3558
|
if (!skill) {
|
|
2185
|
-
console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
|
|
2186
3559
|
return this.sendJson(res, 404, {
|
|
2187
3560
|
success: false,
|
|
2188
|
-
paymentSettled:
|
|
3561
|
+
paymentSettled: true,
|
|
3562
|
+
// Payment already happened on Tempo
|
|
2189
3563
|
error: `Service not found: ${service}`
|
|
2190
3564
|
});
|
|
2191
3565
|
}
|
|
@@ -2198,73 +3572,36 @@ var MoltsPayServer = class {
|
|
|
2198
3572
|
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
2199
3573
|
)
|
|
2200
3574
|
]);
|
|
2201
|
-
console.log(`[MoltsPay] /proxy: Skill succeeded, now settling payment...`);
|
|
2202
3575
|
} catch (err) {
|
|
2203
|
-
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}
|
|
3576
|
+
console.error(`[MoltsPay] /proxy MPP: Skill failed: ${err.message}`);
|
|
2204
3577
|
return this.sendJson(res, 500, {
|
|
2205
3578
|
success: false,
|
|
2206
|
-
paymentSettled:
|
|
3579
|
+
paymentSettled: true,
|
|
2207
3580
|
error: `Service execution failed: ${err.message}`
|
|
2208
3581
|
});
|
|
2209
3582
|
}
|
|
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
3583
|
return this.sendJson(res, 200, {
|
|
2231
3584
|
success: true,
|
|
2232
3585
|
verified: true,
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
from: payment.payload?.authorization?.from,
|
|
2236
|
-
// Buyer's wallet address
|
|
3586
|
+
txHash,
|
|
3587
|
+
chain: "tempo_moderato",
|
|
2237
3588
|
paidTo: wallet,
|
|
2238
3589
|
amount: amountNum,
|
|
2239
|
-
currency:
|
|
2240
|
-
facilitator:
|
|
3590
|
+
currency: "USDC",
|
|
3591
|
+
facilitator: verification.facilitator,
|
|
2241
3592
|
memo,
|
|
2242
3593
|
result
|
|
2243
3594
|
});
|
|
2244
3595
|
}
|
|
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
3596
|
this.sendJson(res, 200, {
|
|
2258
3597
|
success: true,
|
|
2259
3598
|
verified: true,
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
from: payment.payload?.authorization?.from,
|
|
2263
|
-
// Buyer's wallet address
|
|
3599
|
+
txHash,
|
|
3600
|
+
chain: "tempo_moderato",
|
|
2264
3601
|
paidTo: wallet,
|
|
2265
3602
|
amount: amountNum,
|
|
2266
|
-
currency:
|
|
2267
|
-
facilitator:
|
|
3603
|
+
currency: "USDC",
|
|
3604
|
+
facilitator: verification.facilitator,
|
|
2268
3605
|
memo
|
|
2269
3606
|
});
|
|
2270
3607
|
}
|
|
@@ -2279,7 +3616,7 @@ var MoltsPayServer = class {
|
|
|
2279
3616
|
const tokenAddresses = TOKEN_ADDRESSES[networkId] || TOKEN_ADDRESSES[this.networkId] || {};
|
|
2280
3617
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
2281
3618
|
const tokenDomain = getTokenDomain(networkId, selectedToken);
|
|
2282
|
-
|
|
3619
|
+
const requirements = {
|
|
2283
3620
|
scheme: "exact",
|
|
2284
3621
|
network: networkId,
|
|
2285
3622
|
asset: tokenAddress,
|
|
@@ -2289,6 +3626,17 @@ var MoltsPayServer = class {
|
|
|
2289
3626
|
maxTimeoutSeconds: 300,
|
|
2290
3627
|
extra: tokenDomain
|
|
2291
3628
|
};
|
|
3629
|
+
if (networkId === "eip155:56" || networkId === "eip155:97") {
|
|
3630
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
3631
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
3632
|
+
if (spenderAddress) {
|
|
3633
|
+
requirements.extra = {
|
|
3634
|
+
...requirements.extra || {},
|
|
3635
|
+
bnbSpender: spenderAddress
|
|
3636
|
+
};
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
return requirements;
|
|
2292
3640
|
}
|
|
2293
3641
|
/**
|
|
2294
3642
|
* Return 402 with x402 payment requirements for proxy endpoint
|
|
@@ -2338,14 +3686,14 @@ if (!globalThis.crypto) {
|
|
|
2338
3686
|
}
|
|
2339
3687
|
function getVersion() {
|
|
2340
3688
|
const locations = [
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
3689
|
+
join5(__dirname, "../../package.json"),
|
|
3690
|
+
join5(__dirname, "../package.json"),
|
|
3691
|
+
join5(process.cwd(), "node_modules/moltspay/package.json")
|
|
2344
3692
|
];
|
|
2345
3693
|
for (const loc of locations) {
|
|
2346
3694
|
try {
|
|
2347
|
-
if (
|
|
2348
|
-
const pkg = JSON.parse(
|
|
3695
|
+
if (existsSync5(loc)) {
|
|
3696
|
+
const pkg = JSON.parse(readFileSync5(loc, "utf-8"));
|
|
2349
3697
|
if (pkg.name === "moltspay") return pkg.version;
|
|
2350
3698
|
}
|
|
2351
3699
|
} catch {
|
|
@@ -2353,11 +3701,94 @@ function getVersion() {
|
|
|
2353
3701
|
}
|
|
2354
3702
|
return "0.0.0";
|
|
2355
3703
|
}
|
|
3704
|
+
var BNB_SPONSOR_KEY = process.env.MOLTSPAY_BNB_SPONSOR_KEY;
|
|
3705
|
+
var BNB_SPENDER_ADDRESS = process.env.MOLTSPAY_BNB_SPENDER || "0xEBB45208D806A0c73F9673E0c5713FF720DD6b79";
|
|
3706
|
+
var ERC20_APPROVE_ABI = [
|
|
3707
|
+
"function approve(address spender, uint256 amount) returns (bool)",
|
|
3708
|
+
"function allowance(address owner, address spender) view returns (uint256)"
|
|
3709
|
+
];
|
|
3710
|
+
async function setupBNBApprovals(client, chain, spenderAddress, sponsorGas = false) {
|
|
3711
|
+
const chainConfig = CHAINS[chain];
|
|
3712
|
+
const provider = new ethers2.JsonRpcProvider(chainConfig.rpc);
|
|
3713
|
+
const wallet = client.getWallet();
|
|
3714
|
+
if (!wallet) {
|
|
3715
|
+
console.log(" \u274C No wallet found");
|
|
3716
|
+
return;
|
|
3717
|
+
}
|
|
3718
|
+
const signer = wallet.connect(provider);
|
|
3719
|
+
console.log(` Spender: ${spenderAddress}`);
|
|
3720
|
+
let bnbBalance = await provider.getBalance(wallet.address);
|
|
3721
|
+
const minGasRequired = ethers2.parseEther("0.0005");
|
|
3722
|
+
if (bnbBalance < minGasRequired) {
|
|
3723
|
+
if (sponsorGas && BNB_SPONSOR_KEY) {
|
|
3724
|
+
console.log(" \u23F3 Sponsoring BNB gas for approvals...");
|
|
3725
|
+
try {
|
|
3726
|
+
const sponsorWallet = new ethers2.Wallet(BNB_SPONSOR_KEY, provider);
|
|
3727
|
+
const tx = await sponsorWallet.sendTransaction({
|
|
3728
|
+
to: wallet.address,
|
|
3729
|
+
value: ethers2.parseEther("0.001")
|
|
3730
|
+
});
|
|
3731
|
+
await tx.wait();
|
|
3732
|
+
console.log(` \u2705 Sponsored 0.001 BNB (tx: ${tx.hash.slice(0, 10)}...)`);
|
|
3733
|
+
bnbBalance = await provider.getBalance(wallet.address);
|
|
3734
|
+
} catch (err) {
|
|
3735
|
+
console.log(` \u26A0\uFE0F Gas sponsorship failed: ${err.message}`);
|
|
3736
|
+
console.log(` \u{1F4A1} Get testnet BNB: https://testnet.bnbchain.org/faucet-smart`);
|
|
3737
|
+
return;
|
|
3738
|
+
}
|
|
3739
|
+
} else {
|
|
3740
|
+
console.log(` \u26A0\uFE0F Need BNB for gas (~0.0005 BNB)`);
|
|
3741
|
+
console.log(` \u{1F4A1} Run: npx moltspay faucet --chain bnb_testnet`);
|
|
3742
|
+
console.log(` Then run: npx moltspay approve --chain ${chain} --spender ${spenderAddress}`);
|
|
3743
|
+
return;
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
for (const tokenSymbol of ["USDT", "USDC"]) {
|
|
3747
|
+
const tokenConfig = chainConfig.tokens[tokenSymbol];
|
|
3748
|
+
const tokenContract = new ethers2.Contract(tokenConfig.address, ERC20_APPROVE_ABI, signer);
|
|
3749
|
+
const allowance = await tokenContract.allowance(wallet.address, spenderAddress);
|
|
3750
|
+
if (allowance > 0n) {
|
|
3751
|
+
console.log(` \u2705 ${tokenSymbol}: already approved for ${spenderAddress.slice(0, 10)}...`);
|
|
3752
|
+
continue;
|
|
3753
|
+
}
|
|
3754
|
+
console.log(` \u23F3 Approving ${tokenSymbol}...`);
|
|
3755
|
+
try {
|
|
3756
|
+
const tx = await tokenContract.approve(spenderAddress, ethers2.MaxUint256);
|
|
3757
|
+
await tx.wait();
|
|
3758
|
+
console.log(` \u2705 ${tokenSymbol}: approved (tx: ${tx.hash.slice(0, 10)}...)`);
|
|
3759
|
+
} catch (err) {
|
|
3760
|
+
console.log(` \u274C ${tokenSymbol}: approval failed - ${err.message}`);
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
console.log("");
|
|
3764
|
+
}
|
|
3765
|
+
async function checkBNBApprovals(address, chain, configDir = DEFAULT_CONFIG_DIR2) {
|
|
3766
|
+
const chainConfig = CHAINS[chain];
|
|
3767
|
+
const provider = new ethers2.JsonRpcProvider(chainConfig.rpc);
|
|
3768
|
+
let spenderAddress = null;
|
|
3769
|
+
try {
|
|
3770
|
+
const walletPath = join5(configDir, "wallet.json");
|
|
3771
|
+
const walletData = JSON.parse(readFileSync5(walletPath, "utf-8"));
|
|
3772
|
+
spenderAddress = walletData.approvals?.[chain] || null;
|
|
3773
|
+
} catch {
|
|
3774
|
+
}
|
|
3775
|
+
const result = { usdt: false, usdc: false, spender: spenderAddress };
|
|
3776
|
+
if (!spenderAddress) {
|
|
3777
|
+
return result;
|
|
3778
|
+
}
|
|
3779
|
+
for (const tokenSymbol of ["USDT", "USDC"]) {
|
|
3780
|
+
const tokenConfig = chainConfig.tokens[tokenSymbol];
|
|
3781
|
+
const tokenContract = new ethers2.Contract(tokenConfig.address, ERC20_APPROVE_ABI, provider);
|
|
3782
|
+
const allowance = await tokenContract.allowance(address, spenderAddress);
|
|
3783
|
+
result[tokenSymbol.toLowerCase()] = allowance > 0n;
|
|
3784
|
+
}
|
|
3785
|
+
return result;
|
|
3786
|
+
}
|
|
2356
3787
|
var program = new Command();
|
|
2357
|
-
var
|
|
2358
|
-
var PID_FILE =
|
|
2359
|
-
if (!
|
|
2360
|
-
|
|
3788
|
+
var DEFAULT_CONFIG_DIR2 = join5(homedir3(), ".moltspay");
|
|
3789
|
+
var PID_FILE = join5(DEFAULT_CONFIG_DIR2, "server.pid");
|
|
3790
|
+
if (!existsSync5(DEFAULT_CONFIG_DIR2)) {
|
|
3791
|
+
mkdirSync3(DEFAULT_CONFIG_DIR2, { recursive: true });
|
|
2361
3792
|
}
|
|
2362
3793
|
function prompt(question) {
|
|
2363
3794
|
const rl = readline.createInterface({
|
|
@@ -2372,19 +3803,49 @@ function prompt(question) {
|
|
|
2372
3803
|
});
|
|
2373
3804
|
}
|
|
2374
3805
|
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
|
-
}
|
|
3806
|
+
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
3807
|
let chain = options.chain;
|
|
2383
|
-
const
|
|
3808
|
+
const supportedEVMChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
|
|
3809
|
+
const supportedSolanaChains = ["solana", "solana_devnet"];
|
|
3810
|
+
const supportedChains = [...supportedEVMChains, ...supportedSolanaChains];
|
|
2384
3811
|
if (!supportedChains.includes(chain)) {
|
|
2385
3812
|
console.error(`\u274C Unknown chain: ${chain}. Supported: ${supportedChains.join(", ")}`);
|
|
2386
3813
|
process.exit(1);
|
|
2387
3814
|
}
|
|
3815
|
+
if (supportedSolanaChains.includes(chain)) {
|
|
3816
|
+
console.log("\n\u{1F7E3} Solana Wallet Setup\n");
|
|
3817
|
+
if (solanaWalletExists(options.configDir)) {
|
|
3818
|
+
const existingAddress = getSolanaAddress(options.configDir);
|
|
3819
|
+
console.log(`\u26A0\uFE0F Solana wallet already exists: ${existingAddress}`);
|
|
3820
|
+
console.log(` Config dir: ${options.configDir}`);
|
|
3821
|
+
return;
|
|
3822
|
+
}
|
|
3823
|
+
console.log("Creating Solana wallet...");
|
|
3824
|
+
const keypair = createSolanaWallet(options.configDir);
|
|
3825
|
+
const address = keypair.publicKey.toBase58();
|
|
3826
|
+
console.log(`
|
|
3827
|
+
\u2705 Solana wallet created: ${address}`);
|
|
3828
|
+
console.log(`
|
|
3829
|
+
\u{1F4C1} Config saved to: ${join5(options.configDir, "wallet-solana.json")}`);
|
|
3830
|
+
console.log(`
|
|
3831
|
+
\u26A0\uFE0F IMPORTANT: Back up your wallet file!`);
|
|
3832
|
+
console.log(` This file contains your private key!
|
|
3833
|
+
`);
|
|
3834
|
+
if (chain === "solana_devnet") {
|
|
3835
|
+
console.log("\u{1F4A1} Get testnet tokens:");
|
|
3836
|
+
console.log(" npx moltspay faucet --chain solana_devnet\n");
|
|
3837
|
+
} else {
|
|
3838
|
+
console.log(`\u{1F4B0} Fund your wallet with USDC on Solana to start (gasless - no SOL needed).
|
|
3839
|
+
`);
|
|
3840
|
+
}
|
|
3841
|
+
return;
|
|
3842
|
+
}
|
|
3843
|
+
console.log("\n\u{1F510} MoltsPay Client Setup\n");
|
|
3844
|
+
if (existsSync5(join5(options.configDir, "wallet.json"))) {
|
|
3845
|
+
console.log('\u26A0\uFE0F EVM wallet already initialized. Use "moltspay config" to update settings.');
|
|
3846
|
+
console.log(` Config dir: ${options.configDir}`);
|
|
3847
|
+
return;
|
|
3848
|
+
}
|
|
2388
3849
|
let maxPerTx = options.maxPerTx ? parseFloat(options.maxPerTx) : null;
|
|
2389
3850
|
let maxPerDay = options.maxPerDay ? parseFloat(options.maxPerDay) : null;
|
|
2390
3851
|
if (!maxPerTx) {
|
|
@@ -2406,13 +3867,21 @@ program.command("init").description("Initialize MoltsPay client (create wallet,
|
|
|
2406
3867
|
console.log(`
|
|
2407
3868
|
\u{1F4C1} Config saved to: ${result.configDir}`);
|
|
2408
3869
|
console.log(`
|
|
2409
|
-
\u26A0\uFE0F IMPORTANT: Back up ${
|
|
3870
|
+
\u26A0\uFE0F IMPORTANT: Back up ${join5(result.configDir, "wallet.json")}`);
|
|
2410
3871
|
console.log(` This file contains your private key!
|
|
2411
3872
|
`);
|
|
3873
|
+
if (chain === "bnb" || chain === "bnb_testnet") {
|
|
3874
|
+
console.log("\u{1F4CB} Setting up BNB chain approvals...\n");
|
|
3875
|
+
console.log(" \u2139\uFE0F Using default spender. For other services, run:");
|
|
3876
|
+
console.log(` npx moltspay approve --chain ${chain} --spender <address>
|
|
3877
|
+
`);
|
|
3878
|
+
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
3879
|
+
await setupBNBApprovals(client, chain, BNB_SPENDER_ADDRESS, true);
|
|
3880
|
+
}
|
|
2412
3881
|
console.log(`\u{1F4B0} Fund your wallet with USDC on ${chain} to start using services.
|
|
2413
3882
|
`);
|
|
2414
3883
|
});
|
|
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",
|
|
3884
|
+
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
3885
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
2417
3886
|
if (!client.isInitialized) {
|
|
2418
3887
|
console.log("\u274C Not initialized. Run: npx moltspay init");
|
|
@@ -2447,25 +3916,40 @@ program.command("config").description("Update MoltsPay settings").option("--max-
|
|
|
2447
3916
|
}
|
|
2448
3917
|
}
|
|
2449
3918
|
});
|
|
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
|
|
3919
|
+
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
3920
|
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
3921
|
const amount = parseFloat(amountStr);
|
|
2457
3922
|
if (isNaN(amount) || amount < 5) {
|
|
2458
3923
|
console.log("\u274C Minimum $5.");
|
|
2459
3924
|
return;
|
|
2460
3925
|
}
|
|
2461
3926
|
const chain = options.chain?.toLowerCase() || "base";
|
|
2462
|
-
if (!["base", "polygon", "base_sepolia"].includes(chain)) {
|
|
2463
|
-
console.log("\u274C Invalid chain. Use: base, polygon, or
|
|
3927
|
+
if (!["base", "polygon", "base_sepolia", "solana", "bnb", "bnb_testnet"].includes(chain)) {
|
|
3928
|
+
console.log("\u274C Invalid chain. Use: base, polygon, solana, base_sepolia, bnb, or bnb_testnet");
|
|
2464
3929
|
return;
|
|
2465
3930
|
}
|
|
3931
|
+
let walletAddress;
|
|
3932
|
+
if (chain === "solana") {
|
|
3933
|
+
const solanaWallet = loadSolanaWallet(options.configDir || DEFAULT_CONFIG_DIR2);
|
|
3934
|
+
if (!solanaWallet) {
|
|
3935
|
+
console.log("\u274C No Solana wallet found. Run: npx moltspay init --chain solana");
|
|
3936
|
+
return;
|
|
3937
|
+
}
|
|
3938
|
+
walletAddress = getSolanaAddress(options.configDir || DEFAULT_CONFIG_DIR2) || "";
|
|
3939
|
+
if (!walletAddress) {
|
|
3940
|
+
console.log("\u274C Could not get Solana wallet address.");
|
|
3941
|
+
return;
|
|
3942
|
+
}
|
|
3943
|
+
} else {
|
|
3944
|
+
if (!client.isInitialized) {
|
|
3945
|
+
console.log("\u274C Not initialized. Run: npx moltspay init");
|
|
3946
|
+
return;
|
|
3947
|
+
}
|
|
3948
|
+
walletAddress = client.address;
|
|
3949
|
+
}
|
|
2466
3950
|
if (chain === "base_sepolia") {
|
|
2467
3951
|
console.log("\n\u{1F9EA} Testnet Funding\n");
|
|
2468
|
-
console.log(` Wallet: ${
|
|
3952
|
+
console.log(` Wallet: ${walletAddress}`);
|
|
2469
3953
|
console.log(` Chain: Base Sepolia (testnet)
|
|
2470
3954
|
`);
|
|
2471
3955
|
console.log("\u{1F4A1} Use the MoltsPay faucet to get free testnet USDC:\n");
|
|
@@ -2473,9 +3957,36 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
|
|
|
2473
3957
|
console.log(" Or get from Circle Faucet: https://faucet.circle.com/\n");
|
|
2474
3958
|
return;
|
|
2475
3959
|
}
|
|
3960
|
+
if (chain === "bnb_testnet") {
|
|
3961
|
+
console.log("\n\u{1F9EA} BNB Testnet Funding\n");
|
|
3962
|
+
console.log(` Wallet: ${walletAddress}`);
|
|
3963
|
+
console.log(` Chain: BNB Testnet
|
|
3964
|
+
`);
|
|
3965
|
+
console.log("\u{1F4A1} Use the MoltsPay faucet to get testnet USDC + tBNB:\n");
|
|
3966
|
+
console.log(" npx moltspay faucet --chain bnb_testnet\n");
|
|
3967
|
+
console.log(" This gives you:\n");
|
|
3968
|
+
console.log(" \u2022 1 USDC (testnet) for payments");
|
|
3969
|
+
console.log(" \u2022 0.001 tBNB for gas (first approval tx)\n");
|
|
3970
|
+
return;
|
|
3971
|
+
}
|
|
3972
|
+
if (chain === "bnb") {
|
|
3973
|
+
console.log("\n\u{1F4CB} BNB Chain Funding\n");
|
|
3974
|
+
console.log(` Wallet: ${walletAddress}
|
|
3975
|
+
`);
|
|
3976
|
+
console.log(" To use MoltsPay on BNB Chain, you need:\n");
|
|
3977
|
+
console.log(" 1. USDC for payments");
|
|
3978
|
+
console.log(" \u2192 Withdraw from Binance/exchange to your wallet address\n");
|
|
3979
|
+
console.log(" 2. Small amount of BNB for gas (~0.001 BNB / ~$0.60)");
|
|
3980
|
+
console.log(" \u2192 First approval transaction requires gas");
|
|
3981
|
+
console.log(" \u2192 After approval, all payments are gasless\n");
|
|
3982
|
+
console.log(" \u{1F4A1} Tip: Most exchanges include BNB dust when you withdraw to BNB Chain\n");
|
|
3983
|
+
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");
|
|
3984
|
+
console.log(" After funding, check status: npx moltspay status\n");
|
|
3985
|
+
return;
|
|
3986
|
+
}
|
|
2476
3987
|
console.log("\n\u{1F4B3} Fund your agent wallet\n");
|
|
2477
|
-
console.log(` Wallet: ${
|
|
2478
|
-
console.log(` Chain: ${chain}`);
|
|
3988
|
+
console.log(` Wallet: ${walletAddress}`);
|
|
3989
|
+
console.log(` Chain: ${chain === "solana" ? "Solana" : chain}`);
|
|
2479
3990
|
console.log(` Amount: $${amount.toFixed(2)}
|
|
2480
3991
|
`);
|
|
2481
3992
|
try {
|
|
@@ -2484,7 +3995,7 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
|
|
|
2484
3995
|
method: "POST",
|
|
2485
3996
|
headers: { "Content-Type": "application/json" },
|
|
2486
3997
|
body: JSON.stringify({
|
|
2487
|
-
address:
|
|
3998
|
+
address: walletAddress,
|
|
2488
3999
|
amount,
|
|
2489
4000
|
chain
|
|
2490
4001
|
})
|
|
@@ -2502,11 +4013,92 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
|
|
|
2502
4013
|
console.log(`\u274C ${error.message}`);
|
|
2503
4014
|
}
|
|
2504
4015
|
});
|
|
2505
|
-
program.command("
|
|
4016
|
+
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) => {
|
|
4017
|
+
const chain = options.chain;
|
|
4018
|
+
if (chain !== "bnb" && chain !== "bnb_testnet") {
|
|
4019
|
+
console.log("\u274C approve command is only for BNB chains (bnb or bnb_testnet)");
|
|
4020
|
+
return;
|
|
4021
|
+
}
|
|
4022
|
+
if (!options.spender.match(/^0x[a-fA-F0-9]{40}$/)) {
|
|
4023
|
+
console.log("\u274C Invalid spender address format");
|
|
4024
|
+
return;
|
|
4025
|
+
}
|
|
4026
|
+
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
4027
|
+
if (!client.isInitialized) {
|
|
4028
|
+
console.log("\u274C Wallet not initialized. Run: npx moltspay init --chain " + chain);
|
|
4029
|
+
return;
|
|
4030
|
+
}
|
|
4031
|
+
console.log(`
|
|
4032
|
+
\u{1F510} Approving spender for ${chain}...
|
|
4033
|
+
`);
|
|
4034
|
+
await setupBNBApprovals(client, chain, options.spender, false);
|
|
4035
|
+
const walletPath = join5(options.configDir || DEFAULT_CONFIG_DIR2, "wallet.json");
|
|
4036
|
+
try {
|
|
4037
|
+
const walletData = JSON.parse(readFileSync5(walletPath, "utf-8"));
|
|
4038
|
+
walletData.approvals = walletData.approvals || {};
|
|
4039
|
+
walletData.approvals[chain] = options.spender;
|
|
4040
|
+
writeFileSync3(walletPath, JSON.stringify(walletData, null, 2));
|
|
4041
|
+
console.log(`\u2705 Approval complete! Spender saved for ${chain}.
|
|
4042
|
+
`);
|
|
4043
|
+
} catch (err) {
|
|
4044
|
+
console.log("\u2705 Approval complete!\n");
|
|
4045
|
+
console.log("\u26A0\uFE0F Could not save spender to wallet config");
|
|
4046
|
+
}
|
|
4047
|
+
});
|
|
4048
|
+
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
4049
|
let address = options.address;
|
|
2507
4050
|
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
|
|
4051
|
+
if (!["base_sepolia", "tempo_moderato", "bnb_testnet", "solana_devnet"].includes(chain)) {
|
|
4052
|
+
console.log("\u274C Invalid chain. Use: base_sepolia, tempo_moderato, bnb_testnet, or solana_devnet");
|
|
4053
|
+
return;
|
|
4054
|
+
}
|
|
4055
|
+
if (chain === "solana_devnet") {
|
|
4056
|
+
if (!address) {
|
|
4057
|
+
address = getSolanaAddress(options.configDir);
|
|
4058
|
+
if (!address) {
|
|
4059
|
+
console.log("\u274C No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
|
|
4060
|
+
return;
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
if (!isValidSolanaAddress(address)) {
|
|
4064
|
+
console.log("\u274C Invalid Solana address");
|
|
4065
|
+
return;
|
|
4066
|
+
}
|
|
4067
|
+
console.log("\n\u{1F6B0} Solana Devnet Faucet (Gasless Mode)\n");
|
|
4068
|
+
console.log(` Address: ${address}
|
|
4069
|
+
`);
|
|
4070
|
+
let usdcSuccess = false;
|
|
4071
|
+
try {
|
|
4072
|
+
console.log(" \u23F3 Requesting 1 USDC from faucet...");
|
|
4073
|
+
const FAUCET_API = process.env.MOLTSPAY_FAUCET_API || "https://moltspay.com/api/v1/faucet";
|
|
4074
|
+
const response = await fetch(FAUCET_API, {
|
|
4075
|
+
method: "POST",
|
|
4076
|
+
headers: { "Content-Type": "application/json" },
|
|
4077
|
+
body: JSON.stringify({ address, chain: "solana_devnet" })
|
|
4078
|
+
});
|
|
4079
|
+
const result = await response.json();
|
|
4080
|
+
if (!response.ok) {
|
|
4081
|
+
console.log(` \u26A0\uFE0F USDC faucet: ${result.error || "Request failed"}`);
|
|
4082
|
+
if (result.hint) console.log(` ${result.hint}`);
|
|
4083
|
+
if (result.retry_after) console.log(` Retry after: ${result.retry_after}`);
|
|
4084
|
+
} else {
|
|
4085
|
+
console.log(` \u2705 Received ${result.amount} USDC!`);
|
|
4086
|
+
console.log(` Transaction: ${result.explorer}`);
|
|
4087
|
+
if (result.faucet_balance) {
|
|
4088
|
+
console.log(` Faucet balance: ${result.faucet_balance} USDC remaining`);
|
|
4089
|
+
}
|
|
4090
|
+
usdcSuccess = true;
|
|
4091
|
+
}
|
|
4092
|
+
} catch (error) {
|
|
4093
|
+
console.log(` \u26A0\uFE0F USDC faucet error: ${error.message}`);
|
|
4094
|
+
}
|
|
4095
|
+
console.log("");
|
|
4096
|
+
if (usdcSuccess) {
|
|
4097
|
+
console.log("\u{1F4A1} Check your balance:");
|
|
4098
|
+
console.log(" npx moltspay status\n");
|
|
4099
|
+
} else {
|
|
4100
|
+
console.log("\u274C Faucet request failed. Try again in a few minutes.\n");
|
|
4101
|
+
}
|
|
2510
4102
|
return;
|
|
2511
4103
|
}
|
|
2512
4104
|
if (!address) {
|
|
@@ -2554,6 +4146,46 @@ program.command("faucet").description("Request testnet tokens from faucet (Base
|
|
|
2554
4146
|
console.log(`\u274C ${error.message}`);
|
|
2555
4147
|
console.log("\n Try Tempo Wallet instead: https://wallet.tempo.xyz\n");
|
|
2556
4148
|
}
|
|
4149
|
+
} else if (chain === "bnb_testnet") {
|
|
4150
|
+
console.log(` Requesting 1 USDC on BNB Testnet...`);
|
|
4151
|
+
console.log(` Address: ${address}
|
|
4152
|
+
`);
|
|
4153
|
+
try {
|
|
4154
|
+
const FAUCET_API = process.env.MOLTSPAY_FAUCET_API || "https://moltspay.com/api/v1/faucet";
|
|
4155
|
+
const response = await fetch(FAUCET_API, {
|
|
4156
|
+
method: "POST",
|
|
4157
|
+
headers: { "Content-Type": "application/json" },
|
|
4158
|
+
body: JSON.stringify({ address, chain: "bnb_testnet" })
|
|
4159
|
+
});
|
|
4160
|
+
const result = await response.json();
|
|
4161
|
+
if (!response.ok) {
|
|
4162
|
+
console.log(`\u274C ${result.error || "Request failed"}`);
|
|
4163
|
+
if (result.hint) console.log(` ${result.hint}`);
|
|
4164
|
+
if (result.retry_after) console.log(` Retry after: ${result.retry_after}`);
|
|
4165
|
+
console.log("\n\u{1F4A1} Alternatively, get tokens manually:");
|
|
4166
|
+
console.log(` 1. Get test BNB: https://www.bnbchain.org/en/testnet-faucet`);
|
|
4167
|
+
console.log(` 2. Select "Peggy Tokens" -> USDC`);
|
|
4168
|
+
console.log(` 3. Enter: ${address}
|
|
4169
|
+
`);
|
|
4170
|
+
return;
|
|
4171
|
+
}
|
|
4172
|
+
console.log(`\u2705 Received ${result.amount} ${result.token || "USDC"} on ${result.chain_name || "BNB Testnet"}!
|
|
4173
|
+
`);
|
|
4174
|
+
console.log(` Transaction: ${result.explorer || `https://testnet.bscscan.com/tx/${result.transaction}`}`);
|
|
4175
|
+
if (result.faucet_balance) {
|
|
4176
|
+
console.log(` Faucet balance: ${result.faucet_balance} USDC`);
|
|
4177
|
+
}
|
|
4178
|
+
console.log("\n\u{1F4A1} Now you can test BNB payments:");
|
|
4179
|
+
console.log(` npx moltspay pay <service-url> <service-id> --chain bnb_testnet
|
|
4180
|
+
`);
|
|
4181
|
+
} catch (error) {
|
|
4182
|
+
console.log(`\u274C ${error.message}`);
|
|
4183
|
+
console.log("\n\u{1F4A1} Get tokens manually:");
|
|
4184
|
+
console.log(` 1. Get test BNB: https://www.bnbchain.org/en/testnet-faucet`);
|
|
4185
|
+
console.log(` 2. Select "Peggy Tokens" -> USDC`);
|
|
4186
|
+
console.log(` 3. Enter: ${address}
|
|
4187
|
+
`);
|
|
4188
|
+
}
|
|
2557
4189
|
} else {
|
|
2558
4190
|
console.log(` Requesting 1 USDC on Base Sepolia...`);
|
|
2559
4191
|
console.log(` Address: ${address}
|
|
@@ -2563,7 +4195,7 @@ program.command("faucet").description("Request testnet tokens from faucet (Base
|
|
|
2563
4195
|
const response = await fetch(FAUCET_API, {
|
|
2564
4196
|
method: "POST",
|
|
2565
4197
|
headers: { "Content-Type": "application/json" },
|
|
2566
|
-
body: JSON.stringify({ address })
|
|
4198
|
+
body: JSON.stringify({ address, chain: "base_sepolia" })
|
|
2567
4199
|
});
|
|
2568
4200
|
const result = await response.json();
|
|
2569
4201
|
if (!response.ok) {
|
|
@@ -2586,7 +4218,7 @@ program.command("faucet").description("Request testnet tokens from faucet (Base
|
|
|
2586
4218
|
}
|
|
2587
4219
|
}
|
|
2588
4220
|
});
|
|
2589
|
-
program.command("status").description("Show wallet status and balance").option("--config-dir <dir>", "Config directory",
|
|
4221
|
+
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
4222
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
2591
4223
|
if (!client.isInitialized) {
|
|
2592
4224
|
if (options.json) {
|
|
@@ -2603,12 +4235,31 @@ program.command("status").description("Show wallet status and balance").option("
|
|
|
2603
4235
|
} catch (err) {
|
|
2604
4236
|
console.error("Warning: Could not fetch balances:", err.message);
|
|
2605
4237
|
}
|
|
4238
|
+
const solanaAddress = getSolanaAddress(options.configDir);
|
|
4239
|
+
let solanaBalances = {};
|
|
4240
|
+
if (solanaAddress) {
|
|
4241
|
+
try {
|
|
4242
|
+
solanaBalances.devnet = await getSolanaBalances(solanaAddress, "solana_devnet");
|
|
4243
|
+
} catch {
|
|
4244
|
+
}
|
|
4245
|
+
try {
|
|
4246
|
+
solanaBalances.mainnet = await getSolanaBalances(solanaAddress, "solana");
|
|
4247
|
+
} catch {
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
2606
4250
|
if (options.json) {
|
|
2607
|
-
|
|
4251
|
+
const output = {
|
|
2608
4252
|
address: client.address,
|
|
2609
4253
|
balances: allBalances,
|
|
2610
4254
|
limits: config.limits
|
|
2611
|
-
}
|
|
4255
|
+
};
|
|
4256
|
+
if (solanaAddress) {
|
|
4257
|
+
output.solana = {
|
|
4258
|
+
address: solanaAddress,
|
|
4259
|
+
balances: solanaBalances
|
|
4260
|
+
};
|
|
4261
|
+
}
|
|
4262
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2612
4263
|
} else {
|
|
2613
4264
|
console.log("\n\u{1F4CA} MoltsPay Wallet Status\n");
|
|
2614
4265
|
console.log(` Address: ${client.address}`);
|
|
@@ -2632,18 +4283,90 @@ program.command("status").description("Show wallet status and balance").option("
|
|
|
2632
4283
|
console.log(` alphaUSD: ${tempo.alphaUSD.toFixed(2)}`);
|
|
2633
4284
|
console.log(` betaUSD: ${tempo.betaUSD.toFixed(2)}`);
|
|
2634
4285
|
console.log(` thetaUSD: ${tempo.thetaUSD.toFixed(2)}`);
|
|
4286
|
+
} else if (chainName === "bnb" || chainName === "bnb_testnet") {
|
|
4287
|
+
const bnbBalance = balance.native;
|
|
4288
|
+
const bnbWarning = bnbBalance < 5e-4 ? " \u26A0\uFE0F Low gas" : "";
|
|
4289
|
+
console.log(` ${chainLabel.padEnd(14)} ${balance.usdc.toFixed(2)} USDC | ${balance.usdt.toFixed(2)} USDT | ${bnbBalance.toFixed(4)} BNB${bnbWarning}`);
|
|
2635
4290
|
} else {
|
|
2636
4291
|
console.log(` ${chainLabel.padEnd(14)} ${balance.usdc.toFixed(2)} USDC | ${balance.usdt.toFixed(2)} USDT`);
|
|
2637
4292
|
}
|
|
2638
4293
|
}
|
|
4294
|
+
const address = client.address;
|
|
4295
|
+
let bnbApprovalStatus = null;
|
|
4296
|
+
let bnbTestnetApprovalStatus = null;
|
|
4297
|
+
try {
|
|
4298
|
+
if (allBalances["bnb"]) {
|
|
4299
|
+
bnbApprovalStatus = await checkBNBApprovals(address, "bnb", options.configDir);
|
|
4300
|
+
}
|
|
4301
|
+
if (allBalances["bnb_testnet"]) {
|
|
4302
|
+
bnbTestnetApprovalStatus = await checkBNBApprovals(address, "bnb_testnet", options.configDir);
|
|
4303
|
+
}
|
|
4304
|
+
} catch {
|
|
4305
|
+
}
|
|
4306
|
+
if (bnbApprovalStatus || bnbTestnetApprovalStatus) {
|
|
4307
|
+
console.log("");
|
|
4308
|
+
console.log(" BNB Approvals (pay-for-success):");
|
|
4309
|
+
if (bnbApprovalStatus) {
|
|
4310
|
+
if (!bnbApprovalStatus.spender) {
|
|
4311
|
+
console.log(" BNB: \u26A0\uFE0F No spender configured");
|
|
4312
|
+
console.log(" \u2514\u2500 Run a payment first, or: npx moltspay approve --chain bnb --spender <address>");
|
|
4313
|
+
} else {
|
|
4314
|
+
const status = bnbApprovalStatus.usdt && bnbApprovalStatus.usdc ? "\u2705" : "\u26A0\uFE0F";
|
|
4315
|
+
const tokens = [
|
|
4316
|
+
bnbApprovalStatus.usdt ? "USDT\u2713" : "USDT\u2717",
|
|
4317
|
+
bnbApprovalStatus.usdc ? "USDC\u2713" : "USDC\u2717"
|
|
4318
|
+
].join(", ");
|
|
4319
|
+
console.log(` BNB: ${status} ${tokens}`);
|
|
4320
|
+
const bnbNative = allBalances["bnb"]?.native || 0;
|
|
4321
|
+
if (!bnbApprovalStatus.usdc && !bnbApprovalStatus.usdt && bnbNative < 5e-4) {
|
|
4322
|
+
console.log(" \u26A0\uFE0F Need ~0.001 BNB for first approval tx. Get from exchange.");
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
if (bnbTestnetApprovalStatus) {
|
|
4327
|
+
if (!bnbTestnetApprovalStatus.spender) {
|
|
4328
|
+
console.log(" BNB Testnet: \u26A0\uFE0F No spender configured");
|
|
4329
|
+
console.log(" \u2514\u2500 Run a payment first, or: npx moltspay approve --chain bnb_testnet --spender <address>");
|
|
4330
|
+
} else {
|
|
4331
|
+
const status = bnbTestnetApprovalStatus.usdt && bnbTestnetApprovalStatus.usdc ? "\u2705" : "\u26A0\uFE0F";
|
|
4332
|
+
const tokens = [
|
|
4333
|
+
bnbTestnetApprovalStatus.usdt ? "USDT\u2713" : "USDT\u2717",
|
|
4334
|
+
bnbTestnetApprovalStatus.usdc ? "USDC\u2713" : "USDC\u2717"
|
|
4335
|
+
].join(", ");
|
|
4336
|
+
console.log(` BNB Testnet: ${status} ${tokens}`);
|
|
4337
|
+
const tbnbNative = allBalances["bnb_testnet"]?.native || 0;
|
|
4338
|
+
if (!bnbTestnetApprovalStatus.usdc && !bnbTestnetApprovalStatus.usdt && tbnbNative < 5e-4) {
|
|
4339
|
+
console.log(" \u26A0\uFE0F Need tBNB for approval. Run: npx moltspay faucet --chain bnb_testnet");
|
|
4340
|
+
}
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
2639
4344
|
console.log("");
|
|
2640
4345
|
console.log(" Spending Limits:");
|
|
2641
4346
|
console.log(` Per Transaction: $${config.limits.maxPerTx}`);
|
|
2642
4347
|
console.log(` Daily: $${config.limits.maxPerDay}`);
|
|
4348
|
+
const solanaAddress2 = getSolanaAddress(options.configDir);
|
|
4349
|
+
if (solanaAddress2) {
|
|
4350
|
+
console.log("");
|
|
4351
|
+
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");
|
|
4352
|
+
console.log(` \u{1F7E3} Solana: ${solanaAddress2}`);
|
|
4353
|
+
try {
|
|
4354
|
+
const devnetBalances = await getSolanaBalances(solanaAddress2, "solana_devnet");
|
|
4355
|
+
console.log(` Devnet: ${devnetBalances.sol.toFixed(4)} SOL | ${devnetBalances.usdc.toFixed(2)} USDC`);
|
|
4356
|
+
} catch (err) {
|
|
4357
|
+
console.log(` Devnet: (unable to fetch)`);
|
|
4358
|
+
}
|
|
4359
|
+
try {
|
|
4360
|
+
const mainnetBalances = await getSolanaBalances(solanaAddress2, "solana");
|
|
4361
|
+
console.log(` Mainnet: ${mainnetBalances.sol.toFixed(4)} SOL | ${mainnetBalances.usdc.toFixed(2)} USDC`);
|
|
4362
|
+
} catch (err) {
|
|
4363
|
+
console.log(` Mainnet: (unable to fetch)`);
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
2643
4366
|
console.log("");
|
|
2644
4367
|
}
|
|
2645
4368
|
});
|
|
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",
|
|
4369
|
+
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
4370
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
2648
4371
|
if (!client.isInitialized) {
|
|
2649
4372
|
console.log("\u274C Not initialized. Run: npx moltspay init");
|
|
@@ -2950,14 +4673,14 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
2950
4673
|
let manifestPath;
|
|
2951
4674
|
let skillDir;
|
|
2952
4675
|
let isSkillDir = false;
|
|
2953
|
-
if (
|
|
2954
|
-
manifestPath =
|
|
4676
|
+
if (existsSync5(join5(resolvedPath, "moltspay.services.json"))) {
|
|
4677
|
+
manifestPath = join5(resolvedPath, "moltspay.services.json");
|
|
2955
4678
|
skillDir = resolvedPath;
|
|
2956
4679
|
isSkillDir = true;
|
|
2957
|
-
} else if (
|
|
4680
|
+
} else if (existsSync5(resolvedPath) && resolvedPath.endsWith(".json")) {
|
|
2958
4681
|
manifestPath = resolvedPath;
|
|
2959
4682
|
skillDir = dirname(resolvedPath);
|
|
2960
|
-
} else if (
|
|
4683
|
+
} else if (existsSync5(resolvedPath)) {
|
|
2961
4684
|
console.error(`\u274C No moltspay.services.json found in: ${resolvedPath}`);
|
|
2962
4685
|
continue;
|
|
2963
4686
|
} else {
|
|
@@ -2966,25 +4689,25 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
2966
4689
|
}
|
|
2967
4690
|
console.log(`\u{1F4E6} Loading: ${manifestPath}`);
|
|
2968
4691
|
try {
|
|
2969
|
-
const manifestContent = JSON.parse(
|
|
4692
|
+
const manifestContent = JSON.parse(readFileSync5(manifestPath, "utf-8"));
|
|
2970
4693
|
if (!provider) {
|
|
2971
4694
|
provider = manifestContent.provider;
|
|
2972
4695
|
}
|
|
2973
4696
|
let skillModule = null;
|
|
2974
4697
|
if (isSkillDir) {
|
|
2975
4698
|
let entryPoint = "index.js";
|
|
2976
|
-
const pkgJsonPath =
|
|
2977
|
-
if (
|
|
4699
|
+
const pkgJsonPath = join5(skillDir, "package.json");
|
|
4700
|
+
if (existsSync5(pkgJsonPath)) {
|
|
2978
4701
|
try {
|
|
2979
|
-
const pkgJson = JSON.parse(
|
|
4702
|
+
const pkgJson = JSON.parse(readFileSync5(pkgJsonPath, "utf-8"));
|
|
2980
4703
|
if (pkgJson.main) {
|
|
2981
4704
|
entryPoint = pkgJson.main;
|
|
2982
4705
|
}
|
|
2983
4706
|
} catch {
|
|
2984
4707
|
}
|
|
2985
4708
|
}
|
|
2986
|
-
const modulePath =
|
|
2987
|
-
if (
|
|
4709
|
+
const modulePath = join5(skillDir, entryPoint);
|
|
4710
|
+
if (existsSync5(modulePath)) {
|
|
2988
4711
|
try {
|
|
2989
4712
|
skillModule = await import(modulePath);
|
|
2990
4713
|
console.log(` \u2705 Loaded module: ${modulePath}`);
|
|
@@ -3062,8 +4785,8 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
3062
4785
|
provider,
|
|
3063
4786
|
services: allServices
|
|
3064
4787
|
};
|
|
3065
|
-
const tempManifestPath =
|
|
3066
|
-
|
|
4788
|
+
const tempManifestPath = join5(DEFAULT_CONFIG_DIR2, "combined-manifest.json");
|
|
4789
|
+
writeFileSync3(tempManifestPath, JSON.stringify(combinedManifest, null, 2));
|
|
3067
4790
|
console.log(`
|
|
3068
4791
|
\u{1F4CB} Combined manifest: ${allServices.length} services`);
|
|
3069
4792
|
console.log(` Provider: ${provider.name}`);
|
|
@@ -3076,12 +4799,12 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
3076
4799
|
server.skill(serviceId, handler);
|
|
3077
4800
|
}
|
|
3078
4801
|
const pidData = { pid: process.pid, port, paths: allPaths };
|
|
3079
|
-
|
|
4802
|
+
writeFileSync3(PID_FILE, JSON.stringify(pidData, null, 2));
|
|
3080
4803
|
server.listen(port);
|
|
3081
4804
|
const cleanup = () => {
|
|
3082
4805
|
try {
|
|
3083
|
-
if (
|
|
3084
|
-
if (
|
|
4806
|
+
if (existsSync5(PID_FILE)) unlinkSync(PID_FILE);
|
|
4807
|
+
if (existsSync5(tempManifestPath)) unlinkSync(tempManifestPath);
|
|
3085
4808
|
} catch {
|
|
3086
4809
|
}
|
|
3087
4810
|
};
|
|
@@ -3102,12 +4825,12 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
3102
4825
|
}
|
|
3103
4826
|
});
|
|
3104
4827
|
program.command("stop").description("Stop the running MoltsPay server").action(async () => {
|
|
3105
|
-
if (!
|
|
4828
|
+
if (!existsSync5(PID_FILE)) {
|
|
3106
4829
|
console.log("\u274C No running server found (no PID file)");
|
|
3107
4830
|
process.exit(1);
|
|
3108
4831
|
}
|
|
3109
4832
|
try {
|
|
3110
|
-
const pidData = JSON.parse(
|
|
4833
|
+
const pidData = JSON.parse(readFileSync5(PID_FILE, "utf-8"));
|
|
3111
4834
|
const { pid, port, manifest } = pidData;
|
|
3112
4835
|
console.log(`
|
|
3113
4836
|
\u{1F6D1} Stopping MoltsPay Server
|
|
@@ -3132,7 +4855,7 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
|
|
|
3132
4855
|
process.kill(pid, "SIGKILL");
|
|
3133
4856
|
} catch {
|
|
3134
4857
|
}
|
|
3135
|
-
if (
|
|
4858
|
+
if (existsSync5(PID_FILE)) {
|
|
3136
4859
|
unlinkSync(PID_FILE);
|
|
3137
4860
|
}
|
|
3138
4861
|
console.log("\u2705 Server stopped\n");
|
|
@@ -3141,7 +4864,7 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
|
|
|
3141
4864
|
process.exit(1);
|
|
3142
4865
|
}
|
|
3143
4866
|
});
|
|
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
|
|
4867
|
+
program.command("pay <server> <service> [params]").description("Pay for a service and get the result").option("--prompt <text>", "Prompt for the service").option("--image <path>", "Image URL or local file path").option("--token <token>", "Token to pay with (USDC or USDT)", "USDC").option("--chain <chain>", "Chain to pay on (base, polygon, base_sepolia, tempo_moderato, solana, or solana_devnet).").option("--config-dir <dir>", "Config directory with wallet.json", DEFAULT_CONFIG_DIR2).option("--json", "Output raw JSON only").action(async (server, service, paramsJson, options) => {
|
|
3145
4868
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
3146
4869
|
if (!client.isInitialized) {
|
|
3147
4870
|
console.error("\u274C Wallet not initialized. Run: npx moltspay init");
|
|
@@ -3163,17 +4886,18 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
3163
4886
|
params.image_url = imagePath;
|
|
3164
4887
|
} else {
|
|
3165
4888
|
const filePath = resolve(imagePath);
|
|
3166
|
-
if (!
|
|
4889
|
+
if (!existsSync5(filePath)) {
|
|
3167
4890
|
console.error(`\u274C Image file not found: ${filePath}`);
|
|
3168
4891
|
process.exit(1);
|
|
3169
4892
|
}
|
|
3170
|
-
const imageData =
|
|
4893
|
+
const imageData = readFileSync5(filePath);
|
|
3171
4894
|
params.image_base64 = imageData.toString("base64");
|
|
3172
4895
|
}
|
|
3173
4896
|
}
|
|
4897
|
+
const supportedPayChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
|
|
3174
4898
|
const chain = options.chain?.toLowerCase();
|
|
3175
|
-
if (chain && !
|
|
3176
|
-
console.error(`\u274C Unknown chain: ${chain}. Supported:
|
|
4899
|
+
if (chain && !supportedPayChains.includes(chain)) {
|
|
4900
|
+
console.error(`\u274C Unknown chain: ${chain}. Supported: ${supportedPayChains.join(", ")}`);
|
|
3177
4901
|
process.exit(1);
|
|
3178
4902
|
}
|
|
3179
4903
|
const imageDisplay = params.image_url || (params.image_base64 ? `[local file: ${options.image}]` : null);
|
|
@@ -3204,22 +4928,10 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
3204
4928
|
console.log("");
|
|
3205
4929
|
}
|
|
3206
4930
|
try {
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
console.log("");
|
|
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
|
-
}
|
|
4931
|
+
const result = await client.pay(server, service, params, {
|
|
4932
|
+
token,
|
|
4933
|
+
chain
|
|
4934
|
+
});
|
|
3223
4935
|
if (options.json) {
|
|
3224
4936
|
console.log(JSON.stringify(result));
|
|
3225
4937
|
} else {
|
|
@@ -3239,9 +4951,9 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
3239
4951
|
program.command("validate <path>").description("Validate a moltspay.services.json file against the schema").action(async (inputPath) => {
|
|
3240
4952
|
const resolvedPath = resolve(inputPath);
|
|
3241
4953
|
let manifestPath;
|
|
3242
|
-
if (
|
|
3243
|
-
manifestPath =
|
|
3244
|
-
} else if (resolvedPath.endsWith(".json") &&
|
|
4954
|
+
if (existsSync5(join5(resolvedPath, "moltspay.services.json"))) {
|
|
4955
|
+
manifestPath = join5(resolvedPath, "moltspay.services.json");
|
|
4956
|
+
} else if (resolvedPath.endsWith(".json") && existsSync5(resolvedPath)) {
|
|
3245
4957
|
manifestPath = resolvedPath;
|
|
3246
4958
|
} else {
|
|
3247
4959
|
console.error(`\u274C Not found: ${resolvedPath}`);
|
|
@@ -3251,7 +4963,7 @@ program.command("validate <path>").description("Validate a moltspay.services.jso
|
|
|
3251
4963
|
\u{1F4CB} Validating: ${manifestPath}
|
|
3252
4964
|
`);
|
|
3253
4965
|
try {
|
|
3254
|
-
const content = JSON.parse(
|
|
4966
|
+
const content = JSON.parse(readFileSync5(manifestPath, "utf-8"));
|
|
3255
4967
|
const errors = [];
|
|
3256
4968
|
if (!content.provider) {
|
|
3257
4969
|
errors.push("Missing required field: provider");
|