@ardrive/turbo-sdk 1.41.3 → 1.42.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/lib/cjs/cli/cli.js +61 -0
  2. package/lib/cjs/cli/commands/arns.js +317 -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 +124 -11
  7. package/lib/cjs/cli/utils.js +3 -19
  8. package/lib/cjs/common/arnsActions.js +102 -0
  9. package/lib/cjs/common/factory.js +1 -3
  10. package/lib/cjs/common/http.js +20 -5
  11. package/lib/cjs/common/index.js +6 -0
  12. package/lib/cjs/common/payment.js +482 -3
  13. package/lib/cjs/common/signer.js +27 -6
  14. package/lib/cjs/common/token/ario.js +104 -83
  15. package/lib/cjs/common/token/index.js +0 -11
  16. package/lib/cjs/common/token/solana.js +1 -0
  17. package/lib/cjs/common/turbo.js +131 -0
  18. package/lib/cjs/common/upload.js +3 -2
  19. package/lib/cjs/node/factory.js +1 -3
  20. package/lib/cjs/types.js +44 -3
  21. package/lib/cjs/utils/common.js +5 -21
  22. package/lib/cjs/utils/errors.js +38 -1
  23. package/lib/cjs/utils/uuid.js +31 -0
  24. package/lib/cjs/web/factory.js +1 -3
  25. package/lib/cjs/web/signer.js +4 -2
  26. package/lib/esm/cli/cli.js +63 -2
  27. package/lib/esm/cli/commands/arns.js +300 -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 +123 -10
  32. package/lib/esm/cli/utils.js +4 -20
  33. package/lib/esm/common/arnsActions.js +93 -0
  34. package/lib/esm/common/factory.js +1 -3
  35. package/lib/esm/common/http.js +19 -4
  36. package/lib/esm/common/index.js +6 -0
  37. package/lib/esm/common/payment.js +482 -3
  38. package/lib/esm/common/signer.js +28 -7
  39. package/lib/esm/common/token/ario.js +102 -84
  40. package/lib/esm/common/token/index.js +1 -12
  41. package/lib/esm/common/token/solana.js +1 -0
  42. package/lib/esm/common/turbo.js +131 -0
  43. package/lib/esm/common/upload.js +4 -3
  44. package/lib/esm/node/factory.js +1 -3
  45. package/lib/esm/types.js +43 -2
  46. package/lib/esm/utils/common.js +4 -20
  47. package/lib/esm/utils/errors.js +35 -0
  48. package/lib/esm/utils/uuid.js +28 -0
  49. package/lib/esm/web/factory.js +1 -3
  50. package/lib/esm/web/signer.js +4 -2
  51. package/lib/types/cli/commands/arns.d.ts +86 -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 +238 -10
  60. package/lib/types/cli/options.d.ts.map +1 -1
  61. package/lib/types/cli/types.d.ts +41 -2
  62. package/lib/types/cli/types.d.ts.map +1 -1
  63. package/lib/types/cli/utils.d.ts.map +1 -1
  64. package/lib/types/common/arnsActions.d.ts +43 -0
  65. package/lib/types/common/arnsActions.d.ts.map +1 -0
  66. package/lib/types/common/factory.d.ts +1 -1
  67. package/lib/types/common/factory.d.ts.map +1 -1
  68. package/lib/types/common/http.d.ts +14 -1
  69. package/lib/types/common/http.d.ts.map +1 -1
  70. package/lib/types/common/index.d.ts +2 -0
  71. package/lib/types/common/index.d.ts.map +1 -1
  72. package/lib/types/common/payment.d.ts +230 -1
  73. package/lib/types/common/payment.d.ts.map +1 -1
  74. package/lib/types/common/signer.d.ts +12 -6
  75. package/lib/types/common/signer.d.ts.map +1 -1
  76. package/lib/types/common/token/ario.d.ts +13 -9
  77. package/lib/types/common/token/ario.d.ts.map +1 -1
  78. package/lib/types/common/token/index.d.ts.map +1 -1
  79. package/lib/types/common/token/solana.d.ts.map +1 -1
  80. package/lib/types/common/turbo.d.ts +140 -1
  81. package/lib/types/common/turbo.d.ts.map +1 -1
  82. package/lib/types/common/upload.d.ts.map +1 -1
  83. package/lib/types/node/factory.d.ts +1 -1
  84. package/lib/types/node/factory.d.ts.map +1 -1
  85. package/lib/types/types.d.ts +483 -7
  86. package/lib/types/types.d.ts.map +1 -1
  87. package/lib/types/utils/common.d.ts +0 -12
  88. package/lib/types/utils/common.d.ts.map +1 -1
  89. package/lib/types/utils/errors.d.ts +29 -0
  90. package/lib/types/utils/errors.d.ts.map +1 -1
  91. package/lib/types/utils/uuid.d.ts +7 -0
  92. package/lib/types/utils/uuid.d.ts.map +1 -0
  93. package/lib/types/web/factory.d.ts +1 -1
  94. package/lib/types/web/factory.d.ts.map +1 -1
  95. package/lib/types/web/signer.d.ts +1 -1
  96. package/lib/types/web/signer.d.ts.map +1 -1
  97. package/package.json +3 -2
@@ -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 { 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';
@@ -30,6 +34,7 @@ export class TurboUnauthenticatedPaymentService {
30
34
  logger: this.logger,
31
35
  });
32
36
  this.token = token;
37
+ this.url = url;
33
38
  }
34
39
  async getBalance(address) {
35
40
  const balance = await this.httpService.get({
@@ -46,6 +51,31 @@ export class TurboUnauthenticatedPaymentService {
46
51
  receivedApprovals: [],
47
52
  };
48
53
  }
54
+ async getFreeStatus(address) {
55
+ const status = await this.httpService.get({
56
+ endpoint: `/account/free?address=${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
+ }
49
79
  getFiatRates() {
50
80
  return this.httpService.get({
51
81
  endpoint: '/rates',
@@ -90,6 +120,188 @@ export class TurboUnauthenticatedPaymentService {
90
120
  equivalentWincTokenAmount: actualPaymentAmount.toString(),
91
121
  };
92
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()}/${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
+ * Fail fast (client-side) on malformed ArNS requests so JS callers that bypass
142
+ * the compile-time intent unions get a clear `ProvidedInputError` instead of an
143
+ * opaque service 4xx. Enforces the required fields per intent:
144
+ * - `Buy-Name`: `type` ('lease' | 'permabuy'); leases also need `years`.
145
+ * `processId` is OPTIONAL — omit it to have the bundler custodially
146
+ * provision the ANT (Turbo owns it), supply it for a user-owned ANT.
147
+ * - `Extend-Lease`: positive `years`
148
+ * - `Increase-Undername-Limit`: positive `increaseQty`
149
+ * - `Upgrade-Name`: just `name`
150
+ */
151
+ validateArNSPurchaseParams(params) {
152
+ const p = params;
153
+ if (!arNSPurchaseIntents.includes(p.intent)) {
154
+ throw new ProvidedInputError(`Invalid ArNS intent '${p.intent}'. Expected one of: ${arNSPurchaseIntents.join(', ')}.`);
155
+ }
156
+ if (typeof p.name !== 'string' || p.name.length === 0) {
157
+ throw new ProvidedInputError('An ArNS `name` is required.');
158
+ }
159
+ const isPositiveNumber = (v) => typeof v === 'number' && Number.isFinite(v) && v > 0;
160
+ switch (p.intent) {
161
+ case 'Buy-Name':
162
+ if (p.type !== 'lease' && p.type !== 'permabuy') {
163
+ throw new ProvidedInputError("Buy-Name requires a `type` of 'lease' or 'permabuy'.");
164
+ }
165
+ // `processId` is optional for Buy-Name: omitting it drives the
166
+ // bundler's custodial provisioning path (Turbo spawns + owns the ANT).
167
+ // If supplied it must be a non-empty string (user-owned ANT).
168
+ if (p.processId !== undefined &&
169
+ (typeof p.processId !== 'string' || p.processId.length === 0)) {
170
+ throw new ProvidedInputError('Buy-Name `processId`, when provided, must be a non-empty string (the ANT the name resolves to).');
171
+ }
172
+ if (p.type === 'lease' && !isPositiveNumber(p.years)) {
173
+ throw new ProvidedInputError('A lease `Buy-Name` requires a positive `years`.');
174
+ }
175
+ break;
176
+ case 'Extend-Lease':
177
+ if (!isPositiveNumber(p.years)) {
178
+ throw new ProvidedInputError('Extend-Lease requires a positive `years`.');
179
+ }
180
+ break;
181
+ case 'Increase-Undername-Limit':
182
+ if (!isPositiveNumber(p.increaseQty)) {
183
+ throw new ProvidedInputError('Increase-Undername-Limit requires a positive `increaseQty`.');
184
+ }
185
+ break;
186
+ case 'Upgrade-Name':
187
+ break;
188
+ }
189
+ }
190
+ getArNSPurchaseStatus({ nonce, }) {
191
+ return this.httpService.get({
192
+ endpoint: `/arns/purchase/${nonce}`,
193
+ });
194
+ }
195
+ buildArNSPurchaseQuery(input) {
196
+ // The intent-specific union members each carry only their own fields; read
197
+ // them through a single widened view rather than narrowing per intent.
198
+ const { type, years, increaseQty, processId, paidBy } = input;
199
+ const params = new URLSearchParams();
200
+ if (type !== undefined)
201
+ params.set('type', type);
202
+ if (years !== undefined)
203
+ params.set('years', `${years}`);
204
+ if (increaseQty !== undefined)
205
+ params.set('increaseQty', `${increaseQty}`);
206
+ if (processId !== undefined)
207
+ params.set('processId', processId);
208
+ if (paidBy !== undefined) {
209
+ for (const payer of Array.isArray(paidBy) ? paidBy : [paidBy]) {
210
+ params.append('paidBy', payer);
211
+ }
212
+ }
213
+ const query = params.toString();
214
+ return query.length > 0 ? `?${query}` : '';
215
+ }
216
+ /**
217
+ * Quote a fiat (Stripe) ArNS purchase — buy a name with a credit card in one
218
+ * step, with no Turbo Credits top-up in between.
219
+ *
220
+ * Returns the recorded `purchaseQuote` (its `nonce` is what
221
+ * `getArNSPurchaseStatus` polls) plus the Stripe `paymentSession` to complete
222
+ * payment with. For `payment-intent`, confirm client-side with
223
+ * `stripe.confirmCardPayment(paymentSession.client_secret, ...)`, then poll
224
+ * the nonce until the purchase reports success or failure.
225
+ *
226
+ * Throws {@link FiatPaymentsDisabledError} when the service has Stripe turned
227
+ * off (normal in the testnet sandbox) so callers can fall back to the
228
+ * credit-paid path without string-matching a generic 503.
229
+ */
230
+ async getArNSFiatPurchaseQuote(params) {
231
+ this.validateArNSPurchaseParams(params);
232
+ const { address, currency, method = 'payment-intent', promoCodes = [], } = params;
233
+ if (typeof address !== 'string' || address.length === 0) {
234
+ throw new ProvidedInputError('A destination `address` is required for a fiat ArNS purchase quote.');
235
+ }
236
+ if (!isCurrency(currency)) {
237
+ throw new ProvidedInputError(`Invalid currency '${currency}'. Supported: ${fiatCurrencyTypes.join(', ')}`);
238
+ }
239
+ // Every interpolated segment is encoded. Five user-controlled values land in
240
+ // the path here, and an unencoded one (e.g. a name or address containing
241
+ // `../`) would silently retarget the request at another route.
242
+ const segments = [
243
+ method,
244
+ address,
245
+ currency,
246
+ params.intent,
247
+ params.name,
248
+ ].map((segment) => encodeURIComponent(segment));
249
+ const query = this.buildArNSFiatQuoteQuery(params, promoCodes);
250
+ try {
251
+ return await this.httpService.get({
252
+ endpoint: `/arns/quote/${segments.join('/')}${query}`,
253
+ });
254
+ }
255
+ catch (error) {
256
+ // The service returns 503 both for "Stripe is disabled" and for internal
257
+ // errors, so the body is what disambiguates them.
258
+ if (error instanceof FailedRequestError &&
259
+ error.status === 503 &&
260
+ /Fiat \(Stripe\).*disabled/i.test(error.message)) {
261
+ throw new FiatPaymentsDisabledError(error.message);
262
+ }
263
+ throw error;
264
+ }
265
+ }
266
+ /**
267
+ * Query string for a fiat quote. Distinct from `buildArNSPurchaseQuery`
268
+ * because this route takes `uiMode` + its paired URLs and has no `paidBy`
269
+ * (fiat has no delegated payer), and because promo codes must be REPEATED
270
+ * params here: the service reads them with `parseQueryParams`, which treats a
271
+ * comma-joined string as one code rather than several.
272
+ */
273
+ buildArNSFiatQuoteQuery(params, promoCodes) {
274
+ const { type, years, increaseQty, processId } = params;
275
+ const search = new URLSearchParams();
276
+ if (type !== undefined)
277
+ search.set('type', type);
278
+ if (years !== undefined)
279
+ search.set('years', `${years}`);
280
+ if (increaseQty !== undefined)
281
+ search.set('increaseQty', `${increaseQty}`);
282
+ if (processId !== undefined)
283
+ search.set('processId', processId);
284
+ const uiMode = params.uiMode;
285
+ if (uiMode !== undefined)
286
+ search.set('uiMode', uiMode);
287
+ if (uiMode === 'embedded') {
288
+ const { returnUrl } = params;
289
+ if (returnUrl !== undefined)
290
+ search.set('returnUrl', returnUrl);
291
+ }
292
+ else {
293
+ const { successUrl, cancelUrl } = params;
294
+ if (successUrl !== undefined)
295
+ search.set('successUrl', successUrl);
296
+ if (cancelUrl !== undefined)
297
+ search.set('cancelUrl', cancelUrl);
298
+ }
299
+ for (const code of promoCodes) {
300
+ search.append('promoCode', code);
301
+ }
302
+ const query = search.toString();
303
+ return query.length > 0 ? `?${query}` : '';
304
+ }
93
305
  appendPromoCodesToQuery(promoCodes) {
94
306
  const promoCodesQuery = promoCodes.join(',');
95
307
  return promoCodesQuery ? `promoCode=${promoCodesQuery}` : '';
@@ -141,6 +353,10 @@ export class TurboUnauthenticatedPaymentService {
141
353
  return this.getCheckout(params);
142
354
  }
143
355
  async submitFundTransaction({ txId, }) {
356
+ this.logger.debug('Submitting fund transaction to Turbo...', {
357
+ txId,
358
+ url: this.url,
359
+ });
144
360
  const response = await this.httpService.post({
145
361
  endpoint: `/account/balance/${this.token}`,
146
362
  data: Buffer.from(JSON.stringify({ tx_id: txId })),
@@ -250,6 +466,272 @@ export class TurboAuthenticatedPaymentService extends TurboUnauthenticatedPaymen
250
466
  userAddress ??= await this.signer.getNativeAddress();
251
467
  return super.getBalance(userAddress);
252
468
  }
469
+ /**
470
+ * Quote a fiat (Stripe) ArNS purchase. `address` defaults to this signer's
471
+ * native address — the wallet that will own the name — so the common case
472
+ * needs no address at all. Pass one explicitly to buy on another wallet's
473
+ * behalf; the route takes the destination as a path param and requires no
474
+ * signature, which is why it is available unauthenticated too.
475
+ */
476
+ async getArNSFiatPurchaseQuote(params) {
477
+ const address = params.address ?? (await this.signer.getNativeAddress());
478
+ return super.getArNSFiatPurchaseQuote({
479
+ ...params,
480
+ address,
481
+ });
482
+ }
483
+ async getFreeStatus(userAddress) {
484
+ userAddress ??= await this.signer.getNativeAddress();
485
+ return super.getFreeStatus(userAddress);
486
+ }
487
+ /**
488
+ * The signer's OWN completed top-up history (crypto + fiat), merged newest
489
+ * first and keyset-paginated. This is a SIGNED GET: unlike `getBalance` /
490
+ * `getFreeStatus` (which name a wallet by `?address=`), payment history is
491
+ * self-scoped and returns only the rows belonging to the signing wallet — the
492
+ * service reads the address from the signature, never a query param.
493
+ *
494
+ * We sign the bare nonce (no action-binding of `limit`/`cursor`) to match the
495
+ * service's `verifySignature` middleware; the pagination params ride in the
496
+ * query string. Pass `cursor` from a prior response to fetch the next page.
497
+ */
498
+ async getPaymentHistory({ limit, cursor, } = {}) {
499
+ const headers = await this.signer.generateSignedRequestHeaders();
500
+ const query = new URLSearchParams();
501
+ if (limit !== undefined) {
502
+ query.set('limit', `${limit}`);
503
+ }
504
+ if (cursor !== undefined) {
505
+ query.set('cursor', cursor);
506
+ }
507
+ const queryString = query.toString();
508
+ return this.httpService.get({
509
+ endpoint: `/account/payments${queryString ? `?${queryString}` : ''}`,
510
+ headers,
511
+ allowedStatuses: [200],
512
+ });
513
+ }
514
+ /**
515
+ * Buy / extend / upgrade an ArNS name, paying with the signer's Turbo credit
516
+ * balance. The bundler performs the on-chain ARIO purchase and debits credits;
517
+ * a `402` (FailedRequestError.status === 402) indicates insufficient credits.
518
+ */
519
+ // ===== ArNS actions — the sponsored surface =====
520
+ //
521
+ // Every ArNS operation is an ACTION, and an action has exactly one of two
522
+ // shapes, chosen by the SERVER rather than the caller: either Turbo already
523
+ // holds the authority (`completed`), or the ANT owner must sign a transaction
524
+ // Turbo has already fee-payer-signed (`awaiting-signature`).
525
+ //
526
+ // The shape is not stable per action, which is why callers must branch on
527
+ // `status` and never on which action they asked for: `set-record` completes
528
+ // alone while Turbo is a controller, and degrades to `awaiting-signature`
529
+ // the moment the customer revokes Turbo.
530
+ //
531
+ // This replaced `/arns/purchase/{intent}/{name}`, `/arns/transfer/{antId}`
532
+ // and `/arns/manage/*`, which were deleted along with Turbo-custodial ANTs.
533
+ // Turbo now takes custody of nothing: every ANT is minted straight to the
534
+ // customer.
535
+ /**
536
+ * Create an action. Returns `completed` or `awaiting-signature`.
537
+ *
538
+ * Credits are debited HERE, not at `/sign`. Capture the returned `nonce`
539
+ * before prompting for a signature: it is the idempotency key, and polling
540
+ * it is how you resume. Never re-create an action to "retry" — that debits
541
+ * a second time. An abandoned action is refunded automatically.
542
+ */
543
+ async createArNSAction(action, params = {}, ownerProof) {
544
+ const nonce = uuidV4();
545
+ const headers = {
546
+ ...(await this.signer.generateSignedRequestHeaders(nonce)),
547
+ 'content-type': 'application/json',
548
+ };
549
+ // Record actions carry a SECOND signature, from the ANT owner's Solana key
550
+ // over a different message. It travels in its own `x-owner-*` headers
551
+ // because two signatures cannot share one header set.
552
+ if (ownerProof !== undefined) {
553
+ Object.assign(headers, await arNSOwnerProofHeaders(ownerProof.owner, ownerProof.message, uuidV4()));
554
+ }
555
+ try {
556
+ return await this.httpService.post({
557
+ endpoint: `/arns/actions/${action}`,
558
+ headers,
559
+ data: Buffer.from(JSON.stringify(params)),
560
+ // Non-idempotent signed write that has already debited. A blind retry
561
+ // risks paying twice for one name; poll the nonce instead.
562
+ retry: false,
563
+ });
564
+ }
565
+ catch (error) {
566
+ if (error instanceof FailedRequestError && error.status === 402) {
567
+ throw new InsufficientCreditsError(error.message);
568
+ }
569
+ throw error;
570
+ }
571
+ }
572
+ /**
573
+ * Submit the owner-signed transaction for an `awaiting-signature` action.
574
+ *
575
+ * `signedTransaction` is the FULL serialized transaction, base64 — not just
576
+ * the signature. Replaying a completed action returns `alreadyCompleted:
577
+ * true` rather than buying twice, so this is safe to call again if a
578
+ * response is lost.
579
+ */
580
+ async signArNSAction(nonce, signedTransaction) {
581
+ return this.httpService.post({
582
+ endpoint: `/arns/actions/${nonce}/sign`,
583
+ headers: {
584
+ ...(await this.signer.generateSignedRequestHeaders(uuidV4())),
585
+ 'content-type': 'application/json',
586
+ },
587
+ data: Buffer.from(JSON.stringify({ transaction: signedTransaction })),
588
+ retry: false,
589
+ });
590
+ }
591
+ /**
592
+ * Status of an action by nonce. Open — no signature required — so it works
593
+ * from a status page or callback handler that never holds the payer's key.
594
+ *
595
+ * Terminal success carries `messageId`; terminal failure carries
596
+ * `failedDate`.
597
+ */
598
+ async getArNSActionStatus(nonce) {
599
+ return this.httpService.get({
600
+ endpoint: `/arns/actions/${nonce}`,
601
+ });
602
+ }
603
+ /**
604
+ * Run an action to a terminal state, signing if the server asks for it.
605
+ *
606
+ * This is the two-shape branch, once, in one place — so callers cannot
607
+ * hardcode which actions need a signature and break when a customer
608
+ * exercises ownership.
609
+ */
610
+ async completeArNSAction(action, params, owner, opts = {}, ownerProofMessage) {
611
+ const created = await this.createArNSAction(action, params, owner !== undefined && ownerProofMessage !== undefined
612
+ ? { owner, message: ownerProofMessage }
613
+ : undefined);
614
+ // Fires before any wallet prompt: the action is already debited, so the
615
+ // caller needs the nonce persisted even if the user walks away here.
616
+ await opts.onNonce?.(created.nonce);
617
+ if (created.status === 'completed')
618
+ return created;
619
+ if (owner === undefined) {
620
+ throw new Error(`ArNS action "${action}" requires the ANT owner's signature, but no owner signer was provided. ` +
621
+ `Pass \`owner\`, or drive createArNSAction/signArNSAction yourself. ` +
622
+ `Nonce ${created.nonce} is already debited — poll it rather than re-creating.`);
623
+ }
624
+ const signed = await owner.signTransaction(created.transaction);
625
+ return this.signArNSAction(created.nonce, signed);
626
+ }
627
+ /**
628
+ * Buy a name. The ANT is minted straight to `owner` — Turbo never holds it.
629
+ *
630
+ * This is the ONLY action that always needs the owner's signature:
631
+ * `ario_ant::initialize` is the one instruction in the whole lifecycle that
632
+ * requires the ANT owner's key. The customer signs once, here, and never
633
+ * again unless they change controllers or transfer the name.
634
+ *
635
+ * The owner needs a Solana key to sign with, NOT a funded one — Turbo pays
636
+ * every lamport of fee and rent.
637
+ */
638
+ async buyArNSName({ name, owner, type = 'lease', years, paidBy, onNonce, }) {
639
+ return this.completeArNSAction('buy-name', {
640
+ name,
641
+ ownerAddress: await owner.getAddress(),
642
+ type,
643
+ ...(years !== undefined ? { years } : {}),
644
+ ...(paidBy !== undefined ? { paidBy } : {}),
645
+ }, owner, { onNonce });
646
+ }
647
+ /** Extend a lease. Permissionless on chain — no owner signature needed. */
648
+ async extendArNSLease({ name, years, paidBy, onNonce, }) {
649
+ return this.completeArNSAction('extend-lease', { name, years, ...(paidBy !== undefined ? { paidBy } : {}) }, undefined, { onNonce });
650
+ }
651
+ /** Upgrade a lease to a permanent name. No owner signature needed. */
652
+ async upgradeArNSName({ name, paidBy, onNonce, }) {
653
+ return this.completeArNSAction('upgrade-name', { name, ...(paidBy !== undefined ? { paidBy } : {}) }, undefined, { onNonce });
654
+ }
655
+ /** Raise the undername limit. No owner signature needed. */
656
+ async increaseArNSUndernameLimit({ name, increaseQty, paidBy, onNonce, }) {
657
+ return this.completeArNSAction('increase-undername-limit', { name, increaseQty, ...(paidBy !== undefined ? { paidBy } : {}) }, undefined, { onNonce });
658
+ }
659
+ /**
660
+ * Point a name (or undername) at an Arweave transaction.
661
+ *
662
+ * Free — Turbo sponsors the Solana fee. Completes in one call while Turbo is
663
+ * a controller of the ANT, and returns `awaiting-signature` once the customer
664
+ * has revoked Turbo, at which point `owner` signs it themselves. Both paths
665
+ * are handled here.
666
+ *
667
+ * The owner proof is required EITHER WAY: Turbo is directing its own
668
+ * controller authority over an asset someone else owns, so nothing on chain
669
+ * records the owner's consent and we demand it. It is a MESSAGE signature,
670
+ * not a transaction — cheap and offline, but still a wallet prompt.
671
+ */
672
+ async setArNSRecord({ antId, owner, transactionId, undername = '@', ttlSeconds = 3600, onNonce, }) {
673
+ return this.completeArNSAction('set-record', {
674
+ antId,
675
+ ownerAddress: await owner.getAddress(),
676
+ transactionId,
677
+ undername,
678
+ ttlSeconds,
679
+ }, owner, { onNonce }, buildArNSCustodyMessage('set-record', [
680
+ antId,
681
+ undername,
682
+ transactionId,
683
+ String(ttlSeconds),
684
+ ]));
685
+ }
686
+ /** Remove a record (an undername). Free; same two-shape rules as setArNSRecord. */
687
+ async removeArNSRecord({ antId, owner, undername, onNonce, }) {
688
+ return this.completeArNSAction('remove-record', { antId, ownerAddress: await owner.getAddress(), undername }, owner, { onNonce }, buildArNSCustodyMessage('remove-record', [antId, undername]));
689
+ }
690
+ /**
691
+ * Grant controller rights on the ANT. Omit `target` for Turbo itself, which
692
+ * is what makes `setArNSRecord` a single call.
693
+ *
694
+ * Owner-signed: changing an ANT's access control is an owner-only
695
+ * instruction. Free to the customer — Turbo funds the ACL page growth.
696
+ */
697
+ async addArNSController({ antId, owner, target, onNonce, }) {
698
+ return this.completeArNSAction('add-controller', {
699
+ antId,
700
+ ownerAddress: await owner.getAddress(),
701
+ ...(target !== undefined ? { target } : {}),
702
+ }, owner, { onNonce });
703
+ }
704
+ /**
705
+ * Revoke controller rights — the escape hatch that keeps "Turbo is not a
706
+ * custodian" honest.
707
+ *
708
+ * Always available, always free, and needs nothing from Turbo but the fee.
709
+ * After revoking, `setArNSRecord` keeps working: it simply starts returning
710
+ * `awaiting-signature` so the owner signs their own record writes.
711
+ */
712
+ async removeArNSController({ antId, owner, target, onNonce, }) {
713
+ return this.completeArNSAction('remove-controller', {
714
+ antId,
715
+ ownerAddress: await owner.getAddress(),
716
+ ...(target !== undefined ? { target } : {}),
717
+ }, owner, { onNonce });
718
+ }
719
+ /**
720
+ * Hand the ANT to a new owner. Irreversible: after this lands, `owner` no
721
+ * longer controls the name. Owner-signed, and sponsored like the rest.
722
+ */
723
+ async transferArNSAnt({ antId, owner, target, onNonce, }) {
724
+ return this.completeArNSAction('transfer', { antId, ownerAddress: await owner.getAddress(), target }, owner, { onNonce });
725
+ }
726
+ /**
727
+ * Defaults to the signer's own address when `userAddress` is omitted
728
+ * (`null`/`undefined`). Passing `''` does NOT trigger this default --
729
+ * mirrors `getBalance`'s existing behavior above.
730
+ */
731
+ async getArNSNames(userAddress) {
732
+ userAddress ??= await this.signer.getNativeAddress();
733
+ return super.getArNSNames(userAddress);
734
+ }
253
735
  async getCreditShareApprovals({ userAddress, }) {
254
736
  userAddress ??= await this.signer.getNativeAddress();
255
737
  return super.getCreditShareApprovals({ userAddress });
@@ -278,9 +760,6 @@ export class TurboAuthenticatedPaymentService extends TurboUnauthenticatedPaymen
278
760
  if (!this.tokenTools) {
279
761
  throw new Error(`Token type not supported for crypto fund ${this.token}`);
280
762
  }
281
- if (this.token === 'ario') {
282
- throw new Error(`Top up with tokens is not supported for ARIO until after SOL migration is complete`);
283
- }
284
763
  if (turboCreditDestinationAddress !== undefined) {
285
764
  if (isAnyValidUserAddress(turboCreditDestinationAddress) === false) {
286
765
  throw new Error(`Invalid turboCreditDestinationAddress provided: ${turboCreditDestinationAddress}`);
@@ -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
  }