@zeroxyz/sdk 0.3.0 → 0.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.
- package/CHANGELOG.md +11 -0
- package/README.md +14 -7
- package/dist/{chunk-LVK77C6A.cjs → chunk-5ZVIL7AZ.cjs} +66 -11
- package/dist/chunk-5ZVIL7AZ.cjs.map +1 -0
- package/dist/{chunk-FH6C6TXR.js → chunk-RPL7VC32.js} +66 -11
- package/dist/chunk-RPL7VC32.js.map +1 -0
- package/dist/{client-CEC0IuN3.d.cts → client-qacxAiJ5.d.cts} +2 -1
- package/dist/{client-CEC0IuN3.d.ts → client-qacxAiJ5.d.ts} +2 -1
- package/dist/index.cjs +31 -31
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/testing.cjs +2 -2
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-FH6C6TXR.js.map +0 -1
- package/dist/chunk-LVK77C6A.cjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ All notable changes to `@zeroxyz/sdk` will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — with the pre-1.0 caveat that minor versions may include breaking changes.
|
|
6
6
|
|
|
7
|
+
## 0.4.0
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Pre-flight **balance gate** on paid `client.fetch()` / `payments.pay()` / `payments.payMpp()`. Before signing a paid leg the SDK reads the payer's USDC balance (x402 → Base; MPP → Tempo, including the Base→Tempo bridge math) and, if it can't cover the call, aborts with the new `ZeroInsufficientFundsError` instead of signing a doomed transaction. Previously an empty wallet signed anyway, the chain rejected, and the run was recorded at status 402 → the server classified it `payment_gated`, **indistinguishable from a capability that correctly payment-gates** — so unfunded wallets silently dinged cap reliability. (#585)
|
|
12
|
+
- New `FetchOutcome` value **`insufficient_funds`** and new error class **`ZeroInsufficientFundsError`** (`ZeroErrorCode` `insufficient_funds`, carrying `have` / `need`). On this outcome `client.fetch()` records **no run** (`status: null`, `runTrackingSkipped` includes `insufficient_funds_not_recorded`) — a wallet condition is never charged against the capability; the wallet funnel stays observable via the `fetch_executed.outcome` analytics event. (#585)
|
|
13
|
+
|
|
14
|
+
### Notes
|
|
15
|
+
|
|
16
|
+
- The balance read is **fail-open**: an RPC error proceeds to attempt payment rather than re-introducing a false `payment_gated`. Free capabilities are unaffected — a non-402 response never enters the payment path, and a `0`-amount challenge passes the gate (`balance < 0` is never true). The `maxPay` contract is unchanged. (#585)
|
|
17
|
+
|
|
7
18
|
## 0.3.0
|
|
8
19
|
|
|
9
20
|
### Added
|
package/README.md
CHANGED
|
@@ -118,9 +118,15 @@ switch (result.outcome) {
|
|
|
118
118
|
case "success":
|
|
119
119
|
// 2xx/3xx — result.body / result.bodyRaw are populated.
|
|
120
120
|
break;
|
|
121
|
+
case "insufficient_funds":
|
|
122
|
+
// Wallet balance is too low to cover the challenge amount. No payment
|
|
123
|
+
// was attempted. Top off via `zero wallet fund` (CLI) or fund the
|
|
124
|
+
// wallet before retrying. result.error is a ZeroInsufficientFundsError
|
|
125
|
+
// with .have and .need fields.
|
|
126
|
+
break;
|
|
121
127
|
case "payment_failed":
|
|
122
|
-
// Couldn't pay: over your maxPay
|
|
123
|
-
//
|
|
128
|
+
// Couldn't pay: over your maxPay or an unrecognized 402 protocol.
|
|
129
|
+
// Nothing settled — generally safe to retry.
|
|
124
130
|
break;
|
|
125
131
|
case "payment_rejected":
|
|
126
132
|
// Paid, but the capability still returned 402 (a settlement/facilitator
|
|
@@ -277,10 +283,11 @@ The SDK throws typed errors instead of strings. Match them with `instanceof`:
|
|
|
277
283
|
|
|
278
284
|
```ts
|
|
279
285
|
import {
|
|
280
|
-
ZeroApiError,
|
|
281
|
-
ZeroAuthError,
|
|
282
|
-
|
|
283
|
-
|
|
286
|
+
ZeroApiError, // a 4xx/5xx from Zero's own API
|
|
287
|
+
ZeroAuthError, // missing auth, failed refresh, expired session
|
|
288
|
+
ZeroInsufficientFundsError, // wallet balance too low; .have and .need in USDC
|
|
289
|
+
ZeroPaymentError, // payment failed before settlement
|
|
290
|
+
ZeroSessionCloseFailedError, // MPP session-close voucher rejected
|
|
284
291
|
ZeroTimeoutError, // the configured timeout was exceeded
|
|
285
292
|
ZeroConfigurationError, // invalid ClientOptions
|
|
286
293
|
ZeroWalletError, // a wallet read or write failed
|
|
@@ -301,7 +308,7 @@ try {
|
|
|
301
308
|
}
|
|
302
309
|
```
|
|
303
310
|
|
|
304
|
-
`client.fetch()` reserves throwing for programmer errors — bad config, missing credentials, and the like. Expected runtime failures (`network_error`, `payment_failed`, an upstream `5xx`) come back as a `FetchResult` with the matching `outcome`, so you handle them by branching on the result rather than by catching.
|
|
311
|
+
`client.fetch()` reserves throwing for programmer errors — bad config, missing credentials, and the like. Expected runtime failures (`insufficient_funds`, `network_error`, `payment_failed`, an upstream `5xx`) come back as a `FetchResult` with the matching `outcome`, so you handle them by branching on the result rather than by catching.
|
|
305
312
|
|
|
306
313
|
## Logging
|
|
307
314
|
|
|
@@ -79,6 +79,16 @@ var ZeroPaymentError = class extends ZeroError {
|
|
|
79
79
|
this.name = "ZeroPaymentError";
|
|
80
80
|
}
|
|
81
81
|
};
|
|
82
|
+
var ZeroInsufficientFundsError = class extends ZeroError {
|
|
83
|
+
have;
|
|
84
|
+
need;
|
|
85
|
+
constructor(message, init) {
|
|
86
|
+
super("insufficient_funds", message, { cause: init.cause });
|
|
87
|
+
this.name = "ZeroInsufficientFundsError";
|
|
88
|
+
this.have = init.have;
|
|
89
|
+
this.need = init.need;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
82
92
|
var ZeroSessionCloseFailedError = class extends ZeroError {
|
|
83
93
|
session;
|
|
84
94
|
response;
|
|
@@ -425,7 +435,12 @@ var FETCH_SKIP_REASONS = Object.freeze({
|
|
|
425
435
|
bodyReadFailed: "body_read_failed",
|
|
426
436
|
mppSessionCloseFailedNotRecorded: "mpp_session_close_failed_not_recorded",
|
|
427
437
|
paidButStill402NotRecorded: FETCH_WARNINGS.paidButStill402NotRecorded,
|
|
428
|
-
paymentSettlementUnconfirmed: FETCH_WARNINGS.paymentSettlementUnconfirmed
|
|
438
|
+
paymentSettlementUnconfirmed: FETCH_WARNINGS.paymentSettlementUnconfirmed,
|
|
439
|
+
// Pre-sign abort: deliberately NOT recorded as a run — the wallet was
|
|
440
|
+
// empty before we reached the capability, so charging it as a 402 would
|
|
441
|
+
// corrupt cap reliability. The wallet funnel is tracked via the
|
|
442
|
+
// fetch_executed.outcome analytics event instead.
|
|
443
|
+
insufficientFunds: "insufficient_funds_not_recorded"
|
|
429
444
|
});
|
|
430
445
|
var MAX_RECEIPT_HEADER_BYTES = 8 * 1024;
|
|
431
446
|
var decodeSessionReceiptHeader = (header) => {
|
|
@@ -567,6 +582,7 @@ var Payments = class {
|
|
|
567
582
|
);
|
|
568
583
|
let capturedAmount = null;
|
|
569
584
|
let capturedNetwork;
|
|
585
|
+
let preflightError = null;
|
|
570
586
|
const x402 = client.registerExactEvmScheme(new client$1.x402Client(), { signer: account });
|
|
571
587
|
x402.onBeforePaymentCreation(async (context) => {
|
|
572
588
|
const selected = context.selectedRequirements;
|
|
@@ -600,6 +616,29 @@ var Payments = class {
|
|
|
600
616
|
reason: `Payment of ${capturedAmount} USDC exceeds maxPay ${input.maxPay}`
|
|
601
617
|
};
|
|
602
618
|
}
|
|
619
|
+
const detected = networkToChain(capturedNetwork);
|
|
620
|
+
const balanceChain = detected === "base-sepolia" ? "base-sepolia" : "base";
|
|
621
|
+
let balance = null;
|
|
622
|
+
try {
|
|
623
|
+
balance = await this.readUsdcBalance(account.address, balanceChain);
|
|
624
|
+
} catch (readErr) {
|
|
625
|
+
this.client.logger?.({
|
|
626
|
+
level: "warn",
|
|
627
|
+
message: "Pre-flight balance read failed; proceeding to attempt payment",
|
|
628
|
+
meta: {
|
|
629
|
+
chain: balanceChain,
|
|
630
|
+
error: readErr instanceof Error ? readErr.message : String(readErr)
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
if (balance !== null && balance < rawAmountUnits) {
|
|
635
|
+
const have = viem.formatUnits(balance, 6);
|
|
636
|
+
preflightError = new ZeroInsufficientFundsError(
|
|
637
|
+
`wallet has ${have} USDC on Base, needs ${capturedAmount}.`,
|
|
638
|
+
{ have, need: capturedAmount }
|
|
639
|
+
);
|
|
640
|
+
return { abort: true, reason: preflightError.message };
|
|
641
|
+
}
|
|
603
642
|
});
|
|
604
643
|
const configuredFetch = buildConfiguredFetch(this.client, {
|
|
605
644
|
timeoutMs: input.timeoutMs
|
|
@@ -620,6 +659,7 @@ var Payments = class {
|
|
|
620
659
|
signal: input.signal
|
|
621
660
|
});
|
|
622
661
|
} catch (err) {
|
|
662
|
+
if (preflightError) throw preflightError;
|
|
623
663
|
if (err instanceof ZeroError) throw err;
|
|
624
664
|
throw new ZeroPaymentError(
|
|
625
665
|
`x402 payment failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -864,23 +904,29 @@ var Payments = class {
|
|
|
864
904
|
);
|
|
865
905
|
if (tempoBalance < requiredRaw) {
|
|
866
906
|
if (coerced.chain === "tempo-testnet") {
|
|
867
|
-
|
|
868
|
-
|
|
907
|
+
const have = viem.formatUnits(tempoBalance, 6);
|
|
908
|
+
throw new ZeroInsufficientFundsError(
|
|
909
|
+
`Insufficient pathUSD on Tempo testnet: have ${have}, need ${capturedAmount}. Fund via the Tempo testnet faucet: https://docs.tempo.xyz/quickstart/faucet`,
|
|
910
|
+
{ have, need: capturedAmount }
|
|
869
911
|
);
|
|
870
912
|
}
|
|
871
|
-
await this.bridgeBaseToTempo(account, requiredRaw - tempoBalance
|
|
913
|
+
await this.bridgeBaseToTempo(account, requiredRaw - tempoBalance, {
|
|
914
|
+
isSessionDeposit: challenge.intent === "session",
|
|
915
|
+
depositTotal: capturedAmount
|
|
916
|
+
});
|
|
872
917
|
}
|
|
873
918
|
return capturedAmount;
|
|
874
919
|
};
|
|
875
|
-
bridgeBaseToTempo = async (account, requiredAmount) => {
|
|
920
|
+
bridgeBaseToTempo = async (account, requiredAmount, depositContext) => {
|
|
876
921
|
this.ensureRelayClient();
|
|
877
922
|
const baseBalance = await this.readUsdcBalance(account.address, "base");
|
|
878
923
|
const buffer = calculateBuffer(baseBalance);
|
|
879
924
|
const bridgeAmount = requiredAmount + buffer;
|
|
880
925
|
if (baseBalance < bridgeAmount) {
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
)
|
|
926
|
+
const have = viem.formatUnits(baseBalance, 6);
|
|
927
|
+
const need = viem.formatUnits(bridgeAmount, 6);
|
|
928
|
+
const message = depositContext?.isSessionDeposit ? `wallet has ${have} USDC on Base, but this capability pre-funds a ${depositContext.depositTotal} USDC MPP payment channel up front (the channel is deposited before the call, so it asks for more than a single charge). Needs ${need} on Base to open it \u2014 fund the wallet, or pass a smaller \`maxPay\` (e.g. '0.05') to size the channel down to one cheap call.` : `wallet has ${have} USDC on Base, needs ${need} to fund this capability's MPP payment channel (${viem.formatUnits(requiredAmount, 6)} + ${viem.formatUnits(buffer, 6)} buffer). Tip: pass a smaller \`maxPay\` (e.g. '0.05') to size a cheaper channel.`;
|
|
929
|
+
throw new ZeroInsufficientFundsError(message, { have, need });
|
|
884
930
|
}
|
|
885
931
|
this.client.logger?.({
|
|
886
932
|
level: "info",
|
|
@@ -1100,7 +1146,7 @@ var Payments = class {
|
|
|
1100
1146
|
|
|
1101
1147
|
// package.json
|
|
1102
1148
|
var package_default = {
|
|
1103
|
-
version: "0.
|
|
1149
|
+
version: "0.4.0"};
|
|
1104
1150
|
|
|
1105
1151
|
// src/version.ts
|
|
1106
1152
|
var SDK_VERSION = package_default.version;
|
|
@@ -2482,6 +2528,15 @@ var clientFetch = async (client, url, opts = {}) => {
|
|
|
2482
2528
|
session: err.session
|
|
2483
2529
|
};
|
|
2484
2530
|
warnings.push(FETCH_WARNINGS.mppSessionCloseFailed);
|
|
2531
|
+
} else if (err instanceof ZeroInsufficientFundsError) {
|
|
2532
|
+
return errorFetchResult(
|
|
2533
|
+
startedAt,
|
|
2534
|
+
err.message,
|
|
2535
|
+
FETCH_SKIP_REASONS.insufficientFunds,
|
|
2536
|
+
capabilityIdRequested,
|
|
2537
|
+
"insufficient_funds",
|
|
2538
|
+
null
|
|
2539
|
+
);
|
|
2485
2540
|
} else if (err instanceof ZeroPaymentError || err instanceof ZeroAuthError) {
|
|
2486
2541
|
const result2 = errorFetchResult(
|
|
2487
2542
|
startedAt,
|
|
@@ -3114,5 +3169,5 @@ exports.coerceTempoChainId = coerceTempoChainId;
|
|
|
3114
3169
|
exports.createManagedAccount = createManagedAccount;
|
|
3115
3170
|
exports.paymentHasAnchor = paymentHasAnchor;
|
|
3116
3171
|
exports.tempoChainLabelFromId = tempoChainLabelFromId;
|
|
3117
|
-
//# sourceMappingURL=chunk-
|
|
3118
|
-
//# sourceMappingURL=chunk-
|
|
3172
|
+
//# sourceMappingURL=chunk-5ZVIL7AZ.cjs.map
|
|
3173
|
+
//# sourceMappingURL=chunk-5ZVIL7AZ.cjs.map
|