@blockrun/llm 3.19.1 → 3.20.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/README.md +260 -66
- package/dist/index.cjs +2207 -203
- package/dist/index.d.cts +319 -21
- package/dist/index.d.ts +319 -21
- package/dist/index.js +2203 -203
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -36,8 +36,11 @@ __export(index_exports, {
|
|
|
36
36
|
AnthropicClient: () => AnthropicClient,
|
|
37
37
|
BASE_CHAIN_ID: () => BASE_CHAIN_ID,
|
|
38
38
|
BLOCKRUN_SOL_OPERATOR: () => BLOCKRUN_SOL_OPERATOR,
|
|
39
|
+
BatchCloseDeferredError: () => BatchCloseDeferredError,
|
|
40
|
+
BatchPaymentUnresolvedError: () => BatchPaymentUnresolvedError,
|
|
39
41
|
BlockrunClient: () => BlockrunClient,
|
|
40
42
|
BlockrunError: () => BlockrunError,
|
|
43
|
+
DEFAULT_BATCH_RATE_LIMIT: () => DEFAULT_BATCH_RATE_LIMIT,
|
|
41
44
|
EVM_NETWORKS: () => EVM_NETWORKS,
|
|
42
45
|
ImageClient: () => ImageClient,
|
|
43
46
|
KNOWN_PROVIDERS: () => KNOWN_PROVIDERS,
|
|
@@ -100,6 +103,7 @@ __export(index_exports, {
|
|
|
100
103
|
loadWallet: () => loadWallet,
|
|
101
104
|
logCost: () => logCost,
|
|
102
105
|
parsePaymentRequired: () => parsePaymentRequired,
|
|
106
|
+
retryDisposition: () => retryDisposition,
|
|
103
107
|
saveSolanaWallet: () => saveSolanaWallet,
|
|
104
108
|
saveToCache: () => saveToCache,
|
|
105
109
|
saveWallet: () => saveWallet,
|
|
@@ -121,6 +125,21 @@ __export(index_exports, {
|
|
|
121
125
|
module.exports = __toCommonJS(index_exports);
|
|
122
126
|
|
|
123
127
|
// src/types.ts
|
|
128
|
+
var RETRY_DISPOSITION = /* @__PURE__ */ Symbol.for("@blockrun/llm/retry-disposition/v1");
|
|
129
|
+
function retryDisposition(err) {
|
|
130
|
+
if (!err || typeof err !== "object" && typeof err !== "function") return void 0;
|
|
131
|
+
const value = err[RETRY_DISPOSITION];
|
|
132
|
+
return value === "unpaid" || value === "paid-or-in-doubt" ? value : void 0;
|
|
133
|
+
}
|
|
134
|
+
function withDisposition(err, disposition) {
|
|
135
|
+
if (!err || typeof err !== "object") return err;
|
|
136
|
+
if (retryDisposition(err) === "paid-or-in-doubt") return err;
|
|
137
|
+
try {
|
|
138
|
+
Object.defineProperty(err, RETRY_DISPOSITION, { value: disposition, configurable: true, writable: true, enumerable: false });
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
return err;
|
|
142
|
+
}
|
|
124
143
|
var BlockrunError = class extends Error {
|
|
125
144
|
constructor(message) {
|
|
126
145
|
super(message);
|
|
@@ -312,6 +331,15 @@ function resolveApiKeyAuth(options) {
|
|
|
312
331
|
const base = options.apiUrl ?? (typeof process !== "undefined" ? process.env?.BLOCKRUN_API_BASE_URL : void 0) ?? API_KEY_URL;
|
|
313
332
|
return new ApiKeyAuth(key.trim(), base);
|
|
314
333
|
}
|
|
334
|
+
function accountErrorDisposition(err) {
|
|
335
|
+
if (!(err instanceof APIError)) return "paid-or-in-doubt";
|
|
336
|
+
if (err.statusCode >= 400 && err.statusCode < 500) return "unpaid";
|
|
337
|
+
if (err.statusCode >= 500 && hasGatewayErrorEnvelope(err.response)) return "unpaid";
|
|
338
|
+
return "paid-or-in-doubt";
|
|
339
|
+
}
|
|
340
|
+
function hasGatewayErrorEnvelope(response) {
|
|
341
|
+
return !!response && typeof response === "object" && typeof response.message === "string" && response.message.length > 0;
|
|
342
|
+
}
|
|
315
343
|
function requireWallet(value) {
|
|
316
344
|
if (value === void 0) throw new Error("This operation requires a wallet; the client uses API key account billing.");
|
|
317
345
|
return value;
|
|
@@ -4178,6 +4206,7 @@ var AUTO_ROUTING_PROFILES = {
|
|
|
4178
4206
|
var BASE_MINIMUM_PAYMENT_USD = 2e-3;
|
|
4179
4207
|
var SOLANA_MINIMUM_PAYMENT_USD = 1e-3;
|
|
4180
4208
|
function isTransientError(err) {
|
|
4209
|
+
if (retryDisposition(err) !== "unpaid") return false;
|
|
4181
4210
|
if (err instanceof PaymentError) return false;
|
|
4182
4211
|
if (err instanceof APIError) {
|
|
4183
4212
|
return [429, 502, 503, 504, 522, 524].includes(err.statusCode);
|
|
@@ -4661,7 +4690,7 @@ function getCostSummary() {
|
|
|
4661
4690
|
}
|
|
4662
4691
|
|
|
4663
4692
|
// src/version.ts
|
|
4664
|
-
var SDK_VERSION = "3.
|
|
4693
|
+
var SDK_VERSION = "3.20.1";
|
|
4665
4694
|
var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
|
|
4666
4695
|
|
|
4667
4696
|
// src/client.ts
|
|
@@ -4933,10 +4962,13 @@ var LLMClient = class _LLMClient {
|
|
|
4933
4962
|
* Full chat completion interface (OpenAI-compatible).
|
|
4934
4963
|
*
|
|
4935
4964
|
* When `fallbackModels` is set, transient failures (timeouts, network
|
|
4936
|
-
* errors, 5xx) on the primary model trigger a retry against the next
|
|
4937
|
-
* model in the list before raising
|
|
4938
|
-
*
|
|
4939
|
-
*
|
|
4965
|
+
* errors, 429, 5xx) on the primary model trigger a retry against the next
|
|
4966
|
+
* model in the list before raising, but only while no payment has been
|
|
4967
|
+
* sent for it (the error's {@link RetryDisposition} is `"unpaid"`). Once a
|
|
4968
|
+
* payment was sent, any failure propagates: the next model would be a
|
|
4969
|
+
* second charge. 4xx errors and PaymentError also propagate immediately —
|
|
4970
|
+
* those aren't "swap upstream and retry" situations. Each fallback hop
|
|
4971
|
+
* logs one stderr line.
|
|
4940
4972
|
*
|
|
4941
4973
|
* @param model - Primary model ID
|
|
4942
4974
|
* @param messages - Array of messages with role and content
|
|
@@ -5039,21 +5071,25 @@ var LLMClient = class _LLMClient {
|
|
|
5039
5071
|
}
|
|
5040
5072
|
/**
|
|
5041
5073
|
* Make a request with automatic x402 payment handling.
|
|
5074
|
+
*
|
|
5075
|
+
* Every error it throws carries a {@link RetryDisposition}, which is what
|
|
5076
|
+
* {@link LLMClient.chatCompletion}'s fallback walk honours: errors of the
|
|
5077
|
+
* unpaid first request (and its one 502/503 retry), of the 402 challenge
|
|
5078
|
+
* and of signing are `"unpaid"`; anything after the signed payment was sent
|
|
5079
|
+
* is `"paid-or-in-doubt"`. With an API key the request itself is billed, so
|
|
5080
|
+
* only the account API's explicit 4xx refusal is `"unpaid"`.
|
|
5042
5081
|
*/
|
|
5043
5082
|
async requestWithPayment(endpoint, body) {
|
|
5044
5083
|
const url = `${this.apiUrl}${endpoint}`;
|
|
5045
|
-
const
|
|
5084
|
+
const unpaid = {
|
|
5046
5085
|
method: "POST",
|
|
5047
5086
|
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
|
|
5048
5087
|
body: JSON.stringify(body)
|
|
5049
|
-
}
|
|
5088
|
+
};
|
|
5089
|
+
const response = await this.sendUnpaid(url, unpaid);
|
|
5050
5090
|
if (response.status === 502 || response.status === 503) {
|
|
5051
5091
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
5052
|
-
const retryResp = await this.
|
|
5053
|
-
method: "POST",
|
|
5054
|
-
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
|
|
5055
|
-
body: JSON.stringify(body)
|
|
5056
|
-
});
|
|
5092
|
+
const retryResp = await this.sendUnpaid(url, unpaid);
|
|
5057
5093
|
if (retryResp.status !== 502 && retryResp.status !== 503) {
|
|
5058
5094
|
if (retryResp.status === 402) return this.handlePaymentAndRetry(url, body, retryResp);
|
|
5059
5095
|
if (!retryResp.ok) {
|
|
@@ -5063,9 +5099,12 @@ var LLMClient = class _LLMClient {
|
|
|
5063
5099
|
} catch {
|
|
5064
5100
|
errorBody = { error: "Request failed" };
|
|
5065
5101
|
}
|
|
5066
|
-
throw
|
|
5102
|
+
throw withDisposition(
|
|
5103
|
+
new APIError(`API error: ${retryResp.status}`, retryResp.status, sanitizeErrorResponse(errorBody)),
|
|
5104
|
+
"unpaid"
|
|
5105
|
+
);
|
|
5067
5106
|
}
|
|
5068
|
-
return this.
|
|
5107
|
+
return this.parseUnpaidChatResponse(retryResp);
|
|
5069
5108
|
}
|
|
5070
5109
|
}
|
|
5071
5110
|
if (response.status === 402) {
|
|
@@ -5078,18 +5117,61 @@ var LLMClient = class _LLMClient {
|
|
|
5078
5117
|
} catch {
|
|
5079
5118
|
errorBody = { error: "Request failed" };
|
|
5080
5119
|
}
|
|
5081
|
-
throw
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5120
|
+
throw withDisposition(
|
|
5121
|
+
new APIError(
|
|
5122
|
+
`API error: ${response.status}`,
|
|
5123
|
+
response.status,
|
|
5124
|
+
sanitizeErrorResponse(errorBody)
|
|
5125
|
+
),
|
|
5126
|
+
"unpaid"
|
|
5085
5127
|
);
|
|
5086
5128
|
}
|
|
5087
|
-
return this.
|
|
5129
|
+
return this.parseUnpaidChatResponse(response);
|
|
5130
|
+
}
|
|
5131
|
+
/**
|
|
5132
|
+
* Send a request that carries no payment, marking what it throws with its
|
|
5133
|
+
* {@link RetryDisposition}: `"unpaid"` with a wallet; with an API key, see
|
|
5134
|
+
* {@link accountErrorDisposition} (the request itself is billed).
|
|
5135
|
+
*/
|
|
5136
|
+
async sendUnpaid(url, init) {
|
|
5137
|
+
try {
|
|
5138
|
+
return await this.fetchWithTimeout(url, init);
|
|
5139
|
+
} catch (err) {
|
|
5140
|
+
throw withDisposition(err, this.apiAuth ? accountErrorDisposition(err) : "unpaid");
|
|
5141
|
+
}
|
|
5142
|
+
}
|
|
5143
|
+
/**
|
|
5144
|
+
* Parse a response to a request that carried no payment: the free tier,
|
|
5145
|
+
* or, with an API key, the billed request itself.
|
|
5146
|
+
*/
|
|
5147
|
+
async parseUnpaidChatResponse(response) {
|
|
5148
|
+
try {
|
|
5149
|
+
return await this.parseChatResponse(response);
|
|
5150
|
+
} catch (err) {
|
|
5151
|
+
throw withDisposition(err, this.apiAuth ? "paid-or-in-doubt" : "unpaid");
|
|
5152
|
+
}
|
|
5088
5153
|
}
|
|
5089
5154
|
/**
|
|
5090
5155
|
* Handle 402 response: parse requirements, sign payment, retry.
|
|
5156
|
+
*
|
|
5157
|
+
* Anything thrown before the signed payment is sent is `"unpaid"`; anything
|
|
5158
|
+
* after it, `"paid-or-in-doubt"` (see {@link RetryDisposition}).
|
|
5091
5159
|
*/
|
|
5092
5160
|
async handlePaymentAndRetry(url, body, response) {
|
|
5161
|
+
let signed;
|
|
5162
|
+
try {
|
|
5163
|
+
signed = await this.signFrom402(url, response);
|
|
5164
|
+
} catch (err) {
|
|
5165
|
+
throw withDisposition(err, "unpaid");
|
|
5166
|
+
}
|
|
5167
|
+
try {
|
|
5168
|
+
return await this.sendPaid(url, body, signed.paymentPayload, signed.details);
|
|
5169
|
+
} catch (err) {
|
|
5170
|
+
throw withDisposition(err, "paid-or-in-doubt");
|
|
5171
|
+
}
|
|
5172
|
+
}
|
|
5173
|
+
/** Read a 402's requirements and sign the exact payment for them. Sends nothing. */
|
|
5174
|
+
async signFrom402(url, response) {
|
|
5093
5175
|
let paymentHeader = response.headers.get("payment-required");
|
|
5094
5176
|
if (!paymentHeader) {
|
|
5095
5177
|
try {
|
|
@@ -5125,6 +5207,10 @@ var LLMClient = class _LLMClient {
|
|
|
5125
5207
|
extensions
|
|
5126
5208
|
}
|
|
5127
5209
|
);
|
|
5210
|
+
return { paymentPayload, details };
|
|
5211
|
+
}
|
|
5212
|
+
/** Send the signed payment and read the answer. The payment may settle from here on. */
|
|
5213
|
+
async sendPaid(url, body, paymentPayload, details) {
|
|
5128
5214
|
const retryResponse = await this.fetchWithTimeout(url, {
|
|
5129
5215
|
method: "POST",
|
|
5130
5216
|
headers: {
|
|
@@ -5261,25 +5347,46 @@ var LLMClient = class _LLMClient {
|
|
|
5261
5347
|
const cached = this.preAuthCache.get(cacheKey2);
|
|
5262
5348
|
const now = Date.now();
|
|
5263
5349
|
if (cached && now - cached.cachedAt < _LLMClient.PRE_AUTH_TTL_MS) {
|
|
5350
|
+
let signed;
|
|
5264
5351
|
try {
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5352
|
+
signed = await this.signPayment(cached.paymentHeader);
|
|
5353
|
+
} catch {
|
|
5354
|
+
this.preAuthCache.delete(cacheKey2);
|
|
5355
|
+
}
|
|
5356
|
+
if (signed) {
|
|
5357
|
+
let preAuthResp;
|
|
5358
|
+
try {
|
|
5359
|
+
preAuthResp = await this.fetchWithTimeout(url, {
|
|
5360
|
+
method: "POST",
|
|
5361
|
+
headers: {
|
|
5362
|
+
"Content-Type": "application/json",
|
|
5363
|
+
"User-Agent": USER_AGENT,
|
|
5364
|
+
"PAYMENT-SIGNATURE": signed.paymentPayload
|
|
5365
|
+
},
|
|
5366
|
+
body: JSON.stringify(body)
|
|
5367
|
+
});
|
|
5368
|
+
} catch (err) {
|
|
5369
|
+
this.preAuthCache.delete(cacheKey2);
|
|
5370
|
+
throw withDisposition(err, "paid-or-in-doubt");
|
|
5371
|
+
}
|
|
5372
|
+
if (preAuthResp.ok) {
|
|
5276
5373
|
this.sessionCalls += 1;
|
|
5277
|
-
this.sessionTotalUsd +=
|
|
5374
|
+
this.sessionTotalUsd += signed.costUsd;
|
|
5278
5375
|
return preAuthResp;
|
|
5279
5376
|
}
|
|
5280
5377
|
this.preAuthCache.delete(cacheKey2);
|
|
5281
|
-
|
|
5282
|
-
|
|
5378
|
+
if (preAuthResp.status !== 402) {
|
|
5379
|
+
let errorBody;
|
|
5380
|
+
try {
|
|
5381
|
+
errorBody = await preAuthResp.json();
|
|
5382
|
+
} catch {
|
|
5383
|
+
errorBody = { error: "Request failed" };
|
|
5384
|
+
}
|
|
5385
|
+
throw withDisposition(
|
|
5386
|
+
new APIError(`API error after payment: ${preAuthResp.status}`, preAuthResp.status, sanitizeErrorResponse(errorBody)),
|
|
5387
|
+
"paid-or-in-doubt"
|
|
5388
|
+
);
|
|
5389
|
+
}
|
|
5283
5390
|
}
|
|
5284
5391
|
}
|
|
5285
5392
|
const firstResp = await this.fetchWithTimeout(url, {
|
|
@@ -9348,11 +9455,11 @@ var WALLET_DIR = path4.join(os3.homedir(), ".blockrun");
|
|
|
9348
9455
|
var SOLANA_WALLET_FILE = path4.join(WALLET_DIR, ".solana-session");
|
|
9349
9456
|
async function createSolanaWallet() {
|
|
9350
9457
|
const { Keypair } = await loadSolanaWeb3("Solana wallet operations");
|
|
9351
|
-
const
|
|
9458
|
+
const bs582 = await import("bs58");
|
|
9352
9459
|
const keypair = Keypair.generate();
|
|
9353
9460
|
return {
|
|
9354
9461
|
address: keypair.publicKey.toBase58(),
|
|
9355
|
-
privateKey: (
|
|
9462
|
+
privateKey: (bs582.default ?? bs582).encode(keypair.secretKey)
|
|
9356
9463
|
};
|
|
9357
9464
|
}
|
|
9358
9465
|
async function solanaKeyToBytes(privateKey) {
|
|
@@ -9390,8 +9497,8 @@ async function solanaKeyToBytes(privateKey) {
|
|
|
9390
9497
|
return bytes;
|
|
9391
9498
|
}
|
|
9392
9499
|
try {
|
|
9393
|
-
const
|
|
9394
|
-
const bytes = (
|
|
9500
|
+
const bs582 = await import("bs58");
|
|
9501
|
+
const bytes = (bs582.default ?? bs582).decode(key);
|
|
9395
9502
|
if (bytes.length !== 64) {
|
|
9396
9503
|
throw new Error(`Invalid Solana key length: expected 64 bytes, got ${bytes.length}`);
|
|
9397
9504
|
}
|
|
@@ -9571,10 +9678,59 @@ async function getOrCreateSolanaWallet() {
|
|
|
9571
9678
|
}
|
|
9572
9679
|
|
|
9573
9680
|
// src/solana-batch.ts
|
|
9681
|
+
var import_async_hooks = require("async_hooks");
|
|
9682
|
+
var import_crypto = require("crypto");
|
|
9574
9683
|
var fs6 = __toESM(require("fs"), 1);
|
|
9575
9684
|
var path5 = __toESM(require("path"), 1);
|
|
9685
|
+
var import_bs58 = __toESM(require("bs58"), 1);
|
|
9576
9686
|
var BATCH_SCHEME = "batch-settlement";
|
|
9577
9687
|
var BLOCKRUN_SOL_OPERATOR = "5YKPQUFjw5WQqhSUkEGKNNfYYVqnRRNbpYyL71qQ1vm3";
|
|
9688
|
+
var DEFAULT_BATCH_RATE_LIMIT = { maxAttempts: 3, maxWaitMs: 6e4 };
|
|
9689
|
+
var BatchPaymentUnresolvedError = class extends PaymentError {
|
|
9690
|
+
/** The {@link SolanaBatchEvent.reason} of the `unresolved` event reported with it. */
|
|
9691
|
+
reason;
|
|
9692
|
+
/** The paying wallet's address. */
|
|
9693
|
+
wallet;
|
|
9694
|
+
/** The request id of the payment that may have been charged. */
|
|
9695
|
+
requestId;
|
|
9696
|
+
/** The channel it paid into. */
|
|
9697
|
+
channelId;
|
|
9698
|
+
/** What it carried. */
|
|
9699
|
+
payloadKind;
|
|
9700
|
+
/** HTTP status of the gateway's last answer to it, when there was one. */
|
|
9701
|
+
status;
|
|
9702
|
+
/** Whether it carried a deposit, which is now in doubt as well. */
|
|
9703
|
+
depositInDoubt;
|
|
9704
|
+
constructor(init) {
|
|
9705
|
+
super(
|
|
9706
|
+
`batch-settlement payment ${init.requestId ?? "<unknown>"} from wallet ${init.wallet} may have been charged and is not paid again${init.detail ? `: ${init.detail}` : ""}`
|
|
9707
|
+
);
|
|
9708
|
+
this.name = "BatchPaymentUnresolvedError";
|
|
9709
|
+
withDisposition(this, "paid-or-in-doubt");
|
|
9710
|
+
this.reason = init.reason;
|
|
9711
|
+
this.wallet = init.wallet;
|
|
9712
|
+
this.requestId = init.requestId;
|
|
9713
|
+
this.channelId = init.channelId;
|
|
9714
|
+
this.payloadKind = init.payloadKind;
|
|
9715
|
+
this.status = init.status;
|
|
9716
|
+
this.depositInDoubt = init.payloadKind === "open" || init.payloadKind === "top-up";
|
|
9717
|
+
if (init.cause !== void 0) this.cause = init.cause;
|
|
9718
|
+
}
|
|
9719
|
+
};
|
|
9720
|
+
var BatchCloseDeferredError = class extends BlockrunError {
|
|
9721
|
+
reason;
|
|
9722
|
+
/** The paying wallet's address. */
|
|
9723
|
+
wallet;
|
|
9724
|
+
/** The channels whose deposit is in doubt (`deposit_in_doubt`). */
|
|
9725
|
+
channelIds;
|
|
9726
|
+
constructor(init) {
|
|
9727
|
+
super(`batch-settlement channel close deferred for wallet ${init.wallet}: ${init.detail}`);
|
|
9728
|
+
this.name = "BatchCloseDeferredError";
|
|
9729
|
+
this.reason = init.reason;
|
|
9730
|
+
this.wallet = init.wallet;
|
|
9731
|
+
this.channelIds = init.channelIds ?? [];
|
|
9732
|
+
}
|
|
9733
|
+
};
|
|
9578
9734
|
var INSTALL_HINT2 = "npm install @x402/core@~2.28.0 @x402/svm@~2.28.0 @solana/kit";
|
|
9579
9735
|
async function load(pkg, importer) {
|
|
9580
9736
|
try {
|
|
@@ -9624,6 +9780,10 @@ var FileChannelStorage = class {
|
|
|
9624
9780
|
delete all[key];
|
|
9625
9781
|
this.write(all);
|
|
9626
9782
|
}
|
|
9783
|
+
/** The keys of every stored channel. */
|
|
9784
|
+
async keys() {
|
|
9785
|
+
return Object.keys(this.read());
|
|
9786
|
+
}
|
|
9627
9787
|
/** Forget every channel in this file. */
|
|
9628
9788
|
clear() {
|
|
9629
9789
|
try {
|
|
@@ -9633,6 +9793,192 @@ var FileChannelStorage = class {
|
|
|
9633
9793
|
}
|
|
9634
9794
|
}
|
|
9635
9795
|
};
|
|
9796
|
+
var MemoryChannelStorage = class {
|
|
9797
|
+
records = /* @__PURE__ */ new Map();
|
|
9798
|
+
async get(key) {
|
|
9799
|
+
const raw = this.records.get(key);
|
|
9800
|
+
return raw === void 0 ? void 0 : JSON.parse(raw);
|
|
9801
|
+
}
|
|
9802
|
+
async set(key, record) {
|
|
9803
|
+
this.records.set(key, JSON.stringify(record));
|
|
9804
|
+
}
|
|
9805
|
+
async delete(key) {
|
|
9806
|
+
this.records.delete(key);
|
|
9807
|
+
}
|
|
9808
|
+
async keys() {
|
|
9809
|
+
return [...this.records.keys()];
|
|
9810
|
+
}
|
|
9811
|
+
clear() {
|
|
9812
|
+
this.records.clear();
|
|
9813
|
+
}
|
|
9814
|
+
};
|
|
9815
|
+
function isPlainObject(value) {
|
|
9816
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9817
|
+
}
|
|
9818
|
+
function intentProblem(key, intent) {
|
|
9819
|
+
const atomic = (value) => typeof value === "string" && /^\d+$/.test(value);
|
|
9820
|
+
if (!isPlainObject(intent)) return "not an object";
|
|
9821
|
+
if (intent.key !== key || !key) return "key does not match its entry";
|
|
9822
|
+
if (typeof intent.channelId !== "string" || !intent.channelId) return "channelId missing";
|
|
9823
|
+
if (!isPlainObject(intent.channelConfig)) return "channelConfig missing";
|
|
9824
|
+
if (intent.kind !== "open" && intent.kind !== "top-up") return "kind is neither open nor top-up";
|
|
9825
|
+
if (!atomic(intent.cumulative)) return "cumulative is not an atomic amount";
|
|
9826
|
+
if (!atomic(intent.expectDeposit)) return "expectDeposit is not an atomic amount";
|
|
9827
|
+
if (intent.knownDeposit !== void 0 && !atomic(intent.knownDeposit)) return "knownDeposit is not an atomic amount";
|
|
9828
|
+
if (intent.requestId !== void 0 && typeof intent.requestId !== "string") return "requestId is not a string";
|
|
9829
|
+
if (intent.anchorHeight !== void 0 && (!Number.isSafeInteger(intent.anchorHeight) || intent.anchorHeight < 0)) {
|
|
9830
|
+
return "anchorHeight is not a block height";
|
|
9831
|
+
}
|
|
9832
|
+
if (typeof intent.at !== "number" || !Number.isFinite(intent.at)) return "at is not a time";
|
|
9833
|
+
return void 0;
|
|
9834
|
+
}
|
|
9835
|
+
function fsyncDirectory(dir) {
|
|
9836
|
+
let fd;
|
|
9837
|
+
try {
|
|
9838
|
+
fd = fs6.openSync(dir, "r");
|
|
9839
|
+
fs6.fsyncSync(fd);
|
|
9840
|
+
} catch (err) {
|
|
9841
|
+
const code = err.code;
|
|
9842
|
+
if (!["EISDIR", "EPERM", "EACCES", "EINVAL", "ENOTSUP", "EBADF"].includes(code ?? "")) throw err;
|
|
9843
|
+
} finally {
|
|
9844
|
+
if (fd !== void 0) fs6.closeSync(fd);
|
|
9845
|
+
}
|
|
9846
|
+
}
|
|
9847
|
+
var FileIntentJournal = class _FileIntentJournal {
|
|
9848
|
+
constructor(file) {
|
|
9849
|
+
this.file = file;
|
|
9850
|
+
}
|
|
9851
|
+
file;
|
|
9852
|
+
durable = true;
|
|
9853
|
+
/** The journal file for a channel store file: its full path plus `.deposit-intents`, one per store. */
|
|
9854
|
+
static beside(channelFile) {
|
|
9855
|
+
return new _FileIntentJournal(`${channelFile}.deposit-intents`);
|
|
9856
|
+
}
|
|
9857
|
+
/**
|
|
9858
|
+
* Where earlier builds kept a store's journal: the store's name without
|
|
9859
|
+
* `.json`, plus `.deposit-intents.json`. Two stores could share it
|
|
9860
|
+
* (`/x/channels` and `/x/channels.json`), so its intents are moved by payer
|
|
9861
|
+
* ({@link adoptLegacy}). No current journal path ever ends in `.json`, so
|
|
9862
|
+
* this never names one.
|
|
9863
|
+
*/
|
|
9864
|
+
static legacyBeside(channelFile) {
|
|
9865
|
+
const base = path5.basename(channelFile).replace(/\.json$/i, "");
|
|
9866
|
+
return path5.join(path5.dirname(channelFile), `${base}.deposit-intents.json`);
|
|
9867
|
+
}
|
|
9868
|
+
/**
|
|
9869
|
+
* Move `payer`'s intents from a journal an earlier build wrote at
|
|
9870
|
+
* `legacyFile` into this one, before this one is used: written here first
|
|
9871
|
+
* (durably), then removed there (the file goes once empty). Intents of other
|
|
9872
|
+
* payers are left where they are, for the store they belong to. An intent
|
|
9873
|
+
* already here is kept (a migration interrupted between the two writes).
|
|
9874
|
+
*
|
|
9875
|
+
* @throws when the old journal cannot be read or is not valid, or holds an
|
|
9876
|
+
* intent naming no payer: the wallet then keeps batch off, as for an
|
|
9877
|
+
* unreadable journal, and nothing is dropped.
|
|
9878
|
+
*/
|
|
9879
|
+
adoptLegacy(legacyFile, payer) {
|
|
9880
|
+
if (legacyFile === this.file) return;
|
|
9881
|
+
const legacy = new _FileIntentJournal(legacyFile);
|
|
9882
|
+
const found = legacy.read();
|
|
9883
|
+
const mine = [];
|
|
9884
|
+
const others = {};
|
|
9885
|
+
for (const [key, intent] of Object.entries(found)) {
|
|
9886
|
+
const owner = intent.channelConfig.payer;
|
|
9887
|
+
if (typeof owner !== "string") {
|
|
9888
|
+
throw new Error(`deposit-intent journal ${legacyFile} holds an intent naming no payer (${JSON.stringify(key)}); move it by hand`);
|
|
9889
|
+
}
|
|
9890
|
+
if (owner === payer) mine.push(intent);
|
|
9891
|
+
else others[key] = intent;
|
|
9892
|
+
}
|
|
9893
|
+
if (mine.length === 0) return;
|
|
9894
|
+
const intents = this.read();
|
|
9895
|
+
for (const intent of mine) intents[intent.key] ??= intent;
|
|
9896
|
+
this.write(intents);
|
|
9897
|
+
legacy.write(others);
|
|
9898
|
+
}
|
|
9899
|
+
/**
|
|
9900
|
+
* The journaled intents. Only a missing file is an empty journal: anything
|
|
9901
|
+
* else that is not a version-1 journal of well-formed intents throws, so a
|
|
9902
|
+
* deposit in doubt is never lost to a damaged or unknown file (the wallet
|
|
9903
|
+
* then keeps batch off, `deposit_journal_unreadable`).
|
|
9904
|
+
*/
|
|
9905
|
+
read() {
|
|
9906
|
+
let text;
|
|
9907
|
+
try {
|
|
9908
|
+
text = fs6.readFileSync(this.file, "utf8");
|
|
9909
|
+
} catch (err) {
|
|
9910
|
+
if (err.code === "ENOENT") return {};
|
|
9911
|
+
throw err;
|
|
9912
|
+
}
|
|
9913
|
+
const invalid = (why) => new Error(`deposit-intent journal ${this.file} is not valid (${why})`);
|
|
9914
|
+
let parsed;
|
|
9915
|
+
try {
|
|
9916
|
+
parsed = JSON.parse(text);
|
|
9917
|
+
} catch (err) {
|
|
9918
|
+
throw invalid(err instanceof Error ? err.message : String(err));
|
|
9919
|
+
}
|
|
9920
|
+
if (!isPlainObject(parsed)) throw invalid("not a JSON object");
|
|
9921
|
+
if (parsed.version !== 1) throw invalid(`unsupported version ${JSON.stringify(parsed.version)}`);
|
|
9922
|
+
const { intents } = parsed;
|
|
9923
|
+
if (!isPlainObject(intents)) throw invalid("intents is not an object");
|
|
9924
|
+
for (const [key, intent] of Object.entries(intents)) {
|
|
9925
|
+
const why = intentProblem(key, intent);
|
|
9926
|
+
if (why) throw invalid(`intent ${JSON.stringify(key)}: ${why}`);
|
|
9927
|
+
}
|
|
9928
|
+
return intents;
|
|
9929
|
+
}
|
|
9930
|
+
write(intents) {
|
|
9931
|
+
const dir = path5.dirname(this.file);
|
|
9932
|
+
fs6.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
9933
|
+
if (Object.keys(intents).length === 0) {
|
|
9934
|
+
try {
|
|
9935
|
+
fs6.unlinkSync(this.file);
|
|
9936
|
+
} catch (err) {
|
|
9937
|
+
if (err.code !== "ENOENT") throw err;
|
|
9938
|
+
return;
|
|
9939
|
+
}
|
|
9940
|
+
fsyncDirectory(dir);
|
|
9941
|
+
return;
|
|
9942
|
+
}
|
|
9943
|
+
const tmp = `${this.file}.${process.pid}.${(0, import_crypto.randomUUID)()}.tmp`;
|
|
9944
|
+
const fd = fs6.openSync(tmp, "w", 384);
|
|
9945
|
+
try {
|
|
9946
|
+
fs6.writeSync(fd, JSON.stringify({ version: 1, intents }, null, 2));
|
|
9947
|
+
fs6.fsyncSync(fd);
|
|
9948
|
+
} finally {
|
|
9949
|
+
fs6.closeSync(fd);
|
|
9950
|
+
}
|
|
9951
|
+
fs6.renameSync(tmp, this.file);
|
|
9952
|
+
fsyncDirectory(dir);
|
|
9953
|
+
}
|
|
9954
|
+
list() {
|
|
9955
|
+
return Object.values(this.read());
|
|
9956
|
+
}
|
|
9957
|
+
put(intent) {
|
|
9958
|
+
const intents = this.read();
|
|
9959
|
+
intents[intent.key] = intent;
|
|
9960
|
+
this.write(intents);
|
|
9961
|
+
}
|
|
9962
|
+
remove(key) {
|
|
9963
|
+
const intents = this.read();
|
|
9964
|
+
if (!(key in intents)) return;
|
|
9965
|
+
delete intents[key];
|
|
9966
|
+
this.write(intents);
|
|
9967
|
+
}
|
|
9968
|
+
};
|
|
9969
|
+
var MemoryIntentJournal = class {
|
|
9970
|
+
durable = false;
|
|
9971
|
+
intents = /* @__PURE__ */ new Map();
|
|
9972
|
+
list() {
|
|
9973
|
+
return [...this.intents.values()];
|
|
9974
|
+
}
|
|
9975
|
+
put(intent) {
|
|
9976
|
+
this.intents.set(intent.key, { ...intent });
|
|
9977
|
+
}
|
|
9978
|
+
remove(key) {
|
|
9979
|
+
this.intents.delete(key);
|
|
9980
|
+
}
|
|
9981
|
+
};
|
|
9636
9982
|
function processAlive(pid) {
|
|
9637
9983
|
try {
|
|
9638
9984
|
process.kill(pid, 0);
|
|
@@ -9641,71 +9987,438 @@ function processAlive(pid) {
|
|
|
9641
9987
|
return err.code === "EPERM";
|
|
9642
9988
|
}
|
|
9643
9989
|
}
|
|
9644
|
-
var
|
|
9645
|
-
var
|
|
9990
|
+
var BATCH_REGISTRY = /* @__PURE__ */ Symbol.for("@blockrun/llm/batch-registry/v1");
|
|
9991
|
+
var registryHost = globalThis;
|
|
9992
|
+
var registry2 = registryHost[BATCH_REGISTRY] ??= {
|
|
9993
|
+
wallets: /* @__PURE__ */ new Map(),
|
|
9994
|
+
locks: /* @__PURE__ */ new Map(),
|
|
9995
|
+
exitHook: false
|
|
9996
|
+
};
|
|
9997
|
+
var lockIds = registry2.lockIds ??= /* @__PURE__ */ new Map();
|
|
9998
|
+
function fileIdentity(file) {
|
|
9999
|
+
try {
|
|
10000
|
+
const stat = fs6.statSync(file);
|
|
10001
|
+
return `${stat.dev}:${stat.ino}:${stat.mtimeMs}`;
|
|
10002
|
+
} catch {
|
|
10003
|
+
return void 0;
|
|
10004
|
+
}
|
|
10005
|
+
}
|
|
10006
|
+
function lockPredatesProcess(lock) {
|
|
10007
|
+
try {
|
|
10008
|
+
return fs6.statSync(lock).mtimeMs < performance.timeOrigin;
|
|
10009
|
+
} catch {
|
|
10010
|
+
return false;
|
|
10011
|
+
}
|
|
10012
|
+
}
|
|
10013
|
+
function holdsChannelFile(file) {
|
|
10014
|
+
const token = registry2.locks.get(file);
|
|
10015
|
+
if (!token) return false;
|
|
10016
|
+
const lock = `${file}.lock`;
|
|
10017
|
+
if (!ownsLock(lock, token)) return false;
|
|
10018
|
+
const id = lockIds.get(file);
|
|
10019
|
+
return id === void 0 || fileIdentity(lock) === id;
|
|
10020
|
+
}
|
|
10021
|
+
function lockTokenFile(lock) {
|
|
10022
|
+
return `${lock}.owner`;
|
|
10023
|
+
}
|
|
10024
|
+
function readLockOwner(lock) {
|
|
10025
|
+
let raw;
|
|
10026
|
+
try {
|
|
10027
|
+
raw = fs6.readFileSync(lock, "utf8");
|
|
10028
|
+
} catch (err) {
|
|
10029
|
+
if (err.code === "ENOENT") return void 0;
|
|
10030
|
+
throw err;
|
|
10031
|
+
}
|
|
10032
|
+
let pid = Number(raw.trim());
|
|
10033
|
+
if (!Number.isInteger(pid)) {
|
|
10034
|
+
try {
|
|
10035
|
+
const parsed = JSON.parse(raw);
|
|
10036
|
+
pid = parsed && typeof parsed === "object" && Number.isInteger(parsed.pid) ? parsed.pid : 0;
|
|
10037
|
+
} catch {
|
|
10038
|
+
pid = 0;
|
|
10039
|
+
}
|
|
10040
|
+
}
|
|
10041
|
+
return { pid, raw };
|
|
10042
|
+
}
|
|
10043
|
+
function readLockToken(lock) {
|
|
10044
|
+
try {
|
|
10045
|
+
const parsed = JSON.parse(fs6.readFileSync(lockTokenFile(lock), "utf8"));
|
|
10046
|
+
return parsed && typeof parsed === "object" && typeof parsed.token === "string" ? parsed.token : void 0;
|
|
10047
|
+
} catch {
|
|
10048
|
+
return void 0;
|
|
10049
|
+
}
|
|
10050
|
+
}
|
|
10051
|
+
function ownsLock(lock, token) {
|
|
10052
|
+
return readLockOwner(lock)?.pid === process.pid && readLockToken(lock) === token;
|
|
10053
|
+
}
|
|
10054
|
+
function releaseHeldLocks() {
|
|
10055
|
+
for (const [file, token] of registry2.locks) {
|
|
10056
|
+
const lock = `${file}.lock`;
|
|
10057
|
+
try {
|
|
10058
|
+
if (!ownsLock(lock, token)) continue;
|
|
10059
|
+
fs6.unlinkSync(lockTokenFile(lock));
|
|
10060
|
+
fs6.unlinkSync(lock);
|
|
10061
|
+
} catch {
|
|
10062
|
+
}
|
|
10063
|
+
}
|
|
10064
|
+
}
|
|
9646
10065
|
function lockChannelFile(file) {
|
|
9647
|
-
if (
|
|
10066
|
+
if (registry2.locks.has(file)) return void 0;
|
|
9648
10067
|
const lock = `${file}.lock`;
|
|
9649
10068
|
fs6.mkdirSync(path5.dirname(lock), { recursive: true, mode: 448 });
|
|
10069
|
+
const token = (0, import_crypto.randomUUID)();
|
|
9650
10070
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
10071
|
+
const tmp = `${lock}.${process.pid}.${token}.tmp`;
|
|
9651
10072
|
try {
|
|
9652
|
-
fs6.writeFileSync(
|
|
9653
|
-
|
|
9654
|
-
|
|
9655
|
-
|
|
9656
|
-
|
|
9657
|
-
|
|
9658
|
-
|
|
9659
|
-
|
|
9660
|
-
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
|
|
10073
|
+
fs6.writeFileSync(tmp, String(process.pid), { mode: 384 });
|
|
10074
|
+
try {
|
|
10075
|
+
fs6.linkSync(tmp, lock);
|
|
10076
|
+
} catch (err) {
|
|
10077
|
+
const code = err.code;
|
|
10078
|
+
if (code !== "EPERM" && code !== "ENOTSUP" && code !== "EXDEV" && code !== "EOPNOTSUPP") throw err;
|
|
10079
|
+
fs6.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
|
|
10080
|
+
}
|
|
10081
|
+
try {
|
|
10082
|
+
writeLockToken(lock, token);
|
|
10083
|
+
} catch (err) {
|
|
10084
|
+
try {
|
|
10085
|
+
fs6.unlinkSync(lock);
|
|
10086
|
+
} catch {
|
|
10087
|
+
}
|
|
10088
|
+
throw err;
|
|
10089
|
+
}
|
|
10090
|
+
registry2.locks.set(file, token);
|
|
10091
|
+
const id = fileIdentity(lock);
|
|
10092
|
+
if (id) lockIds.set(file, id);
|
|
10093
|
+
else lockIds.delete(file);
|
|
10094
|
+
if (!registry2.exitHook) {
|
|
10095
|
+
registry2.exitHook = true;
|
|
10096
|
+
process.once("exit", releaseHeldLocks);
|
|
9664
10097
|
}
|
|
9665
10098
|
return void 0;
|
|
9666
10099
|
} catch (err) {
|
|
9667
10100
|
if (err.code !== "EEXIST") throw err;
|
|
10101
|
+
} finally {
|
|
10102
|
+
try {
|
|
10103
|
+
fs6.unlinkSync(tmp);
|
|
10104
|
+
} catch {
|
|
10105
|
+
}
|
|
10106
|
+
}
|
|
10107
|
+
const owner = readLockOwner(lock);
|
|
10108
|
+
if (!owner) continue;
|
|
10109
|
+
if (owner.pid === process.pid) {
|
|
10110
|
+
if (!lockPredatesProcess(lock)) return process.pid;
|
|
10111
|
+
} else if (owner.pid > 0 && processAlive(owner.pid)) {
|
|
10112
|
+
return owner.pid;
|
|
9668
10113
|
}
|
|
9669
|
-
const owner = Number(fs6.readFileSync(lock, "utf8").trim());
|
|
9670
|
-
if (Number.isInteger(owner) && owner > 0 && owner !== process.pid && processAlive(owner)) return owner;
|
|
9671
10114
|
try {
|
|
9672
|
-
fs6.unlinkSync(lock);
|
|
10115
|
+
if (readLockOwner(lock)?.raw === owner.raw) fs6.unlinkSync(lock);
|
|
9673
10116
|
} catch {
|
|
9674
10117
|
}
|
|
9675
10118
|
}
|
|
9676
10119
|
return -1;
|
|
9677
10120
|
}
|
|
10121
|
+
function writeLockToken(lock, token) {
|
|
10122
|
+
const sidecar = lockTokenFile(lock);
|
|
10123
|
+
const tmp = `${sidecar}.${process.pid}.${token}.tmp`;
|
|
10124
|
+
try {
|
|
10125
|
+
fs6.writeFileSync(tmp, JSON.stringify({ pid: process.pid, token }), { mode: 384 });
|
|
10126
|
+
fs6.renameSync(tmp, sidecar);
|
|
10127
|
+
} finally {
|
|
10128
|
+
try {
|
|
10129
|
+
fs6.unlinkSync(tmp);
|
|
10130
|
+
} catch {
|
|
10131
|
+
}
|
|
10132
|
+
}
|
|
10133
|
+
}
|
|
9678
10134
|
function pendingDeposit(pending) {
|
|
9679
10135
|
const entries = Array.isArray(pending) ? pending : [pending];
|
|
9680
10136
|
return entries.some(
|
|
9681
10137
|
(entry) => entry?.payment?.payload?.type === "deposit"
|
|
9682
10138
|
);
|
|
9683
10139
|
}
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
9688
|
-
|
|
9689
|
-
|
|
9690
|
-
|
|
9691
|
-
|
|
9692
|
-
|
|
10140
|
+
var ChannelResyncRequiredError = class extends Error {
|
|
10141
|
+
constructor(channelId) {
|
|
10142
|
+
super(`batch-settlement channel ${channelId ?? "<unknown>"} needs a chain re-read before it is used`);
|
|
10143
|
+
this.channelId = channelId;
|
|
10144
|
+
this.name = "ChannelResyncRequiredError";
|
|
10145
|
+
}
|
|
10146
|
+
channelId;
|
|
10147
|
+
};
|
|
10148
|
+
function isResyncRequired(err) {
|
|
10149
|
+
return err instanceof Error && err.name === "ChannelResyncRequiredError";
|
|
10150
|
+
}
|
|
10151
|
+
function parseAtomic(value) {
|
|
10152
|
+
return typeof value === "string" && /^\d+$/.test(value) ? BigInt(value) : 0n;
|
|
10153
|
+
}
|
|
10154
|
+
function confirmedCumulative(record, channelId) {
|
|
10155
|
+
if (!record || record.channelId !== channelId) return 0n;
|
|
10156
|
+
return record.pending === void 0 || record.hasConfirmedState ? parseAtomic(record.chargedCumulativeAmount) : 0n;
|
|
10157
|
+
}
|
|
10158
|
+
var ChannelBook = class {
|
|
10159
|
+
constructor(base, onUnverified = () => {
|
|
10160
|
+
}) {
|
|
10161
|
+
this.base = base;
|
|
10162
|
+
this.onUnverified = onUnverified;
|
|
10163
|
+
}
|
|
10164
|
+
base;
|
|
10165
|
+
onUnverified;
|
|
10166
|
+
seen = /* @__PURE__ */ new Map();
|
|
10167
|
+
async get(key) {
|
|
10168
|
+
const record = await this.base.get(key);
|
|
10169
|
+
if (record) this.seen.set(key, record);
|
|
10170
|
+
if (!record?.pending || record.channelConfig?.voucherSigner !== "server") return record;
|
|
10171
|
+
if (record.hasConfirmedState && !pendingDeposit(record.pending)) {
|
|
9693
10172
|
const { pending: _pending, hasConfirmedState: _confirmed, ...confirmed } = record;
|
|
9694
|
-
await
|
|
10173
|
+
await this.set(key, confirmed);
|
|
9695
10174
|
return confirmed;
|
|
9696
|
-
}
|
|
9697
|
-
|
|
9698
|
-
|
|
10175
|
+
}
|
|
10176
|
+
if (typeof record.channelId === "string") {
|
|
10177
|
+
const pendingEntries = Array.isArray(record.pending) ? record.pending : [record.pending];
|
|
10178
|
+
const inDoubt = pendingEntries.find((entry) => entry?.payment?.payload?.type === "deposit");
|
|
10179
|
+
this.onUnverified({
|
|
10180
|
+
key,
|
|
10181
|
+
channelId: record.channelId,
|
|
10182
|
+
channelConfig: record.channelConfig,
|
|
10183
|
+
cumulative: record.hasConfirmedState ? parseAtomic(record.chargedCumulativeAmount) : 0n,
|
|
10184
|
+
...record.hasConfirmedState ? { knownDeposit: parseAtomic(record.deposit) } : {},
|
|
10185
|
+
...inDoubt ? { expectDeposit: parseAtomic(inDoubt.deposit) } : {},
|
|
10186
|
+
reason: "orphaned_deposit"
|
|
10187
|
+
});
|
|
10188
|
+
}
|
|
10189
|
+
throw new ChannelResyncRequiredError(record.channelId);
|
|
10190
|
+
}
|
|
10191
|
+
/**
|
|
10192
|
+
* Store a record, and only once it is stored remember it as the last one
|
|
10193
|
+
* seen. If storing fails (say, while the scheme saves a reconciled
|
|
10194
|
+
* receipt), the scheme reports the receipt unreconciled, and the SDK must
|
|
10195
|
+
* still see the state from before it: `distrust()` adds that call's charge
|
|
10196
|
+
* to it, and a remembered post-receipt cumulative would count it twice.
|
|
10197
|
+
*/
|
|
10198
|
+
async set(key, record) {
|
|
10199
|
+
await this.base.set(key, record);
|
|
10200
|
+
this.seen.set(key, record);
|
|
10201
|
+
}
|
|
10202
|
+
async delete(key) {
|
|
10203
|
+
await this.base.delete(key);
|
|
10204
|
+
}
|
|
10205
|
+
/** The keys of every stored channel. */
|
|
10206
|
+
async keys() {
|
|
10207
|
+
return await this.base.keys?.() ?? [];
|
|
10208
|
+
}
|
|
10209
|
+
/** The last record seen for a channel, even one the scheme has deleted since. */
|
|
10210
|
+
find(channelId) {
|
|
10211
|
+
for (const [key, record] of this.seen) {
|
|
10212
|
+
if (record.channelId === channelId) return { key, record };
|
|
10213
|
+
}
|
|
10214
|
+
return void 0;
|
|
10215
|
+
}
|
|
10216
|
+
clear() {
|
|
10217
|
+
this.seen.clear();
|
|
10218
|
+
this.base.clear?.();
|
|
10219
|
+
}
|
|
10220
|
+
/** Forget one channel: its stored record and the last record seen for it. An emptied store is removed. */
|
|
10221
|
+
async forget(key) {
|
|
10222
|
+
this.seen.delete(key);
|
|
10223
|
+
await this.base.delete(key);
|
|
10224
|
+
if ((await this.keys()).length === 0) this.clear();
|
|
10225
|
+
}
|
|
10226
|
+
};
|
|
10227
|
+
var PAYMENT_CHANNELS_PROGRAM_ID = "CHNLxYvVA28MJP9PrFuDXccuoGXAx7jBacfLEkahyGsX";
|
|
10228
|
+
var CHANNEL = {
|
|
10229
|
+
size: 256,
|
|
10230
|
+
rentPayer: 216,
|
|
10231
|
+
discriminator: 1,
|
|
10232
|
+
status: 3,
|
|
10233
|
+
deposit: 12,
|
|
10234
|
+
settled: 20,
|
|
10235
|
+
closureStartedAt: 36,
|
|
10236
|
+
payer: 88,
|
|
10237
|
+
authorizedSigner: 152,
|
|
10238
|
+
mint: 184
|
|
10239
|
+
};
|
|
10240
|
+
function decodeChannelAccount(owner, data) {
|
|
10241
|
+
if (owner !== PAYMENT_CHANNELS_PROGRAM_ID || data.length < CHANNEL.size || data[0] !== CHANNEL.discriminator) {
|
|
10242
|
+
return void 0;
|
|
10243
|
+
}
|
|
10244
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
10245
|
+
const key = (offset) => import_bs58.default.encode(data.subarray(offset, offset + 32));
|
|
10246
|
+
return {
|
|
10247
|
+
deposit: view.getBigUint64(CHANNEL.deposit, true),
|
|
10248
|
+
settled: view.getBigUint64(CHANNEL.settled, true),
|
|
10249
|
+
open: data[CHANNEL.status] === 0 && view.getBigInt64(CHANNEL.closureStartedAt, true) === 0n,
|
|
10250
|
+
payer: key(CHANNEL.payer),
|
|
10251
|
+
authorizedSigner: key(CHANNEL.authorizedSigner),
|
|
10252
|
+
mint: key(CHANNEL.mint)
|
|
10253
|
+
};
|
|
10254
|
+
}
|
|
10255
|
+
var ChannelUnreadableError = class extends Error {
|
|
10256
|
+
constructor(channelId, why) {
|
|
10257
|
+
super(`account ${channelId} is on chain but is not a readable payment channel (${why})`);
|
|
10258
|
+
this.name = "ChannelUnreadableError";
|
|
10259
|
+
}
|
|
10260
|
+
};
|
|
10261
|
+
var ChannelLayoutError = class extends Error {
|
|
10262
|
+
constructor(message, options) {
|
|
10263
|
+
super(message, options);
|
|
10264
|
+
this.name = "ChannelLayoutError";
|
|
10265
|
+
}
|
|
10266
|
+
};
|
|
10267
|
+
async function readChannelAccount(rpcUrl, channelId, rpcHeaders, options = {}) {
|
|
10268
|
+
await checkChannelLayout();
|
|
10269
|
+
const config = { encoding: "base64", commitment: "finalized" };
|
|
10270
|
+
if (options.minContextSlot !== void 0) config.minContextSlot = options.minContextSlot;
|
|
10271
|
+
const response = await fetch(rpcUrl, {
|
|
10272
|
+
method: "POST",
|
|
10273
|
+
headers: { ...rpcHeaders, "Content-Type": "application/json" },
|
|
10274
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getAccountInfo", params: [channelId, config] }),
|
|
10275
|
+
signal: AbortSignal.timeout(15e3),
|
|
10276
|
+
// rpcHeaders may carry an API key: never let a redirect take it elsewhere.
|
|
10277
|
+
redirect: "error"
|
|
10278
|
+
});
|
|
10279
|
+
if (!response.ok) throw new Error(`getAccountInfo answered HTTP ${response.status}`);
|
|
10280
|
+
const body = await response.json();
|
|
10281
|
+
if (body.error) throw new Error(`getAccountInfo failed: ${body.error.message ?? JSON.stringify(body.error)}`);
|
|
10282
|
+
if (!body.result || typeof body.result !== "object" || !("value" in body.result)) {
|
|
10283
|
+
throw new Error("getAccountInfo returned no result");
|
|
10284
|
+
}
|
|
10285
|
+
const value = body.result.value;
|
|
10286
|
+
if (value === null) return void 0;
|
|
10287
|
+
if (!value || typeof value !== "object") throw new ChannelUnreadableError(channelId, "malformed account value");
|
|
10288
|
+
if (!Array.isArray(value.data) || typeof value.data[0] !== "string" || value.data[1] !== "base64") {
|
|
10289
|
+
throw new ChannelUnreadableError(channelId, "unsupported data encoding");
|
|
10290
|
+
}
|
|
10291
|
+
const channel = decodeChannelAccount(value.owner, Buffer.from(value.data[0], "base64"));
|
|
10292
|
+
if (!channel) throw new ChannelUnreadableError(channelId, "wrong owner or layout");
|
|
10293
|
+
return channel;
|
|
10294
|
+
}
|
|
10295
|
+
async function rpcRequest(rpcUrl, method, params, rpcHeaders) {
|
|
10296
|
+
const response = await fetch(rpcUrl, {
|
|
10297
|
+
method: "POST",
|
|
10298
|
+
headers: { ...rpcHeaders, "Content-Type": "application/json" },
|
|
10299
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
|
|
10300
|
+
signal: AbortSignal.timeout(15e3),
|
|
10301
|
+
// rpcHeaders may carry an API key: never let a redirect take it elsewhere.
|
|
10302
|
+
redirect: "error"
|
|
10303
|
+
});
|
|
10304
|
+
if (!response.ok) throw new Error(`${method} answered HTTP ${response.status}`);
|
|
10305
|
+
const body = await response.json();
|
|
10306
|
+
if (body.error) throw new Error(`${method} failed: ${body.error.message ?? JSON.stringify(body.error)}`);
|
|
10307
|
+
if (body.result === void 0 || body.result === null) throw new Error(`${method} returned no result`);
|
|
10308
|
+
return body.result;
|
|
10309
|
+
}
|
|
10310
|
+
function rpcInteger(value, what) {
|
|
10311
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error(`${what} is not a block height or slot`);
|
|
10312
|
+
return value;
|
|
10313
|
+
}
|
|
10314
|
+
var layoutChecked;
|
|
10315
|
+
function checkChannelLayout() {
|
|
10316
|
+
layoutChecked ??= (async () => {
|
|
10317
|
+
const svm = await load("@x402/svm", () => import("@x402/svm")).catch((err) => {
|
|
10318
|
+
throw new ChannelLayoutError(
|
|
10319
|
+
`cannot check the payment-channel account layout: ${err instanceof Error ? err.message : String(err)}`,
|
|
10320
|
+
{ cause: err }
|
|
10321
|
+
);
|
|
10322
|
+
});
|
|
10323
|
+
if (svm.CHANNEL_ACCOUNT_SIZE !== BigInt(CHANNEL.size) || svm.CHANNEL_RENT_PAYER_OFFSET !== BigInt(CHANNEL.rentPayer)) {
|
|
10324
|
+
throw new ChannelLayoutError("@x402/svm changed the payment-channel account layout; this SDK cannot read channels from it");
|
|
10325
|
+
}
|
|
10326
|
+
})();
|
|
10327
|
+
layoutChecked.catch(() => {
|
|
10328
|
+
layoutChecked = void 0;
|
|
10329
|
+
});
|
|
10330
|
+
return layoutChecked;
|
|
10331
|
+
}
|
|
10332
|
+
var LANDING_MARGIN_BLOCKS = 300;
|
|
10333
|
+
function maxOf(a, b) {
|
|
10334
|
+
if (a === void 0) return b;
|
|
10335
|
+
if (b === void 0) return a;
|
|
10336
|
+
return a > b ? a : b;
|
|
10337
|
+
}
|
|
10338
|
+
var ResyncError = class extends Error {
|
|
10339
|
+
constructor(reason, message) {
|
|
10340
|
+
super(message);
|
|
10341
|
+
this.reason = reason;
|
|
10342
|
+
this.name = "ResyncError";
|
|
10343
|
+
}
|
|
10344
|
+
reason;
|
|
10345
|
+
};
|
|
10346
|
+
function payloadChannelId(payload) {
|
|
10347
|
+
return payload.payload?.authorization?.channelId ?? payload.payload?.voucher?.channelId;
|
|
10348
|
+
}
|
|
10349
|
+
function clientSignedBatch(accept) {
|
|
10350
|
+
return accept.scheme === BATCH_SCHEME && accept.extra?.voucherSigner !== "server";
|
|
10351
|
+
}
|
|
10352
|
+
function batchUnavailable(paymentRequired) {
|
|
10353
|
+
const batch = (paymentRequired.accepts ?? []).filter((accept) => accept.scheme === BATCH_SCHEME);
|
|
10354
|
+
if (batch.length === 0) return { reason: "not_offered" };
|
|
10355
|
+
if (!batch.every(clientSignedBatch)) return void 0;
|
|
10356
|
+
return {
|
|
10357
|
+
reason: "client_signed_not_supported",
|
|
10358
|
+
detail: "the 402 offers only client-signed batch-settlement accepts; this SDK pays batch only into server-signed (operator) channels"
|
|
9699
10359
|
};
|
|
9700
10360
|
}
|
|
9701
|
-
function
|
|
9702
|
-
|
|
10361
|
+
function withoutClientSignedBatch(paymentRequired) {
|
|
10362
|
+
const accepts = paymentRequired.accepts ?? [];
|
|
10363
|
+
return accepts.some(clientSignedBatch) ? { ...paymentRequired, accepts: accepts.filter((accept) => !clientSignedBatch(accept)) } : paymentRequired;
|
|
10364
|
+
}
|
|
10365
|
+
function trustedBatchAccepts(paymentRequired, operators) {
|
|
10366
|
+
return (paymentRequired.accepts ?? []).filter((candidate) => {
|
|
10367
|
+
const extra = candidate.extra;
|
|
10368
|
+
return candidate.scheme === BATCH_SCHEME && extra?.voucherSigner === "server" && operators.includes(extra.operator);
|
|
10369
|
+
});
|
|
10370
|
+
}
|
|
10371
|
+
function trustedBatchAccept(paymentRequired, operators) {
|
|
10372
|
+
return trustedBatchAccepts(paymentRequired, operators)[0];
|
|
9703
10373
|
}
|
|
9704
10374
|
function atomicToUsd(amount) {
|
|
9705
|
-
return amount
|
|
10375
|
+
return typeof amount === "string" && /^\d+$/.test(amount) ? Number(amount) / 1e6 : void 0;
|
|
9706
10376
|
}
|
|
9707
|
-
|
|
9708
|
-
|
|
10377
|
+
function chargedUsd(getReceipt) {
|
|
10378
|
+
let receipt;
|
|
10379
|
+
try {
|
|
10380
|
+
receipt = getReceipt();
|
|
10381
|
+
} catch {
|
|
10382
|
+
return 0;
|
|
10383
|
+
}
|
|
10384
|
+
return atomicToUsd(receipt?.extra?.chargedAmount) ?? atomicToUsd(receipt?.amount) ?? 0;
|
|
10385
|
+
}
|
|
10386
|
+
function rebuiltCharge(settled, payload) {
|
|
10387
|
+
if (settled?.success !== true) return 0n;
|
|
10388
|
+
const charged = parseAtomic(typeof settled.extra?.chargedAmount === "string" ? settled.extra.chargedAmount : settled.amount);
|
|
10389
|
+
const ceiling = parseAtomic(payload.accepted?.amount);
|
|
10390
|
+
return charged > ceiling ? ceiling : charged;
|
|
10391
|
+
}
|
|
10392
|
+
function confirmedState(record) {
|
|
10393
|
+
if (!record || !(record.pending === void 0 || record.hasConfirmedState)) return void 0;
|
|
10394
|
+
return { cumulative: parseAtomic(record.chargedCumulativeAmount), deposit: parseAtomic(record.deposit) };
|
|
10395
|
+
}
|
|
10396
|
+
function recordAdvanced(before, after, payload, receipt) {
|
|
10397
|
+
const deposited = payload.payload?.type === "deposit" ? parseAtomic(payload.payload.deposit?.amount) : 0n;
|
|
10398
|
+
const charged = rebuiltCharge(receipt, payload);
|
|
10399
|
+
if (deposited === 0n && charged === 0n) return true;
|
|
10400
|
+
if (!after) return false;
|
|
10401
|
+
const prior = before ?? { cumulative: 0n, deposit: 0n };
|
|
10402
|
+
return after.deposit >= prior.deposit + deposited && after.cumulative >= prior.cumulative + charged;
|
|
10403
|
+
}
|
|
10404
|
+
var BATCH_REFUSALS = /* @__PURE__ */ new Set([
|
|
10405
|
+
"batch_payer_not_allowed",
|
|
10406
|
+
"batch_payer_not_admitted",
|
|
10407
|
+
"batch_admission_paused",
|
|
10408
|
+
"batch_server_signed_only",
|
|
10409
|
+
"batch_unavailable",
|
|
10410
|
+
"batch_channel_limit",
|
|
10411
|
+
"PAYMENT_VERIFICATION_UNAVAILABLE"
|
|
10412
|
+
]);
|
|
10413
|
+
var PRE_REQUEST_REFUSALS = /* @__PURE__ */ new Set([
|
|
10414
|
+
"batch_payer_not_allowed",
|
|
10415
|
+
"batch_payer_not_admitted",
|
|
10416
|
+
"batch_admission_paused",
|
|
10417
|
+
"batch_server_signed_only",
|
|
10418
|
+
"batch_unavailable",
|
|
10419
|
+
"batch_channel_limit"
|
|
10420
|
+
]);
|
|
10421
|
+
async function batchRefusal(response) {
|
|
9709
10422
|
if (![400, 403, 409, 503].includes(response.status)) return void 0;
|
|
9710
10423
|
let body;
|
|
9711
10424
|
try {
|
|
@@ -9714,20 +10427,254 @@ async function isBatchRefusal(response) {
|
|
|
9714
10427
|
return void 0;
|
|
9715
10428
|
}
|
|
9716
10429
|
const { error, code } = body ?? {};
|
|
9717
|
-
|
|
9718
|
-
|
|
10430
|
+
for (const value of [code, error]) {
|
|
10431
|
+
if (typeof value === "string" && BATCH_REFUSALS.has(value)) return value;
|
|
10432
|
+
}
|
|
10433
|
+
return void 0;
|
|
9719
10434
|
}
|
|
9720
|
-
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
|
|
9724
|
-
|
|
9725
|
-
|
|
9726
|
-
|
|
10435
|
+
async function duplicateAnswer(response) {
|
|
10436
|
+
const codes = [];
|
|
10437
|
+
try {
|
|
10438
|
+
const body = await response.clone().json();
|
|
10439
|
+
if (body && typeof body === "object") codes.push(body.error, body.code, body.reason, body.invalidReason);
|
|
10440
|
+
} catch {
|
|
10441
|
+
}
|
|
10442
|
+
const header = response.headers.get("PAYMENT-REQUIRED");
|
|
10443
|
+
if (header) {
|
|
10444
|
+
try {
|
|
10445
|
+
const challenge = parsePaymentRequired(header);
|
|
10446
|
+
codes.push(challenge.error, challenge.invalidReason);
|
|
10447
|
+
} catch {
|
|
10448
|
+
}
|
|
10449
|
+
}
|
|
10450
|
+
for (const code of codes) {
|
|
10451
|
+
if (typeof code === "string" && /duplicate/i.test(code)) return code;
|
|
10452
|
+
}
|
|
10453
|
+
return void 0;
|
|
10454
|
+
}
|
|
10455
|
+
function cleanRefusalReceipt(receipt) {
|
|
10456
|
+
if (receipt === void 0) return true;
|
|
10457
|
+
return receipt.success === false && !receipt.transaction && receipt.errorReason !== "settlement_pending";
|
|
10458
|
+
}
|
|
10459
|
+
async function afterPaymentError(response) {
|
|
10460
|
+
let body;
|
|
10461
|
+
try {
|
|
10462
|
+
body = await response.clone().json();
|
|
10463
|
+
} catch {
|
|
10464
|
+
body = { error: "Request failed" };
|
|
10465
|
+
}
|
|
10466
|
+
return withDisposition(
|
|
10467
|
+
new APIError(`API error after payment: ${response.status}`, response.status, sanitizeErrorResponse(body)),
|
|
10468
|
+
"paid-or-in-doubt"
|
|
10469
|
+
);
|
|
10470
|
+
}
|
|
10471
|
+
function cancelledReceipt(receipt) {
|
|
10472
|
+
return receipt?.success === false && receipt.errorReason === "batch_cancelled" && !receipt.transaction;
|
|
10473
|
+
}
|
|
10474
|
+
async function cancelledError(response) {
|
|
10475
|
+
let body;
|
|
10476
|
+
try {
|
|
10477
|
+
body = await response.clone().json();
|
|
10478
|
+
} catch {
|
|
10479
|
+
body = { error: "Request failed" };
|
|
10480
|
+
}
|
|
10481
|
+
return withDisposition(new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(body)), "unpaid");
|
|
10482
|
+
}
|
|
10483
|
+
function describeReceipt(receipt) {
|
|
10484
|
+
if (!receipt) return "no receipt";
|
|
10485
|
+
const parts = [`success=${String(receipt.success)}`];
|
|
10486
|
+
if (receipt.errorReason) parts.push(`errorReason=${receipt.errorReason}`);
|
|
10487
|
+
if (receipt.transaction) parts.push(`transaction=${receipt.transaction}`);
|
|
10488
|
+
return `receipt ${parts.join(" ")}`;
|
|
10489
|
+
}
|
|
10490
|
+
var MAX_RETRY_AFTER_MS = 24 * 60 * 60 * 1e3;
|
|
10491
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
10492
|
+
const text = value?.trim();
|
|
10493
|
+
if (!text) return void 0;
|
|
10494
|
+
let ms;
|
|
10495
|
+
if (/^\d+(\.\d+)?$/.test(text)) {
|
|
10496
|
+
ms = Math.round(Number(text) * 1e3);
|
|
10497
|
+
} else {
|
|
10498
|
+
const at = Date.parse(text);
|
|
10499
|
+
if (Number.isNaN(at)) return void 0;
|
|
10500
|
+
ms = Math.max(0, at - now);
|
|
10501
|
+
}
|
|
10502
|
+
return Number.isFinite(ms) && ms <= MAX_RETRY_AFTER_MS ? ms : void 0;
|
|
10503
|
+
}
|
|
10504
|
+
var NOT_BROADCAST_429 = /* @__PURE__ */ new Set([
|
|
10505
|
+
"batch_account_channel_capacity_exhausted",
|
|
10506
|
+
"batch_channel_capacity_exhausted",
|
|
10507
|
+
"batch_deposit_rate_limited"
|
|
10508
|
+
]);
|
|
10509
|
+
function provesNothingBroadcast(receipt) {
|
|
10510
|
+
return receipt?.success === false && !receipt.transaction && typeof receipt.errorReason === "string" && NOT_BROADCAST_429.has(receipt.errorReason);
|
|
10511
|
+
}
|
|
10512
|
+
function backoffDelay(attempt) {
|
|
10513
|
+
const base = 1e3 * 2 ** Math.max(0, attempt - 1);
|
|
10514
|
+
return Math.round(base * (0.75 + Math.random() * 0.5));
|
|
10515
|
+
}
|
|
10516
|
+
async function rateLimitReason(response, settled) {
|
|
10517
|
+
let body;
|
|
10518
|
+
try {
|
|
10519
|
+
body = await response.clone().json();
|
|
10520
|
+
} catch {
|
|
10521
|
+
body = void 0;
|
|
10522
|
+
}
|
|
10523
|
+
const fields = body ?? {};
|
|
10524
|
+
for (const value of [fields.errorReason, fields.error, fields.code, fields.reason, settled?.errorReason]) {
|
|
10525
|
+
if (typeof value === "string" && value) return value;
|
|
10526
|
+
}
|
|
10527
|
+
return void 0;
|
|
10528
|
+
}
|
|
10529
|
+
function creationFailure(err) {
|
|
10530
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
10531
|
+
if (err instanceof Error && err.name === "UntrustedOperatorError") return { reason: "untrusted_operator", detail };
|
|
10532
|
+
if (detail.includes("serverSignedChannelsPolicy maxDeposit")) return { reason: "deposit_over_cap", detail };
|
|
10533
|
+
if (detail.includes("has a pending request")) return { reason: "channel_pending", detail };
|
|
10534
|
+
if (detail.includes("requires the optional peer dependency")) return { reason: "peer_dependency_missing", detail };
|
|
10535
|
+
return { reason: "payment_creation_failed", detail };
|
|
10536
|
+
}
|
|
10537
|
+
function logValue(value) {
|
|
10538
|
+
const text = String(value);
|
|
10539
|
+
return /^[\w.:/@+-]+$/.test(text) ? text : JSON.stringify(text);
|
|
10540
|
+
}
|
|
10541
|
+
function formatEvent(event) {
|
|
10542
|
+
const fields = [
|
|
10543
|
+
["event", event.type],
|
|
10544
|
+
["reason", event.reason],
|
|
10545
|
+
["status", event.status],
|
|
10546
|
+
["errorReason", event.errorReason],
|
|
10547
|
+
["retryAfterMs", event.retryAfterMs],
|
|
10548
|
+
["attempt", event.attempt],
|
|
10549
|
+
["wallet", event.wallet],
|
|
10550
|
+
["next", event.type === "fallback" ? "exact" : event.type === "backoff" ? "retry" : event.type === "unresolved" ? "raise" : void 0],
|
|
10551
|
+
["detail", event.detail]
|
|
10552
|
+
];
|
|
10553
|
+
const parts = fields.filter(([, v]) => v !== void 0 && v !== "").map(([k, v]) => `${k}=${logValue(v)}`);
|
|
10554
|
+
return `[@blockrun/llm] batch-settlement ${parts.join(" ")}`;
|
|
10555
|
+
}
|
|
10556
|
+
var realSleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
10557
|
+
var sleep2 = realSleep;
|
|
10558
|
+
var warnedNoSizer = false;
|
|
10559
|
+
function channelKeyOf(requirements) {
|
|
10560
|
+
const accept = requirements;
|
|
10561
|
+
const extra = accept?.extra ?? {};
|
|
10562
|
+
if (typeof extra.feePayer !== "string" || typeof extra.withdrawDelay !== "number") return void 0;
|
|
10563
|
+
return [
|
|
10564
|
+
accept.network,
|
|
10565
|
+
accept.asset,
|
|
10566
|
+
accept.payTo,
|
|
10567
|
+
extra.feePayer,
|
|
10568
|
+
extra.withdrawDelay,
|
|
10569
|
+
extra.receiverAuthorizer ?? "",
|
|
10570
|
+
extra.voucherSigner ?? "client",
|
|
10571
|
+
extra.operator ?? ""
|
|
10572
|
+
].join(":");
|
|
10573
|
+
}
|
|
10574
|
+
function capOutstandingEscrow(scheme, wallet) {
|
|
10575
|
+
const target = scheme;
|
|
10576
|
+
const original = target.resolveDepositAmount;
|
|
10577
|
+
if (typeof original !== "function") {
|
|
10578
|
+
if (!warnedNoSizer) {
|
|
10579
|
+
warnedNoSizer = true;
|
|
10580
|
+
console.error(
|
|
10581
|
+
"[@blockrun/llm] batch-settlement: this @x402/svm does not expose resolveDepositAmount; maxDeposit caps lifetime deposits"
|
|
10582
|
+
);
|
|
10583
|
+
}
|
|
10584
|
+
return;
|
|
10585
|
+
}
|
|
10586
|
+
target.resolveDepositAmount = function(requirements, requestAmount, needed, context, trust, existingDeposit) {
|
|
10587
|
+
const topUp = wallet.topUp;
|
|
10588
|
+
const widened = topUp && existingDeposit > 0n && existingDeposit === topUp.deposit && channelKeyOf(requirements) === topUp.key && typeof trust?.maxDeposit === "bigint" ? { ...trust, maxDeposit: trust.maxDeposit + topUp.settled } : trust;
|
|
10589
|
+
return original.call(this, requirements, requestAmount, needed, context, widened, existingDeposit);
|
|
10590
|
+
};
|
|
10591
|
+
}
|
|
10592
|
+
var RPC_SCOPE_STORE = /* @__PURE__ */ Symbol.for("@blockrun/llm/batch-rpc-scope/v1");
|
|
10593
|
+
var RPC_HEADER_HOOK = /* @__PURE__ */ Symbol.for("@blockrun/llm/batch-rpc-headers/v1");
|
|
10594
|
+
var sharedGlobal = globalThis;
|
|
10595
|
+
var rpcScope = sharedGlobal[RPC_SCOPE_STORE] ??= new import_async_hooks.AsyncLocalStorage();
|
|
10596
|
+
function requestUrl(input) {
|
|
10597
|
+
return typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
10598
|
+
}
|
|
10599
|
+
function installRpcHeaderHook() {
|
|
10600
|
+
const current = globalThis.fetch;
|
|
10601
|
+
if (current[RPC_HEADER_HOOK]) return;
|
|
10602
|
+
const hooked = (input, init) => {
|
|
10603
|
+
const scope = rpcScope.getStore();
|
|
10604
|
+
if (!scope || requestUrl(input) !== scope.url) return current(input, init);
|
|
10605
|
+
const headers = new Headers(input instanceof Request ? input.headers : void 0);
|
|
10606
|
+
new Headers(scope.headers).forEach((value, name) => headers.set(name, value));
|
|
10607
|
+
new Headers(init?.headers).forEach((value, name) => headers.set(name, value));
|
|
10608
|
+
return current(input, { ...init, headers, redirect: "error" });
|
|
10609
|
+
};
|
|
10610
|
+
hooked[RPC_HEADER_HOOK] = true;
|
|
10611
|
+
globalThis.fetch = hooked;
|
|
10612
|
+
}
|
|
10613
|
+
function addResyncTarget(wallet, target) {
|
|
10614
|
+
const existing = wallet.resyncs.get(target.key);
|
|
10615
|
+
if (!existing) {
|
|
10616
|
+
wallet.resyncs.set(target.key, target);
|
|
10617
|
+
return;
|
|
10618
|
+
}
|
|
10619
|
+
wallet.resyncs.set(target.key, {
|
|
10620
|
+
...existing,
|
|
10621
|
+
channelConfig: target.channelConfig,
|
|
10622
|
+
cumulative: maxOf(existing.cumulative, target.cumulative) ?? 0n,
|
|
10623
|
+
expectDeposit: maxOf(existing.expectDeposit, target.expectDeposit),
|
|
10624
|
+
knownDeposit: maxOf(existing.knownDeposit, target.knownDeposit),
|
|
10625
|
+
anchorHeight: existing.anchorHeight === void 0 || target.anchorHeight === void 0 ? existing.anchorHeight ?? target.anchorHeight : Math.max(existing.anchorHeight, target.anchorHeight)
|
|
10626
|
+
});
|
|
10627
|
+
}
|
|
10628
|
+
function intentTarget(intent) {
|
|
10629
|
+
return {
|
|
10630
|
+
key: intent.key,
|
|
10631
|
+
channelId: intent.channelId,
|
|
10632
|
+
channelConfig: intent.channelConfig,
|
|
10633
|
+
cumulative: parseAtomic(intent.cumulative),
|
|
10634
|
+
expectDeposit: parseAtomic(intent.expectDeposit),
|
|
10635
|
+
...intent.knownDeposit !== void 0 ? { knownDeposit: parseAtomic(intent.knownDeposit) } : {},
|
|
10636
|
+
...intent.anchorHeight !== void 0 ? { anchorHeight: intent.anchorHeight } : {},
|
|
10637
|
+
reason: "orphaned_deposit"
|
|
10638
|
+
};
|
|
10639
|
+
}
|
|
10640
|
+
var SolanaBatchPayer = class {
|
|
10641
|
+
constructor(init) {
|
|
10642
|
+
this.init = init;
|
|
10643
|
+
const { operators, rateLimit } = init.options;
|
|
10644
|
+
if (!Array.isArray(operators) || operators.length === 0 || operators.some((o) => typeof o !== "string" || !o)) {
|
|
10645
|
+
throw new Error("batch.operators must list at least one base58 operator public key");
|
|
10646
|
+
}
|
|
10647
|
+
this.maxAttempts = rateLimit?.maxAttempts ?? DEFAULT_BATCH_RATE_LIMIT.maxAttempts;
|
|
10648
|
+
this.maxWaitMs = rateLimit?.maxWaitMs ?? DEFAULT_BATCH_RATE_LIMIT.maxWaitMs;
|
|
10649
|
+
if (!Number.isInteger(this.maxAttempts) || this.maxAttempts < 1) {
|
|
10650
|
+
throw new Error("batch.rateLimit.maxAttempts must be an integer >= 1");
|
|
10651
|
+
}
|
|
10652
|
+
if (!Number.isFinite(this.maxWaitMs) || this.maxWaitMs < 0) {
|
|
10653
|
+
throw new Error("batch.rateLimit.maxWaitMs must be a finite number of ms >= 0");
|
|
9727
10654
|
}
|
|
9728
10655
|
}
|
|
9729
10656
|
init;
|
|
9730
10657
|
state;
|
|
10658
|
+
maxAttempts;
|
|
10659
|
+
maxWaitMs;
|
|
10660
|
+
counters = {
|
|
10661
|
+
fallbacks: 0,
|
|
10662
|
+
fallbacksByReason: {},
|
|
10663
|
+
backoffs: 0,
|
|
10664
|
+
retries: 0,
|
|
10665
|
+
recoveries: 0,
|
|
10666
|
+
resyncs: 0,
|
|
10667
|
+
unresolved: 0,
|
|
10668
|
+
unresolvedByReason: {}
|
|
10669
|
+
};
|
|
10670
|
+
/** A copy of this client's batch counters. */
|
|
10671
|
+
stats() {
|
|
10672
|
+
return {
|
|
10673
|
+
...this.counters,
|
|
10674
|
+
fallbacksByReason: { ...this.counters.fallbacksByReason },
|
|
10675
|
+
unresolvedByReason: { ...this.counters.unresolvedByReason }
|
|
10676
|
+
};
|
|
10677
|
+
}
|
|
9731
10678
|
async storeFile() {
|
|
9732
10679
|
const { channelStore } = this.init.options;
|
|
9733
10680
|
if (channelStore === false) return void 0;
|
|
@@ -9743,20 +10690,63 @@ var SolanaBatchPayer = class {
|
|
|
9743
10690
|
const address = await this.init.address();
|
|
9744
10691
|
const file = await this.storeFile();
|
|
9745
10692
|
const { operators, maxDeposit } = this.init.options;
|
|
9746
|
-
const
|
|
9747
|
-
const
|
|
10693
|
+
const headers = Object.entries(this.init.rpcHeaders ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
10694
|
+
const config = JSON.stringify([[...operators].sort(), String(maxDeposit ?? ""), file ?? null, this.init.rpcUrl, headers]);
|
|
10695
|
+
const existing = registry2.wallets.get(address);
|
|
9748
10696
|
if (existing) {
|
|
9749
|
-
return existing.config === config ? existing :
|
|
10697
|
+
return existing.config === config ? existing : {
|
|
10698
|
+
reason: "wallet_config_conflict",
|
|
10699
|
+
detail: "another client in this process uses this wallet with different batch options"
|
|
10700
|
+
};
|
|
9750
10701
|
}
|
|
9751
10702
|
if (file) {
|
|
9752
10703
|
const owner = lockChannelFile(file);
|
|
9753
|
-
if (owner !== void 0)
|
|
10704
|
+
if (owner !== void 0) {
|
|
10705
|
+
return {
|
|
10706
|
+
reason: "channel_store_locked",
|
|
10707
|
+
detail: owner === process.pid ? `channel store ${file} is in use by another copy of @blockrun/llm in this process (or by a process that had this pid and died: remove ${file}.lock if nothing uses it)` : `channel store ${file} is in use by process ${owner}`
|
|
10708
|
+
};
|
|
10709
|
+
}
|
|
9754
10710
|
}
|
|
9755
|
-
const
|
|
9756
|
-
|
|
10711
|
+
const resyncs = /* @__PURE__ */ new Map();
|
|
10712
|
+
const base = file ? new FileChannelStorage(file) : new MemoryChannelStorage();
|
|
10713
|
+
const book = new ChannelBook(base, (target) => addResyncTarget(created, target));
|
|
10714
|
+
const intents = file ? FileIntentJournal.beside(file) : new MemoryIntentJournal();
|
|
10715
|
+
let journaled;
|
|
10716
|
+
try {
|
|
10717
|
+
if (file) intents.adoptLegacy(FileIntentJournal.legacyBeside(file), address);
|
|
10718
|
+
journaled = intents.list();
|
|
10719
|
+
} catch (err) {
|
|
10720
|
+
return {
|
|
10721
|
+
reason: "deposit_journal_unreadable",
|
|
10722
|
+
detail: `cannot read ${intents.file}: ${err instanceof Error ? err.message : String(err)}`
|
|
10723
|
+
};
|
|
10724
|
+
}
|
|
10725
|
+
const created = {
|
|
10726
|
+
config,
|
|
10727
|
+
busy: false,
|
|
10728
|
+
cooldownUntil: 0,
|
|
10729
|
+
active: 0,
|
|
10730
|
+
...file ? { file } : {},
|
|
10731
|
+
book,
|
|
10732
|
+
resyncs,
|
|
10733
|
+
intents,
|
|
10734
|
+
closes: 0
|
|
10735
|
+
};
|
|
10736
|
+
for (const intent of journaled) addResyncTarget(created, intentTarget(intent));
|
|
10737
|
+
registry2.wallets.set(address, created);
|
|
9757
10738
|
return created;
|
|
9758
10739
|
})();
|
|
9759
|
-
|
|
10740
|
+
const state = this.state;
|
|
10741
|
+
state.then(
|
|
10742
|
+
(result) => {
|
|
10743
|
+
if ("reason" in result && result.reason !== "wallet_config_conflict" && this.state === state) this.state = void 0;
|
|
10744
|
+
},
|
|
10745
|
+
() => {
|
|
10746
|
+
if (this.state === state) this.state = void 0;
|
|
10747
|
+
}
|
|
10748
|
+
);
|
|
10749
|
+
return state;
|
|
9760
10750
|
}
|
|
9761
10751
|
build(wallet) {
|
|
9762
10752
|
wallet.client ??= (async () => {
|
|
@@ -9766,21 +10756,33 @@ var SolanaBatchPayer = class {
|
|
|
9766
10756
|
load("@solana/kit", () => import("@solana/kit"))
|
|
9767
10757
|
]);
|
|
9768
10758
|
const signer = await kit.createKeyPairSignerFromBytes(await this.init.secretKey());
|
|
9769
|
-
const file = await this.storeFile();
|
|
9770
|
-
const storage = file ? dropOrphanedPending(new FileChannelStorage(file)) : void 0;
|
|
9771
10759
|
const scheme = new svm.BatchSvmScheme(signer, {
|
|
9772
10760
|
rpcUrl: this.init.rpcUrl,
|
|
9773
10761
|
serverSignedChannelsPolicy: {
|
|
9774
10762
|
allowedOperators: this.init.options.operators,
|
|
9775
10763
|
maxDeposit: this.init.options.maxDeposit ?? svm.DEFAULT_SERVER_SIGNED_MAX_DEPOSIT
|
|
9776
10764
|
},
|
|
9777
|
-
|
|
10765
|
+
// Always a store, memory included: a record the SDK repairs from the
|
|
10766
|
+
// chain survives the scheme being rebuilt to pick it up.
|
|
10767
|
+
channelStorage: wallet.book
|
|
9778
10768
|
});
|
|
10769
|
+
capOutstandingEscrow(scheme, wallet);
|
|
9779
10770
|
const client = new core.x402Client().register("solana:*", scheme).registerPolicy(scheme.paymentPolicy);
|
|
9780
10771
|
client.setSpendControls({ maxAmountPerPayment: false });
|
|
10772
|
+
const raw = new core.x402HTTPClient(client);
|
|
10773
|
+
const http = {
|
|
10774
|
+
getPaymentSettleResponse: (getHeader) => raw.getPaymentSettleResponse(getHeader),
|
|
10775
|
+
createPaymentPayload: (paymentRequired) => this.withRpcHeaders(() => raw.createPaymentPayload(paymentRequired)),
|
|
10776
|
+
encodePaymentSignatureHeader: (payload) => raw.encodePaymentSignatureHeader(payload),
|
|
10777
|
+
processPaymentResult: (payload, getHeader, status2) => this.withRpcHeaders(() => raw.processPaymentResult(payload, getHeader, status2))
|
|
10778
|
+
};
|
|
10779
|
+
const loader = scheme.loadChannel;
|
|
9781
10780
|
return {
|
|
9782
|
-
http
|
|
9783
|
-
refund: (url) => scheme.refund(url)
|
|
10781
|
+
http,
|
|
10782
|
+
refund: (url, requirements) => this.withRpcHeaders(() => scheme.refund(url, { requirements })),
|
|
10783
|
+
load: async (key) => {
|
|
10784
|
+
if (typeof loader === "function") await loader.call(scheme, key);
|
|
10785
|
+
}
|
|
9784
10786
|
};
|
|
9785
10787
|
})();
|
|
9786
10788
|
wallet.client.catch(() => {
|
|
@@ -9788,73 +10790,863 @@ var SolanaBatchPayer = class {
|
|
|
9788
10790
|
});
|
|
9789
10791
|
return wallet.client;
|
|
9790
10792
|
}
|
|
10793
|
+
/** Run a scheme call so its RPC requests carry this client's `rpcHeaders`. */
|
|
10794
|
+
withRpcHeaders(call) {
|
|
10795
|
+
const headers = this.init.rpcHeaders;
|
|
10796
|
+
if (!headers || Object.keys(headers).length === 0) return call();
|
|
10797
|
+
installRpcHeaderHook();
|
|
10798
|
+
return rpcScope.run({ url: this.init.rpcUrl, headers }, call);
|
|
10799
|
+
}
|
|
9791
10800
|
/**
|
|
9792
|
-
* Drop everything this wallet knows about
|
|
9793
|
-
* memory
|
|
9794
|
-
*
|
|
10801
|
+
* Drop everything this wallet knows about one channel, on disk and in
|
|
10802
|
+
* memory, leaving its other channels' records alone. The scheme is rebuilt
|
|
10803
|
+
* on the next call, without the closed channel in its memory.
|
|
9795
10804
|
*/
|
|
9796
|
-
async forget(wallet) {
|
|
10805
|
+
async forget(wallet, key) {
|
|
9797
10806
|
wallet.client = void 0;
|
|
9798
|
-
|
|
9799
|
-
|
|
10807
|
+
wallet.resyncs.delete(key);
|
|
10808
|
+
await wallet.book.forget(key);
|
|
10809
|
+
this.forgetIntent(wallet, key);
|
|
9800
10810
|
}
|
|
9801
|
-
|
|
9802
|
-
|
|
9803
|
-
|
|
9804
|
-
|
|
10811
|
+
/**
|
|
10812
|
+
* Stop trusting the record of the channel a payload paid into: the next
|
|
10813
|
+
* payment re-reads that channel from the chain first. The scheme has
|
|
10814
|
+
* already restored (or, for an open, dropped) its record; the book still
|
|
10815
|
+
* knows the channel and the cumulative it last confirmed.
|
|
10816
|
+
*/
|
|
10817
|
+
distrust(wallet, sent, reason, charged = 0n) {
|
|
10818
|
+
const { payload } = sent;
|
|
10819
|
+
const channelId = payloadChannelId(payload);
|
|
10820
|
+
const known = channelId ? wallet.book.find(channelId) : void 0;
|
|
10821
|
+
if (!channelId || !known) {
|
|
10822
|
+
wallet.client = void 0;
|
|
10823
|
+
let intent;
|
|
10824
|
+
try {
|
|
10825
|
+
intent = sent.key ? wallet.intents.list().find((candidate) => candidate.key === sent.key) : void 0;
|
|
10826
|
+
} catch {
|
|
10827
|
+
intent = void 0;
|
|
10828
|
+
}
|
|
10829
|
+
if (intent) addResyncTarget(wallet, { ...intentTarget(intent), reason });
|
|
10830
|
+
return;
|
|
10831
|
+
}
|
|
10832
|
+
const { key, record } = known;
|
|
10833
|
+
const hasConfirmed = record.pending === void 0 || record.hasConfirmedState;
|
|
10834
|
+
const confirmed = hasConfirmed ? parseAtomic(record.chargedCumulativeAmount) : 0n;
|
|
10835
|
+
const expectDeposit = sent.expectDeposit ?? this.depositExpectation(wallet, payload).expectDeposit;
|
|
10836
|
+
addResyncTarget(wallet, {
|
|
10837
|
+
key,
|
|
10838
|
+
channelId,
|
|
10839
|
+
channelConfig: record.channelConfig,
|
|
10840
|
+
cumulative: confirmed + charged,
|
|
10841
|
+
...hasConfirmed ? { knownDeposit: parseAtomic(record.deposit) } : {},
|
|
10842
|
+
...expectDeposit !== void 0 ? { expectDeposit } : {},
|
|
10843
|
+
reason
|
|
10844
|
+
});
|
|
10845
|
+
}
|
|
10846
|
+
/**
|
|
10847
|
+
* The channel's total deposit if a deposit payload landed: the confirmed
|
|
10848
|
+
* deposit plus this one (for an open, nothing plus this one). Undefined for
|
|
10849
|
+
* a payment without a deposit, or a channel the SDK has no record of.
|
|
10850
|
+
*/
|
|
10851
|
+
depositExpectation(wallet, payload) {
|
|
10852
|
+
if (payload.payload?.type !== "deposit") return {};
|
|
10853
|
+
const channelId = payloadChannelId(payload);
|
|
10854
|
+
const record = channelId ? wallet.book.find(channelId)?.record : void 0;
|
|
10855
|
+
if (!record) return {};
|
|
10856
|
+
const hasConfirmed = record.pending === void 0 || record.hasConfirmedState;
|
|
10857
|
+
return { expectDeposit: (hasConfirmed ? parseAtomic(record.deposit) : 0n) + parseAtomic(payload.payload.deposit?.amount) };
|
|
10858
|
+
}
|
|
10859
|
+
/**
|
|
10860
|
+
* Re-read every distrusted channel from the chain, at `finalized`
|
|
10861
|
+
* commitment, and rewrite its record: the on-chain deposit, and the highest
|
|
10862
|
+
* of the cumulative the SDK can vouch for, the cumulative the stored record
|
|
10863
|
+
* already confirmed, and the on-chain settled amount. A closing or closed
|
|
10864
|
+
* channel is dropped. The scheme is rebuilt to load the repaired record.
|
|
10865
|
+
*
|
|
10866
|
+
* The stored cumulative is never moved backwards. A deposit's intent can
|
|
10867
|
+
* outlive the receipt that reconciled it (the process died, or the journal
|
|
10868
|
+
* could not be cleared, after the scheme saved the new state), so its
|
|
10869
|
+
* cumulative may predate vouchers the scheme has since confirmed; and
|
|
10870
|
+
* vouchers are redeemed on-chain asynchronously, so `settled` can lag too.
|
|
10871
|
+
* Rewriting the record with either would leave every later voucher
|
|
10872
|
+
* unreconcilable.
|
|
10873
|
+
*
|
|
10874
|
+
* A deposit in doubt is settled by the chain alone:
|
|
10875
|
+
* - landed: the finalized channel holds at least the deposit it would
|
|
10876
|
+
* bring (and at least what the record already claimed);
|
|
10877
|
+
* - never landed: the finalized chain is {@link LANDING_MARGIN_BLOCKS}
|
|
10878
|
+
* past the target's anchor height, and a read at least that recent still
|
|
10879
|
+
* does not show it. An open's record is then dropped; a top-up's channel
|
|
10880
|
+
* is rewritten with the deposit it really has.
|
|
10881
|
+
* Until one of those holds, it is pending. This settles only the deposit:
|
|
10882
|
+
* whether the gateway charged the payment that carried it is never read
|
|
10883
|
+
* from the chain.
|
|
10884
|
+
*
|
|
10885
|
+
* @throws ResyncError when the chain cannot be read, or a deposit may still
|
|
10886
|
+
* land: the caller pays exact and the re-read is tried again next call.
|
|
10887
|
+
* Nothing opens or tops up a channel while any re-read is pending.
|
|
10888
|
+
*/
|
|
10889
|
+
async resync(wallet) {
|
|
10890
|
+
for (const target of [...wallet.resyncs.values()]) {
|
|
10891
|
+
const failed = (err) => err instanceof ResyncError ? err : err instanceof ChannelUnreadableError ? new ResyncError("channel_unreadable", err.message) : err instanceof ChannelLayoutError ? new ResyncError("channel_unreadable", `channel ${target.channelId}: ${err.message}`) : new ResyncError(
|
|
10892
|
+
"channel_resync_failed",
|
|
10893
|
+
`could not read channel ${target.channelId} from the chain: ${err instanceof Error ? err.message : String(err)}`
|
|
10894
|
+
);
|
|
10895
|
+
let channel;
|
|
10896
|
+
let expired = false;
|
|
10897
|
+
let stored;
|
|
10898
|
+
try {
|
|
10899
|
+
channel = await this.readOwnChannel(target);
|
|
10900
|
+
const required = maxOf(target.expectDeposit, target.knownDeposit) ?? 0n;
|
|
10901
|
+
if (channel === void 0 || channel.open && channel.deposit < required) {
|
|
10902
|
+
if (target.anchorHeight === void 0) {
|
|
10903
|
+
target.anchorHeight = rpcInteger(
|
|
10904
|
+
await rpcRequest(this.init.rpcUrl, "getBlockHeight", [{ commitment: "confirmed" }], this.init.rpcHeaders),
|
|
10905
|
+
"getBlockHeight"
|
|
10906
|
+
);
|
|
10907
|
+
try {
|
|
10908
|
+
const intent = wallet.intents.list().find((candidate) => candidate.key === target.key);
|
|
10909
|
+
if (intent) wallet.intents.put({ ...intent, anchorHeight: target.anchorHeight });
|
|
10910
|
+
} catch {
|
|
10911
|
+
}
|
|
10912
|
+
}
|
|
10913
|
+
const epoch = await rpcRequest(this.init.rpcUrl, "getEpochInfo", [{ commitment: "finalized" }], this.init.rpcHeaders);
|
|
10914
|
+
const finalizedHeight = rpcInteger(epoch.blockHeight, "getEpochInfo blockHeight");
|
|
10915
|
+
const finalizedSlot = rpcInteger(epoch.absoluteSlot, "getEpochInfo absoluteSlot");
|
|
10916
|
+
if (finalizedHeight <= target.anchorHeight + LANDING_MARGIN_BLOCKS) {
|
|
10917
|
+
throw new ResyncError(
|
|
10918
|
+
"channel_resync_pending",
|
|
10919
|
+
channel === void 0 ? `channel ${target.channelId} is not finalized on chain, and its deposit may still land` : `channel ${target.channelId} holds ${channel.deposit} finalized, short of the ${required} it should hold, and a deposit may still land`
|
|
10920
|
+
);
|
|
10921
|
+
}
|
|
10922
|
+
channel = await this.readOwnChannel(target, finalizedSlot);
|
|
10923
|
+
expired = true;
|
|
10924
|
+
}
|
|
10925
|
+
stored = await wallet.book.base.get(target.key);
|
|
10926
|
+
} catch (err) {
|
|
10927
|
+
throw failed(err);
|
|
10928
|
+
}
|
|
10929
|
+
let outcome;
|
|
10930
|
+
if (typeof stored?.channelId === "string" && stored.channelId !== target.channelId) {
|
|
10931
|
+
outcome = `the record under its key now names channel ${stored.channelId}; left untouched`;
|
|
10932
|
+
} else if (channel === void 0) {
|
|
10933
|
+
await wallet.book.delete(target.key);
|
|
10934
|
+
outcome = "no such channel at finalized commitment, past its deposit's last valid block; record dropped";
|
|
10935
|
+
} else if (!channel.open) {
|
|
10936
|
+
await wallet.book.delete(target.key);
|
|
10937
|
+
outcome = "channel is closing or closed; record dropped";
|
|
10938
|
+
} else {
|
|
10939
|
+
const cumulative = maxOf(maxOf(target.cumulative, channel.settled), confirmedCumulative(stored, target.channelId)) ?? 0n;
|
|
10940
|
+
await wallet.book.set(target.key, {
|
|
10941
|
+
channelConfig: target.channelConfig,
|
|
10942
|
+
channelId: target.channelId,
|
|
10943
|
+
chargedCumulativeAmount: cumulative.toString(),
|
|
10944
|
+
deposit: channel.deposit.toString()
|
|
10945
|
+
});
|
|
10946
|
+
outcome = `record rewritten from finalized chain: deposit=${channel.deposit} settled=${channel.settled} cumulative=${cumulative}` + (expired ? " (the deposit in doubt never landed)" : "");
|
|
10947
|
+
}
|
|
10948
|
+
wallet.resyncs.delete(target.key);
|
|
10949
|
+
this.forgetIntent(wallet, target.key);
|
|
10950
|
+
wallet.client = void 0;
|
|
10951
|
+
await this.report({ type: "resync", reason: target.reason, detail: `channel ${target.channelId}: ${outcome}` });
|
|
10952
|
+
}
|
|
10953
|
+
}
|
|
10954
|
+
/**
|
|
10955
|
+
* A distrusted channel's account at `finalized` commitment, checked to be
|
|
10956
|
+
* this wallet's channel for the record's operator and mint.
|
|
10957
|
+
*
|
|
10958
|
+
* @throws ResyncError (`channel_unreadable`) for any other payment channel.
|
|
10959
|
+
*/
|
|
10960
|
+
async readOwnChannel(target, minContextSlot) {
|
|
10961
|
+
const channel = await readChannelAccount(this.init.rpcUrl, target.channelId, this.init.rpcHeaders, { minContextSlot });
|
|
10962
|
+
if (channel !== void 0 && (channel.payer !== await this.init.address() || channel.authorizedSigner !== target.channelConfig.payerAuthorizer || channel.mint !== target.channelConfig.token)) {
|
|
10963
|
+
throw new ResyncError("channel_unreadable", `account ${target.channelId} is a payment channel for another payer, operator or mint`);
|
|
10964
|
+
}
|
|
10965
|
+
return channel;
|
|
10966
|
+
}
|
|
10967
|
+
/**
|
|
10968
|
+
* Make one event observable three ways: a stderr line (every time, never
|
|
10969
|
+
* deduplicated), this client's counters, and the caller's `onEvent`.
|
|
10970
|
+
*/
|
|
10971
|
+
async report(event) {
|
|
10972
|
+
let wallet;
|
|
10973
|
+
try {
|
|
10974
|
+
wallet = await this.init.address();
|
|
10975
|
+
} catch {
|
|
10976
|
+
wallet = "unknown";
|
|
10977
|
+
}
|
|
10978
|
+
const full = { ...event, wallet, at: Date.now() };
|
|
10979
|
+
if (full.type === "fallback") {
|
|
10980
|
+
this.counters.fallbacks += 1;
|
|
10981
|
+
this.counters.fallbacksByReason[full.reason] = (this.counters.fallbacksByReason[full.reason] ?? 0) + 1;
|
|
10982
|
+
} else if (full.type === "backoff") {
|
|
10983
|
+
this.counters.backoffs += 1;
|
|
10984
|
+
} else if (full.type === "recovered") {
|
|
10985
|
+
this.counters.recoveries += 1;
|
|
10986
|
+
} else if (full.type === "unresolved") {
|
|
10987
|
+
this.counters.unresolved += 1;
|
|
10988
|
+
this.counters.unresolvedByReason[full.reason] = (this.counters.unresolvedByReason[full.reason] ?? 0) + 1;
|
|
10989
|
+
} else {
|
|
10990
|
+
this.counters.resyncs += 1;
|
|
10991
|
+
}
|
|
10992
|
+
console.error(formatEvent(full));
|
|
10993
|
+
const failed = (err) => console.error(
|
|
10994
|
+
`[@blockrun/llm] batch-settlement onEvent callback threw: ${JSON.stringify(err instanceof Error ? err.message : String(err))}`
|
|
10995
|
+
);
|
|
10996
|
+
try {
|
|
10997
|
+
const result = this.init.options.onEvent?.(full);
|
|
10998
|
+
if (result && typeof result.then === "function") {
|
|
10999
|
+
result.then(void 0, failed);
|
|
11000
|
+
}
|
|
11001
|
+
} catch (err) {
|
|
11002
|
+
failed(err);
|
|
11003
|
+
}
|
|
11004
|
+
}
|
|
11005
|
+
/** Report a fallback and tell the caller to pay exact. */
|
|
11006
|
+
async fallback(event) {
|
|
11007
|
+
await this.report({ type: "fallback", ...event });
|
|
11008
|
+
return { kind: "fallback", reason: event.reason };
|
|
9805
11009
|
}
|
|
9806
11010
|
/**
|
|
9807
11011
|
* Pay one 402 with batch-settlement, or say why the caller should use exact.
|
|
9808
11012
|
*
|
|
9809
|
-
* `send` replays the original request with the payment headers.
|
|
9810
|
-
*
|
|
9811
|
-
*
|
|
11013
|
+
* `send` replays the original request with the payment headers. Every send
|
|
11014
|
+
* goes through {@link sendOnce}, the one place an answer is classified as
|
|
11015
|
+
* charged, not charged, or in doubt:
|
|
11016
|
+
*
|
|
11017
|
+
* - charged: the 2xx is returned (`paid`);
|
|
11018
|
+
* - not charged (the gateway's explicit answer to a first send): a 429
|
|
11019
|
+
* whose receipt proves nothing was broadcast is backed off
|
|
11020
|
+
* (`Retry-After`, else 1s, 2s, 4s with jitter) and paid again with a new
|
|
11021
|
+
* payment built from a fresh 402 (`rechallenge`: the original's
|
|
11022
|
+
* `recentBlockhash` / `recentSlot` can be stale by then), within
|
|
11023
|
+
* `rateLimit.maxAttempts` / `maxWaitMs`, then with exact; any other is
|
|
11024
|
+
* paid with exact (`fallback`). A fresh challenge served with a 2xx is
|
|
11025
|
+
* the call's result (`served`, nothing paid); one that fails otherwise
|
|
11026
|
+
* is the call's error (`failed`, `"unpaid"`), never an exact payment
|
|
11027
|
+
* against the stale challenge;
|
|
11028
|
+
* - in doubt: ends only with a definitive success receipt from the one
|
|
11029
|
+
* replay a receipt-less 429 is allowed ({@link resolveInDoubt}), or a
|
|
11030
|
+
* thrown {@link BatchPaymentUnresolvedError}. No new authorization, no
|
|
11031
|
+
* new deposit, no exact payment and no fallback model is ever paid for a
|
|
11032
|
+
* call in doubt.
|
|
11033
|
+
*
|
|
11034
|
+
* The wallet stays in cooldown after a 429, so its other calls wait
|
|
11035
|
+
* instead of sending opens of their own.
|
|
11036
|
+
*
|
|
11037
|
+
* @param closesAtStart - {@link closeFence}, taken before the call's first
|
|
11038
|
+
* (unpaid) request. If a close completed since, the call pays exact
|
|
11039
|
+
* (`closed_during_call`): it started before the close, so it must not
|
|
11040
|
+
* open a channel the caller believes is closed.
|
|
9812
11041
|
*/
|
|
9813
|
-
async pay(paymentRequired, send) {
|
|
9814
|
-
|
|
9815
|
-
|
|
9816
|
-
|
|
9817
|
-
|
|
9818
|
-
|
|
11042
|
+
async pay(paymentRequired, send, rechallenge, closesAtStart) {
|
|
11043
|
+
const unavailable = batchUnavailable(paymentRequired);
|
|
11044
|
+
if (unavailable) return this.fallback(unavailable);
|
|
11045
|
+
let wallet;
|
|
11046
|
+
try {
|
|
11047
|
+
wallet = await this.wallet();
|
|
11048
|
+
} catch (err) {
|
|
11049
|
+
return this.fallback(creationFailure(err));
|
|
9819
11050
|
}
|
|
9820
|
-
if (wallet
|
|
9821
|
-
wallet.
|
|
11051
|
+
if ("reason" in wallet) return this.fallback({ reason: wallet.reason, detail: wallet.detail });
|
|
11052
|
+
if (closesAtStart !== void 0 && wallet.closes !== closesAtStart) {
|
|
11053
|
+
return this.fallback({
|
|
11054
|
+
reason: "closed_during_call",
|
|
11055
|
+
detail: "closeBatchChannel() closed this wallet's channel after this call started; it opens no channel"
|
|
11056
|
+
});
|
|
11057
|
+
}
|
|
11058
|
+
wallet.active += 1;
|
|
9822
11059
|
try {
|
|
9823
|
-
|
|
9824
|
-
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
|
|
9828
|
-
|
|
9829
|
-
|
|
9830
|
-
|
|
9831
|
-
|
|
11060
|
+
return await this.payWith(wallet, paymentRequired, send, rechallenge, closesAtStart ?? wallet.closes);
|
|
11061
|
+
} finally {
|
|
11062
|
+
wallet.active -= 1;
|
|
11063
|
+
}
|
|
11064
|
+
}
|
|
11065
|
+
/**
|
|
11066
|
+
* The close fence for a call about to send its first request: how many
|
|
11067
|
+
* closes this wallet has completed so far, to hand to {@link pay}.
|
|
11068
|
+
* `close()` refuses while a call is inside `pay()`, but a call still
|
|
11069
|
+
* waiting for its 402 is not there yet; the fence covers that window.
|
|
11070
|
+
* Undefined when the wallet cannot be named (no fence then; `pay()`
|
|
11071
|
+
* reports why batch is unavailable).
|
|
11072
|
+
*/
|
|
11073
|
+
async closeFence() {
|
|
11074
|
+
try {
|
|
11075
|
+
return registry2.wallets.get(await this.init.address())?.closes ?? 0;
|
|
11076
|
+
} catch {
|
|
11077
|
+
return void 0;
|
|
11078
|
+
}
|
|
11079
|
+
}
|
|
11080
|
+
/** {@link pay}, for a wallet this client may use. */
|
|
11081
|
+
async payWith(wallet, paymentRequired, send, rechallenge, fence) {
|
|
11082
|
+
let current = paymentRequired;
|
|
11083
|
+
const fallback = async (event) => {
|
|
11084
|
+
const result = await this.fallback(event);
|
|
11085
|
+
return current === paymentRequired ? result : { ...result, paymentRequired: current };
|
|
11086
|
+
};
|
|
11087
|
+
const budget = { attempt: 1, waited: 0 };
|
|
11088
|
+
let waitedUntil = 0;
|
|
11089
|
+
let backedOff;
|
|
11090
|
+
let holding = false;
|
|
11091
|
+
let sent;
|
|
11092
|
+
const release = () => {
|
|
11093
|
+
if (holding) wallet.busy = false;
|
|
11094
|
+
holding = false;
|
|
11095
|
+
};
|
|
11096
|
+
try {
|
|
11097
|
+
for (; ; ) {
|
|
11098
|
+
const now = Date.now();
|
|
11099
|
+
if (wallet.cooldownUntil > now && wallet.cooldownUntil !== waitedUntil) {
|
|
11100
|
+
const wait = wallet.cooldownUntil - now;
|
|
11101
|
+
if (budget.waited + wait > this.maxWaitMs) {
|
|
11102
|
+
return fallback({
|
|
11103
|
+
reason: "rate_limited",
|
|
11104
|
+
retryAfterMs: wait,
|
|
11105
|
+
attempt: budget.attempt,
|
|
11106
|
+
detail: "wallet is in a 429 cooldown longer than batch.rateLimit.maxWaitMs allows"
|
|
11107
|
+
});
|
|
11108
|
+
}
|
|
11109
|
+
await this.report({ type: "backoff", reason: "cooldown", retryAfterMs: wait, attempt: budget.attempt });
|
|
11110
|
+
backedOff ??= "cooldown";
|
|
11111
|
+
waitedUntil = wallet.cooldownUntil;
|
|
11112
|
+
budget.waited += wait;
|
|
11113
|
+
await sleep2(wait);
|
|
11114
|
+
continue;
|
|
11115
|
+
}
|
|
11116
|
+
if (budget.attempt > 1 || budget.waited > 0) {
|
|
11117
|
+
let fresh;
|
|
11118
|
+
try {
|
|
11119
|
+
fresh = await rechallenge();
|
|
11120
|
+
} catch (err) {
|
|
11121
|
+
return { kind: "failed", error: withDisposition(err instanceof Error ? err : new Error(String(err)), "unpaid") };
|
|
11122
|
+
}
|
|
11123
|
+
if (fresh.kind === "served") {
|
|
11124
|
+
await this.report({
|
|
11125
|
+
type: "recovered",
|
|
11126
|
+
reason: "served_unpaid_on_rechallenge",
|
|
11127
|
+
status: fresh.response.status,
|
|
11128
|
+
attempt: budget.attempt,
|
|
11129
|
+
detail: "the fresh challenge after the wait was served without a payment; nothing was paid"
|
|
11130
|
+
});
|
|
11131
|
+
return { kind: "served", response: fresh.response };
|
|
11132
|
+
}
|
|
11133
|
+
current = fresh.paymentRequired;
|
|
11134
|
+
const unavailable = batchUnavailable(current);
|
|
11135
|
+
if (unavailable) return fallback({ ...unavailable, attempt: budget.attempt });
|
|
11136
|
+
}
|
|
11137
|
+
if (wallet.closes !== fence) {
|
|
11138
|
+
return fallback({
|
|
11139
|
+
reason: "closed_during_call",
|
|
11140
|
+
attempt: budget.attempt,
|
|
11141
|
+
detail: "closeBatchChannel() closed this wallet's channel after this call started; it opens no channel"
|
|
11142
|
+
});
|
|
11143
|
+
}
|
|
11144
|
+
if (wallet.busy) return fallback({ reason: "channel_busy", attempt: budget.attempt });
|
|
11145
|
+
if (wallet.file && !holdsChannelFile(wallet.file)) {
|
|
11146
|
+
return fallback({
|
|
11147
|
+
reason: "channel_store_locked",
|
|
11148
|
+
attempt: budget.attempt,
|
|
11149
|
+
detail: `this process no longer holds the lock on ${wallet.file}; another owner may be paying into its channels`
|
|
11150
|
+
});
|
|
11151
|
+
}
|
|
11152
|
+
wallet.busy = true;
|
|
11153
|
+
holding = true;
|
|
11154
|
+
let built;
|
|
11155
|
+
try {
|
|
11156
|
+
built = await this.buildPayment(wallet, current);
|
|
11157
|
+
} catch (err) {
|
|
11158
|
+
release();
|
|
11159
|
+
if (err instanceof ResyncError) return fallback({ reason: err.reason, detail: err.message, attempt: budget.attempt });
|
|
11160
|
+
return fallback({ ...creationFailure(err), attempt: budget.attempt });
|
|
11161
|
+
}
|
|
11162
|
+
if (built.kind !== "authorization") {
|
|
11163
|
+
try {
|
|
11164
|
+
this.journalDeposit(wallet, built);
|
|
11165
|
+
} catch (err) {
|
|
11166
|
+
await this.settle(built.http, built.payload, () => null, 0);
|
|
11167
|
+
release();
|
|
11168
|
+
return fallback({
|
|
11169
|
+
reason: "deposit_journal_failed",
|
|
11170
|
+
attempt: budget.attempt,
|
|
11171
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
11172
|
+
});
|
|
11173
|
+
}
|
|
11174
|
+
}
|
|
11175
|
+
sent = built;
|
|
11176
|
+
const outcome = await this.sendOnce(wallet, built, send, backedOff === "rate_limited", false);
|
|
11177
|
+
if (outcome.kind === "charged") {
|
|
11178
|
+
if (built.kind !== "authorization" && !(built.key && wallet.resyncs.has(built.key))) this.forgetIntent(wallet, built.key);
|
|
11179
|
+
sent = void 0;
|
|
11180
|
+
release();
|
|
11181
|
+
if (backedOff) await this.report({ type: "recovered", reason: backedOff, attempt: budget.attempt });
|
|
11182
|
+
return { kind: "paid", response: outcome.response, chargedUsd: outcome.chargedUsd };
|
|
11183
|
+
}
|
|
11184
|
+
if (outcome.kind === "not_charged") {
|
|
11185
|
+
if (built.kind !== "authorization") {
|
|
11186
|
+
if (outcome.depositSafe) this.forgetIntent(wallet, built.key);
|
|
11187
|
+
else this.distrust(wallet, built, "deposit_refused");
|
|
11188
|
+
}
|
|
11189
|
+
sent = void 0;
|
|
11190
|
+
release();
|
|
11191
|
+
if (!outcome.rateLimited) {
|
|
11192
|
+
const { kind: _kind, rateLimited: _limited, depositSafe: _safe, ...event } = outcome;
|
|
11193
|
+
return fallback({ ...event, attempt: budget.attempt });
|
|
11194
|
+
}
|
|
11195
|
+
const wait = this.coolDown(wallet, outcome.rateLimited.retryAfterMs ?? backoffDelay(budget.attempt));
|
|
11196
|
+
if (budget.attempt >= this.maxAttempts || budget.waited + wait > this.maxWaitMs) {
|
|
11197
|
+
return fallback({
|
|
11198
|
+
reason: "rate_limited",
|
|
11199
|
+
status: 429,
|
|
11200
|
+
errorReason: outcome.errorReason,
|
|
11201
|
+
retryAfterMs: wait,
|
|
11202
|
+
attempt: budget.attempt,
|
|
11203
|
+
detail: budget.attempt >= this.maxAttempts ? "batch.rateLimit.maxAttempts reached" : "Retry-After exceeds what is left of batch.rateLimit.maxWaitMs"
|
|
11204
|
+
});
|
|
11205
|
+
}
|
|
11206
|
+
await this.report({
|
|
11207
|
+
type: "backoff",
|
|
11208
|
+
reason: "rate_limited",
|
|
11209
|
+
status: 429,
|
|
11210
|
+
errorReason: outcome.errorReason,
|
|
11211
|
+
retryAfterMs: wait,
|
|
11212
|
+
attempt: budget.attempt
|
|
11213
|
+
});
|
|
11214
|
+
backedOff = "rate_limited";
|
|
11215
|
+
waitedUntil = wallet.cooldownUntil;
|
|
11216
|
+
budget.waited += wait;
|
|
11217
|
+
await sleep2(wait);
|
|
11218
|
+
budget.attempt += 1;
|
|
11219
|
+
continue;
|
|
11220
|
+
}
|
|
11221
|
+
if (outcome.kind === "cancelled") {
|
|
11222
|
+
sent = void 0;
|
|
11223
|
+
release();
|
|
11224
|
+
await this.report({
|
|
11225
|
+
type: "recovered",
|
|
11226
|
+
reason: "batch_cancelled",
|
|
11227
|
+
status: outcome.status,
|
|
11228
|
+
errorReason: "batch_cancelled",
|
|
11229
|
+
attempt: budget.attempt,
|
|
11230
|
+
detail: "the gateway cancelled the payment: nothing was charged, and its error is raised unpaid"
|
|
11231
|
+
});
|
|
11232
|
+
return { kind: "failed", error: outcome.error };
|
|
11233
|
+
}
|
|
11234
|
+
const resolved = await this.resolveInDoubt(wallet, built, outcome, send, budget);
|
|
11235
|
+
if (resolved.kind === "charged") {
|
|
11236
|
+
if (built.kind !== "authorization" && !(built.key && wallet.resyncs.has(built.key))) this.forgetIntent(wallet, built.key);
|
|
11237
|
+
sent = void 0;
|
|
11238
|
+
release();
|
|
11239
|
+
await this.report({ type: "recovered", reason: "rate_limited", attempt: budget.attempt });
|
|
11240
|
+
return { kind: "paid", response: resolved.response, chargedUsd: resolved.chargedUsd };
|
|
11241
|
+
}
|
|
11242
|
+
return await this.raiseUnresolved(wallet, built, resolved, budget.attempt);
|
|
11243
|
+
}
|
|
11244
|
+
} catch (err) {
|
|
11245
|
+
if (err instanceof Error && err.name === "BatchPaymentUnresolvedError") throw err;
|
|
11246
|
+
if (sent) {
|
|
11247
|
+
try {
|
|
11248
|
+
return await this.raiseUnresolved(
|
|
11249
|
+
wallet,
|
|
11250
|
+
sent,
|
|
11251
|
+
{
|
|
11252
|
+
kind: "in_doubt",
|
|
11253
|
+
reason: "outcome_unknown",
|
|
11254
|
+
replayable: false,
|
|
11255
|
+
settled: false,
|
|
11256
|
+
detail: `an error after the payment was sent: ${err instanceof Error ? err.message : String(err)}`,
|
|
11257
|
+
cause: err
|
|
11258
|
+
},
|
|
11259
|
+
budget.attempt
|
|
11260
|
+
);
|
|
11261
|
+
} catch (raised) {
|
|
11262
|
+
throw withDisposition(raised, "paid-or-in-doubt");
|
|
11263
|
+
}
|
|
11264
|
+
}
|
|
11265
|
+
return fallback({ ...creationFailure(err), attempt: budget.attempt });
|
|
11266
|
+
} finally {
|
|
11267
|
+
release();
|
|
11268
|
+
}
|
|
11269
|
+
}
|
|
11270
|
+
/**
|
|
11271
|
+
* Put the wallet in a 429 cooldown for `delay` ms (`Retry-After`, or the
|
|
11272
|
+
* default backoff), and return how long this call would have to wait.
|
|
11273
|
+
*
|
|
11274
|
+
* The shared cooldown is capped at `rateLimit.maxWaitMs`, so one header,
|
|
11275
|
+
* however long, never keeps the wallet's calls off batch for longer than
|
|
11276
|
+
* the retry budget. This call still weighs the full delay: one it cannot
|
|
11277
|
+
* wait out within its budget is not waited at all, and the caller pays
|
|
11278
|
+
* exact (or raises a payment in doubt) at once.
|
|
11279
|
+
*/
|
|
11280
|
+
coolDown(wallet, delay) {
|
|
11281
|
+
const limitedAt = Date.now();
|
|
11282
|
+
wallet.cooldownUntil = Math.max(wallet.cooldownUntil, limitedAt + Math.min(delay, this.maxWaitMs));
|
|
11283
|
+
return Math.max(delay, wallet.cooldownUntil - limitedAt);
|
|
11284
|
+
}
|
|
11285
|
+
/**
|
|
11286
|
+
* Try to end a payment in doubt with the gateway's own word.
|
|
11287
|
+
*
|
|
11288
|
+
* The one source of evidence this SDK accepts today is a definitive
|
|
11289
|
+
* success receipt on the single byte-identical replay a 429 without a
|
|
11290
|
+
* receipt is allowed, after its backoff (the owner's bounded safety
|
|
11291
|
+
* attempt). Any other answer to it (a 402 or `duplicate_settlement`, which
|
|
11292
|
+
* mean the original reached the gateway; another 429; a 5xx; a timeout)
|
|
11293
|
+
* leaves it in doubt. Every other in-doubt outcome is never replayed.
|
|
11294
|
+
*
|
|
11295
|
+
* Chain state is not evidence here: it can settle a deposit (see
|
|
11296
|
+
* `resync`), never whether the gateway charged. Gateway-side evidence (a
|
|
11297
|
+
* receipt on every response, a request-status endpoint with a fence)
|
|
11298
|
+
* plugs in here, before the caller raises.
|
|
11299
|
+
*
|
|
11300
|
+
* @returns the charged replay, or the doubt to raise.
|
|
11301
|
+
*/
|
|
11302
|
+
async resolveInDoubt(wallet, sent, doubt, send, budget) {
|
|
11303
|
+
if (!doubt.replayable) return doubt;
|
|
11304
|
+
const wait = this.coolDown(wallet, doubt.retryAfterMs ?? backoffDelay(budget.attempt));
|
|
11305
|
+
if (budget.attempt >= this.maxAttempts || budget.waited + wait > this.maxWaitMs) {
|
|
11306
|
+
return {
|
|
11307
|
+
...doubt,
|
|
11308
|
+
detail: `${doubt.detail}; batch.rateLimit leaves no room for its one replay`
|
|
11309
|
+
};
|
|
11310
|
+
}
|
|
11311
|
+
await this.report({
|
|
11312
|
+
type: "backoff",
|
|
11313
|
+
reason: "rate_limited",
|
|
11314
|
+
status: 429,
|
|
11315
|
+
errorReason: doubt.errorReason,
|
|
11316
|
+
retryAfterMs: wait,
|
|
11317
|
+
attempt: budget.attempt
|
|
11318
|
+
});
|
|
11319
|
+
budget.waited += wait;
|
|
11320
|
+
await sleep2(wait);
|
|
11321
|
+
budget.attempt += 1;
|
|
11322
|
+
const replayed = await this.sendOnce(wallet, sent, send, true, true);
|
|
11323
|
+
if (replayed.kind === "charged") return replayed;
|
|
11324
|
+
return replayed.kind === "in_doubt" ? replayed : { kind: "in_doubt", reason: "replay_unresolved", replayable: false, settled: true, detail: "the replay was refused" };
|
|
11325
|
+
}
|
|
11326
|
+
/**
|
|
11327
|
+
* Raise a payment in doubt: release the scheme's hold on it (the channel
|
|
11328
|
+
* goes back to its confirmed state), put a deposit's channel in doubt so
|
|
11329
|
+
* it is re-read from the chain before anything pays into it again, report
|
|
11330
|
+
* the `unresolved` event, and throw {@link BatchPaymentUnresolvedError}.
|
|
11331
|
+
*/
|
|
11332
|
+
async raiseUnresolved(wallet, sent, doubt, attempt) {
|
|
11333
|
+
if (!doubt.settled) await this.settle(sent.http, sent.payload, () => null, 0);
|
|
11334
|
+
if (sent.kind !== "authorization") {
|
|
11335
|
+
this.distrust(
|
|
11336
|
+
wallet,
|
|
11337
|
+
sent,
|
|
11338
|
+
doubt.reason === "no_response" ? "deposit_unanswered" : doubt.reason === "replay_unresolved" ? "deposit_rate_limited" : "deposit_failed"
|
|
11339
|
+
);
|
|
11340
|
+
}
|
|
11341
|
+
await this.report({
|
|
11342
|
+
type: "unresolved",
|
|
11343
|
+
reason: doubt.reason,
|
|
11344
|
+
status: doubt.status,
|
|
11345
|
+
errorReason: doubt.errorReason,
|
|
11346
|
+
attempt,
|
|
11347
|
+
detail: `${doubt.detail}; it may have been charged, so it is not paid again`
|
|
11348
|
+
});
|
|
11349
|
+
throw new BatchPaymentUnresolvedError({
|
|
11350
|
+
reason: doubt.reason,
|
|
11351
|
+
wallet: await this.init.address(),
|
|
11352
|
+
requestId: sent.payload.payload?.authorization?.requestId,
|
|
11353
|
+
channelId: payloadChannelId(sent.payload),
|
|
11354
|
+
payloadKind: sent.kind,
|
|
11355
|
+
status: doubt.status,
|
|
11356
|
+
detail: doubt.detail,
|
|
11357
|
+
cause: doubt.cause
|
|
11358
|
+
});
|
|
11359
|
+
}
|
|
11360
|
+
/** Build a new batch payment for a 402: the payload, its headers and its kind. */
|
|
11361
|
+
async buildPayment(wallet, paymentRequired) {
|
|
11362
|
+
const { http, payload } = await this.createPayload(wallet, paymentRequired);
|
|
11363
|
+
const headers = http.encodePaymentSignatureHeader(payload);
|
|
11364
|
+
const channelId = payloadChannelId(payload);
|
|
11365
|
+
const known = channelId ? wallet.book.find(channelId) : void 0;
|
|
11366
|
+
let kind = "authorization";
|
|
11367
|
+
if (payload.payload?.type === "deposit") {
|
|
11368
|
+
const record = known?.record;
|
|
11369
|
+
kind = record && (record.pending === void 0 || record.hasConfirmedState) ? "top-up" : "open";
|
|
11370
|
+
}
|
|
11371
|
+
return { http, payload, headers, kind, key: known?.key, sentAt: Date.now(), sends: 0 };
|
|
11372
|
+
}
|
|
11373
|
+
/**
|
|
11374
|
+
* Write a deposit's intent to the wallet's journal before it is sent
|
|
11375
|
+
* (with the file store: the file and its directory fsynced).
|
|
11376
|
+
*
|
|
11377
|
+
* @throws when it cannot be recorded; the deposit must then not be sent.
|
|
11378
|
+
*/
|
|
11379
|
+
journalDeposit(wallet, sent) {
|
|
11380
|
+
const channelId = payloadChannelId(sent.payload);
|
|
11381
|
+
const record = channelId ? wallet.book.find(channelId)?.record : void 0;
|
|
11382
|
+
const { expectDeposit } = this.depositExpectation(wallet, sent.payload);
|
|
11383
|
+
if (!sent.key || !channelId || !record || expectDeposit === void 0 || sent.kind === "authorization") {
|
|
11384
|
+
throw new Error("the deposit's channel record is missing");
|
|
11385
|
+
}
|
|
11386
|
+
const hasConfirmed = record.pending === void 0 || record.hasConfirmedState;
|
|
11387
|
+
sent.expectDeposit = expectDeposit;
|
|
11388
|
+
wallet.intents.put({
|
|
11389
|
+
key: sent.key,
|
|
11390
|
+
channelId,
|
|
11391
|
+
channelConfig: record.channelConfig,
|
|
11392
|
+
requestId: sent.payload.payload?.authorization?.requestId,
|
|
11393
|
+
kind: sent.kind,
|
|
11394
|
+
cumulative: hasConfirmed ? String(parseAtomic(record.chargedCumulativeAmount)) : "0",
|
|
11395
|
+
expectDeposit: expectDeposit.toString(),
|
|
11396
|
+
...hasConfirmed ? { knownDeposit: String(parseAtomic(record.deposit)) } : {},
|
|
11397
|
+
at: Date.now()
|
|
11398
|
+
});
|
|
11399
|
+
}
|
|
11400
|
+
/**
|
|
11401
|
+
* Remove a deposit's intent once it is reconciled. A failure is logged,
|
|
11402
|
+
* not thrown: the intent then outlives its deposit, and after a restart
|
|
11403
|
+
* the chain re-read settles it again.
|
|
11404
|
+
*/
|
|
11405
|
+
forgetIntent(wallet, key) {
|
|
11406
|
+
if (!key) return;
|
|
11407
|
+
try {
|
|
11408
|
+
wallet.intents.remove(key);
|
|
11409
|
+
} catch (err) {
|
|
11410
|
+
console.error(
|
|
11411
|
+
`[@blockrun/llm] batch-settlement could not clear a deposit intent: ${JSON.stringify(err instanceof Error ? err.message : String(err))}`
|
|
11412
|
+
);
|
|
11413
|
+
}
|
|
11414
|
+
}
|
|
11415
|
+
/**
|
|
11416
|
+
* THE choke point: send a batch payment once and classify the answer.
|
|
11417
|
+
*
|
|
11418
|
+
* The only caller of `send` for a batch payment, and the only place its
|
|
11419
|
+
* outcome is decided (see {@link SendOutcome}). Every exception once the
|
|
11420
|
+
* send has started is `in_doubt`, whatever its type or `cause.code`: the
|
|
11421
|
+
* request may have reached the gateway. "Not charged" is only ever minted
|
|
11422
|
+
* here, and only for a first send.
|
|
11423
|
+
*
|
|
11424
|
+
* @param retry - count this send in `retries` (it follows this call's own 429).
|
|
11425
|
+
* @param replay - this is the one replay of a receipt-less 429: only a 2xx
|
|
11426
|
+
* with a success receipt counts, everything else stays in doubt.
|
|
11427
|
+
*/
|
|
11428
|
+
async sendOnce(wallet, sent, send, retry, replay) {
|
|
11429
|
+
if (retry) this.counters.retries += 1;
|
|
11430
|
+
sent.sends += 1;
|
|
11431
|
+
let response;
|
|
11432
|
+
try {
|
|
11433
|
+
response = await send(sent.headers);
|
|
11434
|
+
} catch (err) {
|
|
11435
|
+
return {
|
|
11436
|
+
kind: "in_doubt",
|
|
11437
|
+
reason: replay ? "replay_unresolved" : "no_response",
|
|
11438
|
+
replayable: false,
|
|
11439
|
+
settled: false,
|
|
11440
|
+
detail: `${replay ? "the replay" : "the payment"} got no answer (${err instanceof Error ? `${err.name}: ${err.message}` : String(err)})`,
|
|
11441
|
+
cause: err
|
|
11442
|
+
};
|
|
11443
|
+
}
|
|
11444
|
+
try {
|
|
11445
|
+
return await this.classify(wallet, sent, response, replay);
|
|
11446
|
+
} catch (err) {
|
|
11447
|
+
return {
|
|
11448
|
+
kind: "in_doubt",
|
|
11449
|
+
reason: replay ? "replay_unresolved" : "outcome_unknown",
|
|
11450
|
+
replayable: false,
|
|
11451
|
+
settled: false,
|
|
11452
|
+
status: response.status,
|
|
11453
|
+
detail: `the answer could not be classified (${err instanceof Error ? err.message : String(err)})`,
|
|
11454
|
+
cause: err
|
|
11455
|
+
};
|
|
11456
|
+
}
|
|
11457
|
+
}
|
|
11458
|
+
/** {@link sendOnce}'s classifier, for an HTTP answer. */
|
|
11459
|
+
async classify(wallet, sent, response, replay) {
|
|
11460
|
+
const { http, payload } = sent;
|
|
11461
|
+
const getHeader = (name) => response.headers.get(name);
|
|
11462
|
+
const receipted = Boolean(getHeader("PAYMENT-RESPONSE") || getHeader("X-PAYMENT-RESPONSE"));
|
|
11463
|
+
const status2 = response.status;
|
|
11464
|
+
if (status2 === 429 && !receipted) {
|
|
11465
|
+
return {
|
|
11466
|
+
kind: "in_doubt",
|
|
11467
|
+
reason: "replay_unresolved",
|
|
11468
|
+
replayable: !replay,
|
|
11469
|
+
settled: false,
|
|
11470
|
+
status: status2,
|
|
11471
|
+
retryAfterMs: parseRetryAfter(getHeader("retry-after")),
|
|
11472
|
+
errorReason: await rateLimitReason(response, void 0),
|
|
11473
|
+
detail: replay ? "the replay was answered 429 without a receipt" : "a 429 without a receipt"
|
|
11474
|
+
};
|
|
11475
|
+
}
|
|
11476
|
+
const channelId = payloadChannelId(payload);
|
|
11477
|
+
const before = channelId ? confirmedState(wallet.book.find(channelId)?.record) : void 0;
|
|
11478
|
+
const reconciled = await this.settle(http, payload, getHeader, status2);
|
|
11479
|
+
const receipt = reconciled.settleResponse;
|
|
11480
|
+
const definitive = receipt?.success === true && reconciled.ok && recordAdvanced(before, channelId ? confirmedState(wallet.book.find(channelId)?.record) : void 0, payload, receipt);
|
|
11481
|
+
if (!response.ok && !replay && sent.kind === "authorization" && cancelledReceipt(receipt)) {
|
|
11482
|
+
return { kind: "cancelled", status: status2, error: await cancelledError(response) };
|
|
11483
|
+
}
|
|
11484
|
+
if (response.ok && (!replay || definitive)) {
|
|
11485
|
+
if (!definitive) {
|
|
11486
|
+
this.distrust(wallet, sent, receipt ? "receipt_unreconciled" : "receipt_missing", rebuiltCharge(receipt, payload));
|
|
9832
11487
|
}
|
|
9833
|
-
|
|
9834
|
-
|
|
11488
|
+
return { kind: "charged", response, chargedUsd: chargedUsd(() => http.getPaymentSettleResponse(getHeader)) };
|
|
11489
|
+
}
|
|
11490
|
+
if (replay) {
|
|
11491
|
+
return {
|
|
11492
|
+
kind: "in_doubt",
|
|
11493
|
+
reason: "replay_unresolved",
|
|
11494
|
+
replayable: false,
|
|
11495
|
+
settled: true,
|
|
11496
|
+
status: status2,
|
|
11497
|
+
errorReason: status2 === 429 ? await rateLimitReason(response, receipt) : receipt?.errorReason,
|
|
11498
|
+
detail: `the replay was answered ${status2} with ${describeReceipt(receipt)}`,
|
|
11499
|
+
cause: await afterPaymentError(response)
|
|
11500
|
+
};
|
|
11501
|
+
}
|
|
11502
|
+
if (status2 === 429) {
|
|
11503
|
+
if (provesNothingBroadcast(receipt)) {
|
|
11504
|
+
return {
|
|
11505
|
+
kind: "not_charged",
|
|
11506
|
+
reason: "rate_limited",
|
|
11507
|
+
status: status2,
|
|
11508
|
+
errorReason: await rateLimitReason(response, receipt),
|
|
11509
|
+
rateLimited: { retryAfterMs: parseRetryAfter(getHeader("retry-after")) },
|
|
11510
|
+
// The facilitator's explicit failed receipt names no transaction: the
|
|
11511
|
+
// deposit was never broadcast (see provesNothingBroadcast).
|
|
11512
|
+
depositSafe: true
|
|
11513
|
+
};
|
|
11514
|
+
}
|
|
11515
|
+
return {
|
|
11516
|
+
kind: "in_doubt",
|
|
11517
|
+
reason: "ambiguous_rate_limit",
|
|
11518
|
+
replayable: false,
|
|
11519
|
+
settled: true,
|
|
11520
|
+
status: status2,
|
|
11521
|
+
errorReason: await rateLimitReason(response, receipt),
|
|
11522
|
+
detail: `a 429 whose ${describeReceipt(receipt)} does not prove nothing was broadcast`,
|
|
11523
|
+
cause: await afterPaymentError(response)
|
|
11524
|
+
};
|
|
11525
|
+
}
|
|
11526
|
+
if (cleanRefusalReceipt(receipt)) {
|
|
11527
|
+
if (status2 === 402) {
|
|
11528
|
+
const duplicate = await duplicateAnswer(response);
|
|
11529
|
+
if (duplicate) {
|
|
11530
|
+
return {
|
|
11531
|
+
kind: "in_doubt",
|
|
11532
|
+
reason: "duplicate_settlement",
|
|
11533
|
+
replayable: false,
|
|
11534
|
+
settled: true,
|
|
11535
|
+
status: status2,
|
|
11536
|
+
errorReason: duplicate,
|
|
11537
|
+
detail: `the payment was answered 402 ${duplicate}: another copy of this request reached the gateway`,
|
|
11538
|
+
cause: await afterPaymentError(response)
|
|
11539
|
+
};
|
|
11540
|
+
}
|
|
11541
|
+
return { kind: "not_charged", reason: "payment_required", status: status2, errorReason: receipt?.errorReason, depositSafe: provesNothingBroadcast(receipt) };
|
|
11542
|
+
}
|
|
11543
|
+
const refusal = await batchRefusal(response);
|
|
11544
|
+
if (refusal) {
|
|
11545
|
+
return { kind: "not_charged", reason: refusal, status: status2, errorReason: refusal, depositSafe: PRE_REQUEST_REFUSALS.has(refusal) };
|
|
11546
|
+
}
|
|
11547
|
+
}
|
|
11548
|
+
return {
|
|
11549
|
+
kind: "in_doubt",
|
|
11550
|
+
reason: "outcome_unknown",
|
|
11551
|
+
replayable: false,
|
|
11552
|
+
settled: true,
|
|
11553
|
+
status: status2,
|
|
11554
|
+
errorReason: receipt?.errorReason,
|
|
11555
|
+
detail: `the payment was answered ${status2} with ${describeReceipt(receipt)}`,
|
|
11556
|
+
cause: await afterPaymentError(response)
|
|
11557
|
+
};
|
|
11558
|
+
}
|
|
11559
|
+
/**
|
|
11560
|
+
* Build the payment payload, re-reading any distrusted channel from the
|
|
11561
|
+
* chain first. A stored record the book refuses to hand over (a deposit a
|
|
11562
|
+
* dead process never heard back about), or one a top-up's read found
|
|
11563
|
+
* unusable ({@link prepareTopUp}), is re-read and the payload rebuilt
|
|
11564
|
+
* once.
|
|
11565
|
+
*/
|
|
11566
|
+
async createPayload(wallet, paymentRequired) {
|
|
11567
|
+
for (let pass = 0; ; pass += 1) {
|
|
11568
|
+
await this.resync(wallet);
|
|
11569
|
+
const http = (await this.build(wallet)).http;
|
|
9835
11570
|
try {
|
|
9836
|
-
|
|
11571
|
+
await this.prepareTopUp(wallet, paymentRequired);
|
|
11572
|
+
return { http, payload: await http.createPaymentPayload(withoutClientSignedBatch(paymentRequired)) };
|
|
9837
11573
|
} catch (err) {
|
|
9838
|
-
if (
|
|
9839
|
-
|
|
9840
|
-
|
|
11574
|
+
if (pass > 0 || !isResyncRequired(err) || wallet.resyncs.size === 0) throw err;
|
|
11575
|
+
} finally {
|
|
11576
|
+
wallet.topUp = void 0;
|
|
9841
11577
|
}
|
|
9842
|
-
|
|
9843
|
-
|
|
9844
|
-
|
|
9845
|
-
|
|
9846
|
-
|
|
9847
|
-
|
|
11578
|
+
}
|
|
11579
|
+
}
|
|
11580
|
+
/**
|
|
11581
|
+
* When this payment will top up the wallet's channel, read how much of its
|
|
11582
|
+
* deposit is already settled on-chain (at `finalized` commitment, so never
|
|
11583
|
+
* more than will stay settled), so `maxDeposit` caps the escrow still at
|
|
11584
|
+
* stake rather than every deposit the channel ever took.
|
|
11585
|
+
*
|
|
11586
|
+
* Only a top-up costs this read (a channel's balance decides it, from the
|
|
11587
|
+
* stored record). If the read fails, nothing settled is assumed, which is
|
|
11588
|
+
* the stricter, lifetime cap. If the chain shows a larger deposit than the
|
|
11589
|
+
* record, the record is behind (a deposit it never heard about landed):
|
|
11590
|
+
* sizing a top-up from it could exceed `maxDeposit`, so this call pays
|
|
11591
|
+
* exact and the channel is re-read before the next payment. If
|
|
11592
|
+
* `@x402/svm`'s channel layout is not the one this SDK decodes, or the
|
|
11593
|
+
* account at the channel's address cannot be read as a channel, the call
|
|
11594
|
+
* pays exact (`channel_unreadable`), record kept. If
|
|
11595
|
+
* the finalized channel is closing or closed, or is not this wallet's for
|
|
11596
|
+
* the record's operator and mint, no top-up is sized from the record: it
|
|
11597
|
+
* is re-read at once (`channel_unusable`), see {@link createPayload}.
|
|
11598
|
+
*/
|
|
11599
|
+
async prepareTopUp(wallet, paymentRequired) {
|
|
11600
|
+
wallet.topUp = void 0;
|
|
11601
|
+
const accept = trustedBatchAccept(paymentRequired, this.init.options.operators);
|
|
11602
|
+
if (!accept) return;
|
|
11603
|
+
const key = channelKeyOf(accept);
|
|
11604
|
+
const record = key ? await wallet.book.get(key) : void 0;
|
|
11605
|
+
if (!key || !record || record.pending || typeof record.channelId !== "string") return;
|
|
11606
|
+
const deposit = parseAtomic(record.deposit);
|
|
11607
|
+
if (parseAtomic(record.chargedCumulativeAmount) + parseAtomic(accept.amount) <= deposit) return;
|
|
11608
|
+
let channel;
|
|
11609
|
+
try {
|
|
11610
|
+
channel = await readChannelAccount(this.init.rpcUrl, record.channelId, this.init.rpcHeaders);
|
|
11611
|
+
} catch (err) {
|
|
11612
|
+
if (err instanceof ChannelLayoutError) {
|
|
11613
|
+
throw new ResyncError("channel_unreadable", `channel ${record.channelId}: ${err.message}; no top-up is sized from it`);
|
|
9848
11614
|
}
|
|
9849
|
-
|
|
9850
|
-
|
|
9851
|
-
this.warn(wallet, refusal);
|
|
9852
|
-
return { kind: "fallback", reason: refusal };
|
|
11615
|
+
if (err instanceof ChannelUnreadableError) {
|
|
11616
|
+
throw new ResyncError("channel_unreadable", `${err.message}; no top-up is signed into it`);
|
|
9853
11617
|
}
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
|
|
11618
|
+
throw new ResyncError(
|
|
11619
|
+
"channel_resync_failed",
|
|
11620
|
+
`could not read channel ${record.channelId} before topping it up: ${err instanceof Error ? err.message : String(err)}`
|
|
11621
|
+
);
|
|
9857
11622
|
}
|
|
11623
|
+
if (channel === void 0) return;
|
|
11624
|
+
if (!channel.open || channel.payer !== await this.init.address() || channel.authorizedSigner !== record.channelConfig.payerAuthorizer || channel.mint !== record.channelConfig.token) {
|
|
11625
|
+
addResyncTarget(wallet, {
|
|
11626
|
+
key,
|
|
11627
|
+
channelId: record.channelId,
|
|
11628
|
+
channelConfig: record.channelConfig,
|
|
11629
|
+
cumulative: parseAtomic(record.chargedCumulativeAmount),
|
|
11630
|
+
knownDeposit: deposit,
|
|
11631
|
+
reason: "channel_unusable"
|
|
11632
|
+
});
|
|
11633
|
+
throw new ChannelResyncRequiredError(record.channelId);
|
|
11634
|
+
}
|
|
11635
|
+
if (channel.deposit > deposit) {
|
|
11636
|
+
addResyncTarget(wallet, {
|
|
11637
|
+
key,
|
|
11638
|
+
channelId: record.channelId,
|
|
11639
|
+
channelConfig: record.channelConfig,
|
|
11640
|
+
cumulative: parseAtomic(record.chargedCumulativeAmount),
|
|
11641
|
+
knownDeposit: channel.deposit,
|
|
11642
|
+
reason: "deposit_unrecorded"
|
|
11643
|
+
});
|
|
11644
|
+
throw new ResyncError(
|
|
11645
|
+
"channel_resync_pending",
|
|
11646
|
+
`channel ${record.channelId} holds ${channel.deposit} on chain, more than the ${deposit} its record knows; it is re-read before any top-up`
|
|
11647
|
+
);
|
|
11648
|
+
}
|
|
11649
|
+
wallet.topUp = { key, deposit, settled: channel.settled };
|
|
9858
11650
|
}
|
|
9859
11651
|
/**
|
|
9860
11652
|
* Hand the gateway's answer to the scheme, which verifies the operator's
|
|
@@ -9865,36 +11657,143 @@ var SolanaBatchPayer = class {
|
|
|
9865
11657
|
*/
|
|
9866
11658
|
async settle(http, payload, getHeader, status2) {
|
|
9867
11659
|
try {
|
|
9868
|
-
return (await http.processPaymentResult(payload, getHeader, status2)).settleResponse;
|
|
11660
|
+
return { settleResponse: (await http.processPaymentResult(payload, getHeader, status2)).settleResponse, ok: true };
|
|
9869
11661
|
} catch (err) {
|
|
9870
11662
|
console.error(
|
|
9871
|
-
`[@blockrun/llm] batch-settlement receipt not reconciled: ${err instanceof Error ? err.message : String(err)}`
|
|
11663
|
+
`[@blockrun/llm] batch-settlement receipt not reconciled: ${JSON.stringify(err instanceof Error ? err.message : String(err))}`
|
|
9872
11664
|
);
|
|
9873
|
-
|
|
11665
|
+
let settleResponse;
|
|
11666
|
+
try {
|
|
11667
|
+
settleResponse = http.getPaymentSettleResponse(getHeader);
|
|
11668
|
+
} catch {
|
|
11669
|
+
settleResponse = void 0;
|
|
11670
|
+
}
|
|
11671
|
+
return { settleResponse, ok: false };
|
|
9874
11672
|
}
|
|
9875
11673
|
}
|
|
9876
11674
|
/**
|
|
9877
|
-
* Close the channel
|
|
11675
|
+
* Close the channel the gateway's current challenge names and return its
|
|
11676
|
+
* unused escrow to the wallet.
|
|
9878
11677
|
*
|
|
9879
11678
|
* The scheme keeps a closed channel in its records, and would go on paying
|
|
9880
|
-
* into it, so a successful close forgets
|
|
9881
|
-
* finds no open channel on-chain and opens a new one.
|
|
11679
|
+
* into it, so a successful close forgets that channel. The next batch call
|
|
11680
|
+
* finds no open channel on-chain and opens a new one. Any other channel
|
|
11681
|
+
* (say, one opened with an operator key that is being rotated out) keeps
|
|
11682
|
+
* its record, so it can still be closed without an on-chain scan. A
|
|
11683
|
+
* client-signed channel an earlier SDK version opened is closed too, once
|
|
11684
|
+
* the trusted server-signed one has no record (see {@link refundTarget}).
|
|
9882
11685
|
*
|
|
9883
11686
|
* @param url - any batch-enabled route on the gateway the channel was opened with.
|
|
11687
|
+
* @throws BatchCloseDeferredError while a batch call for the wallet is in
|
|
11688
|
+
* flight, or while any of its deposits is in doubt (until a finalized
|
|
11689
|
+
* chain read settles it). Nothing is refunded or forgotten then.
|
|
9884
11690
|
*/
|
|
9885
11691
|
async close(url) {
|
|
9886
11692
|
const wallet = await this.wallet();
|
|
9887
|
-
if (
|
|
9888
|
-
if (wallet.busy
|
|
11693
|
+
if ("reason" in wallet) throw new Error(`batch-settlement unavailable: ${wallet.detail}`);
|
|
11694
|
+
if (wallet.busy || wallet.active > 0) {
|
|
11695
|
+
throw new BatchCloseDeferredError({
|
|
11696
|
+
reason: "call_in_flight",
|
|
11697
|
+
wallet: await this.init.address(),
|
|
11698
|
+
detail: "a batch call is in flight (it may be waiting out a 429); close the channel when it returns"
|
|
11699
|
+
});
|
|
11700
|
+
}
|
|
9889
11701
|
wallet.busy = true;
|
|
9890
11702
|
try {
|
|
9891
|
-
|
|
9892
|
-
await this.
|
|
11703
|
+
await this.settleDoubtsForClose(wallet);
|
|
11704
|
+
const { accept, key } = await this.refundTarget(wallet, url);
|
|
11705
|
+
const result = await (await this.prepareClose(wallet, key)).refund(url, accept);
|
|
11706
|
+
wallet.closes += 1;
|
|
11707
|
+
await this.forget(wallet, key);
|
|
9893
11708
|
return result;
|
|
9894
11709
|
} finally {
|
|
9895
11710
|
wallet.busy = false;
|
|
9896
11711
|
}
|
|
9897
11712
|
}
|
|
11713
|
+
/**
|
|
11714
|
+
* Re-read every distrusted channel before a close. A deposit still in
|
|
11715
|
+
* doubt defers the close: refunding (or forgetting) a channel a deposit
|
|
11716
|
+
* may yet land on could strand that deposit, and after a restart nothing
|
|
11717
|
+
* would remember it. A record re-read that only failed for another reason
|
|
11718
|
+
* (an RPC error on a channel with no deposit in doubt) does not.
|
|
11719
|
+
*/
|
|
11720
|
+
async settleDoubtsForClose(wallet) {
|
|
11721
|
+
try {
|
|
11722
|
+
await this.resync(wallet);
|
|
11723
|
+
} catch (err) {
|
|
11724
|
+
if (!(err instanceof ResyncError)) throw err;
|
|
11725
|
+
const inDoubt = [...wallet.resyncs.values()].filter((target) => target.expectDeposit !== void 0);
|
|
11726
|
+
if (inDoubt.length > 0) {
|
|
11727
|
+
throw new BatchCloseDeferredError({
|
|
11728
|
+
reason: "deposit_in_doubt",
|
|
11729
|
+
wallet: await this.init.address(),
|
|
11730
|
+
channelIds: inDoubt.map((target) => target.channelId),
|
|
11731
|
+
detail: `a deposit on channel ${inDoubt.map((target) => target.channelId).join(", ")} is in doubt (${err.message}); close once a finalized chain read settles it`
|
|
11732
|
+
});
|
|
11733
|
+
}
|
|
11734
|
+
}
|
|
11735
|
+
}
|
|
11736
|
+
/**
|
|
11737
|
+
* The channel a close targets: the one the gateway's refund challenge
|
|
11738
|
+
* names, by the scheme's storage key for its trusted server-signed accept
|
|
11739
|
+
* (network, asset, receiver, fee payer, withdraw delay, receiver
|
|
11740
|
+
* authorizer, operator). Probed with an unpaid GET, as `@x402/svm`'s own
|
|
11741
|
+
* refund probes; that accept is then handed to the refund, so upstream
|
|
11742
|
+
* does not probe again and pick another accept.
|
|
11743
|
+
*
|
|
11744
|
+
* A legacy client-signed channel (opened by an earlier SDK version, which
|
|
11745
|
+
* could pay a client-signed accept; this one never opens one) must stay
|
|
11746
|
+
* refundable. When the trusted server-signed accept has no stored record,
|
|
11747
|
+
* or there is none, a client-signed accept in the challenge whose storage
|
|
11748
|
+
* key names a stored client-signed record is the target instead, and the
|
|
11749
|
+
* refund runs through `@x402/svm`'s client-signed path (a refund voucher
|
|
11750
|
+
* signed by the payer at its confirmed cumulative). The signer modes match:
|
|
11751
|
+
* a server-signed record is never refunded under a client-signed accept.
|
|
11752
|
+
*/
|
|
11753
|
+
async refundTarget(wallet, url) {
|
|
11754
|
+
const probe = await fetch(url, { method: "GET" });
|
|
11755
|
+
const header = probe.status === 402 ? probe.headers.get("PAYMENT-REQUIRED") : null;
|
|
11756
|
+
if (!header) throw new Error(`refund probe expected a 402 with PAYMENT-REQUIRED from ${url}, got HTTP ${probe.status}`);
|
|
11757
|
+
const paymentRequired = parsePaymentRequired(header);
|
|
11758
|
+
const trustedAccepts = trustedBatchAccepts(paymentRequired, this.init.options.operators).map((accept) => ({ accept, key: channelKeyOf(accept) })).filter((candidate) => !!candidate.key);
|
|
11759
|
+
for (const candidate of trustedAccepts) {
|
|
11760
|
+
if (await wallet.book.base.get(candidate.key)) return candidate;
|
|
11761
|
+
}
|
|
11762
|
+
for (const accept of (paymentRequired.accepts ?? []).filter(clientSignedBatch)) {
|
|
11763
|
+
const key = channelKeyOf(accept);
|
|
11764
|
+
const record = key ? await wallet.book.base.get(key) : void 0;
|
|
11765
|
+
if (key && record && (record.channelConfig?.voucherSigner ?? "client") === "client") return { accept, key };
|
|
11766
|
+
}
|
|
11767
|
+
const first = trustedAccepts[0];
|
|
11768
|
+
if (!first) throw new Error(`${url} offers no batch-settlement accept with a trusted operator to close against`);
|
|
11769
|
+
return first;
|
|
11770
|
+
}
|
|
11771
|
+
/**
|
|
11772
|
+
* Get the stored channel `key` ready to refund.
|
|
11773
|
+
*
|
|
11774
|
+
* `@x402/svm` refunds a server-signed channel only from the scheme's memory,
|
|
11775
|
+
* or after an on-chain scan that many RPCs refuse; it never reads the
|
|
11776
|
+
* stored record. So the target channel is loaded into a freshly built
|
|
11777
|
+
* scheme, and only that one: upstream's refund falls back to the first
|
|
11778
|
+
* channel in memory for the same receiver and asset, whatever its operator.
|
|
11779
|
+
* A record in doubt (a deposit this process never confirmed, or a pending
|
|
11780
|
+
* deposit left by a process that died) is re-read from the chain before it
|
|
11781
|
+
* is loaded. If a re-read cannot settle a record, the refund is still
|
|
11782
|
+
* attempted and may find the channel by its scan.
|
|
11783
|
+
*/
|
|
11784
|
+
async prepareClose(wallet, key) {
|
|
11785
|
+
for (let pass = 0; ; pass += 1) {
|
|
11786
|
+
await this.settleDoubtsForClose(wallet);
|
|
11787
|
+
wallet.client = void 0;
|
|
11788
|
+
const built = await this.build(wallet);
|
|
11789
|
+
try {
|
|
11790
|
+
await built.load(key);
|
|
11791
|
+
return built;
|
|
11792
|
+
} catch (err) {
|
|
11793
|
+
if (pass > 0 || !isResyncRequired(err)) return built;
|
|
11794
|
+
}
|
|
11795
|
+
}
|
|
11796
|
+
}
|
|
9898
11797
|
};
|
|
9899
11798
|
|
|
9900
11799
|
// src/solana-client.ts
|
|
@@ -9963,6 +11862,18 @@ async function waitForStaleRetry(attempt) {
|
|
|
9963
11862
|
(resolve2) => setTimeout(resolve2, STALE_BLOCKHASH_RETRY_BACKOFFS_MS[attempt])
|
|
9964
11863
|
);
|
|
9965
11864
|
}
|
|
11865
|
+
async function unpaidApiError(response) {
|
|
11866
|
+
let errorBody;
|
|
11867
|
+
try {
|
|
11868
|
+
errorBody = await response.json();
|
|
11869
|
+
} catch {
|
|
11870
|
+
errorBody = { error: "Request failed" };
|
|
11871
|
+
}
|
|
11872
|
+
return withDisposition(
|
|
11873
|
+
new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody)),
|
|
11874
|
+
"unpaid"
|
|
11875
|
+
);
|
|
11876
|
+
}
|
|
9966
11877
|
var DEFAULT_SOLANA_RPC_URL = "https://sol.blockrun.ai/api/v1/solana/rpc";
|
|
9967
11878
|
function resolveRpcConfig(rpcUrl, rpcHeaders) {
|
|
9968
11879
|
const env = typeof process !== "undefined" && process.env ? process.env : {};
|
|
@@ -10029,21 +11940,42 @@ var SolanaLLMClient = class {
|
|
|
10029
11940
|
options: options.batch,
|
|
10030
11941
|
secretKey: () => solanaKeyToBytes(this.privateKey),
|
|
10031
11942
|
address: () => this.getWalletAddress(),
|
|
10032
|
-
rpcUrl: this.rpcUrl
|
|
11943
|
+
rpcUrl: this.rpcUrl,
|
|
11944
|
+
rpcHeaders: this.rpcHeaders
|
|
10033
11945
|
});
|
|
10034
11946
|
}
|
|
10035
11947
|
}
|
|
10036
11948
|
/**
|
|
10037
11949
|
* Close this wallet's batch-settlement channel and return its unused escrow.
|
|
10038
11950
|
*
|
|
10039
|
-
*
|
|
10040
|
-
*
|
|
10041
|
-
*
|
|
11951
|
+
* Closes the channel for the operator the gateway's current 402 names; a
|
|
11952
|
+
* channel opened with another trusted operator key (during a key rotation)
|
|
11953
|
+
* keeps its record. The gateway closes it cooperatively when it can;
|
|
11954
|
+
* otherwise this starts a payer-forced close and the escrow comes back
|
|
11955
|
+
* after the channel's grace period. Only meaningful with the `batch`
|
|
11956
|
+
* option.
|
|
11957
|
+
*
|
|
11958
|
+
* @throws BatchCloseDeferredError, closing nothing, while a chat call for
|
|
11959
|
+
* this wallet is still in flight (including one waiting out a 429, so a
|
|
11960
|
+
* retry can never reopen a channel you just closed), or while one of its
|
|
11961
|
+
* deposits is in doubt (until a `finalized` chain read shows it landed or
|
|
11962
|
+
* that it no longer can, usually a few minutes after it was sent). Call
|
|
11963
|
+
* it again later.
|
|
10042
11964
|
*/
|
|
10043
11965
|
async closeBatchChannel() {
|
|
10044
11966
|
if (!this.batchPayer) throw new Error("closeBatchChannel() requires the `batch` option in wallet mode");
|
|
10045
11967
|
return this.batchPayer.close(`${this.apiUrl}/v1/chat/completions`);
|
|
10046
11968
|
}
|
|
11969
|
+
/**
|
|
11970
|
+
* This client's batch-settlement counters: calls that fell back to `exact`
|
|
11971
|
+
* (in total and by reason), 429 backoffs, batch retries, and recoveries.
|
|
11972
|
+
* Every one of those is also logged to stderr and passed to `batch.onEvent`.
|
|
11973
|
+
* Only meaningful with the `batch` option.
|
|
11974
|
+
*/
|
|
11975
|
+
getBatchStats() {
|
|
11976
|
+
if (!this.batchPayer) throw new Error("getBatchStats() requires the `batch` option in wallet mode");
|
|
11977
|
+
return this.batchPayer.stats();
|
|
11978
|
+
}
|
|
10047
11979
|
/** Get Solana wallet address (public key in base58). */
|
|
10048
11980
|
async getWalletAddress() {
|
|
10049
11981
|
if (!this.addressCache) {
|
|
@@ -10567,17 +12499,34 @@ var SolanaLLMClient = class {
|
|
|
10567
12499
|
isSolana() {
|
|
10568
12500
|
return this.apiUrl.includes("sol.blockrun.ai");
|
|
10569
12501
|
}
|
|
12502
|
+
/**
|
|
12503
|
+
* Chat request with automatic x402 payment.
|
|
12504
|
+
*
|
|
12505
|
+
* Every error it throws carries a {@link RetryDisposition}, which is what
|
|
12506
|
+
* {@link SolanaLLMClient.chatCompletion}'s fallback walk honours: errors of
|
|
12507
|
+
* the unpaid first request, its 402 challenge and the signing step are
|
|
12508
|
+
* `"unpaid"`; anything after a payment was sent (exact or batch) is
|
|
12509
|
+
* `"paid-or-in-doubt"`, and so is any error with an API key once the billed
|
|
12510
|
+
* request was sent, except the account API's explicit 4xx refusal.
|
|
12511
|
+
*/
|
|
10570
12512
|
async requestWithPayment(endpoint, body) {
|
|
10571
12513
|
const url = `${this.apiUrl}${endpoint}`;
|
|
12514
|
+
const closesAtStart = await this.batchPayer?.closeFence();
|
|
10572
12515
|
for (let staleRetries = 0; ; ) {
|
|
10573
|
-
const response = await this.
|
|
12516
|
+
const response = await this.sendUnpaid(url, {
|
|
10574
12517
|
method: "POST",
|
|
10575
12518
|
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
|
|
10576
12519
|
body: JSON.stringify(body)
|
|
10577
12520
|
});
|
|
10578
12521
|
if (response.status === 402) {
|
|
10579
12522
|
try {
|
|
10580
|
-
|
|
12523
|
+
let paymentRequired;
|
|
12524
|
+
try {
|
|
12525
|
+
paymentRequired = await this.readPaymentRequired(response);
|
|
12526
|
+
} catch (error) {
|
|
12527
|
+
throw withDisposition(error, "unpaid");
|
|
12528
|
+
}
|
|
12529
|
+
let exactRequired = paymentRequired;
|
|
10581
12530
|
if (this.batchPayer && staleRetries === 0) {
|
|
10582
12531
|
const batch = await this.batchPayer.pay(
|
|
10583
12532
|
paymentRequired,
|
|
@@ -10585,39 +12534,75 @@ var SolanaLLMClient = class {
|
|
|
10585
12534
|
method: "POST",
|
|
10586
12535
|
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT, ...paymentHeaders },
|
|
10587
12536
|
body: JSON.stringify(body)
|
|
10588
|
-
})
|
|
12537
|
+
}),
|
|
12538
|
+
// A fresh, unpaid challenge after a 429 wait: the request itself,
|
|
12539
|
+
// sent again without a payment. Every error it throws is unpaid.
|
|
12540
|
+
async () => {
|
|
12541
|
+
const challenge = await this.sendUnpaid(url, {
|
|
12542
|
+
method: "POST",
|
|
12543
|
+
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
|
|
12544
|
+
body: JSON.stringify(body)
|
|
12545
|
+
});
|
|
12546
|
+
if (challenge.status === 402) {
|
|
12547
|
+
try {
|
|
12548
|
+
return { kind: "challenge", paymentRequired: await this.readPaymentRequired(challenge) };
|
|
12549
|
+
} catch (error) {
|
|
12550
|
+
throw withDisposition(error, "unpaid");
|
|
12551
|
+
}
|
|
12552
|
+
}
|
|
12553
|
+
if (challenge.ok) return { kind: "served", response: challenge };
|
|
12554
|
+
throw await unpaidApiError(challenge);
|
|
12555
|
+
},
|
|
12556
|
+
closesAtStart
|
|
10589
12557
|
);
|
|
10590
12558
|
if (batch.kind === "paid") {
|
|
10591
12559
|
this.recordSettlement(batch.chargedUsd);
|
|
10592
|
-
return batch.response
|
|
10593
|
-
}
|
|
10594
|
-
if (batch.kind === "failed") {
|
|
10595
|
-
let errorBody;
|
|
10596
|
-
try {
|
|
10597
|
-
errorBody = await batch.response.json();
|
|
10598
|
-
} catch {
|
|
10599
|
-
errorBody = { error: "Request failed" };
|
|
10600
|
-
}
|
|
10601
|
-
throw new APIError(`API error after payment: ${batch.response.status}`, batch.response.status, sanitizeErrorResponse(errorBody));
|
|
12560
|
+
return await this.readPaidJson(batch.response);
|
|
10602
12561
|
}
|
|
12562
|
+
if (batch.kind === "served") return await this.readUnpaidJson(batch.response);
|
|
12563
|
+
if (batch.kind === "failed") throw batch.error;
|
|
12564
|
+
exactRequired = batch.paymentRequired ?? paymentRequired;
|
|
10603
12565
|
}
|
|
10604
|
-
return await this.handlePaymentAndRetry(url, body,
|
|
12566
|
+
return await this.handlePaymentAndRetry(url, body, exactRequired, staleRetries > 0);
|
|
10605
12567
|
} catch (error) {
|
|
10606
12568
|
if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
|
|
10607
12569
|
await waitForStaleRetry(staleRetries++);
|
|
10608
12570
|
continue;
|
|
10609
12571
|
}
|
|
10610
12572
|
}
|
|
10611
|
-
if (!response.ok)
|
|
10612
|
-
|
|
10613
|
-
|
|
10614
|
-
|
|
10615
|
-
|
|
10616
|
-
|
|
10617
|
-
|
|
10618
|
-
|
|
10619
|
-
|
|
10620
|
-
|
|
12573
|
+
if (!response.ok) throw await unpaidApiError(response);
|
|
12574
|
+
return await this.readUnpaidJson(response);
|
|
12575
|
+
}
|
|
12576
|
+
}
|
|
12577
|
+
/**
|
|
12578
|
+
* Read the JSON body of a 2xx served without a payment: the free tier, or
|
|
12579
|
+
* with an API key, where this very request was the billed one.
|
|
12580
|
+
*/
|
|
12581
|
+
async readUnpaidJson(response) {
|
|
12582
|
+
try {
|
|
12583
|
+
return await response.json();
|
|
12584
|
+
} catch (error) {
|
|
12585
|
+
throw withDisposition(error, this.apiAuth ? "paid-or-in-doubt" : "unpaid");
|
|
12586
|
+
}
|
|
12587
|
+
}
|
|
12588
|
+
/**
|
|
12589
|
+
* Send a request that carries no payment, marking what it throws with its
|
|
12590
|
+
* {@link RetryDisposition}: `"unpaid"` with a wallet; with an API key, see
|
|
12591
|
+
* {@link accountErrorDisposition} (the request itself is billed).
|
|
12592
|
+
*/
|
|
12593
|
+
async sendUnpaid(url, init) {
|
|
12594
|
+
try {
|
|
12595
|
+
return await this.fetchWithTimeout(url, init);
|
|
12596
|
+
} catch (error) {
|
|
12597
|
+
throw withDisposition(error, this.apiAuth ? accountErrorDisposition(error) : "unpaid");
|
|
12598
|
+
}
|
|
12599
|
+
}
|
|
12600
|
+
/** Read a paid 2xx response's JSON body. The call was charged, so a failure here is never retried. */
|
|
12601
|
+
async readPaidJson(response) {
|
|
12602
|
+
try {
|
|
12603
|
+
return await response.json();
|
|
12604
|
+
} catch (error) {
|
|
12605
|
+
throw withDisposition(error, "paid-or-in-doubt");
|
|
10621
12606
|
}
|
|
10622
12607
|
}
|
|
10623
12608
|
/**
|
|
@@ -10785,24 +12770,30 @@ var SolanaLLMClient = class {
|
|
|
10785
12770
|
return { paymentPayload, costUsd: parseFloat(details.amount) / 1e6 };
|
|
10786
12771
|
}
|
|
10787
12772
|
async handlePaymentAndRetry(url, body, paymentRequired, forceFreshBlockhash = false) {
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
paymentRequired,
|
|
10791
|
-
|
|
10792
|
-
|
|
10793
|
-
|
|
10794
|
-
const
|
|
10795
|
-
|
|
10796
|
-
|
|
10797
|
-
|
|
10798
|
-
"
|
|
10799
|
-
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
|
|
12773
|
+
let signed;
|
|
12774
|
+
try {
|
|
12775
|
+
signed = await this.signExactPayment(url, paymentRequired, forceFreshBlockhash, `${this.apiUrl}/v1/chat/completions`);
|
|
12776
|
+
} catch (error) {
|
|
12777
|
+
throw withDisposition(error, "unpaid");
|
|
12778
|
+
}
|
|
12779
|
+
const { paymentPayload, costUsd } = signed;
|
|
12780
|
+
let retryResponse;
|
|
12781
|
+
try {
|
|
12782
|
+
retryResponse = await this.fetchWithTimeout(url, {
|
|
12783
|
+
method: "POST",
|
|
12784
|
+
headers: {
|
|
12785
|
+
"Content-Type": "application/json",
|
|
12786
|
+
"User-Agent": USER_AGENT,
|
|
12787
|
+
"PAYMENT-SIGNATURE": paymentPayload
|
|
12788
|
+
},
|
|
12789
|
+
body: JSON.stringify(body)
|
|
12790
|
+
});
|
|
12791
|
+
} catch (error) {
|
|
12792
|
+
throw withDisposition(error, "paid-or-in-doubt");
|
|
12793
|
+
}
|
|
10803
12794
|
await this.assertPaid(retryResponse);
|
|
10804
12795
|
this.recordSettlement(costUsd);
|
|
10805
|
-
return
|
|
12796
|
+
return this.readPaidJson(retryResponse);
|
|
10806
12797
|
}
|
|
10807
12798
|
async requestWithPaymentRaw(endpoint, body) {
|
|
10808
12799
|
const url = `${this.apiUrl}${endpoint}`;
|
|
@@ -10909,14 +12900,15 @@ var SolanaLLMClient = class {
|
|
|
10909
12900
|
* lives.
|
|
10910
12901
|
* @param response - the reply to the paid request.
|
|
10911
12902
|
* @throws SafeStaleBlockhashError when the caller should re-sign, PaymentError
|
|
10912
|
-
* when it should not, APIError for any other failure.
|
|
12903
|
+
* when it should not, APIError for any other failure. Every one but the
|
|
12904
|
+
* first is marked `"paid-or-in-doubt"`: the payment may have settled.
|
|
10913
12905
|
*/
|
|
10914
12906
|
async assertPaid(response) {
|
|
10915
12907
|
if (response.status === 402) {
|
|
10916
12908
|
if (await isSafeStaleBlockhashResponse(response)) {
|
|
10917
|
-
throw new SafeStaleBlockhashError();
|
|
12909
|
+
throw withDisposition(new SafeStaleBlockhashError(), "unpaid");
|
|
10918
12910
|
}
|
|
10919
|
-
throw new PaymentError("Payment was rejected. Check your Solana USDC balance.");
|
|
12911
|
+
throw withDisposition(new PaymentError("Payment was rejected. Check your Solana USDC balance."), "paid-or-in-doubt");
|
|
10920
12912
|
}
|
|
10921
12913
|
if (!response.ok) {
|
|
10922
12914
|
let errorBody;
|
|
@@ -10925,7 +12917,11 @@ var SolanaLLMClient = class {
|
|
|
10925
12917
|
} catch {
|
|
10926
12918
|
errorBody = { error: "Request failed" };
|
|
10927
12919
|
}
|
|
10928
|
-
|
|
12920
|
+
const unverified = response.status === 503 && errorBody?.code === "PAYMENT_VERIFICATION_UNAVAILABLE";
|
|
12921
|
+
throw withDisposition(
|
|
12922
|
+
new APIError(`API error after payment: ${response.status}`, response.status, sanitizeErrorResponse(errorBody)),
|
|
12923
|
+
unverified ? "unpaid" : "paid-or-in-doubt"
|
|
12924
|
+
);
|
|
10929
12925
|
}
|
|
10930
12926
|
}
|
|
10931
12927
|
/** Count one settled x402 payment against the session total. */
|
|
@@ -11396,8 +13392,12 @@ var AnthropicClient = class {
|
|
|
11396
13392
|
baseURL: this._apiUrl,
|
|
11397
13393
|
apiKey: "blockrun",
|
|
11398
13394
|
fetch: this._x402Fetch.bind(this),
|
|
11399
|
-
//
|
|
11400
|
-
|
|
13395
|
+
// Never let the Anthropic SDK retry. It retries 408/409/429/5xx and
|
|
13396
|
+
// connection errors by calling this fetch again, which in wallet mode
|
|
13397
|
+
// runs the whole x402 flow and signs a NEW payment each time (three
|
|
13398
|
+
// payments for one call at the default maxRetries of 2); in account
|
|
13399
|
+
// mode the POST itself may already be billed.
|
|
13400
|
+
maxRetries: 0
|
|
11401
13401
|
});
|
|
11402
13402
|
return this._client;
|
|
11403
13403
|
})();
|
|
@@ -11492,8 +13492,11 @@ var AnthropicClient = class {
|
|
|
11492
13492
|
AnthropicClient,
|
|
11493
13493
|
BASE_CHAIN_ID,
|
|
11494
13494
|
BLOCKRUN_SOL_OPERATOR,
|
|
13495
|
+
BatchCloseDeferredError,
|
|
13496
|
+
BatchPaymentUnresolvedError,
|
|
11495
13497
|
BlockrunClient,
|
|
11496
13498
|
BlockrunError,
|
|
13499
|
+
DEFAULT_BATCH_RATE_LIMIT,
|
|
11497
13500
|
EVM_NETWORKS,
|
|
11498
13501
|
ImageClient,
|
|
11499
13502
|
KNOWN_PROVIDERS,
|
|
@@ -11555,6 +13558,7 @@ var AnthropicClient = class {
|
|
|
11555
13558
|
loadWallet,
|
|
11556
13559
|
logCost,
|
|
11557
13560
|
parsePaymentRequired,
|
|
13561
|
+
retryDisposition,
|
|
11558
13562
|
saveSolanaWallet,
|
|
11559
13563
|
saveToCache,
|
|
11560
13564
|
saveWallet,
|