moltspay 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +292 -34
- package/dist/cdp/index.d.mts +4 -4
- package/dist/cdp/index.d.ts +4 -4
- package/dist/cdp/index.js +110 -30368
- package/dist/cdp/index.js.map +1 -1
- package/dist/cdp/index.mjs +94 -30360
- package/dist/cdp/index.mjs.map +1 -1
- package/dist/cdp-DeohBe1o.d.ts +66 -0
- package/dist/cdp-p_eHuQpb.d.mts +66 -0
- package/dist/chains/index.d.mts +9 -8
- package/dist/chains/index.d.ts +9 -8
- package/dist/chains/index.js +86 -0
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/index.mjs +86 -0
- package/dist/chains/index.mjs.map +1 -1
- package/dist/cli/index.js +2746 -290
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +2752 -282
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +60 -4
- package/dist/client/index.d.ts +60 -4
- package/dist/client/index.js +734 -43
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +732 -41
- package/dist/client/index.mjs.map +1 -1
- package/dist/facilitators/index.d.mts +220 -39
- package/dist/facilitators/index.d.ts +220 -39
- package/dist/facilitators/index.js +897 -1
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +902 -1
- package/dist/facilitators/index.mjs.map +1 -1
- package/dist/{index-DgJPZMBG.d.mts → index-D_2FkLwV.d.mts} +6 -2
- package/dist/{index-DgJPZMBG.d.ts → index-D_2FkLwV.d.ts} +6 -2
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2238 -30837
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2167 -30766
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.d.mts +30 -3
- package/dist/server/index.d.ts +30 -3
- package/dist/server/index.js +1345 -54
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +1355 -54
- package/dist/server/index.mjs.map +1 -1
- package/dist/verify/index.d.mts +1 -1
- package/dist/verify/index.d.ts +1 -1
- package/dist/verify/index.js +86 -0
- package/dist/verify/index.js.map +1 -1
- package/dist/verify/index.mjs +86 -0
- package/dist/verify/index.mjs.map +1 -1
- package/dist/wallet/index.d.mts +3 -3
- package/dist/wallet/index.d.ts +3 -3
- package/dist/wallet/index.js +86 -0
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs +86 -0
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +8 -2
- package/schemas/moltspay.services.schema.json +27 -132
package/dist/cli/index.js
CHANGED
|
@@ -35,17 +35,19 @@ var init_cjs_shims = __esm({
|
|
|
35
35
|
|
|
36
36
|
// src/cli/index.ts
|
|
37
37
|
init_cjs_shims();
|
|
38
|
+
var import_crypto = require("crypto");
|
|
38
39
|
var import_commander = require("commander");
|
|
39
|
-
var
|
|
40
|
-
var
|
|
41
|
-
var
|
|
40
|
+
var import_os3 = require("os");
|
|
41
|
+
var import_path3 = require("path");
|
|
42
|
+
var import_fs5 = require("fs");
|
|
42
43
|
var import_child_process = require("child_process");
|
|
44
|
+
var import_ethers2 = require("ethers");
|
|
43
45
|
|
|
44
46
|
// src/client/index.ts
|
|
45
47
|
init_cjs_shims();
|
|
46
|
-
var
|
|
47
|
-
var
|
|
48
|
-
var
|
|
48
|
+
var import_fs2 = require("fs");
|
|
49
|
+
var import_os2 = require("os");
|
|
50
|
+
var import_path2 = require("path");
|
|
49
51
|
var import_ethers = require("ethers");
|
|
50
52
|
|
|
51
53
|
// src/chains/index.ts
|
|
@@ -127,6 +129,92 @@ var CHAINS = {
|
|
|
127
129
|
explorer: "https://sepolia.basescan.org/address/",
|
|
128
130
|
explorerTx: "https://sepolia.basescan.org/tx/",
|
|
129
131
|
avgBlockTime: 2
|
|
132
|
+
},
|
|
133
|
+
// ============ Tempo Testnet (Moderato) ============
|
|
134
|
+
tempo_moderato: {
|
|
135
|
+
name: "Tempo Moderato",
|
|
136
|
+
chainId: 42431,
|
|
137
|
+
rpc: "https://rpc.moderato.tempo.xyz",
|
|
138
|
+
tokens: {
|
|
139
|
+
// TIP-20 stablecoins on Tempo testnet (from mppx SDK)
|
|
140
|
+
// Note: Tempo uses USD as native gas token, not ETH
|
|
141
|
+
USDC: {
|
|
142
|
+
address: "0x20c0000000000000000000000000000000000000",
|
|
143
|
+
// pathUSD - primary testnet stablecoin
|
|
144
|
+
decimals: 6,
|
|
145
|
+
symbol: "USDC",
|
|
146
|
+
eip712Name: "pathUSD"
|
|
147
|
+
},
|
|
148
|
+
USDT: {
|
|
149
|
+
address: "0x20c0000000000000000000000000000000000001",
|
|
150
|
+
// alphaUSD
|
|
151
|
+
decimals: 6,
|
|
152
|
+
symbol: "USDT",
|
|
153
|
+
eip712Name: "alphaUSD"
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
usdc: "0x20c0000000000000000000000000000000000000",
|
|
157
|
+
explorer: "https://explore.testnet.tempo.xyz/address/",
|
|
158
|
+
explorerTx: "https://explore.testnet.tempo.xyz/tx/",
|
|
159
|
+
avgBlockTime: 0.5
|
|
160
|
+
// ~500ms finality
|
|
161
|
+
},
|
|
162
|
+
// ============ BNB Chain Testnet ============
|
|
163
|
+
bnb_testnet: {
|
|
164
|
+
name: "BNB Testnet",
|
|
165
|
+
chainId: 97,
|
|
166
|
+
rpc: "https://data-seed-prebsc-1-s1.binance.org:8545",
|
|
167
|
+
tokens: {
|
|
168
|
+
// Note: BNB uses 18 decimals for stablecoins (unlike Base/Polygon which use 6)
|
|
169
|
+
// Using official Binance-Peg testnet tokens
|
|
170
|
+
USDC: {
|
|
171
|
+
address: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
172
|
+
// Testnet USDC
|
|
173
|
+
decimals: 18,
|
|
174
|
+
symbol: "USDC",
|
|
175
|
+
eip712Name: "USD Coin"
|
|
176
|
+
},
|
|
177
|
+
USDT: {
|
|
178
|
+
address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
|
|
179
|
+
// Testnet USDT
|
|
180
|
+
decimals: 18,
|
|
181
|
+
symbol: "USDT",
|
|
182
|
+
eip712Name: "Tether USD"
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
usdc: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
186
|
+
explorer: "https://testnet.bscscan.com/address/",
|
|
187
|
+
explorerTx: "https://testnet.bscscan.com/tx/",
|
|
188
|
+
avgBlockTime: 3,
|
|
189
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
190
|
+
requiresApproval: true
|
|
191
|
+
},
|
|
192
|
+
// ============ BNB Chain Mainnet ============
|
|
193
|
+
bnb: {
|
|
194
|
+
name: "BNB Smart Chain",
|
|
195
|
+
chainId: 56,
|
|
196
|
+
rpc: "https://bsc-dataseed.binance.org",
|
|
197
|
+
tokens: {
|
|
198
|
+
// Note: BNB uses 18 decimals for stablecoins
|
|
199
|
+
USDC: {
|
|
200
|
+
address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
201
|
+
decimals: 18,
|
|
202
|
+
symbol: "USDC",
|
|
203
|
+
eip712Name: "USD Coin"
|
|
204
|
+
},
|
|
205
|
+
USDT: {
|
|
206
|
+
address: "0x55d398326f99059fF775485246999027B3197955",
|
|
207
|
+
decimals: 18,
|
|
208
|
+
symbol: "USDT",
|
|
209
|
+
eip712Name: "Tether USD"
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
usdc: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
213
|
+
explorer: "https://bscscan.com/address/",
|
|
214
|
+
explorerTx: "https://bscscan.com/tx/",
|
|
215
|
+
avgBlockTime: 3,
|
|
216
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
217
|
+
requiresApproval: true
|
|
130
218
|
}
|
|
131
219
|
};
|
|
132
220
|
function getChain(name) {
|
|
@@ -137,6 +225,362 @@ function getChain(name) {
|
|
|
137
225
|
return config;
|
|
138
226
|
}
|
|
139
227
|
|
|
228
|
+
// src/wallet/solana.ts
|
|
229
|
+
init_cjs_shims();
|
|
230
|
+
var import_web32 = require("@solana/web3.js");
|
|
231
|
+
var import_spl_token = require("@solana/spl-token");
|
|
232
|
+
var import_fs = require("fs");
|
|
233
|
+
var import_path = require("path");
|
|
234
|
+
var import_os = require("os");
|
|
235
|
+
var import_bs58 = __toESM(require("bs58"));
|
|
236
|
+
|
|
237
|
+
// src/chains/solana.ts
|
|
238
|
+
init_cjs_shims();
|
|
239
|
+
var import_web3 = require("@solana/web3.js");
|
|
240
|
+
var SOLANA_CHAINS = {
|
|
241
|
+
solana: {
|
|
242
|
+
name: "Solana Mainnet",
|
|
243
|
+
cluster: "mainnet-beta",
|
|
244
|
+
rpc: "https://api.mainnet-beta.solana.com",
|
|
245
|
+
explorer: "https://solscan.io/account/",
|
|
246
|
+
explorerTx: "https://solscan.io/tx/",
|
|
247
|
+
tokens: {
|
|
248
|
+
USDC: {
|
|
249
|
+
mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
250
|
+
// Circle official USDC
|
|
251
|
+
decimals: 6
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
solana_devnet: {
|
|
256
|
+
name: "Solana Devnet",
|
|
257
|
+
cluster: "devnet",
|
|
258
|
+
rpc: "https://api.devnet.solana.com",
|
|
259
|
+
explorer: "https://solscan.io/account/",
|
|
260
|
+
explorerTx: "https://solscan.io/tx/",
|
|
261
|
+
tokens: {
|
|
262
|
+
USDC: {
|
|
263
|
+
// Circle's devnet USDC (if not available, we'll deploy our own test token)
|
|
264
|
+
mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
265
|
+
decimals: 6
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
function getSolanaConnection(chain) {
|
|
271
|
+
const config = SOLANA_CHAINS[chain];
|
|
272
|
+
return new import_web3.Connection(config.rpc, "confirmed");
|
|
273
|
+
}
|
|
274
|
+
function getUSDCMint(chain) {
|
|
275
|
+
return new import_web3.PublicKey(SOLANA_CHAINS[chain].tokens.USDC.mint);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/wallet/solana.ts
|
|
279
|
+
var DEFAULT_CONFIG_DIR = (0, import_path.join)((0, import_os.homedir)(), ".moltspay");
|
|
280
|
+
var SOLANA_WALLET_FILE = "wallet-solana.json";
|
|
281
|
+
function getSolanaWalletPath(configDir = DEFAULT_CONFIG_DIR) {
|
|
282
|
+
return (0, import_path.join)(configDir, SOLANA_WALLET_FILE);
|
|
283
|
+
}
|
|
284
|
+
function solanaWalletExists(configDir = DEFAULT_CONFIG_DIR) {
|
|
285
|
+
return (0, import_fs.existsSync)(getSolanaWalletPath(configDir));
|
|
286
|
+
}
|
|
287
|
+
function loadSolanaWallet(configDir = DEFAULT_CONFIG_DIR) {
|
|
288
|
+
const walletPath = getSolanaWalletPath(configDir);
|
|
289
|
+
if (!(0, import_fs.existsSync)(walletPath)) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
const data = JSON.parse((0, import_fs.readFileSync)(walletPath, "utf-8"));
|
|
294
|
+
const secretKey = import_bs58.default.decode(data.secretKey);
|
|
295
|
+
return import_web32.Keypair.fromSecretKey(secretKey);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.error("Failed to load Solana wallet:", error);
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function createSolanaWallet(configDir = DEFAULT_CONFIG_DIR) {
|
|
302
|
+
if (!(0, import_fs.existsSync)(configDir)) {
|
|
303
|
+
(0, import_fs.mkdirSync)(configDir, { recursive: true });
|
|
304
|
+
}
|
|
305
|
+
const keypair = import_web32.Keypair.generate();
|
|
306
|
+
const data = {
|
|
307
|
+
publicKey: keypair.publicKey.toBase58(),
|
|
308
|
+
secretKey: import_bs58.default.encode(keypair.secretKey),
|
|
309
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
310
|
+
};
|
|
311
|
+
const walletPath = getSolanaWalletPath(configDir);
|
|
312
|
+
(0, import_fs.writeFileSync)(walletPath, JSON.stringify(data, null, 2));
|
|
313
|
+
return keypair;
|
|
314
|
+
}
|
|
315
|
+
function getSolanaAddress(configDir = DEFAULT_CONFIG_DIR) {
|
|
316
|
+
const wallet = loadSolanaWallet(configDir);
|
|
317
|
+
return wallet?.publicKey.toBase58() || null;
|
|
318
|
+
}
|
|
319
|
+
async function getSolanaBalance(address, chain) {
|
|
320
|
+
const connection = getSolanaConnection(chain);
|
|
321
|
+
const pubkey = new import_web32.PublicKey(address);
|
|
322
|
+
const balance = await connection.getBalance(pubkey);
|
|
323
|
+
return balance / import_web32.LAMPORTS_PER_SOL;
|
|
324
|
+
}
|
|
325
|
+
async function getSolanaUSDCBalance(address, chain) {
|
|
326
|
+
const connection = getSolanaConnection(chain);
|
|
327
|
+
const owner = new import_web32.PublicKey(address);
|
|
328
|
+
const mint = getUSDCMint(chain);
|
|
329
|
+
try {
|
|
330
|
+
const ata = await (0, import_spl_token.getAssociatedTokenAddress)(mint, owner);
|
|
331
|
+
const account = await (0, import_spl_token.getAccount)(connection, ata);
|
|
332
|
+
return Number(account.amount) / 1e6;
|
|
333
|
+
} catch (error) {
|
|
334
|
+
if (error.name === "TokenAccountNotFoundError" || error.message?.includes("could not find account")) {
|
|
335
|
+
return 0;
|
|
336
|
+
}
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async function getSolanaBalances(address, chain) {
|
|
341
|
+
const [sol, usdc] = await Promise.all([
|
|
342
|
+
getSolanaBalance(address, chain),
|
|
343
|
+
getSolanaUSDCBalance(address, chain)
|
|
344
|
+
]);
|
|
345
|
+
return { sol, usdc };
|
|
346
|
+
}
|
|
347
|
+
function isValidSolanaAddress(address) {
|
|
348
|
+
try {
|
|
349
|
+
new import_web32.PublicKey(address);
|
|
350
|
+
return true;
|
|
351
|
+
} catch {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/facilitators/solana.ts
|
|
357
|
+
init_cjs_shims();
|
|
358
|
+
var import_web33 = require("@solana/web3.js");
|
|
359
|
+
var import_spl_token2 = require("@solana/spl-token");
|
|
360
|
+
|
|
361
|
+
// src/facilitators/interface.ts
|
|
362
|
+
init_cjs_shims();
|
|
363
|
+
var BaseFacilitator = class {
|
|
364
|
+
supportsNetwork(network) {
|
|
365
|
+
return this.supportedNetworks.includes(network);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// src/facilitators/solana.ts
|
|
370
|
+
var SolanaFacilitator = class extends BaseFacilitator {
|
|
371
|
+
name = "solana";
|
|
372
|
+
displayName = "Solana Direct";
|
|
373
|
+
supportedNetworks = ["solana:mainnet", "solana:devnet"];
|
|
374
|
+
connections = /* @__PURE__ */ new Map();
|
|
375
|
+
feePayerKeypair;
|
|
376
|
+
constructor(config) {
|
|
377
|
+
super();
|
|
378
|
+
this.feePayerKeypair = config?.feePayerKeypair;
|
|
379
|
+
for (const [chain, config2] of Object.entries(SOLANA_CHAINS)) {
|
|
380
|
+
this.connections.set(
|
|
381
|
+
chain,
|
|
382
|
+
new import_web33.Connection(config2.rpc, "confirmed")
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
if (this.feePayerKeypair) {
|
|
386
|
+
console.log(`[SolanaFacilitator] Gasless mode enabled. Fee payer: ${this.feePayerKeypair.publicKey.toBase58()}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Get fee payer public key (for gasless transactions)
|
|
391
|
+
*/
|
|
392
|
+
getFeePayerPubkey() {
|
|
393
|
+
return this.feePayerKeypair?.publicKey.toBase58() || null;
|
|
394
|
+
}
|
|
395
|
+
getConnection(chain) {
|
|
396
|
+
const conn = this.connections.get(chain);
|
|
397
|
+
if (!conn) {
|
|
398
|
+
throw new Error(`No connection for chain: ${chain}`);
|
|
399
|
+
}
|
|
400
|
+
return conn;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Convert our chain name to network identifier
|
|
404
|
+
*/
|
|
405
|
+
static chainToNetwork(chain) {
|
|
406
|
+
return chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Convert network identifier to chain name
|
|
410
|
+
*/
|
|
411
|
+
static networkToChain(network) {
|
|
412
|
+
if (network === "solana:mainnet") return "solana";
|
|
413
|
+
if (network === "solana:devnet") return "solana_devnet";
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
async healthCheck() {
|
|
417
|
+
const start = Date.now();
|
|
418
|
+
try {
|
|
419
|
+
const conn = this.getConnection("solana_devnet");
|
|
420
|
+
await conn.getSlot();
|
|
421
|
+
return {
|
|
422
|
+
healthy: true,
|
|
423
|
+
latencyMs: Date.now() - start
|
|
424
|
+
};
|
|
425
|
+
} catch (error) {
|
|
426
|
+
return {
|
|
427
|
+
healthy: false,
|
|
428
|
+
error: error.message
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Verify a Solana payment
|
|
434
|
+
*
|
|
435
|
+
* Checks:
|
|
436
|
+
* 1. Transaction is valid and properly signed
|
|
437
|
+
* 2. Transfer instruction matches expected amount and recipient
|
|
438
|
+
*/
|
|
439
|
+
async verify(paymentPayload, requirements) {
|
|
440
|
+
try {
|
|
441
|
+
const solanaPayload = paymentPayload.payload;
|
|
442
|
+
if (!solanaPayload || !solanaPayload.signedTransaction) {
|
|
443
|
+
return { valid: false, error: "Missing signed transaction" };
|
|
444
|
+
}
|
|
445
|
+
const chain = solanaPayload.chain || "solana_devnet";
|
|
446
|
+
const chainConfig = SOLANA_CHAINS[chain];
|
|
447
|
+
if (!chainConfig) {
|
|
448
|
+
return { valid: false, error: `Invalid chain: ${chain}` };
|
|
449
|
+
}
|
|
450
|
+
const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
|
|
451
|
+
let tx;
|
|
452
|
+
try {
|
|
453
|
+
tx = import_web33.Transaction.from(txBuffer);
|
|
454
|
+
} catch {
|
|
455
|
+
tx = import_web33.VersionedTransaction.deserialize(txBuffer);
|
|
456
|
+
}
|
|
457
|
+
if (tx instanceof import_web33.Transaction) {
|
|
458
|
+
const hasAnySignature = tx.signatures.some(
|
|
459
|
+
(sig) => sig.signature && !sig.signature.every((b) => b === 0)
|
|
460
|
+
);
|
|
461
|
+
if (!hasAnySignature) {
|
|
462
|
+
return { valid: false, error: "Transaction not signed" };
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const expectedAmount = BigInt(requirements.amount);
|
|
466
|
+
const expectedRecipient = new import_web33.PublicKey(requirements.payTo);
|
|
467
|
+
return {
|
|
468
|
+
valid: true,
|
|
469
|
+
details: {
|
|
470
|
+
chain,
|
|
471
|
+
sender: solanaPayload.sender,
|
|
472
|
+
recipient: requirements.payTo,
|
|
473
|
+
amount: requirements.amount
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
} catch (error) {
|
|
477
|
+
return { valid: false, error: error.message };
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Settle a Solana payment
|
|
482
|
+
*
|
|
483
|
+
* Submits the signed transaction to the network.
|
|
484
|
+
* In gasless mode, adds fee payer signature before submitting.
|
|
485
|
+
*/
|
|
486
|
+
async settle(paymentPayload, requirements) {
|
|
487
|
+
try {
|
|
488
|
+
const solanaPayload = paymentPayload.payload;
|
|
489
|
+
if (!solanaPayload || !solanaPayload.signedTransaction) {
|
|
490
|
+
return { success: false, error: "Missing signed transaction" };
|
|
491
|
+
}
|
|
492
|
+
const chain = solanaPayload.chain || "solana_devnet";
|
|
493
|
+
const connection = this.getConnection(chain);
|
|
494
|
+
const txBuffer = Buffer.from(solanaPayload.signedTransaction, "base64");
|
|
495
|
+
let txToSend;
|
|
496
|
+
try {
|
|
497
|
+
const tx = import_web33.Transaction.from(txBuffer);
|
|
498
|
+
if (this.feePayerKeypair && tx.feePayer) {
|
|
499
|
+
const feePayerPubkey = this.feePayerKeypair.publicKey.toBase58();
|
|
500
|
+
const txFeePayer = tx.feePayer.toBase58();
|
|
501
|
+
if (txFeePayer === feePayerPubkey) {
|
|
502
|
+
console.log(`[SolanaFacilitator] Gasless mode: adding fee payer signature`);
|
|
503
|
+
tx.partialSign(this.feePayerKeypair);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
txToSend = tx.serialize();
|
|
507
|
+
} catch (e) {
|
|
508
|
+
txToSend = txBuffer;
|
|
509
|
+
}
|
|
510
|
+
const signature = await connection.sendRawTransaction(txToSend, {
|
|
511
|
+
skipPreflight: false,
|
|
512
|
+
preflightCommitment: "confirmed"
|
|
513
|
+
});
|
|
514
|
+
const confirmation = await connection.confirmTransaction(signature, "confirmed");
|
|
515
|
+
if (confirmation.value.err) {
|
|
516
|
+
return {
|
|
517
|
+
success: false,
|
|
518
|
+
error: `Transaction failed: ${JSON.stringify(confirmation.value.err)}`,
|
|
519
|
+
transaction: signature
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
return {
|
|
523
|
+
success: true,
|
|
524
|
+
transaction: signature,
|
|
525
|
+
status: "confirmed"
|
|
526
|
+
};
|
|
527
|
+
} catch (error) {
|
|
528
|
+
return { success: false, error: error.message };
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
supportsNetwork(network) {
|
|
532
|
+
return this.supportedNetworks.includes(network);
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
|
|
536
|
+
const chainConfig = SOLANA_CHAINS[chain];
|
|
537
|
+
const connection = new import_web33.Connection(chainConfig.rpc, "confirmed");
|
|
538
|
+
const mint = new import_web33.PublicKey(chainConfig.tokens.USDC.mint);
|
|
539
|
+
const actualFeePayer = feePayerPubkey || senderPubkey;
|
|
540
|
+
const senderATA = await (0, import_spl_token2.getAssociatedTokenAddress)(mint, senderPubkey);
|
|
541
|
+
const recipientATA = await (0, import_spl_token2.getAssociatedTokenAddress)(mint, recipientPubkey);
|
|
542
|
+
const transaction = new import_web33.Transaction();
|
|
543
|
+
try {
|
|
544
|
+
await (0, import_spl_token2.getAccount)(connection, recipientATA);
|
|
545
|
+
} catch {
|
|
546
|
+
transaction.add(
|
|
547
|
+
(0, import_spl_token2.createAssociatedTokenAccountInstruction)(
|
|
548
|
+
actualFeePayer,
|
|
549
|
+
// payer (fee payer in gasless mode)
|
|
550
|
+
recipientATA,
|
|
551
|
+
// ata to create
|
|
552
|
+
recipientPubkey,
|
|
553
|
+
// owner
|
|
554
|
+
mint
|
|
555
|
+
// mint
|
|
556
|
+
)
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
transaction.add(
|
|
560
|
+
(0, import_spl_token2.createTransferCheckedInstruction)(
|
|
561
|
+
senderATA,
|
|
562
|
+
// source
|
|
563
|
+
mint,
|
|
564
|
+
// mint
|
|
565
|
+
recipientATA,
|
|
566
|
+
// destination
|
|
567
|
+
senderPubkey,
|
|
568
|
+
// owner (sender still authorizes the transfer)
|
|
569
|
+
amount,
|
|
570
|
+
// amount
|
|
571
|
+
chainConfig.tokens.USDC.decimals
|
|
572
|
+
// decimals
|
|
573
|
+
)
|
|
574
|
+
);
|
|
575
|
+
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
|
|
576
|
+
transaction.recentBlockhash = blockhash;
|
|
577
|
+
transaction.feePayer = actualFeePayer;
|
|
578
|
+
return transaction;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// src/client/index.ts
|
|
582
|
+
var import_web34 = require("@solana/web3.js");
|
|
583
|
+
|
|
140
584
|
// src/client/types.ts
|
|
141
585
|
init_cjs_shims();
|
|
142
586
|
|
|
@@ -159,7 +603,7 @@ var MoltsPayClient = class {
|
|
|
159
603
|
todaySpending = 0;
|
|
160
604
|
lastSpendingReset = 0;
|
|
161
605
|
constructor(options = {}) {
|
|
162
|
-
this.configDir = options.configDir || (0,
|
|
606
|
+
this.configDir = options.configDir || (0, import_path2.join)((0, import_os2.homedir)(), ".moltspay");
|
|
163
607
|
this.config = this.loadConfig();
|
|
164
608
|
this.walletData = this.loadWallet();
|
|
165
609
|
this.loadSpending();
|
|
@@ -179,6 +623,12 @@ var MoltsPayClient = class {
|
|
|
179
623
|
get address() {
|
|
180
624
|
return this.wallet?.address || null;
|
|
181
625
|
}
|
|
626
|
+
/**
|
|
627
|
+
* Get wallet instance (for direct operations like approvals)
|
|
628
|
+
*/
|
|
629
|
+
getWallet() {
|
|
630
|
+
return this.wallet;
|
|
631
|
+
}
|
|
182
632
|
/**
|
|
183
633
|
* Get current config
|
|
184
634
|
*/
|
|
@@ -248,9 +698,14 @@ var MoltsPayClient = class {
|
|
|
248
698
|
}
|
|
249
699
|
throw new Error(data.error || "Unexpected response");
|
|
250
700
|
}
|
|
701
|
+
const wwwAuthHeader = initialRes.headers.get("www-authenticate");
|
|
251
702
|
const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
703
|
+
if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
|
|
704
|
+
console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
|
|
705
|
+
return await this.handleMPPPayment(serverUrl, service, params, wwwAuthHeader);
|
|
706
|
+
}
|
|
252
707
|
if (!paymentRequiredHeader) {
|
|
253
|
-
throw new Error("Missing x-payment-required
|
|
708
|
+
throw new Error("Missing payment header (x-payment-required or www-authenticate)");
|
|
254
709
|
}
|
|
255
710
|
let requirements;
|
|
256
711
|
try {
|
|
@@ -267,17 +722,22 @@ var MoltsPayClient = class {
|
|
|
267
722
|
throw new Error("Invalid x-payment-required header");
|
|
268
723
|
}
|
|
269
724
|
const networkToChainName = (network2) => {
|
|
725
|
+
if (network2 === "solana:mainnet") return "solana";
|
|
726
|
+
if (network2 === "solana:devnet") return "solana_devnet";
|
|
270
727
|
const match = network2.match(/^eip155:(\d+)$/);
|
|
271
728
|
if (!match) return null;
|
|
272
729
|
const chainId = parseInt(match[1]);
|
|
273
730
|
if (chainId === 8453) return "base";
|
|
274
731
|
if (chainId === 137) return "polygon";
|
|
275
732
|
if (chainId === 84532) return "base_sepolia";
|
|
733
|
+
if (chainId === 42431) return "tempo_moderato";
|
|
734
|
+
if (chainId === 56) return "bnb";
|
|
735
|
+
if (chainId === 97) return "bnb_testnet";
|
|
276
736
|
return null;
|
|
277
737
|
};
|
|
278
738
|
const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
|
|
279
|
-
let chainName;
|
|
280
739
|
const userSpecifiedChain = options.chain;
|
|
740
|
+
let selectedChain;
|
|
281
741
|
if (userSpecifiedChain) {
|
|
282
742
|
if (!serverChains.includes(userSpecifiedChain)) {
|
|
283
743
|
throw new Error(
|
|
@@ -285,17 +745,27 @@ var MoltsPayClient = class {
|
|
|
285
745
|
Server accepts: ${serverChains.join(", ")}`
|
|
286
746
|
);
|
|
287
747
|
}
|
|
288
|
-
|
|
748
|
+
selectedChain = userSpecifiedChain;
|
|
289
749
|
} else {
|
|
290
750
|
if (serverChains.length === 1 && serverChains[0] === "base") {
|
|
291
|
-
|
|
751
|
+
selectedChain = "base";
|
|
292
752
|
} else {
|
|
293
753
|
throw new Error(
|
|
294
754
|
`Server accepts: ${serverChains.join(", ")}
|
|
295
|
-
Please specify: --chain
|
|
755
|
+
Please specify: --chain <chain_name>`
|
|
296
756
|
);
|
|
297
757
|
}
|
|
298
758
|
}
|
|
759
|
+
if (selectedChain === "solana" || selectedChain === "solana_devnet") {
|
|
760
|
+
const solanaChain = selectedChain;
|
|
761
|
+
const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
762
|
+
const req2 = requirements.find((r) => r.network === network2);
|
|
763
|
+
if (!req2) {
|
|
764
|
+
throw new Error(`Failed to find payment requirement for ${selectedChain}`);
|
|
765
|
+
}
|
|
766
|
+
return await this.handleSolanaPayment(serverUrl, service, params, req2, solanaChain);
|
|
767
|
+
}
|
|
768
|
+
const chainName = selectedChain;
|
|
299
769
|
const chain = getChain(chainName);
|
|
300
770
|
const network = `eip155:${chain.chainId}`;
|
|
301
771
|
const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
|
|
@@ -330,6 +800,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
330
800
|
} else {
|
|
331
801
|
console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
|
|
332
802
|
}
|
|
803
|
+
if (chainName === "bnb" || chainName === "bnb_testnet") {
|
|
804
|
+
console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
|
|
805
|
+
const payTo2 = req.payTo || req.resource;
|
|
806
|
+
if (!payTo2) {
|
|
807
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
808
|
+
}
|
|
809
|
+
const bnbSpender = req.extra?.bnbSpender;
|
|
810
|
+
if (!bnbSpender) {
|
|
811
|
+
throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
|
|
812
|
+
}
|
|
813
|
+
return await this.handleBNBPayment(serverUrl, service, params, {
|
|
814
|
+
to: payTo2,
|
|
815
|
+
amount,
|
|
816
|
+
token,
|
|
817
|
+
chainName,
|
|
818
|
+
chain,
|
|
819
|
+
spender: bnbSpender
|
|
820
|
+
});
|
|
821
|
+
}
|
|
333
822
|
const payTo = req.payTo || req.resource;
|
|
334
823
|
if (!payTo) {
|
|
335
824
|
throw new Error("Missing payTo address in payment requirements");
|
|
@@ -379,6 +868,300 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
379
868
|
console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
|
|
380
869
|
return result.result;
|
|
381
870
|
}
|
|
871
|
+
/**
|
|
872
|
+
* Handle MPP (Machine Payments Protocol) payment flow
|
|
873
|
+
* Called when pay() detects WWW-Authenticate header in 402 response
|
|
874
|
+
*/
|
|
875
|
+
async handleMPPPayment(serverUrl, service, params, wwwAuthHeader) {
|
|
876
|
+
const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
|
|
877
|
+
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
878
|
+
const { tempoModerato } = await import("viem/chains");
|
|
879
|
+
const { Actions } = await import("viem/tempo");
|
|
880
|
+
const privateKey = this.walletData.privateKey;
|
|
881
|
+
const account = privateKeyToAccount2(privateKey);
|
|
882
|
+
console.log(`[MoltsPay] Using MPP protocol on Tempo`);
|
|
883
|
+
console.log(`[MoltsPay] Account: ${account.address}`);
|
|
884
|
+
const parseAuthParam = (header, key) => {
|
|
885
|
+
const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
|
|
886
|
+
return match ? match[1] : null;
|
|
887
|
+
};
|
|
888
|
+
const challengeId = parseAuthParam(wwwAuthHeader, "id");
|
|
889
|
+
const method = parseAuthParam(wwwAuthHeader, "method");
|
|
890
|
+
const realm = parseAuthParam(wwwAuthHeader, "realm");
|
|
891
|
+
const requestB64 = parseAuthParam(wwwAuthHeader, "request");
|
|
892
|
+
if (method !== "tempo") {
|
|
893
|
+
throw new Error(`Unsupported payment method: ${method}`);
|
|
894
|
+
}
|
|
895
|
+
if (!requestB64) {
|
|
896
|
+
throw new Error("Missing request in WWW-Authenticate");
|
|
897
|
+
}
|
|
898
|
+
const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
|
|
899
|
+
const paymentRequest = JSON.parse(requestJson);
|
|
900
|
+
const { amount, currency, recipient, methodDetails } = paymentRequest;
|
|
901
|
+
const chainId = methodDetails?.chainId || 42431;
|
|
902
|
+
const amountDisplay = Number(amount) / 1e6;
|
|
903
|
+
console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
|
|
904
|
+
this.checkLimits(amountDisplay);
|
|
905
|
+
console.log(`[MoltsPay] Sending transaction on Tempo...`);
|
|
906
|
+
const tempoChain = { ...tempoModerato, feeToken: currency };
|
|
907
|
+
const publicClient = createPublicClient({
|
|
908
|
+
chain: tempoChain,
|
|
909
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
910
|
+
});
|
|
911
|
+
const walletClient = createWalletClient({
|
|
912
|
+
account,
|
|
913
|
+
chain: tempoChain,
|
|
914
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
915
|
+
});
|
|
916
|
+
const txHash = await Actions.token.transfer(walletClient, {
|
|
917
|
+
to: recipient,
|
|
918
|
+
amount: BigInt(amount),
|
|
919
|
+
token: currency
|
|
920
|
+
});
|
|
921
|
+
console.log(`[MoltsPay] Transaction: ${txHash}`);
|
|
922
|
+
await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
923
|
+
console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
|
|
924
|
+
const credential = {
|
|
925
|
+
challenge: {
|
|
926
|
+
id: challengeId,
|
|
927
|
+
realm,
|
|
928
|
+
method: "tempo",
|
|
929
|
+
intent: "charge",
|
|
930
|
+
request: paymentRequest
|
|
931
|
+
},
|
|
932
|
+
payload: { hash: txHash, type: "hash" },
|
|
933
|
+
source: `did:pkh:eip155:${chainId}:${account.address}`
|
|
934
|
+
};
|
|
935
|
+
const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
936
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
937
|
+
method: "POST",
|
|
938
|
+
headers: {
|
|
939
|
+
"Content-Type": "application/json",
|
|
940
|
+
"Authorization": `Payment ${credentialB64}`
|
|
941
|
+
},
|
|
942
|
+
body: JSON.stringify({ service, params, chain: "tempo_moderato" })
|
|
943
|
+
});
|
|
944
|
+
const result = await paidRes.json();
|
|
945
|
+
if (!paidRes.ok) {
|
|
946
|
+
throw new Error(result.error || "Payment verification failed");
|
|
947
|
+
}
|
|
948
|
+
this.recordSpending(amountDisplay);
|
|
949
|
+
console.log(`[MoltsPay] Success!`);
|
|
950
|
+
return result.result || result;
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Handle BNB Chain payment flow (pre-approval + intent signature)
|
|
954
|
+
*
|
|
955
|
+
* Flow:
|
|
956
|
+
* 1. Check client has approved server wallet (done via `moltspay init`)
|
|
957
|
+
* 2. Sign EIP-712 payment intent (no gas, just signature)
|
|
958
|
+
* 3. Send intent to server
|
|
959
|
+
* 4. Server executes service
|
|
960
|
+
* 5. Server calls transferFrom if successful (pay-for-success)
|
|
961
|
+
*/
|
|
962
|
+
async handleBNBPayment(serverUrl, service, params, paymentDetails) {
|
|
963
|
+
const { to, amount, token, chainName, chain, spender } = paymentDetails;
|
|
964
|
+
const tokenConfig = chain.tokens[token];
|
|
965
|
+
const provider = new import_ethers.ethers.JsonRpcProvider(chain.rpc);
|
|
966
|
+
const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
|
|
967
|
+
const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
|
|
968
|
+
if (allowance < amountWeiCheck) {
|
|
969
|
+
const nativeBalance = await provider.getBalance(this.wallet.address);
|
|
970
|
+
const minGasBalance = import_ethers.ethers.parseEther("0.0005");
|
|
971
|
+
if (nativeBalance < minGasBalance) {
|
|
972
|
+
const nativeBNB = parseFloat(import_ethers.ethers.formatEther(nativeBalance)).toFixed(4);
|
|
973
|
+
const isTestnet = chainName === "bnb_testnet";
|
|
974
|
+
if (isTestnet) {
|
|
975
|
+
throw new Error(
|
|
976
|
+
`\u274C Insufficient tBNB for approval transaction
|
|
977
|
+
|
|
978
|
+
Current tBNB: ${nativeBNB}
|
|
979
|
+
Required: ~0.001 tBNB
|
|
980
|
+
|
|
981
|
+
Get testnet tokens: npx moltspay faucet --chain bnb_testnet
|
|
982
|
+
(Gives USDC + tBNB for gas)`
|
|
983
|
+
);
|
|
984
|
+
} else {
|
|
985
|
+
throw new Error(
|
|
986
|
+
`\u274C Insufficient BNB for approval transaction
|
|
987
|
+
|
|
988
|
+
Current BNB: ${nativeBNB}
|
|
989
|
+
Required: ~0.001 BNB (~$0.60)
|
|
990
|
+
|
|
991
|
+
To get BNB:
|
|
992
|
+
\u2022 Withdraw from Binance/exchange to your wallet
|
|
993
|
+
\u2022 Most exchanges include BNB dust with withdrawals
|
|
994
|
+
|
|
995
|
+
After funding, run:
|
|
996
|
+
npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
`Insufficient allowance for ${spender.slice(0, 10)}...
|
|
1002
|
+
Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
|
|
1006
|
+
const intent = {
|
|
1007
|
+
from: this.wallet.address,
|
|
1008
|
+
to,
|
|
1009
|
+
amount: amountWei,
|
|
1010
|
+
token: tokenConfig.address,
|
|
1011
|
+
service,
|
|
1012
|
+
nonce: Date.now(),
|
|
1013
|
+
// Use timestamp as nonce for simplicity
|
|
1014
|
+
deadline: Date.now() + 36e5
|
|
1015
|
+
// 1 hour
|
|
1016
|
+
};
|
|
1017
|
+
const domain = {
|
|
1018
|
+
name: "MoltsPay",
|
|
1019
|
+
version: "1",
|
|
1020
|
+
chainId: chain.chainId
|
|
1021
|
+
};
|
|
1022
|
+
const types = {
|
|
1023
|
+
PaymentIntent: [
|
|
1024
|
+
{ name: "from", type: "address" },
|
|
1025
|
+
{ name: "to", type: "address" },
|
|
1026
|
+
{ name: "amount", type: "uint256" },
|
|
1027
|
+
{ name: "token", type: "address" },
|
|
1028
|
+
{ name: "service", type: "string" },
|
|
1029
|
+
{ name: "nonce", type: "uint256" },
|
|
1030
|
+
{ name: "deadline", type: "uint256" }
|
|
1031
|
+
]
|
|
1032
|
+
};
|
|
1033
|
+
console.log(`[MoltsPay] Signing BNB payment intent...`);
|
|
1034
|
+
const signature = await this.wallet.signTypedData(domain, types, intent);
|
|
1035
|
+
const network = `eip155:${chain.chainId}`;
|
|
1036
|
+
const payload = {
|
|
1037
|
+
x402Version: 2,
|
|
1038
|
+
scheme: "exact",
|
|
1039
|
+
network,
|
|
1040
|
+
payload: {
|
|
1041
|
+
intent: {
|
|
1042
|
+
...intent,
|
|
1043
|
+
signature
|
|
1044
|
+
},
|
|
1045
|
+
chainId: chain.chainId
|
|
1046
|
+
},
|
|
1047
|
+
accepted: {
|
|
1048
|
+
scheme: "exact",
|
|
1049
|
+
network,
|
|
1050
|
+
asset: tokenConfig.address,
|
|
1051
|
+
amount: amountWei,
|
|
1052
|
+
payTo: to,
|
|
1053
|
+
maxTimeoutSeconds: 300
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
1057
|
+
console.log(`[MoltsPay] Sending BNB payment request...`);
|
|
1058
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
1059
|
+
method: "POST",
|
|
1060
|
+
headers: {
|
|
1061
|
+
"Content-Type": "application/json",
|
|
1062
|
+
"X-Payment": paymentHeader
|
|
1063
|
+
},
|
|
1064
|
+
body: JSON.stringify({ service, params, chain: chainName })
|
|
1065
|
+
});
|
|
1066
|
+
const result = await paidRes.json();
|
|
1067
|
+
if (!paidRes.ok) {
|
|
1068
|
+
throw new Error(result.error || "BNB payment failed");
|
|
1069
|
+
}
|
|
1070
|
+
this.recordSpending(amount);
|
|
1071
|
+
console.log(`[MoltsPay] Success! BNB payment settled.`);
|
|
1072
|
+
return result.result || result;
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Handle Solana payment flow
|
|
1076
|
+
*
|
|
1077
|
+
* Solana uses SPL token transfers with pay-for-success model:
|
|
1078
|
+
* 1. Client creates and signs a transfer transaction
|
|
1079
|
+
* 2. Server submits the transaction after service completes
|
|
1080
|
+
*/
|
|
1081
|
+
async handleSolanaPayment(serverUrl, service, params, requirements, chain) {
|
|
1082
|
+
const solanaWallet = loadSolanaWallet(this.configDir);
|
|
1083
|
+
if (!solanaWallet) {
|
|
1084
|
+
throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
|
|
1085
|
+
}
|
|
1086
|
+
const amount = Number(requirements.amount);
|
|
1087
|
+
const amountUSDC = amount / 1e6;
|
|
1088
|
+
this.checkLimits(amountUSDC);
|
|
1089
|
+
console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
|
|
1090
|
+
if (!requirements.payTo) {
|
|
1091
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
1092
|
+
}
|
|
1093
|
+
const solanaFeePayer = requirements.extra?.solanaFeePayer;
|
|
1094
|
+
const feePayerPubkey = solanaFeePayer ? new import_web34.PublicKey(solanaFeePayer) : void 0;
|
|
1095
|
+
if (feePayerPubkey) {
|
|
1096
|
+
console.log(`[MoltsPay] Gasless mode: server pays fees`);
|
|
1097
|
+
}
|
|
1098
|
+
const recipientPubkey = new import_web34.PublicKey(requirements.payTo);
|
|
1099
|
+
const transaction = await createSolanaPaymentTransaction(
|
|
1100
|
+
solanaWallet.publicKey,
|
|
1101
|
+
recipientPubkey,
|
|
1102
|
+
BigInt(amount),
|
|
1103
|
+
chain,
|
|
1104
|
+
feePayerPubkey
|
|
1105
|
+
// Optional fee payer for gasless mode
|
|
1106
|
+
);
|
|
1107
|
+
if (feePayerPubkey) {
|
|
1108
|
+
transaction.partialSign(solanaWallet);
|
|
1109
|
+
} else {
|
|
1110
|
+
transaction.sign(solanaWallet);
|
|
1111
|
+
}
|
|
1112
|
+
const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
|
|
1113
|
+
console.log(`[MoltsPay] Transaction signed, sending to server...`);
|
|
1114
|
+
const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
1115
|
+
const payload = {
|
|
1116
|
+
x402Version: 2,
|
|
1117
|
+
scheme: "exact",
|
|
1118
|
+
network,
|
|
1119
|
+
payload: {
|
|
1120
|
+
signedTransaction: signedTx,
|
|
1121
|
+
sender: solanaWallet.publicKey.toBase58(),
|
|
1122
|
+
chain
|
|
1123
|
+
},
|
|
1124
|
+
accepted: {
|
|
1125
|
+
scheme: "exact",
|
|
1126
|
+
network,
|
|
1127
|
+
asset: requirements.asset,
|
|
1128
|
+
amount: requirements.amount,
|
|
1129
|
+
payTo: requirements.payTo,
|
|
1130
|
+
maxTimeoutSeconds: 300
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
1134
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
1135
|
+
method: "POST",
|
|
1136
|
+
headers: {
|
|
1137
|
+
"Content-Type": "application/json",
|
|
1138
|
+
"X-Payment": paymentHeader
|
|
1139
|
+
},
|
|
1140
|
+
body: JSON.stringify({ service, params, chain })
|
|
1141
|
+
});
|
|
1142
|
+
const result = await paidRes.json();
|
|
1143
|
+
if (!paidRes.ok) {
|
|
1144
|
+
throw new Error(result.error || "Solana payment failed");
|
|
1145
|
+
}
|
|
1146
|
+
this.recordSpending(amountUSDC);
|
|
1147
|
+
console.log(`[MoltsPay] Success! Solana payment settled.`);
|
|
1148
|
+
if (result.payment?.transaction) {
|
|
1149
|
+
const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
|
|
1150
|
+
console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
|
|
1151
|
+
}
|
|
1152
|
+
return result.result || result;
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Check ERC20 allowance for a spender
|
|
1156
|
+
*/
|
|
1157
|
+
async checkAllowance(tokenAddress, spender, provider) {
|
|
1158
|
+
const contract = new import_ethers.ethers.Contract(
|
|
1159
|
+
tokenAddress,
|
|
1160
|
+
["function allowance(address owner, address spender) view returns (uint256)"],
|
|
1161
|
+
provider
|
|
1162
|
+
);
|
|
1163
|
+
return await contract.allowance(this.wallet.address, spender);
|
|
1164
|
+
}
|
|
382
1165
|
/**
|
|
383
1166
|
* Sign EIP-3009 transferWithAuthorization (GASLESS)
|
|
384
1167
|
* This only signs - no on-chain transaction, no gas needed.
|
|
@@ -449,26 +1232,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
449
1232
|
}
|
|
450
1233
|
// --- Config & Wallet Management ---
|
|
451
1234
|
loadConfig() {
|
|
452
|
-
const configPath = (0,
|
|
453
|
-
if ((0,
|
|
454
|
-
const content = (0,
|
|
1235
|
+
const configPath = (0, import_path2.join)(this.configDir, "config.json");
|
|
1236
|
+
if ((0, import_fs2.existsSync)(configPath)) {
|
|
1237
|
+
const content = (0, import_fs2.readFileSync)(configPath, "utf-8");
|
|
455
1238
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
456
1239
|
}
|
|
457
1240
|
return { ...DEFAULT_CONFIG };
|
|
458
1241
|
}
|
|
459
1242
|
saveConfig() {
|
|
460
|
-
(0,
|
|
461
|
-
const configPath = (0,
|
|
462
|
-
(0,
|
|
1243
|
+
(0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
|
|
1244
|
+
const configPath = (0, import_path2.join)(this.configDir, "config.json");
|
|
1245
|
+
(0, import_fs2.writeFileSync)(configPath, JSON.stringify(this.config, null, 2));
|
|
463
1246
|
}
|
|
464
1247
|
/**
|
|
465
1248
|
* Load spending data from disk
|
|
466
1249
|
*/
|
|
467
1250
|
loadSpending() {
|
|
468
|
-
const spendingPath = (0,
|
|
469
|
-
if ((0,
|
|
1251
|
+
const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
|
|
1252
|
+
if ((0, import_fs2.existsSync)(spendingPath)) {
|
|
470
1253
|
try {
|
|
471
|
-
const data = JSON.parse((0,
|
|
1254
|
+
const data = JSON.parse((0, import_fs2.readFileSync)(spendingPath, "utf-8"));
|
|
472
1255
|
const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
|
|
473
1256
|
if (data.date && data.date === today) {
|
|
474
1257
|
this.todaySpending = data.amount || 0;
|
|
@@ -487,29 +1270,29 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
487
1270
|
* Save spending data to disk
|
|
488
1271
|
*/
|
|
489
1272
|
saveSpending() {
|
|
490
|
-
(0,
|
|
491
|
-
const spendingPath = (0,
|
|
1273
|
+
(0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
|
|
1274
|
+
const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
|
|
492
1275
|
const data = {
|
|
493
1276
|
date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
|
|
494
1277
|
amount: this.todaySpending,
|
|
495
1278
|
updatedAt: Date.now()
|
|
496
1279
|
};
|
|
497
|
-
(0,
|
|
1280
|
+
(0, import_fs2.writeFileSync)(spendingPath, JSON.stringify(data, null, 2));
|
|
498
1281
|
}
|
|
499
1282
|
loadWallet() {
|
|
500
|
-
const walletPath = (0,
|
|
501
|
-
if ((0,
|
|
1283
|
+
const walletPath = (0, import_path2.join)(this.configDir, "wallet.json");
|
|
1284
|
+
if ((0, import_fs2.existsSync)(walletPath)) {
|
|
502
1285
|
try {
|
|
503
|
-
const stats = (0,
|
|
1286
|
+
const stats = (0, import_fs2.statSync)(walletPath);
|
|
504
1287
|
const mode = stats.mode & 511;
|
|
505
1288
|
if (mode !== 384) {
|
|
506
1289
|
console.warn(`[MoltsPay] WARNING: wallet.json has insecure permissions (${mode.toString(8)})`);
|
|
507
1290
|
console.warn(`[MoltsPay] Fixing permissions to 0600...`);
|
|
508
|
-
(0,
|
|
1291
|
+
(0, import_fs2.chmodSync)(walletPath, 384);
|
|
509
1292
|
}
|
|
510
1293
|
} catch (err) {
|
|
511
1294
|
}
|
|
512
|
-
const content = (0,
|
|
1295
|
+
const content = (0, import_fs2.readFileSync)(walletPath, "utf-8");
|
|
513
1296
|
return JSON.parse(content);
|
|
514
1297
|
}
|
|
515
1298
|
return null;
|
|
@@ -518,15 +1301,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
518
1301
|
* Initialize a new wallet (called by CLI)
|
|
519
1302
|
*/
|
|
520
1303
|
static init(configDir, options) {
|
|
521
|
-
(0,
|
|
1304
|
+
(0, import_fs2.mkdirSync)(configDir, { recursive: true });
|
|
522
1305
|
const wallet = import_ethers.Wallet.createRandom();
|
|
523
1306
|
const walletData = {
|
|
524
1307
|
address: wallet.address,
|
|
525
1308
|
privateKey: wallet.privateKey,
|
|
526
1309
|
createdAt: Date.now()
|
|
527
1310
|
};
|
|
528
|
-
const walletPath = (0,
|
|
529
|
-
(0,
|
|
1311
|
+
const walletPath = (0, import_path2.join)(configDir, "wallet.json");
|
|
1312
|
+
(0, import_fs2.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
530
1313
|
const config = {
|
|
531
1314
|
chain: options.chain,
|
|
532
1315
|
limits: {
|
|
@@ -534,8 +1317,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
534
1317
|
maxPerDay: options.maxPerDay
|
|
535
1318
|
}
|
|
536
1319
|
};
|
|
537
|
-
const configPath = (0,
|
|
538
|
-
(0,
|
|
1320
|
+
const configPath = (0, import_path2.join)(configDir, "config.json");
|
|
1321
|
+
(0, import_fs2.writeFileSync)(configPath, JSON.stringify(config, null, 2));
|
|
539
1322
|
return { address: wallet.address, configDir };
|
|
540
1323
|
}
|
|
541
1324
|
/**
|
|
@@ -565,30 +1348,59 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
565
1348
|
};
|
|
566
1349
|
}
|
|
567
1350
|
/**
|
|
568
|
-
* Get wallet balances on all supported chains (Base + Polygon)
|
|
1351
|
+
* Get wallet balances on all supported chains (Base + Polygon + Tempo)
|
|
569
1352
|
*/
|
|
570
1353
|
async getAllBalances() {
|
|
571
1354
|
if (!this.wallet) {
|
|
572
1355
|
throw new Error("Client not initialized");
|
|
573
1356
|
}
|
|
574
|
-
const supportedChains = ["base", "polygon", "base_sepolia"];
|
|
1357
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
|
|
575
1358
|
const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
576
1359
|
const results = {};
|
|
1360
|
+
const tempoTokens = {
|
|
1361
|
+
pathUSD: "0x20c0000000000000000000000000000000000000",
|
|
1362
|
+
alphaUSD: "0x20c0000000000000000000000000000000000001",
|
|
1363
|
+
betaUSD: "0x20c0000000000000000000000000000000000002",
|
|
1364
|
+
thetaUSD: "0x20c0000000000000000000000000000000000003"
|
|
1365
|
+
};
|
|
577
1366
|
await Promise.all(
|
|
578
1367
|
supportedChains.map(async (chainName) => {
|
|
579
1368
|
try {
|
|
580
1369
|
const chain = getChain(chainName);
|
|
581
1370
|
const provider = new import_ethers.ethers.JsonRpcProvider(chain.rpc);
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
1371
|
+
if (chainName === "tempo_moderato") {
|
|
1372
|
+
const [nativeBalance, pathUSD, alphaUSD, betaUSD, thetaUSD] = await Promise.all([
|
|
1373
|
+
provider.getBalance(this.wallet.address),
|
|
1374
|
+
new import_ethers.ethers.Contract(tempoTokens.pathUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
1375
|
+
new import_ethers.ethers.Contract(tempoTokens.alphaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
1376
|
+
new import_ethers.ethers.Contract(tempoTokens.betaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
1377
|
+
new import_ethers.ethers.Contract(tempoTokens.thetaUSD, tokenAbi, provider).balanceOf(this.wallet.address)
|
|
1378
|
+
]);
|
|
1379
|
+
results[chainName] = {
|
|
1380
|
+
usdc: parseFloat(import_ethers.ethers.formatUnits(pathUSD, 6)),
|
|
1381
|
+
// pathUSD as default USDC
|
|
1382
|
+
usdt: parseFloat(import_ethers.ethers.formatUnits(alphaUSD, 6)),
|
|
1383
|
+
// alphaUSD as default USDT
|
|
1384
|
+
native: parseFloat(import_ethers.ethers.formatEther(nativeBalance)),
|
|
1385
|
+
tempo: {
|
|
1386
|
+
pathUSD: parseFloat(import_ethers.ethers.formatUnits(pathUSD, 6)),
|
|
1387
|
+
alphaUSD: parseFloat(import_ethers.ethers.formatUnits(alphaUSD, 6)),
|
|
1388
|
+
betaUSD: parseFloat(import_ethers.ethers.formatUnits(betaUSD, 6)),
|
|
1389
|
+
thetaUSD: parseFloat(import_ethers.ethers.formatUnits(thetaUSD, 6))
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
} else {
|
|
1393
|
+
const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
|
|
1394
|
+
provider.getBalance(this.wallet.address),
|
|
1395
|
+
new import_ethers.ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
1396
|
+
new import_ethers.ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
|
|
1397
|
+
]);
|
|
1398
|
+
results[chainName] = {
|
|
1399
|
+
usdc: parseFloat(import_ethers.ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
|
|
1400
|
+
usdt: parseFloat(import_ethers.ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
|
|
1401
|
+
native: parseFloat(import_ethers.ethers.formatEther(nativeBalance))
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
592
1404
|
} catch (err) {
|
|
593
1405
|
results[chainName] = { usdc: 0, usdt: 0, native: 0 };
|
|
594
1406
|
}
|
|
@@ -596,28 +1408,135 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
596
1408
|
);
|
|
597
1409
|
return results;
|
|
598
1410
|
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Pay for a service using MPP (Machine Payments Protocol)
|
|
1413
|
+
*
|
|
1414
|
+
* This implements the MPP flow manually for EOA wallets:
|
|
1415
|
+
* 1. Request service → get 402 with WWW-Authenticate
|
|
1416
|
+
* 2. Parse payment requirements
|
|
1417
|
+
* 3. Execute transfer on Tempo chain
|
|
1418
|
+
* 4. Retry with transaction hash as credential
|
|
1419
|
+
*
|
|
1420
|
+
* @param url - Full URL of the MPP-enabled endpoint
|
|
1421
|
+
* @param options - Request options (body, headers)
|
|
1422
|
+
* @returns Response from the service
|
|
1423
|
+
*/
|
|
1424
|
+
async payWithMPP(url, options = {}) {
|
|
1425
|
+
if (!this.wallet || !this.walletData) {
|
|
1426
|
+
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
1427
|
+
}
|
|
1428
|
+
const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
|
|
1429
|
+
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
1430
|
+
const { tempoModerato } = await import("viem/chains");
|
|
1431
|
+
const { Actions } = await import("viem/tempo");
|
|
1432
|
+
const privateKey = this.walletData.privateKey;
|
|
1433
|
+
const account = privateKeyToAccount2(privateKey);
|
|
1434
|
+
console.log(`[MoltsPay] Making MPP request to: ${url}`);
|
|
1435
|
+
console.log(`[MoltsPay] Using account: ${account.address}`);
|
|
1436
|
+
const initResponse = await fetch(url, {
|
|
1437
|
+
method: "POST",
|
|
1438
|
+
headers: {
|
|
1439
|
+
"Content-Type": "application/json",
|
|
1440
|
+
...options.headers
|
|
1441
|
+
},
|
|
1442
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
1443
|
+
});
|
|
1444
|
+
if (initResponse.status !== 402) {
|
|
1445
|
+
if (initResponse.ok) {
|
|
1446
|
+
return initResponse.json();
|
|
1447
|
+
}
|
|
1448
|
+
const errorText = await initResponse.text();
|
|
1449
|
+
throw new Error(`Request failed (${initResponse.status}): ${errorText}`);
|
|
1450
|
+
}
|
|
1451
|
+
const wwwAuth = initResponse.headers.get("www-authenticate");
|
|
1452
|
+
if (!wwwAuth || !wwwAuth.toLowerCase().includes("payment")) {
|
|
1453
|
+
throw new Error("No WWW-Authenticate Payment challenge in 402 response");
|
|
1454
|
+
}
|
|
1455
|
+
console.log(`[MoltsPay] Got 402, parsing payment challenge...`);
|
|
1456
|
+
const parseAuthParam = (header, key) => {
|
|
1457
|
+
const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
|
|
1458
|
+
return match ? match[1] : null;
|
|
1459
|
+
};
|
|
1460
|
+
const challengeId = parseAuthParam(wwwAuth, "id");
|
|
1461
|
+
const method = parseAuthParam(wwwAuth, "method");
|
|
1462
|
+
const realm = parseAuthParam(wwwAuth, "realm");
|
|
1463
|
+
const requestB64 = parseAuthParam(wwwAuth, "request");
|
|
1464
|
+
if (method !== "tempo") {
|
|
1465
|
+
throw new Error(`Unsupported payment method: ${method}`);
|
|
1466
|
+
}
|
|
1467
|
+
if (!requestB64) {
|
|
1468
|
+
throw new Error("Missing request in WWW-Authenticate");
|
|
1469
|
+
}
|
|
1470
|
+
const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
|
|
1471
|
+
const paymentRequest = JSON.parse(requestJson);
|
|
1472
|
+
console.log(`[MoltsPay] Payment request:`, paymentRequest);
|
|
1473
|
+
const { amount, currency, recipient, methodDetails } = paymentRequest;
|
|
1474
|
+
const chainId = methodDetails?.chainId || 42431;
|
|
1475
|
+
console.log(`[MoltsPay] Executing transfer on Tempo (chainId: ${chainId})...`);
|
|
1476
|
+
console.log(`[MoltsPay] Amount: ${amount}, To: ${recipient}`);
|
|
1477
|
+
const tempoChain = { ...tempoModerato, feeToken: currency };
|
|
1478
|
+
const publicClient = createPublicClient({
|
|
1479
|
+
chain: tempoChain,
|
|
1480
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
1481
|
+
});
|
|
1482
|
+
const walletClient = createWalletClient({
|
|
1483
|
+
account,
|
|
1484
|
+
chain: tempoChain,
|
|
1485
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
1486
|
+
});
|
|
1487
|
+
const txHash = await Actions.token.transfer(walletClient, {
|
|
1488
|
+
to: recipient,
|
|
1489
|
+
amount: BigInt(amount),
|
|
1490
|
+
token: currency
|
|
1491
|
+
});
|
|
1492
|
+
console.log(`[MoltsPay] Transaction sent: ${txHash}`);
|
|
1493
|
+
console.log(`[MoltsPay] Waiting for confirmation...`);
|
|
1494
|
+
await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
1495
|
+
console.log(`[MoltsPay] Transaction confirmed!`);
|
|
1496
|
+
const challenge = {
|
|
1497
|
+
id: challengeId,
|
|
1498
|
+
realm,
|
|
1499
|
+
method: "tempo",
|
|
1500
|
+
intent: "charge",
|
|
1501
|
+
request: paymentRequest
|
|
1502
|
+
};
|
|
1503
|
+
const credential = {
|
|
1504
|
+
challenge,
|
|
1505
|
+
payload: { hash: txHash, type: "hash" },
|
|
1506
|
+
source: `did:pkh:eip155:${chainId}:${account.address}`
|
|
1507
|
+
};
|
|
1508
|
+
const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1509
|
+
console.log(`[MoltsPay] Retrying with payment credential...`);
|
|
1510
|
+
const paidResponse = await fetch(url, {
|
|
1511
|
+
method: "POST",
|
|
1512
|
+
headers: {
|
|
1513
|
+
"Content-Type": "application/json",
|
|
1514
|
+
"Authorization": `Payment ${credentialB64}`,
|
|
1515
|
+
...options.headers
|
|
1516
|
+
},
|
|
1517
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
1518
|
+
});
|
|
1519
|
+
if (!paidResponse.ok) {
|
|
1520
|
+
const errorText = await paidResponse.text();
|
|
1521
|
+
throw new Error(`Payment verification failed (${paidResponse.status}): ${errorText}`);
|
|
1522
|
+
}
|
|
1523
|
+
console.log(`[MoltsPay] Payment verified! Service completed.`);
|
|
1524
|
+
return paidResponse.json();
|
|
1525
|
+
}
|
|
599
1526
|
};
|
|
600
1527
|
|
|
601
1528
|
// src/server/index.ts
|
|
602
1529
|
init_cjs_shims();
|
|
603
|
-
var
|
|
1530
|
+
var import_fs4 = require("fs");
|
|
604
1531
|
var import_http = require("http");
|
|
605
1532
|
var path2 = __toESM(require("path"));
|
|
606
1533
|
|
|
607
1534
|
// src/facilitators/index.ts
|
|
608
1535
|
init_cjs_shims();
|
|
609
1536
|
|
|
610
|
-
// src/facilitators/interface.ts
|
|
611
|
-
init_cjs_shims();
|
|
612
|
-
var BaseFacilitator = class {
|
|
613
|
-
supportsNetwork(network) {
|
|
614
|
-
return this.supportedNetworks.includes(network);
|
|
615
|
-
}
|
|
616
|
-
};
|
|
617
|
-
|
|
618
1537
|
// src/facilitators/cdp.ts
|
|
619
1538
|
init_cjs_shims();
|
|
620
|
-
var
|
|
1539
|
+
var import_fs3 = require("fs");
|
|
621
1540
|
var path = __toESM(require("path"));
|
|
622
1541
|
var X402_VERSION2 = 2;
|
|
623
1542
|
var CDP_URL = "https://api.cdp.coinbase.com/platform/v2/x402";
|
|
@@ -628,9 +1547,9 @@ function loadEnvFile() {
|
|
|
628
1547
|
path.join(process.env.HOME || "", ".moltspay", ".env")
|
|
629
1548
|
];
|
|
630
1549
|
for (const envPath of envPaths) {
|
|
631
|
-
if ((0,
|
|
1550
|
+
if ((0, import_fs3.existsSync)(envPath)) {
|
|
632
1551
|
try {
|
|
633
|
-
const content = (0,
|
|
1552
|
+
const content = (0, import_fs3.readFileSync)(envPath, "utf-8");
|
|
634
1553
|
for (const line of content.split("\n")) {
|
|
635
1554
|
const trimmed = line.trim();
|
|
636
1555
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -820,23 +1739,462 @@ var CDPFacilitator = class extends BaseFacilitator {
|
|
|
820
1739
|
};
|
|
821
1740
|
}
|
|
822
1741
|
/**
|
|
823
|
-
* Check if a chain ID is testnet
|
|
1742
|
+
* Check if a chain ID is testnet
|
|
1743
|
+
*/
|
|
1744
|
+
static isTestnet(chainId) {
|
|
1745
|
+
return TESTNET_CHAIN_IDS.includes(chainId);
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Get configuration summary (for logging)
|
|
1749
|
+
*/
|
|
1750
|
+
getConfigSummary() {
|
|
1751
|
+
const hasCredentials = !!(this.apiKeyId && this.apiKeySecret);
|
|
1752
|
+
const networks = this.supportedNetworks.join(", ");
|
|
1753
|
+
return `CDP Facilitator (networks: ${networks}, credentials: ${hasCredentials ? "yes" : "no"})`;
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
|
|
1757
|
+
// src/facilitators/tempo.ts
|
|
1758
|
+
init_cjs_shims();
|
|
1759
|
+
var TRANSFER_EVENT_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
1760
|
+
var TempoFacilitator = class extends BaseFacilitator {
|
|
1761
|
+
name = "tempo";
|
|
1762
|
+
displayName = "Tempo Testnet";
|
|
1763
|
+
supportedNetworks = ["eip155:42431"];
|
|
1764
|
+
// Tempo Moderato
|
|
1765
|
+
rpcUrl;
|
|
1766
|
+
constructor() {
|
|
1767
|
+
super();
|
|
1768
|
+
this.rpcUrl = CHAINS.tempo_moderato.rpc;
|
|
1769
|
+
}
|
|
1770
|
+
async healthCheck() {
|
|
1771
|
+
const start = Date.now();
|
|
1772
|
+
try {
|
|
1773
|
+
const response = await fetch(this.rpcUrl, {
|
|
1774
|
+
method: "POST",
|
|
1775
|
+
headers: { "Content-Type": "application/json" },
|
|
1776
|
+
body: JSON.stringify({
|
|
1777
|
+
jsonrpc: "2.0",
|
|
1778
|
+
method: "eth_chainId",
|
|
1779
|
+
params: [],
|
|
1780
|
+
id: 1
|
|
1781
|
+
})
|
|
1782
|
+
});
|
|
1783
|
+
const data = await response.json();
|
|
1784
|
+
const chainId = parseInt(data.result, 16);
|
|
1785
|
+
if (chainId !== 42431) {
|
|
1786
|
+
return { healthy: false, error: `Wrong chainId: ${chainId}` };
|
|
1787
|
+
}
|
|
1788
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
1789
|
+
} catch (error) {
|
|
1790
|
+
return { healthy: false, error: String(error) };
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
async verify(paymentPayload, requirements) {
|
|
1794
|
+
try {
|
|
1795
|
+
const tempoPayload = paymentPayload.payload;
|
|
1796
|
+
if (!tempoPayload?.txHash) {
|
|
1797
|
+
return { valid: false, error: "Missing txHash in payment payload" };
|
|
1798
|
+
}
|
|
1799
|
+
const receipt = await this.getTransactionReceipt(tempoPayload.txHash);
|
|
1800
|
+
if (!receipt) {
|
|
1801
|
+
return { valid: false, error: "Transaction not found" };
|
|
1802
|
+
}
|
|
1803
|
+
if (receipt.status !== "0x1") {
|
|
1804
|
+
return { valid: false, error: "Transaction failed" };
|
|
1805
|
+
}
|
|
1806
|
+
const transferLog = receipt.logs.find(
|
|
1807
|
+
(log) => log.topics[0] === TRANSFER_EVENT_TOPIC
|
|
1808
|
+
);
|
|
1809
|
+
if (!transferLog) {
|
|
1810
|
+
return { valid: false, error: "No Transfer event found" };
|
|
1811
|
+
}
|
|
1812
|
+
const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
|
|
1813
|
+
const expectedTo = requirements.payTo.toLowerCase();
|
|
1814
|
+
if (toAddress !== expectedTo) {
|
|
1815
|
+
return {
|
|
1816
|
+
valid: false,
|
|
1817
|
+
error: `Wrong recipient: ${toAddress}, expected ${expectedTo}`
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
const amount = BigInt(transferLog.data);
|
|
1821
|
+
const expectedAmount = BigInt(requirements.amount);
|
|
1822
|
+
if (amount < expectedAmount) {
|
|
1823
|
+
return {
|
|
1824
|
+
valid: false,
|
|
1825
|
+
error: `Insufficient amount: ${amount}, expected ${expectedAmount}`
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
const tokenAddress = transferLog.address.toLowerCase();
|
|
1829
|
+
const expectedToken = requirements.asset.toLowerCase();
|
|
1830
|
+
if (tokenAddress !== expectedToken) {
|
|
1831
|
+
return {
|
|
1832
|
+
valid: false,
|
|
1833
|
+
error: `Wrong token: ${tokenAddress}, expected ${expectedToken}`
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
return {
|
|
1837
|
+
valid: true,
|
|
1838
|
+
details: {
|
|
1839
|
+
txHash: tempoPayload.txHash,
|
|
1840
|
+
from: "0x" + transferLog.topics[1].slice(26),
|
|
1841
|
+
to: toAddress,
|
|
1842
|
+
amount: amount.toString(),
|
|
1843
|
+
token: tokenAddress
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
} catch (error) {
|
|
1847
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
async settle(paymentPayload, requirements) {
|
|
1851
|
+
const verifyResult = await this.verify(paymentPayload, requirements);
|
|
1852
|
+
if (!verifyResult.valid) {
|
|
1853
|
+
return { success: false, error: verifyResult.error };
|
|
1854
|
+
}
|
|
1855
|
+
const tempoPayload = paymentPayload.payload;
|
|
1856
|
+
return {
|
|
1857
|
+
success: true,
|
|
1858
|
+
transaction: tempoPayload.txHash,
|
|
1859
|
+
status: "settled"
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
async getTransactionReceipt(txHash) {
|
|
1863
|
+
const response = await fetch(this.rpcUrl, {
|
|
1864
|
+
method: "POST",
|
|
1865
|
+
headers: { "Content-Type": "application/json" },
|
|
1866
|
+
body: JSON.stringify({
|
|
1867
|
+
jsonrpc: "2.0",
|
|
1868
|
+
method: "eth_getTransactionReceipt",
|
|
1869
|
+
params: [txHash],
|
|
1870
|
+
id: 1
|
|
1871
|
+
})
|
|
1872
|
+
});
|
|
1873
|
+
const data = await response.json();
|
|
1874
|
+
return data.result;
|
|
1875
|
+
}
|
|
1876
|
+
};
|
|
1877
|
+
|
|
1878
|
+
// src/facilitators/bnb.ts
|
|
1879
|
+
init_cjs_shims();
|
|
1880
|
+
var import_accounts = require("viem/accounts");
|
|
1881
|
+
var TRANSFER_EVENT_TOPIC2 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
1882
|
+
var EIP712_DOMAIN = {
|
|
1883
|
+
name: "MoltsPay",
|
|
1884
|
+
version: "1"
|
|
1885
|
+
};
|
|
1886
|
+
var INTENT_TYPES = {
|
|
1887
|
+
PaymentIntent: [
|
|
1888
|
+
{ name: "from", type: "address" },
|
|
1889
|
+
{ name: "to", type: "address" },
|
|
1890
|
+
{ name: "amount", type: "uint256" },
|
|
1891
|
+
{ name: "token", type: "address" },
|
|
1892
|
+
{ name: "service", type: "string" },
|
|
1893
|
+
{ name: "nonce", type: "uint256" },
|
|
1894
|
+
{ name: "deadline", type: "uint256" }
|
|
1895
|
+
]
|
|
1896
|
+
};
|
|
1897
|
+
var BNBFacilitator = class extends BaseFacilitator {
|
|
1898
|
+
name = "bnb";
|
|
1899
|
+
displayName = "BNB Smart Chain";
|
|
1900
|
+
supportedNetworks = ["eip155:56", "eip155:97"];
|
|
1901
|
+
// Mainnet + Testnet
|
|
1902
|
+
serverPrivateKey;
|
|
1903
|
+
spenderAddress = null;
|
|
1904
|
+
chainConfigs;
|
|
1905
|
+
constructor(serverPrivateKey) {
|
|
1906
|
+
super();
|
|
1907
|
+
this.serverPrivateKey = serverPrivateKey || process.env.BNB_SERVER_PRIVATE_KEY || "";
|
|
1908
|
+
if (this.serverPrivateKey) {
|
|
1909
|
+
const key = this.serverPrivateKey.startsWith("0x") ? this.serverPrivateKey : `0x${this.serverPrivateKey}`;
|
|
1910
|
+
const account = (0, import_accounts.privateKeyToAccount)(key);
|
|
1911
|
+
this.spenderAddress = account.address;
|
|
1912
|
+
}
|
|
1913
|
+
this.chainConfigs = {
|
|
1914
|
+
56: { rpc: CHAINS.bnb.rpc, chain: CHAINS.bnb },
|
|
1915
|
+
97: { rpc: CHAINS.bnb_testnet.rpc, chain: CHAINS.bnb_testnet }
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
async healthCheck() {
|
|
1919
|
+
const start = Date.now();
|
|
1920
|
+
try {
|
|
1921
|
+
const response = await fetch(this.chainConfigs[56].rpc, {
|
|
1922
|
+
method: "POST",
|
|
1923
|
+
headers: { "Content-Type": "application/json" },
|
|
1924
|
+
body: JSON.stringify({
|
|
1925
|
+
jsonrpc: "2.0",
|
|
1926
|
+
method: "eth_chainId",
|
|
1927
|
+
params: [],
|
|
1928
|
+
id: 1
|
|
1929
|
+
})
|
|
1930
|
+
});
|
|
1931
|
+
const data = await response.json();
|
|
1932
|
+
const chainId = parseInt(data.result, 16);
|
|
1933
|
+
if (chainId !== 56) {
|
|
1934
|
+
return { healthy: false, error: `Wrong chainId: ${chainId}` };
|
|
1935
|
+
}
|
|
1936
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
1937
|
+
} catch (error) {
|
|
1938
|
+
return { healthy: false, error: String(error) };
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Verify a payment intent signature (before service execution)
|
|
1943
|
+
*
|
|
1944
|
+
* This verifies:
|
|
1945
|
+
* 1. Signature is valid for the intent
|
|
1946
|
+
* 2. Client has approved server wallet
|
|
1947
|
+
* 3. Client has sufficient balance
|
|
1948
|
+
* 4. Intent hasn't expired
|
|
1949
|
+
*/
|
|
1950
|
+
async verify(paymentPayload, requirements) {
|
|
1951
|
+
try {
|
|
1952
|
+
const bnbPayload = paymentPayload.payload;
|
|
1953
|
+
if (!bnbPayload?.intent) {
|
|
1954
|
+
return { valid: false, error: "Missing intent in payment payload" };
|
|
1955
|
+
}
|
|
1956
|
+
const { intent, chainId } = bnbPayload;
|
|
1957
|
+
const config = this.chainConfigs[chainId];
|
|
1958
|
+
if (!config) {
|
|
1959
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
1960
|
+
}
|
|
1961
|
+
if (intent.deadline < Date.now()) {
|
|
1962
|
+
return { valid: false, error: "Intent expired" };
|
|
1963
|
+
}
|
|
1964
|
+
const recoveredAddress = await this.recoverIntentSigner(intent, chainId);
|
|
1965
|
+
if (recoveredAddress.toLowerCase() !== intent.from.toLowerCase()) {
|
|
1966
|
+
return { valid: false, error: "Invalid signature" };
|
|
1967
|
+
}
|
|
1968
|
+
if (intent.to.toLowerCase() !== requirements.payTo.toLowerCase()) {
|
|
1969
|
+
return { valid: false, error: `Wrong recipient: ${intent.to}` };
|
|
1970
|
+
}
|
|
1971
|
+
if (BigInt(intent.amount) < BigInt(requirements.amount)) {
|
|
1972
|
+
return { valid: false, error: `Insufficient amount: ${intent.amount}` };
|
|
1973
|
+
}
|
|
1974
|
+
if (intent.token.toLowerCase() !== requirements.asset.toLowerCase()) {
|
|
1975
|
+
return { valid: false, error: `Wrong token: ${intent.token}` };
|
|
1976
|
+
}
|
|
1977
|
+
const serverAddress = await this.getServerAddress();
|
|
1978
|
+
const allowance = await this.getAllowance(intent.from, serverAddress, intent.token, config.rpc);
|
|
1979
|
+
if (BigInt(allowance) < BigInt(intent.amount)) {
|
|
1980
|
+
return { valid: false, error: "Insufficient allowance. Run: npx moltspay init --chain bnb" };
|
|
1981
|
+
}
|
|
1982
|
+
const balance = await this.getBalance(intent.from, intent.token, config.rpc);
|
|
1983
|
+
if (BigInt(balance) < BigInt(intent.amount)) {
|
|
1984
|
+
return { valid: false, error: "Insufficient balance" };
|
|
1985
|
+
}
|
|
1986
|
+
return {
|
|
1987
|
+
valid: true,
|
|
1988
|
+
details: {
|
|
1989
|
+
from: intent.from,
|
|
1990
|
+
to: intent.to,
|
|
1991
|
+
amount: intent.amount,
|
|
1992
|
+
token: intent.token,
|
|
1993
|
+
service: intent.service,
|
|
1994
|
+
nonce: intent.nonce,
|
|
1995
|
+
deadline: intent.deadline
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
} catch (error) {
|
|
1999
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Settle a payment by executing transferFrom
|
|
2004
|
+
*
|
|
2005
|
+
* This is called AFTER the service has been successfully delivered.
|
|
2006
|
+
* Server pays gas, transfers tokens from client to provider.
|
|
2007
|
+
*/
|
|
2008
|
+
async settle(paymentPayload, requirements) {
|
|
2009
|
+
if (!this.serverPrivateKey) {
|
|
2010
|
+
return { success: false, error: "Server wallet not configured (BNB_SERVER_PRIVATE_KEY)" };
|
|
2011
|
+
}
|
|
2012
|
+
try {
|
|
2013
|
+
const verifyResult = await this.verify(paymentPayload, requirements);
|
|
2014
|
+
if (!verifyResult.valid) {
|
|
2015
|
+
return { success: false, error: verifyResult.error };
|
|
2016
|
+
}
|
|
2017
|
+
const bnbPayload = paymentPayload.payload;
|
|
2018
|
+
const { intent, chainId } = bnbPayload;
|
|
2019
|
+
const config = this.chainConfigs[chainId];
|
|
2020
|
+
const txHash = await this.executeTransferFrom(
|
|
2021
|
+
intent.from,
|
|
2022
|
+
intent.to,
|
|
2023
|
+
intent.amount,
|
|
2024
|
+
intent.token,
|
|
2025
|
+
config.rpc
|
|
2026
|
+
);
|
|
2027
|
+
return {
|
|
2028
|
+
success: true,
|
|
2029
|
+
transaction: txHash,
|
|
2030
|
+
status: "settled"
|
|
2031
|
+
};
|
|
2032
|
+
} catch (error) {
|
|
2033
|
+
return { success: false, error: `Settlement failed: ${error}` };
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
/**
|
|
2037
|
+
* Check if client has approved the server wallet
|
|
2038
|
+
*/
|
|
2039
|
+
async checkApproval(clientAddress, token, chainId) {
|
|
2040
|
+
const config = this.chainConfigs[chainId];
|
|
2041
|
+
if (!config) {
|
|
2042
|
+
throw new Error(`Unsupported chainId: ${chainId}`);
|
|
2043
|
+
}
|
|
2044
|
+
const serverAddress = await this.getServerAddress();
|
|
2045
|
+
const allowance = await this.getAllowance(clientAddress, serverAddress, token, config.rpc);
|
|
2046
|
+
const minAllowance = BigInt("1000000000000000000000");
|
|
2047
|
+
return {
|
|
2048
|
+
approved: BigInt(allowance) >= minAllowance,
|
|
2049
|
+
allowance
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Verify a completed transaction (for checking past payments)
|
|
2054
|
+
*/
|
|
2055
|
+
async verifyTransaction(txHash, expected, chainId) {
|
|
2056
|
+
const config = this.chainConfigs[chainId];
|
|
2057
|
+
if (!config) {
|
|
2058
|
+
return { valid: false, error: `Unsupported chainId: ${chainId}` };
|
|
2059
|
+
}
|
|
2060
|
+
try {
|
|
2061
|
+
const receipt = await this.getTransactionReceipt(txHash, config.rpc);
|
|
2062
|
+
if (!receipt) {
|
|
2063
|
+
return { valid: false, error: "Transaction not found" };
|
|
2064
|
+
}
|
|
2065
|
+
if (receipt.status !== "0x1") {
|
|
2066
|
+
return { valid: false, error: "Transaction failed" };
|
|
2067
|
+
}
|
|
2068
|
+
const transferLog = receipt.logs.find(
|
|
2069
|
+
(log) => log.topics[0] === TRANSFER_EVENT_TOPIC2 && log.address.toLowerCase() === expected.token.toLowerCase()
|
|
2070
|
+
);
|
|
2071
|
+
if (!transferLog) {
|
|
2072
|
+
return { valid: false, error: "No Transfer event found" };
|
|
2073
|
+
}
|
|
2074
|
+
const toAddress = "0x" + transferLog.topics[2].slice(26).toLowerCase();
|
|
2075
|
+
if (toAddress !== expected.to.toLowerCase()) {
|
|
2076
|
+
return { valid: false, error: `Wrong recipient: ${toAddress}` };
|
|
2077
|
+
}
|
|
2078
|
+
const amount = BigInt(transferLog.data);
|
|
2079
|
+
if (amount < BigInt(expected.amount)) {
|
|
2080
|
+
return { valid: false, error: `Insufficient amount: ${amount}` };
|
|
2081
|
+
}
|
|
2082
|
+
return {
|
|
2083
|
+
valid: true,
|
|
2084
|
+
details: {
|
|
2085
|
+
txHash,
|
|
2086
|
+
from: "0x" + transferLog.topics[1].slice(26),
|
|
2087
|
+
to: toAddress,
|
|
2088
|
+
amount: amount.toString(),
|
|
2089
|
+
token: transferLog.address
|
|
2090
|
+
}
|
|
2091
|
+
};
|
|
2092
|
+
} catch (error) {
|
|
2093
|
+
return { valid: false, error: `Verification failed: ${error}` };
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
// ==================== Private Methods ====================
|
|
2097
|
+
/**
|
|
2098
|
+
* Get the server's spender address (public, for 402 responses)
|
|
2099
|
+
* Returns cached value computed at construction time.
|
|
824
2100
|
*/
|
|
825
|
-
|
|
826
|
-
return
|
|
2101
|
+
getSpenderAddress() {
|
|
2102
|
+
return this.spenderAddress;
|
|
827
2103
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
2104
|
+
async getServerAddress() {
|
|
2105
|
+
const { ethers: ethers3 } = await import("ethers");
|
|
2106
|
+
const wallet = new ethers3.Wallet(this.serverPrivateKey);
|
|
2107
|
+
return wallet.address;
|
|
2108
|
+
}
|
|
2109
|
+
async recoverIntentSigner(intent, chainId) {
|
|
2110
|
+
const { ethers: ethers3 } = await import("ethers");
|
|
2111
|
+
const domain = {
|
|
2112
|
+
...EIP712_DOMAIN,
|
|
2113
|
+
chainId
|
|
2114
|
+
};
|
|
2115
|
+
const message = {
|
|
2116
|
+
from: intent.from,
|
|
2117
|
+
to: intent.to,
|
|
2118
|
+
amount: intent.amount,
|
|
2119
|
+
token: intent.token,
|
|
2120
|
+
service: intent.service,
|
|
2121
|
+
nonce: intent.nonce,
|
|
2122
|
+
deadline: intent.deadline
|
|
2123
|
+
};
|
|
2124
|
+
const recoveredAddress = ethers3.verifyTypedData(
|
|
2125
|
+
domain,
|
|
2126
|
+
INTENT_TYPES,
|
|
2127
|
+
message,
|
|
2128
|
+
intent.signature
|
|
2129
|
+
);
|
|
2130
|
+
return recoveredAddress;
|
|
2131
|
+
}
|
|
2132
|
+
async getAllowance(owner, spender, token, rpcUrl) {
|
|
2133
|
+
const selector = "0xdd62ed3e";
|
|
2134
|
+
const ownerPadded = owner.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
2135
|
+
const spenderPadded = spender.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
2136
|
+
const data = selector + ownerPadded + spenderPadded;
|
|
2137
|
+
const response = await fetch(rpcUrl, {
|
|
2138
|
+
method: "POST",
|
|
2139
|
+
headers: { "Content-Type": "application/json" },
|
|
2140
|
+
body: JSON.stringify({
|
|
2141
|
+
jsonrpc: "2.0",
|
|
2142
|
+
method: "eth_call",
|
|
2143
|
+
params: [{ to: token, data }, "latest"],
|
|
2144
|
+
id: 1
|
|
2145
|
+
})
|
|
2146
|
+
});
|
|
2147
|
+
const result = await response.json();
|
|
2148
|
+
return result.result || "0x0";
|
|
2149
|
+
}
|
|
2150
|
+
async getBalance(account, token, rpcUrl) {
|
|
2151
|
+
const selector = "0x70a08231";
|
|
2152
|
+
const accountPadded = account.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
2153
|
+
const data = selector + accountPadded;
|
|
2154
|
+
const response = await fetch(rpcUrl, {
|
|
2155
|
+
method: "POST",
|
|
2156
|
+
headers: { "Content-Type": "application/json" },
|
|
2157
|
+
body: JSON.stringify({
|
|
2158
|
+
jsonrpc: "2.0",
|
|
2159
|
+
method: "eth_call",
|
|
2160
|
+
params: [{ to: token, data }, "latest"],
|
|
2161
|
+
id: 1
|
|
2162
|
+
})
|
|
2163
|
+
});
|
|
2164
|
+
const result = await response.json();
|
|
2165
|
+
return result.result || "0x0";
|
|
2166
|
+
}
|
|
2167
|
+
async executeTransferFrom(from, to, amount, token, rpcUrl) {
|
|
2168
|
+
const { ethers: ethers3 } = await import("ethers");
|
|
2169
|
+
const provider = new ethers3.JsonRpcProvider(rpcUrl);
|
|
2170
|
+
const wallet = new ethers3.Wallet(this.serverPrivateKey, provider);
|
|
2171
|
+
const tokenContract = new ethers3.Contract(token, [
|
|
2172
|
+
"function transferFrom(address from, address to, uint256 amount) returns (bool)"
|
|
2173
|
+
], wallet);
|
|
2174
|
+
const tx = await tokenContract.transferFrom(from, to, amount);
|
|
2175
|
+
const receipt = await tx.wait();
|
|
2176
|
+
return receipt.hash;
|
|
2177
|
+
}
|
|
2178
|
+
async getTransactionReceipt(txHash, rpcUrl) {
|
|
2179
|
+
const response = await fetch(rpcUrl, {
|
|
2180
|
+
method: "POST",
|
|
2181
|
+
headers: { "Content-Type": "application/json" },
|
|
2182
|
+
body: JSON.stringify({
|
|
2183
|
+
jsonrpc: "2.0",
|
|
2184
|
+
method: "eth_getTransactionReceipt",
|
|
2185
|
+
params: [txHash],
|
|
2186
|
+
id: 1
|
|
2187
|
+
})
|
|
2188
|
+
});
|
|
2189
|
+
const data = await response.json();
|
|
2190
|
+
return data.result;
|
|
835
2191
|
}
|
|
836
2192
|
};
|
|
837
2193
|
|
|
838
2194
|
// src/facilitators/registry.ts
|
|
839
2195
|
init_cjs_shims();
|
|
2196
|
+
var import_web35 = require("@solana/web3.js");
|
|
2197
|
+
var import_bs582 = __toESM(require("bs58"));
|
|
840
2198
|
var FacilitatorRegistry = class {
|
|
841
2199
|
factories = /* @__PURE__ */ new Map();
|
|
842
2200
|
instances = /* @__PURE__ */ new Map();
|
|
@@ -844,7 +2202,21 @@ var FacilitatorRegistry = class {
|
|
|
844
2202
|
roundRobinIndex = 0;
|
|
845
2203
|
constructor(selection) {
|
|
846
2204
|
this.registerFactory("cdp", (config) => new CDPFacilitator(config));
|
|
847
|
-
this.
|
|
2205
|
+
this.registerFactory("tempo", () => new TempoFacilitator());
|
|
2206
|
+
this.registerFactory("bnb", (config) => new BNBFacilitator(config?.serverPrivateKey));
|
|
2207
|
+
this.registerFactory("solana", (config) => {
|
|
2208
|
+
let feePayerKeypair;
|
|
2209
|
+
const feePayerKey = config?.feePayerPrivateKey || process.env.SOLANA_FEE_PAYER_KEY;
|
|
2210
|
+
if (feePayerKey) {
|
|
2211
|
+
try {
|
|
2212
|
+
feePayerKeypair = import_web35.Keypair.fromSecretKey(import_bs582.default.decode(feePayerKey));
|
|
2213
|
+
} catch (e) {
|
|
2214
|
+
console.warn(`[SolanaFacilitator] Invalid fee payer key: ${e.message}`);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
return new SolanaFacilitator({ feePayerKeypair });
|
|
2218
|
+
});
|
|
2219
|
+
this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
|
|
848
2220
|
}
|
|
849
2221
|
/**
|
|
850
2222
|
* Register a new facilitator factory
|
|
@@ -1068,6 +2440,9 @@ var X402_VERSION3 = 2;
|
|
|
1068
2440
|
var PAYMENT_REQUIRED_HEADER2 = "x-payment-required";
|
|
1069
2441
|
var PAYMENT_HEADER2 = "x-payment";
|
|
1070
2442
|
var PAYMENT_RESPONSE_HEADER = "x-payment-response";
|
|
2443
|
+
var MPP_AUTH_HEADER = "authorization";
|
|
2444
|
+
var MPP_WWW_AUTH_HEADER = "www-authenticate";
|
|
2445
|
+
var MPP_RECEIPT_HEADER = "payment-receipt";
|
|
1071
2446
|
var TOKEN_ADDRESSES = {
|
|
1072
2447
|
"eip155:8453": {
|
|
1073
2448
|
USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
@@ -1081,13 +2456,47 @@ var TOKEN_ADDRESSES = {
|
|
|
1081
2456
|
"eip155:137": {
|
|
1082
2457
|
USDC: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
1083
2458
|
USDT: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F"
|
|
2459
|
+
},
|
|
2460
|
+
"eip155:42431": {
|
|
2461
|
+
// Tempo Moderato testnet - TIP-20 stablecoins
|
|
2462
|
+
USDC: "0x20c0000000000000000000000000000000000000",
|
|
2463
|
+
// pathUSD
|
|
2464
|
+
USDT: "0x20c0000000000000000000000000000000000001"
|
|
2465
|
+
// alphaUSD
|
|
2466
|
+
},
|
|
2467
|
+
// BNB Smart Chain mainnet
|
|
2468
|
+
"eip155:56": {
|
|
2469
|
+
USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
2470
|
+
USDT: "0x55d398326f99059fF775485246999027B3197955"
|
|
2471
|
+
},
|
|
2472
|
+
// BNB Smart Chain testnet
|
|
2473
|
+
"eip155:97": {
|
|
2474
|
+
USDC: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
2475
|
+
USDT: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd"
|
|
2476
|
+
},
|
|
2477
|
+
// Solana networks use mint addresses (SPL tokens)
|
|
2478
|
+
"solana:mainnet": {
|
|
2479
|
+
USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
|
2480
|
+
// Circle USDC
|
|
2481
|
+
},
|
|
2482
|
+
"solana:devnet": {
|
|
2483
|
+
USDC: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"
|
|
2484
|
+
// Devnet USDC
|
|
1084
2485
|
}
|
|
1085
2486
|
};
|
|
1086
2487
|
var CHAIN_TO_NETWORK = {
|
|
1087
2488
|
"base": "eip155:8453",
|
|
1088
2489
|
"base_sepolia": "eip155:84532",
|
|
1089
|
-
"polygon": "eip155:137"
|
|
2490
|
+
"polygon": "eip155:137",
|
|
2491
|
+
"tempo_moderato": "eip155:42431",
|
|
2492
|
+
"bnb": "eip155:56",
|
|
2493
|
+
"bnb_testnet": "eip155:97",
|
|
2494
|
+
"solana": "solana:mainnet",
|
|
2495
|
+
"solana_devnet": "solana:devnet"
|
|
1090
2496
|
};
|
|
2497
|
+
function isSolanaNetwork(network) {
|
|
2498
|
+
return network.startsWith("solana:");
|
|
2499
|
+
}
|
|
1091
2500
|
var TOKEN_DOMAINS = {
|
|
1092
2501
|
// Base mainnet
|
|
1093
2502
|
"eip155:8453": {
|
|
@@ -1104,6 +2513,21 @@ var TOKEN_DOMAINS = {
|
|
|
1104
2513
|
"eip155:137": {
|
|
1105
2514
|
USDC: { name: "USD Coin", version: "2" },
|
|
1106
2515
|
USDT: { name: "(PoS) Tether USD", version: "2" }
|
|
2516
|
+
},
|
|
2517
|
+
// Tempo Moderato testnet - TIP-20 stablecoins
|
|
2518
|
+
"eip155:42431": {
|
|
2519
|
+
USDC: { name: "pathUSD", version: "1" },
|
|
2520
|
+
USDT: { name: "alphaUSD", version: "1" }
|
|
2521
|
+
},
|
|
2522
|
+
// BNB Smart Chain mainnet
|
|
2523
|
+
"eip155:56": {
|
|
2524
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
2525
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
2526
|
+
},
|
|
2527
|
+
// BNB Smart Chain testnet
|
|
2528
|
+
"eip155:97": {
|
|
2529
|
+
USDC: { name: "USD Coin", version: "1" },
|
|
2530
|
+
USDT: { name: "Tether USD", version: "1" }
|
|
1107
2531
|
}
|
|
1108
2532
|
};
|
|
1109
2533
|
function getTokenDomain(network, token) {
|
|
@@ -1119,9 +2543,9 @@ function loadEnvFile2() {
|
|
|
1119
2543
|
path2.join(process.env.HOME || "", ".moltspay", ".env")
|
|
1120
2544
|
];
|
|
1121
2545
|
for (const envPath of envPaths) {
|
|
1122
|
-
if ((0,
|
|
2546
|
+
if ((0, import_fs4.existsSync)(envPath)) {
|
|
1123
2547
|
try {
|
|
1124
|
-
const content = (0,
|
|
2548
|
+
const content = (0, import_fs4.readFileSync)(envPath, "utf-8");
|
|
1125
2549
|
for (const line of content.split("\n")) {
|
|
1126
2550
|
const trimmed = line.trim();
|
|
1127
2551
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -1152,7 +2576,7 @@ var MoltsPayServer = class {
|
|
|
1152
2576
|
useMainnet;
|
|
1153
2577
|
constructor(servicesPath, options = {}) {
|
|
1154
2578
|
loadEnvFile2();
|
|
1155
|
-
const content = (0,
|
|
2579
|
+
const content = (0, import_fs4.readFileSync)(servicesPath, "utf-8");
|
|
1156
2580
|
this.manifest = JSON.parse(content);
|
|
1157
2581
|
this.options = {
|
|
1158
2582
|
port: options.port || 3e3,
|
|
@@ -1161,9 +2585,11 @@ var MoltsPayServer = class {
|
|
|
1161
2585
|
};
|
|
1162
2586
|
this.useMainnet = process.env.USE_MAINNET?.toLowerCase() === "true";
|
|
1163
2587
|
this.networkId = this.useMainnet ? "eip155:8453" : "eip155:84532";
|
|
2588
|
+
const defaultFallback = ["tempo", "bnb", "solana"];
|
|
2589
|
+
const envFallback = process.env.FACILITATOR_FALLBACK?.split(",").filter(Boolean);
|
|
1164
2590
|
const facilitatorConfig = options.facilitators || {
|
|
1165
2591
|
primary: process.env.FACILITATOR_PRIMARY || "cdp",
|
|
1166
|
-
fallback:
|
|
2592
|
+
fallback: envFallback || defaultFallback,
|
|
1167
2593
|
strategy: process.env.FACILITATOR_STRATEGY || "failover",
|
|
1168
2594
|
config: {
|
|
1169
2595
|
cdp: { useMainnet: this.useMainnet }
|
|
@@ -1202,12 +2628,20 @@ var MoltsPayServer = class {
|
|
|
1202
2628
|
*/
|
|
1203
2629
|
getProviderChains() {
|
|
1204
2630
|
const provider = this.manifest.provider;
|
|
2631
|
+
const getWalletForChain = (chainName, explicitWallet) => {
|
|
2632
|
+
if (explicitWallet) return explicitWallet;
|
|
2633
|
+
if ((chainName === "solana" || chainName === "solana_devnet") && provider.solana_wallet) {
|
|
2634
|
+
return provider.solana_wallet;
|
|
2635
|
+
}
|
|
2636
|
+
return provider.wallet;
|
|
2637
|
+
};
|
|
1205
2638
|
if (provider.chains && provider.chains.length > 0) {
|
|
1206
2639
|
return provider.chains.map((c) => {
|
|
1207
2640
|
const chainName = typeof c === "string" ? c : c.chain;
|
|
2641
|
+
const explicitWallet = typeof c === "object" ? c.wallet : null;
|
|
1208
2642
|
return {
|
|
1209
2643
|
network: CHAIN_TO_NETWORK[chainName] || "eip155:8453",
|
|
1210
|
-
wallet: (
|
|
2644
|
+
wallet: getWalletForChain(chainName, explicitWallet || void 0),
|
|
1211
2645
|
tokens: (typeof c === "object" ? c.tokens : null) || ["USDC"]
|
|
1212
2646
|
};
|
|
1213
2647
|
});
|
|
@@ -1216,7 +2650,7 @@ var MoltsPayServer = class {
|
|
|
1216
2650
|
const network = CHAIN_TO_NETWORK[chain] || this.networkId;
|
|
1217
2651
|
return [{
|
|
1218
2652
|
network,
|
|
1219
|
-
wallet:
|
|
2653
|
+
wallet: getWalletForChain(chain),
|
|
1220
2654
|
tokens: ["USDC"]
|
|
1221
2655
|
}];
|
|
1222
2656
|
}
|
|
@@ -1257,8 +2691,8 @@ var MoltsPayServer = class {
|
|
|
1257
2691
|
async handleRequest(req, res) {
|
|
1258
2692
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1259
2693
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1260
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment");
|
|
1261
|
-
res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response");
|
|
2694
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Payment, Authorization");
|
|
2695
|
+
res.setHeader("Access-Control-Expose-Headers", "X-Payment-Required, X-Payment-Response, WWW-Authenticate, Payment-Receipt");
|
|
1262
2696
|
if (req.method === "OPTIONS") {
|
|
1263
2697
|
res.writeHead(204);
|
|
1264
2698
|
res.end();
|
|
@@ -1287,7 +2721,16 @@ var MoltsPayServer = class {
|
|
|
1287
2721
|
}
|
|
1288
2722
|
const body = await this.readBody(req);
|
|
1289
2723
|
const paymentHeader = req.headers[PAYMENT_HEADER2];
|
|
1290
|
-
|
|
2724
|
+
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
2725
|
+
return await this.handleProxy(body, paymentHeader, authHeader, res);
|
|
2726
|
+
}
|
|
2727
|
+
const servicePath = url.pathname.replace(/^\//, "");
|
|
2728
|
+
const skill = this.skills.get(servicePath);
|
|
2729
|
+
if (skill && (req.method === "POST" || req.method === "GET")) {
|
|
2730
|
+
const body = req.method === "POST" ? await this.readBody(req) : {};
|
|
2731
|
+
const authHeader = req.headers[MPP_AUTH_HEADER];
|
|
2732
|
+
const x402Header = req.headers[PAYMENT_HEADER2];
|
|
2733
|
+
return await this.handleMPPRequest(skill, body, authHeader, x402Header, res);
|
|
1291
2734
|
}
|
|
1292
2735
|
this.sendJson(res, 404, { error: "Not found" });
|
|
1293
2736
|
} catch (err) {
|
|
@@ -1316,7 +2759,9 @@ var MoltsPayServer = class {
|
|
|
1316
2759
|
name: this.manifest.provider.name,
|
|
1317
2760
|
description: this.manifest.provider.description,
|
|
1318
2761
|
wallet: this.manifest.provider.wallet,
|
|
1319
|
-
chain: this.manifest.provider.chain || "base"
|
|
2762
|
+
chain: this.manifest.provider.chain || "base",
|
|
2763
|
+
solana_wallet: this.manifest.provider.solana_wallet,
|
|
2764
|
+
chains: this.manifest.provider.chains
|
|
1320
2765
|
},
|
|
1321
2766
|
services,
|
|
1322
2767
|
endpoints: {
|
|
@@ -1429,6 +2874,21 @@ var MoltsPayServer = class {
|
|
|
1429
2874
|
});
|
|
1430
2875
|
}
|
|
1431
2876
|
console.log(`[MoltsPay] Verified by ${verifyResult.facilitator}`);
|
|
2877
|
+
const isSolana = isSolanaNetwork(paymentNetwork);
|
|
2878
|
+
let settlement = null;
|
|
2879
|
+
if (isSolana) {
|
|
2880
|
+
console.log(`[MoltsPay] Solana detected - settling payment FIRST (blockhash expiry protection)`);
|
|
2881
|
+
try {
|
|
2882
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
2883
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
2884
|
+
} catch (err) {
|
|
2885
|
+
console.error("[MoltsPay] Solana settlement failed:", err.message);
|
|
2886
|
+
return this.sendJson(res, 402, {
|
|
2887
|
+
error: "Payment settlement failed",
|
|
2888
|
+
message: err.message
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
1432
2892
|
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
1433
2893
|
console.log(`[MoltsPay] Executing skill: ${service} (timeout: ${timeoutSeconds}s)`);
|
|
1434
2894
|
let result;
|
|
@@ -1443,16 +2903,19 @@ var MoltsPayServer = class {
|
|
|
1443
2903
|
console.error("[MoltsPay] Skill execution failed:", err.message);
|
|
1444
2904
|
return this.sendJson(res, 500, {
|
|
1445
2905
|
error: "Service execution failed",
|
|
1446
|
-
message: err.message
|
|
2906
|
+
message: err.message,
|
|
2907
|
+
paymentSettled: isSolana ? true : false,
|
|
2908
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
1447
2909
|
});
|
|
1448
2910
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
2911
|
+
if (!isSolana) {
|
|
2912
|
+
console.log(`[MoltsPay] Skill succeeded, settling payment...`);
|
|
2913
|
+
try {
|
|
2914
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
2915
|
+
console.log(`[MoltsPay] Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
2916
|
+
} catch (err) {
|
|
2917
|
+
console.error("[MoltsPay] Settlement failed:", err.message);
|
|
2918
|
+
}
|
|
1456
2919
|
}
|
|
1457
2920
|
const responseHeaders = {};
|
|
1458
2921
|
if (settlement?.success) {
|
|
@@ -1472,6 +2935,187 @@ var MoltsPayServer = class {
|
|
|
1472
2935
|
payment: settlement?.success ? { transaction: settlement.transaction, status: "settled", facilitator: settlement.facilitator } : { status: "pending" }
|
|
1473
2936
|
}, responseHeaders);
|
|
1474
2937
|
}
|
|
2938
|
+
/**
|
|
2939
|
+
* Handle MPP (Machine Payments Protocol) request
|
|
2940
|
+
* Supports both x402 and MPP protocols on service endpoints
|
|
2941
|
+
*/
|
|
2942
|
+
async handleMPPRequest(skill, body, authHeader, x402Header, res) {
|
|
2943
|
+
const config = skill.config;
|
|
2944
|
+
const params = body || {};
|
|
2945
|
+
if (x402Header) {
|
|
2946
|
+
return await this.handleExecute({ service: config.id, params }, x402Header, res);
|
|
2947
|
+
}
|
|
2948
|
+
if (authHeader && authHeader.toLowerCase().startsWith("payment ")) {
|
|
2949
|
+
return await this.handleMPPPayment(skill, params, authHeader, res);
|
|
2950
|
+
}
|
|
2951
|
+
return this.sendMPPPaymentRequired(config, res);
|
|
2952
|
+
}
|
|
2953
|
+
/**
|
|
2954
|
+
* Handle MPP payment verification and service execution
|
|
2955
|
+
*/
|
|
2956
|
+
async handleMPPPayment(skill, params, authHeader, res) {
|
|
2957
|
+
const config = skill.config;
|
|
2958
|
+
const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
|
|
2959
|
+
if (!credentialMatch) {
|
|
2960
|
+
return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
|
|
2961
|
+
}
|
|
2962
|
+
let mppCredential;
|
|
2963
|
+
try {
|
|
2964
|
+
const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
2965
|
+
const decoded = Buffer.from(base64, "base64").toString("utf-8");
|
|
2966
|
+
mppCredential = JSON.parse(decoded);
|
|
2967
|
+
} catch (err) {
|
|
2968
|
+
console.error("[MoltsPay] Failed to parse MPP credential:", err);
|
|
2969
|
+
return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
|
|
2970
|
+
}
|
|
2971
|
+
let txHash;
|
|
2972
|
+
if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
|
|
2973
|
+
txHash = mppCredential.payload.hash;
|
|
2974
|
+
} else if (mppCredential.payload?.type === "transaction") {
|
|
2975
|
+
return this.sendJson(res, 400, {
|
|
2976
|
+
error: "Transaction type not supported. Please use push mode (hash type)."
|
|
2977
|
+
});
|
|
2978
|
+
}
|
|
2979
|
+
if (!txHash) {
|
|
2980
|
+
return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
|
|
2981
|
+
}
|
|
2982
|
+
let chainId = mppCredential.challenge?.request?.methodDetails?.chainId;
|
|
2983
|
+
if (!chainId && mppCredential.source) {
|
|
2984
|
+
const chainMatch = mppCredential.source.match(/eip155:(\d+)/);
|
|
2985
|
+
if (chainMatch) chainId = parseInt(chainMatch[1], 10);
|
|
2986
|
+
}
|
|
2987
|
+
chainId = chainId || 42431;
|
|
2988
|
+
const network = `eip155:${chainId}`;
|
|
2989
|
+
if (!this.isNetworkAccepted(network)) {
|
|
2990
|
+
return this.sendJson(res, 402, {
|
|
2991
|
+
error: `Network not accepted: ${network}`
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
const requirements = this.buildPaymentRequirements(
|
|
2995
|
+
config,
|
|
2996
|
+
network,
|
|
2997
|
+
this.getWalletForNetwork(network),
|
|
2998
|
+
"USDC"
|
|
2999
|
+
);
|
|
3000
|
+
const paymentPayload = {
|
|
3001
|
+
x402Version: X402_VERSION3,
|
|
3002
|
+
scheme: "exact",
|
|
3003
|
+
network,
|
|
3004
|
+
payload: {
|
|
3005
|
+
txHash,
|
|
3006
|
+
chainId
|
|
3007
|
+
}
|
|
3008
|
+
};
|
|
3009
|
+
console.log(`[MoltsPay] Verifying MPP payment: txHash=${txHash}, chainId=${chainId}`);
|
|
3010
|
+
const verification = await this.registry.verify(paymentPayload, requirements);
|
|
3011
|
+
if (!verification.valid) {
|
|
3012
|
+
return this.sendJson(res, 402, {
|
|
3013
|
+
error: `Payment verification failed: ${verification.error}`
|
|
3014
|
+
});
|
|
3015
|
+
}
|
|
3016
|
+
console.log(`[MoltsPay] Payment verified! Executing service: ${config.id}`);
|
|
3017
|
+
let result;
|
|
3018
|
+
try {
|
|
3019
|
+
result = await skill.handler(params);
|
|
3020
|
+
} catch (err) {
|
|
3021
|
+
console.error(`[MoltsPay] Skill execution error:`, err);
|
|
3022
|
+
return this.sendJson(res, 500, {
|
|
3023
|
+
error: `Service execution failed: ${err.message}`
|
|
3024
|
+
});
|
|
3025
|
+
}
|
|
3026
|
+
const receipt = {
|
|
3027
|
+
success: true,
|
|
3028
|
+
txHash,
|
|
3029
|
+
network,
|
|
3030
|
+
facilitator: verification.facilitator
|
|
3031
|
+
};
|
|
3032
|
+
const receiptEncoded = Buffer.from(JSON.stringify(receipt)).toString("base64");
|
|
3033
|
+
res.writeHead(200, {
|
|
3034
|
+
"Content-Type": "application/json",
|
|
3035
|
+
[MPP_RECEIPT_HEADER]: receiptEncoded
|
|
3036
|
+
});
|
|
3037
|
+
res.end(JSON.stringify({
|
|
3038
|
+
success: true,
|
|
3039
|
+
result,
|
|
3040
|
+
payment: {
|
|
3041
|
+
txHash,
|
|
3042
|
+
status: "verified",
|
|
3043
|
+
facilitator: verification.facilitator
|
|
3044
|
+
}
|
|
3045
|
+
}, null, 2));
|
|
3046
|
+
}
|
|
3047
|
+
/**
|
|
3048
|
+
* Return 402 with both x402 and MPP payment requirements
|
|
3049
|
+
*/
|
|
3050
|
+
sendMPPPaymentRequired(config, res) {
|
|
3051
|
+
const acceptedTokens = getAcceptedCurrencies(config);
|
|
3052
|
+
const providerChains = this.getProviderChains();
|
|
3053
|
+
const accepts = [];
|
|
3054
|
+
for (const chainConfig of providerChains) {
|
|
3055
|
+
for (const token of acceptedTokens) {
|
|
3056
|
+
if (chainConfig.tokens.includes(token)) {
|
|
3057
|
+
accepts.push(this.buildPaymentRequirements(config, chainConfig.network, chainConfig.wallet, token));
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
const x402PaymentRequired = {
|
|
3062
|
+
x402Version: X402_VERSION3,
|
|
3063
|
+
accepts,
|
|
3064
|
+
acceptedCurrencies: acceptedTokens,
|
|
3065
|
+
resource: {
|
|
3066
|
+
url: `/${config.id}`,
|
|
3067
|
+
description: `${config.name} - $${config.price} ${config.currency}`
|
|
3068
|
+
}
|
|
3069
|
+
};
|
|
3070
|
+
const x402Encoded = Buffer.from(JSON.stringify(x402PaymentRequired)).toString("base64");
|
|
3071
|
+
const tempoChain = providerChains.find((c) => c.network === "eip155:42431");
|
|
3072
|
+
let mppWwwAuth = "";
|
|
3073
|
+
if (tempoChain) {
|
|
3074
|
+
const challengeId = this.generateChallengeId();
|
|
3075
|
+
const amountInUnits = Math.floor(config.price * 1e6).toString();
|
|
3076
|
+
const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
|
|
3077
|
+
const mppRequest = {
|
|
3078
|
+
amount: amountInUnits,
|
|
3079
|
+
currency: tokenAddress,
|
|
3080
|
+
methodDetails: {
|
|
3081
|
+
chainId: 42431,
|
|
3082
|
+
feePayer: true
|
|
3083
|
+
},
|
|
3084
|
+
recipient: tempoChain.wallet
|
|
3085
|
+
};
|
|
3086
|
+
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
3087
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
3088
|
+
mppWwwAuth = `Payment id="${challengeId}", realm="${this.manifest.provider.name}", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
3089
|
+
}
|
|
3090
|
+
const headers = {
|
|
3091
|
+
"Content-Type": "application/problem+json",
|
|
3092
|
+
[PAYMENT_REQUIRED_HEADER2]: x402Encoded
|
|
3093
|
+
};
|
|
3094
|
+
if (mppWwwAuth) {
|
|
3095
|
+
headers[MPP_WWW_AUTH_HEADER] = mppWwwAuth;
|
|
3096
|
+
}
|
|
3097
|
+
res.writeHead(402, headers);
|
|
3098
|
+
res.end(JSON.stringify({
|
|
3099
|
+
type: "https://paymentauth.org/problems/payment-required",
|
|
3100
|
+
title: "Payment Required",
|
|
3101
|
+
status: 402,
|
|
3102
|
+
detail: `Payment is required (${config.name}).`,
|
|
3103
|
+
service: config.id,
|
|
3104
|
+
price: config.price,
|
|
3105
|
+
currency: config.currency,
|
|
3106
|
+
acceptedCurrencies: acceptedTokens
|
|
3107
|
+
}, null, 2));
|
|
3108
|
+
}
|
|
3109
|
+
/**
|
|
3110
|
+
* Generate a unique challenge ID for MPP
|
|
3111
|
+
*/
|
|
3112
|
+
generateChallengeId() {
|
|
3113
|
+
const bytes = new Uint8Array(24);
|
|
3114
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
3115
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
3116
|
+
}
|
|
3117
|
+
return Buffer.from(bytes).toString("base64url");
|
|
3118
|
+
}
|
|
1475
3119
|
/**
|
|
1476
3120
|
* Return 402 with x402 payment requirements (v2 format)
|
|
1477
3121
|
* Includes requirements for all chains and all accepted currencies
|
|
@@ -1547,7 +3191,7 @@ var MoltsPayServer = class {
|
|
|
1547
3191
|
const tokenAddresses = TOKEN_ADDRESSES[selectedNetwork] || {};
|
|
1548
3192
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
1549
3193
|
const tokenDomain = getTokenDomain(selectedNetwork, selectedToken);
|
|
1550
|
-
|
|
3194
|
+
const requirements = {
|
|
1551
3195
|
scheme: "exact",
|
|
1552
3196
|
network: selectedNetwork,
|
|
1553
3197
|
asset: tokenAddress,
|
|
@@ -1556,6 +3200,27 @@ var MoltsPayServer = class {
|
|
|
1556
3200
|
maxTimeoutSeconds: 300,
|
|
1557
3201
|
extra: tokenDomain
|
|
1558
3202
|
};
|
|
3203
|
+
if (selectedNetwork === "solana:mainnet" || selectedNetwork === "solana:devnet") {
|
|
3204
|
+
const solanaFacilitator = this.registry.get("solana");
|
|
3205
|
+
const feePayerPubkey = solanaFacilitator?.getFeePayerPubkey?.();
|
|
3206
|
+
if (feePayerPubkey) {
|
|
3207
|
+
requirements.extra = {
|
|
3208
|
+
...requirements.extra || {},
|
|
3209
|
+
solanaFeePayer: feePayerPubkey
|
|
3210
|
+
};
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
if (selectedNetwork === "eip155:56" || selectedNetwork === "eip155:97") {
|
|
3214
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
3215
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
3216
|
+
if (spenderAddress) {
|
|
3217
|
+
requirements.extra = {
|
|
3218
|
+
...requirements.extra || {},
|
|
3219
|
+
bnbSpender: spenderAddress
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
return requirements;
|
|
1559
3224
|
}
|
|
1560
3225
|
/**
|
|
1561
3226
|
* Detect which token is being used in the payment
|
|
@@ -1621,31 +3286,42 @@ var MoltsPayServer = class {
|
|
|
1621
3286
|
/**
|
|
1622
3287
|
* POST /proxy - Handle payment for external services (moltspay-creators)
|
|
1623
3288
|
*
|
|
1624
|
-
* This endpoint allows other services to delegate x402 payment handling.
|
|
3289
|
+
* This endpoint allows other services to delegate x402/MPP payment handling.
|
|
1625
3290
|
* It does NOT execute any skill - just handles payment verification/settlement.
|
|
1626
3291
|
*
|
|
1627
3292
|
* Request body:
|
|
1628
3293
|
* { wallet, amount, currency, chain, memo, serviceId, description }
|
|
1629
3294
|
*
|
|
1630
|
-
*
|
|
1631
|
-
*
|
|
3295
|
+
* For x402 (base, polygon, base_sepolia):
|
|
3296
|
+
* Without X-Payment header: returns 402 with X-Payment-Required
|
|
3297
|
+
* With X-Payment header: verifies payment via CDP
|
|
3298
|
+
*
|
|
3299
|
+
* For MPP (tempo_moderato):
|
|
3300
|
+
* Without Authorization header: returns 402 with WWW-Authenticate
|
|
3301
|
+
* With Authorization: Payment header: verifies tx on Tempo chain
|
|
1632
3302
|
*/
|
|
1633
|
-
async handleProxy(body, paymentHeader, res) {
|
|
3303
|
+
async handleProxy(body, paymentHeader, authHeader, res) {
|
|
1634
3304
|
const { wallet, amount, currency, chain, memo, serviceId, description } = body;
|
|
1635
3305
|
if (!wallet || !amount) {
|
|
1636
3306
|
return this.sendJson(res, 400, { error: "Missing required fields: wallet, amount" });
|
|
1637
3307
|
}
|
|
1638
|
-
|
|
1639
|
-
|
|
3308
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
|
|
3309
|
+
if (chain && !supportedChains.includes(chain)) {
|
|
3310
|
+
return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
|
|
3311
|
+
}
|
|
3312
|
+
const isSolanaChain = chain === "solana" || chain === "solana_devnet";
|
|
3313
|
+
const isValidEvmAddress = /^0x[a-fA-F0-9]{40}$/.test(wallet);
|
|
3314
|
+
const isValidSolanaAddress2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(wallet);
|
|
3315
|
+
if (isSolanaChain && !isValidSolanaAddress2) {
|
|
3316
|
+
return this.sendJson(res, 400, { error: "Invalid Solana wallet address format" });
|
|
3317
|
+
}
|
|
3318
|
+
if (!isSolanaChain && !isValidEvmAddress) {
|
|
3319
|
+
return this.sendJson(res, 400, { error: "Invalid EVM wallet address format" });
|
|
1640
3320
|
}
|
|
1641
3321
|
const amountNum = parseFloat(amount);
|
|
1642
3322
|
if (isNaN(amountNum) || amountNum <= 0) {
|
|
1643
3323
|
return this.sendJson(res, 400, { error: "Invalid amount" });
|
|
1644
3324
|
}
|
|
1645
|
-
const supportedChains = ["base", "polygon", "base_sepolia"];
|
|
1646
|
-
if (chain && !supportedChains.includes(chain)) {
|
|
1647
|
-
return this.sendJson(res, 400, { error: `Unsupported chain: ${chain}. Supported: ${supportedChains.join(", ")}` });
|
|
1648
|
-
}
|
|
1649
3325
|
const proxyConfig = {
|
|
1650
3326
|
id: serviceId || "proxy",
|
|
1651
3327
|
name: description || "Proxy Payment",
|
|
@@ -1657,6 +3333,9 @@ var MoltsPayServer = class {
|
|
|
1657
3333
|
input: {},
|
|
1658
3334
|
output: {}
|
|
1659
3335
|
};
|
|
3336
|
+
if (chain === "tempo_moderato") {
|
|
3337
|
+
return await this.handleProxyMPP(body, proxyConfig, authHeader, res);
|
|
3338
|
+
}
|
|
1660
3339
|
const requirements = this.buildProxyPaymentRequirements(proxyConfig, wallet, currency, chain);
|
|
1661
3340
|
if (!paymentHeader) {
|
|
1662
3341
|
return this.sendProxyPaymentRequired(proxyConfig, wallet, memo, chain, res);
|
|
@@ -1692,7 +3371,6 @@ var MoltsPayServer = class {
|
|
|
1692
3371
|
console.log(`[MoltsPay] /proxy: Verified by ${verifyResult.facilitator}`);
|
|
1693
3372
|
const { execute, service, params } = body;
|
|
1694
3373
|
if (execute && service) {
|
|
1695
|
-
console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
|
|
1696
3374
|
const skill = this.skills.get(service);
|
|
1697
3375
|
if (!skill) {
|
|
1698
3376
|
console.log(`[MoltsPay] /proxy: Service not found: ${service} - NOT settling`);
|
|
@@ -1702,6 +3380,32 @@ var MoltsPayServer = class {
|
|
|
1702
3380
|
error: `Service not found: ${service}`
|
|
1703
3381
|
});
|
|
1704
3382
|
}
|
|
3383
|
+
const isSolana = isSolanaNetwork(network);
|
|
3384
|
+
let settlement2 = null;
|
|
3385
|
+
if (isSolana) {
|
|
3386
|
+
console.log(`[MoltsPay] /proxy: Solana detected - settling payment FIRST`);
|
|
3387
|
+
try {
|
|
3388
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
3389
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
3390
|
+
if (!settlement2.success) {
|
|
3391
|
+
console.error(`[MoltsPay] /proxy: Solana settlement failed: ${settlement2.error}`);
|
|
3392
|
+
return this.sendJson(res, 402, {
|
|
3393
|
+
success: false,
|
|
3394
|
+
paymentSettled: false,
|
|
3395
|
+
error: `Payment settlement failed: ${settlement2.error || "Unknown error"}`
|
|
3396
|
+
});
|
|
3397
|
+
}
|
|
3398
|
+
} catch (err) {
|
|
3399
|
+
console.error("[MoltsPay] /proxy: Solana settlement failed:", err.message);
|
|
3400
|
+
return this.sendJson(res, 402, {
|
|
3401
|
+
success: false,
|
|
3402
|
+
paymentSettled: false,
|
|
3403
|
+
error: `Payment settlement failed: ${err.message}`
|
|
3404
|
+
});
|
|
3405
|
+
}
|
|
3406
|
+
} else {
|
|
3407
|
+
console.log(`[MoltsPay] /proxy: Executing skill first (pay on success): ${service}`);
|
|
3408
|
+
}
|
|
1705
3409
|
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
1706
3410
|
let result;
|
|
1707
3411
|
try {
|
|
@@ -1711,73 +3415,199 @@ var MoltsPayServer = class {
|
|
|
1711
3415
|
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
1712
3416
|
)
|
|
1713
3417
|
]);
|
|
1714
|
-
console.log(`[MoltsPay] /proxy: Skill succeeded
|
|
3418
|
+
console.log(`[MoltsPay] /proxy: Skill succeeded`);
|
|
1715
3419
|
} catch (err) {
|
|
1716
|
-
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}
|
|
3420
|
+
console.error(`[MoltsPay] /proxy: Skill failed: ${err.message}`);
|
|
1717
3421
|
return this.sendJson(res, 500, {
|
|
1718
3422
|
success: false,
|
|
1719
|
-
paymentSettled: false,
|
|
1720
|
-
error: `Service execution failed: ${err.message}
|
|
3423
|
+
paymentSettled: isSolana ? true : false,
|
|
3424
|
+
error: `Service execution failed: ${err.message}`,
|
|
3425
|
+
note: isSolana ? "Payment was settled before execution. Contact support for refund." : void 0
|
|
1721
3426
|
});
|
|
1722
3427
|
}
|
|
1723
|
-
|
|
3428
|
+
if (!isSolana) {
|
|
3429
|
+
console.log(`[MoltsPay] /proxy: Settling payment...`);
|
|
3430
|
+
try {
|
|
3431
|
+
settlement2 = await this.registry.settle(payment, requirements);
|
|
3432
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement2.facilitator}: ${settlement2.transaction || "pending"}`);
|
|
3433
|
+
} catch (err) {
|
|
3434
|
+
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
3435
|
+
return this.sendJson(res, 200, {
|
|
3436
|
+
success: true,
|
|
3437
|
+
verified: true,
|
|
3438
|
+
settled: false,
|
|
3439
|
+
settlementError: err.message,
|
|
3440
|
+
from: payment.payload?.authorization?.from,
|
|
3441
|
+
paidTo: wallet,
|
|
3442
|
+
amount: amountNum,
|
|
3443
|
+
currency: currency || "USDC",
|
|
3444
|
+
memo,
|
|
3445
|
+
result
|
|
3446
|
+
});
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
return this.sendJson(res, 200, {
|
|
3450
|
+
success: true,
|
|
3451
|
+
verified: true,
|
|
3452
|
+
settled: settlement2?.success || false,
|
|
3453
|
+
txHash: settlement2?.transaction,
|
|
3454
|
+
from: payment.payload?.authorization?.from,
|
|
3455
|
+
paidTo: wallet,
|
|
3456
|
+
amount: amountNum,
|
|
3457
|
+
currency: currency || "USDC",
|
|
3458
|
+
facilitator: settlement2?.facilitator,
|
|
3459
|
+
memo,
|
|
3460
|
+
result
|
|
3461
|
+
});
|
|
3462
|
+
}
|
|
3463
|
+
console.log(`[MoltsPay] /proxy: Settling payment (no execution)...`);
|
|
3464
|
+
let settlement = null;
|
|
3465
|
+
try {
|
|
3466
|
+
settlement = await this.registry.settle(payment, requirements);
|
|
3467
|
+
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
3468
|
+
} catch (err) {
|
|
3469
|
+
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
3470
|
+
return this.sendJson(res, 500, {
|
|
3471
|
+
success: false,
|
|
3472
|
+
error: `Settlement failed: ${err.message}`
|
|
3473
|
+
});
|
|
3474
|
+
}
|
|
3475
|
+
this.sendJson(res, 200, {
|
|
3476
|
+
success: true,
|
|
3477
|
+
verified: true,
|
|
3478
|
+
settled: settlement?.success || false,
|
|
3479
|
+
txHash: settlement?.transaction,
|
|
3480
|
+
from: payment.payload?.authorization?.from,
|
|
3481
|
+
// Buyer's wallet address
|
|
3482
|
+
paidTo: wallet,
|
|
3483
|
+
amount: amountNum,
|
|
3484
|
+
currency: currency || "USDC",
|
|
3485
|
+
facilitator: settlement?.facilitator,
|
|
3486
|
+
memo
|
|
3487
|
+
});
|
|
3488
|
+
}
|
|
3489
|
+
/**
|
|
3490
|
+
* Handle MPP payment flow for /proxy endpoint (tempo_moderato chain)
|
|
3491
|
+
*/
|
|
3492
|
+
async handleProxyMPP(body, config, authHeader, res) {
|
|
3493
|
+
const { wallet, amount, memo, serviceId } = body;
|
|
3494
|
+
const amountNum = parseFloat(amount);
|
|
3495
|
+
const amountInUnits = Math.floor(amountNum * 1e6).toString();
|
|
3496
|
+
if (!authHeader || !authHeader.toLowerCase().startsWith("payment ")) {
|
|
3497
|
+
const challengeId = this.generateChallengeId();
|
|
3498
|
+
const tokenAddress = TOKEN_ADDRESSES["eip155:42431"]?.USDC || "0x20c0000000000000000000000000000000000000";
|
|
3499
|
+
const mppRequest = {
|
|
3500
|
+
amount: amountInUnits,
|
|
3501
|
+
currency: tokenAddress,
|
|
3502
|
+
methodDetails: {
|
|
3503
|
+
chainId: 42431,
|
|
3504
|
+
feePayer: true
|
|
3505
|
+
},
|
|
3506
|
+
recipient: wallet
|
|
3507
|
+
};
|
|
3508
|
+
const mppRequestEncoded = Buffer.from(JSON.stringify(mppRequest)).toString("base64");
|
|
3509
|
+
const expiresAt = new Date(Date.now() + 5 * 60 * 1e3).toISOString();
|
|
3510
|
+
const wwwAuth = `Payment id="${challengeId}", realm="MoltsPay Proxy", method="tempo", intent="charge", request="${mppRequestEncoded}", description="${config.name}", expires="${expiresAt}"`;
|
|
3511
|
+
res.writeHead(402, {
|
|
3512
|
+
"Content-Type": "application/problem+json",
|
|
3513
|
+
[MPP_WWW_AUTH_HEADER]: wwwAuth
|
|
3514
|
+
});
|
|
3515
|
+
res.end(JSON.stringify({
|
|
3516
|
+
type: "https://paymentauth.org/problems/payment-required",
|
|
3517
|
+
title: "Payment Required",
|
|
3518
|
+
status: 402,
|
|
3519
|
+
detail: `Payment is required (${config.name}).`,
|
|
3520
|
+
service: serviceId || "proxy",
|
|
3521
|
+
price: amountNum,
|
|
3522
|
+
currency: "USDC"
|
|
3523
|
+
}, null, 2));
|
|
3524
|
+
return;
|
|
3525
|
+
}
|
|
3526
|
+
const credentialMatch = authHeader.match(/Payment\s+(.+)/i);
|
|
3527
|
+
if (!credentialMatch) {
|
|
3528
|
+
return this.sendJson(res, 400, { error: "Invalid Authorization header format" });
|
|
3529
|
+
}
|
|
3530
|
+
let mppCredential;
|
|
3531
|
+
try {
|
|
3532
|
+
const base64 = credentialMatch[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
3533
|
+
const decoded = Buffer.from(base64, "base64").toString("utf-8");
|
|
3534
|
+
mppCredential = JSON.parse(decoded);
|
|
3535
|
+
} catch (err) {
|
|
3536
|
+
console.error("[MoltsPay] /proxy MPP: Failed to parse credential:", err);
|
|
3537
|
+
return this.sendJson(res, 400, { error: "Invalid payment credential encoding" });
|
|
3538
|
+
}
|
|
3539
|
+
let txHash;
|
|
3540
|
+
if (mppCredential.payload?.type === "hash" && mppCredential.payload?.hash) {
|
|
3541
|
+
txHash = mppCredential.payload.hash;
|
|
3542
|
+
} else {
|
|
3543
|
+
return this.sendJson(res, 400, { error: "Missing transaction hash in credential" });
|
|
3544
|
+
}
|
|
3545
|
+
console.log(`[MoltsPay] /proxy MPP: Verifying tx ${txHash} on Tempo...`);
|
|
3546
|
+
const requirements = this.buildPaymentRequirements(config, "eip155:42431", wallet, "USDC");
|
|
3547
|
+
const paymentPayload = {
|
|
3548
|
+
x402Version: X402_VERSION3,
|
|
3549
|
+
scheme: "exact",
|
|
3550
|
+
network: "eip155:42431",
|
|
3551
|
+
payload: { txHash, chainId: 42431 }
|
|
3552
|
+
};
|
|
3553
|
+
const verification = await this.registry.verify(paymentPayload, requirements);
|
|
3554
|
+
if (!verification.valid) {
|
|
3555
|
+
return this.sendJson(res, 402, {
|
|
3556
|
+
error: `Payment verification failed: ${verification.error}`
|
|
3557
|
+
});
|
|
3558
|
+
}
|
|
3559
|
+
console.log(`[MoltsPay] /proxy MPP: Payment verified by ${verification.facilitator}`);
|
|
3560
|
+
const { execute, service, params } = body;
|
|
3561
|
+
if (execute && service) {
|
|
3562
|
+
console.log(`[MoltsPay] /proxy MPP: Executing skill: ${service}`);
|
|
3563
|
+
const skill = this.skills.get(service);
|
|
3564
|
+
if (!skill) {
|
|
3565
|
+
return this.sendJson(res, 404, {
|
|
3566
|
+
success: false,
|
|
3567
|
+
paymentSettled: true,
|
|
3568
|
+
// Payment already happened on Tempo
|
|
3569
|
+
error: `Service not found: ${service}`
|
|
3570
|
+
});
|
|
3571
|
+
}
|
|
3572
|
+
const timeoutSeconds = parseInt(process.env.SKILL_TIMEOUT_SECONDS || "1200");
|
|
3573
|
+
let result;
|
|
1724
3574
|
try {
|
|
1725
|
-
|
|
1726
|
-
|
|
3575
|
+
result = await Promise.race([
|
|
3576
|
+
skill.handler(params || {}),
|
|
3577
|
+
new Promise(
|
|
3578
|
+
(_, reject) => setTimeout(() => reject(new Error(`Skill timeout after ${timeoutSeconds}s`)), timeoutSeconds * 1e3)
|
|
3579
|
+
)
|
|
3580
|
+
]);
|
|
1727
3581
|
} catch (err) {
|
|
1728
|
-
console.error(
|
|
1729
|
-
return this.sendJson(res,
|
|
1730
|
-
success:
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
settlementError: err.message,
|
|
1734
|
-
from: payment.payload?.authorization?.from,
|
|
1735
|
-
// Buyer's wallet address
|
|
1736
|
-
paidTo: wallet,
|
|
1737
|
-
amount: amountNum,
|
|
1738
|
-
currency: currency || "USDC",
|
|
1739
|
-
memo,
|
|
1740
|
-
result
|
|
3582
|
+
console.error(`[MoltsPay] /proxy MPP: Skill failed: ${err.message}`);
|
|
3583
|
+
return this.sendJson(res, 500, {
|
|
3584
|
+
success: false,
|
|
3585
|
+
paymentSettled: true,
|
|
3586
|
+
error: `Service execution failed: ${err.message}`
|
|
1741
3587
|
});
|
|
1742
3588
|
}
|
|
1743
3589
|
return this.sendJson(res, 200, {
|
|
1744
3590
|
success: true,
|
|
1745
3591
|
verified: true,
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
from: payment.payload?.authorization?.from,
|
|
1749
|
-
// Buyer's wallet address
|
|
3592
|
+
txHash,
|
|
3593
|
+
chain: "tempo_moderato",
|
|
1750
3594
|
paidTo: wallet,
|
|
1751
3595
|
amount: amountNum,
|
|
1752
|
-
currency:
|
|
1753
|
-
facilitator:
|
|
3596
|
+
currency: "USDC",
|
|
3597
|
+
facilitator: verification.facilitator,
|
|
1754
3598
|
memo,
|
|
1755
3599
|
result
|
|
1756
3600
|
});
|
|
1757
3601
|
}
|
|
1758
|
-
console.log(`[MoltsPay] /proxy: Settling payment (no execution)...`);
|
|
1759
|
-
let settlement = null;
|
|
1760
|
-
try {
|
|
1761
|
-
settlement = await this.registry.settle(payment, requirements);
|
|
1762
|
-
console.log(`[MoltsPay] /proxy: Payment settled by ${settlement.facilitator}: ${settlement.transaction || "pending"}`);
|
|
1763
|
-
} catch (err) {
|
|
1764
|
-
console.error("[MoltsPay] /proxy: Settlement failed:", err.message);
|
|
1765
|
-
return this.sendJson(res, 500, {
|
|
1766
|
-
success: false,
|
|
1767
|
-
error: `Settlement failed: ${err.message}`
|
|
1768
|
-
});
|
|
1769
|
-
}
|
|
1770
3602
|
this.sendJson(res, 200, {
|
|
1771
3603
|
success: true,
|
|
1772
3604
|
verified: true,
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
from: payment.payload?.authorization?.from,
|
|
1776
|
-
// Buyer's wallet address
|
|
3605
|
+
txHash,
|
|
3606
|
+
chain: "tempo_moderato",
|
|
1777
3607
|
paidTo: wallet,
|
|
1778
3608
|
amount: amountNum,
|
|
1779
|
-
currency:
|
|
1780
|
-
facilitator:
|
|
3609
|
+
currency: "USDC",
|
|
3610
|
+
facilitator: verification.facilitator,
|
|
1781
3611
|
memo
|
|
1782
3612
|
});
|
|
1783
3613
|
}
|
|
@@ -1792,7 +3622,7 @@ var MoltsPayServer = class {
|
|
|
1792
3622
|
const tokenAddresses = TOKEN_ADDRESSES[networkId] || TOKEN_ADDRESSES[this.networkId] || {};
|
|
1793
3623
|
const tokenAddress = tokenAddresses[selectedToken];
|
|
1794
3624
|
const tokenDomain = getTokenDomain(networkId, selectedToken);
|
|
1795
|
-
|
|
3625
|
+
const requirements = {
|
|
1796
3626
|
scheme: "exact",
|
|
1797
3627
|
network: networkId,
|
|
1798
3628
|
asset: tokenAddress,
|
|
@@ -1802,6 +3632,17 @@ var MoltsPayServer = class {
|
|
|
1802
3632
|
maxTimeoutSeconds: 300,
|
|
1803
3633
|
extra: tokenDomain
|
|
1804
3634
|
};
|
|
3635
|
+
if (networkId === "eip155:56" || networkId === "eip155:97") {
|
|
3636
|
+
const bnbFacilitator = this.registry.get("bnb");
|
|
3637
|
+
const spenderAddress = bnbFacilitator?.getSpenderAddress?.();
|
|
3638
|
+
if (spenderAddress) {
|
|
3639
|
+
requirements.extra = {
|
|
3640
|
+
...requirements.extra || {},
|
|
3641
|
+
bnbSpender: spenderAddress
|
|
3642
|
+
};
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
return requirements;
|
|
1805
3646
|
}
|
|
1806
3647
|
/**
|
|
1807
3648
|
* Return 402 with x402 payment requirements for proxy endpoint
|
|
@@ -1846,11 +3687,114 @@ async function printQRCode(url) {
|
|
|
1846
3687
|
|
|
1847
3688
|
// src/cli/index.ts
|
|
1848
3689
|
var readline = __toESM(require("readline"));
|
|
3690
|
+
if (!globalThis.crypto) {
|
|
3691
|
+
globalThis.crypto = import_crypto.webcrypto;
|
|
3692
|
+
}
|
|
3693
|
+
function getVersion() {
|
|
3694
|
+
const locations = [
|
|
3695
|
+
(0, import_path3.join)(__dirname, "../../package.json"),
|
|
3696
|
+
(0, import_path3.join)(__dirname, "../package.json"),
|
|
3697
|
+
(0, import_path3.join)(process.cwd(), "node_modules/moltspay/package.json")
|
|
3698
|
+
];
|
|
3699
|
+
for (const loc of locations) {
|
|
3700
|
+
try {
|
|
3701
|
+
if ((0, import_fs5.existsSync)(loc)) {
|
|
3702
|
+
const pkg = JSON.parse((0, import_fs5.readFileSync)(loc, "utf-8"));
|
|
3703
|
+
if (pkg.name === "moltspay") return pkg.version;
|
|
3704
|
+
}
|
|
3705
|
+
} catch {
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
return "0.0.0";
|
|
3709
|
+
}
|
|
3710
|
+
var BNB_SPONSOR_KEY = process.env.MOLTSPAY_BNB_SPONSOR_KEY;
|
|
3711
|
+
var BNB_SPENDER_ADDRESS = process.env.MOLTSPAY_BNB_SPENDER || "0xEBB45208D806A0c73F9673E0c5713FF720DD6b79";
|
|
3712
|
+
var ERC20_APPROVE_ABI = [
|
|
3713
|
+
"function approve(address spender, uint256 amount) returns (bool)",
|
|
3714
|
+
"function allowance(address owner, address spender) view returns (uint256)"
|
|
3715
|
+
];
|
|
3716
|
+
async function setupBNBApprovals(client, chain, spenderAddress, sponsorGas = false) {
|
|
3717
|
+
const chainConfig = CHAINS[chain];
|
|
3718
|
+
const provider = new import_ethers2.ethers.JsonRpcProvider(chainConfig.rpc);
|
|
3719
|
+
const wallet = client.getWallet();
|
|
3720
|
+
if (!wallet) {
|
|
3721
|
+
console.log(" \u274C No wallet found");
|
|
3722
|
+
return;
|
|
3723
|
+
}
|
|
3724
|
+
const signer = wallet.connect(provider);
|
|
3725
|
+
console.log(` Spender: ${spenderAddress}`);
|
|
3726
|
+
let bnbBalance = await provider.getBalance(wallet.address);
|
|
3727
|
+
const minGasRequired = import_ethers2.ethers.parseEther("0.0005");
|
|
3728
|
+
if (bnbBalance < minGasRequired) {
|
|
3729
|
+
if (sponsorGas && BNB_SPONSOR_KEY) {
|
|
3730
|
+
console.log(" \u23F3 Sponsoring BNB gas for approvals...");
|
|
3731
|
+
try {
|
|
3732
|
+
const sponsorWallet = new import_ethers2.ethers.Wallet(BNB_SPONSOR_KEY, provider);
|
|
3733
|
+
const tx = await sponsorWallet.sendTransaction({
|
|
3734
|
+
to: wallet.address,
|
|
3735
|
+
value: import_ethers2.ethers.parseEther("0.001")
|
|
3736
|
+
});
|
|
3737
|
+
await tx.wait();
|
|
3738
|
+
console.log(` \u2705 Sponsored 0.001 BNB (tx: ${tx.hash.slice(0, 10)}...)`);
|
|
3739
|
+
bnbBalance = await provider.getBalance(wallet.address);
|
|
3740
|
+
} catch (err) {
|
|
3741
|
+
console.log(` \u26A0\uFE0F Gas sponsorship failed: ${err.message}`);
|
|
3742
|
+
console.log(` \u{1F4A1} Get testnet BNB: https://testnet.bnbchain.org/faucet-smart`);
|
|
3743
|
+
return;
|
|
3744
|
+
}
|
|
3745
|
+
} else {
|
|
3746
|
+
console.log(` \u26A0\uFE0F Need BNB for gas (~0.0005 BNB)`);
|
|
3747
|
+
console.log(` \u{1F4A1} Run: npx moltspay faucet --chain bnb_testnet`);
|
|
3748
|
+
console.log(` Then run: npx moltspay approve --chain ${chain} --spender ${spenderAddress}`);
|
|
3749
|
+
return;
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
for (const tokenSymbol of ["USDT", "USDC"]) {
|
|
3753
|
+
const tokenConfig = chainConfig.tokens[tokenSymbol];
|
|
3754
|
+
const tokenContract = new import_ethers2.ethers.Contract(tokenConfig.address, ERC20_APPROVE_ABI, signer);
|
|
3755
|
+
const allowance = await tokenContract.allowance(wallet.address, spenderAddress);
|
|
3756
|
+
if (allowance > 0n) {
|
|
3757
|
+
console.log(` \u2705 ${tokenSymbol}: already approved for ${spenderAddress.slice(0, 10)}...`);
|
|
3758
|
+
continue;
|
|
3759
|
+
}
|
|
3760
|
+
console.log(` \u23F3 Approving ${tokenSymbol}...`);
|
|
3761
|
+
try {
|
|
3762
|
+
const tx = await tokenContract.approve(spenderAddress, import_ethers2.ethers.MaxUint256);
|
|
3763
|
+
await tx.wait();
|
|
3764
|
+
console.log(` \u2705 ${tokenSymbol}: approved (tx: ${tx.hash.slice(0, 10)}...)`);
|
|
3765
|
+
} catch (err) {
|
|
3766
|
+
console.log(` \u274C ${tokenSymbol}: approval failed - ${err.message}`);
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
console.log("");
|
|
3770
|
+
}
|
|
3771
|
+
async function checkBNBApprovals(address, chain, configDir = DEFAULT_CONFIG_DIR2) {
|
|
3772
|
+
const chainConfig = CHAINS[chain];
|
|
3773
|
+
const provider = new import_ethers2.ethers.JsonRpcProvider(chainConfig.rpc);
|
|
3774
|
+
let spenderAddress = null;
|
|
3775
|
+
try {
|
|
3776
|
+
const walletPath = (0, import_path3.join)(configDir, "wallet.json");
|
|
3777
|
+
const walletData = JSON.parse((0, import_fs5.readFileSync)(walletPath, "utf-8"));
|
|
3778
|
+
spenderAddress = walletData.approvals?.[chain] || null;
|
|
3779
|
+
} catch {
|
|
3780
|
+
}
|
|
3781
|
+
const result = { usdt: false, usdc: false, spender: spenderAddress };
|
|
3782
|
+
if (!spenderAddress) {
|
|
3783
|
+
return result;
|
|
3784
|
+
}
|
|
3785
|
+
for (const tokenSymbol of ["USDT", "USDC"]) {
|
|
3786
|
+
const tokenConfig = chainConfig.tokens[tokenSymbol];
|
|
3787
|
+
const tokenContract = new import_ethers2.ethers.Contract(tokenConfig.address, ERC20_APPROVE_ABI, provider);
|
|
3788
|
+
const allowance = await tokenContract.allowance(address, spenderAddress);
|
|
3789
|
+
result[tokenSymbol.toLowerCase()] = allowance > 0n;
|
|
3790
|
+
}
|
|
3791
|
+
return result;
|
|
3792
|
+
}
|
|
1849
3793
|
var program = new import_commander.Command();
|
|
1850
|
-
var
|
|
1851
|
-
var PID_FILE = (0,
|
|
1852
|
-
if (!(0,
|
|
1853
|
-
(0,
|
|
3794
|
+
var DEFAULT_CONFIG_DIR2 = (0, import_path3.join)((0, import_os3.homedir)(), ".moltspay");
|
|
3795
|
+
var PID_FILE = (0, import_path3.join)(DEFAULT_CONFIG_DIR2, "server.pid");
|
|
3796
|
+
if (!(0, import_fs5.existsSync)(DEFAULT_CONFIG_DIR2)) {
|
|
3797
|
+
(0, import_fs5.mkdirSync)(DEFAULT_CONFIG_DIR2, { recursive: true });
|
|
1854
3798
|
}
|
|
1855
3799
|
function prompt(question) {
|
|
1856
3800
|
const rl = readline.createInterface({
|
|
@@ -1864,20 +3808,50 @@ function prompt(question) {
|
|
|
1864
3808
|
});
|
|
1865
3809
|
});
|
|
1866
3810
|
}
|
|
1867
|
-
program.name("moltspay").description("MoltsPay - Payment infrastructure for AI Agents").version(
|
|
1868
|
-
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",
|
|
1869
|
-
console.log("\n\u{1F510} MoltsPay Client Setup\n");
|
|
1870
|
-
if ((0, import_fs4.existsSync)((0, import_path2.join)(options.configDir, "wallet.json"))) {
|
|
1871
|
-
console.log('\u26A0\uFE0F Already initialized. Use "moltspay config" to update settings.');
|
|
1872
|
-
console.log(` Config dir: ${options.configDir}`);
|
|
1873
|
-
return;
|
|
1874
|
-
}
|
|
3811
|
+
program.name("moltspay").description("MoltsPay - Payment infrastructure for AI Agents").version(getVersion());
|
|
3812
|
+
program.command("init").description("Initialize MoltsPay client (create wallet, set limits)").option("--chain <chain>", "Blockchain to use", "base").option("--max-per-tx <amount>", "Max amount per transaction").option("--max-per-day <amount>", "Max amount per day").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (options) => {
|
|
1875
3813
|
let chain = options.chain;
|
|
1876
|
-
const
|
|
3814
|
+
const supportedEVMChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
|
|
3815
|
+
const supportedSolanaChains = ["solana", "solana_devnet"];
|
|
3816
|
+
const supportedChains = [...supportedEVMChains, ...supportedSolanaChains];
|
|
1877
3817
|
if (!supportedChains.includes(chain)) {
|
|
1878
3818
|
console.error(`\u274C Unknown chain: ${chain}. Supported: ${supportedChains.join(", ")}`);
|
|
1879
3819
|
process.exit(1);
|
|
1880
3820
|
}
|
|
3821
|
+
if (supportedSolanaChains.includes(chain)) {
|
|
3822
|
+
console.log("\n\u{1F7E3} Solana Wallet Setup\n");
|
|
3823
|
+
if (solanaWalletExists(options.configDir)) {
|
|
3824
|
+
const existingAddress = getSolanaAddress(options.configDir);
|
|
3825
|
+
console.log(`\u26A0\uFE0F Solana wallet already exists: ${existingAddress}`);
|
|
3826
|
+
console.log(` Config dir: ${options.configDir}`);
|
|
3827
|
+
return;
|
|
3828
|
+
}
|
|
3829
|
+
console.log("Creating Solana wallet...");
|
|
3830
|
+
const keypair = createSolanaWallet(options.configDir);
|
|
3831
|
+
const address = keypair.publicKey.toBase58();
|
|
3832
|
+
console.log(`
|
|
3833
|
+
\u2705 Solana wallet created: ${address}`);
|
|
3834
|
+
console.log(`
|
|
3835
|
+
\u{1F4C1} Config saved to: ${(0, import_path3.join)(options.configDir, "wallet-solana.json")}`);
|
|
3836
|
+
console.log(`
|
|
3837
|
+
\u26A0\uFE0F IMPORTANT: Back up your wallet file!`);
|
|
3838
|
+
console.log(` This file contains your private key!
|
|
3839
|
+
`);
|
|
3840
|
+
if (chain === "solana_devnet") {
|
|
3841
|
+
console.log("\u{1F4A1} Get testnet tokens:");
|
|
3842
|
+
console.log(" npx moltspay faucet --chain solana_devnet\n");
|
|
3843
|
+
} else {
|
|
3844
|
+
console.log(`\u{1F4B0} Fund your wallet with USDC on Solana to start (gasless - no SOL needed).
|
|
3845
|
+
`);
|
|
3846
|
+
}
|
|
3847
|
+
return;
|
|
3848
|
+
}
|
|
3849
|
+
console.log("\n\u{1F510} MoltsPay Client Setup\n");
|
|
3850
|
+
if ((0, import_fs5.existsSync)((0, import_path3.join)(options.configDir, "wallet.json"))) {
|
|
3851
|
+
console.log('\u26A0\uFE0F EVM wallet already initialized. Use "moltspay config" to update settings.');
|
|
3852
|
+
console.log(` Config dir: ${options.configDir}`);
|
|
3853
|
+
return;
|
|
3854
|
+
}
|
|
1881
3855
|
let maxPerTx = options.maxPerTx ? parseFloat(options.maxPerTx) : null;
|
|
1882
3856
|
let maxPerDay = options.maxPerDay ? parseFloat(options.maxPerDay) : null;
|
|
1883
3857
|
if (!maxPerTx) {
|
|
@@ -1899,13 +3873,21 @@ program.command("init").description("Initialize MoltsPay client (create wallet,
|
|
|
1899
3873
|
console.log(`
|
|
1900
3874
|
\u{1F4C1} Config saved to: ${result.configDir}`);
|
|
1901
3875
|
console.log(`
|
|
1902
|
-
\u26A0\uFE0F IMPORTANT: Back up ${(0,
|
|
3876
|
+
\u26A0\uFE0F IMPORTANT: Back up ${(0, import_path3.join)(result.configDir, "wallet.json")}`);
|
|
1903
3877
|
console.log(` This file contains your private key!
|
|
1904
3878
|
`);
|
|
3879
|
+
if (chain === "bnb" || chain === "bnb_testnet") {
|
|
3880
|
+
console.log("\u{1F4CB} Setting up BNB chain approvals...\n");
|
|
3881
|
+
console.log(" \u2139\uFE0F Using default spender. For other services, run:");
|
|
3882
|
+
console.log(` npx moltspay approve --chain ${chain} --spender <address>
|
|
3883
|
+
`);
|
|
3884
|
+
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
3885
|
+
await setupBNBApprovals(client, chain, BNB_SPENDER_ADDRESS, true);
|
|
3886
|
+
}
|
|
1905
3887
|
console.log(`\u{1F4B0} Fund your wallet with USDC on ${chain} to start using services.
|
|
1906
3888
|
`);
|
|
1907
3889
|
});
|
|
1908
|
-
program.command("config").description("Update MoltsPay settings").option("--max-per-tx <amount>", "Max amount per transaction").option("--max-per-day <amount>", "Max amount per day").option("--config-dir <dir>", "Config directory",
|
|
3890
|
+
program.command("config").description("Update MoltsPay settings").option("--max-per-tx <amount>", "Max amount per transaction").option("--max-per-day <amount>", "Max amount per day").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (options) => {
|
|
1909
3891
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
1910
3892
|
if (!client.isInitialized) {
|
|
1911
3893
|
console.log("\u274C Not initialized. Run: npx moltspay init");
|
|
@@ -1940,37 +3922,77 @@ program.command("config").description("Update MoltsPay settings").option("--max-
|
|
|
1940
3922
|
}
|
|
1941
3923
|
}
|
|
1942
3924
|
});
|
|
1943
|
-
program.command("fund <amount>").description("Fund wallet with USDC via Coinbase (US debit card / Apple Pay)").option("--chain <chain>", "Chain to fund (base, polygon, or
|
|
3925
|
+
program.command("fund <amount>").description("Fund wallet with USDC via Coinbase (US debit card / Apple Pay)").option("--chain <chain>", "Chain to fund (base, polygon, solana, base_sepolia, bnb, or bnb_testnet)", "base").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (amountStr, options) => {
|
|
1944
3926
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
1945
|
-
if (!client.isInitialized) {
|
|
1946
|
-
console.log("\u274C Not initialized. Run: npx moltspay init");
|
|
1947
|
-
return;
|
|
1948
|
-
}
|
|
1949
3927
|
const amount = parseFloat(amountStr);
|
|
1950
3928
|
if (isNaN(amount) || amount < 5) {
|
|
1951
3929
|
console.log("\u274C Minimum $5.");
|
|
1952
3930
|
return;
|
|
1953
3931
|
}
|
|
1954
3932
|
const chain = options.chain?.toLowerCase() || "base";
|
|
1955
|
-
if (!["base", "polygon", "base_sepolia"].includes(chain)) {
|
|
1956
|
-
console.log("\u274C Invalid chain. Use: base, polygon, or
|
|
3933
|
+
if (!["base", "polygon", "base_sepolia", "solana", "bnb", "bnb_testnet"].includes(chain)) {
|
|
3934
|
+
console.log("\u274C Invalid chain. Use: base, polygon, solana, base_sepolia, bnb, or bnb_testnet");
|
|
1957
3935
|
return;
|
|
1958
3936
|
}
|
|
3937
|
+
let walletAddress;
|
|
3938
|
+
if (chain === "solana") {
|
|
3939
|
+
const solanaWallet = loadSolanaWallet(options.configDir || DEFAULT_CONFIG_DIR2);
|
|
3940
|
+
if (!solanaWallet) {
|
|
3941
|
+
console.log("\u274C No Solana wallet found. Run: npx moltspay init --chain solana");
|
|
3942
|
+
return;
|
|
3943
|
+
}
|
|
3944
|
+
walletAddress = getSolanaAddress(options.configDir || DEFAULT_CONFIG_DIR2) || "";
|
|
3945
|
+
if (!walletAddress) {
|
|
3946
|
+
console.log("\u274C Could not get Solana wallet address.");
|
|
3947
|
+
return;
|
|
3948
|
+
}
|
|
3949
|
+
} else {
|
|
3950
|
+
if (!client.isInitialized) {
|
|
3951
|
+
console.log("\u274C Not initialized. Run: npx moltspay init");
|
|
3952
|
+
return;
|
|
3953
|
+
}
|
|
3954
|
+
walletAddress = client.address;
|
|
3955
|
+
}
|
|
1959
3956
|
if (chain === "base_sepolia") {
|
|
1960
3957
|
console.log("\n\u{1F9EA} Testnet Funding\n");
|
|
1961
|
-
console.log(` Wallet: ${
|
|
3958
|
+
console.log(` Wallet: ${walletAddress}`);
|
|
1962
3959
|
console.log(` Chain: Base Sepolia (testnet)
|
|
1963
3960
|
`);
|
|
1964
|
-
console.log("\u{
|
|
1965
|
-
console.log("
|
|
1966
|
-
console.log("
|
|
1967
|
-
|
|
3961
|
+
console.log("\u{1F4A1} Use the MoltsPay faucet to get free testnet USDC:\n");
|
|
3962
|
+
console.log(" npx moltspay faucet\n");
|
|
3963
|
+
console.log(" Or get from Circle Faucet: https://faucet.circle.com/\n");
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
3966
|
+
if (chain === "bnb_testnet") {
|
|
3967
|
+
console.log("\n\u{1F9EA} BNB Testnet Funding\n");
|
|
3968
|
+
console.log(` Wallet: ${walletAddress}`);
|
|
3969
|
+
console.log(` Chain: BNB Testnet
|
|
3970
|
+
`);
|
|
3971
|
+
console.log("\u{1F4A1} Use the MoltsPay faucet to get testnet USDC + tBNB:\n");
|
|
3972
|
+
console.log(" npx moltspay faucet --chain bnb_testnet\n");
|
|
3973
|
+
console.log(" This gives you:\n");
|
|
3974
|
+
console.log(" \u2022 1 USDC (testnet) for payments");
|
|
3975
|
+
console.log(" \u2022 0.001 tBNB for gas (first approval tx)\n");
|
|
3976
|
+
return;
|
|
3977
|
+
}
|
|
3978
|
+
if (chain === "bnb") {
|
|
3979
|
+
console.log("\n\u{1F4CB} BNB Chain Funding\n");
|
|
3980
|
+
console.log(` Wallet: ${walletAddress}
|
|
1968
3981
|
`);
|
|
3982
|
+
console.log(" To use MoltsPay on BNB Chain, you need:\n");
|
|
3983
|
+
console.log(" 1. USDC for payments");
|
|
3984
|
+
console.log(" \u2192 Withdraw from Binance/exchange to your wallet address\n");
|
|
3985
|
+
console.log(" 2. Small amount of BNB for gas (~0.001 BNB / ~$0.60)");
|
|
3986
|
+
console.log(" \u2192 First approval transaction requires gas");
|
|
3987
|
+
console.log(" \u2192 After approval, all payments are gasless\n");
|
|
3988
|
+
console.log(" \u{1F4A1} Tip: Most exchanges include BNB dust when you withdraw to BNB Chain\n");
|
|
3989
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
3990
|
+
console.log(" After funding, check status: npx moltspay status\n");
|
|
1969
3991
|
return;
|
|
1970
3992
|
}
|
|
1971
3993
|
console.log("\n\u{1F4B3} Fund your agent wallet\n");
|
|
1972
|
-
console.log(` Wallet: ${
|
|
1973
|
-
console.log(` Chain: ${chain}`);
|
|
3994
|
+
console.log(` Wallet: ${walletAddress}`);
|
|
3995
|
+
console.log(` Chain: ${chain === "solana" ? "Solana" : chain}`);
|
|
1974
3996
|
console.log(` Amount: $${amount.toFixed(2)}
|
|
1975
3997
|
`);
|
|
1976
3998
|
try {
|
|
@@ -1979,7 +4001,7 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
|
|
|
1979
4001
|
method: "POST",
|
|
1980
4002
|
headers: { "Content-Type": "application/json" },
|
|
1981
4003
|
body: JSON.stringify({
|
|
1982
|
-
address:
|
|
4004
|
+
address: walletAddress,
|
|
1983
4005
|
amount,
|
|
1984
4006
|
chain
|
|
1985
4007
|
})
|
|
@@ -1997,8 +4019,94 @@ program.command("fund <amount>").description("Fund wallet with USDC via Coinbase
|
|
|
1997
4019
|
console.log(`\u274C ${error.message}`);
|
|
1998
4020
|
}
|
|
1999
4021
|
});
|
|
2000
|
-
program.command("
|
|
4022
|
+
program.command("approve").description("Approve a spender address for BNB chain payments").requiredOption("--spender <address>", "Spender address to approve (from server 402 response)").option("--chain <chain>", "BNB chain (bnb or bnb_testnet)", "bnb_testnet").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (options) => {
|
|
4023
|
+
const chain = options.chain;
|
|
4024
|
+
if (chain !== "bnb" && chain !== "bnb_testnet") {
|
|
4025
|
+
console.log("\u274C approve command is only for BNB chains (bnb or bnb_testnet)");
|
|
4026
|
+
return;
|
|
4027
|
+
}
|
|
4028
|
+
if (!options.spender.match(/^0x[a-fA-F0-9]{40}$/)) {
|
|
4029
|
+
console.log("\u274C Invalid spender address format");
|
|
4030
|
+
return;
|
|
4031
|
+
}
|
|
4032
|
+
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
4033
|
+
if (!client.isInitialized) {
|
|
4034
|
+
console.log("\u274C Wallet not initialized. Run: npx moltspay init --chain " + chain);
|
|
4035
|
+
return;
|
|
4036
|
+
}
|
|
4037
|
+
console.log(`
|
|
4038
|
+
\u{1F510} Approving spender for ${chain}...
|
|
4039
|
+
`);
|
|
4040
|
+
await setupBNBApprovals(client, chain, options.spender, false);
|
|
4041
|
+
const walletPath = (0, import_path3.join)(options.configDir || DEFAULT_CONFIG_DIR2, "wallet.json");
|
|
4042
|
+
try {
|
|
4043
|
+
const walletData = JSON.parse((0, import_fs5.readFileSync)(walletPath, "utf-8"));
|
|
4044
|
+
walletData.approvals = walletData.approvals || {};
|
|
4045
|
+
walletData.approvals[chain] = options.spender;
|
|
4046
|
+
(0, import_fs5.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2));
|
|
4047
|
+
console.log(`\u2705 Approval complete! Spender saved for ${chain}.
|
|
4048
|
+
`);
|
|
4049
|
+
} catch (err) {
|
|
4050
|
+
console.log("\u2705 Approval complete!\n");
|
|
4051
|
+
console.log("\u26A0\uFE0F Could not save spender to wallet config");
|
|
4052
|
+
}
|
|
4053
|
+
});
|
|
4054
|
+
program.command("faucet").description("Request testnet tokens from faucet (Base Sepolia, Tempo Moderato, BNB Testnet, or Solana Devnet)").option("--chain <chain>", "Chain to get tokens on (base_sepolia, tempo_moderato, bnb_testnet, or solana_devnet)", "base_sepolia").option("--address <address>", "Wallet address (defaults to your wallet)").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (options) => {
|
|
2001
4055
|
let address = options.address;
|
|
4056
|
+
const chain = options.chain?.toLowerCase() || "base_sepolia";
|
|
4057
|
+
if (!["base_sepolia", "tempo_moderato", "bnb_testnet", "solana_devnet"].includes(chain)) {
|
|
4058
|
+
console.log("\u274C Invalid chain. Use: base_sepolia, tempo_moderato, bnb_testnet, or solana_devnet");
|
|
4059
|
+
return;
|
|
4060
|
+
}
|
|
4061
|
+
if (chain === "solana_devnet") {
|
|
4062
|
+
if (!address) {
|
|
4063
|
+
address = getSolanaAddress(options.configDir);
|
|
4064
|
+
if (!address) {
|
|
4065
|
+
console.log("\u274C No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
|
|
4066
|
+
return;
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
if (!isValidSolanaAddress(address)) {
|
|
4070
|
+
console.log("\u274C Invalid Solana address");
|
|
4071
|
+
return;
|
|
4072
|
+
}
|
|
4073
|
+
console.log("\n\u{1F6B0} Solana Devnet Faucet (Gasless Mode)\n");
|
|
4074
|
+
console.log(` Address: ${address}
|
|
4075
|
+
`);
|
|
4076
|
+
let usdcSuccess = false;
|
|
4077
|
+
try {
|
|
4078
|
+
console.log(" \u23F3 Requesting 1 USDC from faucet...");
|
|
4079
|
+
const FAUCET_API = process.env.MOLTSPAY_FAUCET_API || "https://moltspay.com/api/v1/faucet";
|
|
4080
|
+
const response = await fetch(FAUCET_API, {
|
|
4081
|
+
method: "POST",
|
|
4082
|
+
headers: { "Content-Type": "application/json" },
|
|
4083
|
+
body: JSON.stringify({ address, chain: "solana_devnet" })
|
|
4084
|
+
});
|
|
4085
|
+
const result = await response.json();
|
|
4086
|
+
if (!response.ok) {
|
|
4087
|
+
console.log(` \u26A0\uFE0F USDC faucet: ${result.error || "Request failed"}`);
|
|
4088
|
+
if (result.hint) console.log(` ${result.hint}`);
|
|
4089
|
+
if (result.retry_after) console.log(` Retry after: ${result.retry_after}`);
|
|
4090
|
+
} else {
|
|
4091
|
+
console.log(` \u2705 Received ${result.amount} USDC!`);
|
|
4092
|
+
console.log(` Transaction: ${result.explorer}`);
|
|
4093
|
+
if (result.faucet_balance) {
|
|
4094
|
+
console.log(` Faucet balance: ${result.faucet_balance} USDC remaining`);
|
|
4095
|
+
}
|
|
4096
|
+
usdcSuccess = true;
|
|
4097
|
+
}
|
|
4098
|
+
} catch (error) {
|
|
4099
|
+
console.log(` \u26A0\uFE0F USDC faucet error: ${error.message}`);
|
|
4100
|
+
}
|
|
4101
|
+
console.log("");
|
|
4102
|
+
if (usdcSuccess) {
|
|
4103
|
+
console.log("\u{1F4A1} Check your balance:");
|
|
4104
|
+
console.log(" npx moltspay status\n");
|
|
4105
|
+
} else {
|
|
4106
|
+
console.log("\u274C Faucet request failed. Try again in a few minutes.\n");
|
|
4107
|
+
}
|
|
4108
|
+
return;
|
|
4109
|
+
}
|
|
2002
4110
|
if (!address) {
|
|
2003
4111
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
2004
4112
|
if (client.isInitialized) {
|
|
@@ -2013,37 +4121,110 @@ program.command("faucet").description("Request testnet USDC from MoltsPay faucet
|
|
|
2013
4121
|
return;
|
|
2014
4122
|
}
|
|
2015
4123
|
console.log("\n\u{1F6B0} MoltsPay Testnet Faucet\n");
|
|
2016
|
-
|
|
2017
|
-
|
|
4124
|
+
if (chain === "tempo_moderato") {
|
|
4125
|
+
console.log(` Requesting testnet tokens on Tempo Moderato...`);
|
|
4126
|
+
console.log(` Address: ${address}
|
|
2018
4127
|
`);
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
4128
|
+
try {
|
|
4129
|
+
const TEMPO_FAUCET_API = "https://docs.tempo.xyz/api/faucet";
|
|
4130
|
+
const response = await fetch(TEMPO_FAUCET_API, {
|
|
4131
|
+
method: "POST",
|
|
4132
|
+
headers: { "Content-Type": "application/json" },
|
|
4133
|
+
body: JSON.stringify({ address })
|
|
4134
|
+
});
|
|
4135
|
+
const result = await response.json();
|
|
4136
|
+
if (response.ok && result.data && result.data.length > 0) {
|
|
4137
|
+
console.log(`\u2705 Received testnet tokens!
|
|
4138
|
+
`);
|
|
4139
|
+
console.log(` Tokens: pathUSD, AlphaUSD, BetaUSD, ThetaUSD (1M each)`);
|
|
4140
|
+
console.log(` Transactions:`);
|
|
4141
|
+
for (const tx of result.data) {
|
|
4142
|
+
console.log(` https://explore.testnet.tempo.xyz/tx/${tx.hash}`);
|
|
4143
|
+
}
|
|
4144
|
+
console.log("\n\u{1F4A1} Use these tokens to test MPP payments:");
|
|
4145
|
+
console.log(` npx moltspay pay <service-url> <service-id> --chain tempo_moderato
|
|
4146
|
+
`);
|
|
4147
|
+
} else {
|
|
4148
|
+
console.log(`\u274C ${result.error || "Faucet request failed"}`);
|
|
4149
|
+
console.log("\n Try again later or use Tempo Wallet: https://wallet.tempo.xyz\n");
|
|
4150
|
+
}
|
|
4151
|
+
} catch (error) {
|
|
4152
|
+
console.log(`\u274C ${error.message}`);
|
|
4153
|
+
console.log("\n Try Tempo Wallet instead: https://wallet.tempo.xyz\n");
|
|
2032
4154
|
}
|
|
2033
|
-
|
|
4155
|
+
} else if (chain === "bnb_testnet") {
|
|
4156
|
+
console.log(` Requesting 1 USDC on BNB Testnet...`);
|
|
4157
|
+
console.log(` Address: ${address}
|
|
2034
4158
|
`);
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
4159
|
+
try {
|
|
4160
|
+
const FAUCET_API = process.env.MOLTSPAY_FAUCET_API || "https://moltspay.com/api/v1/faucet";
|
|
4161
|
+
const response = await fetch(FAUCET_API, {
|
|
4162
|
+
method: "POST",
|
|
4163
|
+
headers: { "Content-Type": "application/json" },
|
|
4164
|
+
body: JSON.stringify({ address, chain: "bnb_testnet" })
|
|
4165
|
+
});
|
|
4166
|
+
const result = await response.json();
|
|
4167
|
+
if (!response.ok) {
|
|
4168
|
+
console.log(`\u274C ${result.error || "Request failed"}`);
|
|
4169
|
+
if (result.hint) console.log(` ${result.hint}`);
|
|
4170
|
+
if (result.retry_after) console.log(` Retry after: ${result.retry_after}`);
|
|
4171
|
+
console.log("\n\u{1F4A1} Alternatively, get tokens manually:");
|
|
4172
|
+
console.log(` 1. Get test BNB: https://www.bnbchain.org/en/testnet-faucet`);
|
|
4173
|
+
console.log(` 2. Select "Peggy Tokens" -> USDC`);
|
|
4174
|
+
console.log(` 3. Enter: ${address}
|
|
2038
4175
|
`);
|
|
2039
|
-
|
|
2040
|
-
|
|
4176
|
+
return;
|
|
4177
|
+
}
|
|
4178
|
+
console.log(`\u2705 Received ${result.amount} ${result.token || "USDC"} on ${result.chain_name || "BNB Testnet"}!
|
|
2041
4179
|
`);
|
|
2042
|
-
|
|
2043
|
-
|
|
4180
|
+
console.log(` Transaction: ${result.explorer || `https://testnet.bscscan.com/tx/${result.transaction}`}`);
|
|
4181
|
+
if (result.faucet_balance) {
|
|
4182
|
+
console.log(` Faucet balance: ${result.faucet_balance} USDC`);
|
|
4183
|
+
}
|
|
4184
|
+
console.log("\n\u{1F4A1} Now you can test BNB payments:");
|
|
4185
|
+
console.log(` npx moltspay pay <service-url> <service-id> --chain bnb_testnet
|
|
4186
|
+
`);
|
|
4187
|
+
} catch (error) {
|
|
4188
|
+
console.log(`\u274C ${error.message}`);
|
|
4189
|
+
console.log("\n\u{1F4A1} Get tokens manually:");
|
|
4190
|
+
console.log(` 1. Get test BNB: https://www.bnbchain.org/en/testnet-faucet`);
|
|
4191
|
+
console.log(` 2. Select "Peggy Tokens" -> USDC`);
|
|
4192
|
+
console.log(` 3. Enter: ${address}
|
|
4193
|
+
`);
|
|
4194
|
+
}
|
|
4195
|
+
} else {
|
|
4196
|
+
console.log(` Requesting 1 USDC on Base Sepolia...`);
|
|
4197
|
+
console.log(` Address: ${address}
|
|
4198
|
+
`);
|
|
4199
|
+
try {
|
|
4200
|
+
const FAUCET_API = process.env.MOLTSPAY_FAUCET_API || "https://moltspay.com/api/v1/faucet";
|
|
4201
|
+
const response = await fetch(FAUCET_API, {
|
|
4202
|
+
method: "POST",
|
|
4203
|
+
headers: { "Content-Type": "application/json" },
|
|
4204
|
+
body: JSON.stringify({ address, chain: "base_sepolia" })
|
|
4205
|
+
});
|
|
4206
|
+
const result = await response.json();
|
|
4207
|
+
if (!response.ok) {
|
|
4208
|
+
console.log(`\u274C ${result.error || "Request failed"}`);
|
|
4209
|
+
if (result.hint) console.log(` ${result.hint}`);
|
|
4210
|
+
if (result.retry_after) console.log(` Retry after: ${result.retry_after}`);
|
|
4211
|
+
return;
|
|
4212
|
+
}
|
|
4213
|
+
console.log(`\u2705 Received ${result.amount} USDC!
|
|
4214
|
+
`);
|
|
4215
|
+
console.log(` Transaction: ${result.transaction}`);
|
|
4216
|
+
console.log(` Explorer: ${result.explorer}`);
|
|
4217
|
+
console.log(` Faucet balance: ${result.faucet_balance} USDC remaining
|
|
4218
|
+
`);
|
|
4219
|
+
console.log("\u{1F4A1} Use this USDC to test x402 payments:");
|
|
4220
|
+
console.log(` npx moltspay pay <service-url> <service-id> --chain base_sepolia
|
|
4221
|
+
`);
|
|
4222
|
+
} catch (error) {
|
|
4223
|
+
console.log(`\u274C ${error.message}`);
|
|
4224
|
+
}
|
|
2044
4225
|
}
|
|
2045
4226
|
});
|
|
2046
|
-
program.command("status").description("Show wallet status and balance").option("--config-dir <dir>", "Config directory",
|
|
4227
|
+
program.command("status").description("Show wallet status and balance").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).option("--json", "Output as JSON").action(async (options) => {
|
|
2047
4228
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
2048
4229
|
if (!client.isInitialized) {
|
|
2049
4230
|
if (options.json) {
|
|
@@ -2060,29 +4241,138 @@ program.command("status").description("Show wallet status and balance").option("
|
|
|
2060
4241
|
} catch (err) {
|
|
2061
4242
|
console.error("Warning: Could not fetch balances:", err.message);
|
|
2062
4243
|
}
|
|
4244
|
+
const solanaAddress = getSolanaAddress(options.configDir);
|
|
4245
|
+
let solanaBalances = {};
|
|
4246
|
+
if (solanaAddress) {
|
|
4247
|
+
try {
|
|
4248
|
+
solanaBalances.devnet = await getSolanaBalances(solanaAddress, "solana_devnet");
|
|
4249
|
+
} catch {
|
|
4250
|
+
}
|
|
4251
|
+
try {
|
|
4252
|
+
solanaBalances.mainnet = await getSolanaBalances(solanaAddress, "solana");
|
|
4253
|
+
} catch {
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
2063
4256
|
if (options.json) {
|
|
2064
|
-
|
|
4257
|
+
const output = {
|
|
2065
4258
|
address: client.address,
|
|
2066
4259
|
balances: allBalances,
|
|
2067
4260
|
limits: config.limits
|
|
2068
|
-
}
|
|
4261
|
+
};
|
|
4262
|
+
if (solanaAddress) {
|
|
4263
|
+
output.solana = {
|
|
4264
|
+
address: solanaAddress,
|
|
4265
|
+
balances: solanaBalances
|
|
4266
|
+
};
|
|
4267
|
+
}
|
|
4268
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2069
4269
|
} else {
|
|
2070
4270
|
console.log("\n\u{1F4CA} MoltsPay Wallet Status\n");
|
|
2071
4271
|
console.log(` Address: ${client.address}`);
|
|
2072
4272
|
console.log("");
|
|
2073
4273
|
console.log(" Balances:");
|
|
2074
4274
|
for (const [chainName, balance] of Object.entries(allBalances)) {
|
|
2075
|
-
|
|
2076
|
-
|
|
4275
|
+
let chainLabel;
|
|
4276
|
+
if (chainName === "base_sepolia") {
|
|
4277
|
+
chainLabel = "Base Sepolia";
|
|
4278
|
+
} else if (chainName === "tempo_moderato") {
|
|
4279
|
+
chainLabel = "Tempo Moderato";
|
|
4280
|
+
} else {
|
|
4281
|
+
chainLabel = chainName.charAt(0).toUpperCase() + chainName.slice(1);
|
|
4282
|
+
}
|
|
4283
|
+
if (chainName === "tempo_moderato" && balance.tempo) {
|
|
4284
|
+
const tempo = balance.tempo;
|
|
4285
|
+
const nativeStr = balance.native > 1e12 ? balance.native.toExponential(2) : balance.native.toFixed(2);
|
|
4286
|
+
console.log(` ${chainLabel}:`);
|
|
4287
|
+
console.log(` Native: ${nativeStr} TEMPO (for gas)`);
|
|
4288
|
+
console.log(` pathUSD: ${tempo.pathUSD.toFixed(2)}`);
|
|
4289
|
+
console.log(` alphaUSD: ${tempo.alphaUSD.toFixed(2)}`);
|
|
4290
|
+
console.log(` betaUSD: ${tempo.betaUSD.toFixed(2)}`);
|
|
4291
|
+
console.log(` thetaUSD: ${tempo.thetaUSD.toFixed(2)}`);
|
|
4292
|
+
} else if (chainName === "bnb" || chainName === "bnb_testnet") {
|
|
4293
|
+
const bnbBalance = balance.native;
|
|
4294
|
+
const bnbWarning = bnbBalance < 5e-4 ? " \u26A0\uFE0F Low gas" : "";
|
|
4295
|
+
console.log(` ${chainLabel.padEnd(14)} ${balance.usdc.toFixed(2)} USDC | ${balance.usdt.toFixed(2)} USDT | ${bnbBalance.toFixed(4)} BNB${bnbWarning}`);
|
|
4296
|
+
} else {
|
|
4297
|
+
console.log(` ${chainLabel.padEnd(14)} ${balance.usdc.toFixed(2)} USDC | ${balance.usdt.toFixed(2)} USDT`);
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
const address = client.address;
|
|
4301
|
+
let bnbApprovalStatus = null;
|
|
4302
|
+
let bnbTestnetApprovalStatus = null;
|
|
4303
|
+
try {
|
|
4304
|
+
if (allBalances["bnb"]) {
|
|
4305
|
+
bnbApprovalStatus = await checkBNBApprovals(address, "bnb", options.configDir);
|
|
4306
|
+
}
|
|
4307
|
+
if (allBalances["bnb_testnet"]) {
|
|
4308
|
+
bnbTestnetApprovalStatus = await checkBNBApprovals(address, "bnb_testnet", options.configDir);
|
|
4309
|
+
}
|
|
4310
|
+
} catch {
|
|
4311
|
+
}
|
|
4312
|
+
if (bnbApprovalStatus || bnbTestnetApprovalStatus) {
|
|
4313
|
+
console.log("");
|
|
4314
|
+
console.log(" BNB Approvals (pay-for-success):");
|
|
4315
|
+
if (bnbApprovalStatus) {
|
|
4316
|
+
if (!bnbApprovalStatus.spender) {
|
|
4317
|
+
console.log(" BNB: \u26A0\uFE0F No spender configured");
|
|
4318
|
+
console.log(" \u2514\u2500 Run a payment first, or: npx moltspay approve --chain bnb --spender <address>");
|
|
4319
|
+
} else {
|
|
4320
|
+
const status = bnbApprovalStatus.usdt && bnbApprovalStatus.usdc ? "\u2705" : "\u26A0\uFE0F";
|
|
4321
|
+
const tokens = [
|
|
4322
|
+
bnbApprovalStatus.usdt ? "USDT\u2713" : "USDT\u2717",
|
|
4323
|
+
bnbApprovalStatus.usdc ? "USDC\u2713" : "USDC\u2717"
|
|
4324
|
+
].join(", ");
|
|
4325
|
+
console.log(` BNB: ${status} ${tokens}`);
|
|
4326
|
+
const bnbNative = allBalances["bnb"]?.native || 0;
|
|
4327
|
+
if (!bnbApprovalStatus.usdc && !bnbApprovalStatus.usdt && bnbNative < 5e-4) {
|
|
4328
|
+
console.log(" \u26A0\uFE0F Need ~0.001 BNB for first approval tx. Get from exchange.");
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
if (bnbTestnetApprovalStatus) {
|
|
4333
|
+
if (!bnbTestnetApprovalStatus.spender) {
|
|
4334
|
+
console.log(" BNB Testnet: \u26A0\uFE0F No spender configured");
|
|
4335
|
+
console.log(" \u2514\u2500 Run a payment first, or: npx moltspay approve --chain bnb_testnet --spender <address>");
|
|
4336
|
+
} else {
|
|
4337
|
+
const status = bnbTestnetApprovalStatus.usdt && bnbTestnetApprovalStatus.usdc ? "\u2705" : "\u26A0\uFE0F";
|
|
4338
|
+
const tokens = [
|
|
4339
|
+
bnbTestnetApprovalStatus.usdt ? "USDT\u2713" : "USDT\u2717",
|
|
4340
|
+
bnbTestnetApprovalStatus.usdc ? "USDC\u2713" : "USDC\u2717"
|
|
4341
|
+
].join(", ");
|
|
4342
|
+
console.log(` BNB Testnet: ${status} ${tokens}`);
|
|
4343
|
+
const tbnbNative = allBalances["bnb_testnet"]?.native || 0;
|
|
4344
|
+
if (!bnbTestnetApprovalStatus.usdc && !bnbTestnetApprovalStatus.usdt && tbnbNative < 5e-4) {
|
|
4345
|
+
console.log(" \u26A0\uFE0F Need tBNB for approval. Run: npx moltspay faucet --chain bnb_testnet");
|
|
4346
|
+
}
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
2077
4349
|
}
|
|
2078
4350
|
console.log("");
|
|
2079
4351
|
console.log(" Spending Limits:");
|
|
2080
4352
|
console.log(` Per Transaction: $${config.limits.maxPerTx}`);
|
|
2081
4353
|
console.log(` Daily: $${config.limits.maxPerDay}`);
|
|
4354
|
+
const solanaAddress2 = getSolanaAddress(options.configDir);
|
|
4355
|
+
if (solanaAddress2) {
|
|
4356
|
+
console.log("");
|
|
4357
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
4358
|
+
console.log(` \u{1F7E3} Solana: ${solanaAddress2}`);
|
|
4359
|
+
try {
|
|
4360
|
+
const devnetBalances = await getSolanaBalances(solanaAddress2, "solana_devnet");
|
|
4361
|
+
console.log(` Devnet: ${devnetBalances.sol.toFixed(4)} SOL | ${devnetBalances.usdc.toFixed(2)} USDC`);
|
|
4362
|
+
} catch (err) {
|
|
4363
|
+
console.log(` Devnet: (unable to fetch)`);
|
|
4364
|
+
}
|
|
4365
|
+
try {
|
|
4366
|
+
const mainnetBalances = await getSolanaBalances(solanaAddress2, "solana");
|
|
4367
|
+
console.log(` Mainnet: ${mainnetBalances.sol.toFixed(4)} SOL | ${mainnetBalances.usdc.toFixed(2)} USDC`);
|
|
4368
|
+
} catch (err) {
|
|
4369
|
+
console.log(` Mainnet: (unable to fetch)`);
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
2082
4372
|
console.log("");
|
|
2083
4373
|
}
|
|
2084
4374
|
});
|
|
2085
|
-
program.command("list").description("List recent transactions").option("--days <n>", "Number of days to look back", "7").option("--chain <chain>", "Chain to query (base, polygon, base_sepolia, or all)", "all").option("--limit <n>", "Max transactions to show", "20").option("--config-dir <dir>", "Config directory",
|
|
4375
|
+
program.command("list").description("List recent transactions").option("--days <n>", "Number of days to look back", "7").option("--chain <chain>", "Chain to query (base, polygon, base_sepolia, or all)", "all").option("--limit <n>", "Max transactions to show", "20").option("--config-dir <dir>", "Config directory", DEFAULT_CONFIG_DIR2).action(async (options) => {
|
|
2086
4376
|
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
2087
4377
|
if (!client.isInitialized) {
|
|
2088
4378
|
console.log("\u274C Not initialized. Run: npx moltspay init");
|
|
@@ -2091,8 +4381,8 @@ program.command("list").description("List recent transactions").option("--days <
|
|
|
2091
4381
|
const days = parseInt(options.days) || 7;
|
|
2092
4382
|
const limit = parseInt(options.limit) || 20;
|
|
2093
4383
|
const chain = options.chain?.toLowerCase() || "all";
|
|
2094
|
-
if (!["base", "polygon", "base_sepolia", "all"].includes(chain)) {
|
|
2095
|
-
console.log("\u274C Invalid chain. Use: base, polygon, base_sepolia, or all");
|
|
4384
|
+
if (!["base", "polygon", "base_sepolia", "tempo_moderato", "all"].includes(chain)) {
|
|
4385
|
+
console.log("\u274C Invalid chain. Use: base, polygon, base_sepolia, tempo_moderato, or all");
|
|
2096
4386
|
return;
|
|
2097
4387
|
}
|
|
2098
4388
|
const wallet = client.address;
|
|
@@ -2112,9 +4402,16 @@ program.command("list").description("List recent transactions").option("--days <
|
|
|
2112
4402
|
api: "https://base-sepolia.blockscout.com/api/v2",
|
|
2113
4403
|
usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
2114
4404
|
name: "Base Sepolia"
|
|
4405
|
+
},
|
|
4406
|
+
// Tempo explorer doesn't have public API yet
|
|
4407
|
+
tempo_moderato: {
|
|
4408
|
+
api: "",
|
|
4409
|
+
// No API available
|
|
4410
|
+
usdc: "0x20c0000000000000000000000000000000000000",
|
|
4411
|
+
name: "Tempo Moderato"
|
|
2115
4412
|
}
|
|
2116
4413
|
};
|
|
2117
|
-
const chainsToQuery = chain === "all" ? ["base", "polygon", "base_sepolia"] : [chain];
|
|
4414
|
+
const chainsToQuery = chain === "all" ? ["base", "polygon", "base_sepolia", "tempo_moderato"] : [chain];
|
|
2118
4415
|
console.log(`
|
|
2119
4416
|
\u{1F4DC} Transactions (last ${days} day${days > 1 ? "s" : ""})
|
|
2120
4417
|
`);
|
|
@@ -2122,27 +4419,136 @@ program.command("list").description("List recent transactions").option("--days <
|
|
|
2122
4419
|
for (const c of chainsToQuery) {
|
|
2123
4420
|
const explorer = explorers[c];
|
|
2124
4421
|
try {
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
4422
|
+
if (c === "tempo_moderato") {
|
|
4423
|
+
const tempoTokens = [
|
|
4424
|
+
{ address: "0x20c0000000000000000000000000000000000000", name: "pathUSD" },
|
|
4425
|
+
{ address: "0x20c0000000000000000000000000000000000001", name: "alphaUSD" },
|
|
4426
|
+
{ address: "0x20c0000000000000000000000000000000000002", name: "betaUSD" },
|
|
4427
|
+
{ address: "0x20c0000000000000000000000000000000000003", name: "thetaUSD" }
|
|
4428
|
+
];
|
|
4429
|
+
const transferTopic = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
4430
|
+
const walletTopic = "0x000000000000000000000000" + wallet.toLowerCase().slice(2);
|
|
4431
|
+
let latestBlock = 0;
|
|
4432
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
4433
|
+
try {
|
|
4434
|
+
const blockRes = await fetch("https://rpc.moderato.tempo.xyz", {
|
|
4435
|
+
method: "POST",
|
|
4436
|
+
headers: { "Content-Type": "application/json" },
|
|
4437
|
+
body: JSON.stringify({ jsonrpc: "2.0", method: "eth_blockNumber", params: [], id: 1 })
|
|
4438
|
+
});
|
|
4439
|
+
const blockData = await blockRes.json();
|
|
4440
|
+
if (blockData.result) {
|
|
4441
|
+
latestBlock = parseInt(blockData.result, 16);
|
|
4442
|
+
break;
|
|
4443
|
+
}
|
|
4444
|
+
} catch (e) {
|
|
4445
|
+
if (attempt === 2) throw e;
|
|
4446
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
if (latestBlock === 0) {
|
|
4450
|
+
console.log(" \u26A0\uFE0F Tempo Moderato: Could not get latest block");
|
|
4451
|
+
continue;
|
|
4452
|
+
}
|
|
4453
|
+
const maxBlocks = 1e5;
|
|
4454
|
+
const blocksPerDay = 172800;
|
|
4455
|
+
const requestedBlocks = blocksPerDay * days;
|
|
4456
|
+
const actualBlocks = Math.min(requestedBlocks, maxBlocks);
|
|
4457
|
+
const fromBlock = "0x" + Math.max(0, latestBlock - actualBlocks).toString(16);
|
|
4458
|
+
const toBlock = "0x" + latestBlock.toString(16);
|
|
4459
|
+
if (requestedBlocks > maxBlocks) {
|
|
4460
|
+
console.log(` \u2139\uFE0F Tempo: querying last ~14 hours (RPC limit: 100k blocks)`);
|
|
4461
|
+
}
|
|
4462
|
+
for (const token of tempoTokens) {
|
|
4463
|
+
try {
|
|
4464
|
+
const inRes = await fetch("https://rpc.moderato.tempo.xyz", {
|
|
4465
|
+
method: "POST",
|
|
4466
|
+
headers: { "Content-Type": "application/json" },
|
|
4467
|
+
body: JSON.stringify({
|
|
4468
|
+
jsonrpc: "2.0",
|
|
4469
|
+
method: "eth_getLogs",
|
|
4470
|
+
params: [{ fromBlock, toBlock, address: token.address, topics: [transferTopic, null, walletTopic] }],
|
|
4471
|
+
id: 1
|
|
4472
|
+
})
|
|
4473
|
+
});
|
|
4474
|
+
const inData = await inRes.json();
|
|
4475
|
+
if (inData.error) {
|
|
4476
|
+
console.log(` \u26A0\uFE0F ${token.name}: ${inData.error.message}`);
|
|
4477
|
+
continue;
|
|
4478
|
+
}
|
|
4479
|
+
if (inData.result && Array.isArray(inData.result)) {
|
|
4480
|
+
for (const log of inData.result) {
|
|
4481
|
+
const timestamp = parseInt(log.blockTimestamp, 16) * 1e3;
|
|
4482
|
+
if (timestamp < cutoffTime) continue;
|
|
4483
|
+
const amount = parseInt(log.data, 16) / 1e6;
|
|
4484
|
+
const from = "0x" + log.topics[1].slice(26);
|
|
4485
|
+
allTxns.push({
|
|
4486
|
+
chain: c,
|
|
4487
|
+
timestamp,
|
|
4488
|
+
type: "IN",
|
|
4489
|
+
amount,
|
|
4490
|
+
other: from,
|
|
4491
|
+
hash: log.transactionHash,
|
|
4492
|
+
token: token.name
|
|
4493
|
+
});
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4496
|
+
const outRes = await fetch("https://rpc.moderato.tempo.xyz", {
|
|
4497
|
+
method: "POST",
|
|
4498
|
+
headers: { "Content-Type": "application/json" },
|
|
4499
|
+
body: JSON.stringify({
|
|
4500
|
+
jsonrpc: "2.0",
|
|
4501
|
+
method: "eth_getLogs",
|
|
4502
|
+
params: [{ fromBlock, toBlock, address: token.address, topics: [transferTopic, walletTopic, null] }],
|
|
4503
|
+
id: 1
|
|
4504
|
+
})
|
|
4505
|
+
});
|
|
4506
|
+
const outData = await outRes.json();
|
|
4507
|
+
if (outData.result && Array.isArray(outData.result)) {
|
|
4508
|
+
for (const log of outData.result) {
|
|
4509
|
+
const timestamp = parseInt(log.blockTimestamp, 16) * 1e3;
|
|
4510
|
+
if (timestamp < cutoffTime) continue;
|
|
4511
|
+
const amount = parseInt(log.data, 16) / 1e6;
|
|
4512
|
+
const to = "0x" + log.topics[2].slice(26);
|
|
4513
|
+
allTxns.push({
|
|
4514
|
+
chain: c,
|
|
4515
|
+
timestamp,
|
|
4516
|
+
type: "OUT",
|
|
4517
|
+
amount,
|
|
4518
|
+
other: to,
|
|
4519
|
+
hash: log.transactionHash,
|
|
4520
|
+
token: token.name
|
|
4521
|
+
});
|
|
4522
|
+
}
|
|
4523
|
+
}
|
|
4524
|
+
} catch (tokenError) {
|
|
4525
|
+
continue;
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
} else {
|
|
4529
|
+
const url = `${explorer.api}/addresses/${wallet}/token-transfers?type=ERC-20&token=${explorer.usdc}`;
|
|
4530
|
+
const response = await fetch(url);
|
|
4531
|
+
const data = await response.json();
|
|
4532
|
+
if (data.items && Array.isArray(data.items)) {
|
|
4533
|
+
for (const tx of data.items) {
|
|
4534
|
+
const timestamp = new Date(tx.timestamp).getTime();
|
|
4535
|
+
if (timestamp < cutoffTime) continue;
|
|
4536
|
+
const isIncoming = tx.to.hash.toLowerCase() === wallet.toLowerCase();
|
|
4537
|
+
const decimals = parseInt(tx.total.decimals) || 6;
|
|
4538
|
+
allTxns.push({
|
|
4539
|
+
chain: c,
|
|
4540
|
+
timestamp,
|
|
4541
|
+
type: isIncoming ? "IN" : "OUT",
|
|
4542
|
+
amount: parseInt(tx.total.value) / Math.pow(10, decimals),
|
|
4543
|
+
other: isIncoming ? tx.from.hash : tx.to.hash,
|
|
4544
|
+
hash: tx.transaction_hash
|
|
4545
|
+
});
|
|
4546
|
+
}
|
|
2142
4547
|
}
|
|
2143
4548
|
}
|
|
2144
4549
|
} catch (error) {
|
|
2145
|
-
|
|
4550
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
4551
|
+
console.log(` \u26A0\uFE0F ${explorer.name}: ${errMsg}`);
|
|
2146
4552
|
}
|
|
2147
4553
|
}
|
|
2148
4554
|
allTxns.sort((a, b) => b.timestamp - a.timestamp);
|
|
@@ -2155,8 +4561,12 @@ program.command("list").description("List recent transactions").option("--days <
|
|
|
2155
4561
|
const color = tx.type === "IN" ? "\x1B[32m" : "\x1B[31m";
|
|
2156
4562
|
const reset = "\x1B[0m";
|
|
2157
4563
|
const date = new Date(tx.timestamp).toISOString().slice(5, 16).replace("T", " ");
|
|
2158
|
-
|
|
2159
|
-
|
|
4564
|
+
let chainLabel = tx.chain.toUpperCase();
|
|
4565
|
+
if (tx.chain === "tempo_moderato") chainLabel = "TEMPO";
|
|
4566
|
+
else if (tx.chain === "base_sepolia") chainLabel = "BASE_SEPOLIA";
|
|
4567
|
+
const chainTag = chain === "all" ? `[${chainLabel}] ` : "";
|
|
4568
|
+
const tokenName = tx.token || "USDC";
|
|
4569
|
+
console.log(` ${color}${sign}${tx.amount.toFixed(2)} ${tokenName}${reset} | ${chainTag}${tx.type === "IN" ? "from" : "to"} ${tx.other.slice(0, 10)}...${tx.other.slice(-4)} | ${date}`);
|
|
2160
4570
|
}
|
|
2161
4571
|
const inTotal = allTxns.filter((t) => t.type === "IN").reduce((s, t) => s + t.amount, 0);
|
|
2162
4572
|
const outTotal = allTxns.filter((t) => t.type === "OUT").reduce((s, t) => s + t.amount, 0);
|
|
@@ -2165,39 +4575,88 @@ program.command("list").description("List recent transactions").option("--days <
|
|
|
2165
4575
|
`);
|
|
2166
4576
|
}
|
|
2167
4577
|
});
|
|
2168
|
-
program.command("services
|
|
4578
|
+
program.command("services [url]").description("List services from registry or a specific provider").option("-q, --query <keyword>", "Search by keyword (name, description, tags)").option("--max-price <price>", "Maximum price in USD").option("--type <type>", "Filter by type: api_service | file_download").option("--tag <tag>", "Filter by tag").option("--json", "Output as JSON").action(async (url, options) => {
|
|
4579
|
+
const MOLTSPAY_REGISTRY = "https://moltspay.com";
|
|
2169
4580
|
try {
|
|
2170
|
-
|
|
2171
|
-
|
|
4581
|
+
let services;
|
|
4582
|
+
let isRegistry = false;
|
|
4583
|
+
if (url) {
|
|
4584
|
+
const client = new MoltsPayClient();
|
|
4585
|
+
services = await client.getServices(url);
|
|
4586
|
+
} else {
|
|
4587
|
+
isRegistry = true;
|
|
4588
|
+
const params = new URLSearchParams();
|
|
4589
|
+
if (options.query) params.set("q", options.query);
|
|
4590
|
+
if (options.maxPrice) params.set("maxPrice", options.maxPrice);
|
|
4591
|
+
if (options.type) params.set("type", options.type);
|
|
4592
|
+
if (options.tag) params.set("tag", options.tag);
|
|
4593
|
+
const queryString = params.toString();
|
|
4594
|
+
const registryUrl = `${MOLTSPAY_REGISTRY}/registry/services${queryString ? "?" + queryString : ""}`;
|
|
4595
|
+
const res = await fetch(registryUrl);
|
|
4596
|
+
if (!res.ok) {
|
|
4597
|
+
throw new Error(`Registry request failed: ${res.status}`);
|
|
4598
|
+
}
|
|
4599
|
+
services = await res.json();
|
|
4600
|
+
}
|
|
2172
4601
|
if (options.json) {
|
|
2173
4602
|
console.log(JSON.stringify(services, null, 2));
|
|
2174
4603
|
} else {
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
4604
|
+
const serviceList = services.services || [];
|
|
4605
|
+
if (isRegistry) {
|
|
4606
|
+
if (options.query) {
|
|
4607
|
+
console.log(`
|
|
4608
|
+
\u{1F50D} Search: "${options.query}" (${serviceList.length} results)
|
|
4609
|
+
`);
|
|
4610
|
+
} else {
|
|
4611
|
+
const filters = [];
|
|
4612
|
+
if (options.maxPrice) filters.push(`max $${options.maxPrice}`);
|
|
4613
|
+
if (options.type) filters.push(options.type);
|
|
4614
|
+
if (options.tag) filters.push(`#${options.tag}`);
|
|
4615
|
+
const filterStr = filters.length > 0 ? ` (${filters.join(", ")})` : "";
|
|
4616
|
+
console.log(`
|
|
4617
|
+
\u{1F50D} MoltsPay Registry${filterStr} - ${serviceList.length} services
|
|
2178
4618
|
`);
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
4619
|
+
}
|
|
4620
|
+
for (const svc of serviceList) {
|
|
4621
|
+
const name = (svc.name || svc.id).slice(0, 30).padEnd(30);
|
|
4622
|
+
const price = `$${svc.price}`.padEnd(8);
|
|
4623
|
+
const type = (svc.type || "unknown").padEnd(14);
|
|
4624
|
+
const provider = `@${svc.provider?.username || "unknown"}`;
|
|
4625
|
+
console.log(` ${name} ${price} ${type} ${provider}`);
|
|
4626
|
+
}
|
|
4627
|
+
if (serviceList.length > 0) {
|
|
4628
|
+
console.log(`
|
|
4629
|
+
\u{1F4A1} Use: moltspay pay <provider-url> <service-id>
|
|
4630
|
+
`);
|
|
4631
|
+
}
|
|
2183
4632
|
} else {
|
|
2184
|
-
|
|
2185
|
-
|
|
4633
|
+
if (services.provider) {
|
|
4634
|
+
console.log(`
|
|
4635
|
+
\u{1F3EA} ${services.provider.name}
|
|
4636
|
+
`);
|
|
4637
|
+
console.log(` ${services.provider.description || ""}`);
|
|
4638
|
+
console.log(` Wallet: ${services.provider.wallet}`);
|
|
4639
|
+
const chains = services.provider.chains ? Array.isArray(services.provider.chains) ? services.provider.chains.map((c) => typeof c === "string" ? c : c.chain).join(", ") : services.provider.chains : services.provider.chain || "base";
|
|
4640
|
+
console.log(` Chains: ${chains}`);
|
|
4641
|
+
} else {
|
|
4642
|
+
console.log(`
|
|
4643
|
+
\u{1F3EA} Provider Services
|
|
2186
4644
|
`);
|
|
2187
|
-
|
|
2188
|
-
}
|
|
2189
|
-
console.log("\n\u{1F4E6} Services:\n");
|
|
2190
|
-
for (const svc of services.services) {
|
|
2191
|
-
const status = svc.available !== false ? "\u2705" : "\u274C";
|
|
2192
|
-
console.log(` ${status} ${svc.id || svc.name}`);
|
|
2193
|
-
console.log(` ${svc.name} - $${svc.price} ${svc.currency}`);
|
|
2194
|
-
if (svc.description) {
|
|
2195
|
-
console.log(` ${svc.description}`);
|
|
4645
|
+
console.log(` ${serviceList.length} services available`);
|
|
2196
4646
|
}
|
|
2197
|
-
|
|
2198
|
-
|
|
4647
|
+
console.log("\n\u{1F4E6} Services:\n");
|
|
4648
|
+
for (const svc of serviceList) {
|
|
4649
|
+
const status = svc.available !== false ? "\u2705" : "\u274C";
|
|
4650
|
+
console.log(` ${status} ${svc.id || svc.name}`);
|
|
4651
|
+
console.log(` ${svc.name} - $${svc.price} ${svc.currency}`);
|
|
4652
|
+
if (svc.description) {
|
|
4653
|
+
console.log(` ${svc.description}`);
|
|
4654
|
+
}
|
|
4655
|
+
if (svc.provider && !services.provider) {
|
|
4656
|
+
console.log(` Provider: ${svc.provider.name || svc.provider.username}`);
|
|
4657
|
+
}
|
|
4658
|
+
console.log("");
|
|
2199
4659
|
}
|
|
2200
|
-
console.log("");
|
|
2201
4660
|
}
|
|
2202
4661
|
}
|
|
2203
4662
|
} catch (err) {
|
|
@@ -2216,18 +4675,18 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
2216
4675
|
const handlers = /* @__PURE__ */ new Map();
|
|
2217
4676
|
let provider = null;
|
|
2218
4677
|
for (const inputPath of allPaths) {
|
|
2219
|
-
const resolvedPath = (0,
|
|
4678
|
+
const resolvedPath = (0, import_path3.resolve)(inputPath);
|
|
2220
4679
|
let manifestPath;
|
|
2221
4680
|
let skillDir;
|
|
2222
4681
|
let isSkillDir = false;
|
|
2223
|
-
if ((0,
|
|
2224
|
-
manifestPath = (0,
|
|
4682
|
+
if ((0, import_fs5.existsSync)((0, import_path3.join)(resolvedPath, "moltspay.services.json"))) {
|
|
4683
|
+
manifestPath = (0, import_path3.join)(resolvedPath, "moltspay.services.json");
|
|
2225
4684
|
skillDir = resolvedPath;
|
|
2226
4685
|
isSkillDir = true;
|
|
2227
|
-
} else if ((0,
|
|
4686
|
+
} else if ((0, import_fs5.existsSync)(resolvedPath) && resolvedPath.endsWith(".json")) {
|
|
2228
4687
|
manifestPath = resolvedPath;
|
|
2229
|
-
skillDir = (0,
|
|
2230
|
-
} else if ((0,
|
|
4688
|
+
skillDir = (0, import_path3.dirname)(resolvedPath);
|
|
4689
|
+
} else if ((0, import_fs5.existsSync)(resolvedPath)) {
|
|
2231
4690
|
console.error(`\u274C No moltspay.services.json found in: ${resolvedPath}`);
|
|
2232
4691
|
continue;
|
|
2233
4692
|
} else {
|
|
@@ -2236,25 +4695,25 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
2236
4695
|
}
|
|
2237
4696
|
console.log(`\u{1F4E6} Loading: ${manifestPath}`);
|
|
2238
4697
|
try {
|
|
2239
|
-
const manifestContent = JSON.parse((0,
|
|
4698
|
+
const manifestContent = JSON.parse((0, import_fs5.readFileSync)(manifestPath, "utf-8"));
|
|
2240
4699
|
if (!provider) {
|
|
2241
4700
|
provider = manifestContent.provider;
|
|
2242
4701
|
}
|
|
2243
4702
|
let skillModule = null;
|
|
2244
4703
|
if (isSkillDir) {
|
|
2245
4704
|
let entryPoint = "index.js";
|
|
2246
|
-
const pkgJsonPath = (0,
|
|
2247
|
-
if ((0,
|
|
4705
|
+
const pkgJsonPath = (0, import_path3.join)(skillDir, "package.json");
|
|
4706
|
+
if ((0, import_fs5.existsSync)(pkgJsonPath)) {
|
|
2248
4707
|
try {
|
|
2249
|
-
const pkgJson = JSON.parse((0,
|
|
4708
|
+
const pkgJson = JSON.parse((0, import_fs5.readFileSync)(pkgJsonPath, "utf-8"));
|
|
2250
4709
|
if (pkgJson.main) {
|
|
2251
4710
|
entryPoint = pkgJson.main;
|
|
2252
4711
|
}
|
|
2253
4712
|
} catch {
|
|
2254
4713
|
}
|
|
2255
4714
|
}
|
|
2256
|
-
const modulePath = (0,
|
|
2257
|
-
if ((0,
|
|
4715
|
+
const modulePath = (0, import_path3.join)(skillDir, entryPoint);
|
|
4716
|
+
if ((0, import_fs5.existsSync)(modulePath)) {
|
|
2258
4717
|
try {
|
|
2259
4718
|
skillModule = await import(modulePath);
|
|
2260
4719
|
console.log(` \u2705 Loaded module: ${modulePath}`);
|
|
@@ -2332,8 +4791,8 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
2332
4791
|
provider,
|
|
2333
4792
|
services: allServices
|
|
2334
4793
|
};
|
|
2335
|
-
const tempManifestPath = (0,
|
|
2336
|
-
(0,
|
|
4794
|
+
const tempManifestPath = (0, import_path3.join)(DEFAULT_CONFIG_DIR2, "combined-manifest.json");
|
|
4795
|
+
(0, import_fs5.writeFileSync)(tempManifestPath, JSON.stringify(combinedManifest, null, 2));
|
|
2337
4796
|
console.log(`
|
|
2338
4797
|
\u{1F4CB} Combined manifest: ${allServices.length} services`);
|
|
2339
4798
|
console.log(` Provider: ${provider.name}`);
|
|
@@ -2346,12 +4805,12 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
2346
4805
|
server.skill(serviceId, handler);
|
|
2347
4806
|
}
|
|
2348
4807
|
const pidData = { pid: process.pid, port, paths: allPaths };
|
|
2349
|
-
(0,
|
|
4808
|
+
(0, import_fs5.writeFileSync)(PID_FILE, JSON.stringify(pidData, null, 2));
|
|
2350
4809
|
server.listen(port);
|
|
2351
4810
|
const cleanup = () => {
|
|
2352
4811
|
try {
|
|
2353
|
-
if ((0,
|
|
2354
|
-
if ((0,
|
|
4812
|
+
if ((0, import_fs5.existsSync)(PID_FILE)) (0, import_fs5.unlinkSync)(PID_FILE);
|
|
4813
|
+
if ((0, import_fs5.existsSync)(tempManifestPath)) (0, import_fs5.unlinkSync)(tempManifestPath);
|
|
2355
4814
|
} catch {
|
|
2356
4815
|
}
|
|
2357
4816
|
};
|
|
@@ -2372,12 +4831,12 @@ program.command("start <paths...>").description("Start MoltsPay server from skil
|
|
|
2372
4831
|
}
|
|
2373
4832
|
});
|
|
2374
4833
|
program.command("stop").description("Stop the running MoltsPay server").action(async () => {
|
|
2375
|
-
if (!(0,
|
|
4834
|
+
if (!(0, import_fs5.existsSync)(PID_FILE)) {
|
|
2376
4835
|
console.log("\u274C No running server found (no PID file)");
|
|
2377
4836
|
process.exit(1);
|
|
2378
4837
|
}
|
|
2379
4838
|
try {
|
|
2380
|
-
const pidData = JSON.parse((0,
|
|
4839
|
+
const pidData = JSON.parse((0, import_fs5.readFileSync)(PID_FILE, "utf-8"));
|
|
2381
4840
|
const { pid, port, manifest } = pidData;
|
|
2382
4841
|
console.log(`
|
|
2383
4842
|
\u{1F6D1} Stopping MoltsPay Server
|
|
@@ -2390,7 +4849,7 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
|
|
|
2390
4849
|
process.kill(pid, 0);
|
|
2391
4850
|
} catch {
|
|
2392
4851
|
console.log("\u26A0\uFE0F Process not running, cleaning up PID file...");
|
|
2393
|
-
(0,
|
|
4852
|
+
(0, import_fs5.unlinkSync)(PID_FILE);
|
|
2394
4853
|
process.exit(0);
|
|
2395
4854
|
}
|
|
2396
4855
|
process.kill(pid, "SIGTERM");
|
|
@@ -2402,8 +4861,8 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
|
|
|
2402
4861
|
process.kill(pid, "SIGKILL");
|
|
2403
4862
|
} catch {
|
|
2404
4863
|
}
|
|
2405
|
-
if ((0,
|
|
2406
|
-
(0,
|
|
4864
|
+
if ((0, import_fs5.existsSync)(PID_FILE)) {
|
|
4865
|
+
(0, import_fs5.unlinkSync)(PID_FILE);
|
|
2407
4866
|
}
|
|
2408
4867
|
console.log("\u2705 Server stopped\n");
|
|
2409
4868
|
} catch (err) {
|
|
@@ -2411,8 +4870,8 @@ program.command("stop").description("Stop the running MoltsPay server").action(a
|
|
|
2411
4870
|
process.exit(1);
|
|
2412
4871
|
}
|
|
2413
4872
|
});
|
|
2414
|
-
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, or
|
|
2415
|
-
const client = new MoltsPayClient();
|
|
4873
|
+
program.command("pay <server> <service> [params]").description("Pay for a service and get the result").option("--prompt <text>", "Prompt for the service").option("--image <path>", "Image URL or local file path").option("--token <token>", "Token to pay with (USDC or USDT)", "USDC").option("--chain <chain>", "Chain to pay on (base, polygon, base_sepolia, tempo_moderato, solana, or solana_devnet).").option("--config-dir <dir>", "Config directory with wallet.json", DEFAULT_CONFIG_DIR2).option("--json", "Output raw JSON only").action(async (server, service, paramsJson, options) => {
|
|
4874
|
+
const client = new MoltsPayClient({ configDir: options.configDir });
|
|
2416
4875
|
if (!client.isInitialized) {
|
|
2417
4876
|
console.error("\u274C Wallet not initialized. Run: npx moltspay init");
|
|
2418
4877
|
process.exit(1);
|
|
@@ -2432,22 +4891,19 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
2432
4891
|
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
|
2433
4892
|
params.image_url = imagePath;
|
|
2434
4893
|
} else {
|
|
2435
|
-
const filePath = (0,
|
|
2436
|
-
if (!(0,
|
|
4894
|
+
const filePath = (0, import_path3.resolve)(imagePath);
|
|
4895
|
+
if (!(0, import_fs5.existsSync)(filePath)) {
|
|
2437
4896
|
console.error(`\u274C Image file not found: ${filePath}`);
|
|
2438
4897
|
process.exit(1);
|
|
2439
4898
|
}
|
|
2440
|
-
const imageData = (0,
|
|
4899
|
+
const imageData = (0, import_fs5.readFileSync)(filePath);
|
|
2441
4900
|
params.image_base64 = imageData.toString("base64");
|
|
2442
4901
|
}
|
|
2443
4902
|
}
|
|
2444
|
-
|
|
2445
|
-
console.error("\u274C Missing prompt. Use --prompt or pass JSON params");
|
|
2446
|
-
process.exit(1);
|
|
2447
|
-
}
|
|
4903
|
+
const supportedPayChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet", "solana", "solana_devnet"];
|
|
2448
4904
|
const chain = options.chain?.toLowerCase();
|
|
2449
|
-
if (chain && !
|
|
2450
|
-
console.error(`\u274C Unknown chain: ${chain}. Supported:
|
|
4905
|
+
if (chain && !supportedPayChains.includes(chain)) {
|
|
4906
|
+
console.error(`\u274C Unknown chain: ${chain}. Supported: ${supportedPayChains.join(", ")}`);
|
|
2451
4907
|
process.exit(1);
|
|
2452
4908
|
}
|
|
2453
4909
|
const imageDisplay = params.image_url || (params.image_base64 ? `[local file: ${options.image}]` : null);
|
|
@@ -2499,11 +4955,11 @@ program.command("pay <server> <service> [params]").description("Pay for a servic
|
|
|
2499
4955
|
}
|
|
2500
4956
|
});
|
|
2501
4957
|
program.command("validate <path>").description("Validate a moltspay.services.json file against the schema").action(async (inputPath) => {
|
|
2502
|
-
const resolvedPath = (0,
|
|
4958
|
+
const resolvedPath = (0, import_path3.resolve)(inputPath);
|
|
2503
4959
|
let manifestPath;
|
|
2504
|
-
if ((0,
|
|
2505
|
-
manifestPath = (0,
|
|
2506
|
-
} else if (resolvedPath.endsWith(".json") && (0,
|
|
4960
|
+
if ((0, import_fs5.existsSync)((0, import_path3.join)(resolvedPath, "moltspay.services.json"))) {
|
|
4961
|
+
manifestPath = (0, import_path3.join)(resolvedPath, "moltspay.services.json");
|
|
4962
|
+
} else if (resolvedPath.endsWith(".json") && (0, import_fs5.existsSync)(resolvedPath)) {
|
|
2507
4963
|
manifestPath = resolvedPath;
|
|
2508
4964
|
} else {
|
|
2509
4965
|
console.error(`\u274C Not found: ${resolvedPath}`);
|
|
@@ -2513,7 +4969,7 @@ program.command("validate <path>").description("Validate a moltspay.services.jso
|
|
|
2513
4969
|
\u{1F4CB} Validating: ${manifestPath}
|
|
2514
4970
|
`);
|
|
2515
4971
|
try {
|
|
2516
|
-
const content = JSON.parse((0,
|
|
4972
|
+
const content = JSON.parse((0, import_fs5.readFileSync)(manifestPath, "utf-8"));
|
|
2517
4973
|
const errors = [];
|
|
2518
4974
|
if (!content.provider) {
|
|
2519
4975
|
errors.push("Missing required field: provider");
|