moltspay 1.5.0 → 2.0.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 +143 -0
- package/dist/cdp/index.js.map +1 -1
- package/dist/cdp/index.mjs.map +1 -1
- package/dist/{cdp-DeohBe1o.d.ts → cdp-B5PhDUTC.d.ts} +1 -1
- package/dist/{cdp-p_eHuQpb.d.mts → cdp-D-5Hg3y_.d.mts} +1 -1
- package/dist/chains/index.d.mts +37 -1
- package/dist/chains/index.d.ts +37 -1
- package/dist/chains/index.js +22 -0
- package/dist/chains/index.js.map +1 -1
- package/dist/chains/index.mjs +19 -0
- package/dist/chains/index.mjs.map +1 -1
- package/dist/cli/index.js +1941 -204
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1938 -201
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.d.mts +46 -0
- package/dist/client/index.d.ts +46 -0
- package/dist/client/index.js +1059 -118
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +1055 -116
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/web/index.d.mts +434 -0
- package/dist/client/web/index.mjs +1300 -0
- package/dist/client/web/index.mjs.map +1 -0
- package/dist/facilitators/index.d.mts +185 -6
- package/dist/facilitators/index.d.ts +185 -6
- package/dist/facilitators/index.js +497 -13
- package/dist/facilitators/index.js.map +1 -1
- package/dist/facilitators/index.mjs +492 -13
- package/dist/facilitators/index.mjs.map +1 -1
- package/dist/index.d.mts +77 -2
- package/dist/index.d.ts +77 -2
- package/dist/index.js +1865 -172
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1854 -167
- package/dist/index.mjs.map +1 -1
- package/dist/mcp/index.js +1062 -123
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/index.mjs +1059 -120
- package/dist/mcp/index.mjs.map +1 -1
- package/dist/{registry-OsEO2dOu.d.mts → registry-DIo0WNoO.d.mts} +8 -1
- package/dist/{registry-OsEO2dOu.d.ts → registry-DIo0WNoO.d.ts} +8 -1
- package/dist/server/index.d.mts +137 -2
- package/dist/server/index.d.ts +137 -2
- package/dist/server/index.js +757 -30
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +757 -30
- 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 +15 -2
- package/schemas/moltspay.services.schema.json +100 -6
- package/scripts/postinstall.js +91 -0
package/dist/mcp/index.js
CHANGED
|
@@ -29,15 +29,15 @@ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
|
29
29
|
|
|
30
30
|
// src/mcp/server.ts
|
|
31
31
|
var import_fs3 = require("fs");
|
|
32
|
-
var
|
|
32
|
+
var import_path4 = require("path");
|
|
33
33
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
34
34
|
var import_zod = require("zod");
|
|
35
35
|
|
|
36
|
-
// src/client/index.ts
|
|
36
|
+
// src/client/node/index.ts
|
|
37
37
|
var import_fs2 = require("fs");
|
|
38
|
-
var
|
|
39
|
-
var
|
|
40
|
-
var
|
|
38
|
+
var import_os3 = require("os");
|
|
39
|
+
var import_path3 = require("path");
|
|
40
|
+
var import_ethers2 = require("ethers");
|
|
41
41
|
|
|
42
42
|
// src/chains/index.ts
|
|
43
43
|
var CHAINS = {
|
|
@@ -278,16 +278,16 @@ function loadSolanaWallet(configDir = DEFAULT_CONFIG_DIR) {
|
|
|
278
278
|
// src/facilitators/solana.ts
|
|
279
279
|
var import_web33 = require("@solana/web3.js");
|
|
280
280
|
var import_spl_token2 = require("@solana/spl-token");
|
|
281
|
-
async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey) {
|
|
281
|
+
async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amount, chain, feePayerPubkey, connection) {
|
|
282
282
|
const chainConfig = SOLANA_CHAINS[chain];
|
|
283
|
-
const
|
|
283
|
+
const conn = connection ?? new import_web33.Connection(chainConfig.rpc, "confirmed");
|
|
284
284
|
const mint = new import_web33.PublicKey(chainConfig.tokens.USDC.mint);
|
|
285
285
|
const actualFeePayer = feePayerPubkey || senderPubkey;
|
|
286
286
|
const senderATA = await (0, import_spl_token2.getAssociatedTokenAddress)(mint, senderPubkey);
|
|
287
287
|
const recipientATA = await (0, import_spl_token2.getAssociatedTokenAddress)(mint, recipientPubkey);
|
|
288
288
|
const transaction = new import_web33.Transaction();
|
|
289
289
|
try {
|
|
290
|
-
await (0, import_spl_token2.getAccount)(
|
|
290
|
+
await (0, import_spl_token2.getAccount)(conn, recipientATA);
|
|
291
291
|
} catch {
|
|
292
292
|
transaction.add(
|
|
293
293
|
(0, import_spl_token2.createAssociatedTokenAccountInstruction)(
|
|
@@ -318,17 +318,915 @@ async function createSolanaPaymentTransaction(senderPubkey, recipientPubkey, amo
|
|
|
318
318
|
// decimals
|
|
319
319
|
)
|
|
320
320
|
);
|
|
321
|
-
const { blockhash, lastValidBlockHeight } = await
|
|
321
|
+
const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash();
|
|
322
322
|
transaction.recentBlockhash = blockhash;
|
|
323
323
|
transaction.feePayer = actualFeePayer;
|
|
324
324
|
return transaction;
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
-
// src/client/index.ts
|
|
328
|
-
var
|
|
327
|
+
// src/client/node/index.ts
|
|
328
|
+
var import_web35 = require("@solana/web3.js");
|
|
329
|
+
|
|
330
|
+
// src/client/core/types.ts
|
|
329
331
|
var X402_VERSION = 2;
|
|
330
332
|
var PAYMENT_REQUIRED_HEADER = "x-payment-required";
|
|
331
333
|
var PAYMENT_HEADER = "x-payment";
|
|
334
|
+
|
|
335
|
+
// src/client/core/chain-map.ts
|
|
336
|
+
var NETWORK_TO_CHAIN = {
|
|
337
|
+
"eip155:8453": "base",
|
|
338
|
+
"eip155:137": "polygon",
|
|
339
|
+
"eip155:84532": "base_sepolia",
|
|
340
|
+
"eip155:42431": "tempo_moderato",
|
|
341
|
+
"eip155:56": "bnb",
|
|
342
|
+
"eip155:97": "bnb_testnet",
|
|
343
|
+
"solana:mainnet": "solana",
|
|
344
|
+
"solana:devnet": "solana_devnet"
|
|
345
|
+
};
|
|
346
|
+
var CHAIN_TO_NETWORK = Object.fromEntries(
|
|
347
|
+
Object.entries(NETWORK_TO_CHAIN).map(([network, chain]) => [chain, network])
|
|
348
|
+
);
|
|
349
|
+
function networkToChainName(network) {
|
|
350
|
+
return NETWORK_TO_CHAIN[network] ?? null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/client/core/base64.ts
|
|
354
|
+
var BufferCtor = globalThis.Buffer;
|
|
355
|
+
|
|
356
|
+
// src/client/core/errors.ts
|
|
357
|
+
var MoltsPayError = class extends Error {
|
|
358
|
+
code;
|
|
359
|
+
constructor(code, message) {
|
|
360
|
+
super(message);
|
|
361
|
+
this.name = "MoltsPayError";
|
|
362
|
+
this.code = code;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// src/client/core/eip3009.ts
|
|
367
|
+
var EIP3009_TYPES = {
|
|
368
|
+
TransferWithAuthorization: [
|
|
369
|
+
{ name: "from", type: "address" },
|
|
370
|
+
{ name: "to", type: "address" },
|
|
371
|
+
{ name: "value", type: "uint256" },
|
|
372
|
+
{ name: "validAfter", type: "uint256" },
|
|
373
|
+
{ name: "validBefore", type: "uint256" },
|
|
374
|
+
{ name: "nonce", type: "bytes32" }
|
|
375
|
+
]
|
|
376
|
+
};
|
|
377
|
+
function buildEIP3009TypedData(args) {
|
|
378
|
+
const validAfter = args.validAfter ?? "0";
|
|
379
|
+
const validBefore = args.validBefore ?? (Math.floor(Date.now() / 1e3) + 3600).toString();
|
|
380
|
+
const authorization = {
|
|
381
|
+
from: args.from,
|
|
382
|
+
to: args.to,
|
|
383
|
+
value: args.value,
|
|
384
|
+
validAfter,
|
|
385
|
+
validBefore,
|
|
386
|
+
nonce: args.nonce
|
|
387
|
+
};
|
|
388
|
+
return {
|
|
389
|
+
domain: {
|
|
390
|
+
name: args.tokenName,
|
|
391
|
+
version: args.tokenVersion,
|
|
392
|
+
chainId: args.chainId,
|
|
393
|
+
verifyingContract: args.tokenAddress
|
|
394
|
+
},
|
|
395
|
+
types: EIP3009_TYPES,
|
|
396
|
+
primaryType: "TransferWithAuthorization",
|
|
397
|
+
message: authorization
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/client/core/bnb-intent.ts
|
|
402
|
+
var BNB_INTENT_TYPES = {
|
|
403
|
+
PaymentIntent: [
|
|
404
|
+
{ name: "from", type: "address" },
|
|
405
|
+
{ name: "to", type: "address" },
|
|
406
|
+
{ name: "amount", type: "uint256" },
|
|
407
|
+
{ name: "token", type: "address" },
|
|
408
|
+
{ name: "service", type: "string" },
|
|
409
|
+
{ name: "nonce", type: "uint256" },
|
|
410
|
+
{ name: "deadline", type: "uint256" }
|
|
411
|
+
]
|
|
412
|
+
};
|
|
413
|
+
var BNB_DOMAIN_NAME = "MoltsPay";
|
|
414
|
+
var BNB_DOMAIN_VERSION = "1";
|
|
415
|
+
function buildBnbIntentTypedData(args) {
|
|
416
|
+
const intent = {
|
|
417
|
+
from: args.from,
|
|
418
|
+
to: args.to,
|
|
419
|
+
amount: args.amount,
|
|
420
|
+
token: args.tokenAddress,
|
|
421
|
+
service: args.service,
|
|
422
|
+
nonce: args.nonce,
|
|
423
|
+
deadline: args.deadline
|
|
424
|
+
};
|
|
425
|
+
return {
|
|
426
|
+
domain: {
|
|
427
|
+
name: BNB_DOMAIN_NAME,
|
|
428
|
+
version: BNB_DOMAIN_VERSION,
|
|
429
|
+
chainId: args.chainId
|
|
430
|
+
},
|
|
431
|
+
types: BNB_INTENT_TYPES,
|
|
432
|
+
primaryType: "PaymentIntent",
|
|
433
|
+
message: intent
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/client/node/signer.ts
|
|
438
|
+
var import_ethers = require("ethers");
|
|
439
|
+
var import_web34 = require("@solana/web3.js");
|
|
440
|
+
var NodeSigner = class {
|
|
441
|
+
evmWallet;
|
|
442
|
+
getSolanaKeypair;
|
|
443
|
+
constructor(evmWallet, options = {}) {
|
|
444
|
+
this.evmWallet = evmWallet;
|
|
445
|
+
this.getSolanaKeypair = options.getSolanaKeypair ?? (() => null);
|
|
446
|
+
}
|
|
447
|
+
async getEvmAddress() {
|
|
448
|
+
return this.evmWallet.address;
|
|
449
|
+
}
|
|
450
|
+
async getSolanaAddress() {
|
|
451
|
+
const kp = this.getSolanaKeypair();
|
|
452
|
+
return kp ? kp.publicKey.toBase58() : null;
|
|
453
|
+
}
|
|
454
|
+
async signTypedData(envelope) {
|
|
455
|
+
const mutableTypes = {};
|
|
456
|
+
for (const [key, fields] of Object.entries(envelope.types)) {
|
|
457
|
+
mutableTypes[key] = [...fields];
|
|
458
|
+
}
|
|
459
|
+
return this.evmWallet.signTypedData(
|
|
460
|
+
envelope.domain,
|
|
461
|
+
mutableTypes,
|
|
462
|
+
envelope.message
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
async sendEvmTransaction(args) {
|
|
466
|
+
const chain = findChainByChainId(args.chainId);
|
|
467
|
+
if (!chain) {
|
|
468
|
+
throw new Error(`sendEvmTransaction: unknown chainId ${args.chainId}`);
|
|
469
|
+
}
|
|
470
|
+
const provider = new import_ethers.ethers.JsonRpcProvider(chain.rpc);
|
|
471
|
+
const connected = this.evmWallet.connect(provider);
|
|
472
|
+
const tx = await connected.sendTransaction({
|
|
473
|
+
to: args.to,
|
|
474
|
+
data: args.data,
|
|
475
|
+
value: args.value ? BigInt(args.value) : 0n
|
|
476
|
+
});
|
|
477
|
+
return tx.hash;
|
|
478
|
+
}
|
|
479
|
+
async signSolanaTransaction(args) {
|
|
480
|
+
const kp = this.getSolanaKeypair();
|
|
481
|
+
if (!kp) {
|
|
482
|
+
throw new Error("signSolanaTransaction: no Solana wallet configured");
|
|
483
|
+
}
|
|
484
|
+
const tx = import_web34.Transaction.from(Buffer.from(args.transactionBase64, "base64"));
|
|
485
|
+
if (args.partialSign) {
|
|
486
|
+
tx.partialSign(kp);
|
|
487
|
+
} else {
|
|
488
|
+
tx.sign(kp);
|
|
489
|
+
}
|
|
490
|
+
return tx.serialize({ requireAllSignatures: false }).toString("base64");
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
function findChainByChainId(chainId) {
|
|
494
|
+
for (const cfg of Object.values(CHAINS)) {
|
|
495
|
+
if (cfg.chainId === chainId) {
|
|
496
|
+
return cfg;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return void 0;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/client/alipay/index.ts
|
|
503
|
+
var import_node_crypto = require("crypto");
|
|
504
|
+
var import_promises = require("fs/promises");
|
|
505
|
+
var import_os2 = require("os");
|
|
506
|
+
var import_path2 = require("path");
|
|
507
|
+
|
|
508
|
+
// src/client/alipay/cli.ts
|
|
509
|
+
var import_child_process = require("child_process");
|
|
510
|
+
|
|
511
|
+
// src/client/alipay/log.ts
|
|
512
|
+
var RANK = { off: 0, info: 1, debug: 2 };
|
|
513
|
+
function normalizeLevel(v) {
|
|
514
|
+
const s = (v ?? "").toLowerCase();
|
|
515
|
+
return s === "info" || s === "debug" ? s : "off";
|
|
516
|
+
}
|
|
517
|
+
var level = normalizeLevel(process.env.MOLTSPAY_ALIPAY_LOG);
|
|
518
|
+
var sink = (line) => {
|
|
519
|
+
process.stderr.write(line + "\n");
|
|
520
|
+
};
|
|
521
|
+
function enabledFor(want) {
|
|
522
|
+
return RANK[level] >= RANK[want];
|
|
523
|
+
}
|
|
524
|
+
function render(event, fields) {
|
|
525
|
+
const parts = ["[moltspay:alipay]", String(fields.ts), event];
|
|
526
|
+
if (fields.flow) parts.push(`flow=${fields.flow}`);
|
|
527
|
+
if (fields.step) parts.push(`step=${fields.step}`);
|
|
528
|
+
if (typeof fields.ms === "number") parts.push(`${fields.ms}ms`);
|
|
529
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
530
|
+
if (["ts", "level", "event", "flow", "step", "ms"].includes(k)) continue;
|
|
531
|
+
parts.push(`${k}=${typeof v === "string" ? v : JSON.stringify(v)}`);
|
|
532
|
+
}
|
|
533
|
+
return parts.join(" ");
|
|
534
|
+
}
|
|
535
|
+
function emit(lvl, event, fields) {
|
|
536
|
+
if (!enabledFor(lvl)) return;
|
|
537
|
+
const full = { ts: (/* @__PURE__ */ new Date()).toISOString(), level: lvl, event, ...fields };
|
|
538
|
+
try {
|
|
539
|
+
sink(render(event, full), full);
|
|
540
|
+
} catch {
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
var alipayLog = {
|
|
544
|
+
info: (event, fields = {}) => emit("info", event, fields),
|
|
545
|
+
debug: (event, fields = {}) => emit("debug", event, fields),
|
|
546
|
+
/** True if anything at all would be logged (cheap guard for hot paths). */
|
|
547
|
+
enabled: () => level !== "off"
|
|
548
|
+
};
|
|
549
|
+
async function timeStep(step, flow, fn) {
|
|
550
|
+
if (!alipayLog.enabled()) return fn();
|
|
551
|
+
const t0 = Date.now();
|
|
552
|
+
alipayLog.debug("step.start", { flow, step });
|
|
553
|
+
try {
|
|
554
|
+
const out = await fn();
|
|
555
|
+
alipayLog.info("step.end", { flow, step, ms: Date.now() - t0, ok: true });
|
|
556
|
+
return out;
|
|
557
|
+
} catch (err2) {
|
|
558
|
+
alipayLog.info("step.end", {
|
|
559
|
+
flow,
|
|
560
|
+
step,
|
|
561
|
+
ms: Date.now() - t0,
|
|
562
|
+
ok: false,
|
|
563
|
+
err: err2 instanceof Error ? err2.message : String(err2)
|
|
564
|
+
});
|
|
565
|
+
throw err2;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/client/alipay/cli.ts
|
|
570
|
+
var ALLOWED_ENV = /* @__PURE__ */ new Set([
|
|
571
|
+
"AIPAY_OUTPUT_CHANNEL",
|
|
572
|
+
"AIPAY_SESSION_ID",
|
|
573
|
+
"AIPAY_FRAMEWORK",
|
|
574
|
+
"AIPAY_MODEL",
|
|
575
|
+
"AIPAY_OS",
|
|
576
|
+
// Minimal survival set for spawn to find the binary and a home dir.
|
|
577
|
+
"PATH",
|
|
578
|
+
"HOME"
|
|
579
|
+
]);
|
|
580
|
+
function filterEnv(env) {
|
|
581
|
+
return Object.fromEntries(
|
|
582
|
+
Object.entries(env).filter(([k]) => ALLOWED_ENV.has(k))
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
function makeLineSplitter(onLine) {
|
|
586
|
+
let buf = "";
|
|
587
|
+
return (chunk) => {
|
|
588
|
+
buf += chunk.toString("utf-8");
|
|
589
|
+
let nl;
|
|
590
|
+
while ((nl = buf.indexOf("\n")) !== -1) {
|
|
591
|
+
onLine(buf.slice(0, nl));
|
|
592
|
+
buf = buf.slice(nl + 1);
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
function profileEnv(step, flow) {
|
|
597
|
+
const want = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE;
|
|
598
|
+
const hook = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE_HOOK;
|
|
599
|
+
if (!want || !hook) return {};
|
|
600
|
+
const steps = want.split(",").map((s) => s.trim());
|
|
601
|
+
if (!steps.includes("all") && !steps.includes(step)) return {};
|
|
602
|
+
const dir = process.env.MOLTSPAY_ALIPAY_CLI_PROFILE_DIR || "/tmp";
|
|
603
|
+
const safe = `${flow ?? "noflow"}-${step}-${Date.now()}`.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
604
|
+
const out = `${dir}/cli-profile-${safe}.json`;
|
|
605
|
+
alipayLog.info("cli.profile", { flow, step, out });
|
|
606
|
+
return {
|
|
607
|
+
NODE_OPTIONS: `--require=${hook}`,
|
|
608
|
+
MOLTSPAY_CLI_PROFILE_OUT: out
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
var runCli = (args, opts = {}) => {
|
|
612
|
+
const bin = opts.bin ?? "alipay-bot";
|
|
613
|
+
const step = opts.step ?? args[0] ?? "(no-arg)";
|
|
614
|
+
const startedAt = Date.now();
|
|
615
|
+
const lines = [];
|
|
616
|
+
const collect = (line) => {
|
|
617
|
+
lines.push(line);
|
|
618
|
+
alipayLog.debug("cli.line", {
|
|
619
|
+
flow: opts.flow,
|
|
620
|
+
step,
|
|
621
|
+
ms: Date.now() - startedAt,
|
|
622
|
+
preview: line.slice(0, 120)
|
|
623
|
+
});
|
|
624
|
+
opts.onLine?.(line);
|
|
625
|
+
};
|
|
626
|
+
alipayLog.debug("step.start", { flow: opts.flow, step });
|
|
627
|
+
return new Promise((resolve, reject) => {
|
|
628
|
+
const child = (0, import_child_process.spawn)(bin, args, {
|
|
629
|
+
env: { ...filterEnv(process.env), ...profileEnv(step, opts.flow), ...opts.env ?? {} }
|
|
630
|
+
});
|
|
631
|
+
if (opts.signal) {
|
|
632
|
+
if (opts.signal.aborted) child.kill("SIGTERM");
|
|
633
|
+
opts.signal.addEventListener("abort", () => child.kill("SIGTERM"), { once: true });
|
|
634
|
+
}
|
|
635
|
+
let sawByte = false;
|
|
636
|
+
const onChunk = (stream) => (chunk) => {
|
|
637
|
+
const ms = Date.now() - startedAt;
|
|
638
|
+
if (!sawByte) {
|
|
639
|
+
sawByte = true;
|
|
640
|
+
alipayLog.debug("cli.firstbyte", { flow: opts.flow, step, ms });
|
|
641
|
+
}
|
|
642
|
+
alipayLog.debug("cli.chunk", { flow: opts.flow, step, ms, stream, bytes: chunk.length });
|
|
643
|
+
};
|
|
644
|
+
const onStdout = makeLineSplitter(collect);
|
|
645
|
+
const onStderr = makeLineSplitter(collect);
|
|
646
|
+
child.stdout?.on("data", onChunk("out"));
|
|
647
|
+
child.stderr?.on("data", onChunk("err"));
|
|
648
|
+
child.stdout?.on("data", onStdout);
|
|
649
|
+
child.stderr?.on("data", onStderr);
|
|
650
|
+
child.on("error", (err2) => {
|
|
651
|
+
alipayLog.info("cli.exit", {
|
|
652
|
+
flow: opts.flow,
|
|
653
|
+
step,
|
|
654
|
+
ms: Date.now() - startedAt,
|
|
655
|
+
ok: false,
|
|
656
|
+
err: err2.message
|
|
657
|
+
});
|
|
658
|
+
reject(err2);
|
|
659
|
+
});
|
|
660
|
+
child.on("close", (code) => {
|
|
661
|
+
alipayLog.info("cli.exit", {
|
|
662
|
+
flow: opts.flow,
|
|
663
|
+
step,
|
|
664
|
+
ms: Date.now() - startedAt,
|
|
665
|
+
ok: (code ?? 1) === 0,
|
|
666
|
+
code: code ?? 1
|
|
667
|
+
});
|
|
668
|
+
resolve({ exitCode: code ?? 1, lines });
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
// src/client/alipay/install.ts
|
|
674
|
+
var import_child_process2 = require("child_process");
|
|
675
|
+
var import_util = require("util");
|
|
676
|
+
|
|
677
|
+
// src/client/alipay/errors.ts
|
|
678
|
+
var AlipayCliNotFoundError = class extends MoltsPayError {
|
|
679
|
+
constructor(message) {
|
|
680
|
+
super("ALIPAY_CLI_NOT_FOUND", message);
|
|
681
|
+
this.name = "AlipayCliNotFoundError";
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
var AlipayCliVersionError = class extends MoltsPayError {
|
|
685
|
+
constructor(message) {
|
|
686
|
+
super("ALIPAY_CLI_VERSION", message);
|
|
687
|
+
this.name = "AlipayCliVersionError";
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
var NeedsWalletSetupError = class extends MoltsPayError {
|
|
691
|
+
constructor(message = "Alipay wallet not set up. Run: moltspay alipay apply") {
|
|
692
|
+
super("ALIPAY_NEEDS_WALLET_SETUP", message);
|
|
693
|
+
this.name = "NeedsWalletSetupError";
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
var AlipayPaymentRejectedError = class extends MoltsPayError {
|
|
697
|
+
constructor(message) {
|
|
698
|
+
super("ALIPAY_PAYMENT_REJECTED", message);
|
|
699
|
+
this.name = "AlipayPaymentRejectedError";
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
var AlipayPaymentTimeoutError = class extends MoltsPayError {
|
|
703
|
+
constructor(message) {
|
|
704
|
+
super("ALIPAY_PAYMENT_TIMEOUT", message);
|
|
705
|
+
this.name = "AlipayPaymentTimeoutError";
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
var AlipayProtocolError = class extends MoltsPayError {
|
|
709
|
+
constructor(message) {
|
|
710
|
+
super("ALIPAY_PROTOCOL", message);
|
|
711
|
+
this.name = "AlipayProtocolError";
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
var UnsupportedRailError = class extends MoltsPayError {
|
|
715
|
+
constructor(rail, message) {
|
|
716
|
+
super("UNSUPPORTED_RAIL", message ?? `Rail not supported by server: ${rail}`);
|
|
717
|
+
this.rail = rail;
|
|
718
|
+
this.name = "UnsupportedRailError";
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
// src/client/alipay/install.ts
|
|
723
|
+
var execFileAsync = (0, import_util.promisify)(import_child_process2.execFile);
|
|
724
|
+
var MIN_CLI_VERSION = "0.3.15";
|
|
725
|
+
function semverLt(a, b) {
|
|
726
|
+
const pa = a.split(".").map((n) => parseInt(n, 10));
|
|
727
|
+
const pb = b.split(".").map((n) => parseInt(n, 10));
|
|
728
|
+
for (let i = 0; i < 3; i++) {
|
|
729
|
+
const x = pa[i] ?? 0;
|
|
730
|
+
const y = pb[i] ?? 0;
|
|
731
|
+
if (x !== y) return x < y;
|
|
732
|
+
}
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
function parseVersion(stdout) {
|
|
736
|
+
return stdout.match(/v?(\d+\.\d+\.\d+)/)?.[1] ?? null;
|
|
737
|
+
}
|
|
738
|
+
var defaultGetVersion = async () => {
|
|
739
|
+
const { stdout } = await execFileAsync("alipay-bot", ["--version"]);
|
|
740
|
+
return stdout;
|
|
741
|
+
};
|
|
742
|
+
async function ensureCliUncached(getVersion) {
|
|
743
|
+
let stdout;
|
|
744
|
+
try {
|
|
745
|
+
stdout = await getVersion();
|
|
746
|
+
} catch (e) {
|
|
747
|
+
if (e?.code === "ENOENT") {
|
|
748
|
+
throw new AlipayCliNotFoundError(
|
|
749
|
+
"alipay-bot not installed. Run: npx -y @alipay/agent-payment install-cli"
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
throw e;
|
|
753
|
+
}
|
|
754
|
+
const version = parseVersion(stdout);
|
|
755
|
+
if (!version || semverLt(version, MIN_CLI_VERSION)) {
|
|
756
|
+
throw new AlipayCliVersionError(
|
|
757
|
+
`alipay-bot ${version ?? "?"} found, need \u2265 ${MIN_CLI_VERSION}. Run: npx -y @alipay/agent-payment@latest update`
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
return version;
|
|
761
|
+
}
|
|
762
|
+
var cachedVersion = null;
|
|
763
|
+
var inflight = null;
|
|
764
|
+
async function ensureCli(getVersion = defaultGetVersion) {
|
|
765
|
+
if (getVersion !== defaultGetVersion) {
|
|
766
|
+
return ensureCliUncached(getVersion);
|
|
767
|
+
}
|
|
768
|
+
if (cachedVersion) return cachedVersion;
|
|
769
|
+
if (!inflight) {
|
|
770
|
+
inflight = ensureCliUncached(getVersion).then((v) => {
|
|
771
|
+
cachedVersion = v;
|
|
772
|
+
return v;
|
|
773
|
+
}).finally(() => {
|
|
774
|
+
inflight = null;
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
return inflight;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/client/alipay/poll.ts
|
|
781
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
782
|
+
var POLL_MAX_INFLIGHT = 2;
|
|
783
|
+
function resolveMaxInflight(override) {
|
|
784
|
+
if (override !== void 0) return Math.max(1, Math.floor(override));
|
|
785
|
+
const raw = process.env.MOLTSPAY_ALIPAY_POLL_MAX_INFLIGHT;
|
|
786
|
+
if (raw !== void 0 && raw.trim() !== "") {
|
|
787
|
+
const n = Number(raw);
|
|
788
|
+
if (Number.isFinite(n) && n >= 1) return Math.floor(n);
|
|
789
|
+
}
|
|
790
|
+
return POLL_MAX_INFLIGHT;
|
|
791
|
+
}
|
|
792
|
+
function resolveLaunchGap(override) {
|
|
793
|
+
if (override !== void 0) return Math.max(0, override);
|
|
794
|
+
const raw = process.env.MOLTSPAY_ALIPAY_POLL_LAUNCH_MS;
|
|
795
|
+
if (raw !== void 0 && raw.trim() !== "") {
|
|
796
|
+
const n = Number(raw);
|
|
797
|
+
if (Number.isFinite(n) && n >= 0) return n;
|
|
798
|
+
}
|
|
799
|
+
return POLL_INTERVAL_MS;
|
|
800
|
+
}
|
|
801
|
+
function parseStatus(lines) {
|
|
802
|
+
const raw = lines.join("\n").trim();
|
|
803
|
+
try {
|
|
804
|
+
const json = JSON.parse(raw);
|
|
805
|
+
if (json && typeof json === "object") {
|
|
806
|
+
if (typeof json.success === "boolean") {
|
|
807
|
+
if (json.success) return "paid";
|
|
808
|
+
const err2 = String(json.errorCode ?? "").toUpperCase();
|
|
809
|
+
if (/UNPAID|WAIT|PENDING|PROCESS|NOTPAY/.test(err2)) return "pending";
|
|
810
|
+
if (/CLOSED|CANCEL|FAIL|REJECT|REFUSE|TIMEOUT|EXPIRE/.test(err2)) return "rejected";
|
|
811
|
+
return "pending";
|
|
812
|
+
}
|
|
813
|
+
if (typeof json.code !== "undefined") {
|
|
814
|
+
if (Number(json.code) === 200) return "paid";
|
|
815
|
+
const msg = `${json.message ?? ""}${json.reason ?? ""}`;
|
|
816
|
+
if (/关闭|失败|拒绝|取消|超时|已撤销/.test(msg)) return "rejected";
|
|
817
|
+
return "pending";
|
|
818
|
+
}
|
|
819
|
+
if (typeof json.body === "string") {
|
|
820
|
+
return classifyReportText(json.body);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
} catch {
|
|
824
|
+
}
|
|
825
|
+
return classifyReportText(raw);
|
|
826
|
+
}
|
|
827
|
+
function classifyReportText(s) {
|
|
828
|
+
const text = s.toUpperCase();
|
|
829
|
+
if (/查询支付状态成功并获取资源/.test(s) || /资源响应状态[^0-9\n]{0,8}200/.test(s) || /TRADE_SUCCESS|TRADE_FINISHED|"STATUS":\s*"FULFILLED"/.test(text)) return "paid";
|
|
830
|
+
if (/TRADE_CLOSED|REJECTED|REFUSE|CANCEL/.test(text) || /交易关闭|支付失败|已拒绝|已取消|已撤销|交易超时/.test(s)) return "rejected";
|
|
831
|
+
if (/WAIT_BUYER_PAY|UNPAID|PENDING|WAITING/.test(text) || /交易未支付|等待付款|待支付|未支付/.test(s)) return "pending";
|
|
832
|
+
return "unknown";
|
|
833
|
+
}
|
|
834
|
+
function defaultSleep(ms, signal) {
|
|
835
|
+
return new Promise((resolve, reject) => {
|
|
836
|
+
if (signal?.aborted) return reject(new Error("aborted"));
|
|
837
|
+
const t = setTimeout(resolve, ms);
|
|
838
|
+
signal?.addEventListener(
|
|
839
|
+
"abort",
|
|
840
|
+
() => {
|
|
841
|
+
clearTimeout(t);
|
|
842
|
+
reject(new Error("aborted"));
|
|
843
|
+
},
|
|
844
|
+
{ once: true }
|
|
845
|
+
);
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
var LAUNCH = /* @__PURE__ */ Symbol("launch");
|
|
849
|
+
async function pollUntil(tradeNo, resourceUrl, opts) {
|
|
850
|
+
const runner = opts.runner ?? runCli;
|
|
851
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
852
|
+
const now = opts.now ?? Date.now;
|
|
853
|
+
const maxInflight = resolveMaxInflight(opts.maxInflight);
|
|
854
|
+
const launchGap = resolveLaunchGap(opts.launchIntervalMs);
|
|
855
|
+
const args = [
|
|
856
|
+
"402-query-payment-status",
|
|
857
|
+
"-t",
|
|
858
|
+
tradeNo,
|
|
859
|
+
"-r",
|
|
860
|
+
resourceUrl,
|
|
861
|
+
...opts.method ? ["-m", opts.method] : [],
|
|
862
|
+
...opts.data ? ["-d", opts.data] : []
|
|
863
|
+
];
|
|
864
|
+
const internal = new AbortController();
|
|
865
|
+
const signal = opts.signal ? AbortSignal.any([opts.signal, internal.signal]) : internal.signal;
|
|
866
|
+
let tick = 0;
|
|
867
|
+
let lastLaunch = -Infinity;
|
|
868
|
+
const inflight2 = /* @__PURE__ */ new Set();
|
|
869
|
+
const launch = () => {
|
|
870
|
+
tick += 1;
|
|
871
|
+
const myTick = tick;
|
|
872
|
+
lastLaunch = now();
|
|
873
|
+
const run = (async () => {
|
|
874
|
+
const { lines } = await runner(args, {
|
|
875
|
+
signal,
|
|
876
|
+
step: "query-payment-status",
|
|
877
|
+
flow: tradeNo
|
|
878
|
+
});
|
|
879
|
+
const status = parseStatus(lines);
|
|
880
|
+
alipayLog.debug("poll.tick", { flow: tradeNo, tick: myTick, status });
|
|
881
|
+
return { status, lines };
|
|
882
|
+
})();
|
|
883
|
+
const tracked = run.finally(() => {
|
|
884
|
+
inflight2.delete(tracked);
|
|
885
|
+
});
|
|
886
|
+
inflight2.add(tracked);
|
|
887
|
+
};
|
|
888
|
+
try {
|
|
889
|
+
for (; ; ) {
|
|
890
|
+
if (opts.signal?.aborted) throw new Error("aborted");
|
|
891
|
+
const expired = now() >= opts.deadline;
|
|
892
|
+
if (expired && inflight2.size === 0) {
|
|
893
|
+
throw new AlipayPaymentTimeoutError(
|
|
894
|
+
`Payment ${tradeNo} not completed before pay_before deadline`
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
while (!expired && inflight2.size < maxInflight && now() - lastLaunch >= launchGap) {
|
|
898
|
+
launch();
|
|
899
|
+
}
|
|
900
|
+
const waiters = [...inflight2];
|
|
901
|
+
if (!expired && inflight2.size < maxInflight) {
|
|
902
|
+
const untilNext = Math.max(0, launchGap - (now() - lastLaunch));
|
|
903
|
+
waiters.push(sleep(untilNext, signal).then(() => LAUNCH, () => LAUNCH));
|
|
904
|
+
}
|
|
905
|
+
const winner = await Promise.race(waiters);
|
|
906
|
+
if (winner === LAUNCH) continue;
|
|
907
|
+
if (winner.status === "paid") return { status: "paid", lines: winner.lines };
|
|
908
|
+
if (winner.status === "rejected") {
|
|
909
|
+
throw new AlipayPaymentRejectedError(`Payment ${tradeNo} was rejected`);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
} finally {
|
|
913
|
+
internal.abort();
|
|
914
|
+
for (const p of inflight2) p.catch(() => void 0);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/client/alipay/index.ts
|
|
919
|
+
var TRADE_NO_RE = /^\d{32}$/;
|
|
920
|
+
function resolveSessionId(explicit, envSession) {
|
|
921
|
+
return explicit ?? envSession ?? (0, import_node_crypto.randomUUID)();
|
|
922
|
+
}
|
|
923
|
+
function assertTradeNo(t) {
|
|
924
|
+
if (!TRADE_NO_RE.test(t)) {
|
|
925
|
+
throw new AlipayProtocolError(`invalid tradeNo (expect 32 digits): ${t}`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
function parseTradeNo(lines) {
|
|
929
|
+
for (const line of lines) {
|
|
930
|
+
const labeled = line.match(/trade[_-]?no["'\s:=]+(\d{32})/i);
|
|
931
|
+
if (labeled) return labeled[1];
|
|
932
|
+
const bare = line.match(/\b(\d{32})\b/);
|
|
933
|
+
if (bare) return bare[1];
|
|
934
|
+
}
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
function parsePaymentUrl(lines) {
|
|
938
|
+
let paymentUrl;
|
|
939
|
+
let shortenUrl;
|
|
940
|
+
for (const line of lines) {
|
|
941
|
+
const m = line.match(/(alipays?:\/\/\S+|https?:\/\/\S+)/i);
|
|
942
|
+
if (!m) continue;
|
|
943
|
+
const url = m[1].replace(/[)\]`>]+$/, "");
|
|
944
|
+
if (/short|qr\.alipay|surl|\/s\//i.test(line) && !shortenUrl) shortenUrl = url;
|
|
945
|
+
else if (!paymentUrl) paymentUrl = url;
|
|
946
|
+
}
|
|
947
|
+
if (!paymentUrl && shortenUrl) paymentUrl = shortenUrl;
|
|
948
|
+
return { paymentUrl, shortenUrl };
|
|
949
|
+
}
|
|
950
|
+
function extractMedia(line) {
|
|
951
|
+
const m = line.match(/^\s*MEDIA:\s*(.+?)\s*$/);
|
|
952
|
+
return m ? m[1] : null;
|
|
953
|
+
}
|
|
954
|
+
function isWalletReady(lines) {
|
|
955
|
+
const text = lines.join("\n").trim();
|
|
956
|
+
try {
|
|
957
|
+
const json = JSON.parse(text);
|
|
958
|
+
if (json && typeof json.code !== "undefined") return Number(json.code) === 200;
|
|
959
|
+
} catch {
|
|
960
|
+
}
|
|
961
|
+
return !/未开通|未开启|NOT[_\s-]*(OPEN|BOUND|SET)|NEEDS?[_\s-]*SETUP|NO[_\s-]*WALLET/i.test(text);
|
|
962
|
+
}
|
|
963
|
+
function extractResourceFromReport(report) {
|
|
964
|
+
const label = report.search(/资源响应体/);
|
|
965
|
+
const start = report.indexOf("{", label === -1 ? 0 : label);
|
|
966
|
+
if (start === -1) return void 0;
|
|
967
|
+
let depth = 0, inStr = false, esc = false;
|
|
968
|
+
for (let i = start; i < report.length; i++) {
|
|
969
|
+
const ch = report[i];
|
|
970
|
+
if (inStr) {
|
|
971
|
+
if (esc) esc = false;
|
|
972
|
+
else if (ch === "\\") esc = true;
|
|
973
|
+
else if (ch === '"') inStr = false;
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
if (ch === '"') inStr = true;
|
|
977
|
+
else if (ch === "{") depth++;
|
|
978
|
+
else if (ch === "}" && --depth === 0) {
|
|
979
|
+
const slice = report.slice(start, i + 1);
|
|
980
|
+
try {
|
|
981
|
+
const obj = JSON.parse(slice);
|
|
982
|
+
const r = obj?.resourceResponse ?? obj?.result ?? obj?.data ?? obj?.body ?? obj;
|
|
983
|
+
return typeof r === "string" ? r : JSON.stringify(r);
|
|
984
|
+
} catch {
|
|
985
|
+
return slice;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
return void 0;
|
|
990
|
+
}
|
|
991
|
+
function extractBody(lines) {
|
|
992
|
+
const text = lines.join("\n").trim();
|
|
993
|
+
try {
|
|
994
|
+
const json = JSON.parse(text);
|
|
995
|
+
if (json && typeof json === "object") {
|
|
996
|
+
if (json.resourceResponse !== void 0 && json.resourceResponse !== null) {
|
|
997
|
+
const rr = json.resourceResponse;
|
|
998
|
+
const r = typeof rr === "object" ? rr.result ?? rr.data ?? rr.body ?? rr : rr;
|
|
999
|
+
return typeof r === "string" ? r : JSON.stringify(r);
|
|
1000
|
+
}
|
|
1001
|
+
if (typeof json.body === "string") {
|
|
1002
|
+
const inner = extractResourceFromReport(json.body);
|
|
1003
|
+
return inner ?? json.body;
|
|
1004
|
+
}
|
|
1005
|
+
const body = json.data ?? json.result ?? json.body ?? json.resource;
|
|
1006
|
+
if (body !== void 0) return typeof body === "string" ? body : JSON.stringify(body);
|
|
1007
|
+
return text;
|
|
1008
|
+
}
|
|
1009
|
+
} catch {
|
|
1010
|
+
}
|
|
1011
|
+
const idx = lines.findIndex((l) => /^\s*BODY:/.test(l));
|
|
1012
|
+
if (idx !== -1) {
|
|
1013
|
+
const first = lines[idx].replace(/^\s*BODY:\s*/, "");
|
|
1014
|
+
return [first, ...lines.slice(idx + 1)].join("\n").trim();
|
|
1015
|
+
}
|
|
1016
|
+
return lines.filter((l) => !/^\s*(MEDIA|STATUS|INFO):/.test(l)).join("\n").trim();
|
|
1017
|
+
}
|
|
1018
|
+
var DEFAULT_WALLET_TTL_MS = 10 * 60 * 1e3;
|
|
1019
|
+
function resolveWalletTtlMs() {
|
|
1020
|
+
const raw = process.env.MOLTSPAY_ALIPAY_WALLET_TTL_MS;
|
|
1021
|
+
if (raw === void 0 || raw.trim() === "") return DEFAULT_WALLET_TTL_MS;
|
|
1022
|
+
const n = Number(raw);
|
|
1023
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_WALLET_TTL_MS;
|
|
1024
|
+
}
|
|
1025
|
+
var walletReadyUntil = /* @__PURE__ */ new Map();
|
|
1026
|
+
var DEFAULT_INTENT_TTL_MS = 10 * 60 * 1e3;
|
|
1027
|
+
function resolveIntentTtlMs() {
|
|
1028
|
+
const raw = process.env.MOLTSPAY_ALIPAY_INTENT_TTL_MS;
|
|
1029
|
+
if (raw === void 0 || raw.trim() === "") return DEFAULT_INTENT_TTL_MS;
|
|
1030
|
+
const n = Number(raw);
|
|
1031
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_INTENT_TTL_MS;
|
|
1032
|
+
}
|
|
1033
|
+
var intentDoneUntil = /* @__PURE__ */ new Map();
|
|
1034
|
+
var AlipayClient = class {
|
|
1035
|
+
sessionId;
|
|
1036
|
+
configDir;
|
|
1037
|
+
framework;
|
|
1038
|
+
runner;
|
|
1039
|
+
getVersion;
|
|
1040
|
+
now;
|
|
1041
|
+
/** Only the default runner may use the process-level wallet cache. */
|
|
1042
|
+
walletCacheable;
|
|
1043
|
+
/** Only the default runner may use the process-level payment-intent cache. */
|
|
1044
|
+
intentCacheable;
|
|
1045
|
+
constructor(opts = {}) {
|
|
1046
|
+
this.sessionId = resolveSessionId(opts.sessionId, process.env.AIPAY_SESSION_ID);
|
|
1047
|
+
this.configDir = opts.configDir ?? (0, import_path2.join)((0, import_os2.homedir)(), ".moltspay");
|
|
1048
|
+
this.framework = opts.framework ?? process.env.AIPAY_FRAMEWORK ?? "openclaw";
|
|
1049
|
+
this.runner = opts.runner ?? runCli;
|
|
1050
|
+
this.getVersion = opts.getVersion;
|
|
1051
|
+
this.now = opts.now ?? Date.now;
|
|
1052
|
+
this.walletCacheable = opts.cacheWallet ?? !opts.runner;
|
|
1053
|
+
this.intentCacheable = opts.cacheIntent ?? !opts.runner;
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Throws NeedsWalletSetupError unless alipay-bot reports an opened wallet.
|
|
1057
|
+
*
|
|
1058
|
+
* Skips the ~22s `check-wallet` spawn entirely when a prior call cached a
|
|
1059
|
+
* "ready" verdict within the TTL (see {@link DEFAULT_WALLET_TTL_MS}).
|
|
1060
|
+
*/
|
|
1061
|
+
async checkWallet(signal, flow) {
|
|
1062
|
+
const ttlMs = resolveWalletTtlMs();
|
|
1063
|
+
const cacheKey = `${this.configDir}::${this.framework}`;
|
|
1064
|
+
const useCache = this.walletCacheable && ttlMs > 0;
|
|
1065
|
+
if (useCache) {
|
|
1066
|
+
const until = walletReadyUntil.get(cacheKey);
|
|
1067
|
+
if (until !== void 0 && this.now() < until) {
|
|
1068
|
+
alipayLog.info("wallet.cache", { flow, step: "check-wallet", hit: true, ttlMsLeft: until - this.now() });
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
const { lines } = await this.runner(["check-wallet"], { signal, step: "check-wallet", flow });
|
|
1073
|
+
if (!isWalletReady(lines)) {
|
|
1074
|
+
if (this.walletCacheable) walletReadyUntil.delete(cacheKey);
|
|
1075
|
+
throw new NeedsWalletSetupError(
|
|
1076
|
+
"Alipay wallet not opened. Run: moltspay alipay apply (then: moltspay alipay bind)"
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
if (useCache) {
|
|
1080
|
+
walletReadyUntil.set(cacheKey, this.now() + ttlMs);
|
|
1081
|
+
alipayLog.info("wallet.cache", { flow, step: "check-wallet", hit: false, cachedForMs: ttlMs });
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
/** Run the full 8-step flow and resolve with the resource body. */
|
|
1085
|
+
async pay402(opts) {
|
|
1086
|
+
const { resourceUrl, requirement, signal } = opts;
|
|
1087
|
+
const extra = requirement.extra ?? {};
|
|
1088
|
+
const paymentNeededHeader = String(extra.payment_needed_header ?? "");
|
|
1089
|
+
if (!paymentNeededHeader) {
|
|
1090
|
+
throw new AlipayProtocolError("alipay requirement missing extra.payment_needed_header");
|
|
1091
|
+
}
|
|
1092
|
+
const flow = this.sessionId;
|
|
1093
|
+
const flowStart = this.now();
|
|
1094
|
+
alipayLog.info("flow.start", { flow, resource: resourceUrl });
|
|
1095
|
+
const media = [];
|
|
1096
|
+
const onLine = (line) => {
|
|
1097
|
+
const m = extractMedia(line);
|
|
1098
|
+
if (m) media.push(m);
|
|
1099
|
+
else opts.onLine?.(line);
|
|
1100
|
+
};
|
|
1101
|
+
const intentSummary = opts.intentSummary?.trim() || `\u652F\u4ED8 ${requirement.amount ?? ""} ${requirement.asset ?? "CNY"}`.trim();
|
|
1102
|
+
await timeStep("ensure-cli", flow, () => ensureCli(this.getVersion));
|
|
1103
|
+
const intentTtlMs = resolveIntentTtlMs();
|
|
1104
|
+
const intentKey = `${this.configDir}::${this.framework}`;
|
|
1105
|
+
const useIntentCache = this.intentCacheable && intentTtlMs > 0;
|
|
1106
|
+
const intentUntil = useIntentCache ? intentDoneUntil.get(intentKey) : void 0;
|
|
1107
|
+
if (intentUntil !== void 0 && this.now() < intentUntil) {
|
|
1108
|
+
alipayLog.info("intent.cache", { flow, step: "payment-intent", hit: true, ttlMsLeft: intentUntil - this.now() });
|
|
1109
|
+
} else {
|
|
1110
|
+
await this.runner(
|
|
1111
|
+
[
|
|
1112
|
+
"payment-intent",
|
|
1113
|
+
"--session-id",
|
|
1114
|
+
this.sessionId,
|
|
1115
|
+
"--intent-summary",
|
|
1116
|
+
intentSummary,
|
|
1117
|
+
"--framework",
|
|
1118
|
+
this.framework
|
|
1119
|
+
],
|
|
1120
|
+
{ onLine, signal, step: "payment-intent", flow }
|
|
1121
|
+
);
|
|
1122
|
+
if (useIntentCache) {
|
|
1123
|
+
intentDoneUntil.set(intentKey, this.now() + intentTtlMs);
|
|
1124
|
+
alipayLog.info("intent.cache", { flow, step: "payment-intent", hit: false, cachedForMs: intentTtlMs });
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
await this.checkWallet(signal, flow);
|
|
1128
|
+
const reqId = String(extra.out_trade_no ?? (0, import_node_crypto.randomUUID)());
|
|
1129
|
+
const dir = (0, import_path2.join)(this.configDir, "alipay");
|
|
1130
|
+
await (0, import_promises.mkdir)(dir, { recursive: true });
|
|
1131
|
+
const challengeFile = (0, import_path2.join)(dir, `402_${reqId}.txt`);
|
|
1132
|
+
await (0, import_promises.writeFile)(challengeFile, paymentNeededHeader, "utf-8");
|
|
1133
|
+
const payArgs = [
|
|
1134
|
+
"402-buyer-pay",
|
|
1135
|
+
"-f",
|
|
1136
|
+
challengeFile,
|
|
1137
|
+
"-r",
|
|
1138
|
+
resourceUrl,
|
|
1139
|
+
"-s",
|
|
1140
|
+
this.sessionId,
|
|
1141
|
+
"-i",
|
|
1142
|
+
intentSummary,
|
|
1143
|
+
"-w",
|
|
1144
|
+
this.framework
|
|
1145
|
+
];
|
|
1146
|
+
if (opts.method) payArgs.push("-m", opts.method);
|
|
1147
|
+
if (opts.data) payArgs.push("-d", opts.data);
|
|
1148
|
+
const payRun = await this.runner(payArgs, { onLine, signal, step: "402-buyer-pay", flow });
|
|
1149
|
+
const tradeNo = parseTradeNo(payRun.lines);
|
|
1150
|
+
if (!tradeNo) {
|
|
1151
|
+
throw new AlipayProtocolError("402-buyer-pay did not return a tradeNo");
|
|
1152
|
+
}
|
|
1153
|
+
assertTradeNo(tradeNo);
|
|
1154
|
+
const { paymentUrl, shortenUrl } = parsePaymentUrl(payRun.lines);
|
|
1155
|
+
if (paymentUrl) {
|
|
1156
|
+
alipayLog.info("flow.pending", { flow, tradeNo, ms: this.now() - flowStart });
|
|
1157
|
+
opts.onPaymentPending?.({ paymentUrl, shortenUrl, tradeNo });
|
|
1158
|
+
}
|
|
1159
|
+
const pendingAt = this.now();
|
|
1160
|
+
const windowMs = (requirement.maxTimeoutSeconds ?? 30 * 60) * 1e3;
|
|
1161
|
+
const deadline = this.now() + (opts.timeoutMs ?? windowMs);
|
|
1162
|
+
const poll = await pollUntil(tradeNo, resourceUrl, {
|
|
1163
|
+
// No onLine: the status-poll output embeds the resource and must not reach
|
|
1164
|
+
// the log stream — the body is surfaced via the return value below (§9.3).
|
|
1165
|
+
deadline,
|
|
1166
|
+
signal,
|
|
1167
|
+
runner: this.runner,
|
|
1168
|
+
now: this.now,
|
|
1169
|
+
// Re-fetch the resource the same way it was paid (POST + body), else the
|
|
1170
|
+
// status poll defaults to GET and 404s on a POST-only `/execute`.
|
|
1171
|
+
method: opts.method,
|
|
1172
|
+
data: opts.data
|
|
1173
|
+
});
|
|
1174
|
+
alipayLog.info("flow.settled", { flow, tradeNo, ms: this.now() - pendingAt });
|
|
1175
|
+
const body = extractBody(poll.lines);
|
|
1176
|
+
void this.runner(["402-buyer-fulfillment-ack", "-t", tradeNo], {
|
|
1177
|
+
onLine,
|
|
1178
|
+
signal,
|
|
1179
|
+
step: "402-buyer-fulfillment-ack",
|
|
1180
|
+
flow
|
|
1181
|
+
}).catch(() => void 0);
|
|
1182
|
+
return { body, payment: { tradeNo, outTradeNo: reqId }, media };
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1186
|
+
// src/client/alipay/router.ts
|
|
1187
|
+
var ALIPAY_RAIL = "alipay";
|
|
1188
|
+
function railOf(req) {
|
|
1189
|
+
if (req.scheme === "alipay-aipay" || req.network === ALIPAY_RAIL) return ALIPAY_RAIL;
|
|
1190
|
+
return networkToChainName(req.network) ?? req.network;
|
|
1191
|
+
}
|
|
1192
|
+
function findRail(accepts, rail) {
|
|
1193
|
+
const requirement = accepts.find((r) => railOf(r) === rail);
|
|
1194
|
+
return requirement ? { rail, requirement } : null;
|
|
1195
|
+
}
|
|
1196
|
+
function selectRail(input) {
|
|
1197
|
+
const { serverAccepts, explicitRail, preference, availability } = input;
|
|
1198
|
+
if (!serverAccepts || serverAccepts.length === 0) {
|
|
1199
|
+
throw new UnsupportedRailError(explicitRail ?? "unknown", "Server offered no payment options");
|
|
1200
|
+
}
|
|
1201
|
+
if (explicitRail) {
|
|
1202
|
+
const hit = findRail(serverAccepts, explicitRail);
|
|
1203
|
+
if (!hit) {
|
|
1204
|
+
const offered = serverAccepts.map(railOf);
|
|
1205
|
+
throw new UnsupportedRailError(
|
|
1206
|
+
explicitRail,
|
|
1207
|
+
`Server doesn't accept rail '${explicitRail}'. Offered: ${offered.join(", ")}`
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
return hit;
|
|
1211
|
+
}
|
|
1212
|
+
if (preference) {
|
|
1213
|
+
for (const pref of preference) {
|
|
1214
|
+
const hit = findRail(serverAccepts, pref);
|
|
1215
|
+
if (hit) return hit;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
if (availability?.evmReady) {
|
|
1219
|
+
const evm = serverAccepts.find((r) => railOf(r) !== ALIPAY_RAIL);
|
|
1220
|
+
if (evm) return { rail: railOf(evm), requirement: evm };
|
|
1221
|
+
}
|
|
1222
|
+
if (availability?.alipayReady) {
|
|
1223
|
+
const hit = findRail(serverAccepts, ALIPAY_RAIL);
|
|
1224
|
+
if (hit) return hit;
|
|
1225
|
+
}
|
|
1226
|
+
return { rail: railOf(serverAccepts[0]), requirement: serverAccepts[0] };
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// src/client/node/index.ts
|
|
332
1230
|
var DEFAULT_CONFIG = {
|
|
333
1231
|
chain: "base",
|
|
334
1232
|
limits: {
|
|
@@ -341,15 +1239,24 @@ var MoltsPayClient = class {
|
|
|
341
1239
|
config;
|
|
342
1240
|
walletData = null;
|
|
343
1241
|
wallet = null;
|
|
1242
|
+
signer = null;
|
|
344
1243
|
todaySpending = 0;
|
|
345
1244
|
lastSpendingReset = 0;
|
|
1245
|
+
railPreference;
|
|
1246
|
+
alipaySessionId;
|
|
346
1247
|
constructor(options = {}) {
|
|
347
|
-
this.configDir = options.configDir || (0,
|
|
1248
|
+
this.configDir = options.configDir || (0, import_path3.join)((0, import_os3.homedir)(), ".moltspay");
|
|
348
1249
|
this.config = this.loadConfig();
|
|
1250
|
+
this.railPreference = options.railPreference ?? this.config.railPreference;
|
|
1251
|
+
this.alipaySessionId = options.alipaySessionId;
|
|
349
1252
|
this.walletData = this.loadWallet();
|
|
350
1253
|
this.loadSpending();
|
|
351
1254
|
if (this.walletData) {
|
|
352
|
-
this.wallet = new
|
|
1255
|
+
this.wallet = new import_ethers2.Wallet(this.walletData.privateKey);
|
|
1256
|
+
const configDir = this.configDir;
|
|
1257
|
+
this.signer = new NodeSigner(this.wallet, {
|
|
1258
|
+
getSolanaKeypair: () => loadSolanaWallet(configDir)
|
|
1259
|
+
});
|
|
353
1260
|
}
|
|
354
1261
|
}
|
|
355
1262
|
/**
|
|
@@ -419,6 +1326,9 @@ var MoltsPayClient = class {
|
|
|
419
1326
|
* @param options - Payment options (token selection)
|
|
420
1327
|
*/
|
|
421
1328
|
async pay(serverUrl, service, params, options = {}) {
|
|
1329
|
+
if (options.rail === ALIPAY_RAIL) {
|
|
1330
|
+
return this.payViaAlipay(serverUrl, service, params, options);
|
|
1331
|
+
}
|
|
422
1332
|
if (!this.wallet || !this.walletData) {
|
|
423
1333
|
throw new Error("Client not initialized. Run: npx moltspay init");
|
|
424
1334
|
}
|
|
@@ -477,20 +1387,6 @@ var MoltsPayClient = class {
|
|
|
477
1387
|
} catch {
|
|
478
1388
|
throw new Error("Invalid x-payment-required header");
|
|
479
1389
|
}
|
|
480
|
-
const networkToChainName = (network2) => {
|
|
481
|
-
if (network2 === "solana:mainnet") return "solana";
|
|
482
|
-
if (network2 === "solana:devnet") return "solana_devnet";
|
|
483
|
-
const match = network2.match(/^eip155:(\d+)$/);
|
|
484
|
-
if (!match) return null;
|
|
485
|
-
const chainId = parseInt(match[1]);
|
|
486
|
-
if (chainId === 8453) return "base";
|
|
487
|
-
if (chainId === 137) return "polygon";
|
|
488
|
-
if (chainId === 84532) return "base_sepolia";
|
|
489
|
-
if (chainId === 42431) return "tempo_moderato";
|
|
490
|
-
if (chainId === 56) return "bnb";
|
|
491
|
-
if (chainId === 97) return "bnb_testnet";
|
|
492
|
-
return null;
|
|
493
|
-
};
|
|
494
1390
|
const serverChains = requirements.map((r) => networkToChainName(r.network)).filter((c) => c !== null);
|
|
495
1391
|
const userSpecifiedChain = options.chain;
|
|
496
1392
|
let selectedChain;
|
|
@@ -624,6 +1520,76 @@ Please specify: --chain <chain_name>`
|
|
|
624
1520
|
console.log(`[MoltsPay] Success! Payment: ${result.payment?.status || "claimed"}`);
|
|
625
1521
|
return result.result || result;
|
|
626
1522
|
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Pay for a service over the Alipay fiat rail (2.0.0).
|
|
1525
|
+
*
|
|
1526
|
+
* Unlike the crypto path this needs no EVM wallet — it shells out to
|
|
1527
|
+
* alipay-bot via {@link AlipayClient}. Flow: hit the resource with no
|
|
1528
|
+
* payment to get the 402 challenge, confirm the server actually offers the
|
|
1529
|
+
* alipay rail (selectRail), then run the 8-step state machine and return the
|
|
1530
|
+
* resource body.
|
|
1531
|
+
*/
|
|
1532
|
+
async payViaAlipay(serverUrl, service, params, options) {
|
|
1533
|
+
const flow = this.alipaySessionId;
|
|
1534
|
+
let executeUrl = `${serverUrl}/execute`;
|
|
1535
|
+
try {
|
|
1536
|
+
const services = await timeStep(
|
|
1537
|
+
"discover-services",
|
|
1538
|
+
flow,
|
|
1539
|
+
() => this.getServices(serverUrl)
|
|
1540
|
+
);
|
|
1541
|
+
const svc = services.services?.find((s) => s.id === service);
|
|
1542
|
+
if (svc?.endpoint) executeUrl = `${serverUrl}${svc.endpoint}`;
|
|
1543
|
+
} catch {
|
|
1544
|
+
}
|
|
1545
|
+
const requestBody = options.rawData ? { service, ...params } : { service, params };
|
|
1546
|
+
const bodyJson = JSON.stringify(requestBody);
|
|
1547
|
+
const res = await timeStep(
|
|
1548
|
+
"challenge-402",
|
|
1549
|
+
flow,
|
|
1550
|
+
() => fetch(executeUrl, {
|
|
1551
|
+
method: "POST",
|
|
1552
|
+
headers: { "Content-Type": "application/json" },
|
|
1553
|
+
body: bodyJson
|
|
1554
|
+
})
|
|
1555
|
+
);
|
|
1556
|
+
if (res.status !== 402) {
|
|
1557
|
+
const data = await res.json().catch(() => ({}));
|
|
1558
|
+
if (res.ok && data.result) return data.result;
|
|
1559
|
+
throw new Error(data.error || `Expected 402, got ${res.status}`);
|
|
1560
|
+
}
|
|
1561
|
+
const header = res.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
1562
|
+
if (!header) throw new Error("Missing x-payment-required header on 402");
|
|
1563
|
+
const parsed = JSON.parse(Buffer.from(header, "base64").toString("utf-8"));
|
|
1564
|
+
const accepts = Array.isArray(parsed) ? parsed : parsed.accepts ?? [parsed];
|
|
1565
|
+
const { requirement } = selectRail({
|
|
1566
|
+
serverAccepts: accepts,
|
|
1567
|
+
explicitRail: ALIPAY_RAIL,
|
|
1568
|
+
preference: this.railPreference,
|
|
1569
|
+
availability: { evmReady: this.isInitialized }
|
|
1570
|
+
});
|
|
1571
|
+
const onLine = options.onLine ?? ((line) => process.stdout.write(line + "\n"));
|
|
1572
|
+
const alipay = new AlipayClient({
|
|
1573
|
+
sessionId: this.alipaySessionId,
|
|
1574
|
+
configDir: this.configDir
|
|
1575
|
+
});
|
|
1576
|
+
const result = await alipay.pay402({
|
|
1577
|
+
resourceUrl: executeUrl,
|
|
1578
|
+
requirement,
|
|
1579
|
+
method: "POST",
|
|
1580
|
+
data: bodyJson,
|
|
1581
|
+
onLine,
|
|
1582
|
+
onPaymentPending: options.onPaymentPending,
|
|
1583
|
+
timeoutMs: options.timeoutMs,
|
|
1584
|
+
signal: options.signal
|
|
1585
|
+
});
|
|
1586
|
+
try {
|
|
1587
|
+
const json = JSON.parse(result.body);
|
|
1588
|
+
return json.result ?? json;
|
|
1589
|
+
} catch {
|
|
1590
|
+
return { body: result.body, payment: result.payment, media: result.media };
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
627
1593
|
/**
|
|
628
1594
|
* Handle MPP (Machine Payments Protocol) payment flow
|
|
629
1595
|
* Called when pay() detects WWW-Authenticate header in 402 response
|
|
@@ -719,14 +1685,14 @@ Please specify: --chain <chain_name>`
|
|
|
719
1685
|
async handleBNBPayment(executeUrl, service, params, paymentDetails, options = {}) {
|
|
720
1686
|
const { to, amount, token, chainName, chain, spender } = paymentDetails;
|
|
721
1687
|
const tokenConfig = chain.tokens[token];
|
|
722
|
-
const provider = new
|
|
1688
|
+
const provider = new import_ethers2.ethers.JsonRpcProvider(chain.rpc);
|
|
723
1689
|
const allowance = await this.checkAllowance(tokenConfig.address, spender, provider);
|
|
724
1690
|
const amountWeiCheck = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals));
|
|
725
1691
|
if (allowance < amountWeiCheck) {
|
|
726
1692
|
const nativeBalance = await provider.getBalance(this.wallet.address);
|
|
727
|
-
const minGasBalance =
|
|
1693
|
+
const minGasBalance = import_ethers2.ethers.parseEther("0.0005");
|
|
728
1694
|
if (nativeBalance < minGasBalance) {
|
|
729
|
-
const nativeBNB = parseFloat(
|
|
1695
|
+
const nativeBNB = parseFloat(import_ethers2.ethers.formatEther(nativeBalance)).toFixed(4);
|
|
730
1696
|
const isTestnet = chainName === "bnb_testnet";
|
|
731
1697
|
if (isTestnet) {
|
|
732
1698
|
throw new Error(
|
|
@@ -760,35 +1726,21 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
760
1726
|
);
|
|
761
1727
|
}
|
|
762
1728
|
const amountWei = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
|
|
763
|
-
const
|
|
1729
|
+
const intentNonce = Date.now();
|
|
1730
|
+
const intentDeadline = Date.now() + 36e5;
|
|
1731
|
+
const envelope = buildBnbIntentTypedData({
|
|
764
1732
|
from: this.wallet.address,
|
|
765
1733
|
to,
|
|
766
1734
|
amount: amountWei,
|
|
767
|
-
|
|
1735
|
+
tokenAddress: tokenConfig.address,
|
|
768
1736
|
service,
|
|
769
|
-
nonce:
|
|
770
|
-
|
|
771
|
-
deadline: Date.now() + 36e5
|
|
772
|
-
// 1 hour
|
|
773
|
-
};
|
|
774
|
-
const domain = {
|
|
775
|
-
name: "MoltsPay",
|
|
776
|
-
version: "1",
|
|
1737
|
+
nonce: intentNonce,
|
|
1738
|
+
deadline: intentDeadline,
|
|
777
1739
|
chainId: chain.chainId
|
|
778
|
-
};
|
|
779
|
-
const types = {
|
|
780
|
-
PaymentIntent: [
|
|
781
|
-
{ name: "from", type: "address" },
|
|
782
|
-
{ name: "to", type: "address" },
|
|
783
|
-
{ name: "amount", type: "uint256" },
|
|
784
|
-
{ name: "token", type: "address" },
|
|
785
|
-
{ name: "service", type: "string" },
|
|
786
|
-
{ name: "nonce", type: "uint256" },
|
|
787
|
-
{ name: "deadline", type: "uint256" }
|
|
788
|
-
]
|
|
789
|
-
};
|
|
1740
|
+
});
|
|
790
1741
|
console.log(`[MoltsPay] Signing BNB payment intent...`);
|
|
791
|
-
const signature = await this.
|
|
1742
|
+
const signature = await this.signer.signTypedData(envelope);
|
|
1743
|
+
const intent = envelope.message;
|
|
792
1744
|
const network = `eip155:${chain.chainId}`;
|
|
793
1745
|
const payload = {
|
|
794
1746
|
x402Version: 2,
|
|
@@ -849,11 +1801,11 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
849
1801
|
throw new Error("Missing payTo address in payment requirements");
|
|
850
1802
|
}
|
|
851
1803
|
const solanaFeePayer = requirements.extra?.solanaFeePayer;
|
|
852
|
-
const feePayerPubkey = solanaFeePayer ? new
|
|
1804
|
+
const feePayerPubkey = solanaFeePayer ? new import_web35.PublicKey(solanaFeePayer) : void 0;
|
|
853
1805
|
if (feePayerPubkey) {
|
|
854
1806
|
console.log(`[MoltsPay] Gasless mode: server pays fees`);
|
|
855
1807
|
}
|
|
856
|
-
const recipientPubkey = new
|
|
1808
|
+
const recipientPubkey = new import_web35.PublicKey(requirements.payTo);
|
|
857
1809
|
const transaction = await createSolanaPaymentTransaction(
|
|
858
1810
|
solanaWallet.publicKey,
|
|
859
1811
|
recipientPubkey,
|
|
@@ -862,12 +1814,11 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
862
1814
|
feePayerPubkey
|
|
863
1815
|
// Optional fee payer for gasless mode
|
|
864
1816
|
);
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
}
|
|
870
|
-
const signedTx = transaction.serialize({ requireAllSignatures: false }).toString("base64");
|
|
1817
|
+
const unsignedBase64 = transaction.serialize({ requireAllSignatures: false, verifySignatures: false }).toString("base64");
|
|
1818
|
+
const signedTx = await this.signer.signSolanaTransaction({
|
|
1819
|
+
transactionBase64: unsignedBase64,
|
|
1820
|
+
partialSign: !!feePayerPubkey
|
|
1821
|
+
});
|
|
871
1822
|
console.log(`[MoltsPay] Transaction signed, sending to server...`);
|
|
872
1823
|
const network = chain === "solana" ? "solana:mainnet" : "solana:devnet";
|
|
873
1824
|
const payload = {
|
|
@@ -914,7 +1865,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
914
1865
|
* Check ERC20 allowance for a spender
|
|
915
1866
|
*/
|
|
916
1867
|
async checkAllowance(tokenAddress, spender, provider) {
|
|
917
|
-
const contract = new
|
|
1868
|
+
const contract = new import_ethers2.ethers.Contract(
|
|
918
1869
|
tokenAddress,
|
|
919
1870
|
["function allowance(address owner, address spender) view returns (uint256)"],
|
|
920
1871
|
provider
|
|
@@ -925,41 +1876,29 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
925
1876
|
* Sign EIP-3009 transferWithAuthorization (GASLESS)
|
|
926
1877
|
* This only signs - no on-chain transaction, no gas needed.
|
|
927
1878
|
* Supports both USDC and USDT.
|
|
1879
|
+
*
|
|
1880
|
+
* Delegates typed-data construction to `core/eip3009.ts` and the signature
|
|
1881
|
+
* itself to `this.signer`. That way Web Client (Phase 4) can reuse the same
|
|
1882
|
+
* flow with an EIP-1193 signer without duplicating typed-data layout.
|
|
928
1883
|
*/
|
|
929
1884
|
async signEIP3009(to, amount, chain, token = "USDC", domainOverride) {
|
|
930
|
-
const validAfter = 0;
|
|
931
|
-
const validBefore = Math.floor(Date.now() / 1e3) + 3600;
|
|
932
|
-
const nonce = import_ethers.ethers.hexlify(import_ethers.ethers.randomBytes(32));
|
|
933
1885
|
const tokenConfig = chain.tokens[token];
|
|
934
1886
|
const value = BigInt(Math.floor(amount * 10 ** tokenConfig.decimals)).toString();
|
|
935
|
-
const
|
|
1887
|
+
const nonce = import_ethers2.ethers.hexlify(import_ethers2.ethers.randomBytes(32));
|
|
1888
|
+
const tokenName = domainOverride?.name || tokenConfig.eip712Name || (token === "USDC" ? "USD Coin" : "Tether USD");
|
|
1889
|
+
const tokenVersion = domainOverride?.version || "2";
|
|
1890
|
+
const envelope = buildEIP3009TypedData({
|
|
936
1891
|
from: this.wallet.address,
|
|
937
1892
|
to,
|
|
938
1893
|
value,
|
|
939
|
-
|
|
940
|
-
validBefore: validBefore.toString(),
|
|
941
|
-
nonce
|
|
942
|
-
};
|
|
943
|
-
const tokenName = domainOverride?.name || tokenConfig.eip712Name || (token === "USDC" ? "USD Coin" : "Tether USD");
|
|
944
|
-
const tokenVersion = domainOverride?.version || "2";
|
|
945
|
-
const domain = {
|
|
946
|
-
name: tokenName,
|
|
947
|
-
version: tokenVersion,
|
|
1894
|
+
nonce,
|
|
948
1895
|
chainId: chain.chainId,
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
{ name: "value", type: "uint256" },
|
|
956
|
-
{ name: "validAfter", type: "uint256" },
|
|
957
|
-
{ name: "validBefore", type: "uint256" },
|
|
958
|
-
{ name: "nonce", type: "bytes32" }
|
|
959
|
-
]
|
|
960
|
-
};
|
|
961
|
-
const signature = await this.wallet.signTypedData(domain, types, authorization);
|
|
962
|
-
return { authorization, signature };
|
|
1896
|
+
tokenAddress: tokenConfig.address,
|
|
1897
|
+
tokenName,
|
|
1898
|
+
tokenVersion
|
|
1899
|
+
});
|
|
1900
|
+
const signature = await this.signer.signTypedData(envelope);
|
|
1901
|
+
return { authorization: envelope.message, signature };
|
|
963
1902
|
}
|
|
964
1903
|
/**
|
|
965
1904
|
* Check spending limits
|
|
@@ -991,7 +1930,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
991
1930
|
}
|
|
992
1931
|
// --- Config & Wallet Management ---
|
|
993
1932
|
loadConfig() {
|
|
994
|
-
const configPath = (0,
|
|
1933
|
+
const configPath = (0, import_path3.join)(this.configDir, "config.json");
|
|
995
1934
|
if ((0, import_fs2.existsSync)(configPath)) {
|
|
996
1935
|
const content = (0, import_fs2.readFileSync)(configPath, "utf-8");
|
|
997
1936
|
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
@@ -1000,14 +1939,14 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1000
1939
|
}
|
|
1001
1940
|
saveConfig() {
|
|
1002
1941
|
(0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
|
|
1003
|
-
const configPath = (0,
|
|
1942
|
+
const configPath = (0, import_path3.join)(this.configDir, "config.json");
|
|
1004
1943
|
(0, import_fs2.writeFileSync)(configPath, JSON.stringify(this.config, null, 2));
|
|
1005
1944
|
}
|
|
1006
1945
|
/**
|
|
1007
1946
|
* Load spending data from disk
|
|
1008
1947
|
*/
|
|
1009
1948
|
loadSpending() {
|
|
1010
|
-
const spendingPath = (0,
|
|
1949
|
+
const spendingPath = (0, import_path3.join)(this.configDir, "spending.json");
|
|
1011
1950
|
if ((0, import_fs2.existsSync)(spendingPath)) {
|
|
1012
1951
|
try {
|
|
1013
1952
|
const data = JSON.parse((0, import_fs2.readFileSync)(spendingPath, "utf-8"));
|
|
@@ -1030,7 +1969,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1030
1969
|
*/
|
|
1031
1970
|
saveSpending() {
|
|
1032
1971
|
(0, import_fs2.mkdirSync)(this.configDir, { recursive: true });
|
|
1033
|
-
const spendingPath = (0,
|
|
1972
|
+
const spendingPath = (0, import_path3.join)(this.configDir, "spending.json");
|
|
1034
1973
|
const data = {
|
|
1035
1974
|
date: this.lastSpendingReset || (/* @__PURE__ */ new Date()).setHours(0, 0, 0, 0),
|
|
1036
1975
|
amount: this.todaySpending,
|
|
@@ -1039,7 +1978,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1039
1978
|
(0, import_fs2.writeFileSync)(spendingPath, JSON.stringify(data, null, 2));
|
|
1040
1979
|
}
|
|
1041
1980
|
loadWallet() {
|
|
1042
|
-
const walletPath = (0,
|
|
1981
|
+
const walletPath = (0, import_path3.join)(this.configDir, "wallet.json");
|
|
1043
1982
|
if ((0, import_fs2.existsSync)(walletPath)) {
|
|
1044
1983
|
if (process.platform !== "win32") {
|
|
1045
1984
|
try {
|
|
@@ -1063,13 +2002,13 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1063
2002
|
*/
|
|
1064
2003
|
static init(configDir, options) {
|
|
1065
2004
|
(0, import_fs2.mkdirSync)(configDir, { recursive: true });
|
|
1066
|
-
const wallet =
|
|
2005
|
+
const wallet = import_ethers2.Wallet.createRandom();
|
|
1067
2006
|
const walletData = {
|
|
1068
2007
|
address: wallet.address,
|
|
1069
2008
|
privateKey: wallet.privateKey,
|
|
1070
2009
|
createdAt: Date.now()
|
|
1071
2010
|
};
|
|
1072
|
-
const walletPath = (0,
|
|
2011
|
+
const walletPath = (0, import_path3.join)(configDir, "wallet.json");
|
|
1073
2012
|
(0, import_fs2.writeFileSync)(walletPath, JSON.stringify(walletData, null, 2), { mode: 384 });
|
|
1074
2013
|
const config = {
|
|
1075
2014
|
chain: options.chain,
|
|
@@ -1078,7 +2017,7 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1078
2017
|
maxPerDay: options.maxPerDay
|
|
1079
2018
|
}
|
|
1080
2019
|
};
|
|
1081
|
-
const configPath = (0,
|
|
2020
|
+
const configPath = (0, import_path3.join)(configDir, "config.json");
|
|
1082
2021
|
(0, import_fs2.writeFileSync)(configPath, JSON.stringify(config, null, 2));
|
|
1083
2022
|
return { address: wallet.address, configDir };
|
|
1084
2023
|
}
|
|
@@ -1095,17 +2034,17 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1095
2034
|
} catch {
|
|
1096
2035
|
throw new Error(`Unknown chain: ${this.config.chain}`);
|
|
1097
2036
|
}
|
|
1098
|
-
const provider = new
|
|
2037
|
+
const provider = new import_ethers2.ethers.JsonRpcProvider(chain.rpc);
|
|
1099
2038
|
const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
|
|
1100
2039
|
const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
|
|
1101
2040
|
provider.getBalance(this.wallet.address),
|
|
1102
|
-
new
|
|
1103
|
-
new
|
|
2041
|
+
new import_ethers2.ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
2042
|
+
new import_ethers2.ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
|
|
1104
2043
|
]);
|
|
1105
2044
|
return {
|
|
1106
|
-
usdc: parseFloat(
|
|
1107
|
-
usdt: parseFloat(
|
|
1108
|
-
native: parseFloat(
|
|
2045
|
+
usdc: parseFloat(import_ethers2.ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
|
|
2046
|
+
usdt: parseFloat(import_ethers2.ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
|
|
2047
|
+
native: parseFloat(import_ethers2.ethers.formatEther(nativeBalance))
|
|
1109
2048
|
};
|
|
1110
2049
|
}
|
|
1111
2050
|
/**
|
|
@@ -1128,38 +2067,38 @@ Run: npx moltspay approve --chain ${chainName} --spender ${spender}`
|
|
|
1128
2067
|
supportedChains.map(async (chainName) => {
|
|
1129
2068
|
try {
|
|
1130
2069
|
const chain = getChain(chainName);
|
|
1131
|
-
const provider = new
|
|
2070
|
+
const provider = new import_ethers2.ethers.JsonRpcProvider(chain.rpc);
|
|
1132
2071
|
if (chainName === "tempo_moderato") {
|
|
1133
2072
|
const [nativeBalance, pathUSD, alphaUSD, betaUSD, thetaUSD] = await Promise.all([
|
|
1134
2073
|
provider.getBalance(this.wallet.address),
|
|
1135
|
-
new
|
|
1136
|
-
new
|
|
1137
|
-
new
|
|
1138
|
-
new
|
|
2074
|
+
new import_ethers2.ethers.Contract(tempoTokens.pathUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
2075
|
+
new import_ethers2.ethers.Contract(tempoTokens.alphaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
2076
|
+
new import_ethers2.ethers.Contract(tempoTokens.betaUSD, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
2077
|
+
new import_ethers2.ethers.Contract(tempoTokens.thetaUSD, tokenAbi, provider).balanceOf(this.wallet.address)
|
|
1139
2078
|
]);
|
|
1140
2079
|
results[chainName] = {
|
|
1141
|
-
usdc: parseFloat(
|
|
2080
|
+
usdc: parseFloat(import_ethers2.ethers.formatUnits(pathUSD, 6)),
|
|
1142
2081
|
// pathUSD as default USDC
|
|
1143
|
-
usdt: parseFloat(
|
|
2082
|
+
usdt: parseFloat(import_ethers2.ethers.formatUnits(alphaUSD, 6)),
|
|
1144
2083
|
// alphaUSD as default USDT
|
|
1145
|
-
native: parseFloat(
|
|
2084
|
+
native: parseFloat(import_ethers2.ethers.formatEther(nativeBalance)),
|
|
1146
2085
|
tempo: {
|
|
1147
|
-
pathUSD: parseFloat(
|
|
1148
|
-
alphaUSD: parseFloat(
|
|
1149
|
-
betaUSD: parseFloat(
|
|
1150
|
-
thetaUSD: parseFloat(
|
|
2086
|
+
pathUSD: parseFloat(import_ethers2.ethers.formatUnits(pathUSD, 6)),
|
|
2087
|
+
alphaUSD: parseFloat(import_ethers2.ethers.formatUnits(alphaUSD, 6)),
|
|
2088
|
+
betaUSD: parseFloat(import_ethers2.ethers.formatUnits(betaUSD, 6)),
|
|
2089
|
+
thetaUSD: parseFloat(import_ethers2.ethers.formatUnits(thetaUSD, 6))
|
|
1151
2090
|
}
|
|
1152
2091
|
};
|
|
1153
2092
|
} else {
|
|
1154
2093
|
const [nativeBalance, usdcBalance, usdtBalance] = await Promise.all([
|
|
1155
2094
|
provider.getBalance(this.wallet.address),
|
|
1156
|
-
new
|
|
1157
|
-
new
|
|
2095
|
+
new import_ethers2.ethers.Contract(chain.tokens.USDC.address, tokenAbi, provider).balanceOf(this.wallet.address),
|
|
2096
|
+
new import_ethers2.ethers.Contract(chain.tokens.USDT.address, tokenAbi, provider).balanceOf(this.wallet.address)
|
|
1158
2097
|
]);
|
|
1159
2098
|
results[chainName] = {
|
|
1160
|
-
usdc: parseFloat(
|
|
1161
|
-
usdt: parseFloat(
|
|
1162
|
-
native: parseFloat(
|
|
2099
|
+
usdc: parseFloat(import_ethers2.ethers.formatUnits(usdcBalance, chain.tokens.USDC.decimals)),
|
|
2100
|
+
usdt: parseFloat(import_ethers2.ethers.formatUnits(usdtBalance, chain.tokens.USDT.decimals)),
|
|
2101
|
+
native: parseFloat(import_ethers2.ethers.formatEther(nativeBalance))
|
|
1163
2102
|
};
|
|
1164
2103
|
}
|
|
1165
2104
|
} catch (err2) {
|
|
@@ -1317,9 +2256,9 @@ function wrap(fn) {
|
|
|
1317
2256
|
}
|
|
1318
2257
|
function getPackageVersion() {
|
|
1319
2258
|
const candidates = [
|
|
1320
|
-
(0,
|
|
1321
|
-
(0,
|
|
1322
|
-
(0,
|
|
2259
|
+
(0, import_path4.join)(__dirname, "../../package.json"),
|
|
2260
|
+
(0, import_path4.join)(__dirname, "../package.json"),
|
|
2261
|
+
(0, import_path4.join)(process.cwd(), "node_modules/moltspay/package.json")
|
|
1323
2262
|
];
|
|
1324
2263
|
for (const loc of candidates) {
|
|
1325
2264
|
try {
|