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