moltspay 0.5.4 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -124
- package/dist/cdp/index.d.mts +111 -0
- package/dist/cdp/index.d.ts +111 -0
- package/dist/cdp/index.js +30655 -0
- package/dist/cdp/index.js.map +1 -0
- package/dist/cdp/index.mjs +30631 -0
- package/dist/cdp/index.mjs.map +1 -0
- package/dist/chains/index.d.mts +1 -1
- package/dist/chains/index.d.ts +1 -1
- package/dist/cli/index.js +990 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/index.mjs +967 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/client/index.d.mts +134 -0
- package/dist/client/index.d.ts +134 -0
- package/dist/client/index.js +331 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/index.mjs +296 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/createWallet-D53qu7ie.d.mts +77 -0
- package/dist/createWallet-D53qu7ie.d.ts +77 -0
- package/dist/index-Dg8n6wdW.d.mts +32 -0
- package/dist/index-Dg8n6wdW.d.ts +32 -0
- package/dist/index.d.mts +6 -1483
- package/dist/index.d.ts +6 -1483
- package/dist/index.js +31039 -4254
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +31042 -4203
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.d.mts +120 -0
- package/dist/server/index.d.ts +120 -0
- package/dist/server/index.js +418 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +393 -0
- package/dist/server/index.mjs.map +1 -0
- package/dist/wallet/index.d.mts +3 -451
- package/dist/wallet/index.d.ts +3 -451
- package/dist/wallet/index.js +5 -1021
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs +16 -1015
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +19 -19
- package/dist/cli.js +0 -1984
- package/dist/cli.js.map +0 -1
- package/dist/cli.mjs +0 -1969
- package/dist/cli.mjs.map +0 -1
- package/dist/guide/index.d.mts +0 -39
- package/dist/guide/index.d.ts +0 -39
- package/dist/guide/index.js +0 -181
- package/dist/guide/index.js.map +0 -1
- package/dist/guide/index.mjs +0 -152
- package/dist/guide/index.mjs.map +0 -1
- package/dist/index-CyFg9s2m.d.mts +0 -161
- package/dist/index-CyFg9s2m.d.ts +0 -161
- package/dist/orders/index.d.mts +0 -97
- package/dist/orders/index.d.ts +0 -97
- package/dist/orders/index.js +0 -162
- package/dist/orders/index.js.map +0 -1
- package/dist/orders/index.mjs +0 -136
- package/dist/orders/index.mjs.map +0 -1
- package/dist/permit/index.d.mts +0 -49
- package/dist/permit/index.d.ts +0 -49
- package/dist/permit/index.js +0 -273
- package/dist/permit/index.js.map +0 -1
- package/dist/permit/index.mjs +0 -246
- package/dist/permit/index.mjs.map +0 -1
- /package/dist/{cli.d.mts → cli/index.d.mts} +0 -0
- /package/dist/{cli.d.ts → cli/index.d.ts} +0 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
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
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/client/index.ts
|
|
31
|
+
var client_exports = {};
|
|
32
|
+
__export(client_exports, {
|
|
33
|
+
MoltsPayClient: () => MoltsPayClient
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(client_exports);
|
|
36
|
+
var import_fs = require("fs");
|
|
37
|
+
var import_os = require("os");
|
|
38
|
+
var import_path = require("path");
|
|
39
|
+
var import_ethers = require("ethers");
|
|
40
|
+
|
|
41
|
+
// src/chains/index.ts
|
|
42
|
+
var CHAINS = {
|
|
43
|
+
// ============ Mainnet ============
|
|
44
|
+
base: {
|
|
45
|
+
name: "Base",
|
|
46
|
+
chainId: 8453,
|
|
47
|
+
rpc: "https://mainnet.base.org",
|
|
48
|
+
usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
49
|
+
explorer: "https://basescan.org/address/",
|
|
50
|
+
explorerTx: "https://basescan.org/tx/",
|
|
51
|
+
avgBlockTime: 2
|
|
52
|
+
},
|
|
53
|
+
polygon: {
|
|
54
|
+
name: "Polygon",
|
|
55
|
+
chainId: 137,
|
|
56
|
+
rpc: "https://polygon-rpc.com",
|
|
57
|
+
usdc: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
58
|
+
explorer: "https://polygonscan.com/address/",
|
|
59
|
+
explorerTx: "https://polygonscan.com/tx/",
|
|
60
|
+
avgBlockTime: 2
|
|
61
|
+
},
|
|
62
|
+
ethereum: {
|
|
63
|
+
name: "Ethereum",
|
|
64
|
+
chainId: 1,
|
|
65
|
+
rpc: "https://eth.llamarpc.com",
|
|
66
|
+
usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
67
|
+
explorer: "https://etherscan.io/address/",
|
|
68
|
+
explorerTx: "https://etherscan.io/tx/",
|
|
69
|
+
avgBlockTime: 12
|
|
70
|
+
},
|
|
71
|
+
// ============ Testnet ============
|
|
72
|
+
base_sepolia: {
|
|
73
|
+
name: "Base Sepolia",
|
|
74
|
+
chainId: 84532,
|
|
75
|
+
rpc: "https://sepolia.base.org",
|
|
76
|
+
usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
77
|
+
explorer: "https://sepolia.basescan.org/address/",
|
|
78
|
+
explorerTx: "https://sepolia.basescan.org/tx/",
|
|
79
|
+
avgBlockTime: 2
|
|
80
|
+
},
|
|
81
|
+
sepolia: {
|
|
82
|
+
name: "Sepolia",
|
|
83
|
+
chainId: 11155111,
|
|
84
|
+
rpc: "https://rpc.sepolia.org",
|
|
85
|
+
usdc: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
|
|
86
|
+
explorer: "https://sepolia.etherscan.io/address/",
|
|
87
|
+
explorerTx: "https://sepolia.etherscan.io/tx/",
|
|
88
|
+
avgBlockTime: 12
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
function getChain(name) {
|
|
92
|
+
const config = CHAINS[name];
|
|
93
|
+
if (!config) {
|
|
94
|
+
throw new Error(`Unsupported chain: ${name}. Supported: ${Object.keys(CHAINS).join(", ")}`);
|
|
95
|
+
}
|
|
96
|
+
return config;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/client/index.ts
|
|
100
|
+
var DEFAULT_CONFIG = {
|
|
101
|
+
chain: "base",
|
|
102
|
+
limits: {
|
|
103
|
+
maxPerTx: 100,
|
|
104
|
+
maxPerDay: 1e3
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
var MoltsPayClient = class {
|
|
108
|
+
configDir;
|
|
109
|
+
config;
|
|
110
|
+
walletData = null;
|
|
111
|
+
wallet = null;
|
|
112
|
+
todaySpending = 0;
|
|
113
|
+
lastSpendingReset = 0;
|
|
114
|
+
constructor(options = {}) {
|
|
115
|
+
this.configDir = options.configDir || (0, import_path.join)((0, import_os.homedir)(), ".moltspay");
|
|
116
|
+
this.config = this.loadConfig();
|
|
117
|
+
this.walletData = this.loadWallet();
|
|
118
|
+
if (this.walletData) {
|
|
119
|
+
this.wallet = new import_ethers.Wallet(this.walletData.privateKey);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Check if client is initialized (has wallet)
|
|
124
|
+
*/
|
|
125
|
+
get isInitialized() {
|
|
126
|
+
return this.wallet !== null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Get wallet address
|
|
130
|
+
*/
|
|
131
|
+
get address() {
|
|
132
|
+
return this.wallet?.address || null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get current config
|
|
136
|
+
*/
|
|
137
|
+
getConfig() {
|
|
138
|
+
return { ...this.config };
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Update config
|
|
142
|
+
*/
|
|
143
|
+
updateConfig(updates) {
|
|
144
|
+
if (updates.maxPerTx !== void 0) {
|
|
145
|
+
this.config.limits.maxPerTx = updates.maxPerTx;
|
|
146
|
+
}
|
|
147
|
+
if (updates.maxPerDay !== void 0) {
|
|
148
|
+
this.config.limits.maxPerDay = updates.maxPerDay;
|
|
149
|
+
}
|
|
150
|
+
this.saveConfig();
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Get services from a provider
|
|
154
|
+
*/
|
|
155
|
+
async getServices(serverUrl) {
|
|
156
|
+
const res = await fetch(`${serverUrl}/services`);
|
|
157
|
+
if (!res.ok) {
|
|
158
|
+
throw new Error(`Failed to get services: ${res.statusText}`);
|
|
159
|
+
}
|
|
160
|
+
return res.json();
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Pay for a service and get the result
|
|
164
|
+
*/
|
|
165
|
+
async pay(serverUrl, service, params) {
|
|
166
|
+
if (!this.wallet) {
|
|
167
|
+
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
168
|
+
}
|
|
169
|
+
const payRes = await fetch(`${serverUrl}/pay`, {
|
|
170
|
+
method: "POST",
|
|
171
|
+
headers: { "Content-Type": "application/json" },
|
|
172
|
+
body: JSON.stringify({ service, params })
|
|
173
|
+
});
|
|
174
|
+
if (payRes.status !== 402) {
|
|
175
|
+
const err = await payRes.json();
|
|
176
|
+
throw new Error(err.error || "Unexpected response");
|
|
177
|
+
}
|
|
178
|
+
const paymentReq = await payRes.json();
|
|
179
|
+
const { payment } = paymentReq;
|
|
180
|
+
this.checkLimits(payment.amount);
|
|
181
|
+
console.log(`[MoltsPay] Paying $${payment.amount} ${payment.currency} to ${payment.wallet}`);
|
|
182
|
+
const txHash = await this.executePayment(payment);
|
|
183
|
+
console.log(`[MoltsPay] Payment tx: ${txHash}`);
|
|
184
|
+
const verifyRes = await fetch(`${serverUrl}/verify`, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers: { "Content-Type": "application/json" },
|
|
187
|
+
body: JSON.stringify({
|
|
188
|
+
chargeId: payment.chargeId,
|
|
189
|
+
txHash
|
|
190
|
+
})
|
|
191
|
+
});
|
|
192
|
+
if (!verifyRes.ok) {
|
|
193
|
+
const err = await verifyRes.json();
|
|
194
|
+
throw new Error(err.error || "Verification failed");
|
|
195
|
+
}
|
|
196
|
+
const result = await verifyRes.json();
|
|
197
|
+
this.recordSpending(payment.amount);
|
|
198
|
+
return result.result;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Check spending limits
|
|
202
|
+
*/
|
|
203
|
+
checkLimits(amount) {
|
|
204
|
+
if (amount > this.config.limits.maxPerTx) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`Amount $${amount} exceeds max per transaction ($${this.config.limits.maxPerTx})`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
|
|
210
|
+
if (today > this.lastSpendingReset) {
|
|
211
|
+
this.todaySpending = 0;
|
|
212
|
+
this.lastSpendingReset = today;
|
|
213
|
+
}
|
|
214
|
+
if (this.todaySpending + amount > this.config.limits.maxPerDay) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`Would exceed daily limit ($${this.todaySpending} + $${amount} > $${this.config.limits.maxPerDay})`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Record spending
|
|
222
|
+
*/
|
|
223
|
+
recordSpending(amount) {
|
|
224
|
+
this.todaySpending += amount;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Execute payment on-chain
|
|
228
|
+
*/
|
|
229
|
+
async executePayment(payment) {
|
|
230
|
+
let chain;
|
|
231
|
+
try {
|
|
232
|
+
chain = getChain(payment.chain);
|
|
233
|
+
} catch {
|
|
234
|
+
throw new Error(`Unknown chain: ${payment.chain}`);
|
|
235
|
+
}
|
|
236
|
+
const { ethers } = await import("ethers");
|
|
237
|
+
const provider = new ethers.JsonRpcProvider(chain.rpc);
|
|
238
|
+
const signer = new ethers.Wallet(this.walletData.privateKey, provider);
|
|
239
|
+
const usdcAddress = chain.usdc;
|
|
240
|
+
const usdcAbi = [
|
|
241
|
+
"function transfer(address to, uint256 amount) returns (bool)",
|
|
242
|
+
"function balanceOf(address account) view returns (uint256)"
|
|
243
|
+
];
|
|
244
|
+
const usdc = new ethers.Contract(usdcAddress, usdcAbi, signer);
|
|
245
|
+
const amountInUnits = ethers.parseUnits(payment.amount.toString(), 6);
|
|
246
|
+
const balance = await usdc.balanceOf(this.wallet.address);
|
|
247
|
+
if (balance < amountInUnits) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`Insufficient USDC balance: ${ethers.formatUnits(balance, 6)} < ${payment.amount}`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
const tx = await usdc.transfer(payment.wallet, amountInUnits);
|
|
253
|
+
const receipt = await tx.wait();
|
|
254
|
+
return receipt.hash;
|
|
255
|
+
}
|
|
256
|
+
// --- Config & Wallet Management ---
|
|
257
|
+
loadConfig() {
|
|
258
|
+
const configPath = (0, import_path.join)(this.configDir, "config.json");
|
|
259
|
+
if ((0, import_fs.existsSync)(configPath)) {
|
|
260
|
+
const content = (0, import_fs.readFileSync)(configPath, "utf-8");
|
|
261
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
262
|
+
}
|
|
263
|
+
return { ...DEFAULT_CONFIG };
|
|
264
|
+
}
|
|
265
|
+
saveConfig() {
|
|
266
|
+
(0, import_fs.mkdirSync)(this.configDir, { recursive: true });
|
|
267
|
+
const configPath = (0, import_path.join)(this.configDir, "config.json");
|
|
268
|
+
(0, import_fs.writeFileSync)(configPath, JSON.stringify(this.config, null, 2));
|
|
269
|
+
}
|
|
270
|
+
loadWallet() {
|
|
271
|
+
const walletPath = (0, import_path.join)(this.configDir, "wallet.json");
|
|
272
|
+
if ((0, import_fs.existsSync)(walletPath)) {
|
|
273
|
+
const content = (0, import_fs.readFileSync)(walletPath, "utf-8");
|
|
274
|
+
return JSON.parse(content);
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Initialize a new wallet (called by CLI)
|
|
280
|
+
*/
|
|
281
|
+
static init(configDir, options) {
|
|
282
|
+
(0, import_fs.mkdirSync)(configDir, { recursive: true });
|
|
283
|
+
const wallet = import_ethers.Wallet.createRandom();
|
|
284
|
+
const walletData = {
|
|
285
|
+
address: wallet.address,
|
|
286
|
+
privateKey: wallet.privateKey,
|
|
287
|
+
createdAt: Date.now()
|
|
288
|
+
};
|
|
289
|
+
const walletPath = (0, import_path.join)(configDir, "wallet.json");
|
|
290
|
+
(0, import_fs.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2));
|
|
291
|
+
const config = {
|
|
292
|
+
chain: options.chain,
|
|
293
|
+
limits: {
|
|
294
|
+
maxPerTx: options.maxPerTx,
|
|
295
|
+
maxPerDay: options.maxPerDay
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
const configPath = (0, import_path.join)(configDir, "config.json");
|
|
299
|
+
(0, import_fs.writeFileSync)(configPath, JSON.stringify(config, null, 2));
|
|
300
|
+
return { address: wallet.address, configDir };
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Get wallet balance
|
|
304
|
+
*/
|
|
305
|
+
async getBalance() {
|
|
306
|
+
if (!this.wallet) {
|
|
307
|
+
throw new Error("Client not initialized");
|
|
308
|
+
}
|
|
309
|
+
let chain;
|
|
310
|
+
try {
|
|
311
|
+
chain = getChain(this.config.chain);
|
|
312
|
+
} catch {
|
|
313
|
+
throw new Error(`Unknown chain: ${this.config.chain}`);
|
|
314
|
+
}
|
|
315
|
+
const { ethers } = await import("ethers");
|
|
316
|
+
const provider = new ethers.JsonRpcProvider(chain.rpc);
|
|
317
|
+
const nativeBalance = await provider.getBalance(this.wallet.address);
|
|
318
|
+
const usdcAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
319
|
+
const usdc = new ethers.Contract(chain.usdc, usdcAbi, provider);
|
|
320
|
+
const usdcBalance = await usdc.balanceOf(this.wallet.address);
|
|
321
|
+
return {
|
|
322
|
+
usdc: parseFloat(ethers.formatUnits(usdcBalance, 6)),
|
|
323
|
+
native: parseFloat(ethers.formatEther(nativeBalance))
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
328
|
+
0 && (module.exports = {
|
|
329
|
+
MoltsPayClient
|
|
330
|
+
});
|
|
331
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/client/index.ts","../../src/chains/index.ts"],"sourcesContent":["/**\n * MoltsPay Client - Pay for AI Agent services\n * \n * Usage:\n * const client = new MoltsPayClient(); // Loads from ~/.moltspay/\n * const services = await client.getServices('http://provider:3000');\n * const result = await client.pay('http://provider:3000', 'text-to-video', { prompt: '...' });\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { homedir } from 'os';\nimport { join } from 'path';\nimport { Wallet } from 'ethers';\nimport { getChain, type ChainName } from '../chains/index.js';\nimport {\n ClientConfig,\n WalletData,\n PaymentRequired,\n ServicesResponse,\n VerifyResponse,\n MoltsPayClientOptions,\n} from './types.js';\n\nexport * from './types.js';\n\nconst DEFAULT_CONFIG: ClientConfig = {\n chain: 'base',\n limits: {\n maxPerTx: 100,\n maxPerDay: 1000,\n },\n};\n\nexport class MoltsPayClient {\n private configDir: string;\n private config: ClientConfig;\n private walletData: WalletData | null = null;\n private wallet: Wallet | null = null;\n private todaySpending: number = 0;\n private lastSpendingReset: number = 0;\n\n constructor(options: MoltsPayClientOptions = {}) {\n this.configDir = options.configDir || join(homedir(), '.moltspay');\n this.config = this.loadConfig();\n this.walletData = this.loadWallet();\n \n if (this.walletData) {\n this.wallet = new Wallet(this.walletData.privateKey);\n }\n }\n\n /**\n * Check if client is initialized (has wallet)\n */\n get isInitialized(): boolean {\n return this.wallet !== null;\n }\n\n /**\n * Get wallet address\n */\n get address(): string | null {\n return this.wallet?.address || null;\n }\n\n /**\n * Get current config\n */\n getConfig(): ClientConfig {\n return { ...this.config };\n }\n\n /**\n * Update config\n */\n updateConfig(updates: Partial<ClientConfig['limits']>): void {\n if (updates.maxPerTx !== undefined) {\n this.config.limits.maxPerTx = updates.maxPerTx;\n }\n if (updates.maxPerDay !== undefined) {\n this.config.limits.maxPerDay = updates.maxPerDay;\n }\n this.saveConfig();\n }\n\n /**\n * Get services from a provider\n */\n async getServices(serverUrl: string): Promise<ServicesResponse> {\n const res = await fetch(`${serverUrl}/services`);\n if (!res.ok) {\n throw new Error(`Failed to get services: ${res.statusText}`);\n }\n return res.json() as Promise<ServicesResponse>;\n }\n\n /**\n * Pay for a service and get the result\n */\n async pay(\n serverUrl: string,\n service: string,\n params: Record<string, any>\n ): Promise<Record<string, any>> {\n if (!this.wallet) {\n throw new Error('Client not initialized. Run: npx moltspay init');\n }\n\n // Step 1: Request payment info\n const payRes = await fetch(`${serverUrl}/pay`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ service, params }),\n });\n\n if (payRes.status !== 402) {\n const err = await payRes.json() as { error?: string };\n throw new Error(err.error || 'Unexpected response');\n }\n\n const paymentReq = await payRes.json() as PaymentRequired;\n const { payment } = paymentReq;\n\n // Step 2: Check limits\n this.checkLimits(payment.amount);\n\n // Step 3: Execute payment on-chain\n console.log(`[MoltsPay] Paying $${payment.amount} ${payment.currency} to ${payment.wallet}`);\n const txHash = await this.executePayment(payment);\n console.log(`[MoltsPay] Payment tx: ${txHash}`);\n\n // Step 4: Verify and get result\n const verifyRes = await fetch(`${serverUrl}/verify`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n chargeId: payment.chargeId,\n txHash,\n }),\n });\n\n if (!verifyRes.ok) {\n const err = await verifyRes.json() as { error?: string };\n throw new Error(err.error || 'Verification failed');\n }\n\n const result = await verifyRes.json() as VerifyResponse;\n \n // Update spending tracking\n this.recordSpending(payment.amount);\n \n return result.result;\n }\n\n /**\n * Check spending limits\n */\n private checkLimits(amount: number): void {\n // Check per-tx limit\n if (amount > this.config.limits.maxPerTx) {\n throw new Error(\n `Amount $${amount} exceeds max per transaction ($${this.config.limits.maxPerTx})`\n );\n }\n\n // Reset daily spending if new day\n const today = new Date().setHours(0, 0, 0, 0);\n if (today > this.lastSpendingReset) {\n this.todaySpending = 0;\n this.lastSpendingReset = today;\n }\n\n // Check daily limit\n if (this.todaySpending + amount > this.config.limits.maxPerDay) {\n throw new Error(\n `Would exceed daily limit ($${this.todaySpending} + $${amount} > $${this.config.limits.maxPerDay})`\n );\n }\n }\n\n /**\n * Record spending\n */\n private recordSpending(amount: number): void {\n this.todaySpending += amount;\n }\n\n /**\n * Execute payment on-chain\n */\n private async executePayment(payment: PaymentRequired['payment']): Promise<string> {\n let chain;\n try {\n chain = getChain(payment.chain as ChainName);\n } catch {\n throw new Error(`Unknown chain: ${payment.chain}`);\n }\n\n // For now, we'll use a simple USDC transfer\n // In production, this would connect to the actual chain\n const { ethers } = await import('ethers');\n \n const provider = new ethers.JsonRpcProvider(chain.rpc);\n const signer = new ethers.Wallet(this.walletData!.privateKey, provider);\n\n // USDC contract (Base mainnet)\n const usdcAddress = chain.usdc;\n const usdcAbi = [\n 'function transfer(address to, uint256 amount) returns (bool)',\n 'function balanceOf(address account) view returns (uint256)',\n ];\n \n const usdc = new ethers.Contract(usdcAddress, usdcAbi, signer);\n\n // Convert amount to USDC decimals (6)\n const amountInUnits = ethers.parseUnits(payment.amount.toString(), 6);\n\n // Check balance\n const balance = await usdc.balanceOf(this.wallet!.address);\n if (balance < amountInUnits) {\n throw new Error(\n `Insufficient USDC balance: ${ethers.formatUnits(balance, 6)} < ${payment.amount}`\n );\n }\n\n // Send transaction\n const tx = await usdc.transfer(payment.wallet, amountInUnits);\n const receipt = await tx.wait();\n\n return receipt.hash;\n }\n\n // --- Config & Wallet Management ---\n\n private loadConfig(): ClientConfig {\n const configPath = join(this.configDir, 'config.json');\n if (existsSync(configPath)) {\n const content = readFileSync(configPath, 'utf-8');\n return { ...DEFAULT_CONFIG, ...JSON.parse(content) };\n }\n return { ...DEFAULT_CONFIG };\n }\n\n private saveConfig(): void {\n mkdirSync(this.configDir, { recursive: true });\n const configPath = join(this.configDir, 'config.json');\n writeFileSync(configPath, JSON.stringify(this.config, null, 2));\n }\n\n private loadWallet(): WalletData | null {\n const walletPath = join(this.configDir, 'wallet.json');\n if (existsSync(walletPath)) {\n const content = readFileSync(walletPath, 'utf-8');\n return JSON.parse(content);\n }\n return null;\n }\n\n /**\n * Initialize a new wallet (called by CLI)\n */\n static init(\n configDir: string,\n options: { chain: string; maxPerTx: number; maxPerDay: number }\n ): { address: string; configDir: string } {\n mkdirSync(configDir, { recursive: true });\n\n // Create wallet\n const wallet = Wallet.createRandom();\n const walletData: WalletData = {\n address: wallet.address,\n privateKey: wallet.privateKey,\n createdAt: Date.now(),\n };\n\n // Save wallet\n const walletPath = join(configDir, 'wallet.json');\n writeFileSync(walletPath, JSON.stringify(walletData, null, 2));\n\n // Save config\n const config: ClientConfig = {\n chain: options.chain,\n limits: {\n maxPerTx: options.maxPerTx,\n maxPerDay: options.maxPerDay,\n },\n };\n const configPath = join(configDir, 'config.json');\n writeFileSync(configPath, JSON.stringify(config, null, 2));\n\n return { address: wallet.address, configDir };\n }\n\n /**\n * Get wallet balance\n */\n async getBalance(): Promise<{ usdc: number; native: number }> {\n if (!this.wallet) {\n throw new Error('Client not initialized');\n }\n\n let chain;\n try {\n chain = getChain(this.config.chain as ChainName);\n } catch {\n throw new Error(`Unknown chain: ${this.config.chain}`);\n }\n\n const { ethers } = await import('ethers');\n const provider = new ethers.JsonRpcProvider(chain.rpc);\n\n // Get native balance\n const nativeBalance = await provider.getBalance(this.wallet.address);\n\n // Get USDC balance\n const usdcAbi = ['function balanceOf(address) view returns (uint256)'];\n const usdc = new ethers.Contract(chain.usdc, usdcAbi, provider);\n const usdcBalance = await usdc.balanceOf(this.wallet.address);\n\n return {\n usdc: parseFloat(ethers.formatUnits(usdcBalance, 6)),\n native: parseFloat(ethers.formatEther(nativeBalance)),\n };\n }\n}\n","/**\n * Blockchain Configuration\n */\n\nimport type { ChainConfig, ChainName } from '../types/index.js';\n\nexport const CHAINS: Record<ChainName, ChainConfig> = {\n // ============ Mainnet ============\n base: {\n name: 'Base',\n chainId: 8453,\n rpc: 'https://mainnet.base.org',\n usdc: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',\n explorer: 'https://basescan.org/address/',\n explorerTx: 'https://basescan.org/tx/',\n avgBlockTime: 2,\n },\n polygon: {\n name: 'Polygon',\n chainId: 137,\n rpc: 'https://polygon-rpc.com',\n usdc: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',\n explorer: 'https://polygonscan.com/address/',\n explorerTx: 'https://polygonscan.com/tx/',\n avgBlockTime: 2,\n },\n ethereum: {\n name: 'Ethereum',\n chainId: 1,\n rpc: 'https://eth.llamarpc.com',\n usdc: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',\n explorer: 'https://etherscan.io/address/',\n explorerTx: 'https://etherscan.io/tx/',\n avgBlockTime: 12,\n },\n\n // ============ Testnet ============\n base_sepolia: {\n name: 'Base Sepolia',\n chainId: 84532,\n rpc: 'https://sepolia.base.org',\n usdc: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',\n explorer: 'https://sepolia.basescan.org/address/',\n explorerTx: 'https://sepolia.basescan.org/tx/',\n avgBlockTime: 2,\n },\n sepolia: {\n name: 'Sepolia',\n chainId: 11155111,\n rpc: 'https://rpc.sepolia.org',\n usdc: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',\n explorer: 'https://sepolia.etherscan.io/address/',\n explorerTx: 'https://sepolia.etherscan.io/tx/',\n avgBlockTime: 12,\n },\n};\n\n/**\n * Get chain configuration\n */\nexport function getChain(name: ChainName): ChainConfig {\n const config = CHAINS[name];\n if (!config) {\n throw new Error(`Unsupported chain: ${name}. Supported: ${Object.keys(CHAINS).join(', ')}`);\n }\n return config;\n}\n\n/**\n * List all supported chains\n */\nexport function listChains(): ChainName[] {\n return Object.keys(CHAINS) as ChainName[];\n}\n\n/**\n * Get chain config by chainId\n */\nexport function getChainById(chainId: number): ChainConfig | undefined {\n return Object.values(CHAINS).find(c => c.chainId === chainId);\n}\n\n/**\n * ERC20 ABI (minimal, only required methods)\n */\nexport const ERC20_ABI = [\n 'function balanceOf(address owner) view returns (uint256)',\n 'function transfer(address to, uint256 amount) returns (bool)',\n 'function approve(address spender, uint256 amount) returns (bool)',\n 'function allowance(address owner, address spender) view returns (uint256)',\n 'function decimals() view returns (uint8)',\n 'function symbol() view returns (string)',\n 'function name() view returns (string)',\n 'function nonces(address owner) view returns (uint256)',\n 'function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)',\n 'event Transfer(address indexed from, address indexed to, uint256 value)',\n 'event Approval(address indexed owner, address indexed spender, uint256 value)',\n];\n\nexport type { ChainConfig, ChainName };\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// src/client/index.ts
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { Wallet } from "ethers";
|
|
6
|
+
|
|
7
|
+
// src/chains/index.ts
|
|
8
|
+
var CHAINS = {
|
|
9
|
+
// ============ Mainnet ============
|
|
10
|
+
base: {
|
|
11
|
+
name: "Base",
|
|
12
|
+
chainId: 8453,
|
|
13
|
+
rpc: "https://mainnet.base.org",
|
|
14
|
+
usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
15
|
+
explorer: "https://basescan.org/address/",
|
|
16
|
+
explorerTx: "https://basescan.org/tx/",
|
|
17
|
+
avgBlockTime: 2
|
|
18
|
+
},
|
|
19
|
+
polygon: {
|
|
20
|
+
name: "Polygon",
|
|
21
|
+
chainId: 137,
|
|
22
|
+
rpc: "https://polygon-rpc.com",
|
|
23
|
+
usdc: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
24
|
+
explorer: "https://polygonscan.com/address/",
|
|
25
|
+
explorerTx: "https://polygonscan.com/tx/",
|
|
26
|
+
avgBlockTime: 2
|
|
27
|
+
},
|
|
28
|
+
ethereum: {
|
|
29
|
+
name: "Ethereum",
|
|
30
|
+
chainId: 1,
|
|
31
|
+
rpc: "https://eth.llamarpc.com",
|
|
32
|
+
usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
33
|
+
explorer: "https://etherscan.io/address/",
|
|
34
|
+
explorerTx: "https://etherscan.io/tx/",
|
|
35
|
+
avgBlockTime: 12
|
|
36
|
+
},
|
|
37
|
+
// ============ Testnet ============
|
|
38
|
+
base_sepolia: {
|
|
39
|
+
name: "Base Sepolia",
|
|
40
|
+
chainId: 84532,
|
|
41
|
+
rpc: "https://sepolia.base.org",
|
|
42
|
+
usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
43
|
+
explorer: "https://sepolia.basescan.org/address/",
|
|
44
|
+
explorerTx: "https://sepolia.basescan.org/tx/",
|
|
45
|
+
avgBlockTime: 2
|
|
46
|
+
},
|
|
47
|
+
sepolia: {
|
|
48
|
+
name: "Sepolia",
|
|
49
|
+
chainId: 11155111,
|
|
50
|
+
rpc: "https://rpc.sepolia.org",
|
|
51
|
+
usdc: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
|
|
52
|
+
explorer: "https://sepolia.etherscan.io/address/",
|
|
53
|
+
explorerTx: "https://sepolia.etherscan.io/tx/",
|
|
54
|
+
avgBlockTime: 12
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
function getChain(name) {
|
|
58
|
+
const config = CHAINS[name];
|
|
59
|
+
if (!config) {
|
|
60
|
+
throw new Error(`Unsupported chain: ${name}. Supported: ${Object.keys(CHAINS).join(", ")}`);
|
|
61
|
+
}
|
|
62
|
+
return config;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/client/index.ts
|
|
66
|
+
var DEFAULT_CONFIG = {
|
|
67
|
+
chain: "base",
|
|
68
|
+
limits: {
|
|
69
|
+
maxPerTx: 100,
|
|
70
|
+
maxPerDay: 1e3
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var MoltsPayClient = class {
|
|
74
|
+
configDir;
|
|
75
|
+
config;
|
|
76
|
+
walletData = null;
|
|
77
|
+
wallet = null;
|
|
78
|
+
todaySpending = 0;
|
|
79
|
+
lastSpendingReset = 0;
|
|
80
|
+
constructor(options = {}) {
|
|
81
|
+
this.configDir = options.configDir || join(homedir(), ".moltspay");
|
|
82
|
+
this.config = this.loadConfig();
|
|
83
|
+
this.walletData = this.loadWallet();
|
|
84
|
+
if (this.walletData) {
|
|
85
|
+
this.wallet = new Wallet(this.walletData.privateKey);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Check if client is initialized (has wallet)
|
|
90
|
+
*/
|
|
91
|
+
get isInitialized() {
|
|
92
|
+
return this.wallet !== null;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get wallet address
|
|
96
|
+
*/
|
|
97
|
+
get address() {
|
|
98
|
+
return this.wallet?.address || null;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get current config
|
|
102
|
+
*/
|
|
103
|
+
getConfig() {
|
|
104
|
+
return { ...this.config };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Update config
|
|
108
|
+
*/
|
|
109
|
+
updateConfig(updates) {
|
|
110
|
+
if (updates.maxPerTx !== void 0) {
|
|
111
|
+
this.config.limits.maxPerTx = updates.maxPerTx;
|
|
112
|
+
}
|
|
113
|
+
if (updates.maxPerDay !== void 0) {
|
|
114
|
+
this.config.limits.maxPerDay = updates.maxPerDay;
|
|
115
|
+
}
|
|
116
|
+
this.saveConfig();
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get services from a provider
|
|
120
|
+
*/
|
|
121
|
+
async getServices(serverUrl) {
|
|
122
|
+
const res = await fetch(`${serverUrl}/services`);
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
throw new Error(`Failed to get services: ${res.statusText}`);
|
|
125
|
+
}
|
|
126
|
+
return res.json();
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Pay for a service and get the result
|
|
130
|
+
*/
|
|
131
|
+
async pay(serverUrl, service, params) {
|
|
132
|
+
if (!this.wallet) {
|
|
133
|
+
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
134
|
+
}
|
|
135
|
+
const payRes = await fetch(`${serverUrl}/pay`, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: { "Content-Type": "application/json" },
|
|
138
|
+
body: JSON.stringify({ service, params })
|
|
139
|
+
});
|
|
140
|
+
if (payRes.status !== 402) {
|
|
141
|
+
const err = await payRes.json();
|
|
142
|
+
throw new Error(err.error || "Unexpected response");
|
|
143
|
+
}
|
|
144
|
+
const paymentReq = await payRes.json();
|
|
145
|
+
const { payment } = paymentReq;
|
|
146
|
+
this.checkLimits(payment.amount);
|
|
147
|
+
console.log(`[MoltsPay] Paying $${payment.amount} ${payment.currency} to ${payment.wallet}`);
|
|
148
|
+
const txHash = await this.executePayment(payment);
|
|
149
|
+
console.log(`[MoltsPay] Payment tx: ${txHash}`);
|
|
150
|
+
const verifyRes = await fetch(`${serverUrl}/verify`, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers: { "Content-Type": "application/json" },
|
|
153
|
+
body: JSON.stringify({
|
|
154
|
+
chargeId: payment.chargeId,
|
|
155
|
+
txHash
|
|
156
|
+
})
|
|
157
|
+
});
|
|
158
|
+
if (!verifyRes.ok) {
|
|
159
|
+
const err = await verifyRes.json();
|
|
160
|
+
throw new Error(err.error || "Verification failed");
|
|
161
|
+
}
|
|
162
|
+
const result = await verifyRes.json();
|
|
163
|
+
this.recordSpending(payment.amount);
|
|
164
|
+
return result.result;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Check spending limits
|
|
168
|
+
*/
|
|
169
|
+
checkLimits(amount) {
|
|
170
|
+
if (amount > this.config.limits.maxPerTx) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Amount $${amount} exceeds max per transaction ($${this.config.limits.maxPerTx})`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
const today = (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0);
|
|
176
|
+
if (today > this.lastSpendingReset) {
|
|
177
|
+
this.todaySpending = 0;
|
|
178
|
+
this.lastSpendingReset = today;
|
|
179
|
+
}
|
|
180
|
+
if (this.todaySpending + amount > this.config.limits.maxPerDay) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Would exceed daily limit ($${this.todaySpending} + $${amount} > $${this.config.limits.maxPerDay})`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Record spending
|
|
188
|
+
*/
|
|
189
|
+
recordSpending(amount) {
|
|
190
|
+
this.todaySpending += amount;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Execute payment on-chain
|
|
194
|
+
*/
|
|
195
|
+
async executePayment(payment) {
|
|
196
|
+
let chain;
|
|
197
|
+
try {
|
|
198
|
+
chain = getChain(payment.chain);
|
|
199
|
+
} catch {
|
|
200
|
+
throw new Error(`Unknown chain: ${payment.chain}`);
|
|
201
|
+
}
|
|
202
|
+
const { ethers } = await import("ethers");
|
|
203
|
+
const provider = new ethers.JsonRpcProvider(chain.rpc);
|
|
204
|
+
const signer = new ethers.Wallet(this.walletData.privateKey, provider);
|
|
205
|
+
const usdcAddress = chain.usdc;
|
|
206
|
+
const usdcAbi = [
|
|
207
|
+
"function transfer(address to, uint256 amount) returns (bool)",
|
|
208
|
+
"function balanceOf(address account) view returns (uint256)"
|
|
209
|
+
];
|
|
210
|
+
const usdc = new ethers.Contract(usdcAddress, usdcAbi, signer);
|
|
211
|
+
const amountInUnits = ethers.parseUnits(payment.amount.toString(), 6);
|
|
212
|
+
const balance = await usdc.balanceOf(this.wallet.address);
|
|
213
|
+
if (balance < amountInUnits) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Insufficient USDC balance: ${ethers.formatUnits(balance, 6)} < ${payment.amount}`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const tx = await usdc.transfer(payment.wallet, amountInUnits);
|
|
219
|
+
const receipt = await tx.wait();
|
|
220
|
+
return receipt.hash;
|
|
221
|
+
}
|
|
222
|
+
// --- Config & Wallet Management ---
|
|
223
|
+
loadConfig() {
|
|
224
|
+
const configPath = join(this.configDir, "config.json");
|
|
225
|
+
if (existsSync(configPath)) {
|
|
226
|
+
const content = readFileSync(configPath, "utf-8");
|
|
227
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
228
|
+
}
|
|
229
|
+
return { ...DEFAULT_CONFIG };
|
|
230
|
+
}
|
|
231
|
+
saveConfig() {
|
|
232
|
+
mkdirSync(this.configDir, { recursive: true });
|
|
233
|
+
const configPath = join(this.configDir, "config.json");
|
|
234
|
+
writeFileSync(configPath, JSON.stringify(this.config, null, 2));
|
|
235
|
+
}
|
|
236
|
+
loadWallet() {
|
|
237
|
+
const walletPath = join(this.configDir, "wallet.json");
|
|
238
|
+
if (existsSync(walletPath)) {
|
|
239
|
+
const content = readFileSync(walletPath, "utf-8");
|
|
240
|
+
return JSON.parse(content);
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Initialize a new wallet (called by CLI)
|
|
246
|
+
*/
|
|
247
|
+
static init(configDir, options) {
|
|
248
|
+
mkdirSync(configDir, { recursive: true });
|
|
249
|
+
const wallet = Wallet.createRandom();
|
|
250
|
+
const walletData = {
|
|
251
|
+
address: wallet.address,
|
|
252
|
+
privateKey: wallet.privateKey,
|
|
253
|
+
createdAt: Date.now()
|
|
254
|
+
};
|
|
255
|
+
const walletPath = join(configDir, "wallet.json");
|
|
256
|
+
writeFileSync(walletPath, JSON.stringify(walletData, null, 2));
|
|
257
|
+
const config = {
|
|
258
|
+
chain: options.chain,
|
|
259
|
+
limits: {
|
|
260
|
+
maxPerTx: options.maxPerTx,
|
|
261
|
+
maxPerDay: options.maxPerDay
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
const configPath = join(configDir, "config.json");
|
|
265
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
266
|
+
return { address: wallet.address, configDir };
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Get wallet balance
|
|
270
|
+
*/
|
|
271
|
+
async getBalance() {
|
|
272
|
+
if (!this.wallet) {
|
|
273
|
+
throw new Error("Client not initialized");
|
|
274
|
+
}
|
|
275
|
+
let chain;
|
|
276
|
+
try {
|
|
277
|
+
chain = getChain(this.config.chain);
|
|
278
|
+
} catch {
|
|
279
|
+
throw new Error(`Unknown chain: ${this.config.chain}`);
|
|
280
|
+
}
|
|
281
|
+
const { ethers } = await import("ethers");
|
|
282
|
+
const provider = new ethers.JsonRpcProvider(chain.rpc);
|
|
283
|
+
const nativeBalance = await provider.getBalance(this.wallet.address);
|
|
284
|
+
const usdcAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
285
|
+
const usdc = new ethers.Contract(chain.usdc, usdcAbi, provider);
|
|
286
|
+
const usdcBalance = await usdc.balanceOf(this.wallet.address);
|
|
287
|
+
return {
|
|
288
|
+
usdc: parseFloat(ethers.formatUnits(usdcBalance, 6)),
|
|
289
|
+
native: parseFloat(ethers.formatEther(nativeBalance))
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
export {
|
|
294
|
+
MoltsPayClient
|
|
295
|
+
};
|
|
296
|
+
//# sourceMappingURL=index.mjs.map
|