@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.
- package/lib/cjs/common/arnsActions.js +23 -0
- package/lib/cjs/common/chunked.js +51 -1
- package/lib/cjs/common/http.js +12 -1
- package/lib/cjs/common/payment.js +55 -4
- package/lib/cjs/common/turbo.js +19 -3
- package/lib/cjs/common/upload.js +23 -12
- package/lib/cjs/types.js +12 -3
- package/lib/esm/common/arnsActions.js +21 -0
- package/lib/esm/common/chunked.js +51 -1
- package/lib/esm/common/http.js +12 -1
- package/lib/esm/common/payment.js +56 -5
- package/lib/esm/common/turbo.js +19 -3
- package/lib/esm/common/upload.js +23 -12
- package/lib/esm/types.js +12 -3
- package/lib/types/common/arnsActions.d.ts +16 -1
- package/lib/types/common/arnsActions.d.ts.map +1 -1
- package/lib/types/common/chunked.d.ts +47 -2
- package/lib/types/common/chunked.d.ts.map +1 -1
- package/lib/types/common/http.d.ts +11 -1
- package/lib/types/common/http.d.ts.map +1 -1
- package/lib/types/common/payment.d.ts +52 -4
- package/lib/types/common/payment.d.ts.map +1 -1
- package/lib/types/common/turbo.d.ts +33 -3
- package/lib/types/common/turbo.d.ts.map +1 -1
- package/lib/types/common/upload.d.ts.map +1 -1
- package/lib/types/types.d.ts +31 -4
- package/lib/types/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -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,
|
package/lib/cjs/common/http.js
CHANGED
|
@@ -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
|
-
*
|
|
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).
|
|
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.
|
|
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,
|
|
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
|
*/
|
package/lib/cjs/common/turbo.js
CHANGED
|
@@ -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.
|
|
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,
|
|
277
|
+
/** Revoke controller rights. Always available; costs credits, never SOL. */
|
|
278
278
|
removeArNSController(params) {
|
|
279
279
|
return this.paymentService.removeArNSController(params);
|
|
280
280
|
}
|
|
281
|
-
/**
|
|
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
|
}
|
package/lib/cjs/common/upload.js
CHANGED
|
@@ -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
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
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,
|
package/lib/esm/common/http.js
CHANGED
|
@@ -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
|
-
*
|
|
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).
|
|
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.
|
|
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,
|
|
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
|
*/
|
package/lib/esm/common/turbo.js
CHANGED
|
@@ -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.
|
|
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,
|
|
273
|
+
/** Revoke controller rights. Always available; costs credits, never SOL. */
|
|
274
274
|
removeArNSController(params) {
|
|
275
275
|
return this.paymentService.removeArNSController(params);
|
|
276
276
|
}
|
|
277
|
-
/**
|
|
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
|
}
|