@ardrive/turbo-sdk 1.42.0 → 1.43.0-alpha.2

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.
Files changed (99) hide show
  1. package/lib/cjs/cli/cli.js +97 -0
  2. package/lib/cjs/cli/commands/arns.js +475 -0
  3. package/lib/cjs/cli/commands/freeStatus.js +47 -0
  4. package/lib/cjs/cli/commands/index.js +3 -0
  5. package/lib/cjs/cli/commands/paymentHistory.js +25 -0
  6. package/lib/cjs/cli/options.js +202 -1
  7. package/lib/cjs/common/arnsActions.js +125 -0
  8. package/lib/cjs/common/chunked.js +51 -1
  9. package/lib/cjs/common/folderIndex.js +401 -0
  10. package/lib/cjs/common/http.js +40 -6
  11. package/lib/cjs/common/index.js +7 -0
  12. package/lib/cjs/common/payment.js +548 -2
  13. package/lib/cjs/common/signer.js +27 -6
  14. package/lib/cjs/common/token/index.js +0 -11
  15. package/lib/cjs/common/turbo.js +162 -0
  16. package/lib/cjs/common/upload.js +314 -31
  17. package/lib/cjs/node/folderIndex.js +132 -0
  18. package/lib/cjs/node/index.js +1 -0
  19. package/lib/cjs/node/upload.js +7 -0
  20. package/lib/cjs/types.js +53 -3
  21. package/lib/cjs/utils/common.js +0 -4
  22. package/lib/cjs/utils/errors.js +38 -1
  23. package/lib/cjs/utils/folderIndex.js +88 -0
  24. package/lib/cjs/utils/uuid.js +31 -0
  25. package/lib/cjs/web/signer.js +4 -2
  26. package/lib/esm/cli/cli.js +99 -2
  27. package/lib/esm/cli/commands/arns.js +450 -0
  28. package/lib/esm/cli/commands/freeStatus.js +44 -0
  29. package/lib/esm/cli/commands/index.js +3 -0
  30. package/lib/esm/cli/commands/paymentHistory.js +22 -0
  31. package/lib/esm/cli/options.js +201 -0
  32. package/lib/esm/common/arnsActions.js +114 -0
  33. package/lib/esm/common/chunked.js +51 -1
  34. package/lib/esm/common/folderIndex.js +396 -0
  35. package/lib/esm/common/http.js +39 -5
  36. package/lib/esm/common/index.js +7 -0
  37. package/lib/esm/common/payment.js +548 -2
  38. package/lib/esm/common/signer.js +28 -7
  39. package/lib/esm/common/token/index.js +1 -12
  40. package/lib/esm/common/turbo.js +162 -0
  41. package/lib/esm/common/upload.js +316 -33
  42. package/lib/esm/node/folderIndex.js +129 -0
  43. package/lib/esm/node/index.js +1 -0
  44. package/lib/esm/node/upload.js +7 -0
  45. package/lib/esm/types.js +52 -2
  46. package/lib/esm/utils/common.js +0 -4
  47. package/lib/esm/utils/errors.js +35 -0
  48. package/lib/esm/utils/folderIndex.js +81 -0
  49. package/lib/esm/utils/uuid.js +28 -0
  50. package/lib/esm/web/signer.js +4 -2
  51. package/lib/types/cli/commands/arns.d.ts +147 -0
  52. package/lib/types/cli/commands/arns.d.ts.map +1 -0
  53. package/lib/types/cli/commands/freeStatus.d.ts +3 -0
  54. package/lib/types/cli/commands/freeStatus.d.ts.map +1 -0
  55. package/lib/types/cli/commands/index.d.ts +3 -0
  56. package/lib/types/cli/commands/index.d.ts.map +1 -1
  57. package/lib/types/cli/commands/paymentHistory.d.ts +18 -0
  58. package/lib/types/cli/commands/paymentHistory.d.ts.map +1 -0
  59. package/lib/types/cli/options.d.ts +423 -0
  60. package/lib/types/cli/options.d.ts.map +1 -1
  61. package/lib/types/cli/types.d.ts +78 -0
  62. package/lib/types/cli/types.d.ts.map +1 -1
  63. package/lib/types/common/arnsActions.d.ts +58 -0
  64. package/lib/types/common/arnsActions.d.ts.map +1 -0
  65. package/lib/types/common/chunked.d.ts +47 -2
  66. package/lib/types/common/chunked.d.ts.map +1 -1
  67. package/lib/types/common/folderIndex.d.ts +54 -0
  68. package/lib/types/common/folderIndex.d.ts.map +1 -0
  69. package/lib/types/common/http.d.ts +25 -2
  70. package/lib/types/common/http.d.ts.map +1 -1
  71. package/lib/types/common/index.d.ts +3 -0
  72. package/lib/types/common/index.d.ts.map +1 -1
  73. package/lib/types/common/payment.d.ts +291 -1
  74. package/lib/types/common/payment.d.ts.map +1 -1
  75. package/lib/types/common/signer.d.ts +12 -6
  76. package/lib/types/common/signer.d.ts.map +1 -1
  77. package/lib/types/common/token/index.d.ts.map +1 -1
  78. package/lib/types/common/turbo.d.ts +183 -1
  79. package/lib/types/common/turbo.d.ts.map +1 -1
  80. package/lib/types/common/upload.d.ts +41 -0
  81. package/lib/types/common/upload.d.ts.map +1 -1
  82. package/lib/types/node/folderIndex.d.ts +28 -0
  83. package/lib/types/node/folderIndex.d.ts.map +1 -0
  84. package/lib/types/node/index.d.ts +1 -0
  85. package/lib/types/node/index.d.ts.map +1 -1
  86. package/lib/types/node/upload.d.ts +1 -0
  87. package/lib/types/node/upload.d.ts.map +1 -1
  88. package/lib/types/types.d.ts +694 -3
  89. package/lib/types/types.d.ts.map +1 -1
  90. package/lib/types/utils/common.d.ts.map +1 -1
  91. package/lib/types/utils/errors.d.ts +29 -0
  92. package/lib/types/utils/errors.d.ts.map +1 -1
  93. package/lib/types/utils/folderIndex.d.ts +47 -0
  94. package/lib/types/utils/folderIndex.d.ts.map +1 -0
  95. package/lib/types/utils/uuid.d.ts +7 -0
  96. package/lib/types/utils/uuid.d.ts.map +1 -0
  97. package/lib/types/web/signer.d.ts +1 -1
  98. package/lib/types/web/signer.d.ts.map +1 -1
  99. package/package.json +1 -1
