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.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
|
|
@@ -82,6 +82,92 @@ var CHAINS = {
|
|
|
82
82
|
explorer: "https://sepolia.basescan.org/address/",
|
|
83
83
|
explorerTx: "https://sepolia.basescan.org/tx/",
|
|
84
84
|
avgBlockTime: 2
|
|
85
|
+
},
|
|
86
|
+
// ============ Tempo Testnet (Moderato) ============
|
|
87
|
+
tempo_moderato: {
|
|
88
|
+
name: "Tempo Moderato",
|
|
89
|
+
chainId: 42431,
|
|
90
|
+
rpc: "https://rpc.moderato.tempo.xyz",
|
|
91
|
+
tokens: {
|
|
92
|
+
// TIP-20 stablecoins on Tempo testnet (from mppx SDK)
|
|
93
|
+
// Note: Tempo uses USD as native gas token, not ETH
|
|
94
|
+
USDC: {
|
|
95
|
+
address: "0x20c0000000000000000000000000000000000000",
|
|
96
|
+
// pathUSD - primary testnet stablecoin
|
|
97
|
+
decimals: 6,
|
|
98
|
+
symbol: "USDC",
|
|
99
|
+
eip712Name: "pathUSD"
|
|
100
|
+
},
|
|
101
|
+
USDT: {
|
|
102
|
+
address: "0x20c0000000000000000000000000000000000001",
|
|
103
|
+
// alphaUSD
|
|
104
|
+
decimals: 6,
|
|
105
|
+
symbol: "USDT",
|
|
106
|
+
eip712Name: "alphaUSD"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
usdc: "0x20c0000000000000000000000000000000000000",
|
|
110
|
+
explorer: "https://explore.testnet.tempo.xyz/address/",
|
|
111
|
+
explorerTx: "https://explore.testnet.tempo.xyz/tx/",
|
|
112
|
+
avgBlockTime: 0.5
|
|
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
|
|
85
171
|
}
|
|
86
172
|
};
|
|
87
173
|
function getChain(name) {
|
|
@@ -92,7 +178,129 @@ function getChain(name) {
|
|
|
92
178
|
return config;
|
|
93
179
|
}
|
|
94
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
|
+
|
|
95
302
|
// src/client/index.ts
|
|
303
|
+
import { PublicKey as PublicKey4 } from "@solana/web3.js";
|
|
96
304
|
var X402_VERSION = 2;
|
|
97
305
|
var PAYMENT_REQUIRED_HEADER = "x-payment-required";
|
|
98
306
|
var PAYMENT_HEADER = "x-payment";
|
|
@@ -111,7 +319,7 @@ var MoltsPayClient = class {
|
|
|
111
319
|
todaySpending = 0;
|
|
112
320
|
lastSpendingReset = 0;
|
|
113
321
|
constructor(options = {}) {
|
|
114
|
-
this.configDir = options.configDir ||
|
|
322
|
+
this.configDir = options.configDir || join2(homedir2(), ".moltspay");
|
|
115
323
|
this.config = this.loadConfig();
|
|
116
324
|
this.walletData = this.loadWallet();
|
|
117
325
|
this.loadSpending();
|
|
@@ -131,6 +339,12 @@ var MoltsPayClient = class {
|
|
|
131
339
|
get address() {
|
|
132
340
|
return this.wallet?.address || null;
|
|
133
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Get wallet instance (for direct operations like approvals)
|
|
344
|
+
*/
|
|
345
|
+
getWallet() {
|
|
346
|
+
return this.wallet;
|
|
347
|
+
}
|
|
134
348
|
/**
|
|
135
349
|
* Get current config
|
|
136
350
|
*/
|
|
@@ -200,9 +414,14 @@ var MoltsPayClient = class {
|
|
|
200
414
|
}
|
|
201
415
|
throw new Error(data.error || "Unexpected response");
|
|
202
416
|
}
|
|
417
|
+
const wwwAuthHeader = initialRes.headers.get("www-authenticate");
|
|
203
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
|
+
}
|
|
204
423
|
if (!paymentRequiredHeader) {
|
|
205
|
-
throw new Error("Missing x-payment-required
|
|
424
|
+
throw new Error("Missing payment header (x-payment-required or www-authenticate)");
|
|
206
425
|
}
|
|
207
426
|
let requirements;
|
|
208
427
|
try {
|
|
@@ -219,17 +438,22 @@ var MoltsPayClient = class {
|
|
|
219
438
|
throw new Error("Invalid x-payment-required header");
|
|
220
439
|
}
|
|
221
440
|
const networkToChainName = (network2) => {
|
|
441
|
+
if (network2 === "solana:mainnet") return "solana";
|
|
442
|
+
if (network2 === "solana:devnet") return "solana_devnet";
|
|
222
443
|
const match = network2.match(/^eip155:(\d+)$/);
|
|
223
444
|
if (!match) return null;
|
|
224
445
|
const chainId = parseInt(match[1]);
|
|
225
446
|
if (chainId === 8453) return "base";
|
|
226
447
|
if (chainId === 137) return "polygon";
|
|
227
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";
|
|
228
452
|
return null;
|
|
229
453
|
};
|
|
230
454
|
const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
|
|
231
|
-
let chainName;
|
|
232
455
|
const userSpecifiedChain = options.chain;
|
|
456
|
+
let selectedChain;
|
|
233
457
|
if (userSpecifiedChain) {
|
|
234
458
|
if (!serverChains.includes(userSpecifiedChain)) {
|
|
235
459
|
throw new Error(
|
|
@@ -237,17 +461,27 @@ var MoltsPayClient = class {
|
|
|
237
461
|
Server accepts: ${serverChains.join(", ")}`
|
|
238
462
|
);
|
|
239
463
|
}
|
|
240
|
-
|
|
464
|
+
selectedChain = userSpecifiedChain;
|
|
241
465
|
} else {
|
|
242
466
|
if (serverChains.length === 1 && serverChains[0] === "base") {
|
|
243
|
-
|
|
467
|
+
selectedChain = "base";
|
|
244
468
|
} else {
|
|
245
469
|
throw new Error(
|
|
246
470
|
`Server accepts: ${serverChains.join(", ")}
|
|
247
|
-
Please specify: --chain
|
|
471
|
+
Please specify: --chain <chain_name>`
|
|
248
472
|
);
|
|
249
473
|
}
|
|
250
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;
|
|
251
485
|
const chain = getChain(chainName);
|
|
252
486
|
const network = `eip155:${chain.chainId}`;
|
|
253
487
|
const req = requirements.find((r) => r.scheme === "exact" && r.network === network);
|
|
@@ -282,6 +516,25 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
282
516
|
} else {
|
|
283
517
|
console.log(`[MoltsPay] Signing payment: $${amount} ${token} (gasless)`);
|
|
284
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
|
+
}
|
|
285
538
|
const payTo = req.payTo || req.resource;
|
|
286
539
|
if (!payTo) {
|
|
287
540
|
throw new Error("Missing payTo address in payment requirements");
|
|
@@ -331,6 +584,300 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
331
584
|
console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
|
|
332
585
|
return result.result;
|
|
333
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
|
+
}
|
|
334
881
|
/**
|
|
335
882
|
* Sign EIP-3009 transferWithAuthorization (GASLESS)
|
|
336
883
|
* This only signs - no on-chain transaction, no gas needed.
|
|
@@ -401,26 +948,26 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
401
948
|
}
|
|
402
949
|
// --- Config & Wallet Management ---
|
|
403
950
|
loadConfig() {
|
|
404
|
-
const configPath =
|
|
405
|
-
if (
|
|
406
|
-
const content =
|
|
951
|
+
const configPath = join2(this.configDir, "config.json");
|
|
952
|
+
if (existsSync2(configPath)) {
|
|
953
|
+
const content = readFileSync2(configPath, "utf-8");
|
|
407
954
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
408
955
|
}
|
|
409
956
|
return { ...DEFAULT_CONFIG };
|
|
410
957
|
}
|
|
411
958
|
saveConfig() {
|
|
412
|
-
|
|
413
|
-
const configPath =
|
|
414
|
-
|
|
959
|
+
mkdirSync2(this.configDir, { recursive: true });
|
|
960
|
+
const configPath = join2(this.configDir, "config.json");
|
|
961
|
+
writeFileSync2(configPath, JSON.stringify(this.config, null, 2));
|
|
415
962
|
}
|
|
416
963
|
/**
|
|
417
964
|
* Load spending data from disk
|
|
418
965
|
*/
|
|
419
966
|
loadSpending() {
|
|
420
|
-
const spendingPath =
|
|
421
|
-
if (
|
|
967
|
+
const spendingPath = join2(this.configDir, "spending.json");
|
|
968
|
+
if (existsSync2(spendingPath)) {
|
|
422
969
|
try {
|
|
423
|
-
const data = JSON.parse(
|
|
970
|
+
const data = JSON.parse(readFileSync2(spendingPath, "utf-8"));
|
|
424
971
|
const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
|
|
425
972
|
if (data.date && data.date === today) {
|
|
426
973
|
this.todaySpending = data.amount || 0;
|
|
@@ -439,18 +986,18 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
439
986
|
* Save spending data to disk
|
|
440
987
|
*/
|
|
441
988
|
saveSpending() {
|
|
442
|
-
|
|
443
|
-
const spendingPath =
|
|
989
|
+
mkdirSync2(this.configDir, { recursive: true });
|
|
990
|
+
const spendingPath = join2(this.configDir, "spending.json");
|
|
444
991
|
const data = {
|
|
445
992
|
date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
|
|
446
993
|
amount: this.todaySpending,
|
|
447
994
|
updatedAt: Date.now()
|
|
448
995
|
};
|
|
449
|
-
|
|
996
|
+
writeFileSync2(spendingPath, JSON.stringify(data, null, 2));
|
|
450
997
|
}
|
|
451
998
|
loadWallet() {
|
|
452
|
-
const walletPath =
|
|
453
|
-
if (
|
|
999
|
+
const walletPath = join2(this.configDir, "wallet.json");
|
|
1000
|
+
if (existsSync2(walletPath)) {
|
|
454
1001
|
try {
|
|
455
1002
|
const stats = statSync(walletPath);
|
|
456
1003
|
const mode = stats.mode & 511;
|
|
@@ -461,7 +1008,7 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
461
1008
|
}
|
|
462
1009
|
} catch (err) {
|
|
463
1010
|
}
|
|
464
|
-
const content =
|
|
1011
|
+
const content = readFileSync2(walletPath, "utf-8");
|
|
465
1012
|
return JSON.parse(content);
|
|
466
1013
|
}
|
|
467
1014
|
return null;
|
|
@@ -470,15 +1017,15 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
470
1017
|
* Initialize a new wallet (called by CLI)
|
|
471
1018
|
*/
|
|
472
1019
|
static init(configDir, options) {
|
|
473
|
-
|
|
1020
|
+
mkdirSync2(configDir, { recursive: true });
|
|
474
1021
|
const wallet = Wallet.createRandom();
|
|
475
1022
|
const walletData = {
|
|
476
1023
|
address: wallet.address,
|
|
477
1024
|
privateKey: wallet.privateKey,
|
|
478
1025
|
createdAt: Date.now()
|
|
479
1026
|
};
|
|
480
|
-
const walletPath =
|
|
481
|
-
|
|
1027
|
+
const walletPath = join2(configDir, "wallet.json");
|
|
1028
|
+
writeFileSync2(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
482
1029
|
const config = {
|
|
483
1030
|
chain: options.chain,
|
|
484
1031
|
limits: {
|
|
@@ -486,8 +1033,8 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
486
1033
|
maxPerDay: options.maxPerDay
|
|
487
1034
|
}
|
|
488
1035
|
};
|
|
489
|
-
const configPath =
|
|
490
|
-
|
|
1036
|
+
const configPath = join2(configDir, "config.json");
|
|
1037
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2));
|
|
491
1038
|
return { address: wallet.address, configDir };
|
|
492
1039
|
}
|
|
493
1040
|
/**
|
|
@@ -517,30 +1064,59 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
517
1064
|
};
|
|
518
1065
|
}
|
|
519
1066
|
/**
|
|
520
|
-
* Get wallet balances on all supported chains (Base + Polygon)
|
|
1067
|
+
* Get wallet balances on all supported chains (Base + Polygon + Tempo)
|
|
521
1068
|
*/
|
|
522
1069
|
async getAllBalances() {
|
|
523
1070
|
if (!this.wallet) {
|
|
524
1071
|
throw new Error("Client not initialized");
|
|
525
1072
|
}
|
|
526
|
-
const supportedChains = ["base", "polygon", "base_sepolia"];
|
|
1073
|
+
const supportedChains = ["base", "polygon", "base_sepolia", "tempo_moderato", "bnb", "bnb_testnet"];
|
|
527
1074
|
const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
528
1075
|
const results = {};
|
|
1076
|
+
const tempoTokens = {
|
|
1077
|
+
pathUSD: "0x20c0000000000000000000000000000000000000",
|
|
1078
|
+
alphaUSD: "0x20c0000000000000000000000000000000000001",
|
|
1079
|
+
betaUSD: "0x20c0000000000000000000000000000000000002",
|
|
1080
|
+
thetaUSD: "0x20c0000000000000000000000000000000000003"
|
|
1081
|
+
};
|
|
529
1082
|
await Promise.all(
|
|
530
1083
|
supportedChains.map(async (chainName) => {
|
|
531
1084
|
try {
|
|
532
1085
|
const chain = getChain(chainName);
|
|
533
1086
|
const provider = new ethers.JsonRpcProvider(chain.rpc);
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
1087
|
+
if (chainName === "tempo_moderato") {
|
|
1088
|
+
const [nativeBalance, pathUSD, alphaUSD, betaUSD, thetaUSD] = await Promise.all([
|
|
1089
|
+
provider.getBalance(this.wallet.address),
|
|
1090
|
+
new ethers.Contract(tempoTokens.pathUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
1091
|
+
new ethers.Contract(tempoTokens.alphaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
1092
|
+
new ethers.Contract(tempoTokens.betaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
1093
|
+
new ethers.Contract(tempoTokens.thetaUSD, tokenAbi, provider).balanceOf(this.wallet.address)
|
|
1094
|
+
]);
|
|
1095
|
+
results[chainName] = {
|
|
1096
|
+
usdc: parseFloat(ethers.formatUnits(pathUSD, 6)),
|
|
1097
|
+
// pathUSD as default USDC
|
|
1098
|
+
usdt: parseFloat(ethers.formatUnits(alphaUSD, 6)),
|
|
1099
|
+
// alphaUSD as default USDT
|
|
1100
|
+
native: parseFloat(ethers.formatEther(nativeBalance)),
|
|
1101
|
+
tempo: {
|
|
1102
|
+
pathUSD: parseFloat(ethers.formatUnits(pathUSD, 6)),
|
|
1103
|
+
alphaUSD: parseFloat(ethers.formatUnits(alphaUSD, 6)),
|
|
1104
|
+
betaUSD: parseFloat(ethers.formatUnits(betaUSD, 6)),
|
|
1105
|
+
thetaUSD: parseFloat(ethers.formatUnits(thetaUSD, 6))
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
} else {
|
|
1109
|
+
const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
|
|
1110
|
+
provider.getBalance(this.wallet.address),
|
|
1111
|
+
new ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
1112
|
+
new ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
|
|
1113
|
+
]);
|
|
1114
|
+
results[chainName] = {
|
|
1115
|
+
usdc: parseFloat(ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
|
|
1116
|
+
usdt: parseFloat(ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
|
|
1117
|
+
native: parseFloat(ethers.formatEther(nativeBalance))
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
544
1120
|
} catch (err) {
|
|
545
1121
|
results[chainName] = { usdc: 0, usdt: 0, native: 0 };
|
|
546
1122
|
}
|
|
@@ -548,6 +1124,121 @@ Please specify: --chain base, --chain polygon, or --chain base_sepolia`
|
|
|
548
1124
|
);
|
|
549
1125
|
return results;
|
|
550
1126
|
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Pay for a service using MPP (Machine Payments Protocol)
|
|
1129
|
+
*
|
|
1130
|
+
* This implements the MPP flow manually for EOA wallets:
|
|
1131
|
+
* 1. Request service → get 402 with WWW-Authenticate
|
|
1132
|
+
* 2. Parse payment requirements
|
|
1133
|
+
* 3. Execute transfer on Tempo chain
|
|
1134
|
+
* 4. Retry with transaction hash as credential
|
|
1135
|
+
*
|
|
1136
|
+
* @param url - Full URL of the MPP-enabled endpoint
|
|
1137
|
+
* @param options - Request options (body, headers)
|
|
1138
|
+
* @returns Response from the service
|
|
1139
|
+
*/
|
|
1140
|
+
async payWithMPP(url, options = {}) {
|
|
1141
|
+
if (!this.wallet || !this.walletData) {
|
|
1142
|
+
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
1143
|
+
}
|
|
1144
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
1145
|
+
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
1146
|
+
const { tempoModerato } = await import("viem/chains");
|
|
1147
|
+
const { Actions } = await import("viem/tempo");
|
|
1148
|
+
const privateKey = this.walletData.privateKey;
|
|
1149
|
+
const account = privateKeyToAccount(privateKey);
|
|
1150
|
+
console.log(`[MoltsPay] Making MPP request to: ${url}`);
|
|
1151
|
+
console.log(`[MoltsPay] Using account: ${account.address}`);
|
|
1152
|
+
const initResponse = await fetch(url, {
|
|
1153
|
+
method: "POST",
|
|
1154
|
+
headers: {
|
|
1155
|
+
"Content-Type": "application/json",
|
|
1156
|
+
...options.headers
|
|
1157
|
+
},
|
|
1158
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
1159
|
+
});
|
|
1160
|
+
if (initResponse.status !== 402) {
|
|
1161
|
+
if (initResponse.ok) {
|
|
1162
|
+
return initResponse.json();
|
|
1163
|
+
}
|
|
1164
|
+
const errorText = await initResponse.text();
|
|
1165
|
+
throw new Error(`Request failed (${initResponse.status}): ${errorText}`);
|
|
1166
|
+
}
|
|
1167
|
+
const wwwAuth = initResponse.headers.get("www-authenticate");
|
|
1168
|
+
if (!wwwAuth || !wwwAuth.toLowerCase().includes("payment")) {
|
|
1169
|
+
throw new Error("No WWW-Authenticate Payment challenge in 402 response");
|
|
1170
|
+
}
|
|
1171
|
+
console.log(`[MoltsPay] Got 402, parsing payment challenge...`);
|
|
1172
|
+
const parseAuthParam = (header, key) => {
|
|
1173
|
+
const match = header.match(new RegExp(`${key}="([^"]+)"`, "i"));
|
|
1174
|
+
return match ? match[1] : null;
|
|
1175
|
+
};
|
|
1176
|
+
const challengeId = parseAuthParam(wwwAuth, "id");
|
|
1177
|
+
const method = parseAuthParam(wwwAuth, "method");
|
|
1178
|
+
const realm = parseAuthParam(wwwAuth, "realm");
|
|
1179
|
+
const requestB64 = parseAuthParam(wwwAuth, "request");
|
|
1180
|
+
if (method !== "tempo") {
|
|
1181
|
+
throw new Error(`Unsupported payment method: ${method}`);
|
|
1182
|
+
}
|
|
1183
|
+
if (!requestB64) {
|
|
1184
|
+
throw new Error("Missing request in WWW-Authenticate");
|
|
1185
|
+
}
|
|
1186
|
+
const requestJson = Buffer.from(requestB64, "base64").toString("utf-8");
|
|
1187
|
+
const paymentRequest = JSON.parse(requestJson);
|
|
1188
|
+
console.log(`[MoltsPay] Payment request:`, paymentRequest);
|
|
1189
|
+
const { amount, currency, recipient, methodDetails } = paymentRequest;
|
|
1190
|
+
const chainId = methodDetails?.chainId || 42431;
|
|
1191
|
+
console.log(`[MoltsPay] Executing transfer on Tempo (chainId: ${chainId})...`);
|
|
1192
|
+
console.log(`[MoltsPay] Amount: ${amount}, To: ${recipient}`);
|
|
1193
|
+
const tempoChain = { ...tempoModerato, feeToken: currency };
|
|
1194
|
+
const publicClient = createPublicClient({
|
|
1195
|
+
chain: tempoChain,
|
|
1196
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
1197
|
+
});
|
|
1198
|
+
const walletClient = createWalletClient({
|
|
1199
|
+
account,
|
|
1200
|
+
chain: tempoChain,
|
|
1201
|
+
transport: http("https://rpc.moderato.tempo.xyz")
|
|
1202
|
+
});
|
|
1203
|
+
const txHash = await Actions.token.transfer(walletClient, {
|
|
1204
|
+
to: recipient,
|
|
1205
|
+
amount: BigInt(amount),
|
|
1206
|
+
token: currency
|
|
1207
|
+
});
|
|
1208
|
+
console.log(`[MoltsPay] Transaction sent: ${txHash}`);
|
|
1209
|
+
console.log(`[MoltsPay] Waiting for confirmation...`);
|
|
1210
|
+
await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
1211
|
+
console.log(`[MoltsPay] Transaction confirmed!`);
|
|
1212
|
+
const challenge = {
|
|
1213
|
+
id: challengeId,
|
|
1214
|
+
realm,
|
|
1215
|
+
method: "tempo",
|
|
1216
|
+
intent: "charge",
|
|
1217
|
+
request: paymentRequest
|
|
1218
|
+
};
|
|
1219
|
+
const credential = {
|
|
1220
|
+
challenge,
|
|
1221
|
+
payload: { hash: txHash, type: "hash" },
|
|
1222
|
+
source: `did:pkh:eip155:${chainId}:${account.address}`
|
|
1223
|
+
};
|
|
1224
|
+
const credentialB64 = Buffer.from(JSON.stringify(credential)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1225
|
+
console.log(`[MoltsPay] Retrying with payment credential...`);
|
|
1226
|
+
const paidResponse = await fetch(url, {
|
|
1227
|
+
method: "POST",
|
|
1228
|
+
headers: {
|
|
1229
|
+
"Content-Type": "application/json",
|
|
1230
|
+
"Authorization": `Payment ${credentialB64}`,
|
|
1231
|
+
...options.headers
|
|
1232
|
+
},
|
|
1233
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
1234
|
+
});
|
|
1235
|
+
if (!paidResponse.ok) {
|
|
1236
|
+
const errorText = await paidResponse.text();
|
|
1237
|
+
throw new Error(`Payment verification failed (${paidResponse.status}): ${errorText}`);
|
|
1238
|
+
}
|
|
1239
|
+
console.log(`[MoltsPay] Payment verified! Service completed.`);
|
|
1240
|
+
return paidResponse.json();
|
|
1241
|
+
}
|
|
551
1242
|
};
|
|
552
1243
|
export {
|
|
553
1244
|
MoltsPayClient
|