@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.
@@ -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,
@@ -270,7 +270,10 @@ function getTagsFromOptions(options) {
270
270
  return parseTags(options.tags);
271
271
  }
272
272
  function onDemandOptionsFromOptions(options) {
273
- if (!options.onDemand) {
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');
@@ -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
+ }
@@ -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
+ }
@@ -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',
@@ -17,4 +17,4 @@
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.version = void 0;
19
19
  // AUTOMATICALLY GENERATED FILE - DO NOT TOUCH
20
- exports.version = '1.36.0';
20
+ exports.version = '1.37.0';
@@ -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,
@@ -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 (!options.onDemand) {
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');
@@ -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
+ }
@@ -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
+ }