@@ -14,7 +14,11 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import { BigNumber } from 'bignumber.js';
17
+ import { arNSPurchaseIntents, fiatCurrencyTypes, isCurrency, } from '../types.js';
17
18
  import { isAnyValidUserAddress } from '../utils/common.js';
19
+ import { FailedRequestError, FiatPaymentsDisabledError, InsufficientCreditsError, ProvidedInputError, } from '../utils/errors.js';
20
+ import { uuidV4 } from '../utils/uuid.js';
21
+ import { arNSKeywordsField, arNSMetadataField, arNSOwnerProofHeaders, buildArNSCustodyMessage, } from './arnsActions.js';
18
22
  import { defaultRetryConfig } from './http.js';
19
23
  import { TurboHTTPService } from './http.js';
20
24
  import { Logger } from './logger.js';
@@ -34,7 +38,7 @@ export class TurboUnauthenticatedPaymentService {
34
38
  }
35
39
  async getBalance(address) {
36
40
  const balance = await this.httpService.get({
37
- endpoint: `/account/balance/${this.token}?address=${address}`,
41
+ endpoint: `/account/balance/${this.token}?address=${encodeURIComponent(address)}`,
38
42
  allowedStatuses: [200, 404],
39
43
  });
40
44
  return balance.winc
@@ -47,6 +51,31 @@ export class TurboUnauthenticatedPaymentService {
47
51
  receivedApprovals: [],
48
52
  };
49
53
  }
