@oneshot-agent/sdk 0.28.1 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/dist/deadline.d.ts +9 -0
- package/dist/deadline.d.ts.map +1 -0
- package/dist/deadline.js +39 -0
- package/dist/deadline.js.map +1 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +15 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +79 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +498 -184
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +199 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -37,9 +37,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.OneShot = exports.executeSwap = exports.getSwapQuote = exports.CdpWalletProvider = exports.EthersWalletProvider = void 0;
|
|
40
|
+
const deadline_1 = require("./deadline");
|
|
41
|
+
const errors_1 = require("./errors");
|
|
40
42
|
const ethers_1 = require("ethers");
|
|
41
43
|
const ethers_2 = require("./providers/ethers");
|
|
42
|
-
const
|
|
44
|
+
const errors_2 = require("./errors");
|
|
43
45
|
var ethers_3 = require("./providers/ethers");
|
|
44
46
|
Object.defineProperty(exports, "EthersWalletProvider", { enumerable: true, get: function () { return ethers_3.EthersWalletProvider; } });
|
|
45
47
|
var cdp_1 = require("./providers/cdp");
|
|
@@ -49,7 +51,7 @@ Object.defineProperty(exports, "getSwapQuote", { enumerable: true, get: function
|
|
|
49
51
|
Object.defineProperty(exports, "executeSwap", { enumerable: true, get: function () { return swap_1.executeSwap; } });
|
|
50
52
|
__exportStar(require("./errors"), exports);
|
|
51
53
|
// Keep in sync with package.json `version`. Guarded by version.test.ts.
|
|
52
|
-
const SDK_VERSION = '0.
|
|
54
|
+
const SDK_VERSION = '0.31.0';
|
|
53
55
|
/** HTTP poll cadence while push is unconfirmed: fast first checks, settling at 2s. */
|
|
54
56
|
const HTTP_POLL_BACKOFF_MS = [300, 600, 1000, 2000];
|
|
55
57
|
/** HTTP poll cadence once the WebSocket has delivered for this request. */
|
|
@@ -66,6 +68,29 @@ function chainIdFromNetwork(network) {
|
|
|
66
68
|
return m ? Number(m[1]) : undefined;
|
|
67
69
|
}
|
|
68
70
|
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
|
|
71
|
+
// ETH-currency mode. A payment is an EIP-3009 authorization that the
|
|
72
|
+
// facilitator settles AFTER the API has responded, so for seconds after a
|
|
73
|
+
// successful call `balanceOf` still shows pre-payment funds. The SDK therefore
|
|
74
|
+
// keeps a ledger of signed-but-unsettled authorizations and works from
|
|
75
|
+
// `balanceOf − pending`, over-reserving (one early buffered swap) rather than
|
|
76
|
+
// under-reserving (a failed settlement).
|
|
77
|
+
/** One balanceOf read per paid call, deduped within roughly one Base block. */
|
|
78
|
+
const USDC_BALANCE_CACHE_MS = 2000;
|
|
79
|
+
/** How long a signed authorization stays subtracted from the on-chain balance (settlement lands well within this). */
|
|
80
|
+
const USDC_RESERVATION_TTL_MS = 90000;
|
|
81
|
+
const DEFAULT_SWAP_BUFFER_MULTIPLIER = 10;
|
|
82
|
+
const MAX_SWAP_BUFFER_MULTIPLIER = 1000;
|
|
83
|
+
/** Fixed-point scale for the multiplier (6 decimals, matching USDC). */
|
|
84
|
+
const SWAP_MULTIPLIER_SCALE = 1000000;
|
|
85
|
+
const ERC20_BALANCE_ABI = ['function balanceOf(address) view returns (uint256)'];
|
|
86
|
+
function validateSwapBufferMultiplier(m) {
|
|
87
|
+
if (m === undefined)
|
|
88
|
+
return DEFAULT_SWAP_BUFFER_MULTIPLIER;
|
|
89
|
+
if (typeof m !== 'number' || !Number.isFinite(m) || m < 1 || m > MAX_SWAP_BUFFER_MULTIPLIER) {
|
|
90
|
+
throw new errors_2.ValidationError(`swapBufferMultiplier must be a finite number between 1 and ${MAX_SWAP_BUFFER_MULTIPLIER}`, 'swapBufferMultiplier');
|
|
91
|
+
}
|
|
92
|
+
return m;
|
|
93
|
+
}
|
|
69
94
|
// ============================================================================
|
|
70
95
|
// Public types — defined in ./types.ts. Re-exported below so existing
|
|
71
96
|
// consumer imports (`import { EmailToolOptions, ... } from '@oneshot-agent/sdk'`)
|
|
@@ -95,21 +120,21 @@ function validateBudgetConfig(budgets) {
|
|
|
95
120
|
// A typo'd key (`daliy`) from an untyped caller must not silently mean "no cap".
|
|
96
121
|
for (const key of Object.keys(budgets)) {
|
|
97
122
|
if (!['daily', 'perTransaction', 'alertAt', 'pauseAt'].includes(key)) {
|
|
98
|
-
throw new
|
|
123
|
+
throw new errors_2.ValidationError(`budgets.${key} is not a recognized field`, `budgets.${key}`);
|
|
99
124
|
}
|
|
100
125
|
}
|
|
101
126
|
const positive = (v, field) => {
|
|
102
127
|
if (v === undefined)
|
|
103
128
|
return;
|
|
104
129
|
if (typeof v !== 'number' || !Number.isFinite(v) || v <= 0) {
|
|
105
|
-
throw new
|
|
130
|
+
throw new errors_2.ValidationError(`budgets.${field} must be a positive number`, `budgets.${field}`);
|
|
106
131
|
}
|
|
107
132
|
};
|
|
108
133
|
const fraction = (v, field) => {
|
|
109
134
|
if (v === undefined)
|
|
110
135
|
return;
|
|
111
136
|
if (typeof v !== 'number' || !Number.isFinite(v) || v <= 0 || v > 1) {
|
|
112
|
-
throw new
|
|
137
|
+
throw new errors_2.ValidationError(`budgets.${field} must be a fraction in (0, 1]`, `budgets.${field}`);
|
|
113
138
|
}
|
|
114
139
|
};
|
|
115
140
|
positive(budgets.daily, 'daily');
|
|
@@ -147,18 +172,24 @@ class OneShot {
|
|
|
147
172
|
const walletProvider = new ethers_2.EthersWalletProvider(config.privateKey, rpcProvider);
|
|
148
173
|
return new OneShot(config, walletProvider);
|
|
149
174
|
}
|
|
150
|
-
throw new
|
|
175
|
+
throw new errors_2.ValidationError('Provide one of: privateKey, cdp, or walletProvider', 'config');
|
|
151
176
|
}
|
|
152
177
|
/**
|
|
153
178
|
* Sync constructor — works with privateKey (backwards compatible).
|
|
154
179
|
* For CDP wallets, use OneShot.create() instead.
|
|
155
180
|
*/
|
|
156
181
|
constructor(config, walletProvider) {
|
|
182
|
+
/** ETH mode: signed payments not yet observed as settled, keyed by reservation id. */
|
|
183
|
+
this._usdcPending = new Map();
|
|
184
|
+
this._usdcReservationSeq = 0;
|
|
185
|
+
/** ETH mode: serializes read → decide → swap → reserve per instance (also prevents concurrent swap nonce races). */
|
|
186
|
+
this._usdcLock = Promise.resolve();
|
|
157
187
|
this.baseUrl = config.baseUrl ?? BASE_URL;
|
|
158
188
|
this.debug = config.debug ?? false;
|
|
159
189
|
this.logger = config.logger ?? console.log;
|
|
160
190
|
this._currency = config.currency ?? 'USDC';
|
|
161
191
|
this._slippage = config.slippage ?? 0.01;
|
|
192
|
+
this._swapBufferMultiplier = validateSwapBufferMultiplier(config.swapBufferMultiplier);
|
|
162
193
|
this._budgets = validateBudgetConfig(config.budgets);
|
|
163
194
|
this._alertEmail = config.alerts?.email;
|
|
164
195
|
this.rpcProvider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl ?? RPC_URL);
|
|
@@ -169,10 +200,10 @@ class OneShot {
|
|
|
169
200
|
this.provider = new ethers_2.EthersWalletProvider(config.privateKey, this.rpcProvider);
|
|
170
201
|
}
|
|
171
202
|
else {
|
|
172
|
-
throw new
|
|
203
|
+
throw new errors_2.ValidationError('Provide privateKey or use OneShot.create() for CDP/custom wallets', 'config');
|
|
173
204
|
}
|
|
174
205
|
if (this._currency === 'ETH' && !this.provider.sendTransaction) {
|
|
175
|
-
throw new
|
|
206
|
+
throw new errors_2.ValidationError('ETH currency mode requires a wallet provider that supports sendTransaction', 'currency');
|
|
176
207
|
}
|
|
177
208
|
if (this.debug) {
|
|
178
209
|
this.log(`SDK initialized — chain=${CHAIN_ID} currency=${this._currency}`);
|
|
@@ -196,6 +227,10 @@ class OneShot {
|
|
|
196
227
|
get slippage() {
|
|
197
228
|
return this._slippage;
|
|
198
229
|
}
|
|
230
|
+
/** ETH mode: how many payments' worth of USDC a swap buys (default 10). */
|
|
231
|
+
get swapBufferMultiplier() {
|
|
232
|
+
return this._swapBufferMultiplier;
|
|
233
|
+
}
|
|
199
234
|
/** The budget config this instance was constructed with, if any. */
|
|
200
235
|
get budgetConfig() {
|
|
201
236
|
return this._budgets;
|
|
@@ -280,7 +315,7 @@ class OneShot {
|
|
|
280
315
|
headers: this.headers(),
|
|
281
316
|
});
|
|
282
317
|
if (!response.ok) {
|
|
283
|
-
throw new
|
|
318
|
+
throw new errors_2.ToolError('Failed to list domains', response.status, await response.text());
|
|
284
319
|
}
|
|
285
320
|
return response.json();
|
|
286
321
|
}
|
|
@@ -292,7 +327,7 @@ class OneShot {
|
|
|
292
327
|
headers: this.headers(),
|
|
293
328
|
});
|
|
294
329
|
if (!response.ok) {
|
|
295
|
-
throw new
|
|
330
|
+
throw new errors_2.ToolError('Failed to pause domain', response.status, await response.text());
|
|
296
331
|
}
|
|
297
332
|
return response.json();
|
|
298
333
|
}
|
|
@@ -304,7 +339,7 @@ class OneShot {
|
|
|
304
339
|
headers: this.headers(),
|
|
305
340
|
});
|
|
306
341
|
if (!response.ok) {
|
|
307
|
-
throw new
|
|
342
|
+
throw new errors_2.ToolError('Failed to resume domain', response.status, await response.text());
|
|
308
343
|
}
|
|
309
344
|
return response.json();
|
|
310
345
|
}
|
|
@@ -317,7 +352,7 @@ class OneShot {
|
|
|
317
352
|
}
|
|
318
353
|
async enrichProfile(options) {
|
|
319
354
|
if (!options.linkedin_url && !options.email && !options.name) {
|
|
320
|
-
throw new
|
|
355
|
+
throw new errors_2.ValidationError('At least one of linkedin_url, email, or name is required', 'identifier');
|
|
321
356
|
}
|
|
322
357
|
return this.tool('enrich/profile', { ...options });
|
|
323
358
|
}
|
|
@@ -326,14 +361,58 @@ class OneShot {
|
|
|
326
361
|
}
|
|
327
362
|
async enrichCompany(options) {
|
|
328
363
|
if (!options.domain && !options.name && !options.linkedin_url && !options.ticker) {
|
|
329
|
-
throw new
|
|
364
|
+
throw new errors_2.ValidationError('At least one of domain, name, linkedin_url, or ticker is required', 'identifier');
|
|
330
365
|
}
|
|
331
366
|
return this.tool('enrich/company', { ...options });
|
|
332
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* Discover local businesses (restaurants, contractors, practices) by
|
|
370
|
+
* category/keywords × location. Flat price per search, not per row.
|
|
371
|
+
*/
|
|
372
|
+
async localSearch(options) {
|
|
373
|
+
if (!options.location || options.location.length === 0) {
|
|
374
|
+
throw new errors_2.ValidationError('location is required (e.g. ["Austin, TX"])', 'location');
|
|
375
|
+
}
|
|
376
|
+
if (!(options.category && options.category.length) && !(options.keywords && options.keywords.length)) {
|
|
377
|
+
throw new errors_2.ValidationError('At least one of category or keywords is required', 'category');
|
|
378
|
+
}
|
|
379
|
+
return this.tool('local/search', { ...options, limit: options.limit ?? 100 });
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Resolve a business name + one locating field to its domain, phone,
|
|
383
|
+
* category and operating status. A miss resolves with `found: false`
|
|
384
|
+
* (a completed job), never a rejection.
|
|
385
|
+
*/
|
|
386
|
+
async localResolve(options) {
|
|
387
|
+
this.validate(options.name, 'name');
|
|
388
|
+
if (!options.address && !options.city && !options.postal_code && !options.phone) {
|
|
389
|
+
throw new errors_2.ValidationError('name plus at least one of address, city, postal_code, or phone is required', 'address');
|
|
390
|
+
}
|
|
391
|
+
return this.tool('local/resolve', { ...options });
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Federal contract opportunities (SAM.gov) by NAICS code — Sources Sought
|
|
395
|
+
* and Presolicitation notices with the contracting officer's published
|
|
396
|
+
* contact. Flat price per search; zero notices is a completed result.
|
|
397
|
+
*/
|
|
398
|
+
async govSolicitations(options) {
|
|
399
|
+
if (!options.naics || options.naics.length === 0) {
|
|
400
|
+
throw new errors_2.ValidationError('naics is required (one or more 6-digit codes, e.g. ["541511"])', 'naics');
|
|
401
|
+
}
|
|
402
|
+
const bad = options.naics.find(c => !/^\d{6}$/.test(String(c)));
|
|
403
|
+
if (bad !== undefined) {
|
|
404
|
+
throw new errors_2.ValidationError(`NAICS codes are 6 digits (got ${JSON.stringify(bad)})`, 'naics');
|
|
405
|
+
}
|
|
406
|
+
return this.tool('gov/solicitations', {
|
|
407
|
+
...options,
|
|
408
|
+
notice_types: options.notice_types ?? ['r', 'p'],
|
|
409
|
+
limit: options.limit ?? 100,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
333
412
|
async findEmail(options) {
|
|
334
413
|
this.validate(options.company_domain, 'company_domain');
|
|
335
414
|
if (!options.full_name && !(options.first_name && options.last_name)) {
|
|
336
|
-
throw new
|
|
415
|
+
throw new errors_2.ValidationError('Either full_name or both first_name and last_name required', 'name');
|
|
337
416
|
}
|
|
338
417
|
return this.tool('enrich/email', { ...options });
|
|
339
418
|
}
|
|
@@ -343,13 +422,13 @@ class OneShot {
|
|
|
343
422
|
}
|
|
344
423
|
async deepResearchPerson(options) {
|
|
345
424
|
if (!options.email && !options.social_media_url && !options.name) {
|
|
346
|
-
throw new
|
|
425
|
+
throw new errors_2.ValidationError('At least one of email, social_media_url, or name is required', 'identifier');
|
|
347
426
|
}
|
|
348
427
|
return this.tool('research/person', { ...options });
|
|
349
428
|
}
|
|
350
429
|
async socialProfiles(options) {
|
|
351
430
|
if (!options.email && !options.social_media_url) {
|
|
352
|
-
throw new
|
|
431
|
+
throw new errors_2.ValidationError('At least one of email or social_media_url is required', 'identifier');
|
|
353
432
|
}
|
|
354
433
|
return this.tool('research/social', { ...options });
|
|
355
434
|
}
|
|
@@ -364,7 +443,7 @@ class OneShot {
|
|
|
364
443
|
}
|
|
365
444
|
async personInterests(options) {
|
|
366
445
|
if (!options.email && !options.phone && !options.social_media_url) {
|
|
367
|
-
throw new
|
|
446
|
+
throw new errors_2.ValidationError('At least one of email, phone, or social_media_url is required', 'identifier');
|
|
368
447
|
}
|
|
369
448
|
return this.tool('research/interests', { ...options });
|
|
370
449
|
}
|
|
@@ -382,7 +461,7 @@ class OneShot {
|
|
|
382
461
|
headers: await this.signedReadHeaders()
|
|
383
462
|
});
|
|
384
463
|
if (!response.ok) {
|
|
385
|
-
throw new
|
|
464
|
+
throw new errors_2.ToolError('Failed to list inbox', response.status, await response.text());
|
|
386
465
|
}
|
|
387
466
|
return response.json();
|
|
388
467
|
}
|
|
@@ -392,10 +471,10 @@ class OneShot {
|
|
|
392
471
|
headers: await this.signedReadHeaders()
|
|
393
472
|
});
|
|
394
473
|
if (response.status === 404) {
|
|
395
|
-
throw new
|
|
474
|
+
throw new errors_2.ToolError('Email not found', 404, 'Email not found');
|
|
396
475
|
}
|
|
397
476
|
if (!response.ok) {
|
|
398
|
-
throw new
|
|
477
|
+
throw new errors_2.ToolError('Failed to get email', response.status, await response.text());
|
|
399
478
|
}
|
|
400
479
|
return response.json();
|
|
401
480
|
}
|
|
@@ -460,13 +539,13 @@ class OneShot {
|
|
|
460
539
|
this.validate(options.objective, 'objective');
|
|
461
540
|
this.validate(options.target_number, 'target_number');
|
|
462
541
|
if (Array.isArray(options.target_number) && options.target_number.length === 0) {
|
|
463
|
-
throw new
|
|
542
|
+
throw new errors_2.ValidationError('target_number array cannot be empty', 'target_number');
|
|
464
543
|
}
|
|
465
544
|
if (options.objective.length < 10) {
|
|
466
|
-
throw new
|
|
545
|
+
throw new errors_2.ValidationError('Objective must be at least 10 characters', 'objective');
|
|
467
546
|
}
|
|
468
547
|
if (options.max_duration_minutes !== undefined && (options.max_duration_minutes < 1 || options.max_duration_minutes > 30)) {
|
|
469
|
-
throw new
|
|
548
|
+
throw new errors_2.ValidationError('max_duration_minutes must be between 1 and 30', 'max_duration_minutes');
|
|
470
549
|
}
|
|
471
550
|
const payload = {
|
|
472
551
|
objective: options.objective,
|
|
@@ -489,12 +568,12 @@ class OneShot {
|
|
|
489
568
|
on400: async (resp) => {
|
|
490
569
|
const errorData = await resp.json();
|
|
491
570
|
if (errorData.error === 'content_blocked') {
|
|
492
|
-
throw new
|
|
571
|
+
throw new errors_2.ContentBlockedError(errorData.message, errorData.categories || []);
|
|
493
572
|
}
|
|
494
573
|
if (errorData.error === 'emergency_number_blocked') {
|
|
495
|
-
throw new
|
|
574
|
+
throw new errors_2.EmergencyNumberError(errorData.message, errorData.blocked_number || '');
|
|
496
575
|
}
|
|
497
|
-
throw new
|
|
576
|
+
throw new errors_2.ValidationError(errorData.message || 'Invalid request', 'request');
|
|
498
577
|
},
|
|
499
578
|
});
|
|
500
579
|
if (callResp.status !== 202) {
|
|
@@ -523,17 +602,17 @@ class OneShot {
|
|
|
523
602
|
this.validate(options.message, 'message');
|
|
524
603
|
this.validate(options.to_number, 'to_number');
|
|
525
604
|
if (Array.isArray(options.to_number) && options.to_number.length === 0) {
|
|
526
|
-
throw new
|
|
605
|
+
throw new errors_2.ValidationError('to_number array cannot be empty', 'to_number');
|
|
527
606
|
}
|
|
528
607
|
if (options.message.length < 1) {
|
|
529
|
-
throw new
|
|
608
|
+
throw new errors_2.ValidationError('Message is required', 'message');
|
|
530
609
|
}
|
|
531
610
|
if (options.message.length > 1600) {
|
|
532
|
-
throw new
|
|
611
|
+
throw new errors_2.ValidationError('Message must be 1600 characters or less', 'message');
|
|
533
612
|
}
|
|
534
613
|
const recipientCount = Array.isArray(options.to_number) ? options.to_number.length : 1;
|
|
535
614
|
if (recipientCount > 10) {
|
|
536
|
-
throw new
|
|
615
|
+
throw new errors_2.ValidationError('Maximum 10 recipients allowed', 'to_number');
|
|
537
616
|
}
|
|
538
617
|
const payload = {
|
|
539
618
|
message: options.message,
|
|
@@ -551,12 +630,12 @@ class OneShot {
|
|
|
551
630
|
on400: async (resp) => {
|
|
552
631
|
const errorData = await resp.json();
|
|
553
632
|
if (errorData.error === 'content_blocked') {
|
|
554
|
-
throw new
|
|
633
|
+
throw new errors_2.ContentBlockedError(errorData.message, errorData.categories || []);
|
|
555
634
|
}
|
|
556
635
|
if (errorData.error === 'emergency_number_blocked') {
|
|
557
|
-
throw new
|
|
636
|
+
throw new errors_2.EmergencyNumberError(errorData.message, errorData.blocked_number || '');
|
|
558
637
|
}
|
|
559
|
-
throw new
|
|
638
|
+
throw new errors_2.ValidationError(errorData.message || 'Invalid request', 'request');
|
|
560
639
|
},
|
|
561
640
|
});
|
|
562
641
|
if (sendResp.status !== 202) {
|
|
@@ -590,7 +669,7 @@ class OneShot {
|
|
|
590
669
|
this.validate(options.product?.name, 'product.name');
|
|
591
670
|
this.validate(options.product?.description, 'product.description');
|
|
592
671
|
if (options.product.description.length < 10) {
|
|
593
|
-
throw new
|
|
672
|
+
throw new errors_2.ValidationError('Product description must be at least 10 characters', 'product.description');
|
|
594
673
|
}
|
|
595
674
|
const payload = {
|
|
596
675
|
type: options.type ?? 'saas',
|
|
@@ -625,7 +704,7 @@ class OneShot {
|
|
|
625
704
|
},
|
|
626
705
|
on400: async (resp) => {
|
|
627
706
|
const errorData = await resp.json();
|
|
628
|
-
throw new
|
|
707
|
+
throw new errors_2.ValidationError(errorData.message || 'Invalid request', 'request');
|
|
629
708
|
},
|
|
630
709
|
});
|
|
631
710
|
if (buildResp.status !== 202) {
|
|
@@ -653,10 +732,10 @@ class OneShot {
|
|
|
653
732
|
async browser(options) {
|
|
654
733
|
this.validate(options.task, 'task');
|
|
655
734
|
if (options.task.length < 10) {
|
|
656
|
-
throw new
|
|
735
|
+
throw new errors_2.ValidationError('Task must be at least 10 characters', 'task');
|
|
657
736
|
}
|
|
658
737
|
if (options.max_steps !== undefined && (options.max_steps < 1 || options.max_steps > 100)) {
|
|
659
|
-
throw new
|
|
738
|
+
throw new errors_2.ValidationError('max_steps must be between 1 and 100', 'max_steps');
|
|
660
739
|
}
|
|
661
740
|
const payload = {
|
|
662
741
|
task: options.task,
|
|
@@ -687,7 +766,7 @@ class OneShot {
|
|
|
687
766
|
onQuote: (ctx) => this.log(`Browser quote: $${ctx.estimated_cost} for ~${ctx.estimated_steps} steps`),
|
|
688
767
|
on400: async (resp) => {
|
|
689
768
|
const errorData = await resp.json();
|
|
690
|
-
throw new
|
|
769
|
+
throw new errors_2.ValidationError(errorData.message || 'Invalid request', 'request');
|
|
691
770
|
},
|
|
692
771
|
});
|
|
693
772
|
if (execResp.status !== 202) {
|
|
@@ -717,7 +796,7 @@ class OneShot {
|
|
|
717
796
|
body: JSON.stringify({ name }),
|
|
718
797
|
});
|
|
719
798
|
if (!response.ok) {
|
|
720
|
-
throw new
|
|
799
|
+
throw new errors_2.ToolError('Failed to create browser profile', response.status, await response.text());
|
|
721
800
|
}
|
|
722
801
|
return response.json();
|
|
723
802
|
}
|
|
@@ -737,7 +816,7 @@ class OneShot {
|
|
|
737
816
|
headers: await this.signedReadHeaders(),
|
|
738
817
|
});
|
|
739
818
|
if (!response.ok) {
|
|
740
|
-
throw new
|
|
819
|
+
throw new errors_2.ToolError('Failed to list browser profiles', response.status, await response.text());
|
|
741
820
|
}
|
|
742
821
|
const data = await response.json();
|
|
743
822
|
return data.profiles;
|
|
@@ -757,7 +836,7 @@ class OneShot {
|
|
|
757
836
|
headers: await this.signedReadHeaders(),
|
|
758
837
|
});
|
|
759
838
|
if (!response.ok) {
|
|
760
|
-
throw new
|
|
839
|
+
throw new errors_2.ToolError('Failed to delete browser profile', response.status, await response.text());
|
|
761
840
|
}
|
|
762
841
|
}
|
|
763
842
|
/**
|
|
@@ -803,7 +882,7 @@ class OneShot {
|
|
|
803
882
|
headers: await this.signedReadHeaders()
|
|
804
883
|
});
|
|
805
884
|
if (!response.ok) {
|
|
806
|
-
throw new
|
|
885
|
+
throw new errors_2.ToolError('Failed to list SMS inbox', response.status, await response.text());
|
|
807
886
|
}
|
|
808
887
|
return response.json();
|
|
809
888
|
}
|
|
@@ -822,10 +901,10 @@ class OneShot {
|
|
|
822
901
|
headers: await this.signedReadHeaders()
|
|
823
902
|
});
|
|
824
903
|
if (response.status === 404) {
|
|
825
|
-
throw new
|
|
904
|
+
throw new errors_2.ToolError('SMS message not found', 404, 'Message not found');
|
|
826
905
|
}
|
|
827
906
|
if (!response.ok) {
|
|
828
|
-
throw new
|
|
907
|
+
throw new errors_2.ToolError('Failed to get SMS message', response.status, await response.text());
|
|
829
908
|
}
|
|
830
909
|
return response.json();
|
|
831
910
|
}
|
|
@@ -850,7 +929,7 @@ class OneShot {
|
|
|
850
929
|
headers: await this.signedReadHeaders()
|
|
851
930
|
});
|
|
852
931
|
if (!response.ok) {
|
|
853
|
-
throw new
|
|
932
|
+
throw new errors_2.ToolError('Failed to list notifications', response.status, await response.text());
|
|
854
933
|
}
|
|
855
934
|
return response.json();
|
|
856
935
|
}
|
|
@@ -869,10 +948,10 @@ class OneShot {
|
|
|
869
948
|
headers: await this.signedReadHeaders()
|
|
870
949
|
});
|
|
871
950
|
if (response.status === 404) {
|
|
872
|
-
throw new
|
|
951
|
+
throw new errors_2.ToolError('Notification not found', 404, 'Notification not found');
|
|
873
952
|
}
|
|
874
953
|
if (!response.ok) {
|
|
875
|
-
throw new
|
|
954
|
+
throw new errors_2.ToolError('Failed to mark notification as read', response.status, await response.text());
|
|
876
955
|
}
|
|
877
956
|
}
|
|
878
957
|
async getUnifiedBalance() {
|
|
@@ -880,7 +959,7 @@ class OneShot {
|
|
|
880
959
|
headers: await this.signedReadHeaders()
|
|
881
960
|
});
|
|
882
961
|
if (!response.ok) {
|
|
883
|
-
throw new
|
|
962
|
+
throw new errors_2.ToolError('Failed to fetch balance', response.status, await response.text());
|
|
884
963
|
}
|
|
885
964
|
return response.json();
|
|
886
965
|
}
|
|
@@ -950,9 +1029,9 @@ class OneShot {
|
|
|
950
1029
|
on400: async (resp) => {
|
|
951
1030
|
const errorData = await resp.json();
|
|
952
1031
|
if (errorData.error === 'content_blocked') {
|
|
953
|
-
throw new
|
|
1032
|
+
throw new errors_2.ContentBlockedError(errorData.message, []);
|
|
954
1033
|
}
|
|
955
|
-
throw new
|
|
1034
|
+
throw new errors_2.ValidationError(errorData.message || 'Invalid request', 'request');
|
|
956
1035
|
},
|
|
957
1036
|
});
|
|
958
1037
|
if (createResp.status !== 202) {
|
|
@@ -975,10 +1054,10 @@ class OneShot {
|
|
|
975
1054
|
headers: this.headers()
|
|
976
1055
|
});
|
|
977
1056
|
if (response.status === 404) {
|
|
978
|
-
throw new
|
|
1057
|
+
throw new errors_2.ToolError('Goal not found', 404, 'Goal not found');
|
|
979
1058
|
}
|
|
980
1059
|
if (!response.ok) {
|
|
981
|
-
throw new
|
|
1060
|
+
throw new errors_2.ToolError('Failed to get compute goal', response.status, await response.text());
|
|
982
1061
|
}
|
|
983
1062
|
const json = await response.json();
|
|
984
1063
|
return json.data;
|
|
@@ -1000,7 +1079,7 @@ class OneShot {
|
|
|
1000
1079
|
headers: this.headers()
|
|
1001
1080
|
});
|
|
1002
1081
|
if (!response.ok) {
|
|
1003
|
-
throw new
|
|
1082
|
+
throw new errors_2.ToolError('Failed to get compute tasks', response.status, await response.text());
|
|
1004
1083
|
}
|
|
1005
1084
|
const json = await response.json();
|
|
1006
1085
|
return json.data;
|
|
@@ -1020,10 +1099,10 @@ class OneShot {
|
|
|
1020
1099
|
headers: this.headers()
|
|
1021
1100
|
});
|
|
1022
1101
|
if (response.status === 404) {
|
|
1023
|
-
throw new
|
|
1102
|
+
throw new errors_2.ToolError('Budget not found', 404, 'Budget not found for this goal');
|
|
1024
1103
|
}
|
|
1025
1104
|
if (!response.ok) {
|
|
1026
|
-
throw new
|
|
1105
|
+
throw new errors_2.ToolError('Failed to get compute budget', response.status, await response.text());
|
|
1027
1106
|
}
|
|
1028
1107
|
const json = await response.json();
|
|
1029
1108
|
return json.data;
|
|
@@ -1045,10 +1124,10 @@ class OneShot {
|
|
|
1045
1124
|
body: JSON.stringify({ reason })
|
|
1046
1125
|
});
|
|
1047
1126
|
if (response.status === 404) {
|
|
1048
|
-
throw new
|
|
1127
|
+
throw new errors_2.ToolError('Goal not found', 404, 'Goal not found');
|
|
1049
1128
|
}
|
|
1050
1129
|
if (!response.ok) {
|
|
1051
|
-
throw new
|
|
1130
|
+
throw new errors_2.ToolError('Failed to cancel compute goal', response.status, await response.text());
|
|
1052
1131
|
}
|
|
1053
1132
|
const json = await response.json();
|
|
1054
1133
|
return json.data;
|
|
@@ -1074,10 +1153,10 @@ class OneShot {
|
|
|
1074
1153
|
body: JSON.stringify(input)
|
|
1075
1154
|
});
|
|
1076
1155
|
if (response.status === 404) {
|
|
1077
|
-
throw new
|
|
1156
|
+
throw new errors_2.ToolError('Goal or task not found', 404, await response.text());
|
|
1078
1157
|
}
|
|
1079
1158
|
if (!response.ok) {
|
|
1080
|
-
throw new
|
|
1159
|
+
throw new errors_2.ToolError('Failed to respond to compute task', response.status, await response.text());
|
|
1081
1160
|
}
|
|
1082
1161
|
const json = await response.json();
|
|
1083
1162
|
return json.data;
|
|
@@ -1098,7 +1177,7 @@ class OneShot {
|
|
|
1098
1177
|
body: JSON.stringify({ reason })
|
|
1099
1178
|
});
|
|
1100
1179
|
if (!response.ok) {
|
|
1101
|
-
throw new
|
|
1180
|
+
throw new errors_2.ToolError('Failed to pause compute goal', response.status, await response.text());
|
|
1102
1181
|
}
|
|
1103
1182
|
const json = await response.json();
|
|
1104
1183
|
return json.data;
|
|
@@ -1120,7 +1199,7 @@ class OneShot {
|
|
|
1120
1199
|
body: JSON.stringify({})
|
|
1121
1200
|
});
|
|
1122
1201
|
if (!response.ok) {
|
|
1123
|
-
throw new
|
|
1202
|
+
throw new errors_2.ToolError('Failed to resume compute goal', response.status, await response.text());
|
|
1124
1203
|
}
|
|
1125
1204
|
const json = await response.json();
|
|
1126
1205
|
return json.data;
|
|
@@ -1140,7 +1219,7 @@ class OneShot {
|
|
|
1140
1219
|
async fundComputeGoal(goalId, amount) {
|
|
1141
1220
|
this.validate(goalId, 'goalId');
|
|
1142
1221
|
if (!amount || amount <= 0) {
|
|
1143
|
-
throw new
|
|
1222
|
+
throw new errors_2.ValidationError('amount must be a positive number', 'amount');
|
|
1144
1223
|
}
|
|
1145
1224
|
const path = `/v1/compute/${goalId}/fund`;
|
|
1146
1225
|
const payload = { amount };
|
|
@@ -1164,8 +1243,8 @@ class OneShot {
|
|
|
1164
1243
|
};
|
|
1165
1244
|
const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, path, payload, quoteData.context.quote_id);
|
|
1166
1245
|
paymentInfo.amount = this.chargeAmount(accepted, quoteData.payment_request.amount);
|
|
1167
|
-
const
|
|
1168
|
-
const fundResp = await this.
|
|
1246
|
+
const signed = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
|
|
1247
|
+
const fundResp = await this.makePaidRequest(signed, path, payload, quoteData.context.quote_id);
|
|
1169
1248
|
if (!fundResp.ok) {
|
|
1170
1249
|
await this.failFromResponse('Failed to fund compute goal', fundResp);
|
|
1171
1250
|
}
|
|
@@ -1193,7 +1272,7 @@ class OneShot {
|
|
|
1193
1272
|
headers: this.headers()
|
|
1194
1273
|
});
|
|
1195
1274
|
if (!response.ok) {
|
|
1196
|
-
throw new
|
|
1275
|
+
throw new errors_2.ToolError('Failed to get spend breakdown', response.status, await response.text());
|
|
1197
1276
|
}
|
|
1198
1277
|
return response.json();
|
|
1199
1278
|
}
|
|
@@ -1212,7 +1291,7 @@ class OneShot {
|
|
|
1212
1291
|
headers: this.headers()
|
|
1213
1292
|
});
|
|
1214
1293
|
if (!response.ok) {
|
|
1215
|
-
throw new
|
|
1294
|
+
throw new errors_2.ToolError('Failed to get RoCS', response.status, await response.text());
|
|
1216
1295
|
}
|
|
1217
1296
|
return response.json();
|
|
1218
1297
|
}
|
|
@@ -1248,7 +1327,7 @@ class OneShot {
|
|
|
1248
1327
|
headers: this.headers()
|
|
1249
1328
|
});
|
|
1250
1329
|
if (!response.ok) {
|
|
1251
|
-
throw new
|
|
1330
|
+
throw new errors_2.ToolError('Failed to list receipts', response.status, await response.text());
|
|
1252
1331
|
}
|
|
1253
1332
|
return response.json();
|
|
1254
1333
|
}
|
|
@@ -1287,7 +1366,7 @@ class OneShot {
|
|
|
1287
1366
|
body: JSON.stringify({ goal_id: ref.goalId, ...valueTag }),
|
|
1288
1367
|
});
|
|
1289
1368
|
if (!response.ok) {
|
|
1290
|
-
throw new
|
|
1369
|
+
throw new errors_2.ToolError('Failed to record outcome value', response.status, await response.text());
|
|
1291
1370
|
}
|
|
1292
1371
|
return;
|
|
1293
1372
|
}
|
|
@@ -1299,10 +1378,10 @@ class OneShot {
|
|
|
1299
1378
|
body: JSON.stringify(valueTag)
|
|
1300
1379
|
});
|
|
1301
1380
|
if (response.status === 404) {
|
|
1302
|
-
throw new
|
|
1381
|
+
throw new errors_2.ToolError('Receipt not found', 404, 'Receipt not found or not owned by this agent');
|
|
1303
1382
|
}
|
|
1304
1383
|
if (!response.ok) {
|
|
1305
|
-
throw new
|
|
1384
|
+
throw new errors_2.ToolError('Failed to tag receipt value', response.status, await response.text());
|
|
1306
1385
|
}
|
|
1307
1386
|
}
|
|
1308
1387
|
/**
|
|
@@ -1328,7 +1407,7 @@ class OneShot {
|
|
|
1328
1407
|
headers: this.headers()
|
|
1329
1408
|
});
|
|
1330
1409
|
if (!response.ok) {
|
|
1331
|
-
throw new
|
|
1410
|
+
throw new errors_2.ToolError('Failed to get RoCS by goal', response.status, await response.text());
|
|
1332
1411
|
}
|
|
1333
1412
|
return response.json();
|
|
1334
1413
|
}
|
|
@@ -1341,7 +1420,7 @@ class OneShot {
|
|
|
1341
1420
|
}
|
|
1342
1421
|
validate(value, field) {
|
|
1343
1422
|
if (!value)
|
|
1344
|
-
throw new
|
|
1423
|
+
throw new errors_2.ValidationError(`${field} is required`, field);
|
|
1345
1424
|
}
|
|
1346
1425
|
headers() {
|
|
1347
1426
|
return {
|
|
@@ -1435,11 +1514,11 @@ class OneShot {
|
|
|
1435
1514
|
});
|
|
1436
1515
|
}
|
|
1437
1516
|
catch (err) {
|
|
1438
|
-
throw new
|
|
1517
|
+
throw new errors_2.BudgetSyncError(`Could not sync spend budget (network): ${err}`);
|
|
1439
1518
|
}
|
|
1440
1519
|
if (!response.ok) {
|
|
1441
1520
|
const text = await response.text();
|
|
1442
|
-
throw new
|
|
1521
|
+
throw new errors_2.BudgetSyncError(`Could not sync spend budget (${response.status}): ${text}`, response.status, text);
|
|
1443
1522
|
}
|
|
1444
1523
|
this.log('Budget synced');
|
|
1445
1524
|
})();
|
|
@@ -1467,7 +1546,7 @@ class OneShot {
|
|
|
1467
1546
|
return;
|
|
1468
1547
|
const amount = parseFloat(total);
|
|
1469
1548
|
if (Number.isFinite(amount) && amount > cap) {
|
|
1470
|
-
throw new
|
|
1549
|
+
throw new errors_2.BudgetExceededError(`Quote $${total} exceeds this agent's per-transaction budget of $${cap}`, 'per_transaction', cap, undefined, amount);
|
|
1471
1550
|
}
|
|
1472
1551
|
}
|
|
1473
1552
|
/**
|
|
@@ -1484,7 +1563,7 @@ class OneShot {
|
|
|
1484
1563
|
headers: await this.signedReadHeaders(),
|
|
1485
1564
|
});
|
|
1486
1565
|
if (!response.ok) {
|
|
1487
|
-
throw new
|
|
1566
|
+
throw new errors_2.ToolError('Failed to fetch budgets', response.status, await response.text());
|
|
1488
1567
|
}
|
|
1489
1568
|
const body = await response.json();
|
|
1490
1569
|
return (body.data ?? body);
|
|
@@ -1492,7 +1571,7 @@ class OneShot {
|
|
|
1492
1571
|
/** Local fast-fail guard: throw when a quote total exceeds the caller's cap. */
|
|
1493
1572
|
assertWithinMaxCost(total, maxCost) {
|
|
1494
1573
|
if (maxCost && parseFloat(total) > maxCost) {
|
|
1495
|
-
throw new
|
|
1574
|
+
throw new errors_2.OneShotError(`Quote $${total} exceeds maxCost $${maxCost}`);
|
|
1496
1575
|
}
|
|
1497
1576
|
}
|
|
1498
1577
|
/**
|
|
@@ -1538,7 +1617,7 @@ class OneShot {
|
|
|
1538
1617
|
const budget = response.status === 403 ? this.parseBudgetRejection(text) : undefined;
|
|
1539
1618
|
if (budget)
|
|
1540
1619
|
throw budget;
|
|
1541
|
-
throw new
|
|
1620
|
+
throw new errors_2.ToolError(message, response.status, text);
|
|
1542
1621
|
}
|
|
1543
1622
|
/**
|
|
1544
1623
|
* Map a 403 `budget_exceeded` body onto a typed error, so callers can catch
|
|
@@ -1551,7 +1630,7 @@ class OneShot {
|
|
|
1551
1630
|
if (body.error !== 'budget_exceeded')
|
|
1552
1631
|
return undefined;
|
|
1553
1632
|
const b = body.budget ?? {};
|
|
1554
|
-
return new
|
|
1633
|
+
return new errors_2.BudgetExceededError(body.message ?? 'Agent spend budget exceeded', b.reason === 'per_transaction' ? 'per_transaction' : 'daily', b.cap !== undefined ? parseFloat(b.cap) : undefined, b.spent !== undefined ? parseFloat(b.spent) : undefined, b.charge !== undefined ? parseFloat(b.charge) : undefined, b.resets_at);
|
|
1555
1634
|
}
|
|
1556
1635
|
catch {
|
|
1557
1636
|
return undefined;
|
|
@@ -1574,7 +1653,7 @@ class OneShot {
|
|
|
1574
1653
|
expectedAmount ? `expected $${expectedAmount}` : null,
|
|
1575
1654
|
receivedAmount ? `signed $${receivedAmount}` : null,
|
|
1576
1655
|
].filter(Boolean).join(', ');
|
|
1577
|
-
return new
|
|
1656
|
+
return new errors_2.PaymentError(`payment rejected: ${reason}${detail ? ` — ${detail}` : ''}${data.message ? ` (${data.message})` : ''}`, reason, {
|
|
1578
1657
|
amount: expectedAmount,
|
|
1579
1658
|
asset: data.expected?.asset,
|
|
1580
1659
|
network: data.expected?.network,
|
|
@@ -1610,8 +1689,59 @@ class OneShot {
|
|
|
1610
1689
|
return undefined;
|
|
1611
1690
|
return { 'Idempotency-Key': idempotencyKey };
|
|
1612
1691
|
}
|
|
1692
|
+
async readReliabilityJson(path, signed, allowDegraded = false) {
|
|
1693
|
+
const scope = (0, deadline_1.deadlineScope)(undefined, signed ? 10000 : 5000);
|
|
1694
|
+
try {
|
|
1695
|
+
return await (0, deadline_1.abortable)((async () => {
|
|
1696
|
+
const headers = signed ? await this.signedReadHeaders() : undefined;
|
|
1697
|
+
if (scope.signal.aborted)
|
|
1698
|
+
throw new errors_2.OneShotError('Read deadline exceeded');
|
|
1699
|
+
const response = await fetch(`${this.baseUrl}${path}`, { headers, signal: scope.signal });
|
|
1700
|
+
if (!response.ok && !(allowDegraded && response.status === 503))
|
|
1701
|
+
await this.failFromResponse('Reliability read failed', response);
|
|
1702
|
+
return await response.json();
|
|
1703
|
+
})(), scope.signal);
|
|
1704
|
+
}
|
|
1705
|
+
finally {
|
|
1706
|
+
scope.close();
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
async recoverRequest(options) {
|
|
1710
|
+
const query = new URLSearchParams({ endpoint: options.endpoint, key: options.idempotencyKey });
|
|
1711
|
+
return this.readReliabilityJson(`/v1/submissions/recover?${query}`, true);
|
|
1712
|
+
}
|
|
1713
|
+
async getServiceStatus() {
|
|
1714
|
+
return this.readReliabilityJson('/v1/status', false, true);
|
|
1715
|
+
}
|
|
1613
1716
|
async executeToolRequest(endpoint, options, quoteId) {
|
|
1614
|
-
const
|
|
1717
|
+
const reliable = /(?:^|\/)(enrich\/(profile|email)|verify\/email)$/.test(endpoint);
|
|
1718
|
+
const key = options.idempotencyKey ?? (reliable ? ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(16)) : undefined);
|
|
1719
|
+
if (options.totalTimeoutMs !== undefined && (!Number.isFinite(options.totalTimeoutMs) || options.totalTimeoutMs <= 0)) {
|
|
1720
|
+
throw new errors_2.ValidationError('totalTimeoutMs must be positive', 'totalTimeoutMs');
|
|
1721
|
+
}
|
|
1722
|
+
const scope = (0, deadline_1.deadlineScope)(options.signal, options.totalTimeoutMs);
|
|
1723
|
+
const context = { phase: 'initialization' };
|
|
1724
|
+
const started = Date.now();
|
|
1725
|
+
try {
|
|
1726
|
+
if (scope.signal.aborted)
|
|
1727
|
+
throw new errors_2.OneShotError('Operation cancelled');
|
|
1728
|
+
if (key)
|
|
1729
|
+
options.onRequestCreated?.({ idempotencyKey: key });
|
|
1730
|
+
return await (0, deadline_1.abortable)(this.executeToolRequestImpl(endpoint, { ...options, idempotencyKey: key, signal: scope.signal }, quoteId, context), scope.signal);
|
|
1731
|
+
}
|
|
1732
|
+
catch (error) {
|
|
1733
|
+
if (scope.timedOut())
|
|
1734
|
+
throw new errors_1.RequestTimeoutError(Date.now() - started, context.phase, key, context.requestId, context.receiptId);
|
|
1735
|
+
if (error instanceof Error)
|
|
1736
|
+
Object.assign(error, { idempotencyKey: key, requestId: context.requestId, receiptId: context.receiptId, phase: context.phase });
|
|
1737
|
+
throw error;
|
|
1738
|
+
}
|
|
1739
|
+
finally {
|
|
1740
|
+
scope.close();
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
async executeToolRequestImpl(endpoint, options, quoteId, context = { phase: "initialization" }) {
|
|
1744
|
+
const { totalTimeoutMs, onRequestCreated, onAccepted, signal, onStatusUpdate, wait = true, waitForPhones, phoneTimeoutSec, idempotencyKey, maxCost, ...payload } = options;
|
|
1615
1745
|
const extraHeaders = {
|
|
1616
1746
|
...this.maxCostHeader(maxCost),
|
|
1617
1747
|
...this.idempotencyHeader(idempotencyKey),
|
|
@@ -1640,11 +1770,18 @@ class OneShot {
|
|
|
1640
1770
|
}
|
|
1641
1771
|
}
|
|
1642
1772
|
if (signal?.aborted) {
|
|
1643
|
-
throw new
|
|
1773
|
+
throw new errors_2.OneShotError('Operation cancelled');
|
|
1644
1774
|
}
|
|
1645
1775
|
// One-time push of config.budgets before the first paid call, so the
|
|
1646
1776
|
// server-side gate knows about them on this very request.
|
|
1647
1777
|
await this.ensureBudgetsSynced();
|
|
1778
|
+
if (signal?.aborted)
|
|
1779
|
+
throw new errors_2.OneShotError('Operation cancelled');
|
|
1780
|
+
if (idempotencyKey)
|
|
1781
|
+
Object.assign(extraHeaders, { 'x-agent-proof': 'required' });
|
|
1782
|
+
if (signal?.aborted)
|
|
1783
|
+
throw new errors_2.OneShotError('Operation cancelled');
|
|
1784
|
+
context.phase = 'submission';
|
|
1648
1785
|
let response = await this.makeRequest(endpoint, payload, undefined, quoteId, signal, undefined, extraHeaders);
|
|
1649
1786
|
// Handle 402 Payment Required
|
|
1650
1787
|
if (response.status === 402) {
|
|
@@ -1665,8 +1802,11 @@ class OneShot {
|
|
|
1665
1802
|
this.log(`Payment required: ${paymentInfo.amount} USDC`);
|
|
1666
1803
|
this.assertWithinBudget(paymentInfo.amount);
|
|
1667
1804
|
this.checkAbortBeforePayment(signal);
|
|
1668
|
-
|
|
1669
|
-
|
|
1805
|
+
context.phase = 'payment';
|
|
1806
|
+
const signed = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
|
|
1807
|
+
this.checkAbortBeforePayment(signal);
|
|
1808
|
+
context.phase = 'submission';
|
|
1809
|
+
response = await this.makePaidRequest(signed, endpoint, payload, quoteId, signal, undefined, extraHeaders);
|
|
1670
1810
|
}
|
|
1671
1811
|
if (!response.ok) {
|
|
1672
1812
|
await this.failFromResponse('Tool request failed', response);
|
|
@@ -1674,11 +1814,19 @@ class OneShot {
|
|
|
1674
1814
|
const result = await response.json();
|
|
1675
1815
|
// Handle async jobs
|
|
1676
1816
|
if ((result.status === 'pending' || result.status === 'processing') && result.request_id) {
|
|
1817
|
+
context.requestId = String(result.request_id);
|
|
1818
|
+
context.receiptId = typeof result.receipt_id === 'string' ? result.receipt_id : undefined;
|
|
1819
|
+
onAccepted?.({ request_id: context.requestId, receipt_id: context.receiptId, idempotencyKey });
|
|
1820
|
+
context.phase = 'polling';
|
|
1677
1821
|
this.log(`Job queued: ${result.request_id}`);
|
|
1678
1822
|
if (!wait) {
|
|
1679
|
-
return {
|
|
1823
|
+
return { ...result, idempotencyKey };
|
|
1824
|
+
}
|
|
1825
|
+
const completed = await this.pollJob(result.request_id, options.timeout, signal, onStatusUpdate, waitForPhones ? { waitForPhones, phoneTimeoutSec } : undefined);
|
|
1826
|
+
if (completed && typeof completed === 'object' && !Array.isArray(completed)) {
|
|
1827
|
+
return { ...completed, request_id: context.requestId, receipt_id: completed.receipt_id ?? context.receiptId, idempotencyKey };
|
|
1680
1828
|
}
|
|
1681
|
-
return
|
|
1829
|
+
return completed;
|
|
1682
1830
|
}
|
|
1683
1831
|
return (result.data ?? result);
|
|
1684
1832
|
}
|
|
@@ -1716,8 +1864,8 @@ class OneShot {
|
|
|
1716
1864
|
this.checkAbortBeforePayment(cfg.signal);
|
|
1717
1865
|
const { accepted, resource, extensions } = await this.getAcceptedRequirements(quoteResp, cfg.endpoint, cfg.payload, quoteData.context.quote_id, cfg.signal);
|
|
1718
1866
|
paymentInfo.amount = this.chargeAmount(accepted, quoteData.payment_request.amount);
|
|
1719
|
-
const
|
|
1720
|
-
const execResp = await this.
|
|
1867
|
+
const signed = await this.signPaymentAuthorization(paymentInfo, accepted, resource, extensions);
|
|
1868
|
+
const execResp = await this.makePaidRequest(signed, cfg.endpoint, cfg.payload, quoteData.context.quote_id, cfg.signal, cfg.execTimeoutMs);
|
|
1721
1869
|
return { context: quoteData.context, execResp };
|
|
1722
1870
|
}
|
|
1723
1871
|
/**
|
|
@@ -1757,7 +1905,7 @@ class OneShot {
|
|
|
1757
1905
|
const wsBranch = this.waitViaWebSocket(requestId, inner.signal, emit, wait).catch((err) => {
|
|
1758
1906
|
// A failed job or a cancellation is a real outcome. Anything else is a
|
|
1759
1907
|
// transport problem: never settle the race on it, HTTP carries on.
|
|
1760
|
-
if (err instanceof
|
|
1908
|
+
if (err instanceof errors_2.OneShotError)
|
|
1761
1909
|
throw err;
|
|
1762
1910
|
this.log(`WebSocket unavailable (${err instanceof Error ? err.message : String(err)}) — relying on HTTP polling`);
|
|
1763
1911
|
return new Promise(() => { });
|
|
@@ -1827,7 +1975,7 @@ class OneShot {
|
|
|
1827
1975
|
let lastResult = initialResult;
|
|
1828
1976
|
while (Date.now() < deadline) {
|
|
1829
1977
|
if (signal?.aborted)
|
|
1830
|
-
throw new
|
|
1978
|
+
throw new errors_2.OneShotError('Operation cancelled');
|
|
1831
1979
|
try {
|
|
1832
1980
|
const resp = await fetch(`${this.baseUrl}/v1/requests/${requestId}`, {
|
|
1833
1981
|
headers: this.headers(),
|
|
@@ -1838,7 +1986,7 @@ class OneShot {
|
|
|
1838
1986
|
// whatever we last had so consumers don't lose the sync result.
|
|
1839
1987
|
if (lastResult !== undefined)
|
|
1840
1988
|
return lastResult;
|
|
1841
|
-
throw new
|
|
1989
|
+
throw new errors_2.ToolError('Failed to check job status', resp.status, await resp.text());
|
|
1842
1990
|
}
|
|
1843
1991
|
const job = await resp.json();
|
|
1844
1992
|
lastResult = (job.result ?? job);
|
|
@@ -1847,7 +1995,7 @@ class OneShot {
|
|
|
1847
1995
|
}
|
|
1848
1996
|
}
|
|
1849
1997
|
catch (err) {
|
|
1850
|
-
if (err instanceof
|
|
1998
|
+
if (err instanceof errors_2.OneShotError)
|
|
1851
1999
|
throw err;
|
|
1852
2000
|
if (lastResult !== undefined)
|
|
1853
2001
|
return lastResult;
|
|
@@ -1873,7 +2021,7 @@ class OneShot {
|
|
|
1873
2021
|
return reject(new Error('WebSocket not available'));
|
|
1874
2022
|
}
|
|
1875
2023
|
if (signal.aborted) {
|
|
1876
|
-
return reject(new
|
|
2024
|
+
return reject(new errors_2.OneShotError('Operation cancelled'));
|
|
1877
2025
|
}
|
|
1878
2026
|
const wsUrl = this.baseUrl.replace(/^http/, 'ws') +
|
|
1879
2027
|
`/v1/requests/subscribe?wallet=${encodeURIComponent(this.provider.address)}`;
|
|
@@ -1901,7 +2049,7 @@ class OneShot {
|
|
|
1901
2049
|
const onAbort = () => {
|
|
1902
2050
|
settle(() => {
|
|
1903
2051
|
cleanup();
|
|
1904
|
-
reject(new
|
|
2052
|
+
reject(new errors_2.OneShotError('Operation cancelled'));
|
|
1905
2053
|
});
|
|
1906
2054
|
};
|
|
1907
2055
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
@@ -1934,7 +2082,7 @@ class OneShot {
|
|
|
1934
2082
|
settle(() => {
|
|
1935
2083
|
cleanup();
|
|
1936
2084
|
wait.via = 'ws';
|
|
1937
|
-
reject(new
|
|
2085
|
+
reject(new errors_2.JobError(`Job failed: ${msg.error ?? 'Unknown'}`, requestId, String(msg.error ?? 'Unknown'), msg.error_code));
|
|
1938
2086
|
});
|
|
1939
2087
|
}
|
|
1940
2088
|
else {
|
|
@@ -1967,6 +2115,21 @@ class OneShot {
|
|
|
1967
2115
|
* Owns the caller's deadline (`JobTimeoutError`).
|
|
1968
2116
|
*/
|
|
1969
2117
|
async pollJobHttp(requestId, timeoutSec, signal, emit, wait) {
|
|
2118
|
+
const scope = (0, deadline_1.deadlineScope)(signal, (timeoutSec ?? 120) * 1000);
|
|
2119
|
+
const started = Date.now();
|
|
2120
|
+
try {
|
|
2121
|
+
return await (0, deadline_1.abortable)(this.pollJobHttpImpl(requestId, timeoutSec, scope.signal, emit, wait), scope.signal);
|
|
2122
|
+
}
|
|
2123
|
+
catch (error) {
|
|
2124
|
+
if (scope.timedOut())
|
|
2125
|
+
throw new errors_2.JobTimeoutError(requestId, Date.now() - started);
|
|
2126
|
+
throw error;
|
|
2127
|
+
}
|
|
2128
|
+
finally {
|
|
2129
|
+
scope.close();
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
async pollJobHttpImpl(requestId, timeoutSec, signal, emit, wait) {
|
|
1970
2133
|
const maxWaitMs = (timeoutSec ?? 120) * 1000;
|
|
1971
2134
|
const startTime = Date.now();
|
|
1972
2135
|
let retries = 0;
|
|
@@ -1974,7 +2137,7 @@ class OneShot {
|
|
|
1974
2137
|
let polls = 0;
|
|
1975
2138
|
while (Date.now() - startTime < maxWaitMs) {
|
|
1976
2139
|
if (signal?.aborted)
|
|
1977
|
-
throw new
|
|
2140
|
+
throw new errors_2.OneShotError('Operation cancelled');
|
|
1978
2141
|
try {
|
|
1979
2142
|
const resp = await fetch(`${this.baseUrl}/v1/requests/${requestId}`, {
|
|
1980
2143
|
headers: this.headers(),
|
|
@@ -1987,7 +2150,7 @@ class OneShot {
|
|
|
1987
2150
|
if (resp.status >= 500 || resp.status === 429) {
|
|
1988
2151
|
throw new Error(`Poll returned ${resp.status}: ${body.slice(0, 200)}`);
|
|
1989
2152
|
}
|
|
1990
|
-
throw new
|
|
2153
|
+
throw new errors_2.ToolError('Failed to check job status', resp.status, body);
|
|
1991
2154
|
}
|
|
1992
2155
|
const job = await resp.json();
|
|
1993
2156
|
if (job.status === 'completed') {
|
|
@@ -1999,12 +2162,18 @@ class OneShot {
|
|
|
1999
2162
|
if (job.request_id && typeof result === 'object' && result !== null && !('request_id' in result)) {
|
|
2000
2163
|
result.request_id = job.request_id;
|
|
2001
2164
|
}
|
|
2165
|
+
if (result && typeof result === 'object' && !Array.isArray(result)) {
|
|
2166
|
+
if (job.receipt_id && !result.receipt_id)
|
|
2167
|
+
result.receipt_id = job.receipt_id;
|
|
2168
|
+
if (job.settlement_status && !result.settlement_status)
|
|
2169
|
+
result.settlement_status = job.settlement_status;
|
|
2170
|
+
}
|
|
2002
2171
|
return result;
|
|
2003
2172
|
}
|
|
2004
2173
|
if (job.status === 'failed') {
|
|
2005
2174
|
if (wait)
|
|
2006
2175
|
wait.via = 'http';
|
|
2007
|
-
throw new
|
|
2176
|
+
throw new errors_2.JobError(`Job failed: ${job.error ?? 'Unknown'}`, requestId, String(job.error ?? 'Unknown'), job.error_code);
|
|
2008
2177
|
}
|
|
2009
2178
|
emit?.(String(job.status));
|
|
2010
2179
|
retries = 0;
|
|
@@ -2018,27 +2187,27 @@ class OneShot {
|
|
|
2018
2187
|
await this.sleep(Math.min(interval, remaining), signal);
|
|
2019
2188
|
}
|
|
2020
2189
|
catch (err) {
|
|
2021
|
-
if (err instanceof
|
|
2190
|
+
if (err instanceof errors_2.OneShotError)
|
|
2022
2191
|
throw err;
|
|
2023
2192
|
if (++retries > maxRetries) {
|
|
2024
|
-
throw new
|
|
2193
|
+
throw new errors_2.OneShotError(`Polling failed after ${maxRetries} retries: ${err}`);
|
|
2025
2194
|
}
|
|
2026
2195
|
const backoff = 2000 * Math.pow(2, retries - 1);
|
|
2027
2196
|
this.log(`Retry ${retries}/${maxRetries} in ${backoff}ms`);
|
|
2028
2197
|
await this.sleep(backoff, signal);
|
|
2029
2198
|
}
|
|
2030
2199
|
}
|
|
2031
|
-
throw new
|
|
2200
|
+
throw new errors_2.JobTimeoutError(requestId, Date.now() - startTime);
|
|
2032
2201
|
}
|
|
2033
2202
|
sleep(ms, signal) {
|
|
2034
2203
|
return new Promise((resolve, reject) => {
|
|
2035
2204
|
if (signal?.aborted) {
|
|
2036
|
-
return reject(new
|
|
2205
|
+
return reject(new errors_2.OneShotError('Operation cancelled'));
|
|
2037
2206
|
}
|
|
2038
|
-
const timer = setTimeout(resolve, ms);
|
|
2207
|
+
const timer = setTimeout(() => { signal?.removeEventListener('abort', onAbort); resolve(); }, ms);
|
|
2039
2208
|
const onAbort = () => {
|
|
2040
2209
|
clearTimeout(timer);
|
|
2041
|
-
reject(new
|
|
2210
|
+
reject(new errors_2.OneShotError('Operation cancelled'));
|
|
2042
2211
|
};
|
|
2043
2212
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
2044
2213
|
});
|
|
@@ -2047,7 +2216,8 @@ class OneShot {
|
|
|
2047
2216
|
const headers = {
|
|
2048
2217
|
'Content-Type': 'application/json',
|
|
2049
2218
|
...this.headers(),
|
|
2050
|
-
...(extraHeaders ?? {})
|
|
2219
|
+
...(extraHeaders ?? {}),
|
|
2220
|
+
...(extraHeaders?.['x-agent-proof'] ? await this.signedReadHeaders(/(enrich\/(profile|email)|verify\/email)$/.test(endpoint) ? 'submit' : 'read') : {})
|
|
2051
2221
|
};
|
|
2052
2222
|
if (payment) {
|
|
2053
2223
|
const paymentJson = JSON.stringify(payment);
|
|
@@ -2058,42 +2228,179 @@ class OneShot {
|
|
|
2058
2228
|
}
|
|
2059
2229
|
if (quoteId)
|
|
2060
2230
|
headers['x-quote-id'] = quoteId;
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2231
|
+
const transportDeadline = timeoutMs === undefined ? undefined : (0, deadline_1.deadlineScope)(signal, timeoutMs);
|
|
2232
|
+
const fetchSignal = transportDeadline?.signal ?? signal;
|
|
2233
|
+
// Node 18 compatibility: do not require AbortSignal.any. Retain the deadline
|
|
2234
|
+
// through response-body consumption; cleanup occurs when it expires/aborts.
|
|
2235
|
+
transportDeadline?.signal.addEventListener('abort', () => transportDeadline.close(), { once: true });
|
|
2236
|
+
if (fetchSignal?.aborted)
|
|
2237
|
+
throw new errors_2.OneShotError('Operation cancelled');
|
|
2238
|
+
const work = fetch(`${this.baseUrl}${endpoint}`, {
|
|
2239
|
+
method: 'POST', headers, body: JSON.stringify(data), signal: fetchSignal,
|
|
2240
|
+
});
|
|
2068
2241
|
try {
|
|
2069
|
-
return await
|
|
2070
|
-
method: 'POST',
|
|
2071
|
-
headers,
|
|
2072
|
-
body: JSON.stringify(data),
|
|
2073
|
-
signal: fetchSignal
|
|
2074
|
-
});
|
|
2242
|
+
return await (fetchSignal ? (0, deadline_1.abortable)(work, fetchSignal) : work);
|
|
2075
2243
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2244
|
+
catch (error) {
|
|
2245
|
+
transportDeadline?.close();
|
|
2246
|
+
throw error;
|
|
2079
2247
|
}
|
|
2080
2248
|
}
|
|
2081
2249
|
checkAbortBeforePayment(signal) {
|
|
2082
2250
|
if (signal?.aborted) {
|
|
2083
|
-
throw new
|
|
2251
|
+
throw new errors_2.OneShotError('Operation cancelled before payment');
|
|
2084
2252
|
}
|
|
2085
2253
|
}
|
|
2254
|
+
// ---------------------------------------------------------------------------
|
|
2255
|
+
// ETH-currency mode: USDC ledger + buffered swaps
|
|
2256
|
+
// ---------------------------------------------------------------------------
|
|
2086
2257
|
/**
|
|
2087
|
-
*
|
|
2088
|
-
*
|
|
2258
|
+
* ETH mode only. Make sure the wallet's *effective* USDC (on-chain balance
|
|
2259
|
+
* minus payments signed but not yet settled) covers this charge, swapping
|
|
2260
|
+
* ETH→USDC for a buffer of `swapBufferMultiplier` payments when it does not,
|
|
2261
|
+
* then reserve the charge. Returns the reservation so the caller can release
|
|
2262
|
+
* it if the payment never settles (request failed). Runs under a per-instance
|
|
2263
|
+
* lock so concurrent calls never double-swap or race the wallet nonce.
|
|
2089
2264
|
*/
|
|
2090
2265
|
async ensureUsdcBalance(paymentInfo) {
|
|
2091
2266
|
if (this._currency !== 'ETH')
|
|
2092
|
-
return;
|
|
2267
|
+
return undefined;
|
|
2268
|
+
const { chainId, usdcAddress } = this.assertEthModeSupported(paymentInfo);
|
|
2269
|
+
const charge = ethers_1.ethers.parseUnits(paymentInfo.amount, paymentInfo.token.decimals);
|
|
2270
|
+
return this.withUsdcLock(async () => {
|
|
2271
|
+
let balance = await this.readUsdcBalance(usdcAddress);
|
|
2272
|
+
const effective = this.effectiveUsdcBalance(balance);
|
|
2273
|
+
if (effective >= charge) {
|
|
2274
|
+
this.log(`USDC balance covers payment (${ethers_1.ethers.formatUnits(effective, 6)} available for ${paymentInfo.amount}); skipping ETH swap`);
|
|
2275
|
+
}
|
|
2276
|
+
else {
|
|
2277
|
+
const swapAmount = await this.sizeSwap(charge, effective, chainId);
|
|
2278
|
+
const swapAmountStr = ethers_1.ethers.formatUnits(swapAmount, 6);
|
|
2279
|
+
this.log(`USDC ${ethers_1.ethers.formatUnits(effective, 6)} is below ${paymentInfo.amount}; swapping ETH→USDC for ${swapAmountStr} USDC (${this._swapBufferMultiplier}x buffer, slippage: ${this._slippage * 100}%)`);
|
|
2280
|
+
try {
|
|
2281
|
+
const result = await this.executeUsdcSwap(swapAmountStr, chainId);
|
|
2282
|
+
balance += result.usdcReceived;
|
|
2283
|
+
// exactOutput delivers exactly amountOut and tx.wait() has confirmed it.
|
|
2284
|
+
this._usdcBalanceCache = { balance, at: Date.now() };
|
|
2285
|
+
this.log(`Swap complete: tx=${result.txHash}, USDC received=${ethers_1.ethers.formatUnits(result.usdcReceived, 6)}`);
|
|
2286
|
+
}
|
|
2287
|
+
catch (err) {
|
|
2288
|
+
this._usdcBalanceCache = undefined;
|
|
2289
|
+
throw err;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
return this.reserveUsdc(charge);
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
/** ETH mode is Base-mainnet-only (that is where the Uniswap route lives); fail clearly before any RPC. */
|
|
2296
|
+
assertEthModeSupported(paymentInfo) {
|
|
2297
|
+
const chainId = chainIdFromNetwork(paymentInfo.network) ?? CHAIN_ID;
|
|
2298
|
+
if (chainId !== CHAIN_ID) {
|
|
2299
|
+
throw new errors_2.ValidationError(`ETH currency mode is only supported on Base mainnet (eip155:${CHAIN_ID}); this payment is on ${paymentInfo.network}. Fund the wallet with USDC or use currency: 'USDC'.`, 'currency');
|
|
2300
|
+
}
|
|
2301
|
+
const usdcAddress = paymentInfo.token.address;
|
|
2302
|
+
if (usdcAddress.toLowerCase() !== USDC_ADDRESS.toLowerCase()) {
|
|
2303
|
+
throw new errors_2.ValidationError(`ETH→USDC swap buys ${USDC_ADDRESS} but this payment requires ${usdcAddress}`, 'currency');
|
|
2304
|
+
}
|
|
2305
|
+
return { chainId, usdcAddress };
|
|
2306
|
+
}
|
|
2307
|
+
withUsdcLock(fn) {
|
|
2308
|
+
// A failed predecessor must not poison the chain: the waiter runs its own attempt.
|
|
2309
|
+
const run = this._usdcLock.then(fn, fn);
|
|
2310
|
+
this._usdcLock = run.then(() => undefined, () => undefined);
|
|
2311
|
+
return run;
|
|
2312
|
+
}
|
|
2313
|
+
/** On-chain USDC balance (atomic units), deduped within one block. Overridable in tests. */
|
|
2314
|
+
async readUsdcBalance(usdcAddress) {
|
|
2315
|
+
const cached = this._usdcBalanceCache;
|
|
2316
|
+
if (cached && Date.now() - cached.at < USDC_BALANCE_CACHE_MS)
|
|
2317
|
+
return cached.balance;
|
|
2318
|
+
const usdc = new ethers_1.ethers.Contract(usdcAddress, ERC20_BALANCE_ABI, this.rpcProvider);
|
|
2319
|
+
const balance = BigInt(await usdc.balanceOf(this.provider.address));
|
|
2320
|
+
this._usdcBalanceCache = { balance, at: Date.now() };
|
|
2321
|
+
return balance;
|
|
2322
|
+
}
|
|
2323
|
+
/** `balanceOf − Σ pending` (never negative); drops reservations older than the TTL. */
|
|
2324
|
+
effectiveUsdcBalance(balance) {
|
|
2325
|
+
const cutoff = Date.now() - USDC_RESERVATION_TTL_MS;
|
|
2326
|
+
let pending = 0n;
|
|
2327
|
+
for (const [id, r] of this._usdcPending) {
|
|
2328
|
+
if (r.createdAt < cutoff)
|
|
2329
|
+
this._usdcPending.delete(id);
|
|
2330
|
+
else
|
|
2331
|
+
pending += r.amount;
|
|
2332
|
+
}
|
|
2333
|
+
return balance > pending ? balance - pending : 0n;
|
|
2334
|
+
}
|
|
2335
|
+
reserveUsdc(amount) {
|
|
2336
|
+
const reservation = { id: ++this._usdcReservationSeq, amount, createdAt: Date.now() };
|
|
2337
|
+
this._usdcPending.set(reservation.id, reservation);
|
|
2338
|
+
return reservation;
|
|
2339
|
+
}
|
|
2340
|
+
/** Forget a reservation whose payment will never settle (signing or request failed). */
|
|
2341
|
+
releaseUsdcReservation(reservation) {
|
|
2342
|
+
if (reservation)
|
|
2343
|
+
this._usdcPending.delete(reservation.id);
|
|
2344
|
+
}
|
|
2345
|
+
/**
|
|
2346
|
+
* How much USDC to buy: top up to `charge × swapBufferMultiplier`, counting
|
|
2347
|
+
* whatever effective balance is already there. When the wallet provider
|
|
2348
|
+
* exposes `getBalance` and its ETH cannot cover the buffered quote, fall
|
|
2349
|
+
* back to the bare shortfall so an ETH-poor wallet can still make the one
|
|
2350
|
+
* payment in front of it (a send-only provider always gets the buffer).
|
|
2351
|
+
*/
|
|
2352
|
+
async sizeSwap(charge, effective, chainId) {
|
|
2353
|
+
const shortfall = charge - effective;
|
|
2354
|
+
// Integer math on a 10^6 scale (multiplier ≤ 1000, so ≤ 10^9 — always a
|
|
2355
|
+
// finite, exact integer): keeps the multiplier's precision, no float→BigInt
|
|
2356
|
+
// on an unbounded value.
|
|
2357
|
+
const mScaled = BigInt(Math.round(this._swapBufferMultiplier * SWAP_MULTIPLIER_SCALE));
|
|
2358
|
+
const target = (charge * mScaled + BigInt(SWAP_MULTIPLIER_SCALE) - 1n) / BigInt(SWAP_MULTIPLIER_SCALE);
|
|
2359
|
+
let amount = target > effective ? target - effective : shortfall;
|
|
2360
|
+
if (amount > shortfall && this.provider.getBalance) {
|
|
2361
|
+
try {
|
|
2362
|
+
const amountInMax = await this.quoteSwapAmountInMax(ethers_1.ethers.formatUnits(amount, 6), chainId);
|
|
2363
|
+
const eth = await this.provider.getBalance();
|
|
2364
|
+
if (amountInMax !== undefined && eth < amountInMax) {
|
|
2365
|
+
this.log(`ETH balance cannot cover a ${ethers_1.ethers.formatUnits(amount, 6)} USDC buffer; swapping only the ${ethers_1.ethers.formatUnits(shortfall, 6)} USDC shortfall`);
|
|
2366
|
+
amount = shortfall;
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
catch {
|
|
2370
|
+
// Quoting failed — let executeSwap quote again and report properly.
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
return amount;
|
|
2374
|
+
}
|
|
2375
|
+
/** Max ETH the buffered swap could cost (quote), or undefined if unavailable. Overridable in tests. */
|
|
2376
|
+
async quoteSwapAmountInMax(usdcAmount, chainId) {
|
|
2377
|
+
const { getSwapQuote } = await Promise.resolve().then(() => __importStar(require('./swap')));
|
|
2378
|
+
const quote = await getSwapQuote(this.rpcProvider, usdcAmount, chainId, this._slippage);
|
|
2379
|
+
return quote?.amountInMax;
|
|
2380
|
+
}
|
|
2381
|
+
/** Perform the on-chain swap. Overridable in tests. */
|
|
2382
|
+
async executeUsdcSwap(usdcAmount, chainId) {
|
|
2093
2383
|
const { executeSwap } = await Promise.resolve().then(() => __importStar(require('./swap')));
|
|
2094
|
-
this.
|
|
2095
|
-
|
|
2096
|
-
|
|
2384
|
+
return executeSwap(this.provider, this.rpcProvider, usdcAmount, chainId, this._slippage);
|
|
2385
|
+
}
|
|
2386
|
+
/**
|
|
2387
|
+
* Send the paid leg of a request. The server settles the authorization only
|
|
2388
|
+
* on acceptance. A transport failure on a durable endpoint is ambiguous;
|
|
2389
|
+
* retain its short-lived ETH-mode reservation until recovery or expiry.
|
|
2390
|
+
*/
|
|
2391
|
+
async makePaidRequest(signed, endpoint, data, quoteId, signal, timeoutMs, extraHeaders) {
|
|
2392
|
+
let resp;
|
|
2393
|
+
try {
|
|
2394
|
+
resp = await this.makeRequest(endpoint, data, signed.auth, quoteId, signal, timeoutMs, extraHeaders);
|
|
2395
|
+
}
|
|
2396
|
+
catch (err) {
|
|
2397
|
+
if (!/(enrich\/(profile|email)|verify\/email)$/.test(endpoint))
|
|
2398
|
+
this.releaseUsdcReservation(signed.reservation);
|
|
2399
|
+
throw err;
|
|
2400
|
+
}
|
|
2401
|
+
if (!resp.ok && !(extraHeaders?.['Idempotency-Key'] && resp.status >= 500))
|
|
2402
|
+
this.releaseUsdcReservation(signed.reservation);
|
|
2403
|
+
return resp;
|
|
2097
2404
|
}
|
|
2098
2405
|
/** Parse the PAYMENT-REQUIRED header from a 402 response into the accepted requirements and Bazaar metadata. */
|
|
2099
2406
|
parsePaymentRequired(header) {
|
|
@@ -2148,26 +2455,26 @@ class OneShot {
|
|
|
2148
2455
|
// always receives a payment-signature header (avoids 402 from x402 SDK).
|
|
2149
2456
|
if (parseFloat(paymentInfo.amount) === 0) {
|
|
2150
2457
|
this.log('Credits cover full cost — sending zero-cost authorization');
|
|
2151
|
-
return {
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2458
|
+
return { auth: {
|
|
2459
|
+
x402Version: 2,
|
|
2460
|
+
...(resource ? { resource } : {}),
|
|
2461
|
+
...(extensions ? { extensions } : {}),
|
|
2462
|
+
accepted,
|
|
2463
|
+
payload: {
|
|
2464
|
+
signature: '0x',
|
|
2465
|
+
authorization: {
|
|
2466
|
+
from: this.provider.address,
|
|
2467
|
+
to: paymentInfo.payTo,
|
|
2468
|
+
value: '0',
|
|
2469
|
+
validAfter: '0',
|
|
2470
|
+
validBefore: '0',
|
|
2471
|
+
nonce: '0x' + '00'.repeat(32),
|
|
2472
|
+
},
|
|
2165
2473
|
},
|
|
2166
|
-
}
|
|
2167
|
-
};
|
|
2474
|
+
} };
|
|
2168
2475
|
}
|
|
2169
|
-
// If paying with ETH,
|
|
2170
|
-
await this.ensureUsdcBalance(paymentInfo);
|
|
2476
|
+
// If paying with ETH, make sure USDC covers the charge (swapping a buffer if not) and reserve it
|
|
2477
|
+
const reservation = await this.ensureUsdcBalance(paymentInfo);
|
|
2171
2478
|
const now = Math.floor(Date.now() / 1000);
|
|
2172
2479
|
const nonce = ethers_1.ethers.randomBytes(32);
|
|
2173
2480
|
const value = ethers_1.ethers.parseUnits(paymentInfo.amount, paymentInfo.token.decimals);
|
|
@@ -2182,46 +2489,53 @@ class OneShot {
|
|
|
2182
2489
|
const domainVersion = accepted.extra?.version || '2';
|
|
2183
2490
|
const chainId = chainIdFromNetwork(accepted.network ?? paymentInfo.network) ?? CHAIN_ID;
|
|
2184
2491
|
// Sign EIP-3009 TransferWithAuthorization
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2492
|
+
let signature;
|
|
2493
|
+
try {
|
|
2494
|
+
signature = await this.provider.signTypedData({
|
|
2495
|
+
name: domainName,
|
|
2496
|
+
version: domainVersion,
|
|
2497
|
+
chainId,
|
|
2498
|
+
verifyingContract: paymentInfo.token.address
|
|
2499
|
+
}, {
|
|
2500
|
+
TransferWithAuthorization: [
|
|
2501
|
+
{ name: 'from', type: 'address' },
|
|
2502
|
+
{ name: 'to', type: 'address' },
|
|
2503
|
+
{ name: 'value', type: 'uint256' },
|
|
2504
|
+
{ name: 'validAfter', type: 'uint256' },
|
|
2505
|
+
{ name: 'validBefore', type: 'uint256' },
|
|
2506
|
+
{ name: 'nonce', type: 'bytes32' }
|
|
2507
|
+
]
|
|
2508
|
+
}, {
|
|
2509
|
+
from: this.provider.address,
|
|
2510
|
+
to: paymentInfo.payTo,
|
|
2511
|
+
value,
|
|
2512
|
+
validAfter,
|
|
2513
|
+
validBefore,
|
|
2514
|
+
nonce: nonceHex
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2517
|
+
catch (err) {
|
|
2518
|
+
this.releaseUsdcReservation(reservation);
|
|
2519
|
+
throw err;
|
|
2520
|
+
}
|
|
2207
2521
|
// Return x402 PaymentPayload v2 format (including resource + extensions for Bazaar discovery)
|
|
2208
|
-
return {
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2522
|
+
return { reservation, auth: {
|
|
2523
|
+
x402Version: 2,
|
|
2524
|
+
...(resource ? { resource } : {}),
|
|
2525
|
+
...(extensions ? { extensions } : {}),
|
|
2526
|
+
accepted,
|
|
2527
|
+
payload: {
|
|
2528
|
+
signature,
|
|
2529
|
+
authorization: {
|
|
2530
|
+
from: this.provider.address,
|
|
2531
|
+
to: paymentInfo.payTo,
|
|
2532
|
+
value: value.toString(),
|
|
2533
|
+
validAfter: validAfter.toString(),
|
|
2534
|
+
validBefore: validBefore.toString(),
|
|
2535
|
+
nonce: nonceHex,
|
|
2536
|
+
},
|
|
2222
2537
|
},
|
|
2223
|
-
}
|
|
2224
|
-
};
|
|
2538
|
+
} };
|
|
2225
2539
|
}
|
|
2226
2540
|
}
|
|
2227
2541
|
exports.OneShot = OneShot;
|