moltspay 2.0.0 → 2.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/.env.example +23 -0
- package/CHANGELOG.md +551 -0
- package/README.md +466 -8
- package/dist/cdp/index.js.map +1 -1
- package/dist/cdp/index.mjs.map +1 -1
- package/dist/chains/index.d.mts +69 -4
- package/dist/chains/index.d.ts +69 -4
- package/dist/chains/index.js +45 -1
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/index.mjs +39 -1
- package/dist/chains/index.mjs.map +1 -1
- package/dist/cli/index.js +5444 -2126
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +5457 -2133
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +307 -1
- package/dist/client/index.d.ts +307 -1
- package/dist/client/index.js +639 -34
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +656 -52
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/web/index.mjs.map +1 -1
- package/dist/facilitators/index.d.mts +512 -10
- package/dist/facilitators/index.d.ts +512 -10
- package/dist/facilitators/index.js +925 -13
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +906 -12
- package/dist/facilitators/index.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2843 -551
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2849 -558
- package/dist/index.mjs.map +1 -1
- package/dist/mcp/index.js +635 -32
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/index.mjs +660 -57
- package/dist/mcp/index.mjs.map +1 -1
- package/dist/server/index.d.mts +252 -11
- package/dist/server/index.d.ts +252 -11
- package/dist/server/index.js +2049 -261
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +2049 -261
- package/dist/server/index.mjs.map +1 -1
- package/dist/verify/index.js.map +1 -1
- package/dist/verify/index.mjs.map +1 -1
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +14 -2
- package/schemas/moltspay.services.schema.json +127 -16
package/dist/client/index.js
CHANGED
|
@@ -30,7 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/client/index.ts
|
|
31
31
|
var client_exports = {};
|
|
32
32
|
__export(client_exports, {
|
|
33
|
-
MoltsPayClient: () => MoltsPayClient
|
|
33
|
+
MoltsPayClient: () => MoltsPayClient,
|
|
34
|
+
WechatClient: () => WechatClient
|
|
34
35
|
});
|
|
35
36
|
module.exports = __toCommonJS(client_exports);
|
|
36
37
|
|
|
@@ -38,7 +39,8 @@ module.exports = __toCommonJS(client_exports);
|
|
|
38
39
|
var import_fs2 = require("fs");
|
|
39
40
|
var import_os3 = require("os");
|
|
40
41
|
var import_path3 = require("path");
|
|
41
|
-
var
|
|
42
|
+
var import_node_crypto3 = require("crypto");
|
|
43
|
+
var import_ethers3 = require("ethers");
|
|
42
44
|
|
|
43
45
|
// src/chains/index.ts
|
|
44
46
|
var CHAINS = {
|
|
@@ -1099,7 +1101,7 @@ var AlipayClient = class {
|
|
|
1099
1101
|
if (m) media.push(m);
|
|
1100
1102
|
else opts.onLine?.(line);
|
|
1101
1103
|
};
|
|
1102
|
-
const intentSummary = opts.intentSummary?.trim() ||
|
|
1104
|
+
const intentSummary = opts.intentSummary?.trim() || `Pay ${requirement.amount ?? ""} ${requirement.asset ?? "CNY"}`.trim();
|
|
1103
1105
|
await timeStep("ensure-cli", flow, () => ensureCli(this.getVersion));
|
|
1104
1106
|
const intentTtlMs = resolveIntentTtlMs();
|
|
1105
1107
|
const intentKey = `${this.configDir}::${this.framework}`;
|
|
@@ -1184,10 +1186,205 @@ var AlipayClient = class {
|
|
|
1184
1186
|
}
|
|
1185
1187
|
};
|
|
1186
1188
|
|
|
1189
|
+
// src/client/wechat/index.ts
|
|
1190
|
+
var import_node_crypto2 = require("crypto");
|
|
1191
|
+
var import_node_fs = require("fs");
|
|
1192
|
+
var import_node_os = require("os");
|
|
1193
|
+
var import_node_path = require("path");
|
|
1194
|
+
var WECHAT_SCHEME = "wechatpay-native";
|
|
1195
|
+
var WECHAT_NETWORK = "wechat";
|
|
1196
|
+
var POLL_INTERVAL_MS2 = 3e3;
|
|
1197
|
+
var TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1198
|
+
var WechatClient = class {
|
|
1199
|
+
configDir;
|
|
1200
|
+
sessionDir;
|
|
1201
|
+
now;
|
|
1202
|
+
constructor(options = {}) {
|
|
1203
|
+
this.configDir = options.configDir || (0, import_node_path.join)((0, import_node_os.homedir)(), ".moltspay");
|
|
1204
|
+
this.sessionDir = (0, import_node_path.join)(this.configDir, "wechat-sessions");
|
|
1205
|
+
this.now = options.now ?? Date.now;
|
|
1206
|
+
}
|
|
1207
|
+
async pay402(opts) {
|
|
1208
|
+
const session = this.start402({ ...opts });
|
|
1209
|
+
const result = await this.pollSession(session.paymentSessionId, {
|
|
1210
|
+
pollIntervalMs: opts.pollIntervalMs,
|
|
1211
|
+
timeoutMs: opts.timeoutMs,
|
|
1212
|
+
signal: opts.signal
|
|
1213
|
+
});
|
|
1214
|
+
if (result.status === "paid" || result.status === "completed") {
|
|
1215
|
+
return {
|
|
1216
|
+
body: result.resultBody ?? "",
|
|
1217
|
+
status: result.lastHttpStatus ?? 200
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
throw new Error(result.lastError || `WeChat payment ended with status ${result.status}`);
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Start a recoverable WeChat payment session. This returns immediately after
|
|
1224
|
+
* persisting `out_trade_no`, QR payload, original request body, and context.
|
|
1225
|
+
*/
|
|
1226
|
+
start402(opts) {
|
|
1227
|
+
const extra = opts.requirement.extra ?? {};
|
|
1228
|
+
const codeUrl = typeof extra.code_url === "string" ? extra.code_url : "";
|
|
1229
|
+
const outTradeNo = typeof extra.out_trade_no === "string" ? extra.out_trade_no : "";
|
|
1230
|
+
if (!codeUrl || !outTradeNo) {
|
|
1231
|
+
throw new Error(
|
|
1232
|
+
"WechatClient.pay402: wechatpay-native requirement is missing extra.code_url / extra.out_trade_no"
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
const now = new Date(this.now());
|
|
1236
|
+
const timeoutMs = opts.timeoutMs ?? (opts.requirement.maxTimeoutSeconds ?? TIMEOUT_MS / 1e3) * 1e3;
|
|
1237
|
+
const session = {
|
|
1238
|
+
paymentSessionId: opts.paymentSessionId ?? `mpay_sess_${(0, import_node_crypto2.randomUUID)()}`,
|
|
1239
|
+
status: "pending",
|
|
1240
|
+
resourceUrl: opts.resourceUrl,
|
|
1241
|
+
method: opts.method ?? "POST",
|
|
1242
|
+
data: opts.data,
|
|
1243
|
+
requirement: opts.requirement,
|
|
1244
|
+
codeUrl,
|
|
1245
|
+
outTradeNo,
|
|
1246
|
+
createdAt: now.toISOString(),
|
|
1247
|
+
updatedAt: now.toISOString(),
|
|
1248
|
+
expiresAt: new Date(this.now() + timeoutMs).toISOString(),
|
|
1249
|
+
context: opts.context
|
|
1250
|
+
};
|
|
1251
|
+
this.saveSession(session);
|
|
1252
|
+
opts.onPaymentPending?.({ codeUrl, outTradeNo });
|
|
1253
|
+
return session;
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Query a persisted session once by replaying the original request with the
|
|
1257
|
+
* stored `out_trade_no` proof. 200 means paid + fulfilled; 402 means pending.
|
|
1258
|
+
*/
|
|
1259
|
+
async status(identifier) {
|
|
1260
|
+
const session = this.loadSession(identifier);
|
|
1261
|
+
if (session.status === "cancelled" || session.status === "completed") return session;
|
|
1262
|
+
if (this.now() >= new Date(session.expiresAt).getTime()) {
|
|
1263
|
+
return this.updateSession(session, {
|
|
1264
|
+
status: "expired",
|
|
1265
|
+
lastError: `WeChat payment expired at ${session.expiresAt}`
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
const xPayment = this.buildPaymentHeader(session);
|
|
1269
|
+
const res = await fetch(session.resourceUrl, {
|
|
1270
|
+
method: session.method,
|
|
1271
|
+
headers: { "Content-Type": "application/json", "X-Payment": xPayment },
|
|
1272
|
+
body: session.method === "POST" ? session.data : void 0
|
|
1273
|
+
});
|
|
1274
|
+
const text = await res.text().catch(() => "");
|
|
1275
|
+
if (res.status === 200) {
|
|
1276
|
+
return this.updateSession(session, {
|
|
1277
|
+
status: "completed",
|
|
1278
|
+
lastHttpStatus: res.status,
|
|
1279
|
+
lastError: void 0,
|
|
1280
|
+
resultBody: text
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
if (res.status === 402) {
|
|
1284
|
+
return this.updateSession(session, {
|
|
1285
|
+
status: "pending",
|
|
1286
|
+
lastHttpStatus: res.status,
|
|
1287
|
+
lastError: text.slice(0, 500) || "wechat payment pending"
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
return this.updateSession(session, {
|
|
1291
|
+
status: "failed",
|
|
1292
|
+
lastHttpStatus: res.status,
|
|
1293
|
+
lastError: `WeChat /execute returned HTTP ${res.status}: ${text.slice(0, 500)}`
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
/** Poll a session until it completes, expires, fails, or is aborted. */
|
|
1297
|
+
async pollSession(identifier, opts = {}) {
|
|
1298
|
+
const first = this.loadSession(identifier);
|
|
1299
|
+
const deadline = Math.min(
|
|
1300
|
+
new Date(first.expiresAt).getTime(),
|
|
1301
|
+
this.now() + (opts.timeoutMs ?? TIMEOUT_MS)
|
|
1302
|
+
);
|
|
1303
|
+
const interval = opts.pollIntervalMs ?? POLL_INTERVAL_MS2;
|
|
1304
|
+
let session = first;
|
|
1305
|
+
while (this.now() < deadline) {
|
|
1306
|
+
if (opts.signal?.aborted) throw new Error("WeChat payment aborted");
|
|
1307
|
+
session = await this.status(session.paymentSessionId);
|
|
1308
|
+
if (session.status !== "pending") return session;
|
|
1309
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
1310
|
+
}
|
|
1311
|
+
return this.updateSession(session, {
|
|
1312
|
+
status: "expired",
|
|
1313
|
+
lastError: `WeChat payment timed out after ${Math.round((opts.timeoutMs ?? TIMEOUT_MS) / 1e3)}s (out_trade_no=${session.outTradeNo}, last status ${session.lastHttpStatus ?? 0})`
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
/** `fulfill` is an idempotent status check: paid sessions return stored body. */
|
|
1317
|
+
async fulfill(identifier) {
|
|
1318
|
+
const session = this.loadSession(identifier);
|
|
1319
|
+
if (session.status === "completed") return session;
|
|
1320
|
+
return this.status(identifier);
|
|
1321
|
+
}
|
|
1322
|
+
/** Mark a local session cancelled. Closing the merchant order is server-side. */
|
|
1323
|
+
cancel(identifier) {
|
|
1324
|
+
const session = this.loadSession(identifier);
|
|
1325
|
+
return this.updateSession(session, {
|
|
1326
|
+
status: "cancelled",
|
|
1327
|
+
lastError: "cancelled locally"
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
listSessions() {
|
|
1331
|
+
this.ensureSessionDir();
|
|
1332
|
+
return (0, import_node_fs.readdirSync)(this.sessionDir).filter((name) => name.endsWith(".json")).map((name) => JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path.join)(this.sessionDir, name), "utf-8"))).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
1333
|
+
}
|
|
1334
|
+
buildPaymentHeader(session) {
|
|
1335
|
+
const proof = {
|
|
1336
|
+
x402Version: 2,
|
|
1337
|
+
scheme: WECHAT_SCHEME,
|
|
1338
|
+
network: WECHAT_NETWORK,
|
|
1339
|
+
accepted: {
|
|
1340
|
+
scheme: WECHAT_SCHEME,
|
|
1341
|
+
network: WECHAT_NETWORK,
|
|
1342
|
+
extra: { out_trade_no: session.outTradeNo }
|
|
1343
|
+
},
|
|
1344
|
+
payload: { out_trade_no: session.outTradeNo }
|
|
1345
|
+
};
|
|
1346
|
+
return Buffer.from(JSON.stringify(proof)).toString("base64");
|
|
1347
|
+
}
|
|
1348
|
+
loadSession(identifier) {
|
|
1349
|
+
this.ensureSessionDir();
|
|
1350
|
+
const direct = (0, import_node_path.join)(this.sessionDir, `${identifier}.json`);
|
|
1351
|
+
try {
|
|
1352
|
+
return JSON.parse((0, import_node_fs.readFileSync)(direct, "utf-8"));
|
|
1353
|
+
} catch {
|
|
1354
|
+
const byTradeNo = this.listSessions().find((s) => s.outTradeNo === identifier);
|
|
1355
|
+
if (byTradeNo) return byTradeNo;
|
|
1356
|
+
throw new Error(`WeChat payment session not found: ${identifier}`);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
saveSession(session) {
|
|
1360
|
+
this.ensureSessionDir();
|
|
1361
|
+
(0, import_node_fs.writeFileSync)(
|
|
1362
|
+
(0, import_node_path.join)(this.sessionDir, `${session.paymentSessionId}.json`),
|
|
1363
|
+
JSON.stringify(session, null, 2)
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
updateSession(session, updates) {
|
|
1367
|
+
const next = {
|
|
1368
|
+
...session,
|
|
1369
|
+
...updates,
|
|
1370
|
+
updatedAt: new Date(this.now()).toISOString()
|
|
1371
|
+
};
|
|
1372
|
+
this.saveSession(next);
|
|
1373
|
+
return next;
|
|
1374
|
+
}
|
|
1375
|
+
ensureSessionDir() {
|
|
1376
|
+
(0, import_node_fs.mkdirSync)(this.sessionDir, { recursive: true });
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1379
|
+
|
|
1187
1380
|
// src/client/alipay/router.ts
|
|
1188
1381
|
var ALIPAY_RAIL = "alipay";
|
|
1382
|
+
var WECHAT_RAIL = "wechat";
|
|
1383
|
+
var BALANCE_RAIL = "balance";
|
|
1189
1384
|
function railOf(req) {
|
|
1190
1385
|
if (req.scheme === "alipay-aipay" || req.network === ALIPAY_RAIL) return ALIPAY_RAIL;
|
|
1386
|
+
if (req.scheme === "wechatpay-native" || req.network === WECHAT_RAIL) return WECHAT_RAIL;
|
|
1387
|
+
if (req.scheme === BALANCE_RAIL || req.network === BALANCE_RAIL) return BALANCE_RAIL;
|
|
1191
1388
|
return networkToChainName(req.network) ?? req.network;
|
|
1192
1389
|
}
|
|
1193
1390
|
function findRail(accepts, rail) {
|
|
@@ -1227,6 +1424,14 @@ function selectRail(input) {
|
|
|
1227
1424
|
return { rail: railOf(serverAccepts[0]), requirement: serverAccepts[0] };
|
|
1228
1425
|
}
|
|
1229
1426
|
|
|
1427
|
+
// src/facilitators/balance/auth.ts
|
|
1428
|
+
var import_ethers2 = require("ethers");
|
|
1429
|
+
var BALANCE_AUTH_DOMAIN = "moltspay-balance-auth:v1";
|
|
1430
|
+
var BALANCE_AUTH_MAX_SKEW_MS = 5 * 60 * 1e3;
|
|
1431
|
+
function buildDeductMessage(f) {
|
|
1432
|
+
return [BALANCE_AUTH_DOMAIN, "balance-deduct", f.buyerId, f.requestId, f.service, String(f.timestamp)].join("\n");
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1230
1435
|
// src/client/node/index.ts
|
|
1231
1436
|
var DEFAULT_CONFIG = {
|
|
1232
1437
|
chain: "base",
|
|
@@ -1253,7 +1458,7 @@ var MoltsPayClient = class {
|
|
|
1253
1458
|
this.walletData = this.loadWallet();
|
|
1254
1459
|
this.loadSpending();
|
|
1255
1460
|
if (this.walletData) {
|
|
1256
|
-
this.wallet = new
|
|
1461
|
+
this.wallet = new import_ethers3.Wallet(this.walletData.privateKey);
|
|
1257
1462
|
const configDir = this.configDir;
|
|
1258
1463
|
this.signer = new NodeSigner(this.wallet, {
|
|
1259
1464
|
getSolanaKeypair: () => loadSolanaWallet(configDir)
|
|
@@ -1330,6 +1535,12 @@ var MoltsPayClient = class {
|
|
|
1330
1535
|
if (options.rail === ALIPAY_RAIL) {
|
|
1331
1536
|
return this.payViaAlipay(serverUrl, service, params, options);
|
|
1332
1537
|
}
|
|
1538
|
+
if (options.rail === WECHAT_RAIL) {
|
|
1539
|
+
return this.payViaWechat(serverUrl, service, params, options);
|
|
1540
|
+
}
|
|
1541
|
+
if (options.rail === BALANCE_RAIL) {
|
|
1542
|
+
return this.payViaBalance(serverUrl, service, params, options);
|
|
1543
|
+
}
|
|
1333
1544
|
if (!this.wallet || !this.walletData) {
|
|
1334
1545
|
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
1335
1546
|
}
|
|
@@ -1591,6 +1802,399 @@ Please specify: --chain <chain_name>`
|
|
|
1591
1802
|
return { body: result.body, payment: result.payment, media: result.media };
|
|
1592
1803
|
}
|
|
1593
1804
|
}
|
|
1805
|
+
/**
|
|
1806
|
+
* Pay for a service over the WeChat Pay Native fiat rail (2.1.0).
|
|
1807
|
+
*
|
|
1808
|
+
* Mirrors {@link payViaAlipay} and needs no EVM wallet. The server placed the
|
|
1809
|
+
* Native order when it built the 402, so its `wechatpay-native` accepts[]
|
|
1810
|
+
* entry already carries `extra.code_url` + `extra.out_trade_no`. Flow: hit the
|
|
1811
|
+
* resource to get the 402, confirm the server offers the wechat rail, surface
|
|
1812
|
+
* the code_url (caller renders a QR), then poll the resource with the
|
|
1813
|
+
* out_trade_no proof until the server verifies the order paid and delivers.
|
|
1814
|
+
*/
|
|
1815
|
+
async payViaWechat(serverUrl, service, params, options) {
|
|
1816
|
+
const session = await this.startWechatPayment(serverUrl, service, params, options);
|
|
1817
|
+
const wechat = new WechatClient({ configDir: this.configDir });
|
|
1818
|
+
const result = await wechat.pollSession(session.paymentSessionId, {
|
|
1819
|
+
timeoutMs: options.timeoutMs,
|
|
1820
|
+
signal: options.signal
|
|
1821
|
+
});
|
|
1822
|
+
if (result.status !== "completed") {
|
|
1823
|
+
throw new Error(result.lastError || `WeChat payment ended with status ${result.status}`);
|
|
1824
|
+
}
|
|
1825
|
+
try {
|
|
1826
|
+
const json = JSON.parse(result.resultBody ?? "");
|
|
1827
|
+
return json.result ?? json;
|
|
1828
|
+
} catch {
|
|
1829
|
+
return { body: result.resultBody ?? "" };
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* The ethers wallet used to sign balance-rail deductions: the client's EVM
|
|
1834
|
+
* wallet if it has one, else a per-configDir identity key persisted at
|
|
1835
|
+
* `<configDir>/balance-identity.key` (0600) so a balance-only client works
|
|
1836
|
+
* without a full crypto wallet. Under `agent 统一代付`, one key spends every
|
|
1837
|
+
* account the agent tops up.
|
|
1838
|
+
*/
|
|
1839
|
+
balanceSigner() {
|
|
1840
|
+
if (this.wallet) return this.wallet;
|
|
1841
|
+
const p = (0, import_path3.join)(this.configDir, "balance-identity.key");
|
|
1842
|
+
let pk;
|
|
1843
|
+
if ((0, import_fs2.existsSync)(p)) {
|
|
1844
|
+
pk = (0, import_fs2.readFileSync)(p, "utf-8").trim();
|
|
1845
|
+
} else {
|
|
1846
|
+
(0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
|
|
1847
|
+
pk = import_ethers3.Wallet.createRandom().privateKey;
|
|
1848
|
+
(0, import_fs2.writeFileSync)(p, pk, { mode: 384 });
|
|
1849
|
+
}
|
|
1850
|
+
return new import_ethers3.Wallet(pk);
|
|
1851
|
+
}
|
|
1852
|
+
/** The balance-rail spending signer address (lowercase 0x…). Stable per
|
|
1853
|
+
* configDir; this is the identity the server TOFU-binds and later verifies. */
|
|
1854
|
+
getBalanceSignerAddress() {
|
|
1855
|
+
return this.balanceSigner().address.toLowerCase();
|
|
1856
|
+
}
|
|
1857
|
+
/** The buyer id for the balance rail: explicit option > persisted config. */
|
|
1858
|
+
resolveBuyerId(explicit) {
|
|
1859
|
+
const buyerId = explicit ?? this.config.buyerId;
|
|
1860
|
+
if (!buyerId) {
|
|
1861
|
+
throw new Error(
|
|
1862
|
+
"Balance rail needs a buyer id. Pass { buyerId } or persist one with setBuyerId()."
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
return buyerId;
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* Pay via the custodial balance rail (2.2.0, password-free).
|
|
1869
|
+
*
|
|
1870
|
+
* No wallet, no QR: the request carries `{buyer_id, request_id}` in the
|
|
1871
|
+
* X-Payment payload and the server deducts the prepaid balance atomically
|
|
1872
|
+
* before running the skill. The client-generated `request_id` makes the
|
|
1873
|
+
* charge idempotent — a network retry can never double-deduct.
|
|
1874
|
+
*/
|
|
1875
|
+
async payViaBalance(serverUrl, service, params, options) {
|
|
1876
|
+
const buyerId = this.resolveBuyerId(options.buyerId);
|
|
1877
|
+
let attempt = await this.balanceDeduct(serverUrl, service, params, options, buyerId);
|
|
1878
|
+
if (attempt.ok) return attempt.result;
|
|
1879
|
+
const fundable = attempt.status === 402 && (attempt.code === "buyer_not_found" || attempt.code === "insufficient_balance" || /insufficient balance|unknown buyer|top up first/i.test(attempt.error || ""));
|
|
1880
|
+
if (!fundable || options.autoTopup === false) {
|
|
1881
|
+
throw new Error(attempt.error || `Balance payment failed with HTTP ${attempt.status}`);
|
|
1882
|
+
}
|
|
1883
|
+
if (options.topupMode === "manual") {
|
|
1884
|
+
const order = await this.createBalanceTopupOrder(serverUrl, {
|
|
1885
|
+
pack: options.topupPack,
|
|
1886
|
+
buyerId,
|
|
1887
|
+
context: { service }
|
|
1888
|
+
});
|
|
1889
|
+
options.onTopupRequired?.(order.pack, order.codeUrl);
|
|
1890
|
+
return {
|
|
1891
|
+
status: "topup_required",
|
|
1892
|
+
out_trade_no: order.outTradeNo,
|
|
1893
|
+
code_url: order.codeUrl,
|
|
1894
|
+
pack: order.pack,
|
|
1895
|
+
server_url: serverUrl
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
const credited = await this.topupBalancePack(serverUrl, {
|
|
1899
|
+
pack: options.topupPack,
|
|
1900
|
+
buyerId,
|
|
1901
|
+
pollIntervalMs: options.topupPollIntervalMs,
|
|
1902
|
+
signal: options.signal,
|
|
1903
|
+
onCodeUrl: (pack, codeUrl) => options.onTopupRequired?.(pack, codeUrl)
|
|
1904
|
+
});
|
|
1905
|
+
options.onTopupCredited?.(credited.balance);
|
|
1906
|
+
attempt = await this.balanceDeduct(serverUrl, service, params, options, buyerId);
|
|
1907
|
+
if (attempt.ok) return attempt.result;
|
|
1908
|
+
throw new Error(attempt.error || "Balance payment failed after top-up");
|
|
1909
|
+
}
|
|
1910
|
+
/** One password-free deduct attempt. Never throws on an HTTP error; the
|
|
1911
|
+
* caller inspects `{ ok, status, error }` to decide whether to auto-fund. */
|
|
1912
|
+
async balanceDeduct(serverUrl, service, params, options, buyerId) {
|
|
1913
|
+
let executeUrl = `${serverUrl}/execute`;
|
|
1914
|
+
try {
|
|
1915
|
+
const services = await this.getServices(serverUrl);
|
|
1916
|
+
const svc = services.services?.find((s) => s.id === service);
|
|
1917
|
+
if (svc?.endpoint) executeUrl = `${serverUrl}${svc.endpoint}`;
|
|
1918
|
+
} catch {
|
|
1919
|
+
}
|
|
1920
|
+
const requestBody = options.rawData ? { service, ...params } : { service, params };
|
|
1921
|
+
const requestId = (0, import_node_crypto3.randomUUID)();
|
|
1922
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
1923
|
+
const signature = await this.balanceSigner().signMessage(
|
|
1924
|
+
buildDeductMessage({ buyerId, requestId, service, timestamp })
|
|
1925
|
+
);
|
|
1926
|
+
const xPayment = Buffer.from(JSON.stringify({
|
|
1927
|
+
x402Version: 2,
|
|
1928
|
+
scheme: BALANCE_RAIL,
|
|
1929
|
+
network: BALANCE_RAIL,
|
|
1930
|
+
payload: { buyer_id: buyerId, request_id: requestId, auth: { timestamp, signature } }
|
|
1931
|
+
})).toString("base64");
|
|
1932
|
+
const res = await fetch(executeUrl, {
|
|
1933
|
+
method: "POST",
|
|
1934
|
+
headers: { "Content-Type": "application/json", "X-Payment": xPayment },
|
|
1935
|
+
body: JSON.stringify(requestBody),
|
|
1936
|
+
signal: options.signal
|
|
1937
|
+
});
|
|
1938
|
+
const data = await res.json().catch(() => ({}));
|
|
1939
|
+
if (!res.ok) return { ok: false, status: res.status, error: data.error, code: data.code };
|
|
1940
|
+
return { ok: true, status: res.status, result: data.result ?? data };
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Non-blocking: POST /balance/topup/order, persist a recoverable session, and
|
|
1944
|
+
* return at once (no polling). Use with {@link confirmBalanceTopup} for
|
|
1945
|
+
* turn-based agents; the blocking {@link topupBalancePack} is built on this.
|
|
1946
|
+
*/
|
|
1947
|
+
async createBalanceTopupOrder(serverUrl, opts = {}) {
|
|
1948
|
+
const id = this.resolveBuyerId(opts.buyerId);
|
|
1949
|
+
const orderRes = await fetch(`${serverUrl}/balance/topup/order`, {
|
|
1950
|
+
method: "POST",
|
|
1951
|
+
headers: { "Content-Type": "application/json" },
|
|
1952
|
+
// Carry the spending signer so the server binds it to the account on
|
|
1953
|
+
// confirm (topup-time binding). The same key signs deductions later.
|
|
1954
|
+
body: JSON.stringify({ buyer_id: id, pack: opts.pack, signer_address: this.getBalanceSignerAddress() })
|
|
1955
|
+
});
|
|
1956
|
+
const order = await orderRes.json().catch(() => ({}));
|
|
1957
|
+
if (!orderRes.ok) throw new Error(order.error || `Top-up order failed with HTTP ${orderRes.status}`);
|
|
1958
|
+
const maxTimeoutSeconds = order.max_timeout_seconds ?? 300;
|
|
1959
|
+
const now = Date.now();
|
|
1960
|
+
this.saveBalanceTopupSession({
|
|
1961
|
+
out_trade_no: order.out_trade_no,
|
|
1962
|
+
buyer_id: id,
|
|
1963
|
+
pack: order.pack,
|
|
1964
|
+
server_url: serverUrl,
|
|
1965
|
+
code_url: order.code_url,
|
|
1966
|
+
status: "pending",
|
|
1967
|
+
created_at: new Date(now).toISOString(),
|
|
1968
|
+
expires_at: new Date(now + maxTimeoutSeconds * 1e3).toISOString(),
|
|
1969
|
+
context: opts.context
|
|
1970
|
+
});
|
|
1971
|
+
return { outTradeNo: order.out_trade_no, codeUrl: order.code_url, pack: order.pack, maxTimeoutSeconds };
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* One-shot: POST /balance/topup/confirm for a single order. No polling.
|
|
1975
|
+
* Updates the persisted session on credit. `serverUrl` defaults to the one
|
|
1976
|
+
* recorded in the session (recover by out_trade_no alone).
|
|
1977
|
+
*/
|
|
1978
|
+
async confirmBalanceTopup(outTradeNo, opts = {}) {
|
|
1979
|
+
const session = this.getBalanceTopupSession(outTradeNo);
|
|
1980
|
+
const serverUrl = opts.serverUrl || session?.server_url;
|
|
1981
|
+
if (!serverUrl) {
|
|
1982
|
+
return { credited: false, reason: `No server URL for ${outTradeNo}: pass serverUrl or run topup-order first` };
|
|
1983
|
+
}
|
|
1984
|
+
const confRes = await fetch(`${serverUrl}/balance/topup/confirm`, {
|
|
1985
|
+
method: "POST",
|
|
1986
|
+
headers: { "Content-Type": "application/json" },
|
|
1987
|
+
body: JSON.stringify({ out_trade_no: outTradeNo })
|
|
1988
|
+
});
|
|
1989
|
+
const conf = await confRes.json().catch(() => ({}));
|
|
1990
|
+
if (!confRes.ok) return { credited: false, reason: conf.error || `Confirm failed with HTTP ${confRes.status}` };
|
|
1991
|
+
if (conf.credited) {
|
|
1992
|
+
if (session) {
|
|
1993
|
+
session.status = "credited";
|
|
1994
|
+
session.tx_id = conf.tx_id;
|
|
1995
|
+
session.balance = conf.balance;
|
|
1996
|
+
this.saveBalanceTopupSession(session);
|
|
1997
|
+
}
|
|
1998
|
+
return { credited: true, balance: conf.balance, txId: conf.tx_id };
|
|
1999
|
+
}
|
|
2000
|
+
return { credited: false, pending: !!conf.pending, reason: conf.reason };
|
|
2001
|
+
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Blocking terminal wrapper: create the order then poll confirm until the
|
|
2004
|
+
* scan is credited (or the order expires). Built on
|
|
2005
|
+
* {@link createBalanceTopupOrder} + {@link confirmBalanceTopup}.
|
|
2006
|
+
*/
|
|
2007
|
+
async topupBalancePack(serverUrl, opts = {}) {
|
|
2008
|
+
const order = await this.createBalanceTopupOrder(serverUrl, { pack: opts.pack, buyerId: opts.buyerId });
|
|
2009
|
+
opts.onCodeUrl?.(order.pack, order.codeUrl);
|
|
2010
|
+
const interval = opts.pollIntervalMs ?? 2e3;
|
|
2011
|
+
const deadline = Date.now() + order.maxTimeoutSeconds * 1e3;
|
|
2012
|
+
while (Date.now() < deadline) {
|
|
2013
|
+
if (opts.signal?.aborted) throw new Error("Top-up aborted");
|
|
2014
|
+
const conf = await this.confirmBalanceTopup(order.outTradeNo, { serverUrl });
|
|
2015
|
+
if (conf.credited) return { balance: conf.balance, outTradeNo: order.outTradeNo, txId: conf.txId };
|
|
2016
|
+
await this.sleep(interval, opts.signal);
|
|
2017
|
+
}
|
|
2018
|
+
throw new Error("Top-up timed out before the payment was confirmed");
|
|
2019
|
+
}
|
|
2020
|
+
// --- Recoverable balance top-up sessions (<configDir>/balance-topup-sessions) ---
|
|
2021
|
+
balanceTopupSessionDir() {
|
|
2022
|
+
return (0, import_path3.join)(this.configDir, "balance-topup-sessions");
|
|
2023
|
+
}
|
|
2024
|
+
saveBalanceTopupSession(session) {
|
|
2025
|
+
const dir = this.balanceTopupSessionDir();
|
|
2026
|
+
(0, import_fs2.mkdirSync)(dir, { recursive: true });
|
|
2027
|
+
(0, import_fs2.writeFileSync)((0, import_path3.join)(dir, `${session.out_trade_no}.json`), JSON.stringify(session, null, 2));
|
|
2028
|
+
}
|
|
2029
|
+
/** Read a persisted top-up session by out_trade_no, or null. */
|
|
2030
|
+
getBalanceTopupSession(outTradeNo) {
|
|
2031
|
+
const p = (0, import_path3.join)(this.balanceTopupSessionDir(), `${outTradeNo}.json`);
|
|
2032
|
+
if (!(0, import_fs2.existsSync)(p)) return null;
|
|
2033
|
+
try {
|
|
2034
|
+
return JSON.parse((0, import_fs2.readFileSync)(p, "utf-8"));
|
|
2035
|
+
} catch {
|
|
2036
|
+
return null;
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
/** List persisted top-up sessions, newest first. */
|
|
2040
|
+
listBalanceTopupSessions() {
|
|
2041
|
+
const dir = this.balanceTopupSessionDir();
|
|
2042
|
+
if (!(0, import_fs2.existsSync)(dir)) return [];
|
|
2043
|
+
return (0, import_fs2.readdirSync)(dir).filter((f) => f.endsWith(".json")).map((f) => {
|
|
2044
|
+
try {
|
|
2045
|
+
return JSON.parse((0, import_fs2.readFileSync)((0, import_path3.join)(dir, f), "utf-8"));
|
|
2046
|
+
} catch {
|
|
2047
|
+
return null;
|
|
2048
|
+
}
|
|
2049
|
+
}).filter((s) => s !== null).sort((a, b) => (b.created_at || "").localeCompare(a.created_at || ""));
|
|
2050
|
+
}
|
|
2051
|
+
/** Abortable sleep used by the top-up poll loop. */
|
|
2052
|
+
sleep(ms, signal) {
|
|
2053
|
+
return new Promise((resolve, reject) => {
|
|
2054
|
+
if (signal?.aborted) return reject(new Error("aborted"));
|
|
2055
|
+
const t = setTimeout(resolve, ms);
|
|
2056
|
+
signal?.addEventListener("abort", () => {
|
|
2057
|
+
clearTimeout(t);
|
|
2058
|
+
reject(new Error("aborted"));
|
|
2059
|
+
}, { once: true });
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
/** Persist the buyer id used by the balance rail (bearer semantics). */
|
|
2063
|
+
setBuyerId(buyerId) {
|
|
2064
|
+
this.config.buyerId = buyerId;
|
|
2065
|
+
this.saveConfig();
|
|
2066
|
+
}
|
|
2067
|
+
/** GET /balance — custodial balance, limits, and today's spend for a buyer. */
|
|
2068
|
+
async getBuyerBalance(serverUrl, buyerId) {
|
|
2069
|
+
const id = this.resolveBuyerId(buyerId);
|
|
2070
|
+
const res = await fetch(`${serverUrl}/balance?buyer_id=${encodeURIComponent(id)}`);
|
|
2071
|
+
const data = await res.json().catch(() => ({}));
|
|
2072
|
+
if (!res.ok) throw new Error(data.error || `Balance query failed with HTTP ${res.status}`);
|
|
2073
|
+
return data;
|
|
2074
|
+
}
|
|
2075
|
+
/**
|
|
2076
|
+
* POST /balance/topup — report an externally settled payment (on-chain
|
|
2077
|
+
* tx hash / Alipay trade_no / WeChat out_trade_no) so the server verifies
|
|
2078
|
+
* and credits the ledger. Idempotent per reference.
|
|
2079
|
+
*/
|
|
2080
|
+
async topupBalance(serverUrl, opts) {
|
|
2081
|
+
const id = this.resolveBuyerId(opts.buyerId);
|
|
2082
|
+
const res = await fetch(`${serverUrl}/balance/topup`, {
|
|
2083
|
+
method: "POST",
|
|
2084
|
+
headers: { "Content-Type": "application/json" },
|
|
2085
|
+
body: JSON.stringify({
|
|
2086
|
+
buyer_id: id,
|
|
2087
|
+
rail: opts.rail,
|
|
2088
|
+
amount: opts.amount,
|
|
2089
|
+
tx_hash: opts.txHash,
|
|
2090
|
+
chain: opts.chain,
|
|
2091
|
+
trade_no: opts.tradeNo,
|
|
2092
|
+
out_trade_no: opts.outTradeNo
|
|
2093
|
+
})
|
|
2094
|
+
});
|
|
2095
|
+
const data = await res.json().catch(() => ({}));
|
|
2096
|
+
if (!res.ok) throw new Error(data.error || `Top-up failed with HTTP ${res.status}`);
|
|
2097
|
+
return data;
|
|
2098
|
+
}
|
|
2099
|
+
/** GET /balance/transactions — ledger history for a buyer, newest first. */
|
|
2100
|
+
async listBalanceTransactions(serverUrl, opts = {}) {
|
|
2101
|
+
const id = this.resolveBuyerId(opts.buyerId);
|
|
2102
|
+
const qs = new URLSearchParams({ buyer_id: id });
|
|
2103
|
+
if (opts.limit) qs.set("limit", String(opts.limit));
|
|
2104
|
+
if (opts.offset) qs.set("offset", String(opts.offset));
|
|
2105
|
+
const res = await fetch(`${serverUrl}/balance/transactions?${qs}`);
|
|
2106
|
+
const data = await res.json().catch(() => ({}));
|
|
2107
|
+
if (!res.ok) throw new Error(data.error || `Transaction query failed with HTTP ${res.status}`);
|
|
2108
|
+
return data;
|
|
2109
|
+
}
|
|
2110
|
+
/**
|
|
2111
|
+
* Start a recoverable WeChat Pay Native session and return immediately with
|
|
2112
|
+
* QR metadata. The SDK client persists enough context to poll/fulfill later.
|
|
2113
|
+
*/
|
|
2114
|
+
async startWechatPayment(serverUrl, service, params, options = {}) {
|
|
2115
|
+
let executeUrl = `${serverUrl}/execute`;
|
|
2116
|
+
try {
|
|
2117
|
+
const services = await this.getServices(serverUrl);
|
|
2118
|
+
const svc = services.services?.find((s) => s.id === service);
|
|
2119
|
+
if (svc?.endpoint) executeUrl = `${serverUrl}${svc.endpoint}`;
|
|
2120
|
+
} catch {
|
|
2121
|
+
}
|
|
2122
|
+
const requestBody = options.rawData ? { service, ...params } : { service, params };
|
|
2123
|
+
const bodyJson = JSON.stringify(requestBody);
|
|
2124
|
+
const res = await fetch(executeUrl, {
|
|
2125
|
+
method: "POST",
|
|
2126
|
+
headers: { "Content-Type": "application/json" },
|
|
2127
|
+
body: bodyJson
|
|
2128
|
+
});
|
|
2129
|
+
if (res.status !== 402) {
|
|
2130
|
+
const data = await res.json().catch(() => ({}));
|
|
2131
|
+
if (res.ok && data.result) return data.result;
|
|
2132
|
+
throw new Error(data.error || `Expected 402, got ${res.status}`);
|
|
2133
|
+
}
|
|
2134
|
+
const header = res.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
2135
|
+
if (!header) throw new Error("Missing x-payment-required header on 402");
|
|
2136
|
+
const parsed = JSON.parse(Buffer.from(header, "base64").toString("utf-8"));
|
|
2137
|
+
const accepts = Array.isArray(parsed) ? parsed : parsed.accepts ?? [parsed];
|
|
2138
|
+
const { requirement } = selectRail({
|
|
2139
|
+
serverAccepts: accepts,
|
|
2140
|
+
explicitRail: WECHAT_RAIL,
|
|
2141
|
+
preference: this.railPreference,
|
|
2142
|
+
availability: { evmReady: this.isInitialized }
|
|
2143
|
+
});
|
|
2144
|
+
const wechat = new WechatClient({ configDir: this.configDir });
|
|
2145
|
+
const session = wechat.start402({
|
|
2146
|
+
resourceUrl: executeUrl,
|
|
2147
|
+
requirement,
|
|
2148
|
+
method: "POST",
|
|
2149
|
+
data: bodyJson,
|
|
2150
|
+
onPaymentPending: options.onPaymentPending ? (info) => options.onPaymentPending({ paymentUrl: info.codeUrl, tradeNo: info.outTradeNo }) : void 0,
|
|
2151
|
+
timeoutMs: options.timeoutMs,
|
|
2152
|
+
signal: options.signal,
|
|
2153
|
+
context: {
|
|
2154
|
+
serverUrl,
|
|
2155
|
+
service,
|
|
2156
|
+
params,
|
|
2157
|
+
rawData: options.rawData ?? false,
|
|
2158
|
+
rail: WECHAT_RAIL
|
|
2159
|
+
}
|
|
2160
|
+
});
|
|
2161
|
+
if (options.autoPoll || options.onWechatPaymentCompleted || options.onWechatPaymentFailed) {
|
|
2162
|
+
void wechat.pollSession(session.paymentSessionId, {
|
|
2163
|
+
timeoutMs: options.timeoutMs,
|
|
2164
|
+
signal: options.signal
|
|
2165
|
+
}).then(async (finalSession) => {
|
|
2166
|
+
if (finalSession.status === "completed") {
|
|
2167
|
+
await options.onWechatPaymentCompleted?.(finalSession);
|
|
2168
|
+
} else {
|
|
2169
|
+
await options.onWechatPaymentFailed?.(finalSession);
|
|
2170
|
+
}
|
|
2171
|
+
}).catch(async (error) => {
|
|
2172
|
+
const failed = await wechat.fulfill(session.paymentSessionId).catch(() => ({
|
|
2173
|
+
...session,
|
|
2174
|
+
status: "failed",
|
|
2175
|
+
lastError: error instanceof Error ? error.message : String(error)
|
|
2176
|
+
}));
|
|
2177
|
+
await options.onWechatPaymentFailed?.(failed);
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
return session;
|
|
2181
|
+
}
|
|
2182
|
+
/** Query a persisted WeChat session once. */
|
|
2183
|
+
async getWechatPaymentStatus(identifier) {
|
|
2184
|
+
return new WechatClient({ configDir: this.configDir }).status(identifier);
|
|
2185
|
+
}
|
|
2186
|
+
/** Idempotently fulfill a paid WeChat session, returning stored or fetched result. */
|
|
2187
|
+
async fulfillWechatPayment(identifier) {
|
|
2188
|
+
return new WechatClient({ configDir: this.configDir }).fulfill(identifier);
|
|
2189
|
+
}
|
|
2190
|
+
/** Mark a local WeChat session as cancelled. */
|
|
2191
|
+
cancelWechatPayment(identifier) {
|
|
2192
|
+
return new WechatClient({ configDir: this.configDir }).cancel(identifier);
|
|
2193
|
+
}
|
|
2194
|
+
/** List persisted WeChat sessions, newest first. */
|
|
2195
|
+
listWechatPaymentSessions() {
|
|
2196
|
+
return new WechatClient({ configDir: this.configDir }).listSessions();
|
|
2197
|
+
}
|
|
1594
2198
|
/**
|
|
1595
2199
|
* Handle MPP (Machine Payments Protocol) payment flow
|
|
1596
2200
|
* Called when pay() detects WWW-Authenticate header in 402 response
|
|
@@ -1686,14 +2290,14 @@ Please specify: --chain <chain_name>`
|
|
|
1686
2290
|
async handleBNBPayment(executeUrl, service, params, paymentDetails, options = {}) {
|
|
1687
2291
|
const { to, amount, token, chainName, chain, spender } = paymentDetails;
|
|
1688
2292
|
const tokenConfig = chain.tokens[token];
|
|
1689
|
-
const provider = new
|
|
2293
|
+
const provider = new import_ethers3.ethers.JsonRpcProvider(chain.rpc);
|
|
1690
2294
|
const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
|
|
1691
2295
|
const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
|
|
1692
2296
|
if (allowance < amountWeiCheck) {
|
|
1693
2297
|
const nativeBalance = await provider.getBalance(this.wallet.address);
|
|
1694
|
-
const minGasBalance =
|
|
2298
|
+
const minGasBalance = import_ethers3.ethers.parseEther("0.0005");
|
|
1695
2299
|
if (nativeBalance < minGasBalance) {
|
|
1696
|
-
const nativeBNB = parseFloat(
|
|
2300
|
+
const nativeBNB = parseFloat(import_ethers3.ethers.formatEther(nativeBalance)).toFixed(4);
|
|
1697
2301
|
const isTestnet = chainName === "bnb_testnet";
|
|
1698
2302
|
if (isTestnet) {
|
|
1699
2303
|
throw new Error(
|
|
@@ -1866,7 +2470,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1866
2470
|
* Check ERC20 allowance for a spender
|
|
1867
2471
|
*/
|
|
1868
2472
|
async checkAllowance(tokenAddress, spender, provider) {
|
|
1869
|
-
const contract = new
|
|
2473
|
+
const contract = new import_ethers3.ethers.Contract(
|
|
1870
2474
|
tokenAddress,
|
|
1871
2475
|
["function allowance(address owner, address spender) view returns (uint256)"],
|
|
1872
2476
|
provider
|
|
@@ -1885,7 +2489,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1885
2489
|
async signEIP3009(to, amount, chain, token = "USDC", domainOverride) {
|
|
1886
2490
|
const tokenConfig = chain.tokens[token];
|
|
1887
2491
|
const value = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
|
|
1888
|
-
const nonce =
|
|
2492
|
+
const nonce = import_ethers3.ethers.hexlify(import_ethers3.ethers.randomBytes(32));
|
|
1889
2493
|
const tokenName = domainOverride?.name || tokenConfig.eip712Name || (token === "USDC" ? "USD Coin" : "Tether USD");
|
|
1890
2494
|
const tokenVersion = domainOverride?.version || "2";
|
|
1891
2495
|
const envelope = buildEIP3009TypedData({
|
|
@@ -2003,7 +2607,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
2003
2607
|
*/
|
|
2004
2608
|
static init(configDir, options) {
|
|
2005
2609
|
(0, import_fs2.mkdirSync)(configDir, { recursive: true });
|
|
2006
|
-
const wallet =
|
|
2610
|
+
const wallet = import_ethers3.Wallet.createRandom();
|
|
2007
2611
|
const walletData = {
|
|
2008
2612
|
address: wallet.address,
|
|
2009
2613
|
privateKey: wallet.privateKey,
|
|
@@ -2035,17 +2639,17 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
2035
2639
|
} catch {
|
|
2036
2640
|
throw new Error(`Unknown chain: ${this.config.chain}`);
|
|
2037
2641
|
}
|
|
2038
|
-
const provider = new
|
|
2642
|
+
const provider = new import_ethers3.ethers.JsonRpcProvider(chain.rpc);
|
|
2039
2643
|
const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
2040
2644
|
const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
|
|
2041
2645
|
provider.getBalance(this.wallet.address),
|
|
2042
|
-
new
|
|
2043
|
-
new
|
|
2646
|
+
new import_ethers3.ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
2647
|
+
new import_ethers3.ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
|
|
2044
2648
|
]);
|
|
2045
2649
|
return {
|
|
2046
|
-
usdc: parseFloat(
|
|
2047
|
-
usdt: parseFloat(
|
|
2048
|
-
native: parseFloat(
|
|
2650
|
+
usdc: parseFloat(import_ethers3.ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
|
|
2651
|
+
usdt: parseFloat(import_ethers3.ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
|
|
2652
|
+
native: parseFloat(import_ethers3.ethers.formatEther(nativeBalance))
|
|
2049
2653
|
};
|
|
2050
2654
|
}
|
|
2051
2655
|
/**
|
|
@@ -2068,38 +2672,38 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
2068
2672
|
supportedChains.map(async (chainName) => {
|
|
2069
2673
|
try {
|
|
2070
2674
|
const chain = getChain(chainName);
|
|
2071
|
-
const provider = new
|
|
2675
|
+
const provider = new import_ethers3.ethers.JsonRpcProvider(chain.rpc);
|
|
2072
2676
|
if (chainName === "tempo_moderato") {
|
|
2073
2677
|
const [nativeBalance, pathUSD, alphaUSD, betaUSD, thetaUSD] = await Promise.all([
|
|
2074
2678
|
provider.getBalance(this.wallet.address),
|
|
2075
|
-
new
|
|
2076
|
-
new
|
|
2077
|
-
new
|
|
2078
|
-
new
|
|
2679
|
+
new import_ethers3.ethers.Contract(tempoTokens.pathUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
2680
|
+
new import_ethers3.ethers.Contract(tempoTokens.alphaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
2681
|
+
new import_ethers3.ethers.Contract(tempoTokens.betaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
2682
|
+
new import_ethers3.ethers.Contract(tempoTokens.thetaUSD, tokenAbi, provider).balanceOf(this.wallet.address)
|
|
2079
2683
|
]);
|
|
2080
2684
|
results[chainName] = {
|
|
2081
|
-
usdc: parseFloat(
|
|
2685
|
+
usdc: parseFloat(import_ethers3.ethers.formatUnits(pathUSD, 6)),
|
|
2082
2686
|
// pathUSD as default USDC
|
|
2083
|
-
usdt: parseFloat(
|
|
2687
|
+
usdt: parseFloat(import_ethers3.ethers.formatUnits(alphaUSD, 6)),
|
|
2084
2688
|
// alphaUSD as default USDT
|
|
2085
|
-
native: parseFloat(
|
|
2689
|
+
native: parseFloat(import_ethers3.ethers.formatEther(nativeBalance)),
|
|
2086
2690
|
tempo: {
|
|
2087
|
-
pathUSD: parseFloat(
|
|
2088
|
-
alphaUSD: parseFloat(
|
|
2089
|
-
betaUSD: parseFloat(
|
|
2090
|
-
thetaUSD: parseFloat(
|
|
2691
|
+
pathUSD: parseFloat(import_ethers3.ethers.formatUnits(pathUSD, 6)),
|
|
2692
|
+
alphaUSD: parseFloat(import_ethers3.ethers.formatUnits(alphaUSD, 6)),
|
|
2693
|
+
betaUSD: parseFloat(import_ethers3.ethers.formatUnits(betaUSD, 6)),
|
|
2694
|
+
thetaUSD: parseFloat(import_ethers3.ethers.formatUnits(thetaUSD, 6))
|
|
2091
2695
|
}
|
|
2092
2696
|
};
|
|
2093
2697
|
} else {
|
|
2094
2698
|
const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
|
|
2095
2699
|
provider.getBalance(this.wallet.address),
|
|
2096
|
-
new
|
|
2097
|
-
new
|
|
2700
|
+
new import_ethers3.ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
2701
|
+
new import_ethers3.ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
|
|
2098
2702
|
]);
|
|
2099
2703
|
results[chainName] = {
|
|
2100
|
-
usdc: parseFloat(
|
|
2101
|
-
usdt: parseFloat(
|
|
2102
|
-
native: parseFloat(
|
|
2704
|
+
usdc: parseFloat(import_ethers3.ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
|
|
2705
|
+
usdt: parseFloat(import_ethers3.ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
|
|
2706
|
+
native: parseFloat(import_ethers3.ethers.formatEther(nativeBalance))
|
|
2103
2707
|
};
|
|
2104
2708
|
}
|
|
2105
2709
|
} catch (err) {
|
|
@@ -2227,6 +2831,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
2227
2831
|
};
|
|
2228
2832
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2229
2833
|
0 && (module.exports = {
|
|
2230
|
-
MoltsPayClient
|
|
2834
|
+
MoltsPayClient,
|
|
2835
|
+
WechatClient
|
|
2231
2836
|
});
|
|
2232
2837
|
//# sourceMappingURL=index.js.map
|