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.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
|
*/
|
|
@@ -229,9 +414,14 @@ var MoltsPayClient = class {
|
|
|
229
414
|
}
|
|
230
415
|
throw new Error(data.error || "Unexpected response");
|
|
231
416
|
}
|
|
417
|
+
const wwwAuthHeader = initialRes.headers.get("www-authenticate");
|
|
232
418
|
const paymentRequiredHeader = initialRes.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
419
|
+
if (wwwAuthHeader && wwwAuthHeader.toLowerCase().includes("payment")) {
|
|
420
|
+
console.log("[MoltsPay] Detected MPP protocol, using Tempo flow...");
|
|
421
|
+
return await this.handleMPPPayment(serverUrl, service, params, wwwAuthHeader);
|
|
422
|
+
}
|
|
233
423
|
if (!paymentRequiredHeader) {
|
|
234
|
-
throw new Error("Missing x-payment-required
|
|
424
|
+
throw new Error("Missing payment header (x-payment-required or www-authenticate)");
|
|
235
425
|
}
|
|
236
426
|
let requirements;
|
|
237
427
|
try {
|
|
@@ -248,17 +438,22 @@ var MoltsPayClient = class {
|
|
|
248
438
|
throw new Error("Invalid x-payment-required header");
|
|
249
439
|
}
|
|
250
440
|
const networkToChainName = (network2) => {
|
|
441
|
+
if (network2 === "solana:mainnet") return "solana";
|
|
442
|
+
if (network2 === "solana:devnet") return "solana_devnet";
|
|
251
443
|
const match = network2.match(/^eip155:(\d+)$/);
|
|
252
444
|
if (!match) return null;
|
|
253
445
|
const chainId = parseInt(match[1]);
|
|
254
446
|
if (chainId === 8453) return "base";
|
|
255
447
|
if (chainId === 137) return "polygon";
|
|
256
448
|
if (chainId === 84532) return "base_sepolia";
|
|
449
|
+
if (chainId === 42431) return "tempo_moderato";
|
|
450
|
+
if (chainId === 56) return "bnb";
|
|
451
|
+
if (chainId === 97) return "bnb_testnet";
|
|
257
452
|
return null;
|
|
258
453
|
};
|
|
259
454
|
const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
|
|
260
|
-
let chainName;
|
|
261
455
|
const userSpecifiedChain = options.chain;
|
|
456
|
+
let selectedChain;
|
|
262
457
|
if (userSpecifiedChain) {
|
|
263
458
|
if (!serverChains.includes(userSpecifiedChain)) {
|
|
264
459
|
throw new Error(
|
|
@@ -266,17 +461,27 @@ var MoltsPayClient = class {
|
|
|
266
461
|
Server accepts: ${serverChains.join(", ")}`
|
|
267
462
|
);
|
|
268
463
|
}
|
|
269
|
-
|
|
464
|
+
selectedChain = userSpecifiedChain;
|
|
270
465
|
} else {
|
|
271
466
|
if (serverChains.length === 1 && serverChains[0] === "base") {
|
|
272
|
-
|
|
467
|
+
selectedChain = "base";
|
|
273
468
|
} else {
|
|
274
469
|
throw new Error(
|
|
275
470
|
`Server accepts: ${serverChains.join(", ")}
|
|
276
|
-
Please specify: --chain
|
|
471
|
+
Please specify: --chain <chain_name>`
|
|
277
472
|
);
|
|
278
473
|
}
|
|
279
474
|
}
|
|
475
|
+
if (selectedChain === "solana" || selectedChain === "solana_devnet") {
|
|
476
|
+
const solanaChain = selectedChain;
|
|
477
|
+
const network2 = solanaChain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
478
|
+
const req2 = requirements.find((r) => r.network === network2);
|
|
479
|
+
if (!req2) {
|
|
480
|
+
throw new Error(`Failed to find payment requirement for ${selectedChain}`);
|
|
481
|
+
}
|
|
482
|
+
return await this.handleSolanaPayment(serverUrl, service, params, req2, solanaChain);
|
|
483
|
+
}
|
|
484
|
+
const chainName = selectedChain;
|
|
280
485
|
const chain = getChain(chainName);
|
|
281
486
|
const network = `eip155:${chain.chainId}`;
|
|
282
487
|
const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
|
|
@@ -311,6 +516,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
311
516
|
} else {
|
|
312
517
|
console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
|
|
313
518
|
}
|
|
519
|
+
if (chainName === "bnb" || chainName === "bnb_testnet") {
|
|
520
|
+
console.log(`[MoltsPay] Using BNB intent-based payment flow...`);
|
|
521
|
+
const payTo2 = req.payTo || req.resource;
|
|
522
|
+
if (!payTo2) {
|
|
523
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
524
|
+
}
|
|
525
|
+
const bnbSpender = req.extra?.bnbSpender;
|
|
526
|
+
if (!bnbSpender) {
|
|
527
|
+
throw new Error("Server did not provide bnbSpender address. Server may not support BNB payments.");
|
|
528
|
+
}
|
|
529
|
+
return await this.handleBNBPayment(serverUrl, service, params, {
|
|
530
|
+
to: payTo2,
|
|
531
|
+
amount,
|
|
532
|
+
token,
|
|
533
|
+
chainName,
|
|
534
|
+
chain,
|
|
535
|
+
spender: bnbSpender
|
|
536
|
+
});
|
|
537
|
+
}
|
|
314
538
|
const payTo = req.payTo || req.resource;
|
|
315
539
|
if (!payTo) {
|
|
316
540
|
throw new Error("Missing payTo address in payment requirements");
|
|
@@ -360,6 +584,300 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
360
584
|
console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
|
|
361
585
|
return result.result;
|
|
362
586
|
}
|
|
587
|
+
/**
|
|
588
|
+
* Handle MPP (Machine Payments Protocol) payment flow
|
|
589
|
+
* Called when pay() detects WWW-Authenticate header in 402 response
|
|
590
|
+
*/
|
|
591
|
+
async handleMPPPayment(serverUrl, service, params, wwwAuthHeader) {
|
|
592
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
593
|
+
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
594
|
+
const { tempoModerato } = await import("viem/chains");
|
|
595
|
+
const { Actions } = await import("viem/tempo");
|
|
596
|
+
const privateKey = this.walletData.privateKey;
|
|
597
|
+
const account = privateKeyToAccount(privateKey);
|
|
598
|
+
console.log(`[MoltsPay] Using MPP protocol on Tempo`);
|
|
599
|
+
console.log(`[MoltsPay] Account: ${account.address}`);
|
|
600
|
+
const parseAuthParam = (header, key) => {
|
|
601
|
+
const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
|
|
602
|
+
return match ? match[1] : null;
|
|
603
|
+
};
|
|
604
|
+
const challengeId = parseAuthParam(wwwAuthHeader, "id");
|
|
605
|
+
const method = parseAuthParam(wwwAuthHeader, "method");
|
|
606
|
+
const realm = parseAuthParam(wwwAuthHeader, "realm");
|
|
607
|
+
const requestB64 = parseAuthParam(wwwAuthHeader, "request");
|
|
608
|
+
if (method !== "tempo") {
|
|
609
|
+
throw new Error(`Unsupported payment method: ${method}`);
|
|
610
|
+
}
|
|
611
|
+
if (!requestB64) {
|
|
612
|
+
throw new Error("Missing request in WWW-Authenticate");
|
|
613
|
+
}
|
|
614
|
+
const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
|
|
615
|
+
const paymentRequest = JSON.parse(requestJson);
|
|
616
|
+
const { amount, currency, recipient, methodDetails } = paymentRequest;
|
|
617
|
+
const chainId = methodDetails?.chainId || 42431;
|
|
618
|
+
const amountDisplay = Number(amount) / 1e6;
|
|
619
|
+
console.log(`[MoltsPay] Payment: $${amountDisplay} to ${recipient}`);
|
|
620
|
+
this.checkLimits(amountDisplay);
|
|
621
|
+
console.log(`[MoltsPay] Sending transaction on Tempo...`);
|
|
622
|
+
const tempoChain = { ...tempoModerato, feeToken: currency };
|
|
623
|
+
const publicClient = createPublicClient({
|
|
624
|
+
chain: tempoChain,
|
|
625
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
626
|
+
});
|
|
627
|
+
const walletClient = createWalletClient({
|
|
628
|
+
account,
|
|
629
|
+
chain: tempoChain,
|
|
630
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
631
|
+
});
|
|
632
|
+
const txHash = await Actions.token.transfer(walletClient, {
|
|
633
|
+
to: recipient,
|
|
634
|
+
amount: BigInt(amount),
|
|
635
|
+
token: currency
|
|
636
|
+
});
|
|
637
|
+
console.log(`[MoltsPay] Transaction: ${txHash}`);
|
|
638
|
+
await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
639
|
+
console.log(`[MoltsPay] Confirmed! Retrying with credential...`);
|
|
640
|
+
const credential = {
|
|
641
|
+
challenge: {
|
|
642
|
+
id: challengeId,
|
|
643
|
+
realm,
|
|
644
|
+
method: "tempo",
|
|
645
|
+
intent: "charge",
|
|
646
|
+
request: paymentRequest
|
|
647
|
+
},
|
|
648
|
+
payload: { hash: txHash, type: "hash" },
|
|
649
|
+
source: `did:pkh:eip155:${chainId}:${account.address}`
|
|
650
|
+
};
|
|
651
|
+
const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
652
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
653
|
+
method: "POST",
|
|
654
|
+
headers: {
|
|
655
|
+
"Content-Type": "application/json",
|
|
656
|
+
"Authorization": `Payment ${credentialB64}`
|
|
657
|
+
},
|
|
658
|
+
body: JSON.stringify({ service, params, chain: "tempo_moderato" })
|
|
659
|
+
});
|
|
660
|
+
const result = await paidRes.json();
|
|
661
|
+
if (!paidRes.ok) {
|
|
662
|
+
throw new Error(result.error || "Payment verification failed");
|
|
663
|
+
}
|
|
664
|
+
this.recordSpending(amountDisplay);
|
|
665
|
+
console.log(`[MoltsPay] Success!`);
|
|
666
|
+
return result.result || result;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Handle BNB Chain payment flow (pre-approval + intent signature)
|
|
670
|
+
*
|
|
671
|
+
* Flow:
|
|
672
|
+
* 1. Check client has approved server wallet (done via `moltspay init`)
|
|
673
|
+
* 2. Sign EIP-712 payment intent (no gas, just signature)
|
|
674
|
+
* 3. Send intent to server
|
|
675
|
+
* 4. Server executes service
|
|
676
|
+
* 5. Server calls transferFrom if successful (pay-for-success)
|
|
677
|
+
*/
|
|
678
|
+
async handleBNBPayment(serverUrl, service, params, paymentDetails) {
|
|
679
|
+
const { to, amount, token, chainName, chain, spender } = paymentDetails;
|
|
680
|
+
const tokenConfig = chain.tokens[token];
|
|
681
|
+
const provider = new ethers.JsonRpcProvider(chain.rpc);
|
|
682
|
+
const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
|
|
683
|
+
const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
|
|
684
|
+
if (allowance < amountWeiCheck) {
|
|
685
|
+
const nativeBalance = await provider.getBalance(this.wallet.address);
|
|
686
|
+
const minGasBalance = ethers.parseEther("0.0005");
|
|
687
|
+
if (nativeBalance < minGasBalance) {
|
|
688
|
+
const nativeBNB = parseFloat(ethers.formatEther(nativeBalance)).toFixed(4);
|
|
689
|
+
const isTestnet = chainName === "bnb_testnet";
|
|
690
|
+
if (isTestnet) {
|
|
691
|
+
throw new Error(
|
|
692
|
+
`\u274C Insufficient tBNB for approval transaction
|
|
693
|
+
|
|
694
|
+
Current tBNB: ${nativeBNB}
|
|
695
|
+
Required: ~0.001 tBNB
|
|
696
|
+
|
|
697
|
+
Get testnet tokens: npx moltspay faucet --chain bnb_testnet
|
|
698
|
+
(Gives USDC + tBNB for gas)`
|
|
699
|
+
);
|
|
700
|
+
} else {
|
|
701
|
+
throw new Error(
|
|
702
|
+
`\u274C Insufficient BNB for approval transaction
|
|
703
|
+
|
|
704
|
+
Current BNB: ${nativeBNB}
|
|
705
|
+
Required: ~0.001 BNB (~$0.60)
|
|
706
|
+
|
|
707
|
+
To get BNB:
|
|
708
|
+
\u2022 Withdraw from Binance/exchange to your wallet
|
|
709
|
+
\u2022 Most exchanges include BNB dust with withdrawals
|
|
710
|
+
|
|
711
|
+
After funding, run:
|
|
712
|
+
npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
throw new Error(
|
|
717
|
+
`Insufficient allowance for ${spender.slice(0, 10)}...
|
|
718
|
+
Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
|
|
722
|
+
const intent = {
|
|
723
|
+
from: this.wallet.address,
|
|
724
|
+
to,
|
|
725
|
+
amount: amountWei,
|
|
726
|
+
token: tokenConfig.address,
|
|
727
|
+
service,
|
|
728
|
+
nonce: Date.now(),
|
|
729
|
+
// Use timestamp as nonce for simplicity
|
|
730
|
+
deadline: Date.now() + 36e5
|
|
731
|
+
// 1 hour
|
|
732
|
+
};
|
|
733
|
+
const domain = {
|
|
734
|
+
name: "MoltsPay",
|
|
735
|
+
version: "1",
|
|
736
|
+
chainId: chain.chainId
|
|
737
|
+
};
|
|
738
|
+
const types = {
|
|
739
|
+
PaymentIntent: [
|
|
740
|
+
{ name: "from", type: "address" },
|
|
741
|
+
{ name: "to", type: "address" },
|
|
742
|
+
{ name: "amount", type: "uint256" },
|
|
743
|
+
{ name: "token", type: "address" },
|
|
744
|
+
{ name: "service", type: "string" },
|
|
745
|
+
{ name: "nonce", type: "uint256" },
|
|
746
|
+
{ name: "deadline", type: "uint256" }
|
|
747
|
+
]
|
|
748
|
+
};
|
|
749
|
+
console.log(`[MoltsPay] Signing BNB payment intent...`);
|
|
750
|
+
const signature = await this.wallet.signTypedData(domain, types, intent);
|
|
751
|
+
const network = `eip155:${chain.chainId}`;
|
|
752
|
+
const payload = {
|
|
753
|
+
x402Version: 2,
|
|
754
|
+
scheme: "exact",
|
|
755
|
+
network,
|
|
756
|
+
payload: {
|
|
757
|
+
intent: {
|
|
758
|
+
...intent,
|
|
759
|
+
signature
|
|
760
|
+
},
|
|
761
|
+
chainId: chain.chainId
|
|
762
|
+
},
|
|
763
|
+
accepted: {
|
|
764
|
+
scheme: "exact",
|
|
765
|
+
network,
|
|
766
|
+
asset: tokenConfig.address,
|
|
767
|
+
amount: amountWei,
|
|
768
|
+
payTo: to,
|
|
769
|
+
maxTimeoutSeconds: 300
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
773
|
+
console.log(`[MoltsPay] Sending BNB payment request...`);
|
|
774
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
775
|
+
method: "POST",
|
|
776
|
+
headers: {
|
|
777
|
+
"Content-Type": "application/json",
|
|
778
|
+
"X-Payment": paymentHeader
|
|
779
|
+
},
|
|
780
|
+
body: JSON.stringify({ service, params, chain: chainName })
|
|
781
|
+
});
|
|
782
|
+
const result = await paidRes.json();
|
|
783
|
+
if (!paidRes.ok) {
|
|
784
|
+
throw new Error(result.error || "BNB payment failed");
|
|
785
|
+
}
|
|
786
|
+
this.recordSpending(amount);
|
|
787
|
+
console.log(`[MoltsPay] Success! BNB payment settled.`);
|
|
788
|
+
return result.result || result;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Handle Solana payment flow
|
|
792
|
+
*
|
|
793
|
+
* Solana uses SPL token transfers with pay-for-success model:
|
|
794
|
+
* 1. Client creates and signs a transfer transaction
|
|
795
|
+
* 2. Server submits the transaction after service completes
|
|
796
|
+
*/
|
|
797
|
+
async handleSolanaPayment(serverUrl, service, params, requirements, chain) {
|
|
798
|
+
const solanaWallet = loadSolanaWallet(this.configDir);
|
|
799
|
+
if (!solanaWallet) {
|
|
800
|
+
throw new Error("No Solana wallet found. Run: npx moltspay init --chain solana_devnet");
|
|
801
|
+
}
|
|
802
|
+
const amount = Number(requirements.amount);
|
|
803
|
+
const amountUSDC = amount / 1e6;
|
|
804
|
+
this.checkLimits(amountUSDC);
|
|
805
|
+
console.log(`[MoltsPay] Creating Solana payment: $${amountUSDC} USDC`);
|
|
806
|
+
if (!requirements.payTo) {
|
|
807
|
+
throw new Error("Missing payTo address in payment requirements");
|
|
808
|
+
}
|
|
809
|
+
const solanaFeePayer = requirements.extra?.solanaFeePayer;
|
|
810
|
+
const feePayerPubkey = solanaFeePayer ? new PublicKey4(solanaFeePayer) : void 0;
|
|
811
|
+
if (feePayerPubkey) {
|
|
812
|
+
console.log(`[MoltsPay] Gasless mode: server pays fees`);
|
|
813
|
+
}
|
|
814
|
+
const recipientPubkey = new PublicKey4(requirements.payTo);
|
|
815
|
+
const transaction = await createSolanaPaymentTransaction(
|
|
816
|
+
solanaWallet.publicKey,
|
|
817
|
+
recipientPubkey,
|
|
818
|
+
BigInt(amount),
|
|
819
|
+
chain,
|
|
820
|
+
feePayerPubkey
|
|
821
|
+
// Optional fee payer for gasless mode
|
|
822
|
+
);
|
|
823
|
+
if (feePayerPubkey) {
|
|
824
|
+
transaction.partialSign(solanaWallet);
|
|
825
|
+
} else {
|
|
826
|
+
transaction.sign(solanaWallet);
|
|
827
|
+
}
|
|
828
|
+
const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
|
|
829
|
+
console.log(`[MoltsPay] Transaction signed, sending to server...`);
|
|
830
|
+
const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
831
|
+
const payload = {
|
|
832
|
+
x402Version: 2,
|
|
833
|
+
scheme: "exact",
|
|
834
|
+
network,
|
|
835
|
+
payload: {
|
|
836
|
+
signedTransaction: signedTx,
|
|
837
|
+
sender: solanaWallet.publicKey.toBase58(),
|
|
838
|
+
chain
|
|
839
|
+
},
|
|
840
|
+
accepted: {
|
|
841
|
+
scheme: "exact",
|
|
842
|
+
network,
|
|
843
|
+
asset: requirements.asset,
|
|
844
|
+
amount: requirements.amount,
|
|
845
|
+
payTo: requirements.payTo,
|
|
846
|
+
maxTimeoutSeconds: 300
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
const paymentHeader = Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
850
|
+
const paidRes = await fetch(`${serverUrl}/execute`, {
|
|
851
|
+
method: "POST",
|
|
852
|
+
headers: {
|
|
853
|
+
"Content-Type": "application/json",
|
|
854
|
+
"X-Payment": paymentHeader
|
|
855
|
+
},
|
|
856
|
+
body: JSON.stringify({ service, params, chain })
|
|
857
|
+
});
|
|
858
|
+
const result = await paidRes.json();
|
|
859
|
+
if (!paidRes.ok) {
|
|
860
|
+
throw new Error(result.error || "Solana payment failed");
|
|
861
|
+
}
|
|
862
|
+
this.recordSpending(amountUSDC);
|
|
863
|
+
console.log(`[MoltsPay] Success! Solana payment settled.`);
|
|
864
|
+
if (result.payment?.transaction) {
|
|
865
|
+
const explorerUrl = chain === "solana" ? `https://solscan.io/tx/${result.payment.transaction}` : `https://solscan.io/tx/${result.payment.transaction}?cluster=devnet`;
|
|
866
|
+
console.log(`[MoltsPay] Transaction: ${explorerUrl}`);
|
|
867
|
+
}
|
|
868
|
+
return result.result || result;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Check ERC20 allowance for a spender
|
|
872
|
+
*/
|
|
873
|
+
async checkAllowance(tokenAddress, spender, provider) {
|
|
874
|
+
const contract = new ethers.Contract(
|
|
875
|
+
tokenAddress,
|
|
876
|
+
["function allowance(address owner, address spender) view returns (uint256)"],
|
|
877
|
+
provider
|
|
878
|
+
);
|
|
879
|
+
return await contract.allowance(this.wallet.address, spender);
|
|
880
|
+
}
|
|
363
881
|
/**
|
|
364
882
|
* Sign EIP-3009 transferWithAuthorization (GASLESS)
|
|
365
883
|
* This only signs - no on-chain transaction, no gas needed.
|
|
@@ -430,26 +948,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
430
948
|
}
|
|
431
949
|
// --- Config & Wallet Management ---
|
|
432
950
|
loadConfig() {
|
|
433
|
-
const configPath =
|
|
434
|
-
if (
|
|
435
|
-
const content =
|
|
951
|
+
const configPath = join2(this.configDir, "config.json");
|
|
952
|
+
if (existsSync2(configPath)) {
|
|
953
|
+
const content = readFileSync2(configPath, "utf-8");
|
|
436
954
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
437
955
|
}
|
|
438
956
|
return { ...DEFAULT_CONFIG };
|
|
439
957
|
}
|
|
440
958
|
saveConfig() {
|
|
441
|
-
|
|
442
|
-
const configPath =
|
|
443
|
-
|
|
959
|
+
mkdirSync2(this.configDir, { recursive: true });
|
|
960
|
+
const configPath = join2(this.configDir, "config.json");
|
|
961
|
+
writeFileSync2(configPath, JSON.stringify(this.config, null, 2));
|
|
444
962
|
}
|
|
445
963
|
/**
|
|
446
964
|
* Load spending data from disk
|
|
447
965
|
*/
|
|
448
966
|
loadSpending() {
|
|
449
|
-
const spendingPath =
|
|
450
|
-
if (
|
|
967
|
+
const spendingPath = join2(this.configDir, "spending.json");
|
|
968
|
+
if (existsSync2(spendingPath)) {
|
|
451
969
|
try {
|
|
452
|
-
const data = JSON.parse(
|
|
970
|
+
const data = JSON.parse(readFileSync2(spendingPath, "utf-8"));
|
|
453
971
|
const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
|
|
454
972
|
if (data.date && data.date === today) {
|
|
455
973
|
this.todaySpending = data.amount || 0;
|
|
@@ -468,18 +986,18 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
468
986
|
* Save spending data to disk
|
|
469
987
|
*/
|
|
470
988
|
saveSpending() {
|
|
471
|
-
|
|
472
|
-
const spendingPath =
|
|
989
|
+
mkdirSync2(this.configDir, { recursive: true });
|
|
990
|
+
const spendingPath = join2(this.configDir, "spending.json");
|
|
473
991
|
const data = {
|
|
474
992
|
date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
|
|
475
993
|
amount: this.todaySpending,
|
|
476
994
|
updatedAt: Date.now()
|
|
477
995
|
};
|
|
478
|
-
|
|
996
|
+
writeFileSync2(spendingPath, JSON.stringify(data, null, 2));
|
|
479
997
|
}
|
|
480
998
|
loadWallet() {
|
|
481
|
-
const walletPath =
|
|
482
|
-
if (
|
|
999
|
+
const walletPath = join2(this.configDir, "wallet.json");
|
|
1000
|
+
if (existsSync2(walletPath)) {
|
|
483
1001
|
try {
|
|
484
1002
|
const stats = statSync(walletPath);
|
|
485
1003
|
const mode = stats.mode & 511;
|
|
@@ -490,7 +1008,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
490
1008
|
}
|
|
491
1009
|
} catch (err) {
|
|
492
1010
|
}
|
|
493
|
-
const content =
|
|
1011
|
+
const content = readFileSync2(walletPath, "utf-8");
|
|
494
1012
|
return JSON.parse(content);
|
|
495
1013
|
}
|
|
496
1014
|
return null;
|
|
@@ -499,15 +1017,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
499
1017
|
* Initialize a new wallet (called by CLI)
|
|
500
1018
|
*/
|
|
501
1019
|
static init(configDir, options) {
|
|
502
|
-
|
|
1020
|
+
mkdirSync2(configDir, { recursive: true });
|
|
503
1021
|
const wallet = Wallet.createRandom();
|
|
504
1022
|
const walletData = {
|
|
505
1023
|
address: wallet.address,
|
|
506
1024
|
privateKey: wallet.privateKey,
|
|
507
1025
|
createdAt: Date.now()
|
|
508
1026
|
};
|
|
509
|
-
const walletPath =
|
|
510
|
-
|
|
1027
|
+
const walletPath = join2(configDir, "wallet.json");
|
|
1028
|
+
writeFileSync2(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
511
1029
|
const config = {
|
|
512
1030
|
chain: options.chain,
|
|
513
1031
|
limits: {
|
|
@@ -515,8 +1033,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
515
1033
|
maxPerDay: options.maxPerDay
|
|
516
1034
|
}
|
|
517
1035
|
};
|
|
518
|
-
const configPath =
|
|
519
|
-
|
|
1036
|
+
const configPath = join2(configDir, "config.json");
|
|
1037
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2));
|
|
520
1038
|
return { address: wallet.address, configDir };
|
|
521
1039
|
}
|
|
522
1040
|
/**
|
|
@@ -552,7 +1070,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
552
1070
|
if (!this.wallet) {
|
|
553
1071
|
throw new Error("Client not initialized");
|
|
554
1072
|
}
|
|
555
|
-
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato"];
|
|
1073
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
|
|
556
1074
|
const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
557
1075
|
const results = {};
|
|
558
1076
|
const tempoTokens = {
|