54
+ async getFreeStatus(address) {
55
+ const status = await this.httpService.get({
56
+ endpoint: `/account/free?address=${encodeURIComponent(address)}`,
57
+ allowedStatuses: [200, 404],
58
+ });
59
+ // Normalize: preserve a legitimate `0` (free tier off) or `null` (unlimited),
60
+ // and coerce a missing field (e.g. a 404 body) to `null`.
61
+ return { bytesRemaining: status?.bytesRemaining ?? null };
62
+ }
63
+ /**
64
+ * Returns the ArNS names a wallet owns or controls -- both custodial names
65
+ * bought via Turbo's ArNS-with-credits feature (Turbo may spawn and hold
66
+ * the ANT on the caller's behalf, depending on the buy) and self-custody
67
+ * names, in one list. See `TurboArNSName` for field semantics, including
68
+ * the `custodial` flag distinguishing the two.
69
+ *
70
+ * To read a name's current records or lease/expiration state, use
71
+ * `@ar.io/sdk` directly against the returned `antId` -- it talks to the
72
+ * chain directly and needs no round-trip through this SDK/backend.
73
+ */
74
+ getArNSNames(address) {
75
+ return this.httpService.get({
76
+ endpoint: `/arns/my-names/${encodeURIComponent(address)}`,
77
+ });
78
+ }
50
79
  getFiatRates() {
51
80
  return this.httpService.get({
52
81
  endpoint: '/rates',
@@ -91,6 +120,206 @@ export class TurboUnauthenticatedPaymentService {
91
120
  equivalentWincTokenAmount: actualPaymentAmount.toString(),
92
121
  };
93
122
  }
123
+ async getArNSPriceForName(params) {
124
+ // `async` so a validation failure surfaces as a rejected promise (consistent
125
+ // with `purchaseArNSName`) rather than a synchronous throw.
126
+ this.validateArNSPurchaseParams(params);
127
+ const price = await this.httpService.get({
128
+ endpoint: `/arns/price/${params.intent.toLowerCase()}/${encodeURIComponent(params.name)}${this.buildArNSPurchaseQuery(params)}`,
129
+ });
130
+ // Normalize the figure to charge into ONE field. `winc` is the name only
131
+ // and excludes the ANT spawn surcharge — for a Buy-Name that surcharge can
132
+ // exceed the name's own price, so a caller reading `winc` silently
133
+ // under-quotes every purchase. Surfacing `wincTotal` makes the correct
134
+ // field the obvious one.
135
+ return {
136
+ ...price,
137
+ wincTotal: price.wincTotalWithAntSpawn ?? price.winc,
138
+ };
139
+ }
140
+ /**
141
+ * Preview the flat credits margin one of the eight non-purchase actions
142
+ * (everything except Buy-Name/Extend-Lease/Upgrade-Name/
143
+ * Increase-Undername-Limit) will debit, without creating it. Those four
144
+ * purchase actions are priced by `getArNSPriceForName` instead — their
145
+ * cost is dominated by the ARIO purchase, not this margin, so this route
146
+ * rejects them.
147
+ *
148
+ * No signature required: this mirrors `getArNSPriceForName`'s read-only,
149
+ * unauthenticated shape rather than `getArNSActionStatus`'s (which happens
150
+ * to live on the authenticated client today despite needing no signature
151
+ * either).
152
+ */
153
+ async getArNSActionPrice(action) {
154
+ return this.httpService.get({
155
+ endpoint: `/arns/actions/${action}/price`,
156
+ });
157
+ }
158
+ /**
159
+ * Fail fast (client-side) on malformed ArNS requests so JS callers that bypass
160
+ * the compile-time intent unions get a clear `ProvidedInputError` instead of an
161
+ * opaque service 4xx. Enforces the required fields per intent:
162
+ * - `Buy-Name`: `type` ('lease' | 'permabuy'); leases also need `years`.
163
+ * `processId` is OPTIONAL — omit it to have the bundler custodially
164
+ * provision the ANT (Turbo owns it), supply it for a user-owned ANT.
165
+ * - `Extend-Lease`: positive `years`
166
+ * - `Increase-Undername-Limit`: positive `increaseQty`
167
+ * - `Upgrade-Name`: just `name`
168
+ */
169
+ validateArNSPurchaseParams(params) {
170
+ const p = params;
171
+ if (!arNSPurchaseIntents.includes(p.intent)) {
172
+ throw new ProvidedInputError(`Invalid ArNS intent '${p.intent}'. Expected one of: ${arNSPurchaseIntents.join(', ')}.`);
173
+ }
174
+ if (typeof p.name !== 'string' || p.name.length === 0) {
175
+ throw new ProvidedInputError('An ArNS `name` is required.');
176
+ }
177
+ const isPositiveNumber = (v) => typeof v === 'number' && Number.isFinite(v) && v > 0;
178
+ switch (p.intent) {
179
+ case 'Buy-Name':
180
+ if (p.type !== 'lease' && p.type !== 'permabuy') {
181
+ throw new ProvidedInputError("Buy-Name requires a `type` of 'lease' or 'permabuy'.");
182
+ }
183
+ // `processId` is optional for Buy-Name: omitting it drives the
184
+ // bundler's custodial provisioning path (Turbo spawns + owns the ANT).
185
+ // If supplied it must be a non-empty string (user-owned ANT).
186
+ if (p.processId !== undefined &&
187
+ (typeof p.processId !== 'string' || p.processId.length === 0)) {
188
+ throw new ProvidedInputError('Buy-Name `processId`, when provided, must be a non-empty string (the ANT the name resolves to).');
189
+ }
190
+ if (p.type === 'lease' && !isPositiveNumber(p.years)) {
191
+ throw new ProvidedInputError('A lease `Buy-Name` requires a positive `years`.');
192
+ }
193
+ break;
194
+ case 'Extend-Lease':
195
+ if (!isPositiveNumber(p.years)) {
196
+ throw new ProvidedInputError('Extend-Lease requires a positive `years`.');
197
+ }
198
+ break;
199
+ case 'Increase-Undername-Limit':
200
+ if (!isPositiveNumber(p.increaseQty)) {
201
+ throw new ProvidedInputError('Increase-Undername-Limit requires a positive `increaseQty`.');
202
+ }
203
+ break;
204
+ case 'Upgrade-Name':
205
+ break;
206
+ }
207
+ }
208
+ getArNSPurchaseStatus({ nonce, }) {
209
+ return this.httpService.get({
210
+ endpoint: `/arns/purchase/${encodeURIComponent(nonce)}`,
211
+ });
212
+ }
213
+ buildArNSPurchaseQuery(input) {
214
+ // The intent-specific union members each carry only their own fields; read
215
+ // them through a single widened view rather than narrowing per intent.
216
+ const { type, years, increaseQty, processId, paidBy } = input;
217
+ const params = new URLSearchParams();
218
+ if (type !== undefined)
219
+ params.set('type', type);
220
+ if (years !== undefined)
221
+ params.set('years', `${years}`);
222
+ if (increaseQty !== undefined)
223
+ params.set('increaseQty', `${increaseQty}`);
224
+ if (processId !== undefined)
225
+ params.set('processId', processId);
226
+ if (paidBy !== undefined) {
227
+ for (const payer of Array.isArray(paidBy) ? paidBy : [paidBy]) {
228
+ params.append('paidBy', payer);
229
+ }
230
+ }
231
+ const query = params.toString();
232
+ return query.length > 0 ? `?${query}` : '';
233
+ }
234
+ /**
235
+ * Quote a fiat (Stripe) ArNS purchase — buy a name with a credit card in one
236
+ * step, with no Turbo Credits top-up in between.
237
+ *
238
+ * Returns the recorded `purchaseQuote` (its `nonce` is what
239
+ * `getArNSPurchaseStatus` polls) plus the Stripe `paymentSession` to complete
240
+ * payment with. For `payment-intent`, confirm client-side with
241
+ * `stripe.confirmCardPayment(paymentSession.client_secret, ...)`, then poll
242
+ * the nonce until the purchase reports success or failure.
243
+ *
244
+ * Throws {@link FiatPaymentsDisabledError} when the service has Stripe turned
245
+ * off (normal in the testnet sandbox) so callers can fall back to the
246
+ * credit-paid path without string-matching a generic 503.
247
+ */
248
+ async getArNSFiatPurchaseQuote(params) {
249
+ this.validateArNSPurchaseParams(params);
250
+ const { address, currency, method = 'payment-intent', promoCodes = [], } = params;
251
+ if (typeof address !== 'string' || address.length === 0) {
252
+ throw new ProvidedInputError('A destination `address` is required for a fiat ArNS purchase quote.');
253
+ }
254
+ if (!isCurrency(currency)) {
255
+ throw new ProvidedInputError(`Invalid currency '${currency}'. Supported: ${fiatCurrencyTypes.join(', ')}`);
256
+ }
257
+ // Every interpolated segment is encoded. Five user-controlled values land in
258
+ // the path here, and an unencoded one (e.g. a name or address containing
259
+ // `../`) would silently retarget the request at another route.
260
+ const segments = [
261
+ method,
262
+ address,
263
+ currency,
264
+ params.intent,
265
+ params.name,
266
+ ].map((segment) => encodeURIComponent(segment));
267
+ const query = this.buildArNSFiatQuoteQuery(params, promoCodes);
268
+ try {
269
+ return await this.httpService.get({
270
+ endpoint: `/arns/quote/${segments.join('/')}${query}`,
271
+ });
272
+ }
273
+ catch (error) {
274
+ // The service returns 503 both for "Stripe is disabled" and for internal
275
+ // errors, so the body is what disambiguates them.
276
+ if (error instanceof FailedRequestError &&
277
+ error.status === 503 &&
278
+ /Fiat \(Stripe\).*disabled/i.test(error.message)) {
279
+ throw new FiatPaymentsDisabledError(error.message);
280
+ }
281
+ throw error;
282
+ }
283
+ }
284
+ /**
285
+ * Query string for a fiat quote. Distinct from `buildArNSPurchaseQuery`
286
+ * because this route takes `uiMode` + its paired URLs and has no `paidBy`
287
+ * (fiat has no delegated payer), and because promo codes must be REPEATED
288
+ * params here: the service reads them with `parseQueryParams`, which treats a
289
+ * comma-joined string as one code rather than several.
290
+ */
291
+ buildArNSFiatQuoteQuery(params, promoCodes) {
292
+ const { type, years, increaseQty, processId } = params;
293
+ const search = new URLSearchParams();
294
+ if (type !== undefined)
295
+ search.set('type', type);
296
+ if (years !== undefined)
297
+ search.set('years', `${years}`);
298
+ if (increaseQty !== undefined)
299
+ search.set('increaseQty', `${increaseQty}`);
300
+ if (processId !== undefined)
301
+ search.set('processId', processId);
302
+ const uiMode = params.uiMode;
303
+ if (uiMode !== undefined)
304
+ search.set('uiMode', uiMode);
305
+ if (uiMode === 'embedded') {
306
+ const { returnUrl } = params;
307
+ if (returnUrl !== undefined)
308
+ search.set('returnUrl', returnUrl);
309
+ }
310
+ else {
311
+ const { successUrl, cancelUrl } = params;
312
+ if (successUrl !== undefined)
313
+ search.set('successUrl', successUrl);
314
+ if (cancelUrl !== undefined)
315
+ search.set('cancelUrl', cancelUrl);
316
+ }
317
+ for (const code of promoCodes) {
318
+ search.append('promoCode', code);
319
+ }
320
+ const query = search.toString();
321
+ return query.length > 0 ? `?${query}` : '';
322
+ }
94
323
  appendPromoCodesToQuery(promoCodes) {
95
324
  const promoCodesQuery = promoCodes.join(',');
96
325
  return promoCodesQuery ? `promoCode=${promoCodesQuery}` : '';
@@ -191,7 +420,7 @@ export class TurboUnauthenticatedPaymentService {
191
420
  }
192
421
  async getCreditShareApprovals({ userAddress, }) {
193
422
  const response = await this.httpService.get({
194
- endpoint: `/account/approvals/get?userAddress=${userAddress}`,
423
+ endpoint: `/account/approvals/get?userAddress=${encodeURIComponent(userAddress)}`,
195
424
  allowedStatuses: [200, 404],
196
425
  });
197
426
  if (response?.givenApprovals === undefined &&
@@ -255,6 +484,323 @@ export class TurboAuthenticatedPaymentService extends TurboUnauthenticatedPaymen
255
484
  userAddress ??= await this.signer.getNativeAddress();
256
485
  return super.getBalance(userAddress);
257
486
  }
487
+ /**
488
+ * Quote a fiat (Stripe) ArNS purchase. `address` defaults to this signer's
489
+ * native address — the wallet that will own the name — so the common case
490
+ * needs no address at all. Pass one explicitly to buy on another wallet's
491
+ * behalf; the route takes the destination as a path param and requires no
492
+ * signature, which is why it is available unauthenticated too.
493
+ */
494
+ async getArNSFiatPurchaseQuote(params) {
495
+ const address = params.address ?? (await this.signer.getNativeAddress());
496
+ return super.getArNSFiatPurchaseQuote({
497
+ ...params,
498
+ address,
499
+ });
500
+ }
501
+ async getFreeStatus(userAddress) {
502
+ userAddress ??= await this.signer.getNativeAddress();
503
+ return super.getFreeStatus(userAddress);
504
+ }
505
+ /**
506
+ * The signer's OWN completed top-up history (crypto + fiat), merged newest
507
+ * first and keyset-paginated. This is a SIGNED GET: unlike `getBalance` /
508
+ * `getFreeStatus` (which name a wallet by `?address=`), payment history is
509
+ * self-scoped and returns only the rows belonging to the signing wallet — the
510
+ * service reads the address from the signature, never a query param.
511
+ *
512
+ * We sign the bare nonce (no action-binding of `limit`/`cursor`) to match the
513
+ * service's `verifySignature` middleware; the pagination params ride in the
514
+ * query string. Pass `cursor` from a prior response to fetch the next page.
515
+ */
516
+ async getPaymentHistory({ limit, cursor, } = {}) {
517
+ const headers = await this.signer.generateSignedRequestHeaders();
518
+ const query = new URLSearchParams();
519
+ if (limit !== undefined) {
520
+ query.set('limit', `${limit}`);
521
+ }
522
+ if (cursor !== undefined) {
523
+ query.set('cursor', cursor);
524
+ }
525
+ const queryString = query.toString();
526
+ return this.httpService.get({
527
+ endpoint: `/account/payments${queryString ? `?${queryString}` : ''}`,
528
+ headers,
529
+ allowedStatuses: [200],
530
+ });
531
+ }
532
+ /**
533
+ * Buy / extend / upgrade an ArNS name, paying with the signer's Turbo credit
534
+ * balance. The bundler performs the on-chain ARIO purchase and debits credits;
535
+ * a `402` (FailedRequestError.status === 402) indicates insufficient credits.
536
+ */
537
+ // ===== ArNS actions — the sponsored surface =====
538
+ //
539
+ // Every ArNS operation is an ACTION, and an action has exactly one of two
540
+ // shapes, chosen by the SERVER rather than the caller: either Turbo already
541
+ // holds the authority (`completed`), or the ANT owner must sign a transaction
542
+ // Turbo has already fee-payer-signed (`awaiting-signature`).
543
+ //
544
+ // The shape is not stable per action, which is why callers must branch on
545
+ // `status` and never on which action they asked for: `set-record` completes
546
+ // alone while Turbo is a controller, and degrades to `awaiting-signature`
547
+ // the moment the customer revokes Turbo.
548
+ //
549
+ // This replaced `/arns/purchase/{intent}/{name}`, `/arns/transfer/{antId}`
550
+ // and `/arns/manage/*`, which were deleted along with Turbo-custodial ANTs.
551
+ // Turbo now takes custody of nothing: every ANT is minted straight to the
552
+ // customer.
553
+ /**
554
+ * Create an action. Returns `completed` or `awaiting-signature`.
555
+ *
556
+ * Credits are debited HERE, not at `/sign`. Capture the returned `nonce`
557
+ * before prompting for a signature: it is the idempotency key, and polling
558
+ * it is how you resume. Never re-create an action to "retry" — that debits
559
+ * a second time. An abandoned action is refunded automatically.
560
+ */
561
+ async createArNSAction(action, params = {}, ownerProof) {
562
+ const nonce = uuidV4();
563
+ const headers = {
564
+ ...(await this.signer.generateSignedRequestHeaders(nonce)),
565
+ 'content-type': 'application/json',
566
+ };
567
+ // Record actions carry a SECOND signature, from the ANT owner's Solana key
568
+ // over a different message. It travels in its own `x-owner-*` headers
569
+ // because two signatures cannot share one header set.
570
+ if (ownerProof !== undefined) {
571
+ Object.assign(headers, await arNSOwnerProofHeaders(ownerProof.owner, ownerProof.message, uuidV4()));
572
+ }
573
+ try {
574
+ return await this.httpService.post({
575
+ endpoint: `/arns/actions/${action}`,
576
+ headers,
577
+ data: Buffer.from(JSON.stringify(params)),
578
+ // Non-idempotent signed write that has already debited. A blind retry
579
+ // risks paying twice for one name; poll the nonce instead.
580
+ retry: false,
581
+ });
582
+ }
583
+ catch (error) {
584
+ if (error instanceof FailedRequestError && error.status === 402) {
585
+ throw new InsufficientCreditsError(error.message);
586
+ }
587
+ throw error;
588
+ }
589
+ }
590
+ /**
591
+ * Submit the owner-signed transaction for an `awaiting-signature` action.
592
+ *
593
+ * `signedTransaction` is the FULL serialized transaction, base64 — not just
594
+ * the signature. Replaying a completed action returns `alreadyCompleted:
595
+ * true` rather than buying twice, so this is safe to call again if a
596
+ * response is lost.
597
+ */
598
+ async signArNSAction(nonce, signedTransaction) {
599
+ return this.httpService.post({
600
+ endpoint: `/arns/actions/${nonce}/sign`,
601
+ headers: {
602
+ ...(await this.signer.generateSignedRequestHeaders(uuidV4())),
603
+ 'content-type': 'application/json',
604
+ },
605
+ data: Buffer.from(JSON.stringify({ transaction: signedTransaction })),
606
+ retry: false,
607
+ });
608
+ }
609
+ /**
610
+ * Status of an action by nonce. Open — no signature required — so it works
611
+ * from a status page or callback handler that never holds the payer's key.
612
+ *
613
+ * Terminal success carries `messageId`; terminal failure carries
614
+ * `failedDate`.
615
+ */
616
+ async getArNSActionStatus(nonce) {
617
+ return this.httpService.get({
618
+ endpoint: `/arns/actions/${nonce}`,
619
+ });
620
+ }
621
+ /**
622
+ * Run an action to a terminal state, signing if the server asks for it.
623
+ *
624
+ * This is the two-shape branch, once, in one place — so callers cannot
625
+ * hardcode which actions need a signature and break when a customer
626
+ * exercises ownership.
627
+ */
628
+ async completeArNSAction(action, params, owner, opts = {}, ownerProofMessage) {
629
+ const created = await this.createArNSAction(action, params, owner !== undefined && ownerProofMessage !== undefined
630
+ ? { owner, message: ownerProofMessage }
631
+ : undefined);
632
+ // Fires before any wallet prompt: the action is already debited, so the
633
+ // caller needs the nonce persisted even if the user walks away here.
634
+ await opts.onNonce?.(created.nonce);
635
+ if (created.status === 'completed')
636
+ return created;
637
+ if (owner === undefined) {
638
+ throw new Error(`ArNS action "${action}" requires the ANT owner's signature, but no owner signer was provided. ` +
639
+ `Pass \`owner\`, or drive createArNSAction/signArNSAction yourself. ` +
640
+ `Nonce ${created.nonce} is already debited — poll it rather than re-creating.`);
641
+ }
642
+ const signed = await owner.signTransaction(created.transaction);
643
+ return this.signArNSAction(created.nonce, signed);
644
+ }
645
+ /**
646
+ * Buy a name. The ANT is minted straight to `owner` — Turbo never holds it.
647
+ *
648
+ * This is the ONLY action that always needs the owner's signature:
649
+ * `ario_ant::initialize` is the one instruction in the whole lifecycle that
650
+ * requires the ANT owner's key. The customer signs once, here, and never
651
+ * again unless they change controllers or transfer the name.
652
+ *
653
+ * The owner needs a Solana key to sign with, NOT a funded one — Turbo pays
654
+ * every lamport of fee and rent.
655
+ */
656
+ async buyArNSName({ name, owner, type = 'lease', years, paidBy, onNonce, }) {
657
+ return this.completeArNSAction('buy-name', {
658
+ name,
659
+ ownerAddress: await owner.getAddress(),
660
+ type,
661
+ ...(years !== undefined ? { years } : {}),
662
+ ...(paidBy !== undefined ? { paidBy } : {}),
663
+ }, owner, { onNonce });
664
+ }
665
+ /** Extend a lease. Permissionless on chain — no owner signature needed. */
666
+ async extendArNSLease({ name, years, paidBy, onNonce, }) {
667
+ return this.completeArNSAction('extend-lease', { name, years, ...(paidBy !== undefined ? { paidBy } : {}) }, undefined, { onNonce });
668
+ }
669
+ /** Upgrade a lease to a permanent name. No owner signature needed. */
670
+ async upgradeArNSName({ name, paidBy, onNonce, }) {
671
+ return this.completeArNSAction('upgrade-name', { name, ...(paidBy !== undefined ? { paidBy } : {}) }, undefined, { onNonce });
672
+ }
673
+ /** Raise the undername limit. No owner signature needed. */
674
+ async increaseArNSUndernameLimit({ name, increaseQty, paidBy, onNonce, }) {
675
+ return this.completeArNSAction('increase-undername-limit', { name, increaseQty, ...(paidBy !== undefined ? { paidBy } : {}) }, undefined, { onNonce });
676
+ }
677
+ /**
678
+ * Point a name (or undername) at an Arweave transaction.
679
+ *
680
+ * Costs a small credit margin — never SOL, which Turbo sponsors. Completes in one call while Turbo is
681
+ * a controller of the ANT, and returns `awaiting-signature` once the customer
682
+ * has revoked Turbo, at which point `owner` signs it themselves. Both paths
683
+ * are handled here.
684
+ *
685
+ * The owner proof is required EITHER WAY: Turbo is directing its own
686
+ * controller authority over an asset someone else owns, so nothing on chain
687
+ * records the owner's consent and we demand it. It is a MESSAGE signature,
688
+ * not a transaction — cheap and offline, but still a wallet prompt.
689
+ */
690
+ async setArNSRecord({ antId, owner, transactionId, undername = '@', ttlSeconds = 3600, onNonce, }) {
691
+ return this.completeArNSAction('set-record', {
692
+ antId,
693
+ ownerAddress: await owner.getAddress(),
694
+ transactionId,
695
+ undername,
696
+ ttlSeconds,
697
+ }, owner, { onNonce }, buildArNSCustodyMessage('set-record', [
698
+ antId,
699
+ undername,
700
+ transactionId,
701
+ String(ttlSeconds),
702
+ ]));
703
+ }
704
+ /** Remove a record (an undername). Costs credits, never SOL. */
705
+ async removeArNSRecord({ antId, owner, undername, onNonce, }) {
706
+ return this.completeArNSAction('remove-record', { antId, ownerAddress: await owner.getAddress(), undername }, owner, { onNonce }, buildArNSCustodyMessage('remove-record', [antId, undername]));
707
+ }
708
+ /**
709
+ * Edit a RECORD's metadata — its display name, logo, description, keywords.
710
+ *
711
+ * Costs a small credit margin (never SOL), and is owner-or-controller on
712
+ * chain, so it behaves exactly like
713
+ * {@link setArNSRecord}: Turbo-alone while it is a controller, owner-signed
714
+ * after a revoke.
715
+ *
716
+ * Fields are TRI-STATE. Omit one to leave it unchanged; pass `null` to clear
717
+ * it. Those are bound distinctly by the owner proof, so "clear the
718
+ * description" and "set it to empty" are different authorizations.
719
+ *
720
+ * Note this is RECORD metadata. ANT-level metadata (the ANT's own name,
721
+ * ticker, description, keywords, logo) is NOT sponsored and stays on the
722
+ * direct-signer path via `@ar.io/sdk`.
723
+ */
724
+ async setArNSRecordMetadata({ antId, owner, undername = '@', displayName, recordLogo, recordDescription, recordKeywords, onNonce, }) {
725
+ return this.completeArNSAction('set-record-metadata', {
726
+ antId,
727
+ ownerAddress: await owner.getAddress(),
728
+ undername,
729
+ // Sent explicitly, including `null`, so the server sees the same
730
+ // tri-state the proof was signed over.
731
+ ...(displayName !== undefined ? { displayName } : {}),
732
+ ...(recordLogo !== undefined ? { recordLogo } : {}),
733
+ ...(recordDescription !== undefined ? { recordDescription } : {}),
734
+ ...(recordKeywords !== undefined ? { recordKeywords } : {}),
735
+ }, owner, { onNonce }, buildArNSCustodyMessage('set-record-metadata', [
736
+ antId,
737
+ undername,
738
+ arNSMetadataField(displayName),
739
+ arNSMetadataField(recordLogo),
740
+ arNSMetadataField(recordDescription),
741
+ arNSKeywordsField(recordKeywords),
742
+ ]));
743
+ }
744
+ /** Clear a record's metadata. Costs credits, never SOL; same two-shape rules. */
745
+ async removeArNSRecordMetadata({ antId, owner, undername, onNonce, }) {
746
+ return this.completeArNSAction('remove-record-metadata', { antId, ownerAddress: await owner.getAddress(), undername }, owner, { onNonce }, buildArNSCustodyMessage('remove-record-metadata', [antId, undername]));
747
+ }
748
+ /**
749
+ * Hand ONE record to another address.
750
+ *
751
+ * Distinct from {@link transferArNSAnt}, which hands over the whole ANT and
752
+ * every record on it. Confusing the two gives away far more than intended.
753
+ */
754
+ async transferArNSRecord({ antId, owner, undername, target, onNonce, }) {
755
+ return this.completeArNSAction('transfer-record', { antId, ownerAddress: await owner.getAddress(), undername, target }, owner, { onNonce }, buildArNSCustodyMessage('transfer-record', [antId, undername, target]));
756
+ }
757
+ /**
758
+ * Grant controller rights on the ANT. Omit `target` for Turbo itself, which
759
+ * is what makes `setArNSRecord` a single call.
760
+ *
761
+ * Owner-signed: changing an ANT's access control is an owner-only
762
+ * instruction. Costs a small credit margin; Turbo funds the ACL page growth
763
+ * in SOL.
764
+ */
765
+ async addArNSController({ antId, owner, target, onNonce, }) {
766
+ return this.completeArNSAction('add-controller', {
767
+ antId,
768
+ ownerAddress: await owner.getAddress(),
769
+ ...(target !== undefined ? { target } : {}),
770
+ }, owner, { onNonce });
771
+ }
772
+ /**
773
+ * Revoke controller rights — the escape hatch that keeps "Turbo is not a
774
+ * custodian" honest.
775
+ *
776
+ * Always available, and needs nothing from Turbo but the fee. Costs a small
777
+ * credit margin rather than SOL.
778
+ * After revoking, `setArNSRecord` keeps working: it simply starts returning
779
+ * `awaiting-signature` so the owner signs their own record writes.
780
+ */
781
+ async removeArNSController({ antId, owner, target, onNonce, }) {
782
+ return this.completeArNSAction('remove-controller', {
783
+ antId,
784
+ ownerAddress: await owner.getAddress(),
785
+ ...(target !== undefined ? { target } : {}),
786
+ }, owner, { onNonce });
787
+ }
788
+ /**
789
+ * Hand the ANT to a new owner. Irreversible: after this lands, `owner` no
790
+ * longer controls the name. Owner-signed, and sponsored like the rest.
791
+ */
792
+ async transferArNSAnt({ antId, owner, target, onNonce, }) {
793
+ return this.completeArNSAction('transfer', { antId, ownerAddress: await owner.getAddress(), target }, owner, { onNonce });
794
+ }
795
+ /**
796
+ * Defaults to the signer's own address when `userAddress` is omitted
797
+ * (`null`/`undefined`). Passing `''` does NOT trigger this default --
798
+ * mirrors `getBalance`'s existing behavior above.
799
+ */
800
+ async getArNSNames(userAddress) {
801
+ userAddress ??= await this.signer.getNativeAddress();
802
+ return super.getArNSNames(userAddress);
803
+ }
258
804
  async getCreditShareApprovals({ userAddress, }) {
259
805
  userAddress ??= await this.signer.getNativeAddress();
260
806
  return super.getCreditShareApprovals({ userAddress });
@@ -27,7 +27,7 @@ import { computeAddress } from 'ethers';
27
27
  import nacl from 'tweetnacl';
28
28
  import { createWalletClient, custom, http } from 'viem';
29
29
  import { privateKeyToAccount } from 'viem/accounts';
30
- import { baseSepolia } from 'viem/chains';
30
+ import { base } from 'viem/chains';
31
31
  import { isEthereumWalletAdapter, isSolanaWalletAdapter, } from '../types.js';
32
32
  import { fromB64Url, ownerToAddress as ownerToB64Address, toB64Url, } from '../utils/base64.js';
33
33
  import { Logger } from './logger.js';
@@ -53,7 +53,6 @@ export class TurboDataItemAbstractSigner {
53
53
  case 'base-eth':
54
54
  case 'usdc':
55
55
  case 'base-usdc':
56
- case 'base-ario':
57
56
  case 'polygon-usdc':
58
57
  return computeAddress(computePublicKey(fromB64Url(owner)));
59
58
  case 'kyve':
@@ -66,15 +65,27 @@ export class TurboDataItemAbstractSigner {
66
65
  return ownerToB64Address(owner);
67
66
  }
68
67
  }
69
- async generateSignedRequestHeaders() {
70
- const nonce = randomBytes(16).toString('hex');
71
- const buffer = Buffer.from(nonce);
68
+ async generateSignedRequestHeaders(
69
+ // Callers may supply the nonce (e.g. a UUID required by some routes); the
70
+ // nonce round-trips to the service in `x-nonce` unchanged.
71
+ nonce = randomBytes(16).toString('hex'),
72
+ // Optional ACTION-BINDING data prepended to the nonce for SIGNING only (not
73
+ // sent): the service reconstructs the same string from the request and
74
+ // verifies the signature over `additionalData + nonce`. This binds the
75
+ // signature to a specific operation + params so it can't be replayed against
76
+ // a different request. Omitted → signs the bare nonce (unchanged behavior).
77
+ additionalData) {
78
+ const buffer = Buffer.from((additionalData ?? '') + nonce);
72
79
  const signature = await this.signer.sign(Uint8Array.from(buffer));
73
80
  const publicKey = toB64Url(this.signer.publicKey);
74
81
  return {
75
82
  'x-public-key': publicKey,
76
83
  'x-nonce': nonce,
77
84
  'x-signature': toB64Url(Buffer.from(signature)),
85
+ // Advertise the signature scheme so the service verifies with the right
86
+ // algorithm. Absent this, the server defaults to Arweave and every
87
+ // non-Arweave signed request (Ethereum, Solana, …) fails verification.
88
+ 'x-signature-type': this.signer.signatureType.toString(),
78
89
  };
79
90
  }
80
91
  async getPublicKey() {
@@ -152,13 +163,23 @@ export class TurboDataItemAbstractSigner {
152
163
  return this.signer.sign(dataToSign);
153
164
  }
154
165
  }
166
+ /**
167
+ * Builds the wallet client x402-fetch signs payment authorizations with.
168
+ *
169
+ * The chain matters because `wrapFetchWithPayment` maps `walletClient.chain.id`
170
+ * to a network name and prefers the matching entry in the service's `accepts`
171
+ * list. The upload service advertises Base mainnet (`base`), and x402 support
172
+ * here is limited to `base-usdc`, so mainnet is the correct default. Callers
173
+ * needing another network can supply their own signer via
174
+ * `X402Funding({ signer })`.
175
+ */
155
176
  export async function makeX402Signer(arbundlesSigner) {
156
177
  // Node: our SDK uses EthereumSigner with a raw private key
157
178
  if (arbundlesSigner instanceof EthereumSigner) {
158
179
  return createWalletClient({
159
180
  account: privateKeyToAccount(('0x' +
160
181
  Buffer.from(arbundlesSigner.key).toString('hex'))),
161
- chain: baseSepolia,
182
+ chain: base,
162
183
  transport: http(),
163
184
  });
164
185
  }
@@ -177,7 +198,7 @@ export async function makeX402Signer(arbundlesSigner) {
177
198
  const account = accounts[0];
178
199
  return createWalletClient({
179
200
  account,
180
- chain: baseSepolia,
201
+ chain: base,
181
202
  transport: custom(provider),
182
203
  });
183
204
  }