@piprail/sdk 1.3.1 → 1.5.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/CHAINS.md +31 -7
- package/CHANGELOG.md +74 -0
- package/ERRORS.md +13 -2
- package/README.md +46 -12
- package/dist/algorand-B67G4335.js +397 -0
- package/dist/algorand-IJJKE35X.cjs +397 -0
- package/dist/{aptos-MKZ5MAGL.cjs → aptos-X3G2UBYW.cjs} +44 -16
- package/dist/{aptos-DTAONNMM.js → aptos-YQWTGFRZ.js} +29 -1
- package/dist/{chunk-YJPWIK5L.cjs → chunk-IQGT65WS.cjs} +4 -2
- package/dist/{chunk-AGKC3C7Y.js → chunk-QDS6FBZP.js} +4 -2
- package/dist/index.cjs +358 -67
- package/dist/index.d.cts +162 -4
- package/dist/index.d.ts +162 -4
- package/dist/index.js +308 -17
- package/dist/{near-YX3XOASO.js → near-7MBBCDUE.js} +51 -1
- package/dist/{near-DISWUB7Y.cjs → near-GGUHLXAF.cjs} +76 -26
- package/dist/{solana-37F2PR5H.js → solana-7WJVZGDW.js} +23 -1
- package/dist/{solana-RJPNEFSN.cjs → solana-W24TCJV4.cjs} +39 -17
- package/dist/{stellar-ALOVOMFD.js → stellar-HV6VGZX3.js} +51 -1
- package/dist/{stellar-SUGNX52Z.cjs → stellar-YMY3K2YB.cjs} +70 -20
- package/dist/{sui-OLC5ID4X.js → sui-2WFWVFJX.js} +24 -1
- package/dist/{sui-HZWPHVU4.cjs → sui-32KVESR5.cjs} +40 -17
- package/dist/{ton-NIDWF77T.js → ton-DGZB7W4U.js} +24 -1
- package/dist/{ton-C4KTFXDL.cjs → ton-FIQGV2LC.cjs} +37 -14
- package/dist/{tron-LPMK57H7.js → tron-RLIL2FDI.js} +29 -1
- package/dist/{tron-DTU7NPEM.cjs → tron-ZSXAPZ2C.cjs} +52 -24
- package/dist/{xrpl-N6ZAJRGC.cjs → xrpl-2PKP7HOI.cjs} +81 -21
- package/dist/{xrpl-6ODQS7JR.js → xrpl-UEC2GYVV.js} +61 -1
- package/package.json +9 -2
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
parseUnits,
|
|
21
21
|
rejectForeignToken,
|
|
22
22
|
toInsufficientFundsError
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-QDS6FBZP.js";
|
|
24
24
|
|
|
25
25
|
// src/drivers/registry.ts
|
|
26
26
|
var byFamily = /* @__PURE__ */ new Map();
|
|
@@ -40,6 +40,7 @@ function familyForChain(chain) {
|
|
|
40
40
|
if (chain.startsWith("sui")) return "sui";
|
|
41
41
|
if (chain.startsWith("near")) return "near";
|
|
42
42
|
if (chain.startsWith("aptos")) return "aptos";
|
|
43
|
+
if (chain.startsWith("algorand")) return "algorand";
|
|
43
44
|
return "evm";
|
|
44
45
|
}
|
|
45
46
|
return "evm";
|
|
@@ -62,7 +63,7 @@ function resolveNetwork(opts) {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
// src/drivers/evm/index.ts
|
|
65
|
-
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";
|
|
66
67
|
|
|
67
68
|
// src/drivers/evm/chains.ts
|
|
68
69
|
import { defineChain } from "viem";
|
|
@@ -715,6 +716,27 @@ function makeEvmNetwork(resolved) {
|
|
|
715
716
|
});
|
|
716
717
|
}
|
|
717
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
|
+
// EVM has no receive prerequisite — any 0x address receives native or ERC-20 immediately.
|
|
737
|
+
async recipientReady() {
|
|
738
|
+
return { ready: "n/a" };
|
|
739
|
+
},
|
|
718
740
|
async verify(ref, accept) {
|
|
719
741
|
return verifyEvm({
|
|
720
742
|
publicClient,
|
|
@@ -738,7 +760,7 @@ var loaders = {
|
|
|
738
760
|
solana: async () => {
|
|
739
761
|
let mod;
|
|
740
762
|
try {
|
|
741
|
-
mod = await import("./solana-
|
|
763
|
+
mod = await import("./solana-7WJVZGDW.js");
|
|
742
764
|
} catch (cause) {
|
|
743
765
|
throw new MissingDriverError(
|
|
744
766
|
`Solana selected, but its packages aren't installed. Run: npm install @solana/web3.js @solana/spl-token bs58`,
|
|
@@ -750,7 +772,7 @@ var loaders = {
|
|
|
750
772
|
ton: async () => {
|
|
751
773
|
let mod;
|
|
752
774
|
try {
|
|
753
|
-
mod = await import("./ton-
|
|
775
|
+
mod = await import("./ton-DGZB7W4U.js");
|
|
754
776
|
} catch (cause) {
|
|
755
777
|
throw new MissingDriverError(
|
|
756
778
|
`TON selected, but its packages aren't installed. Run: npm install @ton/ton @ton/core @ton/crypto`,
|
|
@@ -762,7 +784,7 @@ var loaders = {
|
|
|
762
784
|
stellar: async () => {
|
|
763
785
|
let mod;
|
|
764
786
|
try {
|
|
765
|
-
mod = await import("./stellar-
|
|
787
|
+
mod = await import("./stellar-HV6VGZX3.js");
|
|
766
788
|
} catch (cause) {
|
|
767
789
|
throw new MissingDriverError(
|
|
768
790
|
`Stellar selected, but its package isn't installed. Run: npm install @stellar/stellar-sdk`,
|
|
@@ -774,7 +796,7 @@ var loaders = {
|
|
|
774
796
|
xrpl: async () => {
|
|
775
797
|
let mod;
|
|
776
798
|
try {
|
|
777
|
-
mod = await import("./xrpl-
|
|
799
|
+
mod = await import("./xrpl-UEC2GYVV.js");
|
|
778
800
|
} catch (cause) {
|
|
779
801
|
throw new MissingDriverError(
|
|
780
802
|
`XRPL selected, but its package isn't installed. Run: npm install xrpl`,
|
|
@@ -786,7 +808,7 @@ var loaders = {
|
|
|
786
808
|
tron: async () => {
|
|
787
809
|
let mod;
|
|
788
810
|
try {
|
|
789
|
-
mod = await import("./tron-
|
|
811
|
+
mod = await import("./tron-RLIL2FDI.js");
|
|
790
812
|
} catch (cause) {
|
|
791
813
|
throw new MissingDriverError(
|
|
792
814
|
`Tron selected, but its package isn't installed. Run: npm install tronweb`,
|
|
@@ -798,7 +820,7 @@ var loaders = {
|
|
|
798
820
|
sui: async () => {
|
|
799
821
|
let mod;
|
|
800
822
|
try {
|
|
801
|
-
mod = await import("./sui-
|
|
823
|
+
mod = await import("./sui-2WFWVFJX.js");
|
|
802
824
|
} catch (cause) {
|
|
803
825
|
throw new MissingDriverError(
|
|
804
826
|
`Sui selected, but its package isn't installed. Run: npm install @mysten/sui`,
|
|
@@ -810,7 +832,7 @@ var loaders = {
|
|
|
810
832
|
near: async () => {
|
|
811
833
|
let mod;
|
|
812
834
|
try {
|
|
813
|
-
mod = await import("./near-
|
|
835
|
+
mod = await import("./near-7MBBCDUE.js");
|
|
814
836
|
} catch (cause) {
|
|
815
837
|
throw new MissingDriverError(
|
|
816
838
|
`NEAR selected, but its package isn't installed. Run: npm install near-api-js`,
|
|
@@ -822,7 +844,7 @@ var loaders = {
|
|
|
822
844
|
aptos: async () => {
|
|
823
845
|
let mod;
|
|
824
846
|
try {
|
|
825
|
-
mod = await import("./aptos-
|
|
847
|
+
mod = await import("./aptos-YQWTGFRZ.js");
|
|
826
848
|
} catch (cause) {
|
|
827
849
|
throw new MissingDriverError(
|
|
828
850
|
`Aptos selected, but its package isn't installed. Run: npm install @aptos-labs/ts-sdk`,
|
|
@@ -830,6 +852,18 @@ var loaders = {
|
|
|
830
852
|
);
|
|
831
853
|
}
|
|
832
854
|
registerDriver(mod.aptosDriver);
|
|
855
|
+
},
|
|
856
|
+
algorand: async () => {
|
|
857
|
+
let mod;
|
|
858
|
+
try {
|
|
859
|
+
mod = await import("./algorand-B67G4335.js");
|
|
860
|
+
} catch (cause) {
|
|
861
|
+
throw new MissingDriverError(
|
|
862
|
+
`Algorand selected, but its package isn't installed. Run: npm install algosdk`,
|
|
863
|
+
{ cause }
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
registerDriver(mod.algorandDriver);
|
|
833
867
|
}
|
|
834
868
|
};
|
|
835
869
|
var inFlight = /* @__PURE__ */ new Map();
|
|
@@ -963,6 +997,12 @@ var SpendLedger = class {
|
|
|
963
997
|
};
|
|
964
998
|
|
|
965
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
|
+
};
|
|
966
1006
|
var PipRailClient = class {
|
|
967
1007
|
opts;
|
|
968
1008
|
maxRetries;
|
|
@@ -1071,6 +1111,44 @@ var PipRailClient = class {
|
|
|
1071
1111
|
spent() {
|
|
1072
1112
|
return this.ledger.summary();
|
|
1073
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
|
+
}
|
|
1074
1152
|
/**
|
|
1075
1153
|
* Lower-level: drive any HTTP method through the 402 flow.
|
|
1076
1154
|
*
|
|
@@ -1087,10 +1165,19 @@ var PipRailClient = class {
|
|
|
1087
1165
|
}
|
|
1088
1166
|
const firstResponse = await fetch(url, init);
|
|
1089
1167
|
if (firstResponse.status !== 402) return firstResponse;
|
|
1090
|
-
const
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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
|
+
}
|
|
1094
1181
|
this.safeEmit({ kind: "payment-required", challenge, accept });
|
|
1095
1182
|
await this.authorize(quote);
|
|
1096
1183
|
const { ref, confirmed } = await this.payAndConfirm(net, wallet, accept);
|
|
@@ -1112,9 +1199,7 @@ var PipRailClient = class {
|
|
|
1112
1199
|
);
|
|
1113
1200
|
}
|
|
1114
1201
|
const { net, wallet } = await this.ensure();
|
|
1115
|
-
const candidates =
|
|
1116
|
-
(a) => a.scheme === "onchain-proof" && net.supports(a.network)
|
|
1117
|
-
);
|
|
1202
|
+
const candidates = this.gatherCandidates(net, challenge);
|
|
1118
1203
|
if (candidates.length === 0) {
|
|
1119
1204
|
const networks = challenge.accepts.map((a) => a.network).join(", ");
|
|
1120
1205
|
throw new NoCompatibleAcceptError(
|
|
@@ -1128,6 +1213,111 @@ var PipRailClient = class {
|
|
|
1128
1213
|
const chosen = priced.find((p) => p.quote.withinPolicy) ?? priced[0];
|
|
1129
1214
|
return { net, wallet, accept: chosen.accept, challenge, quote: chosen.quote };
|
|
1130
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
|
+
}
|
|
1131
1321
|
/** Build the agent-facing quote for an accept: TRUE decimals/symbol (via the
|
|
1132
1322
|
* driver's describeAsset) + the policy verdict + a symbol-mismatch flag. */
|
|
1133
1323
|
buildQuote(net, accept, url, description) {
|
|
@@ -1300,6 +1490,68 @@ var PipRailClient = class {
|
|
|
1300
1490
|
);
|
|
1301
1491
|
}
|
|
1302
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
|
+
}
|
|
1303
1555
|
function hostOf(url) {
|
|
1304
1556
|
try {
|
|
1305
1557
|
return new URL(url).hostname;
|
|
@@ -1359,6 +1611,44 @@ function paymentTools(client) {
|
|
|
1359
1611
|
return quote ? { gated: true, ...quote } : { gated: false, url: String(args.url) };
|
|
1360
1612
|
}
|
|
1361
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
|
+
},
|
|
1362
1652
|
{
|
|
1363
1653
|
name: "piprail_pay_request",
|
|
1364
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.",
|
|
@@ -1725,6 +2015,7 @@ export {
|
|
|
1725
2015
|
parseSignatureHeader,
|
|
1726
2016
|
paymentTools,
|
|
1727
2017
|
pickAccept,
|
|
2018
|
+
planAcross,
|
|
1728
2019
|
registerDriver,
|
|
1729
2020
|
requirePayment,
|
|
1730
2021
|
resolveChain,
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
nativeCost,
|
|
8
8
|
rejectForeignToken,
|
|
9
9
|
toInsufficientFundsError
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-QDS6FBZP.js";
|
|
11
11
|
|
|
12
12
|
// src/drivers/near/index.ts
|
|
13
13
|
import { JsonRpcProvider, Account, actions } from "near-api-js";
|
|
@@ -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 });
|