moltspay 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +14 -0
- package/README.md +319 -89
- package/dist/cdp/index.d.mts +4 -4
- package/dist/cdp/index.d.ts +4 -4
- package/dist/cdp/index.js +57 -0
- package/dist/cdp/index.js.map +1 -1
- package/dist/cdp/index.mjs +57 -0
- package/dist/cdp/index.mjs.map +1 -1
- package/dist/chains/index.d.mts +9 -8
- package/dist/chains/index.d.ts +9 -8
- package/dist/chains/index.js +57 -0
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/index.mjs +57 -0
- package/dist/chains/index.mjs.map +1 -1
- package/dist/cli/index.js +2021 -285
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +2023 -277
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +39 -3
- package/dist/client/index.d.ts +39 -3
- package/dist/client/index.js +563 -37
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +571 -35
- package/dist/client/index.mjs.map +1 -1
- package/dist/facilitators/index.d.mts +220 -1
- package/dist/facilitators/index.d.ts +220 -1
- package/dist/facilitators/index.js +664 -1
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +670 -1
- package/dist/facilitators/index.mjs.map +1 -1
- package/dist/{index-On9ZaGDW.d.mts → index-D_2FkLwV.d.mts} +6 -2
- package/dist/{index-On9ZaGDW.d.ts → index-D_2FkLwV.d.ts} +6 -2
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1440 -153
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1448 -151
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.d.mts +13 -3
- package/dist/server/index.d.ts +13 -3
- package/dist/server/index.js +909 -54
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +919 -54
- package/dist/server/index.mjs.map +1 -1
- package/dist/verify/index.d.mts +1 -1
- package/dist/verify/index.d.ts +1 -1
- package/dist/verify/index.js +57 -0
- package/dist/verify/index.js.map +1 -1
- package/dist/verify/index.mjs +57 -0
- package/dist/verify/index.mjs.map +1 -1
- package/dist/wallet/index.d.mts +3 -3
- package/dist/wallet/index.d.ts +3 -3
- package/dist/wallet/index.js +57 -0
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs +57 -0
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +5 -2
- package/schemas/moltspay.services.schema.json +27 -132
package/dist/client/index.js
CHANGED
|
@@ -33,9 +33,9 @@ __export(client_exports, {
|
|
|
33
33
|
MoltsPayClient: () => MoltsPayClient
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(client_exports);
|
|
36
|
-
var
|
|
37
|
-
var
|
|
38
|
-
var
|
|
36
|
+
var import_fs2 = require("fs");
|
|
37
|
+
var import_os2 = require("os");
|
|
38
|
+
var import_path2 = require("path");
|
|
39
39
|
var import_ethers = require("ethers");
|
|
40
40
|
|
|
41
41
|
// src/chains/index.ts
|
|
@@ -145,6 +145,63 @@ var CHAINS = {
|
|
|
145
145
|
explorerTx: "https://explore.testnet.tempo.xyz/tx/",
|
|
146
146
|
avgBlockTime: 0.5
|
|
147
147
|
// ~500ms finality
|
|
148
|
+
},
|
|
149
|
+
// ============ BNB Chain Testnet ============
|
|
150
|
+
bnb_testnet: {
|
|
151
|
+
name: "BNB Testnet",
|
|
152
|
+
chainId: 97,
|
|
153
|
+
rpc: "https://data-seed-prebsc-1-s1.binance.org:8545",
|
|
154
|
+
tokens: {
|
|
155
|
+
// Note: BNB uses 18 decimals for stablecoins (unlike Base/Polygon which use 6)
|
|
156
|
+
// Using official Binance-Peg testnet tokens
|
|
157
|
+
USDC: {
|
|
158
|
+
address: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
159
|
+
// Testnet USDC
|
|
160
|
+
decimals: 18,
|
|
161
|
+
symbol: "USDC",
|
|
162
|
+
eip712Name: "USD Coin"
|
|
163
|
+
},
|
|
164
|
+
USDT: {
|
|
165
|
+
address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
|
|
166
|
+
// Testnet USDT
|
|
167
|
+
decimals: 18,
|
|
168
|
+
symbol: "USDT",
|
|
169
|
+
eip712Name: "Tether USD"
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
usdc: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
173
|
+
explorer: "https://testnet.bscscan.com/address/",
|
|
174
|
+
explorerTx: "https://testnet.bscscan.com/tx/",
|
|
175
|
+
avgBlockTime: 3,
|
|
176
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
177
|
+
requiresApproval: true
|
|
178
|
+
},
|
|
179
|
+
// ============ BNB Chain Mainnet ============
|
|
180
|
+
bnb: {
|
|
181
|
+
name: "BNB Smart Chain",
|
|
182
|
+
chainId: 56,
|
|
183
|
+
rpc: "https://bsc-dataseed.binance.org",
|
|
184
|
+
tokens: {
|
|
185
|
+
// Note: BNB uses 18 decimals for stablecoins
|
|
186
|
+
USDC: {
|
|
187
|
+
address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
188
|
+
decimals: 18,
|
|
189
|
+
symbol: "USDC",
|
|
190
|
+
eip712Name: "USD Coin"
|
|
191
|
+
},
|
|
192
|
+
USDT: {
|
|
193
|
+
address: "0x55d398326f99059fF775485246999027B3197955",
|
|
194
|
+
decimals: 18,
|
|
195
|
+
symbol: "USDT",
|
|
196
|
+
eip712Name: "Tether USD"
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
usdc: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
200
|
+
explorer: "https://bscscan.com/address/",
|
|
201
|
+
explorerTx: "https://bscscan.com/tx/",
|
|
202
|
+
avgBlockTime: 3,
|
|
203
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
204
|
+
requiresApproval: true
|
|
148
205
|
}
|
|
149
206
|
};
|
|
150
207
|
function getChain(name) {
|
|
@@ -155,7 +212,119 @@ function getChain(name) {
|
|
|
155
212
|
return config;
|
|
156
213
|
}
|
|
157
214
|
|
|
215
|
+
// src/wallet/solana.ts
|
|
216
|
+
var import_web32 = require("@solana/web3.js");
|
|
217
|
+
var import_spl_token = require("@solana/spl-token");
|
|
218
|
+
var import_fs = require("fs");
|
|
219
|
+
var import_path = require("path");
|
|
220
|
+
var import_os = require("os");
|
|
221
|
+
var import_bs58 = __toESM(require("bs58"));
|
|
222
|
+
|
|
223
|
+
// src/chains/solana.ts
|
|
224
|
+
var import_web3 = require("@solana/web3.js");
|
|
225
|
+
var SOLANA_CHAINS = {
|
|
226
|
+
solana: {
|
|
227
|
+
name: "Solana Mainnet",
|
|
228
|
+
cluster: "mainnet-beta",
|
|
229
|
+
rpc: "https://api.mainnet-beta.solana.com",
|
|
230
|
+
explorer: "https://solscan.io/account/",
|
|
231
|
+
explorerTx: "https://solscan.io/tx/",
|
|
232
|
+
tokens: {
|
|
233
|
+
USDC: {
|
|
234
|
+
mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
235
|
+
// Circle official USDC
|
|
236
|
+
decimals: 6
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
solana_devnet: {
|
|
241
|
+
name: "Solana Devnet",
|
|
242
|
+
cluster: "devnet",
|
|
243
|
+
rpc: "https://api.devnet.solana.com",
|
|
244
|
+
explorer: "https://solscan.io/account/",
|
|
245
|
+
explorerTx: "https://solscan.io/tx/",
|
|
246
|
+
tokens: {
|
|
247
|
+
USDC: {
|
|
248
|
+
// Circle's devnet USDC (if not available, we'll deploy our own test token)
|
|
249
|
+
mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
250
|
+
decimals: 6
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// src/wallet/solana.ts
|
|
257
|
+
var DEFAULT_CONFIG_DIR = (0, import_path.join)((0, import_os.homedir)(), ".moltspay");
|
|
258
|
+
var SOLANA_WALLET_FILE = "wallet-solana.json";
|
|
259
|
+
function getSolanaWalletPath(configDir = DEFAULT_CONFIG_DIR) {
|
|
260
|
+
return (0, import_path.join)(configDir, SOLANA_WALLET_FILE);
|
|
261
|
+
}
|
|
262
|
+
function loadSolanaWallet(configDir = DEFAULT_CONFIG_DIR) {
|
|
263
|
+
const walletPath = getSolanaWalletPath(configDir);
|
|
264
|
+
if (!(0, import_fs.existsSync)(walletPath)) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
const data = JSON.parse((0, import_fs.readFileSync)(walletPath, "utf-8"));
|
|
269
|
+
const secretKey = import_bs58.default.decode(data.secretKey);
|
|
270
|
+
return import_web32.Keypair.fromSecretKey(secretKey);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.error("Failed to load Solana wallet:", error);
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/facilitators/solana.ts
|
|
278
|
+
var import_web33 = require("@solana/web3.js");
|
|
279
|
+
var import_spl_token2 = require("@solana/spl-token");
|
|
280
|
+
async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
|
|
281
|
+
const chainConfig = SOLANA_CHAINS[chain];
|
|
282
|
+
const connection = new import_web33.Connection(chainConfig.rpc, "confirmed");
|
|
283
|
+
const mint = new import_web33.PublicKey(chainConfig.tokens.USDC.mint);
|
|
284
|
+
const actualFeePayer = feePayerPubkey || senderPubkey;
|
|
285
|
+
const senderATA = await (0, import_spl_token2.getAssociatedTokenAddress)(mint, senderPubkey);
|
|
286
|
+
const recipientATA = await (0, import_spl_token2.getAssociatedTokenAddress)(mint, recipientPubkey);
|
|
287
|
+
const transaction = new import_web33.Transaction();
|
|
288
|
+
try {
|
|
289
|
+
await (0, import_spl_token2.getAccount)(connection, recipientATA);
|
|
290
|
+
} catch {
|
|
291
|
+
transaction.add(
|
|
292
|
+
(0, import_spl_token2.createAssociatedTokenAccountInstruction)(
|
|
293
|
+
actualFeePayer,
|
|
294
|
+
// payer (fee payer in gasless mode)
|
|
295
|
+
recipientATA,
|
|
296
|
+
// ata to create
|
|
297
|
+
recipientPubkey,
|
|
298
|
+
// owner
|
|
299
|
+
mint
|
|
300
|
+
// mint
|
|
301
|
+
)
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
transaction.add(
|
|
305
|
+
(0, import_spl_token2.createTransferCheckedInstruction)(
|
|
306
|
+
senderATA,
|
|
307
|
+
// source
|
|
308
|
+
mint,
|
|
309
|
+
// mint
|
|
310
|
+
recipientATA,
|
|
311
|
+
// destination
|
|
312
|
+
senderPubkey,
|
|
313
|
+
// owner (sender still authorizes the transfer)
|
|
314
|
+
amount,
|
|
315
|
+
// amount
|
|
316
|
+
chainConfig.tokens.USDC.decimals
|
|
317
|
+
// decimals
|
|
318
|
+
)
|
|
319
|
+
);
|
|
320
|
+
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
|
|
321
|
+
transaction.recentBlockhash = blockhash;
|
|
322
|
+
transaction.feePayer = actualFeePayer;
|
|
323
|
+
return transaction;
|
|
324
|
+
}
|
|
325
|
+
|
|
158
326
|
// src/client/index.ts
|
|
327
|
+
var import_web34 = require("@solana/web3.js");
|
|
159
328
|
var X402_VERSION = 2;
|
|
160
329
|
var PAYMENT_REQUIRED_HEADER = "x-payment-required";
|
|
161
330
|
var PAYMENT_HEADER = "x-payment";
|
|
@@ -174,7 +343,7 @@ var MoltsPayClient = class {
|
|
|
174
343
|
todaySpending = 0;
|
|
175
344
|
lastSpendingReset = 0;
|
|
176
345
|
constructor(options = {}) {
|
|
177
|
-
this.configDir = options.configDir || (0,
|
|
346
|
+
this.configDir = options.configDir || (0, import_path2.join)((0, import_os2.homedir)(), ".moltspay");
|
|
178
347
|
this.config = this.loadConfig();
|
|
179
348
|
this.walletData = this.loadWallet();
|
|
180
349
|
this.loadSpending();
|
|
@@ -194,6 +363,12 @@ var MoltsPayClient = class {
|
|
|
194
363
|
get address() {
|
|
195
364
|
return this.wallet?.address || null;
|
|
196
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* Get wallet instance (for direct operations like approvals)
|
|
368
|
+
*/
|
|
369
|
+
getWallet() {
|
|
370
|
+
return this.wallet;
|
|
371
|
+
}
|
|
197
372
|
/**
|
|
198
373
|
* Get current config
|
|
199
374
|
*/
|
|
@@ -247,11 +422,26 @@ var MoltsPayClient = class {
|
|
|
247
422
|
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
248
423
|
}
|
|
249
424
|
console.log(`[MoltsPay] Requesting service: ${service}`);
|
|
250
|
-
|
|
425
|
+
let executeUrl = `${serverUrl}/execute`;
|
|
426
|
+
try {
|
|
427
|
+
const services = await this.getServices(serverUrl);
|
|
428
|
+
const svc = services.services?.find((s) => s.id === service);
|
|
429
|
+
if (svc?.endpoint) {
|
|
430
|
+
executeUrl = `${serverUrl}${svc.endpoint}`;
|
|
431
|
+
console.log(`[MoltsPay] Using service endpoint: ${svc.endpoint}`);
|
|
432
|
+
}
|
|
433
|
+
} catch {
|
|
434
|
+
}
|
|
435
|
+
let requestBody;
|
|
436
|
+
if (options.rawData) {
|
|
437
|
+
requestBody = { service, ...params };
|
|
438
|
+
} else {
|
|
439
|
+
requestBody = { service, params };
|
|
440
|
+
}
|
|
251
441
|
if (options.chain) {
|
|
252
442
|
requestBody.chain = options.chain;
|
|
253
443
|
}
|
|
254
|
-
const initialRes = await fetch(
|
|
444
|
+
const initialRes = await fetch(executeUrl, {
|
|
255
445
|
method: "POST",
|
|
256
446
|
headers: { "Content-Type": "application/json" },
|
|
257
447
|
body: JSON.stringify(requestBody)
|
|
@@ -263,9 +453,14 @@ var MoltsPayClient = class {
|
|
|
263
453
|
}
|
|
264
454
|
throw new Error(data.error || "Unexpected response");
|
|
265
455
|
}
|
|
456
|
+
const wwwAuthHeader = initialRes.headers.get("www-authenticate");
|
|
266
457
|
const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
458
|
+
if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
|
|
459
|
+
console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
|
|
460
|
+
return await this.handleMPPPayment(executeUrl, service, params, wwwAuthHeader, options);
|
|
461
|
+
}
|
|
267
462
|
if (!paymentRequiredHeader) {
|
|
268
|
-
throw new Error("Missing x-payment-required
|
|
463
|
+
throw new Error("Missing payment header (x-payment-required or www-authenticate)");
|
|
269
464
|
}
|
|
270
465
|
let requirements;
|
|
271
466
|
try {
|
|
@@ -282,17 +477,22 @@ var MoltsPayClient = class {
|
|
|
282
477
|
throw new Error("Invalid x-payment-required header");
|
|
283
478
|
}
|
|
284
479
|
const networkToChainName = (network2) => {
|
|
480
|
+
if (network2 === "solana:mainnet") return "solana";
|
|
481
|
+
if (network2 === "solana:devnet") return "solana_devnet";
|
|
285
482
|
const match = network2.match(/^eip155:(\d+)$/);
|
|
286
483
|
if (!match) return null;
|
|
287
484
|
const chainId = parseInt(match[1]);
|
|
288
485
|
if (chainId === 8453) return "base";
|
|
289
486
|
if (chainId === 137) return "polygon";
|
|
290
487
|
if (chainId === 84532) return "base_sepolia";
|
|
488
|
+
if (chainId === 42431) return "tempo_moderato";
|
|
489
|
+
if (chainId === 56) return "bnb";
|
|
490
|
+
if (chainId === 97) return "bnb_testnet";
|
|
291
491
|
return null;
|
|
292
492
|
};
|
|
293
493
|
const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
|
|
294
|
-
let chainName;
|
|
295
494
|
const userSpecifiedChain = options.chain;
|
|
495
|
+
let selectedChain;
|
|
296
496
|
if (userSpecifiedChain) {
|
|
297
497
|
if (!serverChains.includes(userSpecifiedChain)) {
|
|
298
498
|
throw new Error(
|
|
@@ -300,17 +500,27 @@ var MoltsPayClient = class {
|
|
|
300
500
|
Server accepts: ${serverChains.join(", ")}`
|
|
301
501
|
);
|
|
302
502
|
}
|
|
303
|
-
|
|
503
|
+
selectedChain = userSpecifiedChain;
|
|
304
504
|
} else {
|
|
305
505
|
if (serverChains.length === 1 && serverChains[0] === "base") {
|
|
306
|
-
|
|
506
|
+
selectedChain = "base";
|
|
307
507
|
} else {
|
|
308
508
|
throw new Error(
|
|
309
509
|
`Server accepts: ${serverChains.join(", ")}
|
|
310
|
-
Please specify: --chain
|
|
510
|
+
Please specify: --chain <chain_name>`
|
|
311
511
|
);
|
|
312
512
|
}
|
|
313
513
|
}
|
|
514
|
+
if (selectedChain === "solana" || selectedChain === "solana_devnet") {
|
|
515
|
+
const solanaChain = selectedChain;
|
|
516
|
+
const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
517
|
+
const req2 = requirements.find((r) => r.network === network2);
|
|
518
|
+
if (!req2) {
|
|
519
|
+
throw new Error(`Failed to find payment requirement for ${selectedChain}`);
|
|
520
|
+
}
|
|
521
|
+
return await this.handleSolanaPayment(executeUrl, service, params, req2, solanaChain, options);
|
|
522
|
+
}
|
|
523
|
+
const chainName = selectedChain;
|
|
314
524
|
const chain = getChain(chainName);
|
|
315
525
|
const network = `eip155:${chain.chainId}`;
|
|
316
526
|
const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
|
|
@@ -345,6 +555,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
345
555
|
} else {
|
|
346
556
|
console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
|
|
347
557
|
}
|
|
558
|
+
if (chainName === "bnb" || chainName === "bnb_testnet") {
|
|
559
|
+
console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
|
|
560
|
+
const payTo2 = req.payTo || req.resource;
|
|
561
|
+
if (!payTo2) {
|
|
562
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
563
|
+
}
|
|
564
|
+
const bnbSpender = req.extra?.bnbSpender;
|
|
565
|
+
if (!bnbSpender) {
|
|
566
|
+
throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
|
|
567
|
+
}
|
|
568
|
+
return await this.handleBNBPayment(executeUrl, service, params, {
|
|
569
|
+
to: payTo2,
|
|
570
|
+
amount,
|
|
571
|
+
token,
|
|
572
|
+
chainName,
|
|
573
|
+
chain,
|
|
574
|
+
spender: bnbSpender
|
|
575
|
+
}, options);
|
|
576
|
+
}
|
|
348
577
|
const payTo = req.payTo || req.resource;
|
|
349
578
|
if (!payTo) {
|
|
350
579
|
throw new Error("Missing payTo address in payment requirements");
|
|
@@ -374,11 +603,11 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
374
603
|
};
|
|
375
604
|
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
376
605
|
console.log(`[MoltsPay] Sending request with payment...`);
|
|
377
|
-
const paidRequestBody = { service, params };
|
|
606
|
+
const paidRequestBody = options.rawData ? { service, ...params } : { service, params };
|
|
378
607
|
if (options.chain) {
|
|
379
608
|
paidRequestBody.chain = options.chain;
|
|
380
609
|
}
|
|
381
|
-
const paidRes = await fetch(
|
|
610
|
+
const paidRes = await fetch(executeUrl, {
|
|
382
611
|
method: "POST",
|
|
383
612
|
headers: {
|
|
384
613
|
"Content-Type": "application/json",
|
|
@@ -392,7 +621,304 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
392
621
|
}
|
|
393
622
|
this.recordSpending(amount);
|
|
394
623
|
console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
|
|
395
|
-
return result.result;
|
|
624
|
+
return result.result || result;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Handle MPP (Machine Payments Protocol) payment flow
|
|
628
|
+
* Called when pay() detects WWW-Authenticate header in 402 response
|
|
629
|
+
*/
|
|
630
|
+
async handleMPPPayment(executeUrl, service, params, wwwAuthHeader, options = {}) {
|
|
631
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
632
|
+
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
633
|
+
const { tempoModerato } = await import("viem/chains");
|
|
634
|
+
const { Actions } = await import("viem/tempo");
|
|
635
|
+
const privateKey = this.walletData.privateKey;
|
|
636
|
+
const account = privateKeyToAccount(privateKey);
|
|
637
|
+
console.log(`[MoltsPay] Using MPP protocol on Tempo`);
|
|
638
|
+
console.log(`[MoltsPay] Account: ${account.address}`);
|
|
639
|
+
const parseAuthParam = (header, key) => {
|
|
640
|
+
const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
|
|
641
|
+
return match ? match[1] : null;
|
|
642
|
+
};
|
|
643
|
+
const challengeId = parseAuthParam(wwwAuthHeader, "id");
|
|
644
|
+
const method = parseAuthParam(wwwAuthHeader, "method");
|
|
645
|
+
const realm = parseAuthParam(wwwAuthHeader, "realm");
|
|
646
|
+
const requestB64 = parseAuthParam(wwwAuthHeader, "request");
|
|
647
|
+
if (method !== "tempo") {
|
|
648
|
+
throw new Error(`Unsupported payment method: ${method}`);
|
|
649
|
+
}
|
|
650
|
+
if (!requestB64) {
|
|
651
|
+
throw new Error("Missing request in WWW-Authenticate");
|
|
652
|
+
}
|
|
653
|
+
const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
|
|
654
|
+
const paymentRequest = JSON.parse(requestJson);
|
|
655
|
+
const { amount, currency, recipient, methodDetails } = paymentRequest;
|
|
656
|
+
const chainId = methodDetails?.chainId || 42431;
|
|
657
|
+
const amountDisplay = Number(amount) / 1e6;
|
|
658
|
+
console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
|
|
659
|
+
this.checkLimits(amountDisplay);
|
|
660
|
+
console.log(`[MoltsPay] Sending transaction on Tempo...`);
|
|
661
|
+
const tempoChain = { ...tempoModerato, feeToken: currency };
|
|
662
|
+
const publicClient = createPublicClient({
|
|
663
|
+
chain: tempoChain,
|
|
664
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
665
|
+
});
|
|
666
|
+
const walletClient = createWalletClient({
|
|
667
|
+
account,
|
|
668
|
+
chain: tempoChain,
|
|
669
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
670
|
+
});
|
|
671
|
+
const txHash = await Actions.token.transfer(walletClient, {
|
|
672
|
+
to: recipient,
|
|
673
|
+
amount: BigInt(amount),
|
|
674
|
+
token: currency
|
|
675
|
+
});
|
|
676
|
+
console.log(`[MoltsPay] Transaction: ${txHash}`);
|
|
677
|
+
await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
678
|
+
console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
|
|
679
|
+
const credential = {
|
|
680
|
+
challenge: {
|
|
681
|
+
id: challengeId,
|
|
682
|
+
realm,
|
|
683
|
+
method: "tempo",
|
|
684
|
+
intent: "charge",
|
|
685
|
+
request: paymentRequest
|
|
686
|
+
},
|
|
687
|
+
payload: { hash: txHash, type: "hash" },
|
|
688
|
+
source: `did:pkh:eip155:${chainId}:${account.address}`
|
|
689
|
+
};
|
|
690
|
+
const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
691
|
+
const retryBody = options.rawData ? { service, ...params, chain: "tempo_moderato" } : { service, params, chain: "tempo_moderato" };
|
|
692
|
+
const paidRes = await fetch(executeUrl, {
|
|
693
|
+
method: "POST",
|
|
694
|
+
headers: {
|
|
695
|
+
"Content-Type": "application/json",
|
|
696
|
+
"Authorization": `Payment ${credentialB64}`
|
|
697
|
+
},
|
|
698
|
+
body: JSON.stringify(retryBody)
|
|
699
|
+
});
|
|
700
|
+
const result = await paidRes.json();
|
|
701
|
+
if (!paidRes.ok) {
|
|
702
|
+
throw new Error(result.error || "Payment verification failed");
|
|
703
|
+
}
|
|
704
|
+
this.recordSpending(amountDisplay);
|
|
705
|
+
console.log(`[MoltsPay] Success!`);
|
|
706
|
+
return result.result || result;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Handle BNB Chain payment flow (pre-approval + intent signature)
|
|
710
|
+
*
|
|
711
|
+
* Flow:
|
|
712
|
+
* 1. Check client has approved server wallet (done via `moltspay init`)
|
|
713
|
+
* 2. Sign EIP-712 payment intent (no gas, just signature)
|
|
714
|
+
* 3. Send intent to server
|
|
715
|
+
* 4. Server executes service
|
|
716
|
+
* 5. Server calls transferFrom if successful (pay-for-success)
|
|
717
|
+
*/
|
|
718
|
+
async handleBNBPayment(executeUrl, service, params, paymentDetails, options = {}) {
|
|
719
|
+
const { to, amount, token, chainName, chain, spender } = paymentDetails;
|
|
720
|
+
const tokenConfig = chain.tokens[token];
|
|
721
|
+
const provider = new import_ethers.ethers.JsonRpcProvider(chain.rpc);
|
|
722
|
+
const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
|
|
723
|
+
const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
|
|
724
|
+
if (allowance < amountWeiCheck) {
|
|
725
|
+
const nativeBalance = await provider.getBalance(this.wallet.address);
|
|
726
|
+
const minGasBalance = import_ethers.ethers.parseEther("0.0005");
|
|
727
|
+
if (nativeBalance < minGasBalance) {
|
|
728
|
+
const nativeBNB = parseFloat(import_ethers.ethers.formatEther(nativeBalance)).toFixed(4);
|
|
729
|
+
const isTestnet = chainName === "bnb_testnet";
|
|
730
|
+
if (isTestnet) {
|
|
731
|
+
throw new Error(
|
|
732
|
+
`\u274C Insufficient tBNB for approval transaction
|
|
733
|
+
|
|
734
|
+
Current tBNB: ${nativeBNB}
|
|
735
|
+
Required: ~0.001 tBNB
|
|
736
|
+
|
|
737
|
+
Get testnet tokens: npx moltspay faucet --chain bnb_testnet
|
|
738
|
+
(Gives USDC + tBNB for gas)`
|
|
739
|
+
);
|
|
740
|
+
} else {
|
|
741
|
+
throw new Error(
|
|
742
|
+
`\u274C Insufficient BNB for approval transaction
|
|
743
|
+
|
|
744
|
+
Current BNB: ${nativeBNB}
|
|
745
|
+
Required: ~0.001 BNB (~$0.60)
|
|
746
|
+
|
|
747
|
+
To get BNB:
|
|
748
|
+
\u2022 Withdraw from Binance/exchange to your wallet
|
|
749
|
+
\u2022 Most exchanges include BNB dust with withdrawals
|
|
750
|
+
|
|
751
|
+
After funding, run:
|
|
752
|
+
npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
throw new Error(
|
|
757
|
+
`Insufficient allowance for ${spender.slice(0, 10)}...
|
|
758
|
+
Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
|
|
762
|
+
const intent = {
|
|
763
|
+
from: this.wallet.address,
|
|
764
|
+
to,
|
|
765
|
+
amount: amountWei,
|
|
766
|
+
token: tokenConfig.address,
|
|
767
|
+
service,
|
|
768
|
+
nonce: Date.now(),
|
|
769
|
+
// Use timestamp as nonce for simplicity
|
|
770
|
+
deadline: Date.now() + 36e5
|
|
771
|
+
// 1 hour
|
|
772
|
+
};
|
|
773
|
+
const domain = {
|
|
774
|
+
name: "MoltsPay",
|
|
775
|
+
version: "1",
|
|
776
|
+
chainId: chain.chainId
|
|
777
|
+
};
|
|
778
|
+
const types = {
|
|
779
|
+
PaymentIntent: [
|
|
780
|
+
{ name: "from", type: "address" },
|
|
781
|
+
{ name: "to", type: "address" },
|
|
782
|
+
{ name: "amount", type: "uint256" },
|
|
783
|
+
{ name: "token", type: "address" },
|
|
784
|
+
{ name: "service", type: "string" },
|
|
785
|
+
{ name: "nonce", type: "uint256" },
|
|
786
|
+
{ name: "deadline", type: "uint256" }
|
|
787
|
+
]
|
|
788
|
+
};
|
|
789
|
+
console.log(`[MoltsPay] Signing BNB payment intent...`);
|
|
790
|
+
const signature = await this.wallet.signTypedData(domain, types, intent);
|
|
791
|
+
const network = `eip155:${chain.chainId}`;
|
|
792
|
+
const payload = {
|
|
793
|
+
x402Version: 2,
|
|
794
|
+
scheme: "exact",
|
|
795
|
+
network,
|
|
796
|
+
payload: {
|
|
797
|
+
intent: {
|
|
798
|
+
...intent,
|
|
799
|
+
signature
|
|
800
|
+
},
|
|
801
|
+
chainId: chain.chainId
|
|
802
|
+
},
|
|
803
|
+
accepted: {
|
|
804
|
+
scheme: "exact",
|
|
805
|
+
network,
|
|
806
|
+
asset: tokenConfig.address,
|
|
807
|
+
amount: amountWei,
|
|
808
|
+
payTo: to,
|
|
809
|
+
maxTimeoutSeconds: 300
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
813
|
+
console.log(`[MoltsPay] Sending BNB payment request...`);
|
|
814
|
+
const bnbRequestBody = options.rawData ? { service, ...params, chain: chainName } : { service, params, chain: chainName };
|
|
815
|
+
const paidRes = await fetch(executeUrl, {
|
|
816
|
+
method: "POST",
|
|
817
|
+
headers: {
|
|
818
|
+
"Content-Type": "application/json",
|
|
819
|
+
"X-Payment": paymentHeader
|
|
820
|
+
},
|
|
821
|
+
body: JSON.stringify(bnbRequestBody)
|
|
822
|
+
});
|
|
823
|
+
const result = await paidRes.json();
|
|
824
|
+
if (!paidRes.ok) {
|
|
825
|
+
throw new Error(result.error || "BNB payment failed");
|
|
826
|
+
}
|
|
827
|
+
this.recordSpending(amount);
|
|
828
|
+
console.log(`[MoltsPay] Success! BNB payment settled.`);
|
|
829
|
+
return result.result || result;
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Handle Solana payment flow
|
|
833
|
+
*
|
|
834
|
+
* Solana uses SPL token transfers with pay-for-success model:
|
|
835
|
+
* 1. Client creates and signs a transfer transaction
|
|
836
|
+
* 2. Server submits the transaction after service completes
|
|
837
|
+
*/
|
|
838
|
+
async handleSolanaPayment(executeUrl, service, params, requirements, chain, options = {}) {
|
|
839
|
+
const solanaWallet = loadSolanaWallet(this.configDir);
|
|
840
|
+
if (!solanaWallet) {
|
|
841
|
+
throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
|
|
842
|
+
}
|
|
843
|
+
const amount = Number(requirements.amount);
|
|
844
|
+
const amountUSDC = amount / 1e6;
|
|
845
|
+
this.checkLimits(amountUSDC);
|
|
846
|
+
console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
|
|
847
|
+
if (!requirements.payTo) {
|
|
848
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
849
|
+
}
|
|
850
|
+
const solanaFeePayer = requirements.extra?.solanaFeePayer;
|
|
851
|
+
const feePayerPubkey = solanaFeePayer ? new import_web34.PublicKey(solanaFeePayer) : void 0;
|
|
852
|
+
if (feePayerPubkey) {
|
|
853
|
+
console.log(`[MoltsPay] Gasless mode: server pays fees`);
|
|
854
|
+
}
|
|
855
|
+
const recipientPubkey = new import_web34.PublicKey(requirements.payTo);
|
|
856
|
+
const transaction = await createSolanaPaymentTransaction(
|
|
857
|
+
solanaWallet.publicKey,
|
|
858
|
+
recipientPubkey,
|
|
859
|
+
BigInt(amount),
|
|
860
|
+
chain,
|
|
861
|
+
feePayerPubkey
|
|
862
|
+
// Optional fee payer for gasless mode
|
|
863
|
+
);
|
|
864
|
+
if (feePayerPubkey) {
|
|
865
|
+
transaction.partialSign(solanaWallet);
|
|
866
|
+
} else {
|
|
867
|
+
transaction.sign(solanaWallet);
|
|
868
|
+
}
|
|
869
|
+
const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
|
|
870
|
+
console.log(`[MoltsPay] Transaction signed, sending to server...`);
|
|
871
|
+
const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
872
|
+
const payload = {
|
|
873
|
+
x402Version: 2,
|
|
874
|
+
scheme: "exact",
|
|
875
|
+
network,
|
|
876
|
+
payload: {
|
|
877
|
+
signedTransaction: signedTx,
|
|
878
|
+
sender: solanaWallet.publicKey.toBase58(),
|
|
879
|
+
chain
|
|
880
|
+
},
|
|
881
|
+
accepted: {
|
|
882
|
+
scheme: "exact",
|
|
883
|
+
network,
|
|
884
|
+
asset: requirements.asset,
|
|
885
|
+
amount: requirements.amount,
|
|
886
|
+
payTo: requirements.payTo,
|
|
887
|
+
maxTimeoutSeconds: 300
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
891
|
+
const solanaRequestBody = options.rawData ? { service, ...params, chain } : { service, params, chain };
|
|
892
|
+
const paidRes = await fetch(executeUrl, {
|
|
893
|
+
method: "POST",
|
|
894
|
+
headers: {
|
|
895
|
+
"Content-Type": "application/json",
|
|
896
|
+
"X-Payment": paymentHeader
|
|
897
|
+
},
|
|
898
|
+
body: JSON.stringify(solanaRequestBody)
|
|
899
|
+
});
|
|
900
|
+
const result = await paidRes.json();
|
|
901
|
+
if (!paidRes.ok) {
|
|
902
|
+
throw new Error(result.error || "Solana payment failed");
|
|
903
|
+
}
|
|
904
|
+
this.recordSpending(amountUSDC);
|
|
905
|
+
console.log(`[MoltsPay] Success! Solana payment settled.`);
|
|
906
|
+
if (result.payment?.transaction) {
|
|
907
|
+
const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
|
|
908
|
+
console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
|
|
909
|
+
}
|
|
910
|
+
return result.result || result;
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Check ERC20 allowance for a spender
|
|
914
|
+
*/
|
|
915
|
+
async checkAllowance(tokenAddress, spender, provider) {
|
|
916
|
+
const contract = new import_ethers.ethers.Contract(
|
|
917
|
+
tokenAddress,
|
|
918
|
+
["function allowance(address owner, address spender) view returns (uint256)"],
|
|
919
|
+
provider
|
|
920
|
+
);
|
|
921
|
+
return await contract.allowance(this.wallet.address, spender);
|
|
396
922
|
}
|
|
397
923
|
/**
|
|
398
924
|
* Sign EIP-3009 transferWithAuthorization (GASLESS)
|
|
@@ -464,26 +990,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
464
990
|
}
|
|
465
991
|
// --- Config & Wallet Management ---
|
|
466
992
|
loadConfig() {
|
|
467
|
-
const configPath = (0,
|
|
468
|
-
if ((0,
|
|
469
|
-
const content = (0,
|
|
993
|
+
const configPath = (0, import_path2.join)(this.configDir, "config.json");
|
|
994
|
+
if ((0, import_fs2.existsSync)(configPath)) {
|
|
995
|
+
const content = (0, import_fs2.readFileSync)(configPath, "utf-8");
|
|
470
996
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
471
997
|
}
|
|
472
998
|
return { ...DEFAULT_CONFIG };
|
|
473
999
|
}
|
|
474
1000
|
saveConfig() {
|
|
475
|
-
(0,
|
|
476
|
-
const configPath = (0,
|
|
477
|
-
(0,
|
|
1001
|
+
(0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
|
|
1002
|
+
const configPath = (0, import_path2.join)(this.configDir, "config.json");
|
|
1003
|
+
(0, import_fs2.writeFileSync)(configPath, JSON.stringify(this.config, null, 2));
|
|
478
1004
|
}
|
|
479
1005
|
/**
|
|
480
1006
|
* Load spending data from disk
|
|
481
1007
|
*/
|
|
482
1008
|
loadSpending() {
|
|
483
|
-
const spendingPath = (0,
|
|
484
|
-
if ((0,
|
|
1009
|
+
const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
|
|
1010
|
+
if ((0, import_fs2.existsSync)(spendingPath)) {
|
|
485
1011
|
try {
|
|
486
|
-
const data = JSON.parse((0,
|
|
1012
|
+
const data = JSON.parse((0, import_fs2.readFileSync)(spendingPath, "utf-8"));
|
|
487
1013
|
const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
|
|
488
1014
|
if (data.date && data.date === today) {
|
|
489
1015
|
this.todaySpending = data.amount || 0;
|
|
@@ -502,29 +1028,29 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
502
1028
|
* Save spending data to disk
|
|
503
1029
|
*/
|
|
504
1030
|
saveSpending() {
|
|
505
|
-
(0,
|
|
506
|
-
const spendingPath = (0,
|
|
1031
|
+
(0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
|
|
1032
|
+
const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
|
|
507
1033
|
const data = {
|
|
508
1034
|
date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
|
|
509
1035
|
amount: this.todaySpending,
|
|
510
1036
|
updatedAt: Date.now()
|
|
511
1037
|
};
|
|
512
|
-
(0,
|
|
1038
|
+
(0, import_fs2.writeFileSync)(spendingPath, JSON.stringify(data, null, 2));
|
|
513
1039
|
}
|
|
514
1040
|
loadWallet() {
|
|
515
|
-
const walletPath = (0,
|
|
516
|
-
if ((0,
|
|
1041
|
+
const walletPath = (0, import_path2.join)(this.configDir, "wallet.json");
|
|
1042
|
+
if ((0, import_fs2.existsSync)(walletPath)) {
|
|
517
1043
|
try {
|
|
518
|
-
const stats = (0,
|
|
1044
|
+
const stats = (0, import_fs2.statSync)(walletPath);
|
|
519
1045
|
const mode = stats.mode & 511;
|
|
520
1046
|
if (mode !== 384) {
|
|
521
1047
|
console.warn(`[MoltsPay] WARNING: wallet.json has insecure permissions (${mode.toString(8)})`);
|
|
522
1048
|
console.warn(`[MoltsPay] Fixing permissions to 0600...`);
|
|
523
|
-
(0,
|
|
1049
|
+
(0, import_fs2.chmodSync)(walletPath, 384);
|
|
524
1050
|
}
|
|
525
1051
|
} catch (err) {
|
|
526
1052
|
}
|
|
527
|
-
const content = (0,
|
|
1053
|
+
const content = (0, import_fs2.readFileSync)(walletPath, "utf-8");
|
|
528
1054
|
return JSON.parse(content);
|
|
529
1055
|
}
|
|
530
1056
|
return null;
|
|
@@ -533,15 +1059,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
533
1059
|
* Initialize a new wallet (called by CLI)
|
|
534
1060
|
*/
|
|
535
1061
|
static init(configDir, options) {
|
|
536
|
-
(0,
|
|
1062
|
+
(0, import_fs2.mkdirSync)(configDir, { recursive: true });
|
|
537
1063
|
const wallet = import_ethers.Wallet.createRandom();
|
|
538
1064
|
const walletData = {
|
|
539
1065
|
address: wallet.address,
|
|
540
1066
|
privateKey: wallet.privateKey,
|
|
541
1067
|
createdAt: Date.now()
|
|
542
1068
|
};
|
|
543
|
-
const walletPath = (0,
|
|
544
|
-
(0,
|
|
1069
|
+
const walletPath = (0, import_path2.join)(configDir, "wallet.json");
|
|
1070
|
+
(0, import_fs2.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
545
1071
|
const config = {
|
|
546
1072
|
chain: options.chain,
|
|
547
1073
|
limits: {
|
|
@@ -549,8 +1075,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
549
1075
|
maxPerDay: options.maxPerDay
|
|
550
1076
|
}
|
|
551
1077
|
};
|
|
552
|
-
const configPath = (0,
|
|
553
|
-
(0,
|
|
1078
|
+
const configPath = (0, import_path2.join)(configDir, "config.json");
|
|
1079
|
+
(0, import_fs2.writeFileSync)(configPath, JSON.stringify(config, null, 2));
|
|
554
1080
|
return { address: wallet.address, configDir };
|
|
555
1081
|
}
|
|
556
1082
|
/**
|
|
@@ -586,7 +1112,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
586
1112
|
if (!this.wallet) {
|
|
587
1113
|
throw new Error("Client not initialized");
|
|
588
1114
|
}
|
|
589
|
-
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
|
|
1115
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
|
|
590
1116
|
const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
591
1117
|
const results = {};
|
|
592
1118
|
const tempoTokens = {
|