@piprail/sdk 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -63,7 +63,7 @@ function resolveNetwork(opts) {
63
63
  }
64
64
 
65
65
  // src/drivers/evm/index.ts
66
- import { BaseError, createPublicClient, getAddress as getAddress2, http as http2, isAddress } from "viem";
66
+ import { BaseError, createPublicClient, erc20Abi as erc20Abi3, getAddress as getAddress2, http as http2, isAddress } from "viem";
67
67
 
68
68
  // src/drivers/evm/chains.ts
69
69
  import { defineChain } from "viem";
@@ -716,6 +716,27 @@ function makeEvmNetwork(resolved) {
716
716
  });
717
717
  }
718
718
  },
719
+ async balanceOf(wallet, asset) {
720
+ const owner = wallet._native.account.address;
721
+ const native = await publicClient.getBalance({ address: owner }).catch(() => null);
722
+ if (asset === "native") return { token: native, native };
723
+ let token = null;
724
+ try {
725
+ token = await publicClient.readContract({
726
+ address: getAddress2(asset),
727
+ abi: erc20Abi3,
728
+ functionName: "balanceOf",
729
+ args: [owner]
730
+ });
731
+ } catch {
732
+ token = null;
733
+ }
734
+ return { token, native };
735
+ },
736
+ // No receive prerequisite — any 0x address receives native or ERC-20 immediately.
737
+ async recipientReady() {
738
+ return { ready: "n/a" };
739
+ },
719
740
  async verify(ref, accept) {
720
741
  return verifyEvm({
721
742
  publicClient,
@@ -739,7 +760,7 @@ var loaders = {
739
760
  solana: async () => {
740
761
  let mod;
741
762
  try {
742
- mod = await import("./solana-HNRTS4KM.js");
763
+ mod = await import("./solana-7WJVZGDW.js");
743
764
  } catch (cause) {
744
765
  throw new MissingDriverError(
745
766
  `Solana selected, but its packages aren't installed. Run: npm install @solana/web3.js @solana/spl-token bs58`,
@@ -751,7 +772,7 @@ var loaders = {
751
772
  ton: async () => {
752
773
  let mod;
753
774
  try {
754
- mod = await import("./ton-3XMIM2FU.js");
775
+ mod = await import("./ton-DGZB7W4U.js");
755
776
  } catch (cause) {
756
777
  throw new MissingDriverError(
757
778
  `TON selected, but its packages aren't installed. Run: npm install @ton/ton @ton/core @ton/crypto`,
@@ -763,7 +784,7 @@ var loaders = {
763
784
  stellar: async () => {
764
785
  let mod;
765
786
  try {
766
- mod = await import("./stellar-4TDVVJYO.js");
787
+ mod = await import("./stellar-HV6VGZX3.js");
767
788
  } catch (cause) {
768
789
  throw new MissingDriverError(
769
790
  `Stellar selected, but its package isn't installed. Run: npm install @stellar/stellar-sdk`,
@@ -775,7 +796,7 @@ var loaders = {
775
796
  xrpl: async () => {
776
797
  let mod;
777
798
  try {
778
- mod = await import("./xrpl-ISFG3SSN.js");
799
+ mod = await import("./xrpl-UEC2GYVV.js");
779
800
  } catch (cause) {
780
801
  throw new MissingDriverError(
781
802
  `XRPL selected, but its package isn't installed. Run: npm install xrpl`,
@@ -787,7 +808,7 @@ var loaders = {
787
808
  tron: async () => {
788
809
  let mod;
789
810
  try {
790
- mod = await import("./tron-6D65YJEU.js");
811
+ mod = await import("./tron-RLIL2FDI.js");
791
812
  } catch (cause) {
792
813
  throw new MissingDriverError(
793
814
  `Tron selected, but its package isn't installed. Run: npm install tronweb`,
@@ -799,7 +820,7 @@ var loaders = {
799
820
  sui: async () => {
800
821
  let mod;
801
822
  try {
802
- mod = await import("./sui-ALUTM5GX.js");
823
+ mod = await import("./sui-2WFWVFJX.js");
803
824
  } catch (cause) {
804
825
  throw new MissingDriverError(
805
826
  `Sui selected, but its package isn't installed. Run: npm install @mysten/sui`,
@@ -811,7 +832,7 @@ var loaders = {
811
832
  near: async () => {
812
833
  let mod;
813
834
  try {
814
- mod = await import("./near-NOJTO4GX.js");
835
+ mod = await import("./near-7MBBCDUE.js");
815
836
  } catch (cause) {
816
837
  throw new MissingDriverError(
817
838
  `NEAR selected, but its package isn't installed. Run: npm install near-api-js`,
@@ -823,7 +844,7 @@ var loaders = {
823
844
  aptos: async () => {
824
845
  let mod;
825
846
  try {
826
- mod = await import("./aptos-AWWSCPDH.js");
847
+ mod = await import("./aptos-YQWTGFRZ.js");
827
848
  } catch (cause) {
828
849
  throw new MissingDriverError(
829
850
  `Aptos selected, but its package isn't installed. Run: npm install @aptos-labs/ts-sdk`,
@@ -835,7 +856,7 @@ var loaders = {
835
856
  algorand: async () => {
836
857
  let mod;
837
858
  try {
838
- mod = await import("./algorand-XJ5OVWQB.js");
859
+ mod = await import("./algorand-B67G4335.js");
839
860
  } catch (cause) {
840
861
  throw new MissingDriverError(
841
862
  `Algorand selected, but its package isn't installed. Run: npm install algosdk`,
@@ -976,6 +997,12 @@ var SpendLedger = class {
976
997
  };
977
998
 
978
999
  // src/client.ts
1000
+ var RECIPIENT_FIX = {
1001
+ NO_TRUSTLINE: "the recipient needs a one-time trustline for this asset before it can receive",
1002
+ NOT_REGISTERED: "the recipient must be storage_deposit-registered on this token (NEP-145, one-time)",
1003
+ NOT_OPTED_IN: "the recipient must opt into this asset once (a 0-amount self-transfer)",
1004
+ INACTIVE: "the recipient account doesn't exist yet \u2014 fund it with the chain's base reserve to activate it"
1005
+ };
979
1006
  var PipRailClient = class {
980
1007
  opts;
981
1008
  maxRetries;
@@ -1084,6 +1111,44 @@ var PipRailClient = class {
1084
1111
  spent() {
1085
1112
  return this.ledger.summary();
1086
1113
  }
1114
+ /**
1115
+ * Plan a payment for a gated URL — WITHOUT paying. The read-only completion of
1116
+ * the `quote()` → `estimateCost()` → **`planPayment()`** trio: it surveys every
1117
+ * rail the 402 offers on this client's chain against the wallet's OWN holdings —
1118
+ * token balance, native-coin gas, and recipient-readiness (trustline / ATA /
1119
+ * storage_deposit / ASA opt-in) — and returns, crystal-clear:
1120
+ * - `payable` + `best` — the cheapest rail the wallet can actually settle
1121
+ * - `options[]` — every rail with typed `blockers` + soft `warnings`
1122
+ * - `fundingHint` — one human sentence on exactly what to top up
1123
+ *
1124
+ * NEVER throws for a read problem (a transient/RPC failure surfaces as a rail in
1125
+ * `state: 'unknown'` + a warning, never a false "unaffordable"); returns `null`
1126
+ * when the URL isn't payment-gated (no 402); and when the 402 offers no rail on
1127
+ * this client's chain it EXPLAINS that (status `blocked` + a hint), rather than
1128
+ * throwing. Throws `InvalidEnvelopeError` only on an unparseable challenge.
1129
+ *
1130
+ * Then pay the chosen rail with `fetch(url, { autoRoute: true })`, or branch on
1131
+ * the plan yourself. No funds move.
1132
+ */
1133
+ async planPayment(url, init) {
1134
+ const res = await fetch(url, { ...init ?? {}, method: init?.method ?? "GET" });
1135
+ if (res.status !== 402) return null;
1136
+ const challenge = await parseChallenge(res);
1137
+ if (!challenge) {
1138
+ throw new InvalidEnvelopeError("402 response did not include a parseable x402 challenge.");
1139
+ }
1140
+ const { net, wallet } = await this.ensure();
1141
+ return this.planFromChallenge(net, wallet, challenge, url);
1142
+ }
1143
+ /**
1144
+ * Convenience over {@link planPayment}: can the wallet settle this URL right now?
1145
+ * `true` when at least one rail is payable — or when the URL isn't gated (a free
1146
+ * resource is trivially "affordable"). No funds move.
1147
+ */
1148
+ async canAfford(url, init) {
1149
+ const plan = await this.planPayment(url, init);
1150
+ return plan == null ? true : plan.payable;
1151
+ }
1087
1152
  /**
1088
1153
  * Lower-level: drive any HTTP method through the 402 flow.
1089
1154
  *
@@ -1100,10 +1165,19 @@ var PipRailClient = class {
1100
1165
  }
1101
1166
  const firstResponse = await fetch(url, init);
1102
1167
  if (firstResponse.status !== 402) return firstResponse;
1103
- const { net, wallet, accept, challenge, quote } = await this.resolveChallenge(
1104
- url,
1105
- firstResponse
1106
- );
1168
+ const resolved = await this.resolveChallenge(url, firstResponse);
1169
+ const { net, wallet, challenge } = resolved;
1170
+ let accept = resolved.accept;
1171
+ let quote = resolved.quote;
1172
+ const autoRoute = init?.autoRoute ?? this.opts.autoRoute ?? false;
1173
+ if (autoRoute) {
1174
+ const plan = await this.planFromChallenge(net, wallet, challenge, url);
1175
+ if (!plan.best) {
1176
+ throw new PaymentDeclinedError(plan.fundingHint ?? "No rail is settleable for this payment.");
1177
+ }
1178
+ accept = plan.best.accept;
1179
+ quote = plan.best.quote;
1180
+ }
1107
1181
  this.safeEmit({ kind: "payment-required", challenge, accept });
1108
1182
  await this.authorize(quote);
1109
1183
  const { ref, confirmed } = await this.payAndConfirm(net, wallet, accept);
@@ -1125,9 +1199,7 @@ var PipRailClient = class {
1125
1199
  );
1126
1200
  }
1127
1201
  const { net, wallet } = await this.ensure();
1128
- const candidates = challenge.accepts.filter(
1129
- (a) => a.scheme === "onchain-proof" && net.supports(a.network)
1130
- );
1202
+ const candidates = this.gatherCandidates(net, challenge);
1131
1203
  if (candidates.length === 0) {
1132
1204
  const networks = challenge.accepts.map((a) => a.network).join(", ");
1133
1205
  throw new NoCompatibleAcceptError(
@@ -1141,6 +1213,111 @@ var PipRailClient = class {
1141
1213
  const chosen = priced.find((p) => p.quote.withinPolicy) ?? priced[0];
1142
1214
  return { net, wallet, accept: chosen.accept, challenge, quote: chosen.quote };
1143
1215
  }
1216
+ /** The candidate accepts this client could pay: our scheme, on the bound network. */
1217
+ gatherCandidates(net, challenge) {
1218
+ return challenge.accepts.filter(
1219
+ (a) => a.scheme === "onchain-proof" && net.supports(a.network)
1220
+ );
1221
+ }
1222
+ /** Build the full {@link PaymentPlan} from an already-parsed challenge + bound
1223
+ * net/wallet. Shared by `planPayment` (read-only) and `fetch`'s autoRoute. */
1224
+ async planFromChallenge(net, wallet, challenge, url) {
1225
+ const chainLabel = typeof this.opts.chain === "string" ? this.opts.chain : net.network;
1226
+ const candidates = this.gatherCandidates(net, challenge);
1227
+ if (candidates.length === 0) {
1228
+ const offered = [...new Set(challenge.accepts.map((a) => a.network))].join(", ") || "none";
1229
+ return {
1230
+ url,
1231
+ network: net.network,
1232
+ status: "blocked",
1233
+ payable: false,
1234
+ best: null,
1235
+ options: [],
1236
+ fundingHint: `This 402 isn't offered on your chain (${chainLabel}); it's payable on: ${offered}.`
1237
+ };
1238
+ }
1239
+ const analysed = await Promise.all(
1240
+ candidates.map(
1241
+ (accept) => this.analyzeRail(net, wallet, accept, url, challenge.resource.description)
1242
+ )
1243
+ );
1244
+ const options = rankOptions(analysed);
1245
+ const best = options.find((o) => o.state === "payable") ?? null;
1246
+ const status = best ? "ready" : options.some((o) => o.state === "unknown") ? "unknown" : "blocked";
1247
+ return {
1248
+ url,
1249
+ network: net.network,
1250
+ status,
1251
+ payable: best !== null,
1252
+ best,
1253
+ options,
1254
+ fundingHint: best ? null : buildFundingHint(options, chainLabel)
1255
+ };
1256
+ }
1257
+ /** Analyse ONE rail against the wallet's holdings — quote (existing) + gas
1258
+ * (estimateCost, existing) + balanceOf + recipientReady → a {@link PayOption}. */
1259
+ async analyzeRail(net, wallet, accept, url, description) {
1260
+ const quote = this.buildQuote(net, accept, url, description);
1261
+ const cost = await net.estimateCost(accept);
1262
+ const bal = await net.balanceOf(wallet, accept.asset).catch(() => ({ token: null, native: null }));
1263
+ const rr = await net.recipientReady(accept.payTo, accept.asset).catch(() => ({ ready: "unknown" }));
1264
+ const amount = BigInt(accept.amount);
1265
+ const fee = safeBig(cost.fee);
1266
+ const isNative = accept.asset === "native";
1267
+ const blockers = [];
1268
+ const warnings = [];
1269
+ const shortfall = {};
1270
+ if (!quote.withinPolicy) blockers.push("OUTSIDE_POLICY");
1271
+ if (quote.symbolMismatch) warnings.push("SYMBOL_MISMATCH");
1272
+ if (cost.basis === "heuristic") warnings.push("GAS_HEURISTIC");
1273
+ const tokenKnown = bal.token != null;
1274
+ const nativeKnown = bal.native != null;
1275
+ if (!tokenKnown || !nativeKnown) warnings.push("BALANCE_UNREADABLE");
1276
+ if (isNative) {
1277
+ if (nativeKnown && bal.native < amount + fee) {
1278
+ blockers.push("INSUFFICIENT_TOKEN");
1279
+ shortfall.token = formatUnits(amount + fee - bal.native, quote.decimals);
1280
+ }
1281
+ } else {
1282
+ if (tokenKnown && bal.token < amount) {
1283
+ blockers.push("INSUFFICIENT_TOKEN");
1284
+ shortfall.token = formatUnits(amount - bal.token, quote.decimals);
1285
+ }
1286
+ if (nativeKnown && bal.native < fee) {
1287
+ blockers.push("INSUFFICIENT_GAS");
1288
+ shortfall.native = formatUnits(fee - bal.native, cost.feeDecimals);
1289
+ } else if (nativeKnown && fee > 0n && bal.native < fee * 3n / 2n) {
1290
+ warnings.push("THIN_GAS_MARGIN");
1291
+ }
1292
+ }
1293
+ let recipient;
1294
+ if (rr.ready === false) {
1295
+ blockers.push("RECIPIENT_NOT_READY");
1296
+ recipient = rr.reason ? { ready: false, reason: rr.reason, fix: RECIPIENT_FIX[rr.reason] } : { ready: false };
1297
+ } else if (rr.ready === "unknown") {
1298
+ warnings.push("RECIPIENT_READINESS_UNKNOWN");
1299
+ recipient = { ready: "unknown" };
1300
+ } else {
1301
+ recipient = { ready: rr.ready };
1302
+ }
1303
+ const unreadable = isNative ? !nativeKnown : !tokenKnown || !nativeKnown;
1304
+ const state = blockers.length ? "blocked" : unreadable || rr.ready === "unknown" ? "unknown" : "payable";
1305
+ return {
1306
+ accept,
1307
+ quote,
1308
+ cost,
1309
+ state,
1310
+ blockers,
1311
+ warnings,
1312
+ balance: {
1313
+ token: bal.token != null ? formatUnits(bal.token, quote.decimals) : null,
1314
+ native: bal.native != null ? formatUnits(bal.native, cost.feeDecimals) : null
1315
+ },
1316
+ need: { token: quote.amountFormatted, native: cost.feeFormatted },
1317
+ ...shortfall.token || shortfall.native ? { shortfall } : {},
1318
+ recipient
1319
+ };
1320
+ }
1144
1321
  /** Build the agent-facing quote for an accept: TRUE decimals/symbol (via the
1145
1322
  * driver's describeAsset) + the policy verdict + a symbol-mismatch flag. */
1146
1323
  buildQuote(net, accept, url, description) {
@@ -1313,6 +1490,68 @@ var PipRailClient = class {
1313
1490
  );
1314
1491
  }
1315
1492
  };
1493
+ function safeBig(s) {
1494
+ try {
1495
+ return BigInt(s);
1496
+ } catch {
1497
+ return 0n;
1498
+ }
1499
+ }
1500
+ function shortAddr(a) {
1501
+ return a.length > 14 ? `${a.slice(0, 8)}\u2026${a.slice(-4)}` : a;
1502
+ }
1503
+ function rankOptions(options) {
1504
+ const rank = { payable: 0, unknown: 1, blocked: 2 };
1505
+ return [...options].sort((a, b) => {
1506
+ if (rank[a.state] !== rank[b.state]) return rank[a.state] - rank[b.state];
1507
+ if (a.state === "payable") {
1508
+ const fa = safeBig(a.cost.fee);
1509
+ const fb = safeBig(b.cost.fee);
1510
+ if (fa !== fb) return fa < fb ? -1 : 1;
1511
+ }
1512
+ return 0;
1513
+ });
1514
+ }
1515
+ function buildFundingHint(options, chainLabel) {
1516
+ if (options.length === 0) return null;
1517
+ const target = [...options].sort((a, b) => a.blockers.length - b.blockers.length)[0];
1518
+ const sym = target.quote.symbol ?? "the token";
1519
+ if (target.blockers.includes("RECIPIENT_NOT_READY")) {
1520
+ return `Recipient ${shortAddr(target.accept.payTo)} can't receive on ${chainLabel} yet \u2014 ${target.recipient.fix ?? "recipient not ready"}.`;
1521
+ }
1522
+ if (target.blockers.includes("OUTSIDE_POLICY")) {
1523
+ return `Refused by spend policy: ${target.quote.policyReason ?? "not allowed"}.`;
1524
+ }
1525
+ if (target.state === "unknown") {
1526
+ return `Couldn't fully read your wallet on ${chainLabel} (RPC throttled) \u2014 retry; you may already be able to pay ${target.quote.amountFormatted} ${sym}.`;
1527
+ }
1528
+ const parts = [];
1529
+ if (target.blockers.includes("INSUFFICIENT_TOKEN") && target.shortfall?.token) {
1530
+ parts.push(`top up ${target.shortfall.token} ${sym}`);
1531
+ }
1532
+ if (target.blockers.includes("INSUFFICIENT_GAS") && target.shortfall?.native) {
1533
+ parts.push(`add ~${target.shortfall.native} ${target.cost.feeSymbol} for gas`);
1534
+ }
1535
+ return parts.length ? `Can't settle on ${chainLabel}: ${parts.join(" and ")} (to pay ${target.quote.amountFormatted} ${sym}).` : `Can't settle on ${chainLabel} for ${target.quote.amountFormatted} ${sym}.`;
1536
+ }
1537
+ async function planAcross(clients, url, init) {
1538
+ const plans = await Promise.all(clients.map((c) => c.planPayment(url, init).catch(() => null)));
1539
+ const live = plans.filter((p) => p != null);
1540
+ if (live.length === 0) return null;
1541
+ const options = rankOptions(live.flatMap((p) => p.options));
1542
+ const best = options.find((o) => o.state === "payable") ?? null;
1543
+ const status = best ? "ready" : options.some((o) => o.state === "unknown") ? "unknown" : "blocked";
1544
+ return {
1545
+ url,
1546
+ network: best?.accept.network ?? live[0].network,
1547
+ status,
1548
+ payable: best !== null,
1549
+ best,
1550
+ options,
1551
+ // First non-null hint across clients — each already names its chain.
1552
+ fundingHint: best ? null : live.map((p) => p.fundingHint).find(Boolean) ?? null
1553
+ };
1554
+ }
1316
1555
  function hostOf(url) {
1317
1556
  try {
1318
1557
  return new URL(url).hostname;
@@ -1372,6 +1611,44 @@ function paymentTools(client) {
1372
1611
  return quote ? { gated: true, ...quote } : { gated: false, url: String(args.url) };
1373
1612
  }
1374
1613
  },
1614
+ {
1615
+ name: "piprail_plan_payment",
1616
+ description: "Check whether you CAN pay an x402-gated URL before paying. Reads your wallet balance, native gas, and whether the recipient can receive \u2014 across every rail the URL offers on your chain \u2014 and returns { gated, payable, best, options, fundingHint }. payable:false means do NOT attempt the payment; fundingHint says exactly what to top up. Call this before piprail_pay_request so you never commit to a payment you cannot finish. Returns { gated: false } when no payment is needed.",
1617
+ parameters: {
1618
+ type: "object",
1619
+ properties: {
1620
+ url: { type: "string", description: "Full URL of the gated resource." }
1621
+ },
1622
+ required: ["url"],
1623
+ additionalProperties: false
1624
+ },
1625
+ invoke: async (args) => {
1626
+ const plan = await client.planPayment(String(args.url));
1627
+ if (plan == null) return { gated: false, url: String(args.url) };
1628
+ return {
1629
+ gated: true,
1630
+ payable: plan.payable,
1631
+ status: plan.status,
1632
+ fundingHint: plan.fundingHint,
1633
+ best: plan.best ? {
1634
+ network: plan.best.accept.network,
1635
+ symbol: plan.best.quote.symbol,
1636
+ amount: plan.best.quote.amountFormatted,
1637
+ gasCoin: plan.best.cost.feeSymbol,
1638
+ gas: plan.best.cost.feeFormatted
1639
+ } : null,
1640
+ options: plan.options.map((o) => ({
1641
+ network: o.accept.network,
1642
+ symbol: o.quote.symbol,
1643
+ amount: o.quote.amountFormatted,
1644
+ state: o.state,
1645
+ blockers: o.blockers,
1646
+ warnings: o.warnings,
1647
+ recipientReady: o.recipient.ready
1648
+ }))
1649
+ };
1650
+ }
1651
+ },
1375
1652
  {
1376
1653
  name: "piprail_pay_request",
1377
1654
  description: "Fetch an x402 payment-gated URL, automatically paying the required on-chain payment if needed (subject to the spend policy + approval hook). Returns the HTTP status, the response body, and a payment receipt if one settled. If the payment is refused by policy or the approval hook, returns { declined: true, reason } \u2014 no funds moved.",
@@ -1738,6 +2015,7 @@ export {
1738
2015
  parseSignatureHeader,
1739
2016
  paymentTools,
1740
2017
  pickAccept,
2018
+ planAcross,
1741
2019
  registerDriver,
1742
2020
  requirePayment,
1743
2021
  resolveChain,
@@ -400,6 +400,56 @@ function makeNearNetwork(preset, rpcUrl) {
400
400
  detail: "~14 TGas for ft_transfer (\u22480.0015 NEAR) + 1 yoctoNEAR deposit"
401
401
  });
402
402
  },
403
+ async balanceOf(wallet, asset) {
404
+ let accountId;
405
+ try {
406
+ accountId = resolveNearWallet(wallet._native).accountId;
407
+ } catch {
408
+ return { token: null, native: null };
409
+ }
410
+ let native = null;
411
+ try {
412
+ const r = await provider.query({
413
+ request_type: "view_account",
414
+ finality: "final",
415
+ account_id: accountId
416
+ });
417
+ native = r.amount != null ? BigInt(r.amount) : null;
418
+ } catch (e) {
419
+ native = /does not exist|UNKNOWN_ACCOUNT/i.test(String(e?.message ?? e)) ? 0n : null;
420
+ }
421
+ if (asset === "native") return { token: native, native };
422
+ let token = null;
423
+ try {
424
+ const r = await provider.query({
425
+ request_type: "call_function",
426
+ finality: "final",
427
+ account_id: asset,
428
+ method_name: "ft_balance_of",
429
+ args_base64: Buffer.from(JSON.stringify({ account_id: accountId })).toString("base64")
430
+ });
431
+ token = r.result ? BigInt(JSON.parse(Buffer.from(r.result).toString())) : null;
432
+ } catch (e) {
433
+ token = /does not exist|not registered|UNKNOWN_ACCOUNT/i.test(String(e?.message ?? e)) ? 0n : null;
434
+ }
435
+ return { token, native };
436
+ },
437
+ async recipientReady(payTo, asset) {
438
+ if (asset === "native") return { ready: "n/a" };
439
+ try {
440
+ const r = await provider.query({
441
+ request_type: "call_function",
442
+ finality: "final",
443
+ account_id: asset,
444
+ method_name: "storage_balance_of",
445
+ args_base64: Buffer.from(JSON.stringify({ account_id: payTo })).toString("base64")
446
+ });
447
+ const parsed = r.result ? JSON.parse(Buffer.from(r.result).toString()) : null;
448
+ return parsed == null ? { ready: false, reason: "NOT_REGISTERED" } : { ready: true };
449
+ } catch {
450
+ return { ready: "unknown" };
451
+ }
452
+ },
403
453
  async verify(ref, accept) {
404
454
  const { senderId, hash } = decodeRef(ref);
405
455
  return verifyNear({ reader, hash, senderId, accept });
@@ -101,7 +101,7 @@ function parseFtTransferEvent(line) {
101
101
  if (ev.standard === "nep141" && ev.event === "ft_transfer" && Array.isArray(ev.data)) {
102
102
  return ev.data;
103
103
  }
104
- } catch (e) {
104
+ } catch (e2) {
105
105
  }
106
106
  return null;
107
107
  }
@@ -112,7 +112,7 @@ async function verifyNear(params) {
112
112
  let tx;
113
113
  try {
114
114
  tx = await reader.txStatus(hash, senderId);
115
- } catch (e2) {
115
+ } catch (e3) {
116
116
  return txNotFound(hash);
117
117
  }
118
118
  if (!tx) return txNotFound(hash);
@@ -133,7 +133,7 @@ async function verifyNear(params) {
133
133
  let paid2;
134
134
  try {
135
135
  paid2 = BigInt(_nullishCoalesce(tx.nativeDeposit, () => ( "0")));
136
- } catch (e3) {
136
+ } catch (e4) {
137
137
  paid2 = 0n;
138
138
  }
139
139
  if (paid2 < required) {
@@ -175,7 +175,7 @@ async function verifyNear(params) {
175
175
  if (d.new_owner_id === accept.payTo && (_nullishCoalesce(d.memo, () => ( ""))) === nonce) {
176
176
  try {
177
177
  paid += BigInt(_nullishCoalesce(d.amount, () => ( "0")));
178
- } catch (e4) {
178
+ } catch (e5) {
179
179
  }
180
180
  if (!payer) payer = _nullishCoalesce(d.old_owner_id, () => ( ""));
181
181
  }
@@ -295,7 +295,7 @@ function makeNearNetwork(preset, rpcUrl) {
295
295
  const ns = _nullishCoalesce(_optionalChain([header, 'optionalAccess', _5 => _5.timestamp_nanosec]), () => ( (_optionalChain([header, 'optionalAccess', _6 => _6.timestamp]) != null ? String(header.timestamp) : void 0)));
296
296
  if (ns != null) timestampMs = Number(BigInt(ns) / 1000000n);
297
297
  }
298
- } catch (e5) {
298
+ } catch (e6) {
299
299
  }
300
300
  return { success, receipts, receiverId, nativeDeposit, timestampMs };
301
301
  }
@@ -387,7 +387,7 @@ function makeNearNetwork(preset, rpcUrl) {
387
387
  try {
388
388
  const tx = await reader.txStatus(hash, senderId);
389
389
  if (tx && tx.success) return { height: "0" };
390
- } catch (e6) {
390
+ } catch (e7) {
391
391
  }
392
392
  throw new (0, _chunkIQGT65WScjs.ConfirmationTimeoutError)(`NEAR tx ${hash} not confirmed in time.`);
393
393
  },
@@ -400,6 +400,56 @@ function makeNearNetwork(preset, rpcUrl) {
400
400
  detail: "~14 TGas for ft_transfer (\u22480.0015 NEAR) + 1 yoctoNEAR deposit"
401
401
  });
402
402
  },
403
+ async balanceOf(wallet, asset) {
404
+ let accountId;
405
+ try {
406
+ accountId = resolveNearWallet(wallet._native).accountId;
407
+ } catch (e8) {
408
+ return { token: null, native: null };
409
+ }
410
+ let native = null;
411
+ try {
412
+ const r = await provider.query({
413
+ request_type: "view_account",
414
+ finality: "final",
415
+ account_id: accountId
416
+ });
417
+ native = r.amount != null ? BigInt(r.amount) : null;
418
+ } catch (e) {
419
+ native = /does not exist|UNKNOWN_ACCOUNT/i.test(String(_nullishCoalesce(_optionalChain([e, 'optionalAccess', _9 => _9.message]), () => ( e)))) ? 0n : null;
420
+ }
421
+ if (asset === "native") return { token: native, native };
422
+ let token = null;
423
+ try {
424
+ const r = await provider.query({
425
+ request_type: "call_function",
426
+ finality: "final",
427
+ account_id: asset,
428
+ method_name: "ft_balance_of",
429
+ args_base64: Buffer.from(JSON.stringify({ account_id: accountId })).toString("base64")
430
+ });
431
+ token = r.result ? BigInt(JSON.parse(Buffer.from(r.result).toString())) : null;
432
+ } catch (e) {
433
+ token = /does not exist|not registered|UNKNOWN_ACCOUNT/i.test(String(_nullishCoalesce(_optionalChain([e, 'optionalAccess', _10 => _10.message]), () => ( e)))) ? 0n : null;
434
+ }
435
+ return { token, native };
436
+ },
437
+ async recipientReady(payTo, asset) {
438
+ if (asset === "native") return { ready: "n/a" };
439
+ try {
440
+ const r = await provider.query({
441
+ request_type: "call_function",
442
+ finality: "final",
443
+ account_id: asset,
444
+ method_name: "storage_balance_of",
445
+ args_base64: Buffer.from(JSON.stringify({ account_id: payTo })).toString("base64")
446
+ });
447
+ const parsed = r.result ? JSON.parse(Buffer.from(r.result).toString()) : null;
448
+ return parsed == null ? { ready: false, reason: "NOT_REGISTERED" } : { ready: true };
449
+ } catch (e9) {
450
+ return { ready: "unknown" };
451
+ }
452
+ },
403
453
  async verify(ref, accept) {
404
454
  const { senderId, hash } = decodeRef(ref);
405
455
  return verifyNear({ reader, hash, senderId, accept });
@@ -422,7 +472,7 @@ function sumTransferDeposits(actions2) {
422
472
  if (t && t.deposit != null) {
423
473
  try {
424
474
  sum += BigInt(t.deposit);
425
- } catch (e7) {
475
+ } catch (e10) {
426
476
  }
427
477
  }
428
478
  }
@@ -9,6 +9,11 @@ import {
9
9
 
10
10
  // src/drivers/solana/index.ts
11
11
  import { Connection, PublicKey as PublicKey2 } from "@solana/web3.js";
12
+ import {
13
+ getAccount,
14
+ getAssociatedTokenAddressSync as getAssociatedTokenAddressSync2,
15
+ TokenAccountNotFoundError
16
+ } from "@solana/spl-token";
12
17
 
13
18
  // src/drivers/solana/chains.ts
14
19
  var SOL_DECIMALS = 9;
@@ -332,6 +337,23 @@ function makeSolanaNetwork(preset, rpcUrl) {
332
337
  detail: "1 signature + recipient token-account rent (~0.00204 SOL, if not already created)"
333
338
  });
334
339
  },
340
+ async balanceOf(wallet, asset) {
341
+ const owner = wallet._native.publicKey;
342
+ const native = await connection.getBalance(owner).then((n) => BigInt(n)).catch(() => null);
343
+ if (asset === "native") return { token: native, native };
344
+ let token;
345
+ try {
346
+ const ata = getAssociatedTokenAddressSync2(new PublicKey2(asset), owner);
347
+ token = (await getAccount(connection, ata, "confirmed")).amount;
348
+ } catch (e) {
349
+ token = e instanceof TokenAccountNotFoundError ? 0n : null;
350
+ }
351
+ return { token, native };
352
+ },
353
+ // No receive prerequisite — the payer's tx idempotently creates the recipient's ATA (pay.ts).
354
+ async recipientReady() {
355
+ return { ready: "n/a" };
356
+ },
335
357
  async verify(ref, accept) {
336
358
  return verifySolana({ connection, signature: ref, accept });
337
359
  }