moltspay 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +292 -34
- package/dist/cdp/index.d.mts +4 -4
- package/dist/cdp/index.d.ts +4 -4
- package/dist/cdp/index.js +110 -30368
- package/dist/cdp/index.js.map +1 -1
- package/dist/cdp/index.mjs +94 -30360
- package/dist/cdp/index.mjs.map +1 -1
- package/dist/cdp-DeohBe1o.d.ts +66 -0
- package/dist/cdp-p_eHuQpb.d.mts +66 -0
- package/dist/chains/index.d.mts +9 -8
- package/dist/chains/index.d.ts +9 -8
- package/dist/chains/index.js +86 -0
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/index.mjs +86 -0
- package/dist/chains/index.mjs.map +1 -1
- package/dist/cli/index.js +2746 -290
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +2752 -282
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +60 -4
- package/dist/client/index.d.ts +60 -4
- package/dist/client/index.js +734 -43
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +732 -41
- package/dist/client/index.mjs.map +1 -1
- package/dist/facilitators/index.d.mts +220 -39
- package/dist/facilitators/index.d.ts +220 -39
- package/dist/facilitators/index.js +897 -1
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +902 -1
- package/dist/facilitators/index.mjs.map +1 -1
- package/dist/{index-DgJPZMBG.d.mts → index-D_2FkLwV.d.mts} +6 -2
- package/dist/{index-DgJPZMBG.d.ts → index-D_2FkLwV.d.ts} +6 -2
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2238 -30837
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2167 -30766
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.d.mts +30 -3
- package/dist/server/index.d.ts +30 -3
- package/dist/server/index.js +1345 -54
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +1355 -54
- package/dist/server/index.mjs.map +1 -1
- package/dist/verify/index.d.mts +1 -1
- package/dist/verify/index.d.ts +1 -1
- package/dist/verify/index.js +86 -0
- package/dist/verify/index.js.map +1 -1
- package/dist/verify/index.mjs +86 -0
- package/dist/verify/index.mjs.map +1 -1
- package/dist/wallet/index.d.mts +3 -3
- package/dist/wallet/index.d.ts +3 -3
- package/dist/wallet/index.js +86 -0
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs +86 -0
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +8 -2
- package/schemas/moltspay.services.schema.json +27 -132
package/dist/client/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/client/index.ts
|
|
@@ -23,9 +33,9 @@ __export(client_exports, {
|
|
|
23
33
|
MoltsPayClient: () => MoltsPayClient
|
|
24
34
|
});
|
|
25
35
|
module.exports = __toCommonJS(client_exports);
|
|
26
|
-
var
|
|
27
|
-
var
|
|
28
|
-
var
|
|
36
|
+
var import_fs2 = require("fs");
|
|
37
|
+
var import_os2 = require("os");
|
|
38
|
+
var import_path2 = require("path");
|
|
29
39
|
var import_ethers = require("ethers");
|
|
30
40
|
|
|
31
41
|
// src/chains/index.ts
|
|
@@ -106,6 +116,92 @@ var CHAINS = {
|
|
|
106
116
|
explorer: "https://sepolia.basescan.org/address/",
|
|
107
117
|
explorerTx: "https://sepolia.basescan.org/tx/",
|
|
108
118
|
avgBlockTime: 2
|
|
119
|
+
},
|
|
120
|
+
// ============ Tempo Testnet (Moderato) ============
|
|
121
|
+
tempo_moderato: {
|
|
122
|
+
name: "Tempo Moderato",
|
|
123
|
+
chainId: 42431,
|
|
124
|
+
rpc: "https://rpc.moderato.tempo.xyz",
|
|
125
|
+
tokens: {
|
|
126
|
+
// TIP-20 stablecoins on Tempo testnet (from mppx SDK)
|
|
127
|
+
// Note: Tempo uses USD as native gas token, not ETH
|
|
128
|
+
USDC: {
|
|
129
|
+
address: "0x20c0000000000000000000000000000000000000",
|
|
130
|
+
// pathUSD - primary testnet stablecoin
|
|
131
|
+
decimals: 6,
|
|
132
|
+
symbol: "USDC",
|
|
133
|
+
eip712Name: "pathUSD"
|
|
134
|
+
},
|
|
135
|
+
USDT: {
|
|
136
|
+
address: "0x20c0000000000000000000000000000000000001",
|
|
137
|
+
// alphaUSD
|
|
138
|
+
decimals: 6,
|
|
139
|
+
symbol: "USDT",
|
|
140
|
+
eip712Name: "alphaUSD"
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
usdc: "0x20c0000000000000000000000000000000000000",
|
|
144
|
+
explorer: "https://explore.testnet.tempo.xyz/address/",
|
|
145
|
+
explorerTx: "https://explore.testnet.tempo.xyz/tx/",
|
|
146
|
+
avgBlockTime: 0.5
|
|
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
|
|
109
205
|
}
|
|
110
206
|
};
|
|
111
207
|
function getChain(name) {
|
|
@@ -116,7 +212,119 @@ function getChain(name) {
|
|
|
116
212
|
return config;
|
|
117
213
|
}
|
|
118
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
|
+
|
|
119
326
|
// src/client/index.ts
|
|
327
|
+
var import_web34 = require("@solana/web3.js");
|
|
120
328
|
var X402_VERSION = 2;
|
|
121
329
|
var PAYMENT_REQUIRED_HEADER = "x-payment-required";
|
|
122
330
|
var PAYMENT_HEADER = "x-payment";
|
|
@@ -135,7 +343,7 @@ var MoltsPayClient = class {
|
|
|
135
343
|
todaySpending = 0;
|
|
136
344
|
lastSpendingReset = 0;
|
|
137
345
|
constructor(options = {}) {
|
|
138
|
-
this.configDir = options.configDir || (0,
|
|
346
|
+
this.configDir = options.configDir || (0, import_path2.join)((0, import_os2.homedir)(), ".moltspay");
|
|
139
347
|
this.config = this.loadConfig();
|
|
140
348
|
this.walletData = this.loadWallet();
|
|
141
349
|
this.loadSpending();
|
|
@@ -155,6 +363,12 @@ var MoltsPayClient = class {
|
|
|
155
363
|
get address() {
|
|
156
364
|
return this.wallet?.address || null;
|
|
157
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* Get wallet instance (for direct operations like approvals)
|
|
368
|
+
*/
|
|
369
|
+
getWallet() {
|
|
370
|
+
return this.wallet;
|
|
371
|
+
}
|
|
158
372
|
/**
|
|
159
373
|
* Get current config
|
|
160
374
|
*/
|
|
@@ -224,9 +438,14 @@ var MoltsPayClient = class {
|
|
|
224
438
|
}
|
|
225
439
|
throw new Error(data.error || "Unexpected response");
|
|
226
440
|
}
|
|
441
|
+
const wwwAuthHeader = initialRes.headers.get("www-authenticate");
|
|
227
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
|
+
}
|
|
228
447
|
if (!paymentRequiredHeader) {
|
|
229
|
-
throw new Error("Missing x-payment-required
|
|
448
|
+
throw new Error("Missing payment header (x-payment-required or www-authenticate)");
|
|
230
449
|
}
|
|
231
450
|
let requirements;
|
|
232
451
|
try {
|
|
@@ -243,17 +462,22 @@ var MoltsPayClient = class {
|
|
|
243
462
|
throw new Error("Invalid x-payment-required header");
|
|
244
463
|
}
|
|
245
464
|
const networkToChainName = (network2) => {
|
|
465
|
+
if (network2 === "solana:mainnet") return "solana";
|
|
466
|
+
if (network2 === "solana:devnet") return "solana_devnet";
|
|
246
467
|
const match = network2.match(/^eip155:(\d+)$/);
|
|
247
468
|
if (!match) return null;
|
|
248
469
|
const chainId = parseInt(match[1]);
|
|
249
470
|
if (chainId === 8453) return "base";
|
|
250
471
|
if (chainId === 137) return "polygon";
|
|
251
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";
|
|
252
476
|
return null;
|
|
253
477
|
};
|
|
254
478
|
const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
|
|
255
|
-
let chainName;
|
|
256
479
|
const userSpecifiedChain = options.chain;
|
|
480
|
+
let selectedChain;
|
|
257
481
|
if (userSpecifiedChain) {
|
|
258
482
|
if (!serverChains.includes(userSpecifiedChain)) {
|
|
259
483
|
throw new Error(
|
|
@@ -261,17 +485,27 @@ var MoltsPayClient = class {
|
|
|
261
485
|
Server accepts: ${serverChains.join(", ")}`
|
|
262
486
|
);
|
|
263
487
|
}
|
|
264
|
-
|
|
488
|
+
selectedChain = userSpecifiedChain;
|
|
265
489
|
} else {
|
|
266
490
|
if (serverChains.length === 1 && serverChains[0] === "base") {
|
|
267
|
-
|
|
491
|
+
selectedChain = "base";
|
|
268
492
|
} else {
|
|
269
493
|
throw new Error(
|
|
270
494
|
`Server accepts: ${serverChains.join(", ")}
|
|
271
|
-
Please specify: --chain
|
|
495
|
+
Please specify: --chain <chain_name>`
|
|
272
496
|
);
|
|
273
497
|
}
|
|
274
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;
|
|
275
509
|
const chain = getChain(chainName);
|
|
276
510
|
const network = `eip155:${chain.chainId}`;
|
|
277
511
|
const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
|
|
@@ -306,6 +540,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
306
540
|
} else {
|
|
307
541
|
console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
|
|
308
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
|
+
}
|
|
309
562
|
const payTo = req.payTo || req.resource;
|
|
310
563
|
if (!payTo) {
|
|
311
564
|
throw new Error("Missing payTo address in payment requirements");
|
|
@@ -355,6 +608,300 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
355
608
|
console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
|
|
356
609
|
return result.result;
|
|
357
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
|
+
}
|
|
358
905
|
/**
|
|
359
906
|
* Sign EIP-3009 transferWithAuthorization (GASLESS)
|
|
360
907
|
* This only signs - no on-chain transaction, no gas needed.
|
|
@@ -425,26 +972,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
425
972
|
}
|
|
426
973
|
// --- Config & Wallet Management ---
|
|
427
974
|
loadConfig() {
|
|
428
|
-
const configPath = (0,
|
|
429
|
-
if ((0,
|
|
430
|
-
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");
|
|
431
978
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
432
979
|
}
|
|
433
980
|
return { ...DEFAULT_CONFIG };
|
|
434
981
|
}
|
|
435
982
|
saveConfig() {
|
|
436
|
-
(0,
|
|
437
|
-
const configPath = (0,
|
|
438
|
-
(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));
|
|
439
986
|
}
|
|
440
987
|
/**
|
|
441
988
|
* Load spending data from disk
|
|
442
989
|
*/
|
|
443
990
|
loadSpending() {
|
|
444
|
-
const spendingPath = (0,
|
|
445
|
-
if ((0,
|
|
991
|
+
const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
|
|
992
|
+
if ((0, import_fs2.existsSync)(spendingPath)) {
|
|
446
993
|
try {
|
|
447
|
-
const data = JSON.parse((0,
|
|
994
|
+
const data = JSON.parse((0, import_fs2.readFileSync)(spendingPath, "utf-8"));
|
|
448
995
|
const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
|
|
449
996
|
if (data.date && data.date === today) {
|
|
450
997
|
this.todaySpending = data.amount || 0;
|
|
@@ -463,29 +1010,29 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
463
1010
|
* Save spending data to disk
|
|
464
1011
|
*/
|
|
465
1012
|
saveSpending() {
|
|
466
|
-
(0,
|
|
467
|
-
const spendingPath = (0,
|
|
1013
|
+
(0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
|
|
1014
|
+
const spendingPath = (0, import_path2.join)(this.configDir, "spending.json");
|
|
468
1015
|
const data = {
|
|
469
1016
|
date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
|
|
470
1017
|
amount: this.todaySpending,
|
|
471
1018
|
updatedAt: Date.now()
|
|
472
1019
|
};
|
|
473
|
-
(0,
|
|
1020
|
+
(0, import_fs2.writeFileSync)(spendingPath, JSON.stringify(data, null, 2));
|
|
474
1021
|
}
|
|
475
1022
|
loadWallet() {
|
|
476
|
-
const walletPath = (0,
|
|
477
|
-
if ((0,
|
|
1023
|
+
const walletPath = (0, import_path2.join)(this.configDir, "wallet.json");
|
|
1024
|
+
if ((0, import_fs2.existsSync)(walletPath)) {
|
|
478
1025
|
try {
|
|
479
|
-
const stats = (0,
|
|
1026
|
+
const stats = (0, import_fs2.statSync)(walletPath);
|
|
480
1027
|
const mode = stats.mode & 511;
|
|
481
1028
|
if (mode !== 384) {
|
|
482
1029
|
console.warn(`[MoltsPay] WARNING: wallet.json has insecure permissions (${mode.toString(8)})`);
|
|
483
1030
|
console.warn(`[MoltsPay] Fixing permissions to 0600...`);
|
|
484
|
-
(0,
|
|
1031
|
+
(0, import_fs2.chmodSync)(walletPath, 384);
|
|
485
1032
|
}
|
|
486
1033
|
} catch (err) {
|
|
487
1034
|
}
|
|
488
|
-
const content = (0,
|
|
1035
|
+
const content = (0, import_fs2.readFileSync)(walletPath, "utf-8");
|
|
489
1036
|
return JSON.parse(content);
|
|
490
1037
|
}
|
|
491
1038
|
return null;
|
|
@@ -494,15 +1041,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
494
1041
|
* Initialize a new wallet (called by CLI)
|
|
495
1042
|
*/
|
|
496
1043
|
static init(configDir, options) {
|
|
497
|
-
(0,
|
|
1044
|
+
(0, import_fs2.mkdirSync)(configDir, { recursive: true });
|
|
498
1045
|
const wallet = import_ethers.Wallet.createRandom();
|
|
499
1046
|
const walletData = {
|
|
500
1047
|
address: wallet.address,
|
|
501
1048
|
privateKey: wallet.privateKey,
|
|
502
1049
|
createdAt: Date.now()
|
|
503
1050
|
};
|
|
504
|
-
const walletPath = (0,
|
|
505
|
-
(0,
|
|
1051
|
+
const walletPath = (0, import_path2.join)(configDir, "wallet.json");
|
|
1052
|
+
(0, import_fs2.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
506
1053
|
const config = {
|
|
507
1054
|
chain: options.chain,
|
|
508
1055
|
limits: {
|
|
@@ -510,8 +1057,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
510
1057
|
maxPerDay: options.maxPerDay
|
|
511
1058
|
}
|
|
512
1059
|
};
|
|
513
|
-
const configPath = (0,
|
|
514
|
-
(0,
|
|
1060
|
+
const configPath = (0, import_path2.join)(configDir, "config.json");
|
|
1061
|
+
(0, import_fs2.writeFileSync)(configPath, JSON.stringify(config, null, 2));
|
|
515
1062
|
return { address: wallet.address, configDir };
|
|
516
1063
|
}
|
|
517
1064
|
/**
|
|
@@ -541,30 +1088,59 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
541
1088
|
};
|
|
542
1089
|
}
|
|
543
1090
|
/**
|
|
544
|
-
* Get wallet balances on all supported chains (Base + Polygon)
|
|
1091
|
+
* Get wallet balances on all supported chains (Base + Polygon + Tempo)
|
|
545
1092
|
*/
|
|
546
1093
|
async getAllBalances() {
|
|
547
1094
|
if (!this.wallet) {
|
|
548
1095
|
throw new Error("Client not initialized");
|
|
549
1096
|
}
|
|
550
|
-
const supportedChains = ["base", "polygon", "base_sepolia"];
|
|
1097
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
|
|
551
1098
|
const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
552
1099
|
const results = {};
|
|
1100
|
+
const tempoTokens = {
|
|
1101
|
+
pathUSD: "0x20c0000000000000000000000000000000000000",
|
|
1102
|
+
alphaUSD: "0x20c0000000000000000000000000000000000001",
|
|
1103
|
+
betaUSD: "0x20c0000000000000000000000000000000000002",
|
|
1104
|
+
thetaUSD: "0x20c0000000000000000000000000000000000003"
|
|
1105
|
+
};
|
|
553
1106
|
await Promise.all(
|
|
554
1107
|
supportedChains.map(async (chainName) => {
|
|
555
1108
|
try {
|
|
556
1109
|
const chain = getChain(chainName);
|
|
557
1110
|
const provider = new import_ethers.ethers.JsonRpcProvider(chain.rpc);
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
1111
|
+
if (chainName === "tempo_moderato") {
|
|
1112
|
+
const [nativeBalance, pathUSD, alphaUSD, betaUSD, thetaUSD] = await Promise.all([
|
|
1113
|
+
provider.getBalance(this.wallet.address),
|
|
1114
|
+
new import_ethers.ethers.Contract(tempoTokens.pathUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
1115
|
+
new import_ethers.ethers.Contract(tempoTokens.alphaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
1116
|
+
new import_ethers.ethers.Contract(tempoTokens.betaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
1117
|
+
new import_ethers.ethers.Contract(tempoTokens.thetaUSD, tokenAbi, provider).balanceOf(this.wallet.address)
|
|
1118
|
+
]);
|
|
1119
|
+
results[chainName] = {
|
|
1120
|
+
usdc: parseFloat(import_ethers.ethers.formatUnits(pathUSD, 6)),
|
|
1121
|
+
// pathUSD as default USDC
|
|
1122
|
+
usdt: parseFloat(import_ethers.ethers.formatUnits(alphaUSD, 6)),
|
|
1123
|
+
// alphaUSD as default USDT
|
|
1124
|
+
native: parseFloat(import_ethers.ethers.formatEther(nativeBalance)),
|
|
1125
|
+
tempo: {
|
|
1126
|
+
pathUSD: parseFloat(import_ethers.ethers.formatUnits(pathUSD, 6)),
|
|
1127
|
+
alphaUSD: parseFloat(import_ethers.ethers.formatUnits(alphaUSD, 6)),
|
|
1128
|
+
betaUSD: parseFloat(import_ethers.ethers.formatUnits(betaUSD, 6)),
|
|
1129
|
+
thetaUSD: parseFloat(import_ethers.ethers.formatUnits(thetaUSD, 6))
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
} else {
|
|
1133
|
+
const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
|
|
1134
|
+
provider.getBalance(this.wallet.address),
|
|
1135
|
+
new import_ethers.ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
1136
|
+
new import_ethers.ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
|
|
1137
|
+
]);
|
|
1138
|
+
results[chainName] = {
|
|
1139
|
+
usdc: parseFloat(import_ethers.ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
|
|
1140
|
+
usdt: parseFloat(import_ethers.ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
|
|
1141
|
+
native: parseFloat(import_ethers.ethers.formatEther(nativeBalance))
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
568
1144
|
} catch (err) {
|
|
569
1145
|
results[chainName] = { usdc: 0, usdt: 0, native: 0 };
|
|
570
1146
|
}
|
|
@@ -572,6 +1148,121 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
572
1148
|
);
|
|
573
1149
|
return results;
|
|
574
1150
|
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Pay for a service using MPP (Machine Payments Protocol)
|
|
1153
|
+
*
|
|
1154
|
+
* This implements the MPP flow manually for EOA wallets:
|
|
1155
|
+
* 1. Request service → get 402 with WWW-Authenticate
|
|
1156
|
+
* 2. Parse payment requirements
|
|
1157
|
+
* 3. Execute transfer on Tempo chain
|
|
1158
|
+
* 4. Retry with transaction hash as credential
|
|
1159
|
+
*
|
|
1160
|
+
* @param url - Full URL of the MPP-enabled endpoint
|
|
1161
|
+
* @param options - Request options (body, headers)
|
|
1162
|
+
* @returns Response from the service
|
|
1163
|
+
*/
|
|
1164
|
+
async payWithMPP(url, options = {}) {
|
|
1165
|
+
if (!this.wallet || !this.walletData) {
|
|
1166
|
+
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
1167
|
+
}
|
|
1168
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
1169
|
+
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
1170
|
+
const { tempoModerato } = await import("viem/chains");
|
|
1171
|
+
const { Actions } = await import("viem/tempo");
|
|
1172
|
+
const privateKey = this.walletData.privateKey;
|
|
1173
|
+
const account = privateKeyToAccount(privateKey);
|
|
1174
|
+
console.log(`[MoltsPay] Making MPP request to: ${url}`);
|
|
1175
|
+
console.log(`[MoltsPay] Using account: ${account.address}`);
|
|
1176
|
+
const initResponse = await fetch(url, {
|
|
1177
|
+
method: "POST",
|
|
1178
|
+
headers: {
|
|
1179
|
+
"Content-Type": "application/json",
|
|
1180
|
+
...options.headers
|
|
1181
|
+
},
|
|
1182
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
1183
|
+
});
|
|
1184
|
+
if (initResponse.status !== 402) {
|
|
1185
|
+
if (initResponse.ok) {
|
|
1186
|
+
return initResponse.json();
|
|
1187
|
+
}
|
|
1188
|
+
const errorText = await initResponse.text();
|
|
1189
|
+
throw new Error(`Request failed (${initResponse.status}): ${errorText}`);
|
|
1190
|
+
}
|
|
1191
|
+
const wwwAuth = initResponse.headers.get("www-authenticate");
|
|
1192
|
+
if (!wwwAuth || !wwwAuth.toLowerCase().includes("payment")) {
|
|
1193
|
+
throw new Error("No WWW-Authenticate Payment challenge in 402 response");
|
|
1194
|
+
}
|
|
1195
|
+
console.log(`[MoltsPay] Got 402, parsing payment challenge...`);
|
|
1196
|
+
const parseAuthParam = (header, key) => {
|
|
1197
|
+
const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
|
|
1198
|
+
return match ? match[1] : null;
|
|
1199
|
+
};
|
|
1200
|
+
const challengeId = parseAuthParam(wwwAuth, "id");
|
|
1201
|
+
const method = parseAuthParam(wwwAuth, "method");
|
|
1202
|
+
const realm = parseAuthParam(wwwAuth, "realm");
|
|
1203
|
+
const requestB64 = parseAuthParam(wwwAuth, "request");
|
|
1204
|
+
if (method !== "tempo") {
|
|
1205
|
+
throw new Error(`Unsupported payment method: ${method}`);
|
|
1206
|
+
}
|
|
1207
|
+
if (!requestB64) {
|
|
1208
|
+
throw new Error("Missing request in WWW-Authenticate");
|
|
1209
|
+
}
|
|
1210
|
+
const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
|
|
1211
|
+
const paymentRequest = JSON.parse(requestJson);
|
|
1212
|
+
console.log(`[MoltsPay] Payment request:`, paymentRequest);
|
|
1213
|
+
const { amount, currency, recipient, methodDetails } = paymentRequest;
|
|
1214
|
+
const chainId = methodDetails?.chainId || 42431;
|
|
1215
|
+
console.log(`[MoltsPay] Executing transfer on Tempo (chainId: ${chainId})...`);
|
|
1216
|
+
console.log(`[MoltsPay] Amount: ${amount}, To: ${recipient}`);
|
|
1217
|
+
const tempoChain = { ...tempoModerato, feeToken: currency };
|
|
1218
|
+
const publicClient = createPublicClient({
|
|
1219
|
+
chain: tempoChain,
|
|
1220
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
1221
|
+
});
|
|
1222
|
+
const walletClient = createWalletClient({
|
|
1223
|
+
account,
|
|
1224
|
+
chain: tempoChain,
|
|
1225
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
1226
|
+
});
|
|
1227
|
+
const txHash = await Actions.token.transfer(walletClient, {
|
|
1228
|
+
to: recipient,
|
|
1229
|
+
amount: BigInt(amount),
|
|
1230
|
+
token: currency
|
|
1231
|
+
});
|
|
1232
|
+
console.log(`[MoltsPay] Transaction sent: ${txHash}`);
|
|
1233
|
+
console.log(`[MoltsPay] Waiting for confirmation...`);
|
|
1234
|
+
await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
1235
|
+
console.log(`[MoltsPay] Transaction confirmed!`);
|
|
1236
|
+
const challenge = {
|
|
1237
|
+
id: challengeId,
|
|
1238
|
+
realm,
|
|
1239
|
+
method: "tempo",
|
|
1240
|
+
intent: "charge",
|
|
1241
|
+
request: paymentRequest
|
|
1242
|
+
};
|
|
1243
|
+
const credential = {
|
|
1244
|
+
challenge,
|
|
1245
|
+
payload: { hash: txHash, type: "hash" },
|
|
1246
|
+
source: `did:pkh:eip155:${chainId}:${account.address}`
|
|
1247
|
+
};
|
|
1248
|
+
const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1249
|
+
console.log(`[MoltsPay] Retrying with payment credential...`);
|
|
1250
|
+
const paidResponse = await fetch(url, {
|
|
1251
|
+
method: "POST",
|
|
1252
|
+
headers: {
|
|
1253
|
+
"Content-Type": "application/json",
|
|
1254
|
+
"Authorization": `Payment ${credentialB64}`,
|
|
1255
|
+
...options.headers
|
|
1256
|
+
},
|
|
1257
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
1258
|
+
});
|
|
1259
|
+
if (!paidResponse.ok) {
|
|
1260
|
+
const errorText = await paidResponse.text();
|
|
1261
|
+
throw new Error(`Payment verification failed (${paidResponse.status}): ${errorText}`);
|
|
1262
|
+
}
|
|
1263
|
+
console.log(`[MoltsPay] Payment verified! Service completed.`);
|
|
1264
|
+
return paidResponse.json();
|
|
1265
|
+
}
|
|
575
1266
|
};
|
|
576
1267
|
// Annotate the CommonJS export names for ESM import in node:
|
|
577
1268
|
0 && (module.exports = {
|