@blockrun/llm 3.24.7 → 3.24.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +250 -95
- package/dist/index.d.cts +67 -12
- package/dist/index.d.ts +67 -12
- package/dist/index.js +250 -95
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -554,13 +554,44 @@ function readSettlement(response, quotedUsd, scheme) {
|
|
|
554
554
|
};
|
|
555
555
|
return { costUsd, settlement };
|
|
556
556
|
}
|
|
557
|
+
function settlementSkipReason(response) {
|
|
558
|
+
const headers = response.headers;
|
|
559
|
+
if (typeof headers?.get !== "function") return void 0;
|
|
560
|
+
const reason = [headers.get("X-Settlement-Skipped"), headers.get("X-Payment-Waived")].map((value) => value?.trim()).find((value) => !!value && !NOT_A_SKIP.test(value));
|
|
561
|
+
if (reason === void 0) return void 0;
|
|
562
|
+
const evidence = settlementEvidence(response);
|
|
563
|
+
if (evidence !== void 0) {
|
|
564
|
+
console.error(
|
|
565
|
+
`[@blockrun/llm] settlement: the gateway named a skipped settlement (${JSON.stringify(reason)}) on a response that also carries ${evidence}; booked as charged`
|
|
566
|
+
);
|
|
567
|
+
return void 0;
|
|
568
|
+
}
|
|
569
|
+
return reason;
|
|
570
|
+
}
|
|
571
|
+
var NOT_A_SKIP = /^(false|0|no|none)$/i;
|
|
572
|
+
function settlementEvidence(response) {
|
|
573
|
+
const header = response.headers.get("PAYMENT-RESPONSE") ?? response.headers.get("X-PAYMENT-RESPONSE");
|
|
574
|
+
const receipt = header ? decode(header) : void 0;
|
|
575
|
+
if (receipt && receipt.success !== false) {
|
|
576
|
+
if (typeof receipt.transaction === "string" && receipt.transaction) return "a settlement receipt with a transaction";
|
|
577
|
+
const extra = receipt.extra;
|
|
578
|
+
const charged = receiptChargedUsd({
|
|
579
|
+
amount: receipt.amount,
|
|
580
|
+
extra: extra && typeof extra === "object" ? extra : void 0
|
|
581
|
+
});
|
|
582
|
+
if (charged !== void 0 && charged > 0) return "a settlement receipt with a charge";
|
|
583
|
+
}
|
|
584
|
+
if (response.headers.get("X-Payment-Receipt")?.trim()) return "an X-Payment-Receipt transaction";
|
|
585
|
+
return void 0;
|
|
586
|
+
}
|
|
557
587
|
function settlementSkipped(response) {
|
|
558
|
-
return response
|
|
588
|
+
return settlementSkipReason(response) !== void 0;
|
|
559
589
|
}
|
|
560
590
|
function withCost(body, response, bookedUsd, scheme) {
|
|
561
|
-
|
|
591
|
+
const skipped = settlementSkipReason(response);
|
|
592
|
+
if (skipped !== void 0) {
|
|
562
593
|
body.costUsd = 0;
|
|
563
|
-
body.settlement = { ...readSettlement(response, bookedUsd, scheme).settlement, skipped
|
|
594
|
+
body.settlement = { ...readSettlement(response, bookedUsd, scheme).settlement, skipped };
|
|
564
595
|
return body;
|
|
565
596
|
}
|
|
566
597
|
body.costUsd = bookedUsd;
|
|
@@ -5089,7 +5120,7 @@ function getCostSummary() {
|
|
|
5089
5120
|
}
|
|
5090
5121
|
|
|
5091
5122
|
// src/version.ts
|
|
5092
|
-
var SDK_VERSION = "3.24.
|
|
5123
|
+
var SDK_VERSION = "3.24.9";
|
|
5093
5124
|
var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
|
|
5094
5125
|
|
|
5095
5126
|
// src/client.ts
|
|
@@ -5685,9 +5716,9 @@ var LLMClient = class _LLMClient {
|
|
|
5685
5716
|
return this.bookPaid(url, body, details, retryResponse);
|
|
5686
5717
|
}
|
|
5687
5718
|
/**
|
|
5688
|
-
* Book a served, paid chat call and attach its cost. A response
|
|
5689
|
-
*
|
|
5690
|
-
*
|
|
5719
|
+
* Book a served, paid chat call and attach its cost. A response whose
|
|
5720
|
+
* settlement the gateway skipped ({@link settlementSkipped}: e.g.
|
|
5721
|
+
* `X-Settlement-Skipped: free-fallback`) moved nothing on chain, so it is
|
|
5691
5722
|
* not added to `getSpending()` or the cost log, and reports `costUsd: 0`.
|
|
5692
5723
|
*/
|
|
5693
5724
|
async bookPaid(url, body, details, response) {
|
|
@@ -5699,6 +5730,21 @@ var LLMClient = class _LLMClient {
|
|
|
5699
5730
|
}
|
|
5700
5731
|
return withCost(await this.parseChatResponse(response), response, costUsd, "exact");
|
|
5701
5732
|
}
|
|
5733
|
+
/**
|
|
5734
|
+
* Book a served, paid raw (non-chat) call: the signed amount goes to
|
|
5735
|
+
* `getSpending()` and the cost log, unless the gateway skipped settlement
|
|
5736
|
+
* ({@link settlementSkipped}: e.g. `X-Settlement-Skipped: empty-result` on an
|
|
5737
|
+
* empty prediction-market search, `unexecuted` on a JSON-RPC call that never
|
|
5738
|
+
* ran, or `X-Payment-Waived`), in which case nothing moved and nothing is
|
|
5739
|
+
* booked. `X-Payment-Settled: false` alone is still booked: it is owed.
|
|
5740
|
+
*/
|
|
5741
|
+
bookPaidRaw(url, details, response, body) {
|
|
5742
|
+
if (settlementSkipped(response)) return;
|
|
5743
|
+
const costUsd = parseFloat(details.amount) / 1e6;
|
|
5744
|
+
this.sessionCalls += 1;
|
|
5745
|
+
this.sessionTotalUsd += costUsd;
|
|
5746
|
+
this.recordCost(url, costUsd, body ? { body, network: details.network } : { network: details.network });
|
|
5747
|
+
}
|
|
5702
5748
|
/**
|
|
5703
5749
|
* Sign a payment header and return the PAYMENT-SIGNATURE value.
|
|
5704
5750
|
* Extracted to share logic between streaming and non-streaming flows.
|
|
@@ -5799,8 +5845,10 @@ var LLMClient = class _LLMClient {
|
|
|
5799
5845
|
throw withDisposition(err, "paid-or-in-doubt");
|
|
5800
5846
|
}
|
|
5801
5847
|
if (preAuthResp.ok) {
|
|
5802
|
-
|
|
5803
|
-
|
|
5848
|
+
if (!settlementSkipped(preAuthResp)) {
|
|
5849
|
+
this.sessionCalls += 1;
|
|
5850
|
+
this.sessionTotalUsd += signed.costUsd;
|
|
5851
|
+
}
|
|
5804
5852
|
return preAuthResp;
|
|
5805
5853
|
}
|
|
5806
5854
|
this.preAuthCache.delete(cacheKey2);
|
|
@@ -5865,8 +5913,10 @@ var LLMClient = class _LLMClient {
|
|
|
5865
5913
|
}
|
|
5866
5914
|
throw new APIError(`API error after payment: ${streamResp.status}`, streamResp.status, sanitizeErrorResponse(errorBody));
|
|
5867
5915
|
}
|
|
5868
|
-
|
|
5869
|
-
|
|
5916
|
+
if (!settlementSkipped(streamResp)) {
|
|
5917
|
+
this.sessionCalls += 1;
|
|
5918
|
+
this.sessionTotalUsd += costUsd;
|
|
5919
|
+
}
|
|
5870
5920
|
return streamResp;
|
|
5871
5921
|
}
|
|
5872
5922
|
/**
|
|
@@ -5990,10 +6040,7 @@ var LLMClient = class _LLMClient {
|
|
|
5990
6040
|
}
|
|
5991
6041
|
throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
|
|
5992
6042
|
}
|
|
5993
|
-
|
|
5994
|
-
this.sessionCalls += 1;
|
|
5995
|
-
this.sessionTotalUsd += costUsd2;
|
|
5996
|
-
this.recordCost(url, costUsd2, { body, network: details.network });
|
|
6043
|
+
this.bookPaidRaw(url, details, retryResp2, body);
|
|
5997
6044
|
return retryResp2.json();
|
|
5998
6045
|
}
|
|
5999
6046
|
}
|
|
@@ -6013,10 +6060,7 @@ var LLMClient = class _LLMClient {
|
|
|
6013
6060
|
sanitizeErrorResponse(errorBody)
|
|
6014
6061
|
);
|
|
6015
6062
|
}
|
|
6016
|
-
|
|
6017
|
-
this.sessionCalls += 1;
|
|
6018
|
-
this.sessionTotalUsd += costUsd;
|
|
6019
|
-
this.recordCost(url, costUsd, { body, network: details.network });
|
|
6063
|
+
this.bookPaidRaw(url, details, retryResponse, body);
|
|
6020
6064
|
return retryResponse.json();
|
|
6021
6065
|
}
|
|
6022
6066
|
/**
|
|
@@ -6136,10 +6180,7 @@ var LLMClient = class _LLMClient {
|
|
|
6136
6180
|
}
|
|
6137
6181
|
throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
|
|
6138
6182
|
}
|
|
6139
|
-
|
|
6140
|
-
this.sessionCalls += 1;
|
|
6141
|
-
this.sessionTotalUsd += costUsd2;
|
|
6142
|
-
this.recordCost(url, costUsd2, { network: details.network });
|
|
6183
|
+
this.bookPaidRaw(url, details, retryResp2);
|
|
6143
6184
|
return retryResp2.json();
|
|
6144
6185
|
}
|
|
6145
6186
|
}
|
|
@@ -6159,10 +6200,7 @@ var LLMClient = class _LLMClient {
|
|
|
6159
6200
|
sanitizeErrorResponse(errorBody)
|
|
6160
6201
|
);
|
|
6161
6202
|
}
|
|
6162
|
-
|
|
6163
|
-
this.sessionCalls += 1;
|
|
6164
|
-
this.sessionTotalUsd += costUsd;
|
|
6165
|
-
this.recordCost(url, costUsd, { network: details.network });
|
|
6203
|
+
this.bookPaidRaw(url, details, retryResponse);
|
|
6166
6204
|
return retryResponse.json();
|
|
6167
6205
|
}
|
|
6168
6206
|
/**
|
|
@@ -7175,8 +7213,10 @@ var MusicClient = class {
|
|
|
7175
7213
|
throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
|
|
7176
7214
|
}
|
|
7177
7215
|
const data = await retryResponse.json();
|
|
7178
|
-
|
|
7179
|
-
|
|
7216
|
+
if (!settlementSkipped(retryResponse)) {
|
|
7217
|
+
this.sessionCalls++;
|
|
7218
|
+
this.sessionTotalUsd += 0.1575;
|
|
7219
|
+
}
|
|
7180
7220
|
const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
|
|
7181
7221
|
if (txHash) data.txHash = txHash;
|
|
7182
7222
|
return data;
|
|
@@ -7388,9 +7428,11 @@ var SpeechClient = class {
|
|
|
7388
7428
|
throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
|
|
7389
7429
|
}
|
|
7390
7430
|
const data = await retryResponse.json();
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7431
|
+
if (!settlementSkipped(retryResponse)) {
|
|
7432
|
+
this.sessionCalls++;
|
|
7433
|
+
const paidUsd = Number(details.amount) / 1e6;
|
|
7434
|
+
if (Number.isFinite(paidUsd)) this.sessionTotalUsd += paidUsd;
|
|
7435
|
+
}
|
|
7394
7436
|
const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
|
|
7395
7437
|
if (txHash) data.txHash = txHash;
|
|
7396
7438
|
return data;
|
|
@@ -7687,8 +7729,10 @@ var VideoClient = class {
|
|
|
7687
7729
|
}
|
|
7688
7730
|
if (lastStatus === "completed") {
|
|
7689
7731
|
const data = pollData;
|
|
7690
|
-
|
|
7691
|
-
|
|
7732
|
+
if (!settlementSkipped(pollResp)) {
|
|
7733
|
+
this.sessionCalls++;
|
|
7734
|
+
this.sessionTotalUsd += Number(details.amount) / 1e6;
|
|
7735
|
+
}
|
|
7692
7736
|
const txHash = pollResp.headers.get("x-payment-receipt") || pollResp.headers.get("X-Payment-Receipt");
|
|
7693
7737
|
if (txHash) data.txHash = txHash;
|
|
7694
7738
|
return data;
|
|
@@ -7983,8 +8027,10 @@ var VoiceClient = class {
|
|
|
7983
8027
|
throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
|
|
7984
8028
|
}
|
|
7985
8029
|
const data = await retryResponse.json();
|
|
7986
|
-
|
|
7987
|
-
|
|
8030
|
+
if (!settlementSkipped(retryResponse)) {
|
|
8031
|
+
this.sessionCalls++;
|
|
8032
|
+
this.sessionTotalUsd += CALL_PRICE_USD;
|
|
8033
|
+
}
|
|
7988
8034
|
const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
|
|
7989
8035
|
if (txHash) data.txHash = txHash;
|
|
7990
8036
|
return data;
|
|
@@ -8204,9 +8250,10 @@ var PhoneClient = class {
|
|
|
8204
8250
|
throw new PaymentError("Payment was rejected. Check your wallet balance.");
|
|
8205
8251
|
}
|
|
8206
8252
|
const data = await this.unwrap(retry, true);
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8253
|
+
if (!settlementSkipped(retry)) {
|
|
8254
|
+
this.sessionCalls += 1;
|
|
8255
|
+
this.sessionTotalUsd += Number.parseFloat(details.amount) / 1e6;
|
|
8256
|
+
}
|
|
8210
8257
|
const txHash = retry.headers.get("x-payment-receipt") || retry.headers.get("X-Payment-Receipt");
|
|
8211
8258
|
if (txHash && data && typeof data === "object" && !("txHash" in data)) {
|
|
8212
8259
|
data.txHash = txHash;
|
|
@@ -8371,9 +8418,10 @@ var PortraitClient = class {
|
|
|
8371
8418
|
throw new PaymentError("Payment was rejected. Check your wallet balance.");
|
|
8372
8419
|
}
|
|
8373
8420
|
const data = await this.unwrap(retry, true);
|
|
8374
|
-
|
|
8375
|
-
|
|
8376
|
-
|
|
8421
|
+
if (!settlementSkipped(retry)) {
|
|
8422
|
+
this.sessionCalls += 1;
|
|
8423
|
+
this.sessionTotalUsd += Number.parseFloat(details.amount) / 1e6;
|
|
8424
|
+
}
|
|
8377
8425
|
if (data && typeof data === "object" && !("txHash" in data)) {
|
|
8378
8426
|
const settlement = data.settlement;
|
|
8379
8427
|
if (settlement?.tx_hash) {
|
|
@@ -9190,9 +9238,11 @@ var RpcClient = class {
|
|
|
9190
9238
|
}
|
|
9191
9239
|
throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
|
|
9192
9240
|
}
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9241
|
+
if (!settlementSkipped(retryResponse)) {
|
|
9242
|
+
this.sessionCalls++;
|
|
9243
|
+
const paidUsd = Number(details.amount) / 1e6;
|
|
9244
|
+
if (Number.isFinite(paidUsd)) this.sessionTotalUsd += paidUsd;
|
|
9245
|
+
}
|
|
9196
9246
|
return { data: await retryResponse.json(), headers: retryResponse.headers };
|
|
9197
9247
|
}
|
|
9198
9248
|
async fetchWithTimeout(url, options) {
|
|
@@ -9339,7 +9389,7 @@ var BlockrunClient = class {
|
|
|
9339
9389
|
if (resp402.status !== 402) {
|
|
9340
9390
|
await this.throwApiError(resp402, `poll submit failed (${submitUrl})`);
|
|
9341
9391
|
}
|
|
9342
|
-
const paymentPayload = await this.signFrom402(resp402, submitUrl, {
|
|
9392
|
+
const { paymentPayload, costUsd } = await this.signFrom402(resp402, submitUrl, {
|
|
9343
9393
|
description: "BlockRun async job",
|
|
9344
9394
|
maxTimeoutSeconds: MAX_SIGNED_AUTH_SECONDS
|
|
9345
9395
|
});
|
|
@@ -9358,7 +9408,7 @@ var BlockrunClient = class {
|
|
|
9358
9408
|
await this.throwApiError(submitResp, `poll submit failed (${submitUrl})`);
|
|
9359
9409
|
}
|
|
9360
9410
|
if (submitResp.status === 200) {
|
|
9361
|
-
this.recordSpending();
|
|
9411
|
+
this.recordSpending(submitResp, costUsd);
|
|
9362
9412
|
return submitResp.json();
|
|
9363
9413
|
}
|
|
9364
9414
|
const submitData = await submitResp.json();
|
|
@@ -9395,7 +9445,7 @@ var BlockrunClient = class {
|
|
|
9395
9445
|
);
|
|
9396
9446
|
}
|
|
9397
9447
|
if (pollResp.status === 200 && lastStatus === "completed") {
|
|
9398
|
-
this.recordSpending();
|
|
9448
|
+
this.recordSpending(pollResp, costUsd);
|
|
9399
9449
|
return pollData;
|
|
9400
9450
|
}
|
|
9401
9451
|
if (pollResp.status !== 200 && pollResp.status !== 202 && pollResp.status !== 504) {
|
|
@@ -9436,7 +9486,7 @@ var BlockrunClient = class {
|
|
|
9436
9486
|
if (resp402.status === 200) {
|
|
9437
9487
|
streamResp = resp402;
|
|
9438
9488
|
} else if (resp402.status === 402) {
|
|
9439
|
-
const paymentPayload = await this.signFrom402(resp402, url, {
|
|
9489
|
+
const { paymentPayload, costUsd } = await this.signFrom402(resp402, url, {
|
|
9440
9490
|
description: "BlockRun stream"
|
|
9441
9491
|
});
|
|
9442
9492
|
streamResp = await this.fetchWithTimeout(url, {
|
|
@@ -9455,7 +9505,7 @@ var BlockrunClient = class {
|
|
|
9455
9505
|
if (!streamResp.ok) {
|
|
9456
9506
|
await this.throwApiError(streamResp, `stream failed after payment (${url})`);
|
|
9457
9507
|
}
|
|
9458
|
-
this.recordSpending();
|
|
9508
|
+
this.recordSpending(streamResp, costUsd);
|
|
9459
9509
|
} else {
|
|
9460
9510
|
await this.throwApiError(resp402, `stream failed (${url})`);
|
|
9461
9511
|
return;
|
|
@@ -9511,7 +9561,7 @@ var BlockrunClient = class {
|
|
|
9511
9561
|
return response.json();
|
|
9512
9562
|
}
|
|
9513
9563
|
async handlePaymentAndRetry(url, method, body, response) {
|
|
9514
|
-
const paymentPayload = await this.signFrom402(response, url, {
|
|
9564
|
+
const { paymentPayload, costUsd } = await this.signFrom402(response, url, {
|
|
9515
9565
|
description: "BlockRun"
|
|
9516
9566
|
});
|
|
9517
9567
|
const retryInit = {
|
|
@@ -9534,13 +9584,14 @@ var BlockrunClient = class {
|
|
|
9534
9584
|
if (!retry.ok) {
|
|
9535
9585
|
await this.throwApiError(retry, `${method} ${url} failed after payment`);
|
|
9536
9586
|
}
|
|
9537
|
-
this.recordSpending();
|
|
9587
|
+
this.recordSpending(retry, costUsd);
|
|
9538
9588
|
return retry.json();
|
|
9539
9589
|
}
|
|
9540
9590
|
/**
|
|
9541
9591
|
* Read a 402 response's payment requirements (header or body), then sign and
|
|
9542
|
-
* return the base64 PAYMENT-SIGNATURE payload
|
|
9543
|
-
*
|
|
9592
|
+
* return the base64 PAYMENT-SIGNATURE payload with what it pays, in USD.
|
|
9593
|
+
* The cost stays with the call that signed it (booked by `recordSpending`):
|
|
9594
|
+
* concurrent calls on one client never share it.
|
|
9544
9595
|
*/
|
|
9545
9596
|
async signFrom402(response, url, opts) {
|
|
9546
9597
|
let paymentHeader = response.headers.get("payment-required");
|
|
@@ -9558,8 +9609,8 @@ var BlockrunClient = class {
|
|
|
9558
9609
|
}
|
|
9559
9610
|
const paymentRequired = parsePaymentRequired(paymentHeader);
|
|
9560
9611
|
const details = extractPaymentDetails(paymentRequired);
|
|
9561
|
-
|
|
9562
|
-
|
|
9612
|
+
const costUsd = parseFloat(details.amount) / 1e6;
|
|
9613
|
+
const paymentPayload = await createPaymentPayload(
|
|
9563
9614
|
this.privateKey,
|
|
9564
9615
|
this.account.address,
|
|
9565
9616
|
details.recipient,
|
|
@@ -9576,14 +9627,20 @@ var BlockrunClient = class {
|
|
|
9576
9627
|
asset: details.asset
|
|
9577
9628
|
}
|
|
9578
9629
|
);
|
|
9630
|
+
return { paymentPayload, costUsd };
|
|
9579
9631
|
}
|
|
9580
|
-
/**
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
9632
|
+
/**
|
|
9633
|
+
* Book `costUsd`, what the call that `response` answered signed. A response
|
|
9634
|
+
* whose settlement the gateway skipped ({@link settlementSkipped}:
|
|
9635
|
+
* `X-Settlement-Skipped: <reason>` or `X-Payment-Waived`) moved nothing, so
|
|
9636
|
+
* nothing is booked. `X-Payment-Settled: false` alone is still booked: the
|
|
9637
|
+
* payment is owed.
|
|
9638
|
+
*/
|
|
9639
|
+
recordSpending(response, costUsd) {
|
|
9640
|
+
if (settlementSkipped(response)) return;
|
|
9641
|
+
if (costUsd > 0) {
|
|
9584
9642
|
this.sessionCalls += 1;
|
|
9585
|
-
this.sessionTotalUsd +=
|
|
9586
|
-
this.pendingCostUsd = 0;
|
|
9643
|
+
this.sessionTotalUsd += costUsd;
|
|
9587
9644
|
}
|
|
9588
9645
|
}
|
|
9589
9646
|
async throwApiError(resp, prefix) {
|
|
@@ -10745,7 +10802,7 @@ var ChannelLayoutError = class extends Error {
|
|
|
10745
10802
|
};
|
|
10746
10803
|
async function readChannelAccount(rpcUrl, channelId, rpcHeaders, options = {}) {
|
|
10747
10804
|
await checkChannelLayout();
|
|
10748
|
-
const config = { encoding: "base64", commitment: "finalized" };
|
|
10805
|
+
const config = { encoding: "base64", commitment: options.commitment ?? "finalized" };
|
|
10749
10806
|
if (options.minContextSlot !== void 0) config.minContextSlot = options.minContextSlot;
|
|
10750
10807
|
const response = await fetch(rpcUrl, {
|
|
10751
10808
|
method: "POST",
|
|
@@ -10809,7 +10866,10 @@ function checkChannelLayout() {
|
|
|
10809
10866
|
return layoutChecked;
|
|
10810
10867
|
}
|
|
10811
10868
|
var LANDING_MARGIN_BLOCKS = 300;
|
|
10812
|
-
|
|
10869
|
+
function savedReclaimAnchor(record) {
|
|
10870
|
+
const saved = record.reclaimAnchorHeight;
|
|
10871
|
+
return typeof saved === "number" && Number.isSafeInteger(saved) && saved > 0 ? saved : void 0;
|
|
10872
|
+
}
|
|
10813
10873
|
function maxOf(a, b) {
|
|
10814
10874
|
if (a === void 0) return b;
|
|
10815
10875
|
if (b === void 0) return a;
|
|
@@ -11799,6 +11859,8 @@ var SolanaBatchPayer = class {
|
|
|
11799
11859
|
wallet.refusedFor = outcome.errorReason;
|
|
11800
11860
|
}
|
|
11801
11861
|
}
|
|
11862
|
+
} else if (built.key && outcome.reason === "payment_required" && outcome.refusedForMs === void 0) {
|
|
11863
|
+
await this.recheckRefusedChannel(wallet, built.key, built.reclaimAnchor);
|
|
11802
11864
|
}
|
|
11803
11865
|
sent = void 0;
|
|
11804
11866
|
release();
|
|
@@ -12066,6 +12128,13 @@ var SolanaBatchPayer = class {
|
|
|
12066
12128
|
}
|
|
12067
12129
|
/** Build a new batch payment for a 402: the payload, its headers and its kind. */
|
|
12068
12130
|
async buildPayment(wallet, paymentRequired) {
|
|
12131
|
+
const acceptKey = channelKeyOf(trustedBatchAccept(paymentRequired, this.init.options.operators));
|
|
12132
|
+
let reclaimAnchor;
|
|
12133
|
+
try {
|
|
12134
|
+
const stored = acceptKey ? await wallet.book.base.get(acceptKey) : void 0;
|
|
12135
|
+
reclaimAnchor = stored ? savedReclaimAnchor(stored) : void 0;
|
|
12136
|
+
} catch {
|
|
12137
|
+
}
|
|
12069
12138
|
const { http, payload } = await this.createPayload(wallet, paymentRequired);
|
|
12070
12139
|
const scope = payScope.getStore();
|
|
12071
12140
|
if (scope?.afterError) {
|
|
@@ -12087,7 +12156,16 @@ var SolanaBatchPayer = class {
|
|
|
12087
12156
|
const record = known?.record;
|
|
12088
12157
|
kind = record && (record.pending === void 0 || record.hasConfirmedState) ? "top-up" : "open";
|
|
12089
12158
|
}
|
|
12090
|
-
return {
|
|
12159
|
+
return {
|
|
12160
|
+
http,
|
|
12161
|
+
payload,
|
|
12162
|
+
headers,
|
|
12163
|
+
kind,
|
|
12164
|
+
key: known?.key,
|
|
12165
|
+
...reclaimAnchor !== void 0 && known?.key === acceptKey ? { reclaimAnchor } : {},
|
|
12166
|
+
sentAt: Date.now(),
|
|
12167
|
+
sends: 0
|
|
12168
|
+
};
|
|
12091
12169
|
}
|
|
12092
12170
|
/**
|
|
12093
12171
|
* Write a deposit's intent to the wallet's journal before it is sent
|
|
@@ -12321,6 +12399,84 @@ var SolanaBatchPayer = class {
|
|
|
12321
12399
|
}
|
|
12322
12400
|
}
|
|
12323
12401
|
}
|
|
12402
|
+
/**
|
|
12403
|
+
* Queue a re-read of a stored channel whose account is gone at finalized
|
|
12404
|
+
* and at confirmed commitment (closed, its account reclaimed), anchored at
|
|
12405
|
+
* the SDK's own `getBlockHeight` at confirmed. The anchor is saved in the
|
|
12406
|
+
* record (`reclaimAnchorHeight`), so a short-lived process finishes the
|
|
12407
|
+
* wait instead of starting it over; a failed read leaves resync to take it,
|
|
12408
|
+
* and a failed save only costs that. resync pays exact until finality is
|
|
12409
|
+
* LANDING_MARGIN_BLOCKS past the anchor, then drops the record if the
|
|
12410
|
+
* finalized chain still has no account (a lagging read finds the channel
|
|
12411
|
+
* and rewrites the record, without the anchor).
|
|
12412
|
+
*/
|
|
12413
|
+
async queueReclaimed(wallet, key, record, channelId, savedAnchor) {
|
|
12414
|
+
let anchorHeight = savedReclaimAnchor(record) ?? savedAnchor;
|
|
12415
|
+
if (anchorHeight === void 0) {
|
|
12416
|
+
try {
|
|
12417
|
+
anchorHeight = rpcInteger(
|
|
12418
|
+
await rpcRequest(this.init.rpcUrl, "getBlockHeight", [{ commitment: "confirmed" }], this.init.rpcHeaders),
|
|
12419
|
+
"getBlockHeight"
|
|
12420
|
+
);
|
|
12421
|
+
} catch {
|
|
12422
|
+
}
|
|
12423
|
+
}
|
|
12424
|
+
if (anchorHeight !== void 0 && savedReclaimAnchor(record) !== anchorHeight) {
|
|
12425
|
+
try {
|
|
12426
|
+
await wallet.book.set(key, { ...record, reclaimAnchorHeight: anchorHeight });
|
|
12427
|
+
} catch {
|
|
12428
|
+
}
|
|
12429
|
+
}
|
|
12430
|
+
addResyncTarget(wallet, {
|
|
12431
|
+
key,
|
|
12432
|
+
channelId,
|
|
12433
|
+
channelConfig: record.channelConfig,
|
|
12434
|
+
cumulative: parseAtomic(record.chargedCumulativeAmount),
|
|
12435
|
+
knownDeposit: parseAtomic(record.deposit),
|
|
12436
|
+
...anchorHeight !== void 0 ? { anchorHeight } : {},
|
|
12437
|
+
reason: "channel_unusable"
|
|
12438
|
+
});
|
|
12439
|
+
}
|
|
12440
|
+
/**
|
|
12441
|
+
* After the gateway refused an authorization with a plain 402 (no receipt,
|
|
12442
|
+
* not an admission refusal), check the channel it paid into. That is how
|
|
12443
|
+
* the gateway answers an authorization into a channel the facilitator
|
|
12444
|
+
* closed (its verify rejects it), and a channel with deposit to spare is
|
|
12445
|
+
* otherwise never read again: every call would sign another authorization
|
|
12446
|
+
* into it and pay exact. A channel the finalized chain shows closing,
|
|
12447
|
+
* closed or not this wallet's is re-read through resync (record dropped,
|
|
12448
|
+
* or kept and paid exact); one gone at every commitment is queued as
|
|
12449
|
+
* {@link queueReclaimed}; an open one of this wallet's, or one only at
|
|
12450
|
+
* confirmed (not finalized yet), is left as it is. A queued re-read is run
|
|
12451
|
+
* at once. Best effort: a failed read changes nothing, and the next
|
|
12452
|
+
* refusal checks again.
|
|
12453
|
+
*/
|
|
12454
|
+
async recheckRefusedChannel(wallet, key, savedAnchor) {
|
|
12455
|
+
try {
|
|
12456
|
+
const record = await wallet.book.get(key);
|
|
12457
|
+
if (!record || record.pending || typeof record.channelId !== "string" || wallet.resyncs.has(key)) return;
|
|
12458
|
+
const channelId = record.channelId;
|
|
12459
|
+
const channel = await readChannelAccount(this.init.rpcUrl, channelId, this.init.rpcHeaders);
|
|
12460
|
+
if (channel !== void 0) {
|
|
12461
|
+
if (channel.open && channel.payer === await this.init.address() && channel.authorizedSigner === record.channelConfig.payerAuthorizer && channel.mint === record.channelConfig.token) {
|
|
12462
|
+
return;
|
|
12463
|
+
}
|
|
12464
|
+
addResyncTarget(wallet, {
|
|
12465
|
+
key,
|
|
12466
|
+
channelId,
|
|
12467
|
+
channelConfig: record.channelConfig,
|
|
12468
|
+
cumulative: parseAtomic(record.chargedCumulativeAmount),
|
|
12469
|
+
knownDeposit: parseAtomic(record.deposit),
|
|
12470
|
+
reason: "channel_unusable"
|
|
12471
|
+
});
|
|
12472
|
+
} else {
|
|
12473
|
+
if (await readChannelAccount(this.init.rpcUrl, channelId, this.init.rpcHeaders, { commitment: "confirmed" }) !== void 0) return;
|
|
12474
|
+
await this.queueReclaimed(wallet, key, record, channelId, savedAnchor);
|
|
12475
|
+
}
|
|
12476
|
+
await this.resync(wallet);
|
|
12477
|
+
} catch {
|
|
12478
|
+
}
|
|
12479
|
+
}
|
|
12324
12480
|
/**
|
|
12325
12481
|
* When this payment will top up the wallet's channel, read how much of its
|
|
12326
12482
|
* deposit is already settled on-chain (at `finalized` commitment, so never
|
|
@@ -12338,7 +12494,11 @@ var SolanaBatchPayer = class {
|
|
|
12338
12494
|
* pays exact (`channel_unreadable`), record kept. If
|
|
12339
12495
|
* the finalized channel is closing or closed, or is not this wallet's for
|
|
12340
12496
|
* the record's operator and mint, no top-up is sized from the record: it
|
|
12341
|
-
* is re-read at once (`channel_unusable`), see {@link createPayload}.
|
|
12497
|
+
* is re-read at once (`channel_unusable`), see {@link createPayload}. So is
|
|
12498
|
+
* a channel with no account at finalized or at confirmed commitment (closed
|
|
12499
|
+
* and reclaimed); resync drops its record only once finality is past its
|
|
12500
|
+
* own anchor. One that shows only at confirmed is not finalized yet: the
|
|
12501
|
+
* lifetime cap applies.
|
|
12342
12502
|
*/
|
|
12343
12503
|
async prepareTopUp(wallet, paymentRequired) {
|
|
12344
12504
|
wallet.topUp = void 0;
|
|
@@ -12350,8 +12510,12 @@ var SolanaBatchPayer = class {
|
|
|
12350
12510
|
const deposit = parseAtomic(record.deposit);
|
|
12351
12511
|
if (parseAtomic(record.chargedCumulativeAmount) + parseAtomic(accept.amount) <= deposit) return;
|
|
12352
12512
|
let channel;
|
|
12513
|
+
let unfinalized = false;
|
|
12353
12514
|
try {
|
|
12354
12515
|
channel = await readChannelAccount(this.init.rpcUrl, record.channelId, this.init.rpcHeaders);
|
|
12516
|
+
if (channel === void 0) {
|
|
12517
|
+
unfinalized = await readChannelAccount(this.init.rpcUrl, record.channelId, this.init.rpcHeaders, { commitment: "confirmed" }) !== void 0;
|
|
12518
|
+
}
|
|
12355
12519
|
} catch (err) {
|
|
12356
12520
|
if (err instanceof ChannelLayoutError) {
|
|
12357
12521
|
throw new ResyncError("channel_unreadable", `channel ${record.channelId}: ${err.message}; no top-up is sized from it`);
|
|
@@ -12364,27 +12528,9 @@ var SolanaBatchPayer = class {
|
|
|
12364
12528
|
`could not read channel ${record.channelId} before topping it up: ${err instanceof Error ? err.message : String(err)}`
|
|
12365
12529
|
);
|
|
12366
12530
|
}
|
|
12531
|
+
if (channel === void 0 && unfinalized) return;
|
|
12367
12532
|
if (channel === void 0) {
|
|
12368
|
-
|
|
12369
|
-
const delaySlots = Math.ceil((record.channelConfig.withdrawDelay ?? 86400) / SECONDS_PER_SLOT);
|
|
12370
|
-
let finalizedSlot;
|
|
12371
|
-
try {
|
|
12372
|
-
const epoch = await rpcRequest(this.init.rpcUrl, "getEpochInfo", [{ commitment: "finalized" }], this.init.rpcHeaders);
|
|
12373
|
-
finalizedSlot = rpcInteger(epoch.absoluteSlot, "getEpochInfo absoluteSlot");
|
|
12374
|
-
} catch {
|
|
12375
|
-
return;
|
|
12376
|
-
}
|
|
12377
|
-
if (!Number.isSafeInteger(openSlot) || openSlot <= 0 || finalizedSlot <= openSlot + delaySlots) return;
|
|
12378
|
-
addResyncTarget(wallet, {
|
|
12379
|
-
key,
|
|
12380
|
-
channelId: record.channelId,
|
|
12381
|
-
channelConfig: record.channelConfig,
|
|
12382
|
-
cumulative: parseAtomic(record.chargedCumulativeAmount),
|
|
12383
|
-
knownDeposit: deposit,
|
|
12384
|
-
// No deposit is in doubt: nothing can still land in a reclaimed account.
|
|
12385
|
-
anchorHeight: 0,
|
|
12386
|
-
reason: "channel_unusable"
|
|
12387
|
-
});
|
|
12533
|
+
await this.queueReclaimed(wallet, key, record, record.channelId);
|
|
12388
12534
|
throw new ChannelResyncRequiredError(record.channelId);
|
|
12389
12535
|
}
|
|
12390
12536
|
if (!channel.open || channel.payer !== await this.init.address() || channel.authorizedSigner !== record.channelConfig.payerAuthorizer || channel.mint !== record.channelConfig.token) {
|
|
@@ -13021,7 +13167,7 @@ var SolanaLLMClient = class {
|
|
|
13021
13167
|
body: requestBody
|
|
13022
13168
|
});
|
|
13023
13169
|
await this.assertPaid(paid);
|
|
13024
|
-
this.recordSettlement(costUsd);
|
|
13170
|
+
this.recordSettlement(costUsd, paid);
|
|
13025
13171
|
return paid;
|
|
13026
13172
|
} catch (error) {
|
|
13027
13173
|
if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
|
|
@@ -13408,7 +13554,7 @@ var SolanaLLMClient = class {
|
|
|
13408
13554
|
closesAtStart
|
|
13409
13555
|
);
|
|
13410
13556
|
if (batch.kind === "paid") {
|
|
13411
|
-
this.recordSettlement(batch.chargedUsd);
|
|
13557
|
+
this.recordSettlement(batch.chargedUsd, batch.response);
|
|
13412
13558
|
const paid = await this.readPaidJson(batch.response);
|
|
13413
13559
|
return withCost(paid, batch.response, batch.chargedUsd, "batch");
|
|
13414
13560
|
}
|
|
@@ -13525,7 +13671,7 @@ var SolanaLLMClient = class {
|
|
|
13525
13671
|
};
|
|
13526
13672
|
const deliver = (data, response) => {
|
|
13527
13673
|
if (kind === "video") {
|
|
13528
|
-
if (videoCostUsd !== void 0) this.recordSettlement(videoCostUsd);
|
|
13674
|
+
if (videoCostUsd !== void 0) this.recordSettlement(videoCostUsd, response);
|
|
13529
13675
|
const txHash = response.headers.get("x-payment-receipt");
|
|
13530
13676
|
if (txHash) data.txHash = txHash;
|
|
13531
13677
|
}
|
|
@@ -13772,7 +13918,7 @@ var SolanaLLMClient = class {
|
|
|
13772
13918
|
throw withDisposition(error, "paid-or-in-doubt");
|
|
13773
13919
|
}
|
|
13774
13920
|
await this.assertPaid(retryResponse);
|
|
13775
|
-
|
|
13921
|
+
this.recordSettlement(costUsd, retryResponse);
|
|
13776
13922
|
const paid = await this.readPaidJson(retryResponse);
|
|
13777
13923
|
return withCost(paid, retryResponse, costUsd, "exact");
|
|
13778
13924
|
}
|
|
@@ -13832,11 +13978,11 @@ var SolanaLLMClient = class {
|
|
|
13832
13978
|
await this.assertPaid(retryResponse);
|
|
13833
13979
|
if (retryResponse.status === 202) {
|
|
13834
13980
|
const kind = pollKindFor(url);
|
|
13835
|
-
if (kind !== "video") this.recordSettlement(costUsd);
|
|
13981
|
+
if (kind !== "video") this.recordSettlement(costUsd, retryResponse);
|
|
13836
13982
|
const submitted = await retryResponse.json();
|
|
13837
13983
|
return this.followSolanaJob(submitted, media.pollBudgetMs ?? this.timeout, kind);
|
|
13838
13984
|
}
|
|
13839
|
-
this.recordSettlement(costUsd);
|
|
13985
|
+
this.recordSettlement(costUsd, retryResponse);
|
|
13840
13986
|
return retryResponse.json();
|
|
13841
13987
|
}
|
|
13842
13988
|
async getWithPaymentRaw(endpoint, params) {
|
|
@@ -13880,7 +14026,7 @@ var SolanaLLMClient = class {
|
|
|
13880
14026
|
}
|
|
13881
14027
|
});
|
|
13882
14028
|
await this.assertPaid(retryResponse);
|
|
13883
|
-
this.recordSettlement(costUsd);
|
|
14029
|
+
this.recordSettlement(costUsd, retryResponse);
|
|
13884
14030
|
return retryResponse.json();
|
|
13885
14031
|
}
|
|
13886
14032
|
/**
|
|
@@ -13918,8 +14064,17 @@ var SolanaLLMClient = class {
|
|
|
13918
14064
|
);
|
|
13919
14065
|
}
|
|
13920
14066
|
}
|
|
13921
|
-
/**
|
|
13922
|
-
|
|
14067
|
+
/**
|
|
14068
|
+
* Count one settled x402 payment against the session total.
|
|
14069
|
+
*
|
|
14070
|
+
* Takes the paid response so no booking site can forget the check: a
|
|
14071
|
+
* response whose settlement the gateway skipped ({@link settlementSkipped}:
|
|
14072
|
+
* `X-Settlement-Skipped: free-fallback | empty-result | unexecuted | …`, or
|
|
14073
|
+
* `X-Payment-Waived`) moved nothing on chain and books nothing.
|
|
14074
|
+
* `X-Payment-Settled: false` alone is still booked: the payment is owed.
|
|
14075
|
+
*/
|
|
14076
|
+
recordSettlement(costUsd, response) {
|
|
14077
|
+
if (settlementSkipped(response)) return;
|
|
13923
14078
|
this.sessionCalls += 1;
|
|
13924
14079
|
this.sessionTotalUsd += costUsd;
|
|
13925
14080
|
}
|