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.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/client/index.ts
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, chmodSync } from "fs";
|
|
3
|
-
import { homedir } from "os";
|
|
4
|
-
import { join } from "path";
|
|
2
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, statSync, chmodSync } from "fs";
|
|
3
|
+
import { homedir as homedir2 } from "os";
|
|
4
|
+
import { join as join2 } from "path";
|
|
5
5
|
import { Wallet, ethers } from "ethers";
|
|
6
6
|
|
|
7
7
|
// src/chains/index.ts
|
|
@@ -111,6 +111,63 @@ var CHAINS = {
|
|
|
111
111
|
explorerTx: "https://explore.testnet.tempo.xyz/tx/",
|
|
112
112
|
avgBlockTime: 0.5
|
|
113
113
|
// ~500ms finality
|
|
114
|
+
},
|
|
115
|
+
// ============ BNB Chain Testnet ============
|
|
116
|
+
bnb_testnet: {
|
|
117
|
+
name: "BNB Testnet",
|
|
118
|
+
chainId: 97,
|
|
119
|
+
rpc: "https://data-seed-prebsc-1-s1.binance.org:8545",
|
|
120
|
+
tokens: {
|
|
121
|
+
// Note: BNB uses 18 decimals for stablecoins (unlike Base/Polygon which use 6)
|
|
122
|
+
// Using official Binance-Peg testnet tokens
|
|
123
|
+
USDC: {
|
|
124
|
+
address: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
125
|
+
// Testnet USDC
|
|
126
|
+
decimals: 18,
|
|
127
|
+
symbol: "USDC",
|
|
128
|
+
eip712Name: "USD Coin"
|
|
129
|
+
},
|
|
130
|
+
USDT: {
|
|
131
|
+
address: "0x337610d27c682E347C9cD60BD4b3b107C9d34dDd",
|
|
132
|
+
// Testnet USDT
|
|
133
|
+
decimals: 18,
|
|
134
|
+
symbol: "USDT",
|
|
135
|
+
eip712Name: "Tether USD"
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
usdc: "0x64544969ed7EBf5f083679233325356EbE738930",
|
|
139
|
+
explorer: "https://testnet.bscscan.com/address/",
|
|
140
|
+
explorerTx: "https://testnet.bscscan.com/tx/",
|
|
141
|
+
avgBlockTime: 3,
|
|
142
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
143
|
+
requiresApproval: true
|
|
144
|
+
},
|
|
145
|
+
// ============ BNB Chain Mainnet ============
|
|
146
|
+
bnb: {
|
|
147
|
+
name: "BNB Smart Chain",
|
|
148
|
+
chainId: 56,
|
|
149
|
+
rpc: "https://bsc-dataseed.binance.org",
|
|
150
|
+
tokens: {
|
|
151
|
+
// Note: BNB uses 18 decimals for stablecoins
|
|
152
|
+
USDC: {
|
|
153
|
+
address: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
154
|
+
decimals: 18,
|
|
155
|
+
symbol: "USDC",
|
|
156
|
+
eip712Name: "USD Coin"
|
|
157
|
+
},
|
|
158
|
+
USDT: {
|
|
159
|
+
address: "0x55d398326f99059fF775485246999027B3197955",
|
|
160
|
+
decimals: 18,
|
|
161
|
+
symbol: "USDT",
|
|
162
|
+
eip712Name: "Tether USD"
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
usdc: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
|
|
166
|
+
explorer: "https://bscscan.com/address/",
|
|
167
|
+
explorerTx: "https://bscscan.com/tx/",
|
|
168
|
+
avgBlockTime: 3,
|
|
169
|
+
// BNB-specific: requires approval for pay-for-success flow
|
|
170
|
+
requiresApproval: true
|
|
114
171
|
}
|
|
115
172
|
};
|
|
116
173
|
function getChain(name) {
|
|
@@ -121,7 +178,129 @@ function getChain(name) {
|
|
|
121
178
|
return config;
|
|
122
179
|
}
|
|
123
180
|
|
|
181
|
+
// src/wallet/solana.ts
|
|
182
|
+
import { Keypair, PublicKey as PublicKey2, LAMPORTS_PER_SOL } from "@solana/web3.js";
|
|
183
|
+
import { getAssociatedTokenAddress, getAccount } from "@solana/spl-token";
|
|
184
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
185
|
+
import { join } from "path";
|
|
186
|
+
import { homedir } from "os";
|
|
187
|
+
import bs58 from "bs58";
|
|
188
|
+
|
|
189
|
+
// src/chains/solana.ts
|
|
190
|
+
import { Connection, PublicKey } from "@solana/web3.js";
|
|
191
|
+
var SOLANA_CHAINS = {
|
|
192
|
+
solana: {
|
|
193
|
+
name: "Solana Mainnet",
|
|
194
|
+
cluster: "mainnet-beta",
|
|
195
|
+
rpc: "https://api.mainnet-beta.solana.com",
|
|
196
|
+
explorer: "https://solscan.io/account/",
|
|
197
|
+
explorerTx: "https://solscan.io/tx/",
|
|
198
|
+
tokens: {
|
|
199
|
+
USDC: {
|
|
200
|
+
mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
201
|
+
// Circle official USDC
|
|
202
|
+
decimals: 6
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
solana_devnet: {
|
|
207
|
+
name: "Solana Devnet",
|
|
208
|
+
cluster: "devnet",
|
|
209
|
+
rpc: "https://api.devnet.solana.com",
|
|
210
|
+
explorer: "https://solscan.io/account/",
|
|
211
|
+
explorerTx: "https://solscan.io/tx/",
|
|
212
|
+
tokens: {
|
|
213
|
+
USDC: {
|
|
214
|
+
// Circle's devnet USDC (if not available, we'll deploy our own test token)
|
|
215
|
+
mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
216
|
+
decimals: 6
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// src/wallet/solana.ts
|
|
223
|
+
var DEFAULT_CONFIG_DIR = join(homedir(), ".moltspay");
|
|
224
|
+
var SOLANA_WALLET_FILE = "wallet-solana.json";
|
|
225
|
+
function getSolanaWalletPath(configDir = DEFAULT_CONFIG_DIR) {
|
|
226
|
+
return join(configDir, SOLANA_WALLET_FILE);
|
|
227
|
+
}
|
|
228
|
+
function loadSolanaWallet(configDir = DEFAULT_CONFIG_DIR) {
|
|
229
|
+
const walletPath = getSolanaWalletPath(configDir);
|
|
230
|
+
if (!existsSync(walletPath)) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
const data = JSON.parse(readFileSync(walletPath, "utf-8"));
|
|
235
|
+
const secretKey = bs58.decode(data.secretKey);
|
|
236
|
+
return Keypair.fromSecretKey(secretKey);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error("Failed to load Solana wallet:", error);
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/facilitators/solana.ts
|
|
244
|
+
import {
|
|
245
|
+
Connection as Connection3,
|
|
246
|
+
PublicKey as PublicKey3,
|
|
247
|
+
Transaction,
|
|
248
|
+
VersionedTransaction
|
|
249
|
+
} from "@solana/web3.js";
|
|
250
|
+
import {
|
|
251
|
+
getAssociatedTokenAddress as getAssociatedTokenAddress2,
|
|
252
|
+
createTransferCheckedInstruction,
|
|
253
|
+
getAccount as getAccount2,
|
|
254
|
+
createAssociatedTokenAccountInstruction
|
|
255
|
+
} from "@solana/spl-token";
|
|
256
|
+
async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
|
|
257
|
+
const chainConfig = SOLANA_CHAINS[chain];
|
|
258
|
+
const connection = new Connection3(chainConfig.rpc, "confirmed");
|
|
259
|
+
const mint = new PublicKey3(chainConfig.tokens.USDC.mint);
|
|
260
|
+
const actualFeePayer = feePayerPubkey || senderPubkey;
|
|
261
|
+
const senderATA = await getAssociatedTokenAddress2(mint, senderPubkey);
|
|
262
|
+
const recipientATA = await getAssociatedTokenAddress2(mint, recipientPubkey);
|
|
263
|
+
const transaction = new Transaction();
|
|
264
|
+
try {
|
|
265
|
+
await getAccount2(connection, recipientATA);
|
|
266
|
+
} catch {
|
|
267
|
+
transaction.add(
|
|
268
|
+
createAssociatedTokenAccountInstruction(
|
|
269
|
+
actualFeePayer,
|
|
270
|
+
// payer (fee payer in gasless mode)
|
|
271
|
+
recipientATA,
|
|
272
|
+
// ata to create
|
|
273
|
+
recipientPubkey,
|
|
274
|
+
// owner
|
|
275
|
+
mint
|
|
276
|
+
// mint
|
|
277
|
+
)
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
transaction.add(
|
|
281
|
+
createTransferCheckedInstruction(
|
|
282
|
+
senderATA,
|
|
283
|
+
// source
|
|
284
|
+
mint,
|
|
285
|
+
// mint
|
|
286
|
+
recipientATA,
|
|
287
|
+
// destination
|
|
288
|
+
senderPubkey,
|
|
289
|
+
// owner (sender still authorizes the transfer)
|
|
290
|
+
amount,
|
|
291
|
+
// amount
|
|
292
|
+
chainConfig.tokens.USDC.decimals
|
|
293
|
+
// decimals
|
|
294
|
+
)
|
|
295
|
+
);
|
|
296
|
+
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
|
|
297
|
+
transaction.recentBlockhash = blockhash;
|
|
298
|
+
transaction.feePayer = actualFeePayer;
|
|
299
|
+
return transaction;
|
|
300
|
+
}
|
|
301
|
+
|
|
124
302
|
// src/client/index.ts
|
|
303
|
+
import { PublicKey as PublicKey4 } from "@solana/web3.js";
|
|
125
304
|
var X402_VERSION = 2;
|
|
126
305
|
var PAYMENT_REQUIRED_HEADER = "x-payment-required";
|
|
127
306
|
var PAYMENT_HEADER = "x-payment";
|
|
@@ -140,7 +319,7 @@ var MoltsPayClient = class {
|
|
|
140
319
|
todaySpending = 0;
|
|
141
320
|
lastSpendingReset = 0;
|
|
142
321
|
constructor(options = {}) {
|
|
143
|
-
this.configDir = options.configDir ||
|
|
322
|
+
this.configDir = options.configDir || join2(homedir2(), ".moltspay");
|
|
144
323
|
this.config = this.loadConfig();
|
|
145
324
|
this.walletData = this.loadWallet();
|
|
146
325
|
this.loadSpending();
|
|
@@ -160,6 +339,12 @@ var MoltsPayClient = class {
|
|
|
160
339
|
get address() {
|
|
161
340
|
return this.wallet?.address || null;
|
|
162
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Get wallet instance (for direct operations like approvals)
|
|
344
|
+
*/
|
|
345
|
+
getWallet() {
|
|
346
|
+
return this.wallet;
|
|
347
|
+
}
|
|
163
348
|
/**
|
|
164
349
|
* Get current config
|
|
165
350
|
*/
|
|
@@ -213,11 +398,26 @@ var MoltsPayClient = class {
|
|
|
213
398
|
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
214
399
|
}
|
|
215
400
|
console.log(`[MoltsPay] Requesting service: ${service}`);
|
|
216
|
-
|
|
401
|
+
let executeUrl = `${serverUrl}/execute`;
|
|
402
|
+
try {
|
|
403
|
+
const services = await this.getServices(serverUrl);
|
|
404
|
+
const svc = services.services?.find((s) => s.id === service);
|
|
405
|
+
if (svc?.endpoint) {
|
|
406
|
+
executeUrl = `${serverUrl}${svc.endpoint}`;
|
|
407
|
+
console.log(`[MoltsPay] Using service endpoint: ${svc.endpoint}`);
|
|
408
|
+
}
|
|
409
|
+
} catch {
|
|
410
|
+
}
|
|
411
|
+
let requestBody;
|
|
412
|
+
if (options.rawData) {
|
|
413
|
+
requestBody = { service, ...params };
|
|
414
|
+
} else {
|
|
415
|
+
requestBody = { service, params };
|
|
416
|
+
}
|
|
217
417
|
if (options.chain) {
|
|
218
418
|
requestBody.chain = options.chain;
|
|
219
419
|
}
|
|
220
|
-
const initialRes = await fetch(
|
|
420
|
+
const initialRes = await fetch(executeUrl, {
|
|
221
421
|
method: "POST",
|
|
222
422
|
headers: { "Content-Type": "application/json" },
|
|
223
423
|
body: JSON.stringify(requestBody)
|
|
@@ -229,9 +429,14 @@ var MoltsPayClient = class {
|
|
|
229
429
|
}
|
|
230
430
|
throw new Error(data.error || "Unexpected response");
|
|
231
431
|
}
|
|
432
|
+
const wwwAuthHeader = initialRes.headers.get("www-authenticate");
|
|
232
433
|
const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
434
|
+
if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
|
|
435
|
+
console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
|
|
436
|
+
return await this.handleMPPPayment(executeUrl, service, params, wwwAuthHeader, options);
|
|
437
|
+
}
|
|
233
438
|
if (!paymentRequiredHeader) {
|
|
234
|
-
throw new Error("Missing x-payment-required
|
|
439
|
+
throw new Error("Missing payment header (x-payment-required or www-authenticate)");
|
|
235
440
|
}
|
|
236
441
|
let requirements;
|
|
237
442
|
try {
|
|
@@ -248,17 +453,22 @@ var MoltsPayClient = class {
|
|
|
248
453
|
throw new Error("Invalid x-payment-required header");
|
|
249
454
|
}
|
|
250
455
|
const networkToChainName = (network2) => {
|
|
456
|
+
if (network2 === "solana:mainnet") return "solana";
|
|
457
|
+
if (network2 === "solana:devnet") return "solana_devnet";
|
|
251
458
|
const match = network2.match(/^eip155:(\d+)$/);
|
|
252
459
|
if (!match) return null;
|
|
253
460
|
const chainId = parseInt(match[1]);
|
|
254
461
|
if (chainId === 8453) return "base";
|
|
255
462
|
if (chainId === 137) return "polygon";
|
|
256
463
|
if (chainId === 84532) return "base_sepolia";
|
|
464
|
+
if (chainId === 42431) return "tempo_moderato";
|
|
465
|
+
if (chainId === 56) return "bnb";
|
|
466
|
+
if (chainId === 97) return "bnb_testnet";
|
|
257
467
|
return null;
|
|
258
468
|
};
|
|
259
469
|
const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
|
|
260
|
-
let chainName;
|
|
261
470
|
const userSpecifiedChain = options.chain;
|
|
471
|
+
let selectedChain;
|
|
262
472
|
if (userSpecifiedChain) {
|
|
263
473
|
if (!serverChains.includes(userSpecifiedChain)) {
|
|
264
474
|
throw new Error(
|
|
@@ -266,17 +476,27 @@ var MoltsPayClient = class {
|
|
|
266
476
|
Server accepts: ${serverChains.join(", ")}`
|
|
267
477
|
);
|
|
268
478
|
}
|
|
269
|
-
|
|
479
|
+
selectedChain = userSpecifiedChain;
|
|
270
480
|
} else {
|
|
271
481
|
if (serverChains.length === 1 && serverChains[0] === "base") {
|
|
272
|
-
|
|
482
|
+
selectedChain = "base";
|
|
273
483
|
} else {
|
|
274
484
|
throw new Error(
|
|
275
485
|
`Server accepts: ${serverChains.join(", ")}
|
|
276
|
-
Please specify: --chain
|
|
486
|
+
Please specify: --chain <chain_name>`
|
|
277
487
|
);
|
|
278
488
|
}
|
|
279
489
|
}
|
|
490
|
+
if (selectedChain === "solana" || selectedChain === "solana_devnet") {
|
|
491
|
+
const solanaChain = selectedChain;
|
|
492
|
+
const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
493
|
+
const req2 = requirements.find((r) => r.network === network2);
|
|
494
|
+
if (!req2) {
|
|
495
|
+
throw new Error(`Failed to find payment requirement for ${selectedChain}`);
|
|
496
|
+
}
|
|
497
|
+
return await this.handleSolanaPayment(executeUrl, service, params, req2, solanaChain, options);
|
|
498
|
+
}
|
|
499
|
+
const chainName = selectedChain;
|
|
280
500
|
const chain = getChain(chainName);
|
|
281
501
|
const network = `eip155:${chain.chainId}`;
|
|
282
502
|
const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
|
|
@@ -311,6 +531,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
311
531
|
} else {
|
|
312
532
|
console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
|
|
313
533
|
}
|
|
534
|
+
if (chainName === "bnb" || chainName === "bnb_testnet") {
|
|
535
|
+
console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
|
|
536
|
+
const payTo2 = req.payTo || req.resource;
|
|
537
|
+
if (!payTo2) {
|
|
538
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
539
|
+
}
|
|
540
|
+
const bnbSpender = req.extra?.bnbSpender;
|
|
541
|
+
if (!bnbSpender) {
|
|
542
|
+
throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
|
|
543
|
+
}
|
|
544
|
+
return await this.handleBNBPayment(executeUrl, service, params, {
|
|
545
|
+
to: payTo2,
|
|
546
|
+
amount,
|
|
547
|
+
token,
|
|
548
|
+
chainName,
|
|
549
|
+
chain,
|
|
550
|
+
spender: bnbSpender
|
|
551
|
+
}, options);
|
|
552
|
+
}
|
|
314
553
|
const payTo = req.payTo || req.resource;
|
|
315
554
|
if (!payTo) {
|
|
316
555
|
throw new Error("Missing payTo address in payment requirements");
|
|
@@ -340,11 +579,11 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
340
579
|
};
|
|
341
580
|
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
342
581
|
console.log(`[MoltsPay] Sending request with payment...`);
|
|
343
|
-
const paidRequestBody = { service, params };
|
|
582
|
+
const paidRequestBody = options.rawData ? { service, ...params } : { service, params };
|
|
344
583
|
if (options.chain) {
|
|
345
584
|
paidRequestBody.chain = options.chain;
|
|
346
585
|
}
|
|
347
|
-
const paidRes = await fetch(
|
|
586
|
+
const paidRes = await fetch(executeUrl, {
|
|
348
587
|
method: "POST",
|
|
349
588
|
headers: {
|
|
350
589
|
"Content-Type": "application/json",
|
|
@@ -358,7 +597,304 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
358
597
|
}
|
|
359
598
|
this.recordSpending(amount);
|
|
360
599
|
console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
|
|
361
|
-
return result.result;
|
|
600
|
+
return result.result || result;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Handle MPP (Machine Payments Protocol) payment flow
|
|
604
|
+
* Called when pay() detects WWW-Authenticate header in 402 response
|
|
605
|
+
*/
|
|
606
|
+
async handleMPPPayment(executeUrl, service, params, wwwAuthHeader, options = {}) {
|
|
607
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
608
|
+
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
609
|
+
const { tempoModerato } = await import("viem/chains");
|
|
610
|
+
const { Actions } = await import("viem/tempo");
|
|
611
|
+
const privateKey = this.walletData.privateKey;
|
|
612
|
+
const account = privateKeyToAccount(privateKey);
|
|
613
|
+
console.log(`[MoltsPay] Using MPP protocol on Tempo`);
|
|
614
|
+
console.log(`[MoltsPay] Account: ${account.address}`);
|
|
615
|
+
const parseAuthParam = (header, key) => {
|
|
616
|
+
const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
|
|
617
|
+
return match ? match[1] : null;
|
|
618
|
+
};
|
|
619
|
+
const challengeId = parseAuthParam(wwwAuthHeader, "id");
|
|
620
|
+
const method = parseAuthParam(wwwAuthHeader, "method");
|
|
621
|
+
const realm = parseAuthParam(wwwAuthHeader, "realm");
|
|
622
|
+
const requestB64 = parseAuthParam(wwwAuthHeader, "request");
|
|
623
|
+
if (method !== "tempo") {
|
|
624
|
+
throw new Error(`Unsupported payment method: ${method}`);
|
|
625
|
+
}
|
|
626
|
+
if (!requestB64) {
|
|
627
|
+
throw new Error("Missing request in WWW-Authenticate");
|
|
628
|
+
}
|
|
629
|
+
const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
|
|
630
|
+
const paymentRequest = JSON.parse(requestJson);
|
|
631
|
+
const { amount, currency, recipient, methodDetails } = paymentRequest;
|
|
632
|
+
const chainId = methodDetails?.chainId || 42431;
|
|
633
|
+
const amountDisplay = Number(amount) / 1e6;
|
|
634
|
+
console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
|
|
635
|
+
this.checkLimits(amountDisplay);
|
|
636
|
+
console.log(`[MoltsPay] Sending transaction on Tempo...`);
|
|
637
|
+
const tempoChain = { ...tempoModerato, feeToken: currency };
|
|
638
|
+
const publicClient = createPublicClient({
|
|
639
|
+
chain: tempoChain,
|
|
640
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
641
|
+
});
|
|
642
|
+
const walletClient = createWalletClient({
|
|
643
|
+
account,
|
|
644
|
+
chain: tempoChain,
|
|
645
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
646
|
+
});
|
|
647
|
+
const txHash = await Actions.token.transfer(walletClient, {
|
|
648
|
+
to: recipient,
|
|
649
|
+
amount: BigInt(amount),
|
|
650
|
+
token: currency
|
|
651
|
+
});
|
|
652
|
+
console.log(`[MoltsPay] Transaction: ${txHash}`);
|
|
653
|
+
await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
654
|
+
console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
|
|
655
|
+
const credential = {
|
|
656
|
+
challenge: {
|
|
657
|
+
id: challengeId,
|
|
658
|
+
realm,
|
|
659
|
+
method: "tempo",
|
|
660
|
+
intent: "charge",
|
|
661
|
+
request: paymentRequest
|
|
662
|
+
},
|
|
663
|
+
payload: { hash: txHash, type: "hash" },
|
|
664
|
+
source: `did:pkh:eip155:${chainId}:${account.address}`
|
|
665
|
+
};
|
|
666
|
+
const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
667
|
+
const retryBody = options.rawData ? { service, ...params, chain: "tempo_moderato" } : { service, params, chain: "tempo_moderato" };
|
|
668
|
+
const paidRes = await fetch(executeUrl, {
|
|
669
|
+
method: "POST",
|
|
670
|
+
headers: {
|
|
671
|
+
"Content-Type": "application/json",
|
|
672
|
+
"Authorization": `Payment ${credentialB64}`
|
|
673
|
+
},
|
|
674
|
+
body: JSON.stringify(retryBody)
|
|
675
|
+
});
|
|
676
|
+
const result = await paidRes.json();
|
|
677
|
+
if (!paidRes.ok) {
|
|
678
|
+
throw new Error(result.error || "Payment verification failed");
|
|
679
|
+
}
|
|
680
|
+
this.recordSpending(amountDisplay);
|
|
681
|
+
console.log(`[MoltsPay] Success!`);
|
|
682
|
+
return result.result || result;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Handle BNB Chain payment flow (pre-approval + intent signature)
|
|
686
|
+
*
|
|
687
|
+
* Flow:
|
|
688
|
+
* 1. Check client has approved server wallet (done via `moltspay init`)
|
|
689
|
+
* 2. Sign EIP-712 payment intent (no gas, just signature)
|
|
690
|
+
* 3. Send intent to server
|
|
691
|
+
* 4. Server executes service
|
|
692
|
+
* 5. Server calls transferFrom if successful (pay-for-success)
|
|
693
|
+
*/
|
|
694
|
+
async handleBNBPayment(executeUrl, service, params, paymentDetails, options = {}) {
|
|
695
|
+
const { to, amount, token, chainName, chain, spender } = paymentDetails;
|
|
696
|
+
const tokenConfig = chain.tokens[token];
|
|
697
|
+
const provider = new ethers.JsonRpcProvider(chain.rpc);
|
|
698
|
+
const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
|
|
699
|
+
const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
|
|
700
|
+
if (allowance < amountWeiCheck) {
|
|
701
|
+
const nativeBalance = await provider.getBalance(this.wallet.address);
|
|
702
|
+
const minGasBalance = ethers.parseEther("0.0005");
|
|
703
|
+
if (nativeBalance < minGasBalance) {
|
|
704
|
+
const nativeBNB = parseFloat(ethers.formatEther(nativeBalance)).toFixed(4);
|
|
705
|
+
const isTestnet = chainName === "bnb_testnet";
|
|
706
|
+
if (isTestnet) {
|
|
707
|
+
throw new Error(
|
|
708
|
+
`\u274C Insufficient tBNB for approval transaction
|
|
709
|
+
|
|
710
|
+
Current tBNB: ${nativeBNB}
|
|
711
|
+
Required: ~0.001 tBNB
|
|
712
|
+
|
|
713
|
+
Get testnet tokens: npx moltspay faucet --chain bnb_testnet
|
|
714
|
+
(Gives USDC + tBNB for gas)`
|
|
715
|
+
);
|
|
716
|
+
} else {
|
|
717
|
+
throw new Error(
|
|
718
|
+
`\u274C Insufficient BNB for approval transaction
|
|
719
|
+
|
|
720
|
+
Current BNB: ${nativeBNB}
|
|
721
|
+
Required: ~0.001 BNB (~$0.60)
|
|
722
|
+
|
|
723
|
+
To get BNB:
|
|
724
|
+
\u2022 Withdraw from Binance/exchange to your wallet
|
|
725
|
+
\u2022 Most exchanges include BNB dust with withdrawals
|
|
726
|
+
|
|
727
|
+
After funding, run:
|
|
728
|
+
npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
throw new Error(
|
|
733
|
+
`Insufficient allowance for ${spender.slice(0, 10)}...
|
|
734
|
+
Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
|
|
738
|
+
const intent = {
|
|
739
|
+
from: this.wallet.address,
|
|
740
|
+
to,
|
|
741
|
+
amount: amountWei,
|
|
742
|
+
token: tokenConfig.address,
|
|
743
|
+
service,
|
|
744
|
+
nonce: Date.now(),
|
|
745
|
+
// Use timestamp as nonce for simplicity
|
|
746
|
+
deadline: Date.now() + 36e5
|
|
747
|
+
// 1 hour
|
|
748
|
+
};
|
|
749
|
+
const domain = {
|
|
750
|
+
name: "MoltsPay",
|
|
751
|
+
version: "1",
|
|
752
|
+
chainId: chain.chainId
|
|
753
|
+
};
|
|
754
|
+
const types = {
|
|
755
|
+
PaymentIntent: [
|
|
756
|
+
{ name: "from", type: "address" },
|
|
757
|
+
{ name: "to", type: "address" },
|
|
758
|
+
{ name: "amount", type: "uint256" },
|
|
759
|
+
{ name: "token", type: "address" },
|
|
760
|
+
{ name: "service", type: "string" },
|
|
761
|
+
{ name: "nonce", type: "uint256" },
|
|
762
|
+
{ name: "deadline", type: "uint256" }
|
|
763
|
+
]
|
|
764
|
+
};
|
|
765
|
+
console.log(`[MoltsPay] Signing BNB payment intent...`);
|
|
766
|
+
const signature = await this.wallet.signTypedData(domain, types, intent);
|
|
767
|
+
const network = `eip155:${chain.chainId}`;
|
|
768
|
+
const payload = {
|
|
769
|
+
x402Version: 2,
|
|
770
|
+
scheme: "exact",
|
|
771
|
+
network,
|
|
772
|
+
payload: {
|
|
773
|
+
intent: {
|
|
774
|
+
...intent,
|
|
775
|
+
signature
|
|
776
|
+
},
|
|
777
|
+
chainId: chain.chainId
|
|
778
|
+
},
|
|
779
|
+
accepted: {
|
|
780
|
+
scheme: "exact",
|
|
781
|
+
network,
|
|
782
|
+
asset: tokenConfig.address,
|
|
783
|
+
amount: amountWei,
|
|
784
|
+
payTo: to,
|
|
785
|
+
maxTimeoutSeconds: 300
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
789
|
+
console.log(`[MoltsPay] Sending BNB payment request...`);
|
|
790
|
+
const bnbRequestBody = options.rawData ? { service, ...params, chain: chainName } : { service, params, chain: chainName };
|
|
791
|
+
const paidRes = await fetch(executeUrl, {
|
|
792
|
+
method: "POST",
|
|
793
|
+
headers: {
|
|
794
|
+
"Content-Type": "application/json",
|
|
795
|
+
"X-Payment": paymentHeader
|
|
796
|
+
},
|
|
797
|
+
body: JSON.stringify(bnbRequestBody)
|
|
798
|
+
});
|
|
799
|
+
const result = await paidRes.json();
|
|
800
|
+
if (!paidRes.ok) {
|
|
801
|
+
throw new Error(result.error || "BNB payment failed");
|
|
802
|
+
}
|
|
803
|
+
this.recordSpending(amount);
|
|
804
|
+
console.log(`[MoltsPay] Success! BNB payment settled.`);
|
|
805
|
+
return result.result || result;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Handle Solana payment flow
|
|
809
|
+
*
|
|
810
|
+
* Solana uses SPL token transfers with pay-for-success model:
|
|
811
|
+
* 1. Client creates and signs a transfer transaction
|
|
812
|
+
* 2. Server submits the transaction after service completes
|
|
813
|
+
*/
|
|
814
|
+
async handleSolanaPayment(executeUrl, service, params, requirements, chain, options = {}) {
|
|
815
|
+
const solanaWallet = loadSolanaWallet(this.configDir);
|
|
816
|
+
if (!solanaWallet) {
|
|
817
|
+
throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
|
|
818
|
+
}
|
|
819
|
+
const amount = Number(requirements.amount);
|
|
820
|
+
const amountUSDC = amount / 1e6;
|
|
821
|
+
this.checkLimits(amountUSDC);
|
|
822
|
+
console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
|
|
823
|
+
if (!requirements.payTo) {
|
|
824
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
825
|
+
}
|
|
826
|
+
const solanaFeePayer = requirements.extra?.solanaFeePayer;
|
|
827
|
+
const feePayerPubkey = solanaFeePayer ? new PublicKey4(solanaFeePayer) : void 0;
|
|
828
|
+
if (feePayerPubkey) {
|
|
829
|
+
console.log(`[MoltsPay] Gasless mode: server pays fees`);
|
|
830
|
+
}
|
|
831
|
+
const recipientPubkey = new PublicKey4(requirements.payTo);
|
|
832
|
+
const transaction = await createSolanaPaymentTransaction(
|
|
833
|
+
solanaWallet.publicKey,
|
|
834
|
+
recipientPubkey,
|
|
835
|
+
BigInt(amount),
|
|
836
|
+
chain,
|
|
837
|
+
feePayerPubkey
|
|
838
|
+
// Optional fee payer for gasless mode
|
|
839
|
+
);
|
|
840
|
+
if (feePayerPubkey) {
|
|
841
|
+
transaction.partialSign(solanaWallet);
|
|
842
|
+
} else {
|
|
843
|
+
transaction.sign(solanaWallet);
|
|
844
|
+
}
|
|
845
|
+
const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
|
|
846
|
+
console.log(`[MoltsPay] Transaction signed, sending to server...`);
|
|
847
|
+
const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
848
|
+
const payload = {
|
|
849
|
+
x402Version: 2,
|
|
850
|
+
scheme: "exact",
|
|
851
|
+
network,
|
|
852
|
+
payload: {
|
|
853
|
+
signedTransaction: signedTx,
|
|
854
|
+
sender: solanaWallet.publicKey.toBase58(),
|
|
855
|
+
chain
|
|
856
|
+
},
|
|
857
|
+
accepted: {
|
|
858
|
+
scheme: "exact",
|
|
859
|
+
network,
|
|
860
|
+
asset: requirements.asset,
|
|
861
|
+
amount: requirements.amount,
|
|
862
|
+
payTo: requirements.payTo,
|
|
863
|
+
maxTimeoutSeconds: 300
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
867
|
+
const solanaRequestBody = options.rawData ? { service, ...params, chain } : { service, params, chain };
|
|
868
|
+
const paidRes = await fetch(executeUrl, {
|
|
869
|
+
method: "POST",
|
|
870
|
+
headers: {
|
|
871
|
+
"Content-Type": "application/json",
|
|
872
|
+
"X-Payment": paymentHeader
|
|
873
|
+
},
|
|
874
|
+
body: JSON.stringify(solanaRequestBody)
|
|
875
|
+
});
|
|
876
|
+
const result = await paidRes.json();
|
|
877
|
+
if (!paidRes.ok) {
|
|
878
|
+
throw new Error(result.error || "Solana payment failed");
|
|
879
|
+
}
|
|
880
|
+
this.recordSpending(amountUSDC);
|
|
881
|
+
console.log(`[MoltsPay] Success! Solana payment settled.`);
|
|
882
|
+
if (result.payment?.transaction) {
|
|
883
|
+
const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
|
|
884
|
+
console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
|
|
885
|
+
}
|
|
886
|
+
return result.result || result;
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Check ERC20 allowance for a spender
|
|
890
|
+
*/
|
|
891
|
+
async checkAllowance(tokenAddress, spender, provider) {
|
|
892
|
+
const contract = new ethers.Contract(
|
|
893
|
+
tokenAddress,
|
|
894
|
+
["function allowance(address owner, address spender) view returns (uint256)"],
|
|
895
|
+
provider
|
|
896
|
+
);
|
|
897
|
+
return await contract.allowance(this.wallet.address, spender);
|
|
362
898
|
}
|
|
363
899
|
/**
|
|
364
900
|
* Sign EIP-3009 transferWithAuthorization (GASLESS)
|
|
@@ -430,26 +966,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
430
966
|
}
|
|
431
967
|
// --- Config & Wallet Management ---
|
|
432
968
|
loadConfig() {
|
|
433
|
-
const configPath =
|
|
434
|
-
if (
|
|
435
|
-
const content =
|
|
969
|
+
const configPath = join2(this.configDir, "config.json");
|
|
970
|
+
if (existsSync2(configPath)) {
|
|
971
|
+
const content = readFileSync2(configPath, "utf-8");
|
|
436
972
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
437
973
|
}
|
|
438
974
|
return { ...DEFAULT_CONFIG };
|
|
439
975
|
}
|
|
440
976
|
saveConfig() {
|
|
441
|
-
|
|
442
|
-
const configPath =
|
|
443
|
-
|
|
977
|
+
mkdirSync2(this.configDir, { recursive: true });
|
|
978
|
+
const configPath = join2(this.configDir, "config.json");
|
|
979
|
+
writeFileSync2(configPath, JSON.stringify(this.config, null, 2));
|
|
444
980
|
}
|
|
445
981
|
/**
|
|
446
982
|
* Load spending data from disk
|
|
447
983
|
*/
|
|
448
984
|
loadSpending() {
|
|
449
|
-
const spendingPath =
|
|
450
|
-
if (
|
|
985
|
+
const spendingPath = join2(this.configDir, "spending.json");
|
|
986
|
+
if (existsSync2(spendingPath)) {
|
|
451
987
|
try {
|
|
452
|
-
const data = JSON.parse(
|
|
988
|
+
const data = JSON.parse(readFileSync2(spendingPath, "utf-8"));
|
|
453
989
|
const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
|
|
454
990
|
if (data.date && data.date === today) {
|
|
455
991
|
this.todaySpending = data.amount || 0;
|
|
@@ -468,18 +1004,18 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
468
1004
|
* Save spending data to disk
|
|
469
1005
|
*/
|
|
470
1006
|
saveSpending() {
|
|
471
|
-
|
|
472
|
-
const spendingPath =
|
|
1007
|
+
mkdirSync2(this.configDir, { recursive: true });
|
|
1008
|
+
const spendingPath = join2(this.configDir, "spending.json");
|
|
473
1009
|
const data = {
|
|
474
1010
|
date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
|
|
475
1011
|
amount: this.todaySpending,
|
|
476
1012
|
updatedAt: Date.now()
|
|
477
1013
|
};
|
|
478
|
-
|
|
1014
|
+
writeFileSync2(spendingPath, JSON.stringify(data, null, 2));
|
|
479
1015
|
}
|
|
480
1016
|
loadWallet() {
|
|
481
|
-
const walletPath =
|
|
482
|
-
if (
|
|
1017
|
+
const walletPath = join2(this.configDir, "wallet.json");
|
|
1018
|
+
if (existsSync2(walletPath)) {
|
|
483
1019
|
try {
|
|
484
1020
|
const stats = statSync(walletPath);
|
|
485
1021
|
const mode = stats.mode & 511;
|
|
@@ -490,7 +1026,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
490
1026
|
}
|
|
491
1027
|
} catch (err) {
|
|
492
1028
|
}
|
|
493
|
-
const content =
|
|
1029
|
+
const content = readFileSync2(walletPath, "utf-8");
|
|
494
1030
|
return JSON.parse(content);
|
|
495
1031
|
}
|
|
496
1032
|
return null;
|
|
@@ -499,15 +1035,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
499
1035
|
* Initialize a new wallet (called by CLI)
|
|
500
1036
|
*/
|
|
501
1037
|
static init(configDir, options) {
|
|
502
|
-
|
|
1038
|
+
mkdirSync2(configDir, { recursive: true });
|
|
503
1039
|
const wallet = Wallet.createRandom();
|
|
504
1040
|
const walletData = {
|
|
505
1041
|
address: wallet.address,
|
|
506
1042
|
privateKey: wallet.privateKey,
|
|
507
1043
|
createdAt: Date.now()
|
|
508
1044
|
};
|
|
509
|
-
const walletPath =
|
|
510
|
-
|
|
1045
|
+
const walletPath = join2(configDir, "wallet.json");
|
|
1046
|
+
writeFileSync2(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
511
1047
|
const config = {
|
|
512
1048
|
chain: options.chain,
|
|
513
1049
|
limits: {
|
|
@@ -515,8 +1051,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
515
1051
|
maxPerDay: options.maxPerDay
|
|
516
1052
|
}
|
|
517
1053
|
};
|
|
518
|
-
const configPath =
|
|
519
|
-
|
|
1054
|
+
const configPath = join2(configDir, "config.json");
|
|
1055
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2));
|
|
520
1056
|
return { address: wallet.address, configDir };
|
|
521
1057
|
}
|
|
522
1058
|
/**
|
|
@@ -552,7 +1088,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
552
1088
|
if (!this.wallet) {
|
|
553
1089
|
throw new Error("Client not initialized");
|
|
554
1090
|
}
|
|
555
|
-
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
|
|
1091
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
|
|
556
1092
|
const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
557
1093
|
const results = {};
|
|
558
1094
|
const tempoTokens = {
|