moltspay 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -38
- package/dist/cdp/index.d.mts +4 -4
- package/dist/cdp/index.d.ts +4 -4
- package/dist/cdp/index.js +57 -0
- package/dist/cdp/index.js.map +1 -1
- package/dist/cdp/index.mjs +57 -0
- package/dist/cdp/index.mjs.map +1 -1
- package/dist/chains/index.d.mts +9 -8
- package/dist/chains/index.d.ts +9 -8
- package/dist/chains/index.js +57 -0
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/index.mjs +57 -0
- package/dist/chains/index.mjs.map +1 -1
- package/dist/cli/index.js +1975 -273
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1977 -265
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +36 -3
- package/dist/client/index.d.ts +36 -3
- package/dist/client/index.js +540 -32
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +548 -30
- package/dist/client/index.mjs.map +1 -1
- package/dist/facilitators/index.d.mts +220 -1
- package/dist/facilitators/index.d.ts +220 -1
- package/dist/facilitators/index.js +664 -1
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +670 -1
- package/dist/facilitators/index.mjs.map +1 -1
- package/dist/{index-On9ZaGDW.d.mts → index-D_2FkLwV.d.mts} +6 -2
- package/dist/{index-On9ZaGDW.d.ts → index-D_2FkLwV.d.ts} +6 -2
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1413 -146
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1421 -144
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.d.mts +13 -3
- package/dist/server/index.d.ts +13 -3
- package/dist/server/index.js +905 -52
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +915 -52
- package/dist/server/index.mjs.map +1 -1
- package/dist/verify/index.d.mts +1 -1
- package/dist/verify/index.d.ts +1 -1
- package/dist/verify/index.js +57 -0
- package/dist/verify/index.js.map +1 -1
- package/dist/verify/index.mjs +57 -0
- package/dist/verify/index.mjs.map +1 -1
- package/dist/wallet/index.d.mts +3 -3
- package/dist/wallet/index.d.ts +3 -3
- package/dist/wallet/index.js +57 -0
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs +57 -0
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +4 -1
- package/schemas/moltspay.services.schema.json +27 -132
package/dist/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
|
*/
|
|
@@ -263,9 +438,14 @@ var MoltsPayClient = class {
|
|
|
263
438
|
}
|
|
264
439
|
throw new Error(data.error || "Unexpected response");
|
|
265
440
|
}
|
|
441
|
+
const wwwAuthHeader = initialRes.headers.get("www-authenticate");
|
|
266
442
|
const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
443
|
+
if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
|
|
444
|
+
console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
|
|
445
|
+
return await this.handleMPPPayment(serverUrl, service, params, wwwAuthHeader);
|
|
446
|
+
}
|
|
267
447
|
if (!paymentRequiredHeader) {
|
|
268
|
-
throw new Error("Missing x-payment-required
|
|
448
|
+
throw new Error("Missing payment header (x-payment-required or www-authenticate)");
|
|
269
449
|
}
|
|
270
450
|
let requirements;
|
|
271
451
|
try {
|
|
@@ -282,17 +462,22 @@ var MoltsPayClient = class {
|
|
|
282
462
|
throw new Error("Invalid x-payment-required header");
|
|
283
463
|
}
|
|
284
464
|
const networkToChainName = (network2) => {
|
|
465
|
+
if (network2 === "solana:mainnet") return "solana";
|
|
466
|
+
if (network2 === "solana:devnet") return "solana_devnet";
|
|
285
467
|
const match = network2.match(/^eip155:(\d+)$/);
|
|
286
468
|
if (!match) return null;
|
|
287
469
|
const chainId = parseInt(match[1]);
|
|
288
470
|
if (chainId === 8453) return "base";
|
|
289
471
|
if (chainId === 137) return "polygon";
|
|
290
472
|
if (chainId === 84532) return "base_sepolia";
|
|
473
|
+
if (chainId === 42431) return "tempo_moderato";
|
|
474
|
+
if (chainId === 56) return "bnb";
|
|
475
|
+
if (chainId === 97) return "bnb_testnet";
|
|
291
476
|
return null;
|
|
292
477
|
};
|
|
293
478
|
const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
|
|
294
|
-
let chainName;
|
|
295
479
|
const userSpecifiedChain = options.chain;
|
|
480
|
+
let selectedChain;
|
|
296
481
|
if (userSpecifiedChain) {
|
|
297
482
|
if (!serverChains.includes(userSpecifiedChain)) {
|
|
298
483
|
throw new Error(
|
|
@@ -300,17 +485,27 @@ var MoltsPayClient = class {
|
|
|
300
485
|
Server accepts: ${serverChains.join(", ")}`
|
|
301
486
|
);
|
|
302
487
|
}
|
|
303
|
-
|
|
488
|
+
selectedChain = userSpecifiedChain;
|
|
304
489
|
} else {
|
|
305
490
|
if (serverChains.length === 1 && serverChains[0] === "base") {
|
|
306
|
-
|
|
491
|
+
selectedChain = "base";
|
|
307
492
|
} else {
|
|
308
493
|
throw new Error(
|
|
309
494
|
`Server accepts: ${serverChains.join(", ")}
|
|
310
|
-
Please specify: --chain
|
|
495
|
+
Please specify: --chain <chain_name>`
|
|
311
496
|
);
|
|
312
497
|
}
|
|
313
498
|
}
|
|
499
|
+
if (selectedChain === "solana" || selectedChain === "solana_devnet") {
|
|
500
|
+
const solanaChain = selectedChain;
|
|
501
|
+
const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
502
|
+
const req2 = requirements.find((r) => r.network === network2);
|
|
503
|
+
if (!req2) {
|
|
504
|
+
throw new Error(`Failed to find payment requirement for ${selectedChain}`);
|
|
505
|
+
}
|
|
506
|
+
return await this.handleSolanaPayment(serverUrl, service, params, req2, solanaChain);
|
|
507
|
+
}
|
|
508
|
+
const chainName = selectedChain;
|
|
314
509
|
const chain = getChain(chainName);
|
|
315
510
|
const network = `eip155:${chain.chainId}`;
|
|
316
511
|
const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
|
|
@@ -345,6 +540,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
345
540
|
} else {
|
|
346
541
|
console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
|
|
347
542
|
}
|
|
543
|
+
if (chainName === "bnb" || chainName === "bnb_testnet") {
|
|
544
|
+
console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
|
|
545
|
+
const payTo2 = req.payTo || req.resource;
|
|
546
|
+
if (!payTo2) {
|
|
547
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
548
|
+
}
|
|
549
|
+
const bnbSpender = req.extra?.bnbSpender;
|
|
550
|
+
if (!bnbSpender) {
|
|
551
|
+
throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
|
|
552
|
+
}
|
|
553
|
+
return await this.handleBNBPayment(serverUrl, service, params, {
|
|
554
|
+
to: payTo2,
|
|
555
|
+
amount,
|
|
556
|
+
token,
|
|
557
|
+
chainName,
|
|
558
|
+
chain,
|
|
559
|
+
spender: bnbSpender
|
|
560
|
+
});
|
|
561
|
+
}
|
|
348
562
|
const payTo = req.payTo || req.resource;
|
|
349
563
|
if (!payTo) {
|
|
350
564
|
throw new Error("Missing payTo address in payment requirements");
|
|
@@ -394,6 +608,300 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
394
608
|
console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
|
|
395
609
|
return result.result;
|
|
396
610
|
}
|
|
611
|
+
/**
|
|
612
|
+
* Handle MPP (Machine Payments Protocol) payment flow
|
|
613
|
+
* Called when pay() detects WWW-Authenticate header in 402 response
|
|
614
|
+
*/
|
|
615
|
+
async handleMPPPayment(serverUrl, service, params, wwwAuthHeader) {
|
|
616
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
617
|
+
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
618
|
+
const { tempoModerato } = await import("viem/chains");
|
|
619
|
+
const { Actions } = await import("viem/tempo");
|
|
620
|
+
const privateKey = this.walletData.privateKey;
|
|
621
|
+
const account = privateKeyToAccount(privateKey);
|
|
622
|
+
console.log(`[MoltsPay] Using MPP protocol on Tempo`);
|
|
623
|
+
console.log(`[MoltsPay] Account: ${account.address}`);
|
|
624
|
+
const parseAuthParam = (header, key) => {
|
|
625
|
+
const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
|
|
626
|
+
return match ? match[1] : null;
|
|
627
|
+
};
|
|
628
|
+
const challengeId = parseAuthParam(wwwAuthHeader, "id");
|
|
629
|
+
const method = parseAuthParam(wwwAuthHeader, "method");
|
|
630
|
+
const realm = parseAuthParam(wwwAuthHeader, "realm");
|
|
631
|
+
const requestB64 = parseAuthParam(wwwAuthHeader, "request");
|
|
632
|
+
if (method !== "tempo") {
|
|
633
|
+
throw new Error(`Unsupported payment method: ${method}`);
|
|
634
|
+
}
|
|
635
|
+
if (!requestB64) {
|
|
636
|
+
throw new Error("Missing request in WWW-Authenticate");
|
|
637
|
+
}
|
|
638
|
+
const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
|
|
639
|
+
const paymentRequest = JSON.parse(requestJson);
|
|
640
|
+
const { amount, currency, recipient, methodDetails } = paymentRequest;
|
|
641
|
+
const chainId = methodDetails?.chainId || 42431;
|
|
642
|
+
const amountDisplay = Number(amount) / 1e6;
|
|
643
|
+
console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
|
|
644
|
+
this.checkLimits(amountDisplay);
|
|
645
|
+
console.log(`[MoltsPay] Sending transaction on Tempo...`);
|
|
646
|
+
const tempoChain = { ...tempoModerato, feeToken: currency };
|
|
647
|
+
const publicClient = createPublicClient({
|
|
648
|
+
chain: tempoChain,
|
|
649
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
650
|
+
});
|
|
651
|
+
const walletClient = createWalletClient({
|
|
652
|
+
account,
|
|
653
|
+
chain: tempoChain,
|
|
654
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
655
|
+
});
|
|
656
|
+
const txHash = await Actions.token.transfer(walletClient, {
|
|
657
|
+
to: recipient,
|
|
658
|
+
amount: BigInt(amount),
|
|
659
|
+
token: currency
|
|
660
|
+
});
|
|
661
|
+
console.log(`[MoltsPay] Transaction: ${txHash}`);
|
|
662
|
+
await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
663
|
+
console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
|
|
664
|
+
const credential = {
|
|
665
|
+
challenge: {
|
|
666
|
+
id: challengeId,
|
|
667
|
+
realm,
|
|
668
|
+
method: "tempo",
|
|
669
|
+
intent: "charge",
|
|
670
|
+
request: paymentRequest
|
|
671
|
+
},
|
|
672
|
+
payload: { hash: txHash, type: "hash" },
|
|
673
|
+
source: `did:pkh:eip155:${chainId}:${account.address}`
|
|
674
|
+
};
|
|
675
|
+
const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
676
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
677
|
+
method: "POST",
|
|
678
|
+
headers: {
|
|
679
|
+
"Content-Type": "application/json",
|
|
680
|
+
"Authorization": `Payment ${credentialB64}`
|
|
681
|
+
},
|
|
682
|
+
body: JSON.stringify({ service, params, chain: "tempo_moderato" })
|
|
683
|
+
});
|
|
684
|
+
const result = await paidRes.json();
|
|
685
|
+
if (!paidRes.ok) {
|
|
686
|
+
throw new Error(result.error || "Payment verification failed");
|
|
687
|
+
}
|
|
688
|
+
this.recordSpending(amountDisplay);
|
|
689
|
+
console.log(`[MoltsPay] Success!`);
|
|
690
|
+
return result.result || result;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Handle BNB Chain payment flow (pre-approval + intent signature)
|
|
694
|
+
*
|
|
695
|
+
* Flow:
|
|
696
|
+
* 1. Check client has approved server wallet (done via `moltspay init`)
|
|
697
|
+
* 2. Sign EIP-712 payment intent (no gas, just signature)
|
|
698
|
+
* 3. Send intent to server
|
|
699
|
+
* 4. Server executes service
|
|
700
|
+
* 5. Server calls transferFrom if successful (pay-for-success)
|
|
701
|
+
*/
|
|
702
|
+
async handleBNBPayment(serverUrl, service, params, paymentDetails) {
|
|
703
|
+
const { to, amount, token, chainName, chain, spender } = paymentDetails;
|
|
704
|
+
const tokenConfig = chain.tokens[token];
|
|
705
|
+
const provider = new import_ethers.ethers.JsonRpcProvider(chain.rpc);
|
|
706
|
+
const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
|
|
707
|
+
const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
|
|
708
|
+
if (allowance < amountWeiCheck) {
|
|
709
|
+
const nativeBalance = await provider.getBalance(this.wallet.address);
|
|
710
|
+
const minGasBalance = import_ethers.ethers.parseEther("0.0005");
|
|
711
|
+
if (nativeBalance < minGasBalance) {
|
|
712
|
+
const nativeBNB = parseFloat(import_ethers.ethers.formatEther(nativeBalance)).toFixed(4);
|
|
713
|
+
const isTestnet = chainName === "bnb_testnet";
|
|
714
|
+
if (isTestnet) {
|
|
715
|
+
throw new Error(
|
|
716
|
+
`\u274C Insufficient tBNB for approval transaction
|
|
717
|
+
|
|
718
|
+
Current tBNB: ${nativeBNB}
|
|
719
|
+
Required: ~0.001 tBNB
|
|
720
|
+
|
|
721
|
+
Get testnet tokens: npx moltspay faucet --chain bnb_testnet
|
|
722
|
+
(Gives USDC + tBNB for gas)`
|
|
723
|
+
);
|
|
724
|
+
} else {
|
|
725
|
+
throw new Error(
|
|
726
|
+
`\u274C Insufficient BNB for approval transaction
|
|
727
|
+
|
|
728
|
+
Current BNB: ${nativeBNB}
|
|
729
|
+
Required: ~0.001 BNB (~$0.60)
|
|
730
|
+
|
|
731
|
+
To get BNB:
|
|
732
|
+
\u2022 Withdraw from Binance/exchange to your wallet
|
|
733
|
+
\u2022 Most exchanges include BNB dust with withdrawals
|
|
734
|
+
|
|
735
|
+
After funding, run:
|
|
736
|
+
npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
throw new Error(
|
|
741
|
+
`Insufficient allowance for ${spender.slice(0, 10)}...
|
|
742
|
+
Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
|
|
746
|
+
const intent = {
|
|
747
|
+
from: this.wallet.address,
|
|
748
|
+
to,
|
|
749
|
+
amount: amountWei,
|
|
750
|
+
token: tokenConfig.address,
|
|
751
|
+
service,
|
|
752
|
+
nonce: Date.now(),
|
|
753
|
+
// Use timestamp as nonce for simplicity
|
|
754
|
+
deadline: Date.now() + 36e5
|
|
755
|
+
// 1 hour
|
|
756
|
+
};
|
|
757
|
+
const domain = {
|
|
758
|
+
name: "MoltsPay",
|
|
759
|
+
version: "1",
|
|
760
|
+
chainId: chain.chainId
|
|
761
|
+
};
|
|
762
|
+
const types = {
|
|
763
|
+
PaymentIntent: [
|
|
764
|
+
{ name: "from", type: "address" },
|
|
765
|
+
{ name: "to", type: "address" },
|
|
766
|
+
{ name: "amount", type: "uint256" },
|
|
767
|
+
{ name: "token", type: "address" },
|
|
768
|
+
{ name: "service", type: "string" },
|
|
769
|
+
{ name: "nonce", type: "uint256" },
|
|
770
|
+
{ name: "deadline", type: "uint256" }
|
|
771
|
+
]
|
|
772
|
+
};
|
|
773
|
+
console.log(`[MoltsPay] Signing BNB payment intent...`);
|
|
774
|
+
const signature = await this.wallet.signTypedData(domain, types, intent);
|
|
775
|
+
const network = `eip155:${chain.chainId}`;
|
|
776
|
+
const payload = {
|
|
777
|
+
x402Version: 2,
|
|
778
|
+
scheme: "exact",
|
|
779
|
+
network,
|
|
780
|
+
payload: {
|
|
781
|
+
intent: {
|
|
782
|
+
...intent,
|
|
783
|
+
signature
|
|
784
|
+
},
|
|
785
|
+
chainId: chain.chainId
|
|
786
|
+
},
|
|
787
|
+
accepted: {
|
|
788
|
+
scheme: "exact",
|
|
789
|
+
network,
|
|
790
|
+
asset: tokenConfig.address,
|
|
791
|
+
amount: amountWei,
|
|
792
|
+
payTo: to,
|
|
793
|
+
maxTimeoutSeconds: 300
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
797
|
+
console.log(`[MoltsPay] Sending BNB payment request...`);
|
|
798
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
799
|
+
method: "POST",
|
|
800
|
+
headers: {
|
|
801
|
+
"Content-Type": "application/json",
|
|
802
|
+
"X-Payment": paymentHeader
|
|
803
|
+
},
|
|
804
|
+
body: JSON.stringify({ service, params, chain: chainName })
|
|
805
|
+
});
|
|
806
|
+
const result = await paidRes.json();
|
|
807
|
+
if (!paidRes.ok) {
|
|
808
|
+
throw new Error(result.error || "BNB payment failed");
|
|
809
|
+
}
|
|
810
|
+
this.recordSpending(amount);
|
|
811
|
+
console.log(`[MoltsPay] Success! BNB payment settled.`);
|
|
812
|
+
return result.result || result;
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Handle Solana payment flow
|
|
816
|
+
*
|
|
817
|
+
* Solana uses SPL token transfers with pay-for-success model:
|
|
818
|
+
* 1. Client creates and signs a transfer transaction
|
|
819
|
+
* 2. Server submits the transaction after service completes
|
|
820
|
+
*/
|
|
821
|
+
async handleSolanaPayment(serverUrl, service, params, requirements, chain) {
|
|
822
|
+
const solanaWallet = loadSolanaWallet(this.configDir);
|
|
823
|
+
if (!solanaWallet) {
|
|
824
|
+
throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
|
|
825
|
+
}
|
|
826
|
+
const amount = Number(requirements.amount);
|
|
827
|
+
const amountUSDC = amount / 1e6;
|
|
828
|
+
this.checkLimits(amountUSDC);
|
|
829
|
+
console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
|
|
830
|
+
if (!requirements.payTo) {
|
|
831
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
832
|
+
}
|
|
833
|
+
const solanaFeePayer = requirements.extra?.solanaFeePayer;
|
|
834
|
+
const feePayerPubkey = solanaFeePayer ? new import_web34.PublicKey(solanaFeePayer) : void 0;
|
|
835
|
+
if (feePayerPubkey) {
|
|
836
|
+
console.log(`[MoltsPay] Gasless mode: server pays fees`);
|
|
837
|
+
}
|
|
838
|
+
const recipientPubkey = new import_web34.PublicKey(requirements.payTo);
|
|
839
|
+
const transaction = await createSolanaPaymentTransaction(
|
|
840
|
+
solanaWallet.publicKey,
|
|
841
|
+
recipientPubkey,
|
|
842
|
+
BigInt(amount),
|
|
843
|
+
chain,
|
|
844
|
+
feePayerPubkey
|
|
845
|
+
// Optional fee payer for gasless mode
|
|
846
|
+
);
|
|
847
|
+
if (feePayerPubkey) {
|
|
848
|
+
transaction.partialSign(solanaWallet);
|
|
849
|
+
} else {
|
|
850
|
+
transaction.sign(solanaWallet);
|
|
851
|
+
}
|
|
852
|
+
const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
|
|
853
|
+
console.log(`[MoltsPay] Transaction signed, sending to server...`);
|
|
854
|
+
const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
855
|
+
const payload = {
|
|
856
|
+
x402Version: 2,
|
|
857
|
+
scheme: "exact",
|
|
858
|
+
network,
|
|
859
|
+
payload: {
|
|
860
|
+
signedTransaction: signedTx,
|
|
861
|
+
sender: solanaWallet.publicKey.toBase58(),
|
|
862
|
+
chain
|
|
863
|
+
},
|
|
864
|
+
accepted: {
|
|
865
|
+
scheme: "exact",
|
|
866
|
+
network,
|
|
867
|
+
asset: requirements.asset,
|
|
868
|
+
amount: requirements.amount,
|
|
869
|
+
payTo: requirements.payTo,
|
|
870
|
+
maxTimeoutSeconds: 300
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
874
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
875
|
+
method: "POST",
|
|
876
|
+
headers: {
|
|
877
|
+
"Content-Type": "application/json",
|
|
878
|
+
"X-Payment": paymentHeader
|
|
879
|
+
},
|
|
880
|
+
body: JSON.stringify({ service, params, chain })
|
|
881
|
+
});
|
|
882
|
+
const result = await paidRes.json();
|
|
883
|
+
if (!paidRes.ok) {
|
|
884
|
+
throw new Error(result.error || "Solana payment failed");
|
|
885
|
+
}
|
|
886
|
+
this.recordSpending(amountUSDC);
|
|
887
|
+
console.log(`[MoltsPay] Success! Solana payment settled.`);
|
|
888
|
+
if (result.payment?.transaction) {
|
|
889
|
+
const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
|
|
890
|
+
console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
|
|
891
|
+
}
|
|
892
|
+
return result.result || result;
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Check ERC20 allowance for a spender
|
|
896
|
+
*/
|
|
897
|
+
async checkAllowance(tokenAddress, spender, provider) {
|
|
898
|
+
const contract = new import_ethers.ethers.Contract(
|
|
899
|
+
tokenAddress,
|
|
900
|
+
["function allowance(address owner, address spender) view returns (uint256)"],
|
|
901
|
+
provider
|
|
902
|
+
);
|
|
903
|
+
return await contract.allowance(this.wallet.address, spender);
|
|
904
|
+
}
|
|
397
905
|
/**
|
|
398
906
|
* Sign EIP-3009 transferWithAuthorization (GASLESS)
|
|
399
907
|
* This only signs - no on-chain transaction, no gas needed.
|
|
@@ -464,26 +972,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
464
972
|
}
|
|
465
973
|
// --- Config & Wallet Management ---
|
|
466
974
|
loadConfig() {
|
|
467
|
-
const configPath = (0,
|
|
468
|
-
if ((0,
|
|
469
|
-
const content = (0,
|
|
975
|
+
const configPath = (0, import_path2.join)(this.configDir, "config.json");
|
|
976
|
+
if ((0, import_fs2.existsSync)(configPath)) {
|
|
977
|
+
const content = (0, import_fs2.readFileSync)(configPath, "utf-8");
|
|
470
978
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
471
979
|
}
|
|
472
980
|
return { ...DEFAULT_CONFIG };
|
|
473
981
|
}
|
|
474
982
|
saveConfig() {
|
|
475
|
-
(0,
|
|
476
|
-
const configPath = (0,
|
|
477
|
-
(0,
|
|
983
|
+
(0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
|
|
984
|
+
const configPath = (0, import_path2.join)(this.configDir, "config.json");
|
|
985
|
+
(0, import_fs2.writeFileSync)(configPath, JSON.stringify(this.config, null, 2));
|
|
478
986
|
}
|
|
479
987
|
/**
|
|
480
988
|
* Load spending data from disk
|
|
481
989
|
*/
|
|
482
990
|
loadSpending() {
|
|
483
|
-
const spendingPath = (0,
|
|
484
|
-
if ((0,
|
|
991
|
+
const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
|
|
992
|
+
if ((0, import_fs2.existsSync)(spendingPath)) {
|
|
485
993
|
try {
|
|
486
|
-
const data = JSON.parse((0,
|
|
994
|
+
const data = JSON.parse((0, import_fs2.readFileSync)(spendingPath, "utf-8"));
|
|
487
995
|
const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
|
|
488
996
|
if (data.date && data.date === today) {
|
|
489
997
|
this.todaySpending = data.amount || 0;
|
|
@@ -502,29 +1010,29 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
502
1010
|
* Save spending data to disk
|
|
503
1011
|
*/
|
|
504
1012
|
saveSpending() {
|
|
505
|
-
(0,
|
|
506
|
-
const spendingPath = (0,
|
|
1013
|
+
(0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
|
|
1014
|
+
const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
|
|
507
1015
|
const data = {
|
|
508
1016
|
date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
|
|
509
1017
|
amount: this.todaySpending,
|
|
510
1018
|
updatedAt: Date.now()
|
|
511
1019
|
};
|
|
512
|
-
(0,
|
|
1020
|
+
(0, import_fs2.writeFileSync)(spendingPath, JSON.stringify(data, null, 2));
|
|
513
1021
|
}
|
|
514
1022
|
loadWallet() {
|
|
515
|
-
const walletPath = (0,
|
|
516
|
-
if ((0,
|
|
1023
|
+
const walletPath = (0, import_path2.join)(this.configDir, "wallet.json");
|
|
1024
|
+
if ((0, import_fs2.existsSync)(walletPath)) {
|
|
517
1025
|
try {
|
|
518
|
-
const stats = (0,
|
|
1026
|
+
const stats = (0, import_fs2.statSync)(walletPath);
|
|
519
1027
|
const mode = stats.mode & 511;
|
|
520
1028
|
if (mode !== 384) {
|
|
521
1029
|
console.warn(`[MoltsPay] WARNING: wallet.json has insecure permissions (${mode.toString(8)})`);
|
|
522
1030
|
console.warn(`[MoltsPay] Fixing permissions to 0600...`);
|
|
523
|
-
(0,
|
|
1031
|
+
(0, import_fs2.chmodSync)(walletPath, 384);
|
|
524
1032
|
}
|
|
525
1033
|
} catch (err) {
|
|
526
1034
|
}
|
|
527
|
-
const content = (0,
|
|
1035
|
+
const content = (0, import_fs2.readFileSync)(walletPath, "utf-8");
|
|
528
1036
|
return JSON.parse(content);
|
|
529
1037
|
}
|
|
530
1038
|
return null;
|
|
@@ -533,15 +1041,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
533
1041
|
* Initialize a new wallet (called by CLI)
|
|
534
1042
|
*/
|
|
535
1043
|
static init(configDir, options) {
|
|
536
|
-
(0,
|
|
1044
|
+
(0, import_fs2.mkdirSync)(configDir, { recursive: true });
|
|
537
1045
|
const wallet = import_ethers.Wallet.createRandom();
|
|
538
1046
|
const walletData = {
|
|
539
1047
|
address: wallet.address,
|
|
540
1048
|
privateKey: wallet.privateKey,
|
|
541
1049
|
createdAt: Date.now()
|
|
542
1050
|
};
|
|
543
|
-
const walletPath = (0,
|
|
544
|
-
(0,
|
|
1051
|
+
const walletPath = (0, import_path2.join)(configDir, "wallet.json");
|
|
1052
|
+
(0, import_fs2.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
545
1053
|
const config = {
|
|
546
1054
|
chain: options.chain,
|
|
547
1055
|
limits: {
|
|
@@ -549,8 +1057,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
549
1057
|
maxPerDay: options.maxPerDay
|
|
550
1058
|
}
|
|
551
1059
|
};
|
|
552
|
-
const configPath = (0,
|
|
553
|
-
(0,
|
|
1060
|
+
const configPath = (0, import_path2.join)(configDir, "config.json");
|
|
1061
|
+
(0, import_fs2.writeFileSync)(configPath, JSON.stringify(config, null, 2));
|
|
554
1062
|
return { address: wallet.address, configDir };
|
|
555
1063
|
}
|
|
556
1064
|
/**
|
|
@@ -586,7 +1094,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
586
1094
|
if (!this.wallet) {
|
|
587
1095
|
throw new Error("Client not initialized");
|
|
588
1096
|
}
|
|
589
|
-
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
|
|
1097
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
|
|
590
1098
|
const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
591
1099
|
const results = {};
|
|
592
1100
|
const tempoTokens = {
|