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