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

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.
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.buildArNSCustodyMessage = buildArNSCustodyMessage;
7
+ exports.arNSMetadataField = arNSMetadataField;
8
+ exports.arNSKeywordsField = arNSKeywordsField;
7
9
  exports.arNSOwnerProofHeaders = arNSOwnerProofHeaders;
8
10
  exports.solanaOwnerSigner = solanaOwnerSigner;
9
11
  exports.emptySignatureSlots = emptySignatureSlots;
@@ -39,6 +41,27 @@ const base64_js_1 = require("../utils/base64.js");
39
41
  function buildArNSCustodyMessage(action, fields) {
40
42
  return ['arns', action, ...fields].join('\n');
41
43
  }
44
+ /**
45
+ * A metadata field's ABSENT/EMPTY distinction, as the bundler binds it.
46
+ *
47
+ * `null` (clear the field) must not sign the same message as `''` (set it to
48
+ * empty), or a signature authorizing one would authorize the other. NUL can
49
+ * never appear in a value that reached here — the route rejects control
50
+ * characters — so it is a safe sentinel.
51
+ */
52
+ function arNSMetadataField(value) {
53
+ return value === null || value === undefined ? '\u0000' : value;
54
+ }
55
+ /**
56
+ * Keywords joined on a separator the route rejects INSIDE a keyword, so
57
+ * `['a,b']` and `['a','b']` cannot be re-partitioned into one another with the
58
+ * same signature.
59
+ */
60
+ function arNSKeywordsField(keywords) {
61
+ return keywords === null || keywords === undefined
62
+ ? '\u0000'
63
+ : keywords.join('\u0001');
64
+ }
42
65
  /** Solana's signature-type discriminator in Turbo's signed-request scheme. */
43
66
  const SOLANA_SIGNATURE_TYPE = 4;
44
67
  /**
@@ -41,7 +41,7 @@ const chunkingHeader = { 'x-chunking-version': '2' };
41
41
  * uploading them in parallel, and emitting progress/error events.
42
42
  */
43
43
  class ChunkedUploader {
44
- constructor({ http, token, maxChunkConcurrency = exports.defaultMaxChunkConcurrency, maxFinalizeMs, chunkByteCount = exports.defaultChunkByteCount, logger = logger_js_1.Logger.default, chunkingMode = 'auto', dataItemByteCount, }) {
44
+ constructor({ http, token, maxChunkConcurrency = exports.defaultMaxChunkConcurrency, maxFinalizeMs, chunkByteCount = exports.defaultChunkByteCount, logger = logger_js_1.Logger.default, chunkingMode = 'auto', dataItemByteCount, x402, x402RefundIdentity, }) {
45
45
  this.assertChunkParams({
46
46
  chunkByteCount,
47
47
  chunkingMode,
@@ -60,6 +60,9 @@ class ChunkedUploader {
60
60
  dataItemByteCount,
61
61
  });
62
62
  this.maxBacklogQueue = this.maxChunkConcurrency * backlogQueueFactor;
63
+ this.x402 = x402;
64
+ this.x402RefundIdentity = x402RefundIdentity;
65
+ this.dataItemByteCount = dataItemByteCount;
63
66
  }
64
67
  shouldChunkUpload({ chunkByteCount, chunkingMode, dataItemByteCount, }) {
65
68
  if (chunkingMode === 'disabled') {
@@ -98,7 +101,54 @@ class ChunkedUploader {
98
101
  /**
99
102
  * Initialize or resume an upload session, returning the upload ID.
100
103
  */
104
+ /**
105
+ * Open a multipart upload paid for with x402.
106
+ *
107
+ * The bundler settles the payment at CREATE, before accepting a single chunk
108
+ * — so an unpaid upload never consumes storage. That is why the size is
109
+ * declared here and the money moves here, not at finalize.
110
+ *
111
+ * The 402 handshake is delegated to `wrapFetchWithPayment`, the same
112
+ * mechanism the single-shot x402 POST already uses, so the signer and the
113
+ * spend cap behave identically on both paths.
114
+ */
115
+ async initPaidUpload(x402) {
116
+ if (this.paidUploadId !== undefined) {
117
+ this.logger.debug('Resuming the already-paid chunked upload', {
118
+ uploadId: this.paidUploadId,
119
+ });
120
+ return this.paidUploadId;
121
+ }
122
+ if (this.x402RefundIdentity === undefined) {
123
+ throw new Error('An x402-paid chunked upload needs a refund identity — it is the Turbo wallet credited if the upload delivers fewer bytes than were paid for.');
124
+ }
125
+ const { address, signatureType } = this.x402RefundIdentity;
126
+ const endpoint = `/chunks/${this.token}/-1/-1?chunkSize=${this.chunkByteCount}` +
127
+ `&totalBytes=${this.dataItemByteCount}` +
128
+ `&address=${encodeURIComponent(address)}` +
129
+ `&signatureType=${signatureType}`;
130
+ this.logger.debug('Opening an x402-paid chunked upload', {
131
+ totalBytes: this.dataItemByteCount,
132
+ });
133
+ const res = await this.http.get({
134
+ endpoint: endpoint,
135
+ headers: chunkingHeader,
136
+ x402Options: x402,
137
+ });
138
+ if (res.chunkSize !== undefined && res.chunkSize !== this.chunkByteCount) {
139
+ this.logger.warn('Chunk size mismatch! Overriding with server value.', {
140
+ clientExpected: this.chunkByteCount,
141
+ serverReturned: res.chunkSize,
142
+ });
143
+ this.chunkByteCount = res.chunkSize;
144
+ }
145
+ this.paidUploadId = res.id;
146
+ return res.id;
147
+ }
101
148
  async initUpload() {
149
+ if (this.x402 !== undefined) {
150
+ return this.initPaidUpload(this.x402);
151
+ }
102
152
  const res = await this.http.get({
103
153
  endpoint: `/chunks/${this.token}/-1/-1?chunkSize=${this.chunkByteCount}`,
104
154
  headers: chunkingHeader,
@@ -52,7 +52,18 @@ class TurboHTTPService {
52
52
  this.baseURL = url;
53
53
  this.retryConfig = retryConfig;
54
54
  }
55
- async get({ endpoint, signal, allowedStatuses = [200, 202], headers, }) {
55
+ async get({ endpoint, signal, allowedStatuses = [200, 202], headers, x402Options, }) {
56
+ if (x402Options !== undefined) {
57
+ const maxMUSDCAmount = x402Options.maxMUSDCAmount !== undefined
58
+ ? BigInt(x402Options.maxMUSDCAmount.toString())
59
+ : undefined;
60
+ const fetchWithPay = (0, x402_fetch_1.wrapFetchWithPayment)(fetch, x402Options.signer, maxMUSDCAmount);
61
+ return this.tryRequest(async () => fetchWithPay(this.baseURL + endpoint, {
62
+ method: 'GET',
63
+ headers: { ...defaultHeaders, ...headers },
64
+ signal,
65
+ }), allowedStatuses);
66
+ }
56
67
  return this.withRetry(() => fetch(this.baseURL + endpoint, {
57
68
  method: 'GET',
58
69
  headers: { ...defaultHeaders, ...headers },
@@ -663,7 +663,7 @@ class TurboAuthenticatedPaymentService extends TurboUnauthenticatedPaymentServic
663
663
  /**
664
664
  * Point a name (or undername) at an Arweave transaction.
665
665
  *
666
- * FreeTurbo sponsors the Solana fee. Completes in one call while Turbo is
666
+ * Costs a small credit margin never SOL, which Turbo sponsors. Completes in one call while Turbo is
667
667
  * a controller of the ANT, and returns `awaiting-signature` once the customer
668
668
  * has revoked Turbo, at which point `owner` signs it themselves. Both paths
669
669
  * are handled here.
@@ -687,16 +687,66 @@ class TurboAuthenticatedPaymentService extends TurboUnauthenticatedPaymentServic
687
687
  String(ttlSeconds),
688
688
  ]));
689
689
  }
690
- /** Remove a record (an undername). Free; same two-shape rules as setArNSRecord. */
690
+ /** Remove a record (an undername). Costs credits, never SOL. */
691
691
  async removeArNSRecord({ antId, owner, undername, onNonce, }) {
692
692
  return this.completeArNSAction('remove-record', { antId, ownerAddress: await owner.getAddress(), undername }, owner, { onNonce }, (0, arnsActions_js_1.buildArNSCustodyMessage)('remove-record', [antId, undername]));
693
693
  }
694
+ /**
695
+ * Edit a RECORD's metadata — its display name, logo, description, keywords.
696
+ *
697
+ * Costs a small credit margin (never SOL), and is owner-or-controller on
698
+ * chain, so it behaves exactly like
699
+ * {@link setArNSRecord}: Turbo-alone while it is a controller, owner-signed
700
+ * after a revoke.
701
+ *
702
+ * Fields are TRI-STATE. Omit one to leave it unchanged; pass `null` to clear
703
+ * it. Those are bound distinctly by the owner proof, so "clear the
704
+ * description" and "set it to empty" are different authorizations.
705
+ *
706
+ * Note this is RECORD metadata. ANT-level metadata (the ANT's own name,
707
+ * ticker, description, keywords, logo) is NOT sponsored and stays on the
708
+ * direct-signer path via `@ar.io/sdk`.
709
+ */
710
+ async setArNSRecordMetadata({ antId, owner, undername = '@', displayName, recordLogo, recordDescription, recordKeywords, onNonce, }) {
711
+ return this.completeArNSAction('set-record-metadata', {
712
+ antId,
713
+ ownerAddress: await owner.getAddress(),
714
+ undername,
715
+ // Sent explicitly, including `null`, so the server sees the same
716
+ // tri-state the proof was signed over.
717
+ ...(displayName !== undefined ? { displayName } : {}),
718
+ ...(recordLogo !== undefined ? { recordLogo } : {}),
719
+ ...(recordDescription !== undefined ? { recordDescription } : {}),
720
+ ...(recordKeywords !== undefined ? { recordKeywords } : {}),
721
+ }, owner, { onNonce }, (0, arnsActions_js_1.buildArNSCustodyMessage)('set-record-metadata', [
722
+ antId,
723
+ undername,
724
+ (0, arnsActions_js_1.arNSMetadataField)(displayName),
725
+ (0, arnsActions_js_1.arNSMetadataField)(recordLogo),
726
+ (0, arnsActions_js_1.arNSMetadataField)(recordDescription),
727
+ (0, arnsActions_js_1.arNSKeywordsField)(recordKeywords),
728
+ ]));
729
+ }
730
+ /** Clear a record's metadata. Costs credits, never SOL; same two-shape rules. */
731
+ async removeArNSRecordMetadata({ antId, owner, undername, onNonce, }) {
732
+ return this.completeArNSAction('remove-record-metadata', { antId, ownerAddress: await owner.getAddress(), undername }, owner, { onNonce }, (0, arnsActions_js_1.buildArNSCustodyMessage)('remove-record-metadata', [antId, undername]));
733
+ }
734
+ /**
735
+ * Hand ONE record to another address.
736
+ *
737
+ * Distinct from {@link transferArNSAnt}, which hands over the whole ANT and
738
+ * every record on it. Confusing the two gives away far more than intended.
739
+ */
740
+ async transferArNSRecord({ antId, owner, undername, target, onNonce, }) {
741
+ return this.completeArNSAction('transfer-record', { antId, ownerAddress: await owner.getAddress(), undername, target }, owner, { onNonce }, (0, arnsActions_js_1.buildArNSCustodyMessage)('transfer-record', [antId, undername, target]));
742
+ }
694
743
  /**
695
744
  * Grant controller rights on the ANT. Omit `target` for Turbo itself, which
696
745
  * is what makes `setArNSRecord` a single call.
697
746
  *
698
747
  * Owner-signed: changing an ANT's access control is an owner-only
699
- * instruction. Free to the customer Turbo funds the ACL page growth.
748
+ * instruction. Costs a small credit margin; Turbo funds the ACL page growth
749
+ * in SOL.
700
750
  */
701
751
  async addArNSController({ antId, owner, target, onNonce, }) {
702
752
  return this.completeArNSAction('add-controller', {
@@ -709,7 +759,8 @@ class TurboAuthenticatedPaymentService extends TurboUnauthenticatedPaymentServic
709
759
  * Revoke controller rights — the escape hatch that keeps "Turbo is not a
710
760
  * custodian" honest.
711
761
  *
712
- * Always available, always free, and needs nothing from Turbo but the fee.
762
+ * Always available, and needs nothing from Turbo but the fee. Costs a small
763
+ * credit margin rather than SOL.
713
764
  * After revoking, `setArNSRecord` keeps working: it simply starts returning
714
765
  * `awaiting-signature` so the owner signs their own record writes.
715
766
  */
@@ -270,15 +270,31 @@ class TurboAuthenticatedClient extends TurboUnauthenticatedClient {
270
270
  removeArNSRecord(params) {
271
271
  return this.paymentService.removeArNSRecord(params);
272
272
  }
273
- /** Grant controller rights — omit `target` for Turbo itself. Free. */
273
+ /** Grant controller rights — omit `target` for Turbo itself. Costs credits. */
274
274
  addArNSController(params) {
275
275
  return this.paymentService.addArNSController(params);
276
276
  }
277
- /** Revoke controller rights. Always available, always free. */
277
+ /** Revoke controller rights. Always available; costs credits, never SOL. */
278
278
  removeArNSController(params) {
279
279
  return this.paymentService.removeArNSController(params);
280
280
  }
281
- /** Hand the ANT to a new owner. Irreversible. Free. */
281
+ /**
282
+ * Edit a RECORD's metadata (display name, logo, description, keywords).
283
+ * Free; handles both shapes. Fields are tri-state — omit to leave unchanged,
284
+ * pass `null` to clear.
285
+ */
286
+ setArNSRecordMetadata(params) {
287
+ return this.paymentService.setArNSRecordMetadata(params);
288
+ }
289
+ /** Clear a record's metadata. Costs credits, never SOL; handles both shapes. */
290
+ removeArNSRecordMetadata(params) {
291
+ return this.paymentService.removeArNSRecordMetadata(params);
292
+ }
293
+ /** Hand ONE record over — not the whole ANT. Costs credits, never SOL. */
294
+ transferArNSRecord(params) {
295
+ return this.paymentService.transferArNSRecord(params);
296
+ }
297
+ /** Hand the ANT to a new owner. Irreversible. Costs credits, never SOL. */
282
298
  transferArNSAnt(params) {
283
299
  return this.paymentService.transferArNSAnt(params);
284
300
  }
@@ -225,9 +225,6 @@ class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadServ
225
225
  !this.x402EnabledTokens.includes(this.token)) {
226
226
  throw new Error('x402 uploads are not supported for token: ' + this.token);
227
227
  }
228
- if (params.chunkingMode === 'force' && fundingMode instanceof types_js_1.X402Funding) {
229
- throw new Error("Chunking mode 'force' is not supported when x402 is enabled");
230
- }
231
228
  this.logger.debug('Starting file upload', { params });
232
229
  let retries = 0;
233
230
  const maxRetries = this.retryConfig.retries ?? 3;
@@ -263,6 +260,19 @@ class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadServ
263
260
  // this result due to the wrapped retry logic of this method.
264
261
  try {
265
262
  const { chunkByteCount, maxChunkConcurrency } = params;
263
+ // Built here rather than after the chunked branch so BOTH paths can
264
+ // pay with it. Chunked x402 uploads used to be impossible — the
265
+ // bundler had no way to charge for a multipart upload — so this branch
266
+ // deliberately fell back to a single request, which capped x402 at the
267
+ // single-item limit no matter how the client chunked. The bundler now
268
+ // settles at create, so the fallback is no longer needed.
269
+ const x402Options = fundingMode instanceof types_js_1.X402Funding
270
+ ? {
271
+ signer: fundingMode.signer ??
272
+ (await (0, signer_js_1.makeX402Signer)(this.signer.signer)),
273
+ maxMUSDCAmount: fundingMode.maxMUSDCAmount,
274
+ }
275
+ : undefined;
266
276
  const chunkedUploader = new chunked_js_1.ChunkedUploader({
267
277
  http: this.httpService,
268
278
  token: this.token,
@@ -272,9 +282,17 @@ class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadServ
272
282
  dataItemByteCount: dataItemSizeFactory(),
273
283
  chunkingMode: params.chunkingMode,
274
284
  maxFinalizeMs: params.maxFinalizeMs,
285
+ ...(x402Options ? { x402: x402Options } : {}),
286
+ ...(x402Options
287
+ ? {
288
+ x402RefundIdentity: {
289
+ address: await this.signer.getNativeAddress(),
290
+ signatureType: this.signer.signer.signatureType,
291
+ },
292
+ }
293
+ : {}),
275
294
  });
276
- if (chunkedUploader.shouldUseChunkUploader &&
277
- !(fundingMode instanceof types_js_1.X402Funding)) {
295
+ if (chunkedUploader.shouldUseChunkUploader) {
278
296
  const response = await chunkedUploader.upload({
279
297
  dataItemStreamFactory,
280
298
  dataItemSizeFactory,
@@ -284,13 +302,6 @@ class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadServ
284
302
  });
285
303
  return { ...response, cryptoFundResult };
286
304
  }
287
- const x402Options = fundingMode instanceof types_js_1.X402Funding
288
- ? {
289
- signer: fundingMode.signer ??
290
- (await (0, signer_js_1.makeX402Signer)(this.signer.signer)),
291
- maxMUSDCAmount: fundingMode.maxMUSDCAmount,
292
- }
293
- : undefined;
294
305
  const response = await this.uploadSignedDataItem({
295
306
  dataItemStreamFactory,
296
307
  dataItemSizeFactory,
package/lib/cjs/types.js CHANGED
@@ -117,9 +117,13 @@ exports.arNSPurchaseIntents = [
117
117
  /**
118
118
  * The nine sponsored ArNS actions.
119
119
  *
120
- * NOT included, because the bundler does not sponsor them: `primary-name`,
121
- * `release-name`, `reassign`, and ANT metadata (name/description/keywords/logo).
122
- * Those stay on the direct-signer path and cost the user SOL.
120
+ * Sponsorship covers these twelve and NOTHING else. Everything else in the
121
+ * ArNS, ANT and core programs stays on the direct-signer path and costs the
122
+ * user SOL notably `BuyReturnedName` (auctions, deliberately excluded: the
123
+ * premium is unbounded), `ClaimReservedName`, the primary-name flow (which
124
+ * lives in the ario core program), release/reassign, and ANT-LEVEL metadata.
125
+ * Note ANT-level metadata is distinct from RECORD-level metadata, which
126
+ * `set-record-metadata` does sponsor.
123
127
  */
124
128
  exports.arNSActions = [
125
129
  'buy-name',
@@ -131,6 +135,11 @@ exports.arNSActions = [
131
135
  'add-controller',
132
136
  'remove-controller',
133
137
  'transfer',
138
+ // Record-scoped. Owner-or-controller on chain, so these follow set-record's
139
+ // degrade-on-revoke shape rather than needing a signature every time.
140
+ 'set-record-metadata',
141
+ 'remove-record-metadata',
142
+ 'transfer-record',
134
143
  ];
135
144
  // ===== ArNS purchases paid with fiat (Stripe) — no Turbo Credits in between ====
136
145
  /**
@@ -30,6 +30,27 @@ import { toB64Url } from '../utils/base64.js';
30
30
  export function buildArNSCustodyMessage(action, fields) {
31
31
  return ['arns', action, ...fields].join('\n');
32
32
  }
33
+ /**
34
+ * A metadata field's ABSENT/EMPTY distinction, as the bundler binds it.
35
+ *
36
+ * `null` (clear the field) must not sign the same message as `''` (set it to
37
+ * empty), or a signature authorizing one would authorize the other. NUL can
38
+ * never appear in a value that reached here — the route rejects control
39
+ * characters — so it is a safe sentinel.
40
+ */
41
+ export function arNSMetadataField(value) {
42
+ return value === null || value === undefined ? '\u0000' : value;
43
+ }
44
+ /**
45
+ * Keywords joined on a separator the route rejects INSIDE a keyword, so
46
+ * `['a,b']` and `['a','b']` cannot be re-partitioned into one another with the
47
+ * same signature.
48
+ */
49
+ export function arNSKeywordsField(keywords) {
50
+ return keywords === null || keywords === undefined
51
+ ? '\u0000'
52
+ : keywords.join('\u0001');
53
+ }
33
54
  /** Solana's signature-type discriminator in Turbo's signed-request scheme. */
34
55
  const SOLANA_SIGNATURE_TYPE = 4;
35
56
  /**
@@ -35,7 +35,7 @@ const chunkingHeader = { 'x-chunking-version': '2' };
35
35
  * uploading them in parallel, and emitting progress/error events.
36
36
  */
37
37
  export class ChunkedUploader {
38
- constructor({ http, token, maxChunkConcurrency = defaultMaxChunkConcurrency, maxFinalizeMs, chunkByteCount = defaultChunkByteCount, logger = Logger.default, chunkingMode = 'auto', dataItemByteCount, }) {
38
+ constructor({ http, token, maxChunkConcurrency = defaultMaxChunkConcurrency, maxFinalizeMs, chunkByteCount = defaultChunkByteCount, logger = Logger.default, chunkingMode = 'auto', dataItemByteCount, x402, x402RefundIdentity, }) {
39
39
  this.assertChunkParams({
40
40
  chunkByteCount,
41
41
  chunkingMode,
@@ -54,6 +54,9 @@ export class ChunkedUploader {
54
54
  dataItemByteCount,
55
55
  });
56
56
  this.maxBacklogQueue = this.maxChunkConcurrency * backlogQueueFactor;
57
+ this.x402 = x402;
58
+ this.x402RefundIdentity = x402RefundIdentity;
59
+ this.dataItemByteCount = dataItemByteCount;
57
60
  }
58
61
  shouldChunkUpload({ chunkByteCount, chunkingMode, dataItemByteCount, }) {
59
62
  if (chunkingMode === 'disabled') {
@@ -92,7 +95,54 @@ export class ChunkedUploader {
92
95
  /**
93
96
  * Initialize or resume an upload session, returning the upload ID.
94
97
  */
98
+ /**
99
+ * Open a multipart upload paid for with x402.
100
+ *
101
+ * The bundler settles the payment at CREATE, before accepting a single chunk
102
+ * — so an unpaid upload never consumes storage. That is why the size is
103
+ * declared here and the money moves here, not at finalize.
104
+ *
105
+ * The 402 handshake is delegated to `wrapFetchWithPayment`, the same
106
+ * mechanism the single-shot x402 POST already uses, so the signer and the
107
+ * spend cap behave identically on both paths.
108
+ */
109
+ async initPaidUpload(x402) {
110
+ if (this.paidUploadId !== undefined) {
111
+ this.logger.debug('Resuming the already-paid chunked upload', {
112
+ uploadId: this.paidUploadId,
113
+ });
114
+ return this.paidUploadId;
115
+ }
116
+ if (this.x402RefundIdentity === undefined) {
117
+ throw new Error('An x402-paid chunked upload needs a refund identity — it is the Turbo wallet credited if the upload delivers fewer bytes than were paid for.');
118
+ }
119
+ const { address, signatureType } = this.x402RefundIdentity;
120
+ const endpoint = `/chunks/${this.token}/-1/-1?chunkSize=${this.chunkByteCount}` +
121
+ `&totalBytes=${this.dataItemByteCount}` +
122
+ `&address=${encodeURIComponent(address)}` +
123
+ `&signatureType=${signatureType}`;
124
+ this.logger.debug('Opening an x402-paid chunked upload', {
125
+ totalBytes: this.dataItemByteCount,
126
+ });
127
+ const res = await this.http.get({
128
+ endpoint: endpoint,
129
+ headers: chunkingHeader,
130
+ x402Options: x402,
131
+ });
132
+ if (res.chunkSize !== undefined && res.chunkSize !== this.chunkByteCount) {
133
+ this.logger.warn('Chunk size mismatch! Overriding with server value.', {
134
+ clientExpected: this.chunkByteCount,
135
+ serverReturned: res.chunkSize,
136
+ });
137
+ this.chunkByteCount = res.chunkSize;
138
+ }
139
+ this.paidUploadId = res.id;
140
+ return res.id;
141
+ }
95
142
  async initUpload() {
143
+ if (this.x402 !== undefined) {
144
+ return this.initPaidUpload(this.x402);
145
+ }
96
146
  const res = await this.http.get({
97
147
  endpoint: `/chunks/${this.token}/-1/-1?chunkSize=${this.chunkByteCount}`,
98
148
  headers: chunkingHeader,
@@ -48,7 +48,18 @@ export class TurboHTTPService {
48
48
  this.baseURL = url;
49
49
  this.retryConfig = retryConfig;
50
50
  }
51
- async get({ endpoint, signal, allowedStatuses = [200, 202], headers, }) {
51
+ async get({ endpoint, signal, allowedStatuses = [200, 202], headers, x402Options, }) {
52
+ if (x402Options !== undefined) {
53
+ const maxMUSDCAmount = x402Options.maxMUSDCAmount !== undefined
54
+ ? BigInt(x402Options.maxMUSDCAmount.toString())
55
+ : undefined;
56
+ const fetchWithPay = wrapFetchWithPayment(fetch, x402Options.signer, maxMUSDCAmount);
57
+ return this.tryRequest(async () => fetchWithPay(this.baseURL + endpoint, {
58
+ method: 'GET',
59
+ headers: { ...defaultHeaders, ...headers },
60
+ signal,
61
+ }), allowedStatuses);
62
+ }
52
63
  return this.withRetry(() => fetch(this.baseURL + endpoint, {
53
64
  method: 'GET',
54
65
  headers: { ...defaultHeaders, ...headers },
@@ -18,7 +18,7 @@ import { arNSPurchaseIntents, fiatCurrencyTypes, isCurrency, } from '../types.js
18
18
  import { isAnyValidUserAddress } from '../utils/common.js';
19
19
  import { FailedRequestError, FiatPaymentsDisabledError, InsufficientCreditsError, ProvidedInputError, } from '../utils/errors.js';
20
20
  import { uuidV4 } from '../utils/uuid.js';
21
- import { arNSOwnerProofHeaders, buildArNSCustodyMessage, } from './arnsActions.js';
21
+ import { arNSKeywordsField, arNSMetadataField, arNSOwnerProofHeaders, buildArNSCustodyMessage, } from './arnsActions.js';
22
22
  import { defaultRetryConfig } from './http.js';
23
23
  import { TurboHTTPService } from './http.js';
24
24
  import { Logger } from './logger.js';
@@ -659,7 +659,7 @@ export class TurboAuthenticatedPaymentService extends TurboUnauthenticatedPaymen
659
659
  /**
660
660
  * Point a name (or undername) at an Arweave transaction.
661
661
  *
662
- * FreeTurbo sponsors the Solana fee. Completes in one call while Turbo is
662
+ * Costs a small credit margin never SOL, which Turbo sponsors. Completes in one call while Turbo is
663
663
  * a controller of the ANT, and returns `awaiting-signature` once the customer
664
664
  * has revoked Turbo, at which point `owner` signs it themselves. Both paths
665
665
  * are handled here.
@@ -683,16 +683,66 @@ export class TurboAuthenticatedPaymentService extends TurboUnauthenticatedPaymen
683
683
  String(ttlSeconds),
684
684
  ]));
685
685
  }
686
- /** Remove a record (an undername). Free; same two-shape rules as setArNSRecord. */
686
+ /** Remove a record (an undername). Costs credits, never SOL. */
687
687
  async removeArNSRecord({ antId, owner, undername, onNonce, }) {
688
688
  return this.completeArNSAction('remove-record', { antId, ownerAddress: await owner.getAddress(), undername }, owner, { onNonce }, buildArNSCustodyMessage('remove-record', [antId, undername]));
689
689
  }
690
+ /**
691
+ * Edit a RECORD's metadata — its display name, logo, description, keywords.
692
+ *
693
+ * Costs a small credit margin (never SOL), and is owner-or-controller on
694
+ * chain, so it behaves exactly like
695
+ * {@link setArNSRecord}: Turbo-alone while it is a controller, owner-signed
696
+ * after a revoke.
697
+ *
698
+ * Fields are TRI-STATE. Omit one to leave it unchanged; pass `null` to clear
699
+ * it. Those are bound distinctly by the owner proof, so "clear the
700
+ * description" and "set it to empty" are different authorizations.
701
+ *
702
+ * Note this is RECORD metadata. ANT-level metadata (the ANT's own name,
703
+ * ticker, description, keywords, logo) is NOT sponsored and stays on the
704
+ * direct-signer path via `@ar.io/sdk`.
705
+ */
706
+ async setArNSRecordMetadata({ antId, owner, undername = '@', displayName, recordLogo, recordDescription, recordKeywords, onNonce, }) {
707
+ return this.completeArNSAction('set-record-metadata', {
708
+ antId,
709
+ ownerAddress: await owner.getAddress(),
710
+ undername,
711
+ // Sent explicitly, including `null`, so the server sees the same
712
+ // tri-state the proof was signed over.
713
+ ...(displayName !== undefined ? { displayName } : {}),
714
+ ...(recordLogo !== undefined ? { recordLogo } : {}),
715
+ ...(recordDescription !== undefined ? { recordDescription } : {}),
716
+ ...(recordKeywords !== undefined ? { recordKeywords } : {}),
717
+ }, owner, { onNonce }, buildArNSCustodyMessage('set-record-metadata', [
718
+ antId,
719
+ undername,
720
+ arNSMetadataField(displayName),
721
+ arNSMetadataField(recordLogo),
722
+ arNSMetadataField(recordDescription),
723
+ arNSKeywordsField(recordKeywords),
724
+ ]));
725
+ }
726
+ /** Clear a record's metadata. Costs credits, never SOL; same two-shape rules. */
727
+ async removeArNSRecordMetadata({ antId, owner, undername, onNonce, }) {
728
+ return this.completeArNSAction('remove-record-metadata', { antId, ownerAddress: await owner.getAddress(), undername }, owner, { onNonce }, buildArNSCustodyMessage('remove-record-metadata', [antId, undername]));
729
+ }
730
+ /**
731
+ * Hand ONE record to another address.
732
+ *
733
+ * Distinct from {@link transferArNSAnt}, which hands over the whole ANT and
734
+ * every record on it. Confusing the two gives away far more than intended.
735
+ */
736
+ async transferArNSRecord({ antId, owner, undername, target, onNonce, }) {
737
+ return this.completeArNSAction('transfer-record', { antId, ownerAddress: await owner.getAddress(), undername, target }, owner, { onNonce }, buildArNSCustodyMessage('transfer-record', [antId, undername, target]));
738
+ }
690
739
  /**
691
740
  * Grant controller rights on the ANT. Omit `target` for Turbo itself, which
692
741
  * is what makes `setArNSRecord` a single call.
693
742
  *
694
743
  * Owner-signed: changing an ANT's access control is an owner-only
695
- * instruction. Free to the customer Turbo funds the ACL page growth.
744
+ * instruction. Costs a small credit margin; Turbo funds the ACL page growth
745
+ * in SOL.
696
746
  */
697
747
  async addArNSController({ antId, owner, target, onNonce, }) {
698
748
  return this.completeArNSAction('add-controller', {
@@ -705,7 +755,8 @@ export class TurboAuthenticatedPaymentService extends TurboUnauthenticatedPaymen
705
755
  * Revoke controller rights — the escape hatch that keeps "Turbo is not a
706
756
  * custodian" honest.
707
757
  *
708
- * Always available, always free, and needs nothing from Turbo but the fee.
758
+ * Always available, and needs nothing from Turbo but the fee. Costs a small
759
+ * credit margin rather than SOL.
709
760
  * After revoking, `setArNSRecord` keeps working: it simply starts returning
710
761
  * `awaiting-signature` so the owner signs their own record writes.
711
762
  */
@@ -266,15 +266,31 @@ export class TurboAuthenticatedClient extends TurboUnauthenticatedClient {
266
266
  removeArNSRecord(params) {
267
267
  return this.paymentService.removeArNSRecord(params);
268
268
  }
269
- /** Grant controller rights — omit `target` for Turbo itself. Free. */
269
+ /** Grant controller rights — omit `target` for Turbo itself. Costs credits. */
270
270
  addArNSController(params) {
271
271
  return this.paymentService.addArNSController(params);
272
272
  }
273
- /** Revoke controller rights. Always available, always free. */
273
+ /** Revoke controller rights. Always available; costs credits, never SOL. */
274
274
  removeArNSController(params) {
275
275
  return this.paymentService.removeArNSController(params);
276
276
  }
277
- /** Hand the ANT to a new owner. Irreversible. Free. */
277
+ /**
278
+ * Edit a RECORD's metadata (display name, logo, description, keywords).
279
+ * Free; handles both shapes. Fields are tri-state — omit to leave unchanged,
280
+ * pass `null` to clear.
281
+ */
282
+ setArNSRecordMetadata(params) {
283
+ return this.paymentService.setArNSRecordMetadata(params);
284
+ }
285
+ /** Clear a record's metadata. Costs credits, never SOL; handles both shapes. */
286
+ removeArNSRecordMetadata(params) {
287
+ return this.paymentService.removeArNSRecordMetadata(params);
288
+ }
289
+ /** Hand ONE record over — not the whole ANT. Costs credits, never SOL. */
290
+ transferArNSRecord(params) {
291
+ return this.paymentService.transferArNSRecord(params);
292
+ }
293
+ /** Hand the ANT to a new owner. Irreversible. Costs credits, never SOL. */
278
294
  transferArNSAnt(params) {
279
295
  return this.paymentService.transferArNSAnt(params);
280
296
  }