moltspay 2.0.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.env.example +23 -0
  2. package/CHANGELOG.md +551 -0
  3. package/README.md +466 -8
  4. package/dist/cdp/index.js.map +1 -1
  5. package/dist/cdp/index.mjs.map +1 -1
  6. package/dist/chains/index.d.mts +69 -4
  7. package/dist/chains/index.d.ts +69 -4
  8. package/dist/chains/index.js +45 -1
  9. package/dist/chains/index.js.map +1 -1
  10. package/dist/chains/index.mjs +39 -1
  11. package/dist/chains/index.mjs.map +1 -1
  12. package/dist/cli/index.js +5444 -2126
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/cli/index.mjs +5457 -2133
  15. package/dist/cli/index.mjs.map +1 -1
  16. package/dist/client/index.d.mts +307 -1
  17. package/dist/client/index.d.ts +307 -1
  18. package/dist/client/index.js +639 -34
  19. package/dist/client/index.js.map +1 -1
  20. package/dist/client/index.mjs +656 -52
  21. package/dist/client/index.mjs.map +1 -1
  22. package/dist/client/web/index.mjs.map +1 -1
  23. package/dist/facilitators/index.d.mts +512 -10
  24. package/dist/facilitators/index.d.ts +512 -10
  25. package/dist/facilitators/index.js +925 -13
  26. package/dist/facilitators/index.js.map +1 -1
  27. package/dist/facilitators/index.mjs +906 -12
  28. package/dist/facilitators/index.mjs.map +1 -1
  29. package/dist/index.d.mts +2 -2
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +2843 -551
  32. package/dist/index.js.map +1 -1
  33. package/dist/index.mjs +2849 -558
  34. package/dist/index.mjs.map +1 -1
  35. package/dist/mcp/index.js +635 -32
  36. package/dist/mcp/index.js.map +1 -1
  37. package/dist/mcp/index.mjs +660 -57
  38. package/dist/mcp/index.mjs.map +1 -1
  39. package/dist/server/index.d.mts +252 -11
  40. package/dist/server/index.d.ts +252 -11
  41. package/dist/server/index.js +2049 -261
  42. package/dist/server/index.js.map +1 -1
  43. package/dist/server/index.mjs +2049 -261
  44. package/dist/server/index.mjs.map +1 -1
  45. package/dist/verify/index.js.map +1 -1
  46. package/dist/verify/index.mjs.map +1 -1
  47. package/dist/wallet/index.js.map +1 -1
  48. package/dist/wallet/index.mjs.map +1 -1
  49. package/package.json +14 -2
  50. package/schemas/moltspay.services.schema.json +127 -16
@@ -851,12 +851,12 @@ var BNBFacilitator = class extends BaseFacilitator {
851
851
  return this.spenderAddress;
852
852
  }
853
853
  async getServerAddress() {
854
- const { ethers: ethers2 } = await import("ethers");
855
- const wallet = new ethers2.Wallet(this.serverPrivateKey);
854
+ const { ethers: ethers3 } = await import("ethers");
855
+ const wallet = new ethers3.Wallet(this.serverPrivateKey);
856
856
  return wallet.address;
857
857
  }
858
858
  async recoverIntentSigner(intent, chainId) {
859
- const { ethers: ethers2 } = await import("ethers");
859
+ const { ethers: ethers3 } = await import("ethers");
860
860
  const domain = {
861
861
  ...EIP712_DOMAIN,
862
862
  chainId
@@ -870,7 +870,7 @@ var BNBFacilitator = class extends BaseFacilitator {
870
870
  nonce: intent.nonce,
871
871
  deadline: intent.deadline
872
872
  };
873
- const recoveredAddress = ethers2.verifyTypedData(
873
+ const recoveredAddress = ethers3.verifyTypedData(
874
874
  domain,
875
875
  INTENT_TYPES,
876
876
  message,
@@ -914,10 +914,10 @@ var BNBFacilitator = class extends BaseFacilitator {
914
914
  return result.result || "0x0";
915
915
  }
916
916
  async executeTransferFrom(from, to, amount, token, rpcUrl) {
917
- const { ethers: ethers2 } = await import("ethers");
918
- const provider = new ethers2.JsonRpcProvider(rpcUrl);
919
- const wallet = new ethers2.Wallet(this.serverPrivateKey, provider);
920
- const tokenContract = new ethers2.Contract(token, [
917
+ const { ethers: ethers3 } = await import("ethers");
918
+ const provider = new ethers3.JsonRpcProvider(rpcUrl);
919
+ const wallet = new ethers3.Wallet(this.serverPrivateKey, provider);
920
+ const tokenContract = new ethers3.Contract(token, [
921
921
  "function transferFrom(address from, address to, uint256 amount) returns (bool)"
922
922
  ], wallet);
923
923
  const tx = await tokenContract.transferFrom(from, to, amount);
@@ -1308,7 +1308,7 @@ var ALIPAY_SIGNING_FIELDS = [
1308
1308
  ];
1309
1309
  var AlipayFacilitator = class extends BaseFacilitator {
1310
1310
  name = "alipay";
1311
- displayName = "Alipay AI \u6536";
1311
+ displayName = "Alipay AI Pay";
1312
1312
  supportedNetworks = [ALIPAY_NETWORK];
1313
1313
  config;
1314
1314
  constructor(config) {
@@ -1327,7 +1327,7 @@ var AlipayFacilitator = class extends BaseFacilitator {
1327
1327
  async createPaymentRequirements(opts) {
1328
1328
  if (!ALIPAY_AMOUNT_REGEX.test(opts.priceCny)) {
1329
1329
  throw new Error(
1330
- `AlipayFacilitator.createPaymentRequirements: priceCny "${opts.priceCny}" does not match /^\\d+(\\.\\d{1,2})?$/ (unit is \u5143, not \u5206; e.g. "1.00" not "100")`
1330
+ `AlipayFacilitator.createPaymentRequirements: priceCny "${opts.priceCny}" does not match /^\\d+(\\.\\d{1,2})?$/ (unit is yuan, not fen; e.g. "1.00" not "100")`
1331
1331
  );
1332
1332
  }
1333
1333
  const now = /* @__PURE__ */ new Date();
@@ -1440,7 +1440,7 @@ var AlipayFacilitator = class extends BaseFacilitator {
1440
1440
  * service resource has been returned to the buyer.
1441
1441
  *
1442
1442
  * Per the design (see ALIPAY-INTEGRATION-DESIGN.md §5.1, risk row
1443
- * "履约确认失败"), this is **fire-and-forget**: the caller (registry /
1443
+ * (a fulfillment-confirm failure), this is **fire-and-forget**: the caller (registry /
1444
1444
  * server) is expected to log fulfillment failures but NOT roll back
1445
1445
  * the already-delivered resource.
1446
1446
  *
@@ -1577,6 +1577,880 @@ function decodeProof(proofHeader) {
1577
1577
  return parsed;
1578
1578
  }
1579
1579
 
1580
+ // src/facilitators/wechat.ts
1581
+ import crypto4 from "crypto";
1582
+
1583
+ // src/facilitators/wechat/sign.ts
1584
+ import crypto3 from "crypto";
1585
+ var WECHAT_AUTH_SCHEMA = "WECHATPAY2-SHA256-RSA2048";
1586
+ function buildRequestMessage(method, urlPath, timestamp, nonce, body) {
1587
+ return `${method.toUpperCase()}
1588
+ ${urlPath}
1589
+ ${timestamp}
1590
+ ${nonce}
1591
+ ${body}
1592
+ `;
1593
+ }
1594
+ function wechatV3Sign(method, urlPath, timestamp, nonce, body, privateKeyPem) {
1595
+ const message = buildRequestMessage(method, urlPath, timestamp, nonce, body);
1596
+ const signer = crypto3.createSign("RSA-SHA256");
1597
+ signer.update(message, "utf-8");
1598
+ signer.end();
1599
+ return signer.sign(privateKeyPem, "base64");
1600
+ }
1601
+ function buildAuthorizationToken(args) {
1602
+ const fields = [
1603
+ `mchid="${args.mchid}"`,
1604
+ `nonce_str="${args.nonce}"`,
1605
+ `signature="${args.signature}"`,
1606
+ `timestamp="${args.timestamp}"`,
1607
+ `serial_no="${args.serialNo}"`
1608
+ ].join(",");
1609
+ return `${WECHAT_AUTH_SCHEMA} ${fields}`;
1610
+ }
1611
+ function wechatV3VerifyResponse(timestamp, nonce, body, signature, platformPublicKeyPem) {
1612
+ try {
1613
+ const message = `${timestamp}
1614
+ ${nonce}
1615
+ ${body}
1616
+ `;
1617
+ const verifier = crypto3.createVerify("RSA-SHA256");
1618
+ verifier.update(message, "utf-8");
1619
+ verifier.end();
1620
+ return verifier.verify(platformPublicKeyPem, signature, "base64");
1621
+ } catch {
1622
+ return false;
1623
+ }
1624
+ }
1625
+ function generateNonce() {
1626
+ return crypto3.randomBytes(16).toString("hex");
1627
+ }
1628
+
1629
+ // src/facilitators/wechat/api.ts
1630
+ var WECHAT_API_BASE = "https://api.mch.weixin.qq.com";
1631
+ var WechatApiError = class extends Error {
1632
+ status;
1633
+ code;
1634
+ constructor(message, status, code) {
1635
+ super(message);
1636
+ this.name = "WechatApiError";
1637
+ this.status = status;
1638
+ this.code = code;
1639
+ }
1640
+ };
1641
+ async function wechatV3Call(method, urlPath, body, config) {
1642
+ const base = config.api_base ?? WECHAT_API_BASE;
1643
+ const bodyStr = body === null ? "" : JSON.stringify(body);
1644
+ const timestamp = String(Math.floor(Date.now() / 1e3));
1645
+ const nonce = generateNonce();
1646
+ const signature = wechatV3Sign(
1647
+ method,
1648
+ urlPath,
1649
+ timestamp,
1650
+ nonce,
1651
+ bodyStr,
1652
+ config.private_key_pem
1653
+ );
1654
+ const authorization = buildAuthorizationToken({
1655
+ mchid: config.mchid,
1656
+ serialNo: config.serial_no,
1657
+ nonce,
1658
+ timestamp,
1659
+ signature
1660
+ });
1661
+ const response = await fetch(`${base}${urlPath}`, {
1662
+ method,
1663
+ headers: {
1664
+ Authorization: authorization,
1665
+ Accept: "application/json",
1666
+ "Content-Type": "application/json",
1667
+ // WeChat requires a non-empty UA; some edge nodes 403 a blank one.
1668
+ "User-Agent": "moltspay-wechat/1.0"
1669
+ },
1670
+ body: method === "GET" ? void 0 : bodyStr
1671
+ });
1672
+ const text = await response.text();
1673
+ if (config.platform_public_key_pem && text.length > 0) {
1674
+ const ts = response.headers.get("Wechatpay-Timestamp");
1675
+ const nc = response.headers.get("Wechatpay-Nonce");
1676
+ const sig = response.headers.get("Wechatpay-Signature");
1677
+ if (!ts || !nc || !sig) {
1678
+ throw new Error(
1679
+ `WeChat v3 ${method} ${urlPath}: response missing Wechatpay-Signature headers`
1680
+ );
1681
+ }
1682
+ if (!wechatV3VerifyResponse(ts, nc, text, sig, config.platform_public_key_pem)) {
1683
+ throw new Error(
1684
+ `WeChat v3 ${method} ${urlPath}: response signature verification failed`
1685
+ );
1686
+ }
1687
+ }
1688
+ let json = {};
1689
+ if (text.length > 0) {
1690
+ try {
1691
+ json = JSON.parse(text);
1692
+ } catch {
1693
+ throw new Error(
1694
+ `WeChat v3 ${method} ${urlPath}: non-JSON response (HTTP ${response.status}): ${text.slice(0, 300)}`
1695
+ );
1696
+ }
1697
+ }
1698
+ if (!response.ok) {
1699
+ const code = typeof json.code === "string" ? json.code : void 0;
1700
+ const message = typeof json.message === "string" ? json.message : text.slice(0, 300);
1701
+ throw new WechatApiError(
1702
+ `WeChat v3 ${method} ${urlPath} failed: HTTP ${response.status}${code ? ` ${code}` : ""}: ${message}`,
1703
+ response.status,
1704
+ code
1705
+ );
1706
+ }
1707
+ return { status: response.status, body: json };
1708
+ }
1709
+
1710
+ // src/facilitators/wechat.ts
1711
+ var WECHAT_NETWORK = "wechat";
1712
+ var WECHAT_SCHEME = "wechatpay-native";
1713
+ var WECHAT_AMOUNT_REGEX = /^\d+(\.\d{1,2})?$/;
1714
+ var WECHAT_TIME_EXPIRE_MS = 5 * 60 * 1e3;
1715
+ var WechatFacilitator = class extends BaseFacilitator {
1716
+ name = "wechat";
1717
+ displayName = "WeChat Pay";
1718
+ supportedNetworks = [WECHAT_NETWORK];
1719
+ config;
1720
+ constructor(config) {
1721
+ super();
1722
+ this.config = { api_base: WECHAT_API_BASE, ...config };
1723
+ }
1724
+ /**
1725
+ * Place a Native order and build the 402 challenge. The returned
1726
+ * `code_url` is payer-agnostic — any WeChat user may scan it.
1727
+ */
1728
+ async createPaymentRequirements(opts) {
1729
+ if (!WECHAT_AMOUNT_REGEX.test(opts.priceCny)) {
1730
+ throw new Error(
1731
+ `WechatFacilitator.createPaymentRequirements: priceCny "${opts.priceCny}" does not match /^\\d+(\\.\\d{1,2})?$/ (unit is yuan; e.g. "10.00")`
1732
+ );
1733
+ }
1734
+ const total = cnyToFen(opts.priceCny);
1735
+ if (total < 1) {
1736
+ throw new Error(
1737
+ `WechatFacilitator.createPaymentRequirements: amount ${total} fen is below the 1 fen minimum`
1738
+ );
1739
+ }
1740
+ const outTradeNo = opts.outTradeNo ?? generateOutTradeNo2();
1741
+ const expiresInMs = opts.expiresInMs ?? WECHAT_TIME_EXPIRE_MS;
1742
+ const body = {
1743
+ appid: this.config.appid,
1744
+ mchid: this.config.mchid,
1745
+ description: opts.description,
1746
+ out_trade_no: outTradeNo,
1747
+ notify_url: this.config.notify_url,
1748
+ time_expire: formatTimeExpire(new Date(Date.now() + expiresInMs)),
1749
+ amount: { total, currency: "CNY" }
1750
+ };
1751
+ if (opts.attach) {
1752
+ const attachStr = JSON.stringify(opts.attach);
1753
+ if (Buffer.byteLength(attachStr, "utf8") > 128) {
1754
+ throw new Error(
1755
+ `WechatFacilitator.createPaymentRequirements: attach exceeds WeChat's 128-byte limit (${Buffer.byteLength(attachStr, "utf8")} bytes)`
1756
+ );
1757
+ }
1758
+ body.attach = attachStr;
1759
+ }
1760
+ const { body: resp } = await wechatV3Call(
1761
+ "POST",
1762
+ "/v3/pay/transactions/native",
1763
+ body,
1764
+ this.getApiConfig()
1765
+ );
1766
+ const codeUrl = resp.code_url;
1767
+ if (typeof codeUrl !== "string" || codeUrl.length === 0) {
1768
+ throw new Error(
1769
+ `WeChat Native order returned no code_url: ${JSON.stringify(resp).slice(0, 300)}`
1770
+ );
1771
+ }
1772
+ const x402Accepts = {
1773
+ scheme: WECHAT_SCHEME,
1774
+ network: WECHAT_NETWORK,
1775
+ asset: "CNY",
1776
+ amount: opts.priceCny,
1777
+ payTo: this.config.mchid,
1778
+ maxTimeoutSeconds: Math.floor(expiresInMs / 1e3),
1779
+ extra: {
1780
+ code_url: codeUrl,
1781
+ out_trade_no: outTradeNo
1782
+ }
1783
+ };
1784
+ return { x402Accepts, codeUrl, outTradeNo };
1785
+ }
1786
+ /**
1787
+ * Poll an order: `trade_state === 'SUCCESS'` ⇒ paid. All failure modes
1788
+ * (missing out_trade_no, gateway error, not-yet-paid) return
1789
+ * `{ valid: false, error }`; no exception escapes.
1790
+ */
1791
+ async verify(paymentPayload, requirements) {
1792
+ try {
1793
+ const outTradeNo = extractOutTradeNo(paymentPayload, requirements);
1794
+ const resp = await this.queryOrder(outTradeNo);
1795
+ const tradeState = resp.trade_state;
1796
+ if (tradeState !== "SUCCESS") {
1797
+ return {
1798
+ valid: false,
1799
+ error: `wechat trade_state ${tradeState ?? "UNKNOWN"}`,
1800
+ details: { trade_state: tradeState, out_trade_no: outTradeNo }
1801
+ };
1802
+ }
1803
+ return {
1804
+ valid: true,
1805
+ details: {
1806
+ trade_state: tradeState,
1807
+ transaction_id: resp.transaction_id,
1808
+ out_trade_no: resp.out_trade_no ?? outTradeNo,
1809
+ amount: resp.amount,
1810
+ attach: resp.attach,
1811
+ // Payer identity, gateway-attested. Present on a SUCCESS Native
1812
+ // order even though order creation was payer-agnostic; anchors the
1813
+ // custodial balance to a real WeChat user. @see WECHAT fiat auth design.
1814
+ openid: resp.payer?.openid
1815
+ }
1816
+ };
1817
+ } catch (e) {
1818
+ return { valid: false, error: e instanceof Error ? e.message : String(e) };
1819
+ }
1820
+ }
1821
+ /**
1822
+ * Confirm settlement. Native captures funds at SUCCESS, so this is an
1823
+ * idempotent re-confirm that returns the `transaction_id`. Like Alipay's
1824
+ * fulfillment confirm, failures are surfaced but non-fatal to an
1825
+ * already-delivered resource (caller logs, does not roll back).
1826
+ */
1827
+ async settle(paymentPayload, requirements) {
1828
+ try {
1829
+ const outTradeNo = extractOutTradeNo(paymentPayload, requirements);
1830
+ const resp = await this.queryOrder(outTradeNo);
1831
+ const tradeState = resp.trade_state;
1832
+ const transactionId = resp.transaction_id;
1833
+ if (tradeState !== "SUCCESS") {
1834
+ return {
1835
+ success: false,
1836
+ transaction: transactionId,
1837
+ error: `wechat trade_state ${tradeState ?? "UNKNOWN"} (expected SUCCESS)`,
1838
+ status: tradeState
1839
+ };
1840
+ }
1841
+ return { success: true, transaction: transactionId, status: "fulfilled" };
1842
+ } catch (e) {
1843
+ return { success: false, error: e instanceof Error ? e.message : String(e) };
1844
+ }
1845
+ }
1846
+ /**
1847
+ * Validate keys parse, apiv3 key length, and gateway reachability. Does
1848
+ * NOT make a business API call.
1849
+ */
1850
+ async healthCheck() {
1851
+ const start = Date.now();
1852
+ try {
1853
+ crypto4.createPrivateKey(this.config.private_key_pem);
1854
+ } catch (e) {
1855
+ return {
1856
+ healthy: false,
1857
+ error: `merchant private_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
1858
+ };
1859
+ }
1860
+ if (this.config.platform_public_key_pem) {
1861
+ try {
1862
+ crypto4.createPublicKey(this.config.platform_public_key_pem);
1863
+ } catch (e) {
1864
+ return {
1865
+ healthy: false,
1866
+ error: `platform_public_key_pem parse failed: ${e instanceof Error ? e.message : String(e)}`
1867
+ };
1868
+ }
1869
+ }
1870
+ if (this.config.apiv3_key !== void 0 && Buffer.byteLength(this.config.apiv3_key, "utf-8") !== 32) {
1871
+ return { healthy: false, error: "apiv3_key must be exactly 32 bytes" };
1872
+ }
1873
+ const base = this.config.api_base ?? WECHAT_API_BASE;
1874
+ const controller = new AbortController();
1875
+ const timeout = setTimeout(() => controller.abort(), 5e3);
1876
+ const response = await fetch(base, { method: "HEAD", signal: controller.signal }).catch(() => null);
1877
+ clearTimeout(timeout);
1878
+ const latencyMs = Date.now() - start;
1879
+ if (!response) {
1880
+ return { healthy: false, error: `gateway unreachable: ${base}`, latencyMs };
1881
+ }
1882
+ return { healthy: true, latencyMs };
1883
+ }
1884
+ /** Query a Native order by out_trade_no. The query string is part of the signed path. */
1885
+ async queryOrder(outTradeNo) {
1886
+ const path2 = `/v3/pay/transactions/out-trade-no/${encodeURIComponent(outTradeNo)}?mchid=${encodeURIComponent(this.config.mchid)}`;
1887
+ const { body } = await wechatV3Call("GET", path2, null, this.getApiConfig());
1888
+ return body;
1889
+ }
1890
+ /** Project the facilitator config down to what api.ts needs. */
1891
+ getApiConfig() {
1892
+ return {
1893
+ mchid: this.config.mchid,
1894
+ serial_no: this.config.serial_no,
1895
+ private_key_pem: this.config.private_key_pem,
1896
+ platform_public_key_pem: this.config.platform_public_key_pem,
1897
+ api_base: this.config.api_base
1898
+ };
1899
+ }
1900
+ };
1901
+ function cnyToFen(cny) {
1902
+ return Math.round(parseFloat(cny) * 100);
1903
+ }
1904
+ function generateOutTradeNo2() {
1905
+ return "WX" + crypto4.randomBytes(15).toString("hex");
1906
+ }
1907
+ function parseWechatAttach(attach) {
1908
+ if (typeof attach !== "string" || attach.length === 0) return null;
1909
+ try {
1910
+ const parsed = JSON.parse(attach);
1911
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
1912
+ } catch {
1913
+ return null;
1914
+ }
1915
+ }
1916
+ function formatTimeExpire(d) {
1917
+ const pad = (n) => String(n).padStart(2, "0");
1918
+ const offMin = -d.getTimezoneOffset();
1919
+ const sign = offMin >= 0 ? "+" : "-";
1920
+ const abs = Math.abs(offMin);
1921
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
1922
+ }
1923
+ function extractOutTradeNo(paymentPayload, requirements) {
1924
+ const p = paymentPayload?.payload;
1925
+ if (typeof p === "string" && p.length > 0) return p;
1926
+ if (p !== null && typeof p === "object") {
1927
+ const obj = p;
1928
+ const cand = obj.out_trade_no ?? obj.outTradeNo;
1929
+ if (typeof cand === "string" && cand.length > 0) return cand;
1930
+ }
1931
+ const fromReq = requirements?.extra?.out_trade_no;
1932
+ if (typeof fromReq === "string" && fromReq.length > 0) return fromReq;
1933
+ throw new Error(
1934
+ "wechat payment payload must carry out_trade_no (string, {out_trade_no}/{outTradeNo}, or requirements.extra.out_trade_no)"
1935
+ );
1936
+ }
1937
+
1938
+ // src/facilitators/balance/ledger.ts
1939
+ import { randomUUID } from "crypto";
1940
+ var DEFAULT_SINGLE_LIMIT_SAT = 500;
1941
+ var DEFAULT_DAILY_LIMIT_SAT = 1e3;
1942
+ function toSat(amount) {
1943
+ const s = typeof amount === "number" ? amount.toFixed(2) : amount.trim();
1944
+ if (!/^\d+(\.\d{1,2})?$/.test(s)) {
1945
+ throw new Error(`Invalid amount "${amount}": expected a non-negative decimal with <= 2 places`);
1946
+ }
1947
+ const [whole, frac = ""] = s.split(".");
1948
+ return parseInt(whole, 10) * 100 + parseInt(frac.padEnd(2, "0") || "0", 10);
1949
+ }
1950
+ function fromSat(sat) {
1951
+ return (sat / 100).toFixed(2);
1952
+ }
1953
+ var BalanceLedger = class {
1954
+ db;
1955
+ defaultSingleLimitSat;
1956
+ defaultDailyLimitSat;
1957
+ constructor(config) {
1958
+ const getBuiltin = process.getBuiltinModule;
1959
+ const sqlite = getBuiltin?.("node:sqlite");
1960
+ if (!sqlite?.DatabaseSync) {
1961
+ throw new Error(
1962
+ `The balance rail requires the node:sqlite module (Node.js >= 22.5). Current version: ${process.version}. Upgrade Node or disable provider.balance.`
1963
+ );
1964
+ }
1965
+ const { DatabaseSync } = sqlite;
1966
+ this.db = new DatabaseSync(config.dbPath);
1967
+ this.defaultSingleLimitSat = config.defaultSingleLimitSat ?? DEFAULT_SINGLE_LIMIT_SAT;
1968
+ this.defaultDailyLimitSat = config.defaultDailyLimitSat ?? DEFAULT_DAILY_LIMIT_SAT;
1969
+ this.db.exec("PRAGMA journal_mode = WAL");
1970
+ this.db.exec(`
1971
+ CREATE TABLE IF NOT EXISTS buyers (
1972
+ buyer_id TEXT PRIMARY KEY,
1973
+ display_name TEXT,
1974
+ balance_sat INTEGER NOT NULL DEFAULT 0,
1975
+ total_topup_sat INTEGER NOT NULL DEFAULT 0,
1976
+ total_spent_sat INTEGER NOT NULL DEFAULT 0,
1977
+ daily_limit_sat INTEGER NOT NULL,
1978
+ single_limit_sat INTEGER NOT NULL,
1979
+ status TEXT NOT NULL DEFAULT 'active',
1980
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1981
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1982
+ );
1983
+ CREATE TABLE IF NOT EXISTS ledger_transactions (
1984
+ id TEXT PRIMARY KEY,
1985
+ buyer_id TEXT NOT NULL REFERENCES buyers(buyer_id),
1986
+ type TEXT NOT NULL,
1987
+ amount_sat INTEGER NOT NULL,
1988
+ service TEXT,
1989
+ description TEXT,
1990
+ request_id TEXT,
1991
+ external_ref TEXT,
1992
+ refunds_tx_id TEXT,
1993
+ status TEXT NOT NULL DEFAULT 'completed',
1994
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1995
+ );
1996
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_ledger_request_id
1997
+ ON ledger_transactions(request_id) WHERE request_id IS NOT NULL;
1998
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_ledger_external_ref
1999
+ ON ledger_transactions(external_ref) WHERE external_ref IS NOT NULL;
2000
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_ledger_refunds_tx
2001
+ ON ledger_transactions(refunds_tx_id) WHERE refunds_tx_id IS NOT NULL;
2002
+ CREATE INDEX IF NOT EXISTS idx_ledger_buyer_time
2003
+ ON ledger_transactions(buyer_id, created_at);
2004
+ CREATE TABLE IF NOT EXISTS ledger_meta (
2005
+ key TEXT PRIMARY KEY,
2006
+ value TEXT NOT NULL
2007
+ );
2008
+ `);
2009
+ const cols = this.db.prepare("PRAGMA table_info(buyers)").all().map((c) => c.name);
2010
+ if (!cols.includes("signer_address")) {
2011
+ this.db.exec("ALTER TABLE buyers ADD COLUMN signer_address TEXT");
2012
+ }
2013
+ if (!cols.includes("wechat_openid")) {
2014
+ this.db.exec("ALTER TABLE buyers ADD COLUMN wechat_openid TEXT");
2015
+ }
2016
+ const currency = config.currency ?? "USD";
2017
+ const existingCurrency = this.db.prepare(`SELECT value FROM ledger_meta WHERE key = 'currency'`).get();
2018
+ if (existingCurrency) {
2019
+ if (existingCurrency.value !== currency) {
2020
+ throw new Error(
2021
+ `Balance ledger currency mismatch: db=${existingCurrency.value} config=${currency}. Use a separate db_path for a different currency; do not reinterpret an existing ledger.`
2022
+ );
2023
+ }
2024
+ } else {
2025
+ this.db.prepare(`INSERT INTO ledger_meta (key, value) VALUES ('currency', ?)`).run(currency);
2026
+ }
2027
+ }
2028
+ /** Fetch a buyer, or null. */
2029
+ getBuyer(buyerId) {
2030
+ const row = this.db.prepare("SELECT * FROM buyers WHERE buyer_id = ?").get(buyerId);
2031
+ return row ?? null;
2032
+ }
2033
+ /** Fetch a buyer, creating an empty active account on first sight. */
2034
+ getOrCreateBuyer(buyerId, displayName) {
2035
+ const existing = this.getBuyer(buyerId);
2036
+ if (existing) return existing;
2037
+ this.db.prepare(
2038
+ `INSERT INTO buyers (buyer_id, display_name, daily_limit_sat, single_limit_sat)
2039
+ VALUES (?, ?, ?, ?)`
2040
+ ).run(buyerId, displayName ?? null, this.defaultDailyLimitSat, this.defaultSingleLimitSat);
2041
+ return this.getBuyer(buyerId);
2042
+ }
2043
+ /**
2044
+ * Record the gateway-attested WeChat payer openid that funded a buyer, on
2045
+ * top-up confirm. Idempotent and observational (stage 1a): it never
2046
+ * overwrites an existing binding with a *different* openid — a conflict is
2047
+ * reported to the caller (possible account sharing / spoof) but not
2048
+ * enforced here. Creates the buyer if absent.
2049
+ */
2050
+ bindOpenid(buyerId, openid) {
2051
+ const buyer = this.getOrCreateBuyer(buyerId);
2052
+ if (buyer.wechat_openid === openid) {
2053
+ return { bound: true, conflict: false, existing: openid };
2054
+ }
2055
+ if (buyer.wechat_openid && buyer.wechat_openid !== openid) {
2056
+ return { bound: false, conflict: true, existing: buyer.wechat_openid };
2057
+ }
2058
+ this.db.prepare(`UPDATE buyers SET wechat_openid = ?, updated_at = datetime('now') WHERE buyer_id = ?`).run(openid, buyerId);
2059
+ return { bound: true, conflict: false, existing: null };
2060
+ }
2061
+ /**
2062
+ * TOFU-bind the account's spending signer address (EVM, lowercase). First
2063
+ * signed request records it; later requests must match. A mismatch is
2064
+ * reported (`conflict`) so the caller can reject under `enforce` — it is
2065
+ * never silently overwritten. Creates the buyer if absent.
2066
+ */
2067
+ bindSigner(buyerId, address) {
2068
+ const a = address.toLowerCase();
2069
+ const buyer = this.getOrCreateBuyer(buyerId);
2070
+ if (buyer.signer_address === a) return { bound: true, conflict: false, existing: a };
2071
+ if (buyer.signer_address && buyer.signer_address !== a) {
2072
+ return { bound: false, conflict: true, existing: buyer.signer_address };
2073
+ }
2074
+ this.db.prepare(`UPDATE buyers SET signer_address = ?, updated_at = datetime('now') WHERE buyer_id = ?`).run(a, buyerId);
2075
+ return { bound: true, conflict: false, existing: null };
2076
+ }
2077
+ /** Sum of today's (UTC) completed deducts minus refunds issued against them. */
2078
+ spentTodaySat(buyerId) {
2079
+ const row = this.db.prepare(
2080
+ `SELECT COALESCE(SUM(CASE type WHEN 'deduct' THEN amount_sat ELSE -amount_sat END), 0) AS spent
2081
+ FROM ledger_transactions
2082
+ WHERE buyer_id = ? AND type IN ('deduct','refund') AND date(created_at) = date('now')`
2083
+ ).get(buyerId);
2084
+ return Math.max(0, row.spent);
2085
+ }
2086
+ /**
2087
+ * Read-only deduction precheck (the rail's `verify`). Never mutates.
2088
+ * Returns the same error codes `deduct` would.
2089
+ */
2090
+ checkDeduct(buyerId, amountSat) {
2091
+ const buyer = this.getBuyer(buyerId);
2092
+ if (!buyer) return { success: false, error: "buyer_not_found" };
2093
+ if (buyer.status !== "active") return { success: false, error: "buyer_not_active" };
2094
+ if (amountSat > buyer.single_limit_sat) {
2095
+ return { success: false, error: "exceeds_single_limit", limitSat: buyer.single_limit_sat };
2096
+ }
2097
+ const spent = this.spentTodaySat(buyerId);
2098
+ if (spent + amountSat > buyer.daily_limit_sat) {
2099
+ return { success: false, error: "exceeds_daily_limit", limitSat: buyer.daily_limit_sat };
2100
+ }
2101
+ if (buyer.balance_sat < amountSat) {
2102
+ return { success: false, error: "insufficient_balance", balanceSat: buyer.balance_sat };
2103
+ }
2104
+ return { success: true, balanceSat: buyer.balance_sat };
2105
+ }
2106
+ /**
2107
+ * Atomic deduction (the rail's `settle`). Check + UPDATE run inside one
2108
+ * SQLite transaction; a `request_id` replay returns the original tx
2109
+ * without charging again.
2110
+ */
2111
+ deduct(opts) {
2112
+ if (!Number.isInteger(opts.amountSat) || opts.amountSat <= 0) {
2113
+ throw new Error(`deduct amountSat must be a positive integer, got ${opts.amountSat}`);
2114
+ }
2115
+ if (opts.requestId) {
2116
+ const prior = this.db.prepare(`SELECT * FROM ledger_transactions WHERE request_id = ?`).get(opts.requestId);
2117
+ if (prior) {
2118
+ const buyer = this.getBuyer(prior.buyer_id);
2119
+ return { success: true, txId: prior.id, replayed: true, balanceSat: buyer.balance_sat };
2120
+ }
2121
+ }
2122
+ this.db.exec("BEGIN IMMEDIATE");
2123
+ try {
2124
+ const check = this.checkDeduct(opts.buyerId, opts.amountSat);
2125
+ if (!check.success) {
2126
+ this.db.exec("ROLLBACK");
2127
+ return check;
2128
+ }
2129
+ const updated = this.db.prepare(
2130
+ `UPDATE buyers SET
2131
+ balance_sat = balance_sat - ?,
2132
+ total_spent_sat = total_spent_sat + ?,
2133
+ updated_at = datetime('now')
2134
+ WHERE buyer_id = ? AND balance_sat >= ? AND status = 'active'`
2135
+ ).run(opts.amountSat, opts.amountSat, opts.buyerId, opts.amountSat);
2136
+ if (Number(updated.changes) !== 1) {
2137
+ this.db.exec("ROLLBACK");
2138
+ return { success: false, error: "insufficient_balance" };
2139
+ }
2140
+ const txId = `btx_${randomUUID()}`;
2141
+ this.db.prepare(
2142
+ `INSERT INTO ledger_transactions (id, buyer_id, type, amount_sat, service, description, request_id)
2143
+ VALUES (?, ?, 'deduct', ?, ?, ?, ?)`
2144
+ ).run(txId, opts.buyerId, opts.amountSat, opts.service ?? null, opts.description ?? null, opts.requestId ?? null);
2145
+ this.db.exec("COMMIT");
2146
+ const buyer = this.getBuyer(opts.buyerId);
2147
+ return { success: true, txId, balanceSat: buyer.balance_sat };
2148
+ } catch (err) {
2149
+ try {
2150
+ this.db.exec("ROLLBACK");
2151
+ } catch {
2152
+ }
2153
+ throw err;
2154
+ }
2155
+ }
2156
+ /**
2157
+ * Credit a top-up. `externalRef` is the settlement proof reference
2158
+ * (on-chain tx hash / fiat trade number) and is unique: replaying the same
2159
+ * reference returns the original row without crediting twice.
2160
+ */
2161
+ topup(opts) {
2162
+ if (!Number.isInteger(opts.amountSat) || opts.amountSat <= 0) {
2163
+ throw new Error(`topup amountSat must be a positive integer, got ${opts.amountSat}`);
2164
+ }
2165
+ const prior = this.db.prepare(`SELECT * FROM ledger_transactions WHERE external_ref = ?`).get(opts.externalRef);
2166
+ if (prior) {
2167
+ const buyer2 = this.getBuyer(prior.buyer_id);
2168
+ return { txId: prior.id, balanceSat: buyer2.balance_sat, replayed: true };
2169
+ }
2170
+ this.getOrCreateBuyer(opts.buyerId);
2171
+ const txId = `btx_${randomUUID()}`;
2172
+ this.db.exec("BEGIN IMMEDIATE");
2173
+ try {
2174
+ this.db.prepare(
2175
+ `UPDATE buyers SET
2176
+ balance_sat = balance_sat + ?,
2177
+ total_topup_sat = total_topup_sat + ?,
2178
+ updated_at = datetime('now')
2179
+ WHERE buyer_id = ?`
2180
+ ).run(opts.amountSat, opts.amountSat, opts.buyerId);
2181
+ this.db.prepare(
2182
+ `INSERT INTO ledger_transactions (id, buyer_id, type, amount_sat, description, external_ref)
2183
+ VALUES (?, ?, 'topup', ?, ?, ?)`
2184
+ ).run(txId, opts.buyerId, opts.amountSat, opts.description ?? null, opts.externalRef);
2185
+ this.db.exec("COMMIT");
2186
+ } catch (err) {
2187
+ try {
2188
+ this.db.exec("ROLLBACK");
2189
+ } catch {
2190
+ }
2191
+ throw err;
2192
+ }
2193
+ const buyer = this.getBuyer(opts.buyerId);
2194
+ return { txId, balanceSat: buyer.balance_sat };
2195
+ }
2196
+ /**
2197
+ * Reverse a deduct (service failed after the charge). Idempotent: the
2198
+ * unique index on `refunds_tx_id` means a second refund of the same deduct
2199
+ * returns the original refund row.
2200
+ */
2201
+ refund(deductTxId, reason) {
2202
+ const deductRow = this.db.prepare(`SELECT * FROM ledger_transactions WHERE id = ?`).get(deductTxId);
2203
+ if (!deductRow) return { success: false, error: "tx_not_found" };
2204
+ if (deductRow.type !== "deduct") return { success: false, error: "not_a_deduct" };
2205
+ const priorRefund = this.db.prepare(`SELECT * FROM ledger_transactions WHERE refunds_tx_id = ?`).get(deductTxId);
2206
+ if (priorRefund) {
2207
+ const buyer = this.getBuyer(deductRow.buyer_id);
2208
+ return { success: true, txId: priorRefund.id, balanceSat: buyer.balance_sat, replayed: true };
2209
+ }
2210
+ this.db.exec("BEGIN IMMEDIATE");
2211
+ try {
2212
+ this.db.prepare(
2213
+ `UPDATE buyers SET
2214
+ balance_sat = balance_sat + ?,
2215
+ total_spent_sat = total_spent_sat - ?,
2216
+ updated_at = datetime('now')
2217
+ WHERE buyer_id = ?`
2218
+ ).run(deductRow.amount_sat, deductRow.amount_sat, deductRow.buyer_id);
2219
+ this.db.prepare(`UPDATE ledger_transactions SET status = 'refunded' WHERE id = ?`).run(deductTxId);
2220
+ const txId = `btx_${randomUUID()}`;
2221
+ this.db.prepare(
2222
+ `INSERT INTO ledger_transactions (id, buyer_id, type, amount_sat, description, refunds_tx_id)
2223
+ VALUES (?, ?, 'refund', ?, ?, ?)`
2224
+ ).run(txId, deductRow.buyer_id, deductRow.amount_sat, reason ?? null, deductTxId);
2225
+ this.db.exec("COMMIT");
2226
+ const buyer = this.getBuyer(deductRow.buyer_id);
2227
+ return { success: true, txId, balanceSat: buyer.balance_sat };
2228
+ } catch (err) {
2229
+ try {
2230
+ this.db.exec("ROLLBACK");
2231
+ } catch {
2232
+ }
2233
+ throw err;
2234
+ }
2235
+ }
2236
+ /** Paged transaction history, newest first (rowid breaks same-second ties). */
2237
+ listTransactions(buyerId, limit = 20, offset = 0) {
2238
+ return this.db.prepare(
2239
+ `SELECT * FROM ledger_transactions WHERE buyer_id = ?
2240
+ ORDER BY created_at DESC, rowid DESC LIMIT ? OFFSET ?`
2241
+ ).all(buyerId, limit, offset);
2242
+ }
2243
+ /** Quick integrity probe for healthCheck(). */
2244
+ integrityOk() {
2245
+ const row = this.db.prepare(`PRAGMA quick_check`).get();
2246
+ return row.quick_check === "ok";
2247
+ }
2248
+ close() {
2249
+ this.db.close();
2250
+ }
2251
+ };
2252
+
2253
+ // src/facilitators/balance/auth.ts
2254
+ import { ethers as ethers2 } from "ethers";
2255
+ var BALANCE_AUTH_DOMAIN = "moltspay-balance-auth:v1";
2256
+ var BALANCE_AUTH_MAX_SKEW_MS = 5 * 60 * 1e3;
2257
+ function extractBalanceAuth(raw) {
2258
+ if (!raw || typeof raw !== "object") return null;
2259
+ const a = raw;
2260
+ if (typeof a.signature === "string" && a.signature && typeof a.timestamp === "number" && Number.isFinite(a.timestamp)) {
2261
+ return { timestamp: a.timestamp, signature: a.signature };
2262
+ }
2263
+ return null;
2264
+ }
2265
+ function buildDeductMessage(f) {
2266
+ return [BALANCE_AUTH_DOMAIN, "balance-deduct", f.buyerId, f.requestId, f.service, String(f.timestamp)].join("\n");
2267
+ }
2268
+ function verifyDeductAuth(opts) {
2269
+ const { auth } = opts;
2270
+ if (!auth) return { ok: false, reason: "no_signature" };
2271
+ if (!auth.signature || !Number.isFinite(auth.timestamp)) return { ok: false, reason: "malformed" };
2272
+ if (Math.abs(opts.nowMs - auth.timestamp * 1e3) > BALANCE_AUTH_MAX_SKEW_MS) {
2273
+ return { ok: false, reason: "timestamp_skew" };
2274
+ }
2275
+ const message = buildDeductMessage({
2276
+ buyerId: opts.buyerId,
2277
+ requestId: opts.requestId,
2278
+ service: opts.service,
2279
+ timestamp: auth.timestamp
2280
+ });
2281
+ try {
2282
+ const recovered = ethers2.verifyMessage(message, auth.signature).toLowerCase();
2283
+ return { ok: true, recovered };
2284
+ } catch {
2285
+ return { ok: false, reason: "bad_signature" };
2286
+ }
2287
+ }
2288
+
2289
+ // src/facilitators/balance.ts
2290
+ var BALANCE_NETWORK = "balance";
2291
+ var BALANCE_SCHEME = "balance";
2292
+ function extractBalancePayload(payment) {
2293
+ const p = payment.payload;
2294
+ if (p && typeof p.buyer_id === "string" && p.buyer_id.length > 0) {
2295
+ return {
2296
+ buyer_id: p.buyer_id,
2297
+ request_id: typeof p.request_id === "string" ? p.request_id : void 0,
2298
+ auth: extractBalanceAuth(p.auth) ?? void 0
2299
+ };
2300
+ }
2301
+ return null;
2302
+ }
2303
+ var BalanceFacilitator = class extends BaseFacilitator {
2304
+ name = "balance";
2305
+ displayName = "Custodial Balance";
2306
+ supportedNetworks = [BALANCE_NETWORK];
2307
+ currency;
2308
+ /** User-auth rollout gate for deductions (off | shadow | enforce). */
2309
+ authMode;
2310
+ ledger;
2311
+ constructor(config) {
2312
+ super();
2313
+ this.currency = config.currency ?? "USD";
2314
+ this.authMode = config.auth_mode ?? "off";
2315
+ const ledgerConfig = {
2316
+ dbPath: config.db_path,
2317
+ defaultSingleLimitSat: config.single_limit ? toSat(config.single_limit) : DEFAULT_SINGLE_LIMIT_SAT,
2318
+ defaultDailyLimitSat: config.daily_limit ? toSat(config.daily_limit) : DEFAULT_DAILY_LIMIT_SAT,
2319
+ currency: this.currency
2320
+ };
2321
+ this.ledger = new BalanceLedger(ledgerConfig);
2322
+ }
2323
+ /** Direct ledger access for the server's balance-management endpoints. */
2324
+ getLedger() {
2325
+ return this.ledger;
2326
+ }
2327
+ /**
2328
+ * Build the `accepts[]` entry for a service. Pure — no I/O, nothing minted.
2329
+ */
2330
+ createPaymentRequirements(opts) {
2331
+ toSat(opts.price);
2332
+ return {
2333
+ scheme: BALANCE_SCHEME,
2334
+ network: BALANCE_NETWORK,
2335
+ asset: this.currency,
2336
+ amount: opts.price,
2337
+ payTo: "custodial",
2338
+ maxTimeoutSeconds: 30,
2339
+ extra: opts.serviceId ? { service_id: opts.serviceId } : void 0
2340
+ };
2341
+ }
2342
+ /** Read-only funds/limits precheck. Never mutates the ledger. */
2343
+ async verify(paymentPayload, requirements) {
2344
+ const payload = extractBalancePayload(paymentPayload);
2345
+ if (!payload) {
2346
+ return { valid: false, error: "Missing buyer_id in balance payment payload" };
2347
+ }
2348
+ let amountSat;
2349
+ try {
2350
+ amountSat = toSat(requirements.amount);
2351
+ } catch (err) {
2352
+ return { valid: false, error: err.message };
2353
+ }
2354
+ const check = this.ledger.checkDeduct(payload.buyer_id, amountSat);
2355
+ if (!check.success) {
2356
+ return {
2357
+ valid: false,
2358
+ error: this.describeDeductError(check),
2359
+ details: { code: check.error, balance: check.balanceSat !== void 0 ? fromSat(check.balanceSat) : void 0 }
2360
+ };
2361
+ }
2362
+ return { valid: true, details: { balance: fromSat(check.balanceSat) } };
2363
+ }
2364
+ /**
2365
+ * The atomic deduction. Idempotent on `request_id`; the returned
2366
+ * `transaction` is the ledger tx id (usable for `refund`).
2367
+ */
2368
+ async settle(paymentPayload, requirements) {
2369
+ const payload = extractBalancePayload(paymentPayload);
2370
+ if (!payload) {
2371
+ return { success: false, error: "Missing buyer_id in balance payment payload" };
2372
+ }
2373
+ let amountSat;
2374
+ try {
2375
+ amountSat = toSat(requirements.amount);
2376
+ } catch (err) {
2377
+ return { success: false, error: err.message };
2378
+ }
2379
+ const serviceId = typeof requirements.extra?.service_id === "string" ? requirements.extra.service_id : void 0;
2380
+ let result;
2381
+ try {
2382
+ result = this.ledger.deduct({
2383
+ buyerId: payload.buyer_id,
2384
+ amountSat,
2385
+ requestId: payload.request_id,
2386
+ service: serviceId
2387
+ });
2388
+ } catch (err) {
2389
+ return { success: false, error: `Ledger deduct failed: ${err.message}` };
2390
+ }
2391
+ if (!result.success) {
2392
+ return { success: false, error: this.describeDeductError(result), status: result.error };
2393
+ }
2394
+ return {
2395
+ success: true,
2396
+ transaction: result.txId,
2397
+ status: result.replayed ? "replayed" : "deducted"
2398
+ };
2399
+ }
2400
+ /** Reverse a deduct after a downstream failure. Idempotent per deduct. */
2401
+ refund(deductTxId, reason) {
2402
+ return this.ledger.refund(deductTxId, reason);
2403
+ }
2404
+ /**
2405
+ * Credit a gateway-verified external settlement to a buyer's balance. The
2406
+ * single entry point for every funding path -- WeChat callback, WeChat
2407
+ * polling, and the operator `/balance/topup` endpoint -- so they share one
2408
+ * idempotency boundary: `externalRef` is unique in the ledger, so a replay
2409
+ * credits nothing and returns the original transaction (`replayed: true`).
2410
+ * Callers must pass a gateway-verified `amountSat`, never a client-declared
2411
+ * amount.
2412
+ */
2413
+ credit(opts) {
2414
+ const result = this.ledger.topup({
2415
+ buyerId: opts.buyerId,
2416
+ amountSat: opts.amountSat,
2417
+ externalRef: opts.externalRef,
2418
+ description: opts.description
2419
+ });
2420
+ return {
2421
+ txId: result.txId,
2422
+ balance: fromSat(result.balanceSat),
2423
+ balanceSat: result.balanceSat,
2424
+ replayed: result.replayed ?? false
2425
+ };
2426
+ }
2427
+ async healthCheck() {
2428
+ const start = Date.now();
2429
+ try {
2430
+ const ok = this.ledger.integrityOk();
2431
+ return ok ? { healthy: true, latencyMs: Date.now() - start } : { healthy: false, error: "SQLite quick_check failed" };
2432
+ } catch (err) {
2433
+ return { healthy: false, error: err.message };
2434
+ }
2435
+ }
2436
+ describeDeductError(result) {
2437
+ switch (result.error) {
2438
+ case "buyer_not_found":
2439
+ return "Unknown buyer: top up first to create an account";
2440
+ case "buyer_not_active":
2441
+ return "Buyer account is frozen or banned";
2442
+ case "insufficient_balance":
2443
+ return `Insufficient balance${result.balanceSat !== void 0 ? ` (have ${fromSat(result.balanceSat)})` : ""}`;
2444
+ case "exceeds_single_limit":
2445
+ return `Amount exceeds the per-transaction limit${result.limitSat !== void 0 ? ` of ${fromSat(result.limitSat)}` : ""}`;
2446
+ case "exceeds_daily_limit":
2447
+ return `Amount exceeds the daily spending limit${result.limitSat !== void 0 ? ` of ${fromSat(result.limitSat)}` : ""}`;
2448
+ default:
2449
+ return "Deduction failed";
2450
+ }
2451
+ }
2452
+ };
2453
+
1580
2454
  // src/facilitators/registry.ts
1581
2455
  import { Keypair as Keypair2 } from "@solana/web3.js";
1582
2456
  import bs58 from "bs58";
@@ -1602,6 +2476,8 @@ var FacilitatorRegistry = class {
1602
2476
  return new SolanaFacilitator({ feePayerKeypair });
1603
2477
  });
1604
2478
  this.registerFactory("alipay", (config) => new AlipayFacilitator(config));
2479
+ this.registerFactory("wechat", (config) => new WechatFacilitator(config));
2480
+ this.registerFactory("balance", (config) => new BalanceFacilitator(config));
1605
2481
  this.selection = selection || { primary: "cdp", fallback: ["tempo", "bnb", "solana"], strategy: "failover" };
1606
2482
  }
1607
2483
  /**
@@ -1833,15 +2709,33 @@ export {
1833
2709
  ALIPAY_NETWORK,
1834
2710
  ALIPAY_SCHEME,
1835
2711
  AlipayFacilitator,
2712
+ BALANCE_AUTH_DOMAIN,
2713
+ BALANCE_AUTH_MAX_SKEW_MS,
2714
+ BALANCE_NETWORK,
2715
+ BALANCE_SCHEME,
1836
2716
  BNBFacilitator,
2717
+ BalanceFacilitator,
2718
+ BalanceLedger,
1837
2719
  BaseFacilitator,
1838
2720
  CDPFacilitator,
1839
2721
  FacilitatorRegistry,
1840
2722
  SolanaFacilitator,
1841
2723
  TempoFacilitator,
2724
+ WECHAT_AMOUNT_REGEX,
2725
+ WECHAT_API_BASE,
2726
+ WECHAT_NETWORK,
2727
+ WECHAT_SCHEME,
2728
+ WECHAT_TIME_EXPIRE_MS,
2729
+ WechatFacilitator,
2730
+ buildDeductMessage,
1842
2731
  createIntentTypedData,
1843
2732
  createRegistry,
1844
2733
  createSolanaPaymentTransaction,
1845
- getDefaultRegistry
2734
+ extractBalancePayload,
2735
+ fromSat,
2736
+ getDefaultRegistry,
2737
+ parseWechatAttach,
2738
+ toSat,
2739
+ verifyDeductAuth
1846
2740
  };
1847
2741
  //# sourceMappingURL=index.mjs.map