@blockrun/llm 3.24.8 → 3.24.10
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 +5 -2
- package/dist/index.cjs +298 -100
- package/dist/index.d.cts +77 -12
- package/dist/index.d.ts +77 -12
- package/dist/index.js +298 -100
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -423,13 +423,44 @@ function readSettlement(response, quotedUsd, scheme) {
|
|
|
423
423
|
};
|
|
424
424
|
return { costUsd, settlement };
|
|
425
425
|
}
|
|
426
|
+
function settlementSkipReason(response) {
|
|
427
|
+
const headers = response.headers;
|
|
428
|
+
if (typeof headers?.get !== "function") return void 0;
|
|
429
|
+
const reason = [headers.get("X-Settlement-Skipped"), headers.get("X-Payment-Waived")].map((value) => value?.trim()).find((value) => !!value && !NOT_A_SKIP.test(value));
|
|
430
|
+
if (reason === void 0) return void 0;
|
|
431
|
+
const evidence = settlementEvidence(response);
|
|
432
|
+
if (evidence !== void 0) {
|
|
433
|
+
console.error(
|
|
434
|
+
`[@blockrun/llm] settlement: the gateway named a skipped settlement (${JSON.stringify(reason)}) on a response that also carries ${evidence}; booked as charged`
|
|
435
|
+
);
|
|
436
|
+
return void 0;
|
|
437
|
+
}
|
|
438
|
+
return reason;
|
|
439
|
+
}
|
|
440
|
+
var NOT_A_SKIP = /^(false|0|no|none)$/i;
|
|
441
|
+
function settlementEvidence(response) {
|
|
442
|
+
const header = response.headers.get("PAYMENT-RESPONSE") ?? response.headers.get("X-PAYMENT-RESPONSE");
|
|
443
|
+
const receipt = header ? decode(header) : void 0;
|
|
444
|
+
if (receipt && receipt.success !== false) {
|
|
445
|
+
if (typeof receipt.transaction === "string" && receipt.transaction) return "a settlement receipt with a transaction";
|
|
446
|
+
const extra = receipt.extra;
|
|
447
|
+
const charged = receiptChargedUsd({
|
|
448
|
+
amount: receipt.amount,
|
|
449
|
+
extra: extra && typeof extra === "object" ? extra : void 0
|
|
450
|
+
});
|
|
451
|
+
if (charged !== void 0 && charged > 0) return "a settlement receipt with a charge";
|
|
452
|
+
}
|
|
453
|
+
if (response.headers.get("X-Payment-Receipt")?.trim()) return "an X-Payment-Receipt transaction";
|
|
454
|
+
return void 0;
|
|
455
|
+
}
|
|
426
456
|
function settlementSkipped(response) {
|
|
427
|
-
return response
|
|
457
|
+
return settlementSkipReason(response) !== void 0;
|
|
428
458
|
}
|
|
429
459
|
function withCost(body, response, bookedUsd, scheme) {
|
|
430
|
-
|
|
460
|
+
const skipped = settlementSkipReason(response);
|
|
461
|
+
if (skipped !== void 0) {
|
|
431
462
|
body.costUsd = 0;
|
|
432
|
-
body.settlement = { ...readSettlement(response, bookedUsd, scheme).settlement, skipped
|
|
463
|
+
body.settlement = { ...readSettlement(response, bookedUsd, scheme).settlement, skipped };
|
|
433
464
|
return body;
|
|
434
465
|
}
|
|
435
466
|
body.costUsd = bookedUsd;
|
|
@@ -4958,7 +4989,7 @@ function getCostSummary() {
|
|
|
4958
4989
|
}
|
|
4959
4990
|
|
|
4960
4991
|
// src/version.ts
|
|
4961
|
-
var SDK_VERSION = "3.24.
|
|
4992
|
+
var SDK_VERSION = "3.24.10";
|
|
4962
4993
|
var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
|
|
4963
4994
|
|
|
4964
4995
|
// src/client.ts
|
|
@@ -5554,9 +5585,9 @@ var LLMClient = class _LLMClient {
|
|
|
5554
5585
|
return this.bookPaid(url, body, details, retryResponse);
|
|
5555
5586
|
}
|
|
5556
5587
|
/**
|
|
5557
|
-
* Book a served, paid chat call and attach its cost. A response
|
|
5558
|
-
*
|
|
5559
|
-
*
|
|
5588
|
+
* Book a served, paid chat call and attach its cost. A response whose
|
|
5589
|
+
* settlement the gateway skipped ({@link settlementSkipped}: e.g.
|
|
5590
|
+
* `X-Settlement-Skipped: free-fallback`) moved nothing on chain, so it is
|
|
5560
5591
|
* not added to `getSpending()` or the cost log, and reports `costUsd: 0`.
|
|
5561
5592
|
*/
|
|
5562
5593
|
async bookPaid(url, body, details, response) {
|
|
@@ -5568,6 +5599,21 @@ var LLMClient = class _LLMClient {
|
|
|
5568
5599
|
}
|
|
5569
5600
|
return withCost(await this.parseChatResponse(response), response, costUsd, "exact");
|
|
5570
5601
|
}
|
|
5602
|
+
/**
|
|
5603
|
+
* Book a served, paid raw (non-chat) call: the signed amount goes to
|
|
5604
|
+
* `getSpending()` and the cost log, unless the gateway skipped settlement
|
|
5605
|
+
* ({@link settlementSkipped}: e.g. `X-Settlement-Skipped: empty-result` on an
|
|
5606
|
+
* empty prediction-market search, `unexecuted` on a JSON-RPC call that never
|
|
5607
|
+
* ran, or `X-Payment-Waived`), in which case nothing moved and nothing is
|
|
5608
|
+
* booked. `X-Payment-Settled: false` alone is still booked: it is owed.
|
|
5609
|
+
*/
|
|
5610
|
+
bookPaidRaw(url, details, response, body) {
|
|
5611
|
+
if (settlementSkipped(response)) return;
|
|
5612
|
+
const costUsd = parseFloat(details.amount) / 1e6;
|
|
5613
|
+
this.sessionCalls += 1;
|
|
5614
|
+
this.sessionTotalUsd += costUsd;
|
|
5615
|
+
this.recordCost(url, costUsd, body ? { body, network: details.network } : { network: details.network });
|
|
5616
|
+
}
|
|
5571
5617
|
/**
|
|
5572
5618
|
* Sign a payment header and return the PAYMENT-SIGNATURE value.
|
|
5573
5619
|
* Extracted to share logic between streaming and non-streaming flows.
|
|
@@ -5668,8 +5714,10 @@ var LLMClient = class _LLMClient {
|
|
|
5668
5714
|
throw withDisposition(err, "paid-or-in-doubt");
|
|
5669
5715
|
}
|
|
5670
5716
|
if (preAuthResp.ok) {
|
|
5671
|
-
|
|
5672
|
-
|
|
5717
|
+
if (!settlementSkipped(preAuthResp)) {
|
|
5718
|
+
this.sessionCalls += 1;
|
|
5719
|
+
this.sessionTotalUsd += signed.costUsd;
|
|
5720
|
+
}
|
|
5673
5721
|
return preAuthResp;
|
|
5674
5722
|
}
|
|
5675
5723
|
this.preAuthCache.delete(cacheKey2);
|
|
@@ -5734,8 +5782,10 @@ var LLMClient = class _LLMClient {
|
|
|
5734
5782
|
}
|
|
5735
5783
|
throw new APIError(`API error after payment: ${streamResp.status}`, streamResp.status, sanitizeErrorResponse(errorBody));
|
|
5736
5784
|
}
|
|
5737
|
-
|
|
5738
|
-
|
|
5785
|
+
if (!settlementSkipped(streamResp)) {
|
|
5786
|
+
this.sessionCalls += 1;
|
|
5787
|
+
this.sessionTotalUsd += costUsd;
|
|
5788
|
+
}
|
|
5739
5789
|
return streamResp;
|
|
5740
5790
|
}
|
|
5741
5791
|
/**
|
|
@@ -5859,10 +5909,7 @@ var LLMClient = class _LLMClient {
|
|
|
5859
5909
|
}
|
|
5860
5910
|
throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
|
|
5861
5911
|
}
|
|
5862
|
-
|
|
5863
|
-
this.sessionCalls += 1;
|
|
5864
|
-
this.sessionTotalUsd += costUsd2;
|
|
5865
|
-
this.recordCost(url, costUsd2, { body, network: details.network });
|
|
5912
|
+
this.bookPaidRaw(url, details, retryResp2, body);
|
|
5866
5913
|
return retryResp2.json();
|
|
5867
5914
|
}
|
|
5868
5915
|
}
|
|
@@ -5882,10 +5929,7 @@ var LLMClient = class _LLMClient {
|
|
|
5882
5929
|
sanitizeErrorResponse(errorBody)
|
|
5883
5930
|
);
|
|
5884
5931
|
}
|
|
5885
|
-
|
|
5886
|
-
this.sessionCalls += 1;
|
|
5887
|
-
this.sessionTotalUsd += costUsd;
|
|
5888
|
-
this.recordCost(url, costUsd, { body, network: details.network });
|
|
5932
|
+
this.bookPaidRaw(url, details, retryResponse, body);
|
|
5889
5933
|
return retryResponse.json();
|
|
5890
5934
|
}
|
|
5891
5935
|
/**
|
|
@@ -6005,10 +6049,7 @@ var LLMClient = class _LLMClient {
|
|
|
6005
6049
|
}
|
|
6006
6050
|
throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
|
|
6007
6051
|
}
|
|
6008
|
-
|
|
6009
|
-
this.sessionCalls += 1;
|
|
6010
|
-
this.sessionTotalUsd += costUsd2;
|
|
6011
|
-
this.recordCost(url, costUsd2, { network: details.network });
|
|
6052
|
+
this.bookPaidRaw(url, details, retryResp2);
|
|
6012
6053
|
return retryResp2.json();
|
|
6013
6054
|
}
|
|
6014
6055
|
}
|
|
@@ -6028,10 +6069,7 @@ var LLMClient = class _LLMClient {
|
|
|
6028
6069
|
sanitizeErrorResponse(errorBody)
|
|
6029
6070
|
);
|
|
6030
6071
|
}
|
|
6031
|
-
|
|
6032
|
-
this.sessionCalls += 1;
|
|
6033
|
-
this.sessionTotalUsd += costUsd;
|
|
6034
|
-
this.recordCost(url, costUsd, { network: details.network });
|
|
6072
|
+
this.bookPaidRaw(url, details, retryResponse);
|
|
6035
6073
|
return retryResponse.json();
|
|
6036
6074
|
}
|
|
6037
6075
|
/**
|
|
@@ -7044,8 +7082,10 @@ var MusicClient = class {
|
|
|
7044
7082
|
throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
|
|
7045
7083
|
}
|
|
7046
7084
|
const data = await retryResponse.json();
|
|
7047
|
-
|
|
7048
|
-
|
|
7085
|
+
if (!settlementSkipped(retryResponse)) {
|
|
7086
|
+
this.sessionCalls++;
|
|
7087
|
+
this.sessionTotalUsd += 0.1575;
|
|
7088
|
+
}
|
|
7049
7089
|
const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
|
|
7050
7090
|
if (txHash) data.txHash = txHash;
|
|
7051
7091
|
return data;
|
|
@@ -7257,9 +7297,11 @@ var SpeechClient = class {
|
|
|
7257
7297
|
throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
|
|
7258
7298
|
}
|
|
7259
7299
|
const data = await retryResponse.json();
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7300
|
+
if (!settlementSkipped(retryResponse)) {
|
|
7301
|
+
this.sessionCalls++;
|
|
7302
|
+
const paidUsd = Number(details.amount) / 1e6;
|
|
7303
|
+
if (Number.isFinite(paidUsd)) this.sessionTotalUsd += paidUsd;
|
|
7304
|
+
}
|
|
7263
7305
|
const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
|
|
7264
7306
|
if (txHash) data.txHash = txHash;
|
|
7265
7307
|
return data;
|
|
@@ -7556,8 +7598,10 @@ var VideoClient = class {
|
|
|
7556
7598
|
}
|
|
7557
7599
|
if (lastStatus === "completed") {
|
|
7558
7600
|
const data = pollData;
|
|
7559
|
-
|
|
7560
|
-
|
|
7601
|
+
if (!settlementSkipped(pollResp)) {
|
|
7602
|
+
this.sessionCalls++;
|
|
7603
|
+
this.sessionTotalUsd += Number(details.amount) / 1e6;
|
|
7604
|
+
}
|
|
7561
7605
|
const txHash = pollResp.headers.get("x-payment-receipt") || pollResp.headers.get("X-Payment-Receipt");
|
|
7562
7606
|
if (txHash) data.txHash = txHash;
|
|
7563
7607
|
return data;
|
|
@@ -7852,8 +7896,10 @@ var VoiceClient = class {
|
|
|
7852
7896
|
throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
|
|
7853
7897
|
}
|
|
7854
7898
|
const data = await retryResponse.json();
|
|
7855
|
-
|
|
7856
|
-
|
|
7899
|
+
if (!settlementSkipped(retryResponse)) {
|
|
7900
|
+
this.sessionCalls++;
|
|
7901
|
+
this.sessionTotalUsd += CALL_PRICE_USD;
|
|
7902
|
+
}
|
|
7857
7903
|
const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
|
|
7858
7904
|
if (txHash) data.txHash = txHash;
|
|
7859
7905
|
return data;
|
|
@@ -8073,9 +8119,10 @@ var PhoneClient = class {
|
|
|
8073
8119
|
throw new PaymentError("Payment was rejected. Check your wallet balance.");
|
|
8074
8120
|
}
|
|
8075
8121
|
const data = await this.unwrap(retry, true);
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8122
|
+
if (!settlementSkipped(retry)) {
|
|
8123
|
+
this.sessionCalls += 1;
|
|
8124
|
+
this.sessionTotalUsd += Number.parseFloat(details.amount) / 1e6;
|
|
8125
|
+
}
|
|
8079
8126
|
const txHash = retry.headers.get("x-payment-receipt") || retry.headers.get("X-Payment-Receipt");
|
|
8080
8127
|
if (txHash && data && typeof data === "object" && !("txHash" in data)) {
|
|
8081
8128
|
data.txHash = txHash;
|
|
@@ -8240,9 +8287,10 @@ var PortraitClient = class {
|
|
|
8240
8287
|
throw new PaymentError("Payment was rejected. Check your wallet balance.");
|
|
8241
8288
|
}
|
|
8242
8289
|
const data = await this.unwrap(retry, true);
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8290
|
+
if (!settlementSkipped(retry)) {
|
|
8291
|
+
this.sessionCalls += 1;
|
|
8292
|
+
this.sessionTotalUsd += Number.parseFloat(details.amount) / 1e6;
|
|
8293
|
+
}
|
|
8246
8294
|
if (data && typeof data === "object" && !("txHash" in data)) {
|
|
8247
8295
|
const settlement = data.settlement;
|
|
8248
8296
|
if (settlement?.tx_hash) {
|
|
@@ -9059,9 +9107,11 @@ var RpcClient = class {
|
|
|
9059
9107
|
}
|
|
9060
9108
|
throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
|
|
9061
9109
|
}
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9110
|
+
if (!settlementSkipped(retryResponse)) {
|
|
9111
|
+
this.sessionCalls++;
|
|
9112
|
+
const paidUsd = Number(details.amount) / 1e6;
|
|
9113
|
+
if (Number.isFinite(paidUsd)) this.sessionTotalUsd += paidUsd;
|
|
9114
|
+
}
|
|
9065
9115
|
return { data: await retryResponse.json(), headers: retryResponse.headers };
|
|
9066
9116
|
}
|
|
9067
9117
|
async fetchWithTimeout(url, options) {
|
|
@@ -9208,7 +9258,7 @@ var BlockrunClient = class {
|
|
|
9208
9258
|
if (resp402.status !== 402) {
|
|
9209
9259
|
await this.throwApiError(resp402, `poll submit failed (${submitUrl})`);
|
|
9210
9260
|
}
|
|
9211
|
-
const paymentPayload = await this.signFrom402(resp402, submitUrl, {
|
|
9261
|
+
const { paymentPayload, costUsd } = await this.signFrom402(resp402, submitUrl, {
|
|
9212
9262
|
description: "BlockRun async job",
|
|
9213
9263
|
maxTimeoutSeconds: MAX_SIGNED_AUTH_SECONDS
|
|
9214
9264
|
});
|
|
@@ -9227,7 +9277,7 @@ var BlockrunClient = class {
|
|
|
9227
9277
|
await this.throwApiError(submitResp, `poll submit failed (${submitUrl})`);
|
|
9228
9278
|
}
|
|
9229
9279
|
if (submitResp.status === 200) {
|
|
9230
|
-
this.recordSpending();
|
|
9280
|
+
this.recordSpending(submitResp, costUsd);
|
|
9231
9281
|
return submitResp.json();
|
|
9232
9282
|
}
|
|
9233
9283
|
const submitData = await submitResp.json();
|
|
@@ -9264,7 +9314,7 @@ var BlockrunClient = class {
|
|
|
9264
9314
|
);
|
|
9265
9315
|
}
|
|
9266
9316
|
if (pollResp.status === 200 && lastStatus === "completed") {
|
|
9267
|
-
this.recordSpending();
|
|
9317
|
+
this.recordSpending(pollResp, costUsd);
|
|
9268
9318
|
return pollData;
|
|
9269
9319
|
}
|
|
9270
9320
|
if (pollResp.status !== 200 && pollResp.status !== 202 && pollResp.status !== 504) {
|
|
@@ -9305,7 +9355,7 @@ var BlockrunClient = class {
|
|
|
9305
9355
|
if (resp402.status === 200) {
|
|
9306
9356
|
streamResp = resp402;
|
|
9307
9357
|
} else if (resp402.status === 402) {
|
|
9308
|
-
const paymentPayload = await this.signFrom402(resp402, url, {
|
|
9358
|
+
const { paymentPayload, costUsd } = await this.signFrom402(resp402, url, {
|
|
9309
9359
|
description: "BlockRun stream"
|
|
9310
9360
|
});
|
|
9311
9361
|
streamResp = await this.fetchWithTimeout(url, {
|
|
@@ -9324,7 +9374,7 @@ var BlockrunClient = class {
|
|
|
9324
9374
|
if (!streamResp.ok) {
|
|
9325
9375
|
await this.throwApiError(streamResp, `stream failed after payment (${url})`);
|
|
9326
9376
|
}
|
|
9327
|
-
this.recordSpending();
|
|
9377
|
+
this.recordSpending(streamResp, costUsd);
|
|
9328
9378
|
} else {
|
|
9329
9379
|
await this.throwApiError(resp402, `stream failed (${url})`);
|
|
9330
9380
|
return;
|
|
@@ -9380,7 +9430,7 @@ var BlockrunClient = class {
|
|
|
9380
9430
|
return response.json();
|
|
9381
9431
|
}
|
|
9382
9432
|
async handlePaymentAndRetry(url, method, body, response) {
|
|
9383
|
-
const paymentPayload = await this.signFrom402(response, url, {
|
|
9433
|
+
const { paymentPayload, costUsd } = await this.signFrom402(response, url, {
|
|
9384
9434
|
description: "BlockRun"
|
|
9385
9435
|
});
|
|
9386
9436
|
const retryInit = {
|
|
@@ -9403,13 +9453,14 @@ var BlockrunClient = class {
|
|
|
9403
9453
|
if (!retry.ok) {
|
|
9404
9454
|
await this.throwApiError(retry, `${method} ${url} failed after payment`);
|
|
9405
9455
|
}
|
|
9406
|
-
this.recordSpending();
|
|
9456
|
+
this.recordSpending(retry, costUsd);
|
|
9407
9457
|
return retry.json();
|
|
9408
9458
|
}
|
|
9409
9459
|
/**
|
|
9410
9460
|
* Read a 402 response's payment requirements (header or body), then sign and
|
|
9411
|
-
* return the base64 PAYMENT-SIGNATURE payload
|
|
9412
|
-
*
|
|
9461
|
+
* return the base64 PAYMENT-SIGNATURE payload with what it pays, in USD.
|
|
9462
|
+
* The cost stays with the call that signed it (booked by `recordSpending`):
|
|
9463
|
+
* concurrent calls on one client never share it.
|
|
9413
9464
|
*/
|
|
9414
9465
|
async signFrom402(response, url, opts) {
|
|
9415
9466
|
let paymentHeader = response.headers.get("payment-required");
|
|
@@ -9427,8 +9478,8 @@ var BlockrunClient = class {
|
|
|
9427
9478
|
}
|
|
9428
9479
|
const paymentRequired = parsePaymentRequired(paymentHeader);
|
|
9429
9480
|
const details = extractPaymentDetails(paymentRequired);
|
|
9430
|
-
|
|
9431
|
-
|
|
9481
|
+
const costUsd = parseFloat(details.amount) / 1e6;
|
|
9482
|
+
const paymentPayload = await createPaymentPayload(
|
|
9432
9483
|
this.privateKey,
|
|
9433
9484
|
this.account.address,
|
|
9434
9485
|
details.recipient,
|
|
@@ -9445,14 +9496,20 @@ var BlockrunClient = class {
|
|
|
9445
9496
|
asset: details.asset
|
|
9446
9497
|
}
|
|
9447
9498
|
);
|
|
9499
|
+
return { paymentPayload, costUsd };
|
|
9448
9500
|
}
|
|
9449
|
-
/**
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9501
|
+
/**
|
|
9502
|
+
* Book `costUsd`, what the call that `response` answered signed. A response
|
|
9503
|
+
* whose settlement the gateway skipped ({@link settlementSkipped}:
|
|
9504
|
+
* `X-Settlement-Skipped: <reason>` or `X-Payment-Waived`) moved nothing, so
|
|
9505
|
+
* nothing is booked. `X-Payment-Settled: false` alone is still booked: the
|
|
9506
|
+
* payment is owed.
|
|
9507
|
+
*/
|
|
9508
|
+
recordSpending(response, costUsd) {
|
|
9509
|
+
if (settlementSkipped(response)) return;
|
|
9510
|
+
if (costUsd > 0) {
|
|
9453
9511
|
this.sessionCalls += 1;
|
|
9454
|
-
this.sessionTotalUsd +=
|
|
9455
|
-
this.pendingCostUsd = 0;
|
|
9512
|
+
this.sessionTotalUsd += costUsd;
|
|
9456
9513
|
}
|
|
9457
9514
|
}
|
|
9458
9515
|
async throwApiError(resp, prefix) {
|
|
@@ -10714,6 +10771,38 @@ function withoutClientSignedBatch(paymentRequired) {
|
|
|
10714
10771
|
const accepts = paymentRequired.accepts ?? [];
|
|
10715
10772
|
return accepts.some(clientSignedBatch) ? { ...paymentRequired, accepts: accepts.filter((accept) => !clientSignedBatch(accept)) } : paymentRequired;
|
|
10716
10773
|
}
|
|
10774
|
+
var DEPOSIT_BLOCKHASH_TTL_MS = 1e4;
|
|
10775
|
+
var MAX_DEPOSIT_BLOCKHASH_ENDPOINTS = 8;
|
|
10776
|
+
var depositBlockhashes = /* @__PURE__ */ new Map();
|
|
10777
|
+
async function finalizedBlockhash(rpcUrl, rpcHeaders) {
|
|
10778
|
+
const cached = depositBlockhashes.get(rpcUrl);
|
|
10779
|
+
if (cached && Date.now() - cached.fetchedAt < DEPOSIT_BLOCKHASH_TTL_MS) return cached;
|
|
10780
|
+
const result = await rpcRequest(rpcUrl, "getLatestBlockhash", [{ commitment: "finalized" }], rpcHeaders);
|
|
10781
|
+
const blockhash = result.value?.blockhash;
|
|
10782
|
+
if (typeof blockhash !== "string" || !blockhash) throw new Error("getLatestBlockhash returned no blockhash");
|
|
10783
|
+
const entry = {
|
|
10784
|
+
blockhash,
|
|
10785
|
+
lastValidBlockHeight: rpcInteger(result.value?.lastValidBlockHeight, "getLatestBlockhash lastValidBlockHeight"),
|
|
10786
|
+
fetchedAt: Date.now()
|
|
10787
|
+
};
|
|
10788
|
+
depositBlockhashes.delete(rpcUrl);
|
|
10789
|
+
depositBlockhashes.set(rpcUrl, entry);
|
|
10790
|
+
while (depositBlockhashes.size > MAX_DEPOSIT_BLOCKHASH_ENDPOINTS) {
|
|
10791
|
+
depositBlockhashes.delete(depositBlockhashes.keys().next().value);
|
|
10792
|
+
}
|
|
10793
|
+
return entry;
|
|
10794
|
+
}
|
|
10795
|
+
function withDepositBlockhash(paymentRequired, blockhash) {
|
|
10796
|
+
const accepts = paymentRequired.accepts ?? [];
|
|
10797
|
+
const lacking = (accept) => accept.scheme === BATCH_SCHEME && !accept.extra?.recentBlockhash;
|
|
10798
|
+
if (!accepts.some(lacking)) return paymentRequired;
|
|
10799
|
+
return {
|
|
10800
|
+
...paymentRequired,
|
|
10801
|
+
accepts: accepts.map(
|
|
10802
|
+
(accept) => lacking(accept) ? { ...accept, extra: { ...accept.extra, recentBlockhash: blockhash.blockhash, lastValidBlockHeight: blockhash.lastValidBlockHeight } } : accept
|
|
10803
|
+
)
|
|
10804
|
+
};
|
|
10805
|
+
}
|
|
10717
10806
|
var PAYMENT_IDENTIFIER = "payment-identifier";
|
|
10718
10807
|
function withPaymentIdentifier(paymentRequired) {
|
|
10719
10808
|
const declared = paymentRequired.extensions?.[PAYMENT_IDENTIFIER];
|
|
@@ -11671,6 +11760,8 @@ var SolanaBatchPayer = class {
|
|
|
11671
11760
|
wallet.refusedFor = outcome.errorReason;
|
|
11672
11761
|
}
|
|
11673
11762
|
}
|
|
11763
|
+
} else if (built.key && outcome.reason === "payment_required" && outcome.refusedForMs === void 0) {
|
|
11764
|
+
await this.recheckRefusedChannel(wallet, built.key, built.reclaimAnchor);
|
|
11674
11765
|
}
|
|
11675
11766
|
sent = void 0;
|
|
11676
11767
|
release();
|
|
@@ -11938,6 +12029,13 @@ var SolanaBatchPayer = class {
|
|
|
11938
12029
|
}
|
|
11939
12030
|
/** Build a new batch payment for a 402: the payload, its headers and its kind. */
|
|
11940
12031
|
async buildPayment(wallet, paymentRequired) {
|
|
12032
|
+
const acceptKey = channelKeyOf(trustedBatchAccept(paymentRequired, this.init.options.operators));
|
|
12033
|
+
let reclaimAnchor;
|
|
12034
|
+
try {
|
|
12035
|
+
const stored = acceptKey ? await wallet.book.base.get(acceptKey) : void 0;
|
|
12036
|
+
reclaimAnchor = stored ? savedReclaimAnchor(stored) : void 0;
|
|
12037
|
+
} catch {
|
|
12038
|
+
}
|
|
11941
12039
|
const { http, payload } = await this.createPayload(wallet, paymentRequired);
|
|
11942
12040
|
const scope = payScope.getStore();
|
|
11943
12041
|
if (scope?.afterError) {
|
|
@@ -11959,7 +12057,16 @@ var SolanaBatchPayer = class {
|
|
|
11959
12057
|
const record = known?.record;
|
|
11960
12058
|
kind = record && (record.pending === void 0 || record.hasConfirmedState) ? "top-up" : "open";
|
|
11961
12059
|
}
|
|
11962
|
-
return {
|
|
12060
|
+
return {
|
|
12061
|
+
http,
|
|
12062
|
+
payload,
|
|
12063
|
+
headers,
|
|
12064
|
+
kind,
|
|
12065
|
+
key: known?.key,
|
|
12066
|
+
...reclaimAnchor !== void 0 && known?.key === acceptKey ? { reclaimAnchor } : {},
|
|
12067
|
+
sentAt: Date.now(),
|
|
12068
|
+
sends: 0
|
|
12069
|
+
};
|
|
11963
12070
|
}
|
|
11964
12071
|
/**
|
|
11965
12072
|
* Write a deposit's intent to the wallet's journal before it is sent
|
|
@@ -12185,7 +12292,8 @@ var SolanaBatchPayer = class {
|
|
|
12185
12292
|
const http = (await this.build(wallet)).http;
|
|
12186
12293
|
try {
|
|
12187
12294
|
await this.prepareTopUp(wallet, paymentRequired);
|
|
12188
|
-
|
|
12295
|
+
const required = await this.withDepositBlockhash(wallet, withoutClientSignedBatch(paymentRequired));
|
|
12296
|
+
return { http, payload: await http.createPaymentPayload(withPaymentIdentifier(required)) };
|
|
12189
12297
|
} catch (err) {
|
|
12190
12298
|
if (pass > 0 || !isResyncRequired(err) || wallet.resyncs.size === 0) throw err;
|
|
12191
12299
|
} finally {
|
|
@@ -12193,6 +12301,112 @@ var SolanaBatchPayer = class {
|
|
|
12193
12301
|
}
|
|
12194
12302
|
}
|
|
12195
12303
|
}
|
|
12304
|
+
/**
|
|
12305
|
+
* The 402 to build this payment from: with a finalized blockhash
|
|
12306
|
+
* ({@link withDepositBlockhash}) when the payment will likely carry a
|
|
12307
|
+
* deposit (no usable record for the trusted accept's channel, so an open,
|
|
12308
|
+
* or a top-up {@link prepareTopUp} just sized). An authorization signs no
|
|
12309
|
+
* transaction and costs no RPC read. Best effort: a failed read leaves the
|
|
12310
|
+
* 402 as it is, and `@x402/svm` fetches its own blockhash.
|
|
12311
|
+
*/
|
|
12312
|
+
async withDepositBlockhash(wallet, paymentRequired) {
|
|
12313
|
+
const accept = trustedBatchAccept(paymentRequired, this.init.options.operators);
|
|
12314
|
+
const key = accept ? channelKeyOf(accept) : void 0;
|
|
12315
|
+
if (!accept || accept.extra?.recentBlockhash) return paymentRequired;
|
|
12316
|
+
let deposit = wallet.topUp !== void 0;
|
|
12317
|
+
if (!deposit) {
|
|
12318
|
+
try {
|
|
12319
|
+
const record = key ? await wallet.book.base.get(key) : void 0;
|
|
12320
|
+
deposit = !record || typeof record.channelId !== "string" || record.pending !== void 0;
|
|
12321
|
+
} catch {
|
|
12322
|
+
deposit = true;
|
|
12323
|
+
}
|
|
12324
|
+
}
|
|
12325
|
+
if (!deposit) return paymentRequired;
|
|
12326
|
+
try {
|
|
12327
|
+
return withDepositBlockhash(paymentRequired, await finalizedBlockhash(this.init.rpcUrl, this.init.rpcHeaders));
|
|
12328
|
+
} catch {
|
|
12329
|
+
return paymentRequired;
|
|
12330
|
+
}
|
|
12331
|
+
}
|
|
12332
|
+
/**
|
|
12333
|
+
* Queue a re-read of a stored channel whose account is gone at finalized
|
|
12334
|
+
* and at confirmed commitment (closed, its account reclaimed), anchored at
|
|
12335
|
+
* the SDK's own `getBlockHeight` at confirmed. The anchor is saved in the
|
|
12336
|
+
* record (`reclaimAnchorHeight`), so a short-lived process finishes the
|
|
12337
|
+
* wait instead of starting it over; a failed read leaves resync to take it,
|
|
12338
|
+
* and a failed save only costs that. resync pays exact until finality is
|
|
12339
|
+
* LANDING_MARGIN_BLOCKS past the anchor, then drops the record if the
|
|
12340
|
+
* finalized chain still has no account (a lagging read finds the channel
|
|
12341
|
+
* and rewrites the record, without the anchor).
|
|
12342
|
+
*/
|
|
12343
|
+
async queueReclaimed(wallet, key, record, channelId, savedAnchor) {
|
|
12344
|
+
let anchorHeight = savedReclaimAnchor(record) ?? savedAnchor;
|
|
12345
|
+
if (anchorHeight === void 0) {
|
|
12346
|
+
try {
|
|
12347
|
+
anchorHeight = rpcInteger(
|
|
12348
|
+
await rpcRequest(this.init.rpcUrl, "getBlockHeight", [{ commitment: "confirmed" }], this.init.rpcHeaders),
|
|
12349
|
+
"getBlockHeight"
|
|
12350
|
+
);
|
|
12351
|
+
} catch {
|
|
12352
|
+
}
|
|
12353
|
+
}
|
|
12354
|
+
if (anchorHeight !== void 0 && savedReclaimAnchor(record) !== anchorHeight) {
|
|
12355
|
+
try {
|
|
12356
|
+
await wallet.book.set(key, { ...record, reclaimAnchorHeight: anchorHeight });
|
|
12357
|
+
} catch {
|
|
12358
|
+
}
|
|
12359
|
+
}
|
|
12360
|
+
addResyncTarget(wallet, {
|
|
12361
|
+
key,
|
|
12362
|
+
channelId,
|
|
12363
|
+
channelConfig: record.channelConfig,
|
|
12364
|
+
cumulative: parseAtomic(record.chargedCumulativeAmount),
|
|
12365
|
+
knownDeposit: parseAtomic(record.deposit),
|
|
12366
|
+
...anchorHeight !== void 0 ? { anchorHeight } : {},
|
|
12367
|
+
reason: "channel_unusable"
|
|
12368
|
+
});
|
|
12369
|
+
}
|
|
12370
|
+
/**
|
|
12371
|
+
* After the gateway refused an authorization with a plain 402 (no receipt,
|
|
12372
|
+
* not an admission refusal), check the channel it paid into. That is how
|
|
12373
|
+
* the gateway answers an authorization into a channel the facilitator
|
|
12374
|
+
* closed (its verify rejects it), and a channel with deposit to spare is
|
|
12375
|
+
* otherwise never read again: every call would sign another authorization
|
|
12376
|
+
* into it and pay exact. A channel the finalized chain shows closing,
|
|
12377
|
+
* closed or not this wallet's is re-read through resync (record dropped,
|
|
12378
|
+
* or kept and paid exact); one gone at every commitment is queued as
|
|
12379
|
+
* {@link queueReclaimed}; an open one of this wallet's, or one only at
|
|
12380
|
+
* confirmed (not finalized yet), is left as it is. A queued re-read is run
|
|
12381
|
+
* at once. Best effort: a failed read changes nothing, and the next
|
|
12382
|
+
* refusal checks again.
|
|
12383
|
+
*/
|
|
12384
|
+
async recheckRefusedChannel(wallet, key, savedAnchor) {
|
|
12385
|
+
try {
|
|
12386
|
+
const record = await wallet.book.get(key);
|
|
12387
|
+
if (!record || record.pending || typeof record.channelId !== "string" || wallet.resyncs.has(key)) return;
|
|
12388
|
+
const channelId = record.channelId;
|
|
12389
|
+
const channel = await readChannelAccount(this.init.rpcUrl, channelId, this.init.rpcHeaders);
|
|
12390
|
+
if (channel !== void 0) {
|
|
12391
|
+
if (channel.open && channel.payer === await this.init.address() && channel.authorizedSigner === record.channelConfig.payerAuthorizer && channel.mint === record.channelConfig.token) {
|
|
12392
|
+
return;
|
|
12393
|
+
}
|
|
12394
|
+
addResyncTarget(wallet, {
|
|
12395
|
+
key,
|
|
12396
|
+
channelId,
|
|
12397
|
+
channelConfig: record.channelConfig,
|
|
12398
|
+
cumulative: parseAtomic(record.chargedCumulativeAmount),
|
|
12399
|
+
knownDeposit: parseAtomic(record.deposit),
|
|
12400
|
+
reason: "channel_unusable"
|
|
12401
|
+
});
|
|
12402
|
+
} else {
|
|
12403
|
+
if (await readChannelAccount(this.init.rpcUrl, channelId, this.init.rpcHeaders, { commitment: "confirmed" }) !== void 0) return;
|
|
12404
|
+
await this.queueReclaimed(wallet, key, record, channelId, savedAnchor);
|
|
12405
|
+
}
|
|
12406
|
+
await this.resync(wallet);
|
|
12407
|
+
} catch {
|
|
12408
|
+
}
|
|
12409
|
+
}
|
|
12196
12410
|
/**
|
|
12197
12411
|
* When this payment will top up the wallet's channel, read how much of its
|
|
12198
12412
|
* deposit is already settled on-chain (at `finalized` commitment, so never
|
|
@@ -12246,31 +12460,7 @@ var SolanaBatchPayer = class {
|
|
|
12246
12460
|
}
|
|
12247
12461
|
if (channel === void 0 && unfinalized) return;
|
|
12248
12462
|
if (channel === void 0) {
|
|
12249
|
-
|
|
12250
|
-
if (anchorHeight === void 0) {
|
|
12251
|
-
try {
|
|
12252
|
-
anchorHeight = rpcInteger(
|
|
12253
|
-
await rpcRequest(this.init.rpcUrl, "getBlockHeight", [{ commitment: "confirmed" }], this.init.rpcHeaders),
|
|
12254
|
-
"getBlockHeight"
|
|
12255
|
-
);
|
|
12256
|
-
} catch {
|
|
12257
|
-
}
|
|
12258
|
-
if (anchorHeight !== void 0) {
|
|
12259
|
-
try {
|
|
12260
|
-
await wallet.book.set(key, { ...record, reclaimAnchorHeight: anchorHeight });
|
|
12261
|
-
} catch {
|
|
12262
|
-
}
|
|
12263
|
-
}
|
|
12264
|
-
}
|
|
12265
|
-
addResyncTarget(wallet, {
|
|
12266
|
-
key,
|
|
12267
|
-
channelId: record.channelId,
|
|
12268
|
-
channelConfig: record.channelConfig,
|
|
12269
|
-
cumulative: parseAtomic(record.chargedCumulativeAmount),
|
|
12270
|
-
knownDeposit: deposit,
|
|
12271
|
-
...anchorHeight !== void 0 ? { anchorHeight } : {},
|
|
12272
|
-
reason: "channel_unusable"
|
|
12273
|
-
});
|
|
12463
|
+
await this.queueReclaimed(wallet, key, record, record.channelId);
|
|
12274
12464
|
throw new ChannelResyncRequiredError(record.channelId);
|
|
12275
12465
|
}
|
|
12276
12466
|
if (!channel.open || channel.payer !== await this.init.address() || channel.authorizedSigner !== record.channelConfig.payerAuthorizer || channel.mint !== record.channelConfig.token) {
|
|
@@ -12445,7 +12635,6 @@ var SolanaBatchPayer = class {
|
|
|
12445
12635
|
// src/solana-client.ts
|
|
12446
12636
|
var SOLANA_API_URL = "https://sol.blockrun.ai/api";
|
|
12447
12637
|
var DEFAULT_MAX_TOKENS2 = 1024;
|
|
12448
|
-
var DEFAULT_TIMEOUT14 = 6e4;
|
|
12449
12638
|
var STALE_BLOCKHASH_RETRY_BACKOFFS_MS = [500, 2e3];
|
|
12450
12639
|
var MAX_PAYMENT_FAILURE_BYTES = 64 * 1024;
|
|
12451
12640
|
var DEFAULT_VIDEO_MODEL = "xai/grok-imagine-video";
|
|
@@ -12613,7 +12802,7 @@ var SolanaLLMClient = class {
|
|
|
12613
12802
|
const rpc = resolveRpcConfig(options.rpcUrl, options.rpcHeaders);
|
|
12614
12803
|
this.rpcUrl = rpc.url;
|
|
12615
12804
|
this.rpcHeaders = rpc.headers;
|
|
12616
|
-
this.timeout = options.timeout ||
|
|
12805
|
+
this.timeout = options.timeout || DEFAULT_TIMEOUT;
|
|
12617
12806
|
if (options.batch && !this.apiAuth) {
|
|
12618
12807
|
this.batchPayer = new SolanaBatchPayer({
|
|
12619
12808
|
options: options.batch,
|
|
@@ -12907,7 +13096,7 @@ var SolanaLLMClient = class {
|
|
|
12907
13096
|
body: requestBody
|
|
12908
13097
|
});
|
|
12909
13098
|
await this.assertPaid(paid);
|
|
12910
|
-
this.recordSettlement(costUsd);
|
|
13099
|
+
this.recordSettlement(costUsd, paid);
|
|
12911
13100
|
return paid;
|
|
12912
13101
|
} catch (error) {
|
|
12913
13102
|
if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
|
|
@@ -13294,7 +13483,7 @@ var SolanaLLMClient = class {
|
|
|
13294
13483
|
closesAtStart
|
|
13295
13484
|
);
|
|
13296
13485
|
if (batch.kind === "paid") {
|
|
13297
|
-
this.recordSettlement(batch.chargedUsd);
|
|
13486
|
+
this.recordSettlement(batch.chargedUsd, batch.response);
|
|
13298
13487
|
const paid = await this.readPaidJson(batch.response);
|
|
13299
13488
|
return withCost(paid, batch.response, batch.chargedUsd, "batch");
|
|
13300
13489
|
}
|
|
@@ -13411,7 +13600,7 @@ var SolanaLLMClient = class {
|
|
|
13411
13600
|
};
|
|
13412
13601
|
const deliver = (data, response) => {
|
|
13413
13602
|
if (kind === "video") {
|
|
13414
|
-
if (videoCostUsd !== void 0) this.recordSettlement(videoCostUsd);
|
|
13603
|
+
if (videoCostUsd !== void 0) this.recordSettlement(videoCostUsd, response);
|
|
13415
13604
|
const txHash = response.headers.get("x-payment-receipt");
|
|
13416
13605
|
if (txHash) data.txHash = txHash;
|
|
13417
13606
|
}
|
|
@@ -13658,7 +13847,7 @@ var SolanaLLMClient = class {
|
|
|
13658
13847
|
throw withDisposition(error, "paid-or-in-doubt");
|
|
13659
13848
|
}
|
|
13660
13849
|
await this.assertPaid(retryResponse);
|
|
13661
|
-
|
|
13850
|
+
this.recordSettlement(costUsd, retryResponse);
|
|
13662
13851
|
const paid = await this.readPaidJson(retryResponse);
|
|
13663
13852
|
return withCost(paid, retryResponse, costUsd, "exact");
|
|
13664
13853
|
}
|
|
@@ -13718,11 +13907,11 @@ var SolanaLLMClient = class {
|
|
|
13718
13907
|
await this.assertPaid(retryResponse);
|
|
13719
13908
|
if (retryResponse.status === 202) {
|
|
13720
13909
|
const kind = pollKindFor(url);
|
|
13721
|
-
if (kind !== "video") this.recordSettlement(costUsd);
|
|
13910
|
+
if (kind !== "video") this.recordSettlement(costUsd, retryResponse);
|
|
13722
13911
|
const submitted = await retryResponse.json();
|
|
13723
13912
|
return this.followSolanaJob(submitted, media.pollBudgetMs ?? this.timeout, kind);
|
|
13724
13913
|
}
|
|
13725
|
-
this.recordSettlement(costUsd);
|
|
13914
|
+
this.recordSettlement(costUsd, retryResponse);
|
|
13726
13915
|
return retryResponse.json();
|
|
13727
13916
|
}
|
|
13728
13917
|
async getWithPaymentRaw(endpoint, params) {
|
|
@@ -13766,7 +13955,7 @@ var SolanaLLMClient = class {
|
|
|
13766
13955
|
}
|
|
13767
13956
|
});
|
|
13768
13957
|
await this.assertPaid(retryResponse);
|
|
13769
|
-
this.recordSettlement(costUsd);
|
|
13958
|
+
this.recordSettlement(costUsd, retryResponse);
|
|
13770
13959
|
return retryResponse.json();
|
|
13771
13960
|
}
|
|
13772
13961
|
/**
|
|
@@ -13804,8 +13993,17 @@ var SolanaLLMClient = class {
|
|
|
13804
13993
|
);
|
|
13805
13994
|
}
|
|
13806
13995
|
}
|
|
13807
|
-
/**
|
|
13808
|
-
|
|
13996
|
+
/**
|
|
13997
|
+
* Count one settled x402 payment against the session total.
|
|
13998
|
+
*
|
|
13999
|
+
* Takes the paid response so no booking site can forget the check: a
|
|
14000
|
+
* response whose settlement the gateway skipped ({@link settlementSkipped}:
|
|
14001
|
+
* `X-Settlement-Skipped: free-fallback | empty-result | unexecuted | …`, or
|
|
14002
|
+
* `X-Payment-Waived`) moved nothing on chain and books nothing.
|
|
14003
|
+
* `X-Payment-Settled: false` alone is still booked: the payment is owed.
|
|
14004
|
+
*/
|
|
14005
|
+
recordSettlement(costUsd, response) {
|
|
14006
|
+
if (settlementSkipped(response)) return;
|
|
13809
14007
|
this.sessionCalls += 1;
|
|
13810
14008
|
this.sessionTotalUsd += costUsd;
|
|
13811
14009
|
}
|