@ardrive/turbo-sdk 1.36.0 → 1.37.0
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/README.md +15 -0
- package/bundles/web.bundle.min.js +153347 -109444
- package/lib/cjs/cli/options.js +6 -0
- package/lib/cjs/cli/utils.js +9 -1
- package/lib/cjs/common/http.js +118 -1
- package/lib/cjs/common/signer.js +35 -0
- package/lib/cjs/common/upload.js +24 -2
- package/lib/cjs/types.js +9 -1
- package/lib/cjs/version.js +1 -1
- package/lib/esm/cli/options.js +6 -0
- package/lib/esm/cli/utils.js +10 -2
- package/lib/esm/common/http.js +117 -1
- package/lib/esm/common/signer.js +34 -0
- package/lib/esm/common/upload.js +25 -3
- package/lib/esm/types.js +7 -0
- package/lib/esm/version.js +1 -1
- package/lib/types/cli/options.d.ts +17 -0
- package/lib/types/cli/options.d.ts.map +1 -1
- package/lib/types/cli/types.d.ts +1 -0
- package/lib/types/cli/types.d.ts.map +1 -1
- package/lib/types/cli/utils.d.ts.map +1 -1
- package/lib/types/common/http.d.ts +10 -2
- package/lib/types/common/http.d.ts.map +1 -1
- package/lib/types/common/signer.d.ts +3 -0
- package/lib/types/common/signer.d.ts.map +1 -1
- package/lib/types/common/upload.d.ts +2 -1
- package/lib/types/common/upload.d.ts.map +1 -1
- package/lib/types/types.d.ts +21 -2
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/version.d.ts +1 -1
- package/package.json +3 -2
package/lib/cjs/cli/options.js
CHANGED
|
@@ -186,6 +186,11 @@ exports.optionMap = {
|
|
|
186
186
|
description: 'Enable on-demand crypto top-ups during upload if balance is insufficient',
|
|
187
187
|
default: false,
|
|
188
188
|
},
|
|
189
|
+
x402: {
|
|
190
|
+
alias: '--x402',
|
|
191
|
+
description: 'Pay for the action using x402 funding (if available). Requires token `base-usdc`.',
|
|
192
|
+
default: false,
|
|
193
|
+
},
|
|
189
194
|
topUpBufferMultiplier: {
|
|
190
195
|
alias: '--top-up-buffer-multiplier <topUpBufferMultiplier>',
|
|
191
196
|
description: 'Multiplier to apply to the estimated top-up amount to avoid underpayment during on-demand top-ups. Defaults to 1.1 (10% buffer).',
|
|
@@ -215,6 +220,7 @@ const onDemandOptions = [
|
|
|
215
220
|
exports.optionMap.onDemand,
|
|
216
221
|
exports.optionMap.topUpBufferMultiplier,
|
|
217
222
|
exports.optionMap.maxCryptoTopUpValue,
|
|
223
|
+
exports.optionMap.x402,
|
|
218
224
|
];
|
|
219
225
|
exports.uploadOptions = [
|
|
220
226
|
...exports.walletOptions,
|
package/lib/cjs/cli/utils.js
CHANGED
|
@@ -270,7 +270,10 @@ function getTagsFromOptions(options) {
|
|
|
270
270
|
return parseTags(options.tags);
|
|
271
271
|
}
|
|
272
272
|
function onDemandOptionsFromOptions(options) {
|
|
273
|
-
if (
|
|
273
|
+
if (options.x402 && options.onDemand) {
|
|
274
|
+
throw new Error('Cannot use both --x402 and --on-demand flags');
|
|
275
|
+
}
|
|
276
|
+
if (!options.onDemand && !options.x402) {
|
|
274
277
|
return { fundingMode: new index_js_1.ExistingBalanceFunding() };
|
|
275
278
|
}
|
|
276
279
|
const value = options.maxCryptoTopUpValue;
|
|
@@ -282,6 +285,11 @@ function onDemandOptionsFromOptions(options) {
|
|
|
282
285
|
const token = tokenFromOptions(options);
|
|
283
286
|
maxTokenAmount = index_js_1.tokenToBaseMap[token](value).toString();
|
|
284
287
|
}
|
|
288
|
+
if (options.x402) {
|
|
289
|
+
return {
|
|
290
|
+
fundingMode: new index_js_1.X402Funding({ maxMUSDCAmount: maxTokenAmount }),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
285
293
|
if (options.topUpBufferMultiplier !== undefined &&
|
|
286
294
|
(isNaN(options.topUpBufferMultiplier) || options.topUpBufferMultiplier < 1)) {
|
|
287
295
|
throw new Error('topUpBufferMultiplier must be a number >= 1');
|
package/lib/cjs/common/http.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TurboHTTPService = void 0;
|
|
4
|
+
exports.toX402FetchBody = toX402FetchBody;
|
|
4
5
|
/**
|
|
5
6
|
* Copyright (C) 2022-2024 Permanent Data Solutions, Inc.
|
|
6
7
|
*
|
|
@@ -18,6 +19,7 @@ exports.TurboHTTPService = void 0;
|
|
|
18
19
|
*/
|
|
19
20
|
const axios_1 = require("axios");
|
|
20
21
|
const node_stream_1 = require("node:stream");
|
|
22
|
+
const x402_fetch_1 = require("x402-fetch");
|
|
21
23
|
const axiosClient_js_1 = require("../utils/axiosClient.js");
|
|
22
24
|
const common_js_1 = require("../utils/common.js");
|
|
23
25
|
const errors_js_1 = require("../utils/errors.js");
|
|
@@ -36,7 +38,46 @@ class TurboHTTPService {
|
|
|
36
38
|
async get({ endpoint, signal, allowedStatuses = [200, 202], headers, }) {
|
|
37
39
|
return this.retryRequest(() => this.axios.get(endpoint, { headers, signal }), allowedStatuses);
|
|
38
40
|
}
|
|
39
|
-
async post({ endpoint, signal, allowedStatuses = [200, 202], headers, data, }) {
|
|
41
|
+
async post({ endpoint, signal, allowedStatuses = [200, 202], headers, data, x402Options, }) {
|
|
42
|
+
if (x402Options !== undefined) {
|
|
43
|
+
this.logger.debug('Using X402 options for POST request', {
|
|
44
|
+
endpoint,
|
|
45
|
+
x402Options,
|
|
46
|
+
});
|
|
47
|
+
const { body, duplex } = await toX402FetchBody(data);
|
|
48
|
+
try {
|
|
49
|
+
const maxMUSDCAmount = x402Options.maxMUSDCAmount !== undefined
|
|
50
|
+
? BigInt(x402Options.maxMUSDCAmount.toString())
|
|
51
|
+
: undefined;
|
|
52
|
+
const fetchWithPay = (0, x402_fetch_1.wrapFetchWithPayment)(fetch, x402Options.signer, maxMUSDCAmount);
|
|
53
|
+
const res = await fetchWithPay(this.axios.defaults.baseURL + '/x402/data-item/signed', {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers,
|
|
56
|
+
body,
|
|
57
|
+
signal,
|
|
58
|
+
...(duplex ? { duplex } : {}), // Use duplex only where streams are working
|
|
59
|
+
});
|
|
60
|
+
if (!allowedStatuses.includes(res.status)) {
|
|
61
|
+
const errorText = await res.text();
|
|
62
|
+
throw new errors_js_1.FailedRequestError(
|
|
63
|
+
// Return error message from server if available
|
|
64
|
+
errorText || res.statusText, res.status);
|
|
65
|
+
}
|
|
66
|
+
return res.json();
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error instanceof errors_js_1.FailedRequestError) {
|
|
70
|
+
throw error; // rethrow FailedRequestError
|
|
71
|
+
}
|
|
72
|
+
// Handle CanceledError specifically
|
|
73
|
+
if (error.message.includes('The operation was aborted')) {
|
|
74
|
+
throw new axios_1.CanceledError();
|
|
75
|
+
}
|
|
76
|
+
// Log the error and throw a FailedRequestError
|
|
77
|
+
this.logger.error('Error posting data', { endpoint, error });
|
|
78
|
+
throw new errors_js_1.FailedRequestError(error instanceof Error ? error.message : 'Unknown error', error.response?.status);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
40
81
|
// Buffer and Readable → keep Axios (streams work fine there)
|
|
41
82
|
if (!(data instanceof ReadableStream)) {
|
|
42
83
|
if (data instanceof node_stream_1.Readable) {
|
|
@@ -141,3 +182,79 @@ async function toFetchBody(data) {
|
|
|
141
182
|
const blob = await new Response(data).blob();
|
|
142
183
|
return { body: blob }; // browser sets length
|
|
143
184
|
}
|
|
185
|
+
const isNode = typeof process !== 'undefined' && !!process.versions?.node;
|
|
186
|
+
const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
|
187
|
+
function isFirefoxOrSafari() {
|
|
188
|
+
if (!isBrowser)
|
|
189
|
+
return false;
|
|
190
|
+
const ua = navigator.userAgent;
|
|
191
|
+
return ua.includes('Firefox') || ua.includes('Safari');
|
|
192
|
+
}
|
|
193
|
+
/** Create a re-usable body for x402 fetch protocol */
|
|
194
|
+
async function toX402FetchBody(data) {
|
|
195
|
+
//
|
|
196
|
+
// 🔹 NODE: always buffer to a non-stream body (Buffer)
|
|
197
|
+
// so fetchWithPayment can reuse it safely.
|
|
198
|
+
//
|
|
199
|
+
if (isNode) {
|
|
200
|
+
let buf;
|
|
201
|
+
// Web ReadableStream → Buffer
|
|
202
|
+
if (typeof ReadableStream !== 'undefined' &&
|
|
203
|
+
data instanceof ReadableStream) {
|
|
204
|
+
const ab = await new Response(data).arrayBuffer();
|
|
205
|
+
buf = Buffer.from(ab);
|
|
206
|
+
}
|
|
207
|
+
// Node Readable → Buffer
|
|
208
|
+
else if (data instanceof node_stream_1.Readable) {
|
|
209
|
+
const chunks = [];
|
|
210
|
+
for await (const chunk of data) {
|
|
211
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
212
|
+
}
|
|
213
|
+
buf = Buffer.concat(chunks.map((c) => Uint8Array.from(c)));
|
|
214
|
+
}
|
|
215
|
+
// Buffer / Uint8Array
|
|
216
|
+
else if (Buffer.isBuffer(data)) {
|
|
217
|
+
buf = data;
|
|
218
|
+
}
|
|
219
|
+
else if (data instanceof Uint8Array) {
|
|
220
|
+
buf = Buffer.from(data);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
throw new Error('Unsupported body type for toFetchBody (Node)');
|
|
224
|
+
}
|
|
225
|
+
// For Buffer body, Node *does not* need duplex.
|
|
226
|
+
return { body: buf };
|
|
227
|
+
}
|
|
228
|
+
//
|
|
229
|
+
// 🔹 BROWSER: keep your previous behavior (streams/Blob), no duplex.
|
|
230
|
+
//
|
|
231
|
+
let body;
|
|
232
|
+
// Already a web ReadableStream
|
|
233
|
+
if (typeof ReadableStream !== 'undefined' && data instanceof ReadableStream) {
|
|
234
|
+
body = data;
|
|
235
|
+
}
|
|
236
|
+
// Node Readable in browser (rare, but be safe) → Blob
|
|
237
|
+
else if (data instanceof node_stream_1.Readable) {
|
|
238
|
+
const chunks = [];
|
|
239
|
+
for await (const chunk of data) {
|
|
240
|
+
const buf = typeof chunk === 'string'
|
|
241
|
+
? new TextEncoder().encode(chunk)
|
|
242
|
+
: new Uint8Array(chunk);
|
|
243
|
+
chunks.push(buf);
|
|
244
|
+
}
|
|
245
|
+
body = new Blob(chunks);
|
|
246
|
+
}
|
|
247
|
+
// Buffer / Uint8Array
|
|
248
|
+
else if (Buffer.isBuffer(data) || data instanceof Uint8Array) {
|
|
249
|
+
body = new Blob([Uint8Array.from(data)]);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
throw new Error('Unsupported body type for toFetchBody (browser)');
|
|
253
|
+
}
|
|
254
|
+
// Firefox / Safari – avoid streaming uploads
|
|
255
|
+
if (isFirefoxOrSafari() && body instanceof ReadableStream) {
|
|
256
|
+
const blob = await new Response(body).blob();
|
|
257
|
+
return { body: blob };
|
|
258
|
+
}
|
|
259
|
+
return { body };
|
|
260
|
+
}
|
package/lib/cjs/common/signer.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.TurboDataItemAbstractSigner = void 0;
|
|
7
|
+
exports.makeX402Signer = makeX402Signer;
|
|
7
8
|
/**
|
|
8
9
|
* Copyright (C) 2022-2024 Permanent Data Solutions, Inc.
|
|
9
10
|
*
|
|
@@ -31,6 +32,9 @@ const crypto_2 = require("crypto");
|
|
|
31
32
|
const ethers_1 = require("ethers");
|
|
32
33
|
const ethers_2 = require("ethers");
|
|
33
34
|
const tweetnacl_1 = __importDefault(require("tweetnacl"));
|
|
35
|
+
const viem_1 = require("viem");
|
|
36
|
+
const accounts_1 = require("viem/accounts");
|
|
37
|
+
const chains_1 = require("viem/chains");
|
|
34
38
|
const types_js_1 = require("../types.js");
|
|
35
39
|
const base64_js_1 = require("../utils/base64.js");
|
|
36
40
|
const logger_js_1 = require("./logger.js");
|
|
@@ -155,3 +159,34 @@ class TurboDataItemAbstractSigner {
|
|
|
155
159
|
}
|
|
156
160
|
}
|
|
157
161
|
exports.TurboDataItemAbstractSigner = TurboDataItemAbstractSigner;
|
|
162
|
+
async function makeX402Signer(arbundlesSigner) {
|
|
163
|
+
// Node: our SDK uses EthereumSigner with a raw private key
|
|
164
|
+
if (arbundlesSigner instanceof arbundles_1.EthereumSigner) {
|
|
165
|
+
return (0, viem_1.createWalletClient)({
|
|
166
|
+
account: (0, accounts_1.privateKeyToAccount)(('0x' +
|
|
167
|
+
Buffer.from(arbundlesSigner.key).toString('hex'))),
|
|
168
|
+
chain: chains_1.baseSepolia,
|
|
169
|
+
transport: (0, viem_1.http)(),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
// Browser: use injected wallet + selected account
|
|
173
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
174
|
+
if (typeof window !== 'undefined' && window.ethereum) {
|
|
175
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
176
|
+
const provider = window.ethereum;
|
|
177
|
+
// ask wallet for an account
|
|
178
|
+
const accounts = (await provider.request({
|
|
179
|
+
method: 'eth_requestAccounts',
|
|
180
|
+
}));
|
|
181
|
+
if (accounts === undefined || accounts.length === 0) {
|
|
182
|
+
throw new Error('No accounts returned from wallet');
|
|
183
|
+
}
|
|
184
|
+
const account = accounts[0];
|
|
185
|
+
return (0, viem_1.createWalletClient)({
|
|
186
|
+
account,
|
|
187
|
+
chain: chains_1.baseSepolia,
|
|
188
|
+
transport: (0, viem_1.custom)(provider),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
throw new Error('Unable to construct x402 signer for x402 options');
|
|
192
|
+
}
|
package/lib/cjs/common/upload.js
CHANGED
|
@@ -28,6 +28,7 @@ const events_js_1 = require("./events.js");
|
|
|
28
28
|
const http_js_1 = require("./http.js");
|
|
29
29
|
const index_js_1 = require("./index.js");
|
|
30
30
|
const logger_js_1 = require("./logger.js");
|
|
31
|
+
const signer_js_1 = require("./signer.js");
|
|
31
32
|
function isTurboUploadFileWithStreamFactoryParams(params) {
|
|
32
33
|
return 'fileStreamFactory' in params;
|
|
33
34
|
}
|
|
@@ -53,7 +54,7 @@ class TurboUnauthenticatedUploadService {
|
|
|
53
54
|
});
|
|
54
55
|
this.retryConfig = retryConfig;
|
|
55
56
|
}
|
|
56
|
-
async uploadSignedDataItem({ dataItemStreamFactory, dataItemSizeFactory, dataItemOpts, signal, events = {}, }) {
|
|
57
|
+
async uploadSignedDataItem({ dataItemStreamFactory, dataItemSizeFactory, dataItemOpts, signal, events = {}, x402Options, }) {
|
|
57
58
|
const dataItemSize = dataItemSizeFactory();
|
|
58
59
|
this.logger.debug('Uploading signed data item...');
|
|
59
60
|
// create the tapped stream with events
|
|
@@ -83,6 +84,7 @@ class TurboUnauthenticatedUploadService {
|
|
|
83
84
|
signal,
|
|
84
85
|
data: streamWithUploadEvents,
|
|
85
86
|
headers,
|
|
87
|
+
x402Options,
|
|
86
88
|
});
|
|
87
89
|
// resume the stream so events start flowing to the post
|
|
88
90
|
resume();
|
|
@@ -94,6 +96,7 @@ exports.TurboUnauthenticatedUploadService = TurboUnauthenticatedUploadService;
|
|
|
94
96
|
class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadService {
|
|
95
97
|
constructor({ url = exports.defaultUploadServiceURL, retryConfig, signer, logger, token, paymentService, }) {
|
|
96
98
|
super({ url, retryConfig, logger, token });
|
|
99
|
+
this.x402EnabledTokens = ['base-usdc'];
|
|
97
100
|
this.enabledOnDemandTokens = [
|
|
98
101
|
'ario',
|
|
99
102
|
'solana',
|
|
@@ -176,6 +179,14 @@ class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadServ
|
|
|
176
179
|
}
|
|
177
180
|
async uploadFile(params) {
|
|
178
181
|
const { signal, dataItemOpts, events, fileStreamFactory, fileSizeFactory, fundingMode = new types_js_1.ExistingBalanceFunding(), } = this.resolveUploadFileConfig(params);
|
|
182
|
+
if (fundingMode instanceof types_js_1.X402Funding &&
|
|
183
|
+
!this.x402EnabledTokens.includes(this.token)) {
|
|
184
|
+
throw new Error('x402 uploads are not supported for token: ' + this.token);
|
|
185
|
+
}
|
|
186
|
+
if (params.chunkingMode === 'force' && fundingMode instanceof types_js_1.X402Funding) {
|
|
187
|
+
throw new Error("Chunking mode 'force' is not supported when x402 is enabled");
|
|
188
|
+
}
|
|
189
|
+
this.logger.debug('Starting file upload', { params });
|
|
179
190
|
let retries = 0;
|
|
180
191
|
const maxRetries = this.retryConfig.retries ?? 3;
|
|
181
192
|
const retryDelay = this.retryConfig.retryDelay ??
|
|
@@ -220,7 +231,8 @@ class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadServ
|
|
|
220
231
|
chunkingMode: params.chunkingMode,
|
|
221
232
|
maxFinalizeMs: params.maxFinalizeMs,
|
|
222
233
|
});
|
|
223
|
-
if (chunkedUploader.shouldUseChunkUploader
|
|
234
|
+
if (chunkedUploader.shouldUseChunkUploader &&
|
|
235
|
+
!(fundingMode instanceof types_js_1.X402Funding)) {
|
|
224
236
|
const response = await chunkedUploader.upload({
|
|
225
237
|
dataItemStreamFactory,
|
|
226
238
|
dataItemSizeFactory,
|
|
@@ -230,12 +242,20 @@ class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadServ
|
|
|
230
242
|
});
|
|
231
243
|
return { ...response, cryptoFundResult };
|
|
232
244
|
}
|
|
245
|
+
const x402Options = fundingMode instanceof types_js_1.X402Funding
|
|
246
|
+
? {
|
|
247
|
+
signer: fundingMode.signer ??
|
|
248
|
+
(await (0, signer_js_1.makeX402Signer)(this.signer.signer)),
|
|
249
|
+
maxMUSDCAmount: fundingMode.maxMUSDCAmount,
|
|
250
|
+
}
|
|
251
|
+
: undefined;
|
|
233
252
|
const response = await this.uploadSignedDataItem({
|
|
234
253
|
dataItemStreamFactory,
|
|
235
254
|
dataItemSizeFactory,
|
|
236
255
|
dataItemOpts,
|
|
237
256
|
signal,
|
|
238
257
|
events,
|
|
258
|
+
x402Options,
|
|
239
259
|
});
|
|
240
260
|
return { ...response, cryptoFundResult };
|
|
241
261
|
}
|
|
@@ -387,6 +407,7 @@ class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadServ
|
|
|
387
407
|
});
|
|
388
408
|
},
|
|
389
409
|
},
|
|
410
|
+
fundingMode,
|
|
390
411
|
});
|
|
391
412
|
const relativePath = this.getRelativePath(file, params);
|
|
392
413
|
paths[relativePath] = { id: result.id };
|
|
@@ -479,6 +500,7 @@ class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadServ
|
|
|
479
500
|
maxChunkConcurrency,
|
|
480
501
|
maxFinalizeMs,
|
|
481
502
|
chunkingMode,
|
|
503
|
+
fundingMode,
|
|
482
504
|
});
|
|
483
505
|
emitter.emit('folder-success');
|
|
484
506
|
return {
|
package/lib/cjs/types.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.validChunkingModes = exports.isJWK = exports.isWebUploadFolderParams = exports.isNodeUploadFolderParams = exports.multipartFinalizedStatus = exports.multipartFailedStatus = exports.multipartPendingStatus = exports.OnDemandFunding = exports.ExistingBalanceFunding = exports.supportedEvmSignerTokens = exports.tokenTypes = exports.fiatCurrencyTypes = void 0;
|
|
3
|
+
exports.validChunkingModes = exports.isJWK = exports.isWebUploadFolderParams = exports.isNodeUploadFolderParams = exports.multipartFinalizedStatus = exports.multipartFailedStatus = exports.multipartPendingStatus = exports.X402Funding = exports.OnDemandFunding = exports.ExistingBalanceFunding = exports.supportedEvmSignerTokens = exports.tokenTypes = exports.fiatCurrencyTypes = void 0;
|
|
4
4
|
exports.isCurrency = isCurrency;
|
|
5
5
|
exports.isKyvePrivateKey = isKyvePrivateKey;
|
|
6
6
|
exports.isEthPrivateKey = isEthPrivateKey;
|
|
@@ -62,6 +62,14 @@ class OnDemandFunding {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
exports.OnDemandFunding = OnDemandFunding;
|
|
65
|
+
class X402Funding {
|
|
66
|
+
constructor({ signer, maxMUSDCAmount, }) {
|
|
67
|
+
this.signer = signer;
|
|
68
|
+
this.maxMUSDCAmount =
|
|
69
|
+
maxMUSDCAmount !== undefined ? new bignumber_js_1.BigNumber(maxMUSDCAmount) : undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.X402Funding = X402Funding;
|
|
65
73
|
exports.multipartPendingStatus = [
|
|
66
74
|
'ASSEMBLING',
|
|
67
75
|
'VALIDATING',
|
package/lib/cjs/version.js
CHANGED
package/lib/esm/cli/options.js
CHANGED
|
@@ -183,6 +183,11 @@ export const optionMap = {
|
|
|
183
183
|
description: 'Enable on-demand crypto top-ups during upload if balance is insufficient',
|
|
184
184
|
default: false,
|
|
185
185
|
},
|
|
186
|
+
x402: {
|
|
187
|
+
alias: '--x402',
|
|
188
|
+
description: 'Pay for the action using x402 funding (if available). Requires token `base-usdc`.',
|
|
189
|
+
default: false,
|
|
190
|
+
},
|
|
186
191
|
topUpBufferMultiplier: {
|
|
187
192
|
alias: '--top-up-buffer-multiplier <topUpBufferMultiplier>',
|
|
188
193
|
description: 'Multiplier to apply to the estimated top-up amount to avoid underpayment during on-demand top-ups. Defaults to 1.1 (10% buffer).',
|
|
@@ -212,6 +217,7 @@ const onDemandOptions = [
|
|
|
212
217
|
optionMap.onDemand,
|
|
213
218
|
optionMap.topUpBufferMultiplier,
|
|
214
219
|
optionMap.maxCryptoTopUpValue,
|
|
220
|
+
optionMap.x402,
|
|
215
221
|
];
|
|
216
222
|
export const uploadOptions = [
|
|
217
223
|
...walletOptions,
|
package/lib/esm/cli/utils.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import bs58 from 'bs58';
|
|
17
17
|
import { readFileSync, statSync } from 'fs';
|
|
18
|
-
import { ExistingBalanceFunding, OnDemandFunding, TurboFactory, defaultTurboConfiguration, developmentTurboConfiguration, fiatCurrencyTypes, isCurrency, isTokenType, privateKeyFromKyveMnemonic, tokenToBaseMap, } from '../node/index.js';
|
|
18
|
+
import { ExistingBalanceFunding, OnDemandFunding, TurboFactory, X402Funding, defaultTurboConfiguration, developmentTurboConfiguration, fiatCurrencyTypes, isCurrency, isTokenType, privateKeyFromKyveMnemonic, tokenToBaseMap, } from '../node/index.js';
|
|
19
19
|
import { defaultProdAoConfigs, tokenToDevAoConfigMap, tokenToDevGatewayMap, } from '../utils/common.js';
|
|
20
20
|
import { NoWalletProvidedError } from './errors.js';
|
|
21
21
|
export function exitWithErrorLog(error) {
|
|
@@ -246,7 +246,10 @@ export function getTagsFromOptions(options) {
|
|
|
246
246
|
return parseTags(options.tags);
|
|
247
247
|
}
|
|
248
248
|
export function onDemandOptionsFromOptions(options) {
|
|
249
|
-
if (
|
|
249
|
+
if (options.x402 && options.onDemand) {
|
|
250
|
+
throw new Error('Cannot use both --x402 and --on-demand flags');
|
|
251
|
+
}
|
|
252
|
+
if (!options.onDemand && !options.x402) {
|
|
250
253
|
return { fundingMode: new ExistingBalanceFunding() };
|
|
251
254
|
}
|
|
252
255
|
const value = options.maxCryptoTopUpValue;
|
|
@@ -258,6 +261,11 @@ export function onDemandOptionsFromOptions(options) {
|
|
|
258
261
|
const token = tokenFromOptions(options);
|
|
259
262
|
maxTokenAmount = tokenToBaseMap[token](value).toString();
|
|
260
263
|
}
|
|
264
|
+
if (options.x402) {
|
|
265
|
+
return {
|
|
266
|
+
fundingMode: new X402Funding({ maxMUSDCAmount: maxTokenAmount }),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
261
269
|
if (options.topUpBufferMultiplier !== undefined &&
|
|
262
270
|
(isNaN(options.topUpBufferMultiplier) || options.topUpBufferMultiplier < 1)) {
|
|
263
271
|
throw new Error('topUpBufferMultiplier must be a number >= 1');
|
package/lib/esm/common/http.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { AxiosError, CanceledError } from 'axios';
|
|
17
17
|
import { Readable } from 'node:stream';
|
|
18
|
+
import { wrapFetchWithPayment } from 'x402-fetch';
|
|
18
19
|
import { createAxiosInstance, defaultRetryConfig, } from '../utils/axiosClient.js';
|
|
19
20
|
import { sleep } from '../utils/common.js';
|
|
20
21
|
import { FailedRequestError } from '../utils/errors.js';
|
|
@@ -33,7 +34,46 @@ export class TurboHTTPService {
|
|
|
33
34
|
async get({ endpoint, signal, allowedStatuses = [200, 202], headers, }) {
|
|
34
35
|
return this.retryRequest(() => this.axios.get(endpoint, { headers, signal }), allowedStatuses);
|
|
35
36
|
}
|
|
36
|
-
async post({ endpoint, signal, allowedStatuses = [200, 202], headers, data, }) {
|
|
37
|
+
async post({ endpoint, signal, allowedStatuses = [200, 202], headers, data, x402Options, }) {
|
|
38
|
+
if (x402Options !== undefined) {
|
|
39
|
+
this.logger.debug('Using X402 options for POST request', {
|
|
40
|
+
endpoint,
|
|
41
|
+
x402Options,
|
|
42
|
+
});
|
|
43
|
+
const { body, duplex } = await toX402FetchBody(data);
|
|
44
|
+
try {
|
|
45
|
+
const maxMUSDCAmount = x402Options.maxMUSDCAmount !== undefined
|
|
46
|
+
? BigInt(x402Options.maxMUSDCAmount.toString())
|
|
47
|
+
: undefined;
|
|
48
|
+
const fetchWithPay = wrapFetchWithPayment(fetch, x402Options.signer, maxMUSDCAmount);
|
|
49
|
+
const res = await fetchWithPay(this.axios.defaults.baseURL + '/x402/data-item/signed', {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers,
|
|
52
|
+
body,
|
|
53
|
+
signal,
|
|
54
|
+
...(duplex ? { duplex } : {}), // Use duplex only where streams are working
|
|
55
|
+
});
|
|
56
|
+
if (!allowedStatuses.includes(res.status)) {
|
|
57
|
+
const errorText = await res.text();
|
|
58
|
+
throw new FailedRequestError(
|
|
59
|
+
// Return error message from server if available
|
|
60
|
+
errorText || res.statusText, res.status);
|
|
61
|
+
}
|
|
62
|
+
return res.json();
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error instanceof FailedRequestError) {
|
|
66
|
+
throw error; // rethrow FailedRequestError
|
|
67
|
+
}
|
|
68
|
+
// Handle CanceledError specifically
|
|
69
|
+
if (error.message.includes('The operation was aborted')) {
|
|
70
|
+
throw new CanceledError();
|
|
71
|
+
}
|
|
72
|
+
// Log the error and throw a FailedRequestError
|
|
73
|
+
this.logger.error('Error posting data', { endpoint, error });
|
|
74
|
+
throw new FailedRequestError(error instanceof Error ? error.message : 'Unknown error', error.response?.status);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
37
77
|
// Buffer and Readable → keep Axios (streams work fine there)
|
|
38
78
|
if (!(data instanceof ReadableStream)) {
|
|
39
79
|
if (data instanceof Readable) {
|
|
@@ -137,3 +177,79 @@ async function toFetchBody(data) {
|
|
|
137
177
|
const blob = await new Response(data).blob();
|
|
138
178
|
return { body: blob }; // browser sets length
|
|
139
179
|
}
|
|
180
|
+
const isNode = typeof process !== 'undefined' && !!process.versions?.node;
|
|
181
|
+
const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
|
182
|
+
function isFirefoxOrSafari() {
|
|
183
|
+
if (!isBrowser)
|
|
184
|
+
return false;
|
|
185
|
+
const ua = navigator.userAgent;
|
|
186
|
+
return ua.includes('Firefox') || ua.includes('Safari');
|
|
187
|
+
}
|
|
188
|
+
/** Create a re-usable body for x402 fetch protocol */
|
|
189
|
+
export async function toX402FetchBody(data) {
|
|
190
|
+
//
|
|
191
|
+
// 🔹 NODE: always buffer to a non-stream body (Buffer)
|
|
192
|
+
// so fetchWithPayment can reuse it safely.
|
|
193
|
+
//
|
|
194
|
+
if (isNode) {
|
|
195
|
+
let buf;
|
|
196
|
+
// Web ReadableStream → Buffer
|
|
197
|
+
if (typeof ReadableStream !== 'undefined' &&
|
|
198
|
+
data instanceof ReadableStream) {
|
|
199
|
+
const ab = await new Response(data).arrayBuffer();
|
|
200
|
+
buf = Buffer.from(ab);
|
|
201
|
+
}
|
|
202
|
+
// Node Readable → Buffer
|
|
203
|
+
else if (data instanceof Readable) {
|
|
204
|
+
const chunks = [];
|
|
205
|
+
for await (const chunk of data) {
|
|
206
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
207
|
+
}
|
|
208
|
+
buf = Buffer.concat(chunks.map((c) => Uint8Array.from(c)));
|
|
209
|
+
}
|
|
210
|
+
// Buffer / Uint8Array
|
|
211
|
+
else if (Buffer.isBuffer(data)) {
|
|
212
|
+
buf = data;
|
|
213
|
+
}
|
|
214
|
+
else if (data instanceof Uint8Array) {
|
|
215
|
+
buf = Buffer.from(data);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
throw new Error('Unsupported body type for toFetchBody (Node)');
|
|
219
|
+
}
|
|
220
|
+
// For Buffer body, Node *does not* need duplex.
|
|
221
|
+
return { body: buf };
|
|
222
|
+
}
|
|
223
|
+
//
|
|
224
|
+
// 🔹 BROWSER: keep your previous behavior (streams/Blob), no duplex.
|
|
225
|
+
//
|
|
226
|
+
let body;
|
|
227
|
+
// Already a web ReadableStream
|
|
228
|
+
if (typeof ReadableStream !== 'undefined' && data instanceof ReadableStream) {
|
|
229
|
+
body = data;
|
|
230
|
+
}
|
|
231
|
+
// Node Readable in browser (rare, but be safe) → Blob
|
|
232
|
+
else if (data instanceof Readable) {
|
|
233
|
+
const chunks = [];
|
|
234
|
+
for await (const chunk of data) {
|
|
235
|
+
const buf = typeof chunk === 'string'
|
|
236
|
+
? new TextEncoder().encode(chunk)
|
|
237
|
+
: new Uint8Array(chunk);
|
|
238
|
+
chunks.push(buf);
|
|
239
|
+
}
|
|
240
|
+
body = new Blob(chunks);
|
|
241
|
+
}
|
|
242
|
+
// Buffer / Uint8Array
|
|
243
|
+
else if (Buffer.isBuffer(data) || data instanceof Uint8Array) {
|
|
244
|
+
body = new Blob([Uint8Array.from(data)]);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
throw new Error('Unsupported body type for toFetchBody (browser)');
|
|
248
|
+
}
|
|
249
|
+
// Firefox / Safari – avoid streaming uploads
|
|
250
|
+
if (isFirefoxOrSafari() && body instanceof ReadableStream) {
|
|
251
|
+
const blob = await new Response(body).blob();
|
|
252
|
+
return { body: blob };
|
|
253
|
+
}
|
|
254
|
+
return { body };
|
|
255
|
+
}
|
package/lib/esm/common/signer.js
CHANGED
|
@@ -25,6 +25,9 @@ import { randomBytes } from 'crypto';
|
|
|
25
25
|
import { Wallet as EthereumWallet, ethers, parseEther } from 'ethers';
|
|
26
26
|
import { computeAddress } from 'ethers';
|
|
27
27
|
import nacl from 'tweetnacl';
|
|
28
|
+
import { createWalletClient, custom, http } from 'viem';
|
|
29
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
30
|
+
import { baseSepolia } from 'viem/chains';
|
|
28
31
|
import { isEthereumWalletAdapter, isSolanaWalletAdapter, } from '../types.js';
|
|
29
32
|
import { fromB64Url, ownerToAddress as ownerToB64Address, toB64Url, } from '../utils/base64.js';
|
|
30
33
|
import { TurboWinstonLogger } from './logger.js';
|
|
@@ -148,3 +151,34 @@ export class TurboDataItemAbstractSigner {
|
|
|
148
151
|
return this.signer.sign(dataToSign);
|
|
149
152
|
}
|
|
150
153
|
}
|
|
154
|
+
export async function makeX402Signer(arbundlesSigner) {
|
|
155
|
+
// Node: our SDK uses EthereumSigner with a raw private key
|
|
156
|
+
if (arbundlesSigner instanceof EthereumSigner) {
|
|
157
|
+
return createWalletClient({
|
|
158
|
+
account: privateKeyToAccount(('0x' +
|
|
159
|
+
Buffer.from(arbundlesSigner.key).toString('hex'))),
|
|
160
|
+
chain: baseSepolia,
|
|
161
|
+
transport: http(),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// Browser: use injected wallet + selected account
|
|
165
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
166
|
+
if (typeof window !== 'undefined' && window.ethereum) {
|
|
167
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
168
|
+
const provider = window.ethereum;
|
|
169
|
+
// ask wallet for an account
|
|
170
|
+
const accounts = (await provider.request({
|
|
171
|
+
method: 'eth_requestAccounts',
|
|
172
|
+
}));
|
|
173
|
+
if (accounts === undefined || accounts.length === 0) {
|
|
174
|
+
throw new Error('No accounts returned from wallet');
|
|
175
|
+
}
|
|
176
|
+
const account = accounts[0];
|
|
177
|
+
return createWalletClient({
|
|
178
|
+
account,
|
|
179
|
+
chain: baseSepolia,
|
|
180
|
+
transport: custom(provider),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
throw new Error('Unable to construct x402 signer for x402 options');
|
|
184
|
+
}
|