@ardrive/turbo-sdk 1.42.0-alpha.7 → 1.42.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,10 +31,26 @@ exports.removeArNSRecord = removeArNSRecord;
31
31
  * limitations under the License.
32
32
  */
33
33
  const bignumber_js_1 = require("bignumber.js");
34
+ const arnsActions_js_1 = require("../../common/arnsActions.js");
34
35
  const factory_js_1 = require("../../node/factory.js");
35
36
  const errors_js_1 = require("../../utils/errors.js");
36
37
  const constants_js_1 = require("../constants.js");
37
38
  const utils_js_1 = require("../utils.js");
39
+ /**
40
+ * The ANT owner's Solana key.
41
+ *
42
+ * Deliberately separate from the wallet that pays: the payer holds Turbo
43
+ * credits (and may be Arweave or Ethereum), while the owner holds the ANT and
44
+ * must be Solana. The owner needs a key to SIGN with, not a funded account —
45
+ * Turbo is the fee payer on every sponsored action.
46
+ */
47
+ function ownerFromOptions(options) {
48
+ if (options.ownerKey === undefined || options.ownerKey === '') {
49
+ throw new Error('Must provide --owner-key (a base58 Solana secret key) — it owns the ANT and signs for it. ' +
50
+ 'This is separate from the wallet paying in Turbo Credits.');
51
+ }
52
+ return (0, arnsActions_js_1.solanaOwnerSigner)(options.ownerKey);
53
+ }
38
54
  function requiredNameFromOptions(options) {
39
55
  if (options.name === undefined || options.name.length === 0) {
40
56
  throw new Error('Must provide an ArNS --name');
@@ -128,10 +144,12 @@ async function withCreditErrorMapping(fn) {
128
144
  }
129
145
  function logPurchaseResult(action, result) {
130
146
  console.log(JSON.stringify({
131
- message: `${action} submitted!`,
147
+ message: `${action} completed!`,
132
148
  nonce: result.nonce,
133
- arioWriteId: result.arioWriteResult?.id,
134
- purchaseReceipt: result.purchaseReceipt,
149
+ antId: result.antId,
150
+ // Solana transaction id of the on-chain write.
151
+ messageId: result.messageId,
152
+ ...(result.alreadyCompleted === true ? { alreadyCompleted: true } : {}),
135
153
  }, null, 2));
136
154
  console.log(`\nTrack this purchase with:\n turbo arns-purchase-status --nonce ${result.nonce}`);
137
155
  }
@@ -149,6 +167,14 @@ async function arnsPrice(options, turbo) {
149
167
  }
150
168
  async function arnsFiatQuote(options, turbo) {
151
169
  const params = arnsPriceParamsFromOptions(options);
170
+ // `arnsPriceParamsFromOptions` substitutes PRICING_PLACEHOLDER_PROCESS_ID for
171
+ // an omitted Buy-Name `processId`, which is fine for a price lookup but NOT
172
+ // here: a quote records a real purchase, and that placeholder is not a valid
173
+ // ANT. Send `processId` only when the caller actually supplied one — omitting
174
+ // it is what tells Turbo to custodially provision the ANT.
175
+ if (options.processId === undefined) {
176
+ delete params.processId;
177
+ }
152
178
  const address = options.address;
153
179
  if (address === undefined) {
154
180
  throw new Error('A destination --address is required for a fiat ArNS quote.');
@@ -190,20 +216,19 @@ async function buyArNSName(options, turbo) {
190
216
  const name = requiredNameFromOptions(options);
191
217
  const type = typeFromOptions(options.type);
192
218
  const paidBy = paidByFromArNSOptions(options.paidBy);
193
- // `--process-id` is OPTIONAL: omit it to have Turbo custodially provision the
194
- // ANT (Turbo spawns + owns it Model A); supply it to point the name at a
195
- // user-owned ANT (Model B). When omitted, no `processId` is sent.
196
- const processId = options.processId;
219
+ // Every buy mints a fresh ANT straight to `--owner-key`; Turbo never holds
220
+ // it, and there is no bring-your-own-ANT path any more.
221
+ const owner = ownerFromOptions(options);
197
222
  const client = turbo ?? (await (0, utils_js_1.turboFromOptions)(options));
198
223
  const result = await withCreditErrorMapping(() => type === 'lease'
199
224
  ? client.buyArNSName({
200
225
  name,
226
+ owner,
201
227
  type: 'lease',
202
228
  years: positiveIntFromOption(options.years, '--years'),
203
- processId,
204
229
  paidBy,
205
230
  })
206
- : client.buyArNSName({ name, type: 'permabuy', processId, paidBy }));
231
+ : client.buyArNSName({ name, owner, type: 'permabuy', paidBy }));
207
232
  logPurchaseResult('ArNS name purchase', result);
208
233
  }
209
234
  async function extendArNSLease(options, turbo) {
@@ -252,6 +277,7 @@ async function transferArNSAnt(options, turbo) {
252
277
  const client = turbo ?? (await (0, utils_js_1.turboFromOptions)(options));
253
278
  const result = await client.transferArNSAnt({
254
279
  antId: options.antId,
280
+ owner: ownerFromOptions(options),
255
281
  target: options.target,
256
282
  });
257
283
  console.log(JSON.stringify({ message: 'ANT transfer submitted!', ...result }, null, 2));
@@ -267,6 +293,7 @@ async function setArNSRecord(options, turbo) {
267
293
  const client = turbo ?? (await (0, utils_js_1.turboFromOptions)(options));
268
294
  const result = await client.setArNSRecord({
269
295
  antId: options.antId,
296
+ owner: ownerFromOptions(options),
270
297
  undername: options.undername ?? '@',
271
298
  transactionId: options.transactionId,
272
299
  ttlSeconds,
@@ -283,6 +310,7 @@ async function removeArNSRecord(options, turbo) {
283
310
  const client = turbo ?? (await (0, utils_js_1.turboFromOptions)(options));
284
311
  const result = await client.removeArNSRecord({
285
312
  antId: options.antId,
313
+ owner: ownerFromOptions(options),
286
314
  undername: options.undername,
287
315
  });
288
316
  console.log(JSON.stringify({ message: 'ArNS record removed!', ...result }, null, 2));
@@ -206,9 +206,9 @@ exports.optionMap = {
206
206
  alias: '--increase-qty <qty>',
207
207
  description: 'Number of additional undernames (Increase-Undername-Limit)',
208
208
  },
209
- arnsProcessId: {
210
- alias: '--process-id <processId>',
211
- description: 'ANT process ID the ArNS name resolves to (Buy-Name). Optional: omit for Turbo custodial provisioning (Turbo owns the ANT); supply for a user-owned ANT',
209
+ arnsOwnerKey: {
210
+ alias: '--owner-key <base58SolanaSecretKey>',
211
+ description: 'Base58 Solana secret key that OWNS the ANT and signs for it. Separate from the wallet paying in Turbo Credits the owner needs a key to sign with, not SOL: Turbo pays every fee and rent',
212
212
  },
213
213
  arnsNonce: {
214
214
  alias: '--nonce <nonce>',
@@ -302,14 +302,12 @@ exports.arnsPriceOptions = [
302
302
  exports.optionMap.arnsType,
303
303
  exports.optionMap.arnsYears,
304
304
  exports.optionMap.arnsIncreaseQty,
305
- exports.optionMap.arnsProcessId,
306
305
  ];
307
306
  exports.arnsFiatQuoteOptions = [
308
307
  exports.optionMap.arnsName,
309
308
  exports.optionMap.arnsType,
310
309
  exports.optionMap.arnsYears,
311
310
  exports.optionMap.arnsIncreaseQty,
312
- exports.optionMap.arnsProcessId,
313
311
  exports.optionMap.address,
314
312
  exports.optionMap.currency,
315
313
  {
@@ -326,7 +324,7 @@ exports.buyArNSNameOptions = [
326
324
  exports.optionMap.arnsName,
327
325
  exports.optionMap.arnsType,
328
326
  exports.optionMap.arnsYears,
329
- exports.optionMap.arnsProcessId,
327
+ exports.optionMap.arnsOwnerKey,
330
328
  exports.optionMap.paidBy,
331
329
  ];
332
330
  exports.extendArNSLeaseOptions = [
@@ -349,11 +347,13 @@ exports.upgradeArNSNameOptions = [
349
347
  exports.arnsPurchaseStatusOptions = [exports.optionMap.arnsNonce];
350
348
  exports.transferArNSAntOptions = [
351
349
  ...exports.walletOptions,
350
+ exports.optionMap.arnsOwnerKey,
352
351
  exports.optionMap.arnsAntId,
353
352
  exports.optionMap.arnsTarget,
354
353
  ];
355
354
  exports.setArNSRecordOptions = [
356
355
  ...exports.walletOptions,
356
+ exports.optionMap.arnsOwnerKey,
357
357
  exports.optionMap.arnsAntId,
358
358
  exports.optionMap.arnsUndername,
359
359
  exports.optionMap.arnsTransactionId,
@@ -361,6 +361,7 @@ exports.setArNSRecordOptions = [
361
361
  ];
362
362
  exports.removeArNSRecordOptions = [
363
363
  ...exports.walletOptions,
364
+ exports.optionMap.arnsOwnerKey,
364
365
  exports.optionMap.arnsAntId,
365
366
  exports.optionMap.arnsUndername,
366
367
  ];
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildArNSCustodyMessage = buildArNSCustodyMessage;
7
+ exports.arNSOwnerProofHeaders = arNSOwnerProofHeaders;
8
+ exports.solanaOwnerSigner = solanaOwnerSigner;
9
+ exports.emptySignatureSlots = emptySignatureSlots;
10
+ /**
11
+ * Copyright (C) 2022-2024 Permanent Data Solutions, Inc.
12
+ *
13
+ * Licensed under the Apache License, Version 2.0 (the "License");
14
+ * you may not use this file except in compliance with the License.
15
+ * You may obtain a copy of the License at
16
+ *
17
+ * http://www.apache.org/licenses/LICENSE-2.0
18
+ *
19
+ * Unless required by applicable law or agreed to in writing, software
20
+ * distributed under the License is distributed on an "AS IS" BASIS,
21
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22
+ * See the License for the specific language governing permissions and
23
+ * limitations under the License.
24
+ */
25
+ const web3_js_1 = require("@solana/web3.js");
26
+ const bs58_1 = __importDefault(require("bs58"));
27
+ const tweetnacl_1 = __importDefault(require("tweetnacl"));
28
+ const base64_js_1 = require("../utils/base64.js");
29
+ /**
30
+ * Canonical ACTION-BOUND message for the owner proof.
31
+ *
32
+ * MUST match the bundler's `buildArNSCustodyMessage` byte for byte
33
+ * (newline-delimited) or every signature is rejected. The bundler rebuilds this
34
+ * from the request and verifies the signature over `message + nonce`, so a
35
+ * signature captured for one operation cannot authorize a different one, and
36
+ * the nonce is consumed on use so the same one cannot be replayed (e.g. to
37
+ * revert a record to an older value).
38
+ */
39
+ function buildArNSCustodyMessage(action, fields) {
40
+ return ['arns', action, ...fields].join('\n');
41
+ }
42
+ /** Solana's signature-type discriminator in Turbo's signed-request scheme. */
43
+ const SOLANA_SIGNATURE_TYPE = 4;
44
+ /**
45
+ * The ANT owner's half of the envelope, in its own `x-owner-*` headers.
46
+ *
47
+ * Two signatures travel on a record action — the PAYER's signed request over
48
+ * `"" + nonce`, and the OWNER's action-bound proof over `message + nonce` —
49
+ * from two different keys, usually on two different chains. They cannot share
50
+ * one header set: whichever verifier ran second would reject a signature that
51
+ * was never meant for it.
52
+ */
53
+ async function arNSOwnerProofHeaders(owner, message, nonce) {
54
+ const address = await owner.getAddress();
55
+ const signature = await owner.signMessage(Uint8Array.from(Buffer.from(message + nonce)));
56
+ return {
57
+ // A Solana address IS the base58-encoded 32-byte ed25519 public key, so
58
+ // decoding the address recovers exactly the bytes the bundler verifies
59
+ // against. Keeps ArNSOwnerSigner minimal — no separate getPublicKey().
60
+ 'x-owner-public-key': (0, base64_js_1.toB64Url)(Buffer.from(bs58_1.default.decode(address))),
61
+ 'x-owner-nonce': nonce,
62
+ 'x-owner-signature': (0, base64_js_1.toB64Url)(Buffer.from(signature)),
63
+ 'x-owner-signature-type': String(SOLANA_SIGNATURE_TYPE),
64
+ };
65
+ }
66
+ /**
67
+ * Build an {@link ArNSOwnerSigner} from a raw Solana secret key.
68
+ *
69
+ * For servers and tests. A browser wallet (Phantom, Solflare, or a console's
70
+ * embedded wallet) should implement the interface directly against its own
71
+ * `signTransaction` / `signMessage` rather than exposing a secret key.
72
+ *
73
+ * Note the owner needs a key to SIGN with, not a funded account: Turbo is the
74
+ * fee payer on every sponsored action, so this wallet's SOL balance can stay
75
+ * at zero for the entire life of the name.
76
+ */
77
+ function solanaOwnerSigner(secretKey) {
78
+ const keypair = web3_js_1.Keypair.fromSecretKey(typeof secretKey === 'string' ? bs58_1.default.decode(secretKey) : secretKey);
79
+ return {
80
+ getAddress: () => keypair.publicKey.toBase58(),
81
+ signTransaction: async (transactionBase64) => {
82
+ const tx = web3_js_1.VersionedTransaction.deserialize(Buffer.from(transactionBase64, 'base64'));
83
+ // Sign the bytes as returned. Turbo's fee-payer signature already covers
84
+ // this exact message; rebuilding it from parts invalidates that and the
85
+ // submission is rejected.
86
+ tx.sign([keypair]);
87
+ return Buffer.from(tx.serialize()).toString('base64');
88
+ },
89
+ signMessage: async (message) => tweetnacl_1.default.sign.detached(message, keypair.secretKey),
90
+ };
91
+ }
92
+ /**
93
+ * Count the signature slots a prepared transaction still has empty.
94
+ *
95
+ * Turbo pre-signs as fee payer and leaves exactly one slot for the owner, so a
96
+ * client can assert that before prompting a wallet — a cheap way to catch a
97
+ * malformed or already-signed transaction before bothering the user.
98
+ */
99
+ function emptySignatureSlots(transactionBase64) {
100
+ const tx = web3_js_1.VersionedTransaction.deserialize(Buffer.from(transactionBase64, 'base64'));
101
+ return tx.signatures.filter((sig) => sig.every((byte) => byte === 0)).length;
102
+ }
@@ -21,6 +21,7 @@ const types_js_1 = require("../types.js");
21
21
  const common_js_1 = require("../utils/common.js");
22
22
  const errors_js_1 = require("../utils/errors.js");
23
23
  const uuid_js_1 = require("../utils/uuid.js");
24
+ const arnsActions_js_1 = require("./arnsActions.js");
24
25
  const http_js_1 = require("./http.js");
25
26
  const http_js_2 = require("./http.js");
26
27
  const logger_js_1 = require("./logger.js");
@@ -62,6 +63,22 @@ class TurboUnauthenticatedPaymentService {
62
63
  // and coerce a missing field (e.g. a 404 body) to `null`.
63
64
  return { bytesRemaining: status?.bytesRemaining ?? null };
64
65
  }
66
+ /**
67
+ * Returns the ArNS names a wallet owns or controls -- both custodial names
68
+ * bought via Turbo's ArNS-with-credits feature (Turbo may spawn and hold
69
+ * the ANT on the caller's behalf, depending on the buy) and self-custody
70
+ * names, in one list. See `TurboArNSName` for field semantics, including
71
+ * the `custodial` flag distinguishing the two.
72
+ *
73
+ * To read a name's current records or lease/expiration state, use
74
+ * `@ar.io/sdk` directly against the returned `antId` -- it talks to the
75
+ * chain directly and needs no round-trip through this SDK/backend.
76
+ */
77
+ getArNSNames(address) {
78
+ return this.httpService.get({
79
+ endpoint: `/arns/my-names/${encodeURIComponent(address)}`,
80
+ });
81
+ }
65
82
  getFiatRates() {
66
83
  return this.httpService.get({
67
84
  endpoint: '/rates',
@@ -110,9 +127,18 @@ class TurboUnauthenticatedPaymentService {
110
127
  // `async` so a validation failure surfaces as a rejected promise (consistent
111
128
  // with `purchaseArNSName`) rather than a synchronous throw.
112
129
  this.validateArNSPurchaseParams(params);
113
- return this.httpService.get({
130
+ const price = await this.httpService.get({
114
131
  endpoint: `/arns/price/${params.intent.toLowerCase()}/${params.name}${this.buildArNSPurchaseQuery(params)}`,
115
132
  });
133
+ // Normalize the figure to charge into ONE field. `winc` is the name only
134
+ // and excludes the ANT spawn surcharge — for a Buy-Name that surcharge can
135
+ // exceed the name's own price, so a caller reading `winc` silently
136
+ // under-quotes every purchase. Surfacing `wincTotal` makes the correct
137
+ // field the obvious one.
138
+ return {
139
+ ...price,
140
+ wincTotal: price.wincTotalWithAntSpawn ?? price.winc,
141
+ };
116
142
  }
117
143
  /**
118
144
  * Fail fast (client-side) on malformed ArNS requests so JS callers that bypass
@@ -494,112 +520,221 @@ class TurboAuthenticatedPaymentService extends TurboUnauthenticatedPaymentServic
494
520
  * balance. The bundler performs the on-chain ARIO purchase and debits credits;
495
521
  * a `402` (FailedRequestError.status === 402) indicates insufficient credits.
496
522
  */
497
- async purchaseArNSName(params) {
498
- this.validateArNSPurchaseParams(params);
499
- // The bundler requires the signed nonce to be a UUID; it also doubles as
500
- // the idempotency + status-lookup key (`getArNSPurchaseStatus`).
523
+ // ===== ArNS actions — the sponsored surface =====
524
+ //
525
+ // Every ArNS operation is an ACTION, and an action has exactly one of two
526
+ // shapes, chosen by the SERVER rather than the caller: either Turbo already
527
+ // holds the authority (`completed`), or the ANT owner must sign a transaction
528
+ // Turbo has already fee-payer-signed (`awaiting-signature`).
529
+ //
530
+ // The shape is not stable per action, which is why callers must branch on
531
+ // `status` and never on which action they asked for: `set-record` completes
532
+ // alone while Turbo is a controller, and degrades to `awaiting-signature`
533
+ // the moment the customer revokes Turbo.
534
+ //
535
+ // This replaced `/arns/purchase/{intent}/{name}`, `/arns/transfer/{antId}`
536
+ // and `/arns/manage/*`, which were deleted along with Turbo-custodial ANTs.
537
+ // Turbo now takes custody of nothing: every ANT is minted straight to the
538
+ // customer.
539
+ /**
540
+ * Create an action. Returns `completed` or `awaiting-signature`.
541
+ *
542
+ * Credits are debited HERE, not at `/sign`. Capture the returned `nonce`
543
+ * before prompting for a signature: it is the idempotency key, and polling
544
+ * it is how you resume. Never re-create an action to "retry" — that debits
545
+ * a second time. An abandoned action is refunded automatically.
546
+ */
547
+ async createArNSAction(action, params = {}, ownerProof) {
501
548
  const nonce = (0, uuid_js_1.uuidV4)();
502
- const headers = await this.signer.generateSignedRequestHeaders(nonce);
503
- let response;
549
+ const headers = {
550
+ ...(await this.signer.generateSignedRequestHeaders(nonce)),
551
+ 'content-type': 'application/json',
552
+ };
553
+ // Record actions carry a SECOND signature, from the ANT owner's Solana key
554
+ // over a different message. It travels in its own `x-owner-*` headers
555
+ // because two signatures cannot share one header set.
556
+ if (ownerProof !== undefined) {
557
+ Object.assign(headers, await (0, arnsActions_js_1.arNSOwnerProofHeaders)(ownerProof.owner, ownerProof.message, (0, uuid_js_1.uuidV4)()));
558
+ }
504
559
  try {
505
- response = await this.httpService.post({
506
- endpoint: `/arns/purchase/${params.intent.toLowerCase()}/${params.name}${this.buildArNSPurchaseQuery(params)}`,
560
+ return await this.httpService.post({
561
+ endpoint: `/arns/actions/${action}`,
507
562
  headers,
508
- // Params travel in the query string + signed headers; the service reads
509
- // no body, but the HTTP layer requires a `data` field.
510
- data: Buffer.from([]),
511
- // Non-idempotent signed write: the nonce is single-use, so a retried
512
- // (but already-landed) purchase would 4xx as "already exists". Poll
513
- // status by nonce instead of retrying.
563
+ data: Buffer.from(JSON.stringify(params)),
564
+ // Non-idempotent signed write that has already debited. A blind retry
565
+ // risks paying twice for one name; poll the nonce instead.
514
566
  retry: false,
515
567
  });
516
568
  }
517
569
  catch (error) {
518
- // Surface a credit shortfall as a typed, catchable error so callers can
519
- // prompt a top-up. The `nonce` is the idempotency key: after topping up,
520
- // retry the same purchase (a fresh nonce is fine — the service dedupes by
521
- // the on-chain effect, and a captured nonce lets you poll status).
522
570
  if (error instanceof errors_js_1.FailedRequestError && error.status === 402) {
523
571
  throw new errors_js_1.InsufficientCreditsError(error.message);
524
572
  }
525
573
  throw error;
526
574
  }
527
- // Normalize both nonce fields to the one we signed so callers can poll with
528
- // either `response.nonce` or `response.purchaseReceipt.nonce`.
529
- return {
530
- ...response,
531
- nonce,
532
- purchaseReceipt: { ...response.purchaseReceipt, nonce },
533
- };
534
575
  }
535
- buyArNSName(params) {
536
- return this.purchaseArNSName({
537
- ...params,
538
- intent: 'Buy-Name',
576
+ /**
577
+ * Submit the owner-signed transaction for an `awaiting-signature` action.
578
+ *
579
+ * `signedTransaction` is the FULL serialized transaction, base64 — not just
580
+ * the signature. Replaying a completed action returns `alreadyCompleted:
581
+ * true` rather than buying twice, so this is safe to call again if a
582
+ * response is lost.
583
+ */
584
+ async signArNSAction(nonce, signedTransaction) {
585
+ return this.httpService.post({
586
+ endpoint: `/arns/actions/${nonce}/sign`,
587
+ headers: {
588
+ ...(await this.signer.generateSignedRequestHeaders((0, uuid_js_1.uuidV4)())),
589
+ 'content-type': 'application/json',
590
+ },
591
+ data: Buffer.from(JSON.stringify({ transaction: signedTransaction })),
592
+ retry: false,
539
593
  });
540
594
  }
541
- extendArNSLease(params) {
542
- return this.purchaseArNSName({ ...params, intent: 'Extend-Lease' });
543
- }
544
- increaseArNSUndernameLimit(params) {
545
- return this.purchaseArNSName({
546
- ...params,
547
- intent: 'Increase-Undername-Limit',
595
+ /**
596
+ * Status of an action by nonce. Open no signature required — so it works
597
+ * from a status page or callback handler that never holds the payer's key.
598
+ *
599
+ * Terminal success carries `messageId`; terminal failure carries
600
+ * `failedDate`.
601
+ */
602
+ async getArNSActionStatus(nonce) {
603
+ return this.httpService.get({
604
+ endpoint: `/arns/actions/${nonce}`,
548
605
  });
549
606
  }
550
- upgradeArNSName(params) {
551
- return this.purchaseArNSName({ ...params, intent: 'Upgrade-Name' });
552
- }
553
- // ---- ArNS ANT custody: self-custody exit + manage records ----
554
- // Canonical ACTION-BOUND message. MUST match the bundler's
555
- // buildArNSCustodyMessage byte-for-byte (newline-delimited) or every signature
556
- // is rejected. The bundler reconstructs this from the request and verifies the
557
- // signature over `message + nonce`, so a captured signature can't be replayed
558
- // against a different operation/params.
559
- buildArNSCustodyMessage(action, fields) {
560
- return ['arns', action, ...fields].join('\n');
607
+ /**
608
+ * Run an action to a terminal state, signing if the server asks for it.
609
+ *
610
+ * This is the two-shape branch, once, in one place — so callers cannot
611
+ * hardcode which actions need a signature and break when a customer
612
+ * exercises ownership.
613
+ */
614
+ async completeArNSAction(action, params, owner, opts = {}, ownerProofMessage) {
615
+ const created = await this.createArNSAction(action, params, owner !== undefined && ownerProofMessage !== undefined
616
+ ? { owner, message: ownerProofMessage }
617
+ : undefined);
618
+ // Fires before any wallet prompt: the action is already debited, so the
619
+ // caller needs the nonce persisted even if the user walks away here.
620
+ await opts.onNonce?.(created.nonce);
621
+ if (created.status === 'completed')
622
+ return created;
623
+ if (owner === undefined) {
624
+ throw new Error(`ArNS action "${action}" requires the ANT owner's signature, but no owner signer was provided. ` +
625
+ `Pass \`owner\`, or drive createArNSAction/signArNSAction yourself. ` +
626
+ `Nonce ${created.nonce} is already debited — poll it rather than re-creating.`);
627
+ }
628
+ const signed = await owner.signTransaction(created.transaction);
629
+ return this.signArNSAction(created.nonce, signed);
561
630
  }
562
631
  /**
563
- * Self-custody exit: move a Turbo-custodied ANT to a Solana pubkey you control.
564
- * Authenticated with an action-bound, single-use signature.
632
+ * Buy a name. The ANT is minted straight to `owner` Turbo never holds it.
633
+ *
634
+ * This is the ONLY action that always needs the owner's signature:
635
+ * `ario_ant::initialize` is the one instruction in the whole lifecycle that
636
+ * requires the ANT owner's key. The customer signs once, here, and never
637
+ * again unless they change controllers or transfer the name.
638
+ *
639
+ * The owner needs a Solana key to sign with, NOT a funded one — Turbo pays
640
+ * every lamport of fee and rent.
565
641
  */
566
- async transferArNSAnt({ antId, target, }) {
567
- const nonce = (0, uuid_js_1.uuidV4)();
568
- const headers = await this.signer.generateSignedRequestHeaders(nonce, this.buildArNSCustodyMessage('transfer', [antId, target]));
569
- return this.httpService.post({
570
- endpoint: `/arns/transfer/${antId}?target=${encodeURIComponent(target)}`,
571
- headers,
572
- data: Buffer.from([]),
573
- retry: false, // single-use action-bound nonce; don't re-POST on 5xx
574
- });
642
+ async buyArNSName({ name, owner, type = 'lease', years, paidBy, onNonce, }) {
643
+ return this.completeArNSAction('buy-name', {
644
+ name,
645
+ ownerAddress: await owner.getAddress(),
646
+ type,
647
+ ...(years !== undefined ? { years } : {}),
648
+ ...(paidBy !== undefined ? { paidBy } : {}),
649
+ }, owner, { onNonce });
650
+ }
651
+ /** Extend a lease. Permissionless on chain — no owner signature needed. */
652
+ async extendArNSLease({ name, years, paidBy, onNonce, }) {
653
+ return this.completeArNSAction('extend-lease', { name, years, ...(paidBy !== undefined ? { paidBy } : {}) }, undefined, { onNonce });
654
+ }
655
+ /** Upgrade a lease to a permanent name. No owner signature needed. */
656
+ async upgradeArNSName({ name, paidBy, onNonce, }) {
657
+ return this.completeArNSAction('upgrade-name', { name, ...(paidBy !== undefined ? { paidBy } : {}) }, undefined, { onNonce });
658
+ }
659
+ /** Raise the undername limit. No owner signature needed. */
660
+ async increaseArNSUndernameLimit({ name, increaseQty, paidBy, onNonce, }) {
661
+ return this.completeArNSAction('increase-undername-limit', { name, increaseQty, ...(paidBy !== undefined ? { paidBy } : {}) }, undefined, { onNonce });
575
662
  }
576
- /** Set a resolution record on a custodied ANT (undername defaults to '@'). */
577
- async setArNSRecord({ antId, undername = '@', transactionId, ttlSeconds, }) {
578
- const nonce = (0, uuid_js_1.uuidV4)();
579
- const headers = await this.signer.generateSignedRequestHeaders(nonce, this.buildArNSCustodyMessage('set-record', [
663
+ /**
664
+ * Point a name (or undername) at an Arweave transaction.
665
+ *
666
+ * Free Turbo sponsors the Solana fee. Completes in one call while Turbo is
667
+ * a controller of the ANT, and returns `awaiting-signature` once the customer
668
+ * has revoked Turbo, at which point `owner` signs it themselves. Both paths
669
+ * are handled here.
670
+ *
671
+ * The owner proof is required EITHER WAY: Turbo is directing its own
672
+ * controller authority over an asset someone else owns, so nothing on chain
673
+ * records the owner's consent and we demand it. It is a MESSAGE signature,
674
+ * not a transaction — cheap and offline, but still a wallet prompt.
675
+ */
676
+ async setArNSRecord({ antId, owner, transactionId, undername = '@', ttlSeconds = 3600, onNonce, }) {
677
+ return this.completeArNSAction('set-record', {
678
+ antId,
679
+ ownerAddress: await owner.getAddress(),
680
+ transactionId,
681
+ undername,
682
+ ttlSeconds,
683
+ }, owner, { onNonce }, (0, arnsActions_js_1.buildArNSCustodyMessage)('set-record', [
580
684
  antId,
581
685
  undername,
582
686
  transactionId,
583
687
  String(ttlSeconds),
584
688
  ]));
585
- const query = `?undername=${encodeURIComponent(undername)}&transactionId=${transactionId}&ttlSeconds=${ttlSeconds}`;
586
- return this.httpService.post({
587
- endpoint: `/arns/manage/${antId}/set-record${query}`,
588
- headers,
589
- data: Buffer.from([]),
590
- retry: false, // single-use action-bound nonce; don't re-POST on 5xx
591
- });
592
689
  }
593
- /** Remove a resolution record (an undername) from a custodied ANT. */
594
- async removeArNSRecord({ antId, undername, }) {
595
- const nonce = (0, uuid_js_1.uuidV4)();
596
- const headers = await this.signer.generateSignedRequestHeaders(nonce, this.buildArNSCustodyMessage('remove-record', [antId, undername]));
597
- return this.httpService.post({
598
- endpoint: `/arns/manage/${antId}/remove-record?undername=${encodeURIComponent(undername)}`,
599
- headers,
600
- data: Buffer.from([]),
601
- retry: false, // single-use action-bound nonce; don't re-POST on 5xx
602
- });
690
+ /** Remove a record (an undername). Free; same two-shape rules as setArNSRecord. */
691
+ async removeArNSRecord({ antId, owner, undername, onNonce, }) {
692
+ return this.completeArNSAction('remove-record', { antId, ownerAddress: await owner.getAddress(), undername }, owner, { onNonce }, (0, arnsActions_js_1.buildArNSCustodyMessage)('remove-record', [antId, undername]));
693
+ }
694
+ /**
695
+ * Grant controller rights on the ANT. Omit `target` for Turbo itself, which
696
+ * is what makes `setArNSRecord` a single call.
697
+ *
698
+ * Owner-signed: changing an ANT's access control is an owner-only
699
+ * instruction. Free to the customer — Turbo funds the ACL page growth.
700
+ */
701
+ async addArNSController({ antId, owner, target, onNonce, }) {
702
+ return this.completeArNSAction('add-controller', {
703
+ antId,
704
+ ownerAddress: await owner.getAddress(),
705
+ ...(target !== undefined ? { target } : {}),
706
+ }, owner, { onNonce });
707
+ }
708
+ /**
709
+ * Revoke controller rights — the escape hatch that keeps "Turbo is not a
710
+ * custodian" honest.
711
+ *
712
+ * Always available, always free, and needs nothing from Turbo but the fee.
713
+ * After revoking, `setArNSRecord` keeps working: it simply starts returning
714
+ * `awaiting-signature` so the owner signs their own record writes.
715
+ */
716
+ async removeArNSController({ antId, owner, target, onNonce, }) {
717
+ return this.completeArNSAction('remove-controller', {
718
+ antId,
719
+ ownerAddress: await owner.getAddress(),
720
+ ...(target !== undefined ? { target } : {}),
721
+ }, owner, { onNonce });
722
+ }
723
+ /**
724
+ * Hand the ANT to a new owner. Irreversible: after this lands, `owner` no
725
+ * longer controls the name. Owner-signed, and sponsored like the rest.
726
+ */
727
+ async transferArNSAnt({ antId, owner, target, onNonce, }) {
728
+ return this.completeArNSAction('transfer', { antId, ownerAddress: await owner.getAddress(), target }, owner, { onNonce });
729
+ }
730
+ /**
731
+ * Defaults to the signer's own address when `userAddress` is omitted
732
+ * (`null`/`undefined`). Passing `''` does NOT trigger this default --
733
+ * mirrors `getBalance`'s existing behavior above.
734
+ */
735
+ async getArNSNames(userAddress) {
736
+ userAddress ??= await this.signer.getNativeAddress();
737
+ return super.getArNSNames(userAddress);
603
738
  }
604
739
  async getCreditShareApprovals({ userAddress, }) {
605
740
  userAddress ??= await this.signer.getNativeAddress();