@ardrive/turbo-sdk 1.31.1 → 1.32.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.
Files changed (77) hide show
  1. package/README.md +28 -0
  2. package/bundles/web.bundle.min.js +437 -258
  3. package/lib/cjs/cli/commands/uploadFile.js +1 -0
  4. package/lib/cjs/cli/commands/uploadFolder.js +1 -0
  5. package/lib/cjs/cli/options.js +19 -0
  6. package/lib/cjs/cli/utils.js +25 -0
  7. package/lib/cjs/common/factory.js +1 -0
  8. package/lib/cjs/common/payment.js +1 -1
  9. package/lib/cjs/common/token/ario.js +1 -1
  10. package/lib/cjs/common/token/arweave.js +1 -1
  11. package/lib/cjs/common/token/baseEth.js +17 -1
  12. package/lib/cjs/common/token/ethereum.js +13 -4
  13. package/lib/cjs/common/token/kyve.js +1 -1
  14. package/lib/cjs/common/token/solana.js +1 -1
  15. package/lib/cjs/common/turbo.js +3 -1
  16. package/lib/cjs/common/upload.js +124 -6
  17. package/lib/cjs/node/upload.js +2 -1
  18. package/lib/cjs/types.js +20 -1
  19. package/lib/cjs/version.js +1 -1
  20. package/lib/cjs/web/upload.js +2 -2
  21. package/lib/esm/cli/commands/uploadFile.js +2 -1
  22. package/lib/esm/cli/commands/uploadFolder.js +2 -1
  23. package/lib/esm/cli/options.js +19 -0
  24. package/lib/esm/cli/utils.js +25 -1
  25. package/lib/esm/common/factory.js +1 -0
  26. package/lib/esm/common/payment.js +1 -1
  27. package/lib/esm/common/token/ario.js +1 -1
  28. package/lib/esm/common/token/arweave.js +1 -1
  29. package/lib/esm/common/token/baseEth.js +17 -1
  30. package/lib/esm/common/token/ethereum.js +13 -4
  31. package/lib/esm/common/token/kyve.js +1 -1
  32. package/lib/esm/common/token/solana.js +1 -1
  33. package/lib/esm/common/turbo.js +3 -1
  34. package/lib/esm/common/upload.js +124 -6
  35. package/lib/esm/node/upload.js +2 -1
  36. package/lib/esm/types.js +17 -0
  37. package/lib/esm/version.js +1 -1
  38. package/lib/esm/web/upload.js +2 -2
  39. package/lib/types/cli/commands/uploadFile.d.ts.map +1 -1
  40. package/lib/types/cli/commands/uploadFolder.d.ts.map +1 -1
  41. package/lib/types/cli/options.d.ts +43 -0
  42. package/lib/types/cli/options.d.ts.map +1 -1
  43. package/lib/types/cli/types.d.ts +4 -0
  44. package/lib/types/cli/types.d.ts.map +1 -1
  45. package/lib/types/cli/utils.d.ts +4 -1
  46. package/lib/types/cli/utils.d.ts.map +1 -1
  47. package/lib/types/common/factory.d.ts +4 -1
  48. package/lib/types/common/factory.d.ts.map +1 -1
  49. package/lib/types/common/token/ario.d.ts +1 -1
  50. package/lib/types/common/token/ario.d.ts.map +1 -1
  51. package/lib/types/common/token/arweave.d.ts +1 -1
  52. package/lib/types/common/token/arweave.d.ts.map +1 -1
  53. package/lib/types/common/token/baseEth.d.ts +1 -0
  54. package/lib/types/common/token/baseEth.d.ts.map +1 -1
  55. package/lib/types/common/token/ethereum.d.ts +2 -1
  56. package/lib/types/common/token/ethereum.d.ts.map +1 -1
  57. package/lib/types/common/token/kyve.d.ts +1 -1
  58. package/lib/types/common/token/kyve.d.ts.map +1 -1
  59. package/lib/types/common/token/solana.d.ts +1 -1
  60. package/lib/types/common/token/solana.d.ts.map +1 -1
  61. package/lib/types/common/turbo.d.ts +2 -2
  62. package/lib/types/common/turbo.d.ts.map +1 -1
  63. package/lib/types/common/upload.d.ts +16 -3
  64. package/lib/types/common/upload.d.ts.map +1 -1
  65. package/lib/types/node/factory.d.ts +4 -1
  66. package/lib/types/node/factory.d.ts.map +1 -1
  67. package/lib/types/node/upload.d.ts +4 -1
  68. package/lib/types/node/upload.d.ts.map +1 -1
  69. package/lib/types/types.d.ts +19 -4
  70. package/lib/types/types.d.ts.map +1 -1
  71. package/lib/types/version.d.ts +1 -1
  72. package/lib/types/version.d.ts.map +1 -1
  73. package/lib/types/web/factory.d.ts +4 -1
  74. package/lib/types/web/factory.d.ts.map +1 -1
  75. package/lib/types/web/upload.d.ts +4 -1
  76. package/lib/types/web/upload.d.ts.map +1 -1
  77. package/package.json +1 -1
@@ -33,6 +33,7 @@ async function uploadFile(options) {
33
33
  fileSizeFactory: () => fileSize,
34
34
  dataItemOpts: { tags: [...constants_js_1.turboCliTags, ...customTags], paidBy },
35
35
  ...(0, utils_js_1.getChunkingOptions)(options),
36
+ ...(0, utils_js_1.onDemandOptionsFromOptions)(options),
36
37
  });
37
38
  console.log('Uploaded file:', JSON.stringify(result, null, 2));
38
39
  }
@@ -36,6 +36,7 @@ async function uploadFolder(options) {
36
36
  chunkingMode,
37
37
  maxChunkConcurrency,
38
38
  maxFinalizeMs,
39
+ ...(0, utils_js_1.onDemandOptionsFromOptions)(options),
39
40
  });
40
41
  console.log('Uploaded folder:', JSON.stringify(result, null, 2));
41
42
  }
@@ -176,6 +176,19 @@ exports.optionMap = {
176
176
  description: 'Chunking mode to use for the upload. Can be "auto", "force" or "disabled". Defaults to "auto".',
177
177
  default: 'auto',
178
178
  },
179
+ onDemand: {
180
+ alias: '--on-demand',
181
+ description: 'Enable on-demand crypto top-ups during upload if balance is insufficient',
182
+ default: false,
183
+ },
184
+ topUpBufferMultiplier: {
185
+ alias: '--top-up-buffer-multiplier <topUpBufferMultiplier>',
186
+ description: 'Multiplier to apply to the estimated top-up amount to avoid underpayment during on-demand top-ups. Defaults to 1.1 (10% buffer).',
187
+ },
188
+ maxCryptoTopUpValue: {
189
+ alias: '--max-crypto-top-up-value <maxCryptoTopUpValue>',
190
+ description: 'Maximum crypto top-up value to use for the upload. Defaults to no limit.',
191
+ },
179
192
  };
180
193
  exports.walletOptions = [
181
194
  exports.optionMap.walletFile,
@@ -193,6 +206,11 @@ exports.globalOptions = [
193
206
  exports.optionMap.paymentUrl,
194
207
  exports.optionMap.uploadUrl,
195
208
  ];
209
+ const onDemandOptions = [
210
+ exports.optionMap.onDemand,
211
+ exports.optionMap.topUpBufferMultiplier,
212
+ exports.optionMap.maxCryptoTopUpValue,
213
+ ];
196
214
  exports.uploadOptions = [
197
215
  ...exports.walletOptions,
198
216
  exports.optionMap.paidBy,
@@ -203,6 +221,7 @@ exports.uploadOptions = [
203
221
  exports.optionMap.maxFinalizeMs,
204
222
  exports.optionMap.chunkByteCount,
205
223
  exports.optionMap.chunkingMode,
224
+ ...onDemandOptions,
206
225
  ];
207
226
  exports.uploadFolderOptions = [
208
227
  ...exports.uploadOptions,
@@ -18,6 +18,7 @@ exports.paidByFromOptions = paidByFromOptions;
18
18
  exports.getUploadFolderOptions = getUploadFolderOptions;
19
19
  exports.parseTags = parseTags;
20
20
  exports.getTagsFromOptions = getTagsFromOptions;
21
+ exports.onDemandOptionsFromOptions = onDemandOptionsFromOptions;
21
22
  exports.currencyFromOptions = currencyFromOptions;
22
23
  exports.requiredByteCountFromOptions = requiredByteCountFromOptions;
23
24
  exports.getChunkingOptions = getChunkingOptions;
@@ -268,6 +269,30 @@ function parseTags(tagsArr) {
268
269
  function getTagsFromOptions(options) {
269
270
  return parseTags(options.tags);
270
271
  }
272
+ function onDemandOptionsFromOptions(options) {
273
+ if (!options.onDemand) {
274
+ return { fundingMode: new index_js_1.ExistingBalanceFunding() };
275
+ }
276
+ const value = options.maxCryptoTopUpValue;
277
+ let maxTokenAmount = undefined;
278
+ if (value !== undefined) {
279
+ if (isNaN(+value) || +value <= 0) {
280
+ throw new Error('maxTokenAmount must be a positive number');
281
+ }
282
+ const token = tokenFromOptions(options);
283
+ maxTokenAmount = index_js_1.tokenToBaseMap[token](value).toString();
284
+ }
285
+ if (options.topUpBufferMultiplier !== undefined &&
286
+ (isNaN(options.topUpBufferMultiplier) || options.topUpBufferMultiplier < 1)) {
287
+ throw new Error('topUpBufferMultiplier must be a number >= 1');
288
+ }
289
+ return {
290
+ fundingMode: new index_js_1.OnDemandFunding({
291
+ maxTokenAmount,
292
+ topUpBufferMultiplier: options.topUpBufferMultiplier,
293
+ }),
294
+ };
295
+ }
271
296
  function currencyFromOptions(options) {
272
297
  const currency = options.currency?.toLowerCase();
273
298
  if (!(0, index_js_1.isCurrency)(currency)) {
@@ -109,6 +109,7 @@ class TurboBaseFactory {
109
109
  signer: turboSigner,
110
110
  logger,
111
111
  token,
112
+ paymentService,
112
113
  });
113
114
  return new turbo_js_1.TurboAuthenticatedClient({
114
115
  uploadService,
@@ -291,7 +291,7 @@ class TurboAuthenticatedPaymentService extends TurboUnauthenticatedPaymentServic
291
291
  const txId = fundTx.id;
292
292
  try {
293
293
  // Let transaction settle some time
294
- await this.tokenTools.pollForTxBeingAvailable({ txId });
294
+ await this.tokenTools.pollTxAvailability({ txId });
295
295
  }
296
296
  catch (e) {
297
297
  this.logger.error(`Failed to poll for transaction being available from ${this.token} gateway... Attempting to submit fund tx to Turbo...`, e);
@@ -66,7 +66,7 @@ class ARIOToken {
66
66
  });
67
67
  return { id: txId, target, reward: '0' };
68
68
  }
69
- async pollForTxBeingAvailable() {
69
+ async pollTxAvailability() {
70
70
  // AO finality should be instant -- but we'll wait initial backoff to
71
71
  // provide infra some time to crank without reading the whole result
72
72
  return (0, common_js_1.sleep)(this.pollingOptions.initialBackoffMs);
@@ -78,7 +78,7 @@ class ArweaveToken {
78
78
  await this.submitTx(tx);
79
79
  return { id, target, reward: tx.reward };
80
80
  }
81
- async pollForTxBeingAvailable({ txId, }) {
81
+ async pollTxAvailability({ txId }) {
82
82
  const { maxAttempts, pollingIntervalMs, initialBackoffMs } = this.pollingOptions;
83
83
  this.logger.debug('Polling for transaction...', { txId });
84
84
  await (0, common_js_1.sleep)(initialBackoffMs);
@@ -5,7 +5,7 @@ const common_js_1 = require("../../utils/common.js");
5
5
  const ethereum_js_1 = require("./ethereum.js");
6
6
  class BaseEthToken extends ethereum_js_1.EthereumToken {
7
7
  constructor({ logger, gatewayUrl = common_js_1.defaultProdGatewayUrls['base-eth'], pollingOptions = {
8
- initialBackoffMs: 1_000,
8
+ initialBackoffMs: 2_500,
9
9
  maxAttempts: 10,
10
10
  pollingIntervalMs: 2_500,
11
11
  }, } = {}) {
@@ -15,5 +15,21 @@ class BaseEthToken extends ethereum_js_1.EthereumToken {
15
15
  pollingOptions,
16
16
  });
17
17
  }
18
+ async getTxAvailability(txId) {
19
+ const tx = await this.rpcProvider.getTransactionReceipt(txId);
20
+ if (tx) {
21
+ const confirmations = await tx.confirmations();
22
+ if (confirmations >= 1) {
23
+ this.logger.debug('Transaction is available on chain', {
24
+ txId,
25
+ tx,
26
+ confirmations,
27
+ });
28
+ return true;
29
+ }
30
+ }
31
+ this.logger.debug('Transaction not yet available on chain', { txId, tx });
32
+ return false;
33
+ }
18
34
  }
19
35
  exports.BaseEthToken = BaseEthToken;
@@ -48,13 +48,22 @@ class EthereumToken {
48
48
  target,
49
49
  };
50
50
  }
51
- async pollForTxBeingAvailable({ txId, }) {
51
+ async getTxAvailability(txId) {
52
+ const tx = await this.rpcProvider.getTransaction(txId);
53
+ if (tx) {
54
+ this.logger.debug('Transaction is available on chain', { txId, tx });
55
+ return true;
56
+ }
57
+ this.logger.debug('Transaction not yet available on chain', { txId });
58
+ return false;
59
+ }
60
+ async pollTxAvailability({ txId }) {
52
61
  await new Promise((resolve) => setTimeout(resolve, this.pollingOptions.initialBackoffMs));
53
62
  let attempts = 0;
54
63
  while (attempts < this.pollingOptions.maxAttempts) {
55
64
  try {
56
- const tx = await this.rpcProvider.getTransaction(txId);
57
- if (tx) {
65
+ const txIsAvailable = await this.getTxAvailability(txId);
66
+ if (txIsAvailable) {
58
67
  return;
59
68
  }
60
69
  }
@@ -64,7 +73,7 @@ class EthereumToken {
64
73
  await new Promise((resolve) => setTimeout(resolve, this.pollingOptions.pollingIntervalMs));
65
74
  attempts++;
66
75
  }
67
- throw new Error('Transaction not found after polling!');
76
+ throw new Error(`Transaction ${txId} not found after polling!`);
68
77
  }
69
78
  }
70
79
  exports.EthereumToken = EthereumToken;
@@ -66,7 +66,7 @@ class KyveToken {
66
66
  });
67
67
  return { id: txHash, target };
68
68
  }
69
- async pollForTxBeingAvailable({ txId, }) {
69
+ async pollTxAvailability({ txId }) {
70
70
  const { maxAttempts, pollingIntervalMs, initialBackoffMs } = this.pollingOptions;
71
71
  this.logger.debug('Polling for transaction...', {
72
72
  txId,
@@ -74,7 +74,7 @@ class SolanaToken {
74
74
  lastValidBlockHeight: tx.lastValidBlockHeight,
75
75
  }, 'finalized');
76
76
  }
77
- async pollForTxBeingAvailable({ txId, }) {
77
+ async pollTxAvailability({ txId }) {
78
78
  const { maxAttempts, pollingIntervalMs, initialBackoffMs } = this.pollingOptions;
79
79
  this.logger.debug('Polling for transaction...', {
80
80
  txId,
@@ -160,7 +160,7 @@ class TurboAuthenticatedClient extends TurboUnauthenticatedClient {
160
160
  /**
161
161
  * Signs and uploads raw data to the Turbo Upload Service.
162
162
  */
163
- upload({ data, dataItemOpts, signal, events, chunkByteCount, chunkingMode, maxChunkConcurrency, }) {
163
+ upload({ data, dataItemOpts, signal, events, chunkByteCount, chunkingMode, maxChunkConcurrency, maxFinalizeMs, fundingMode, }) {
164
164
  return this.uploadService.upload({
165
165
  data,
166
166
  dataItemOpts,
@@ -169,6 +169,8 @@ class TurboAuthenticatedClient extends TurboUnauthenticatedClient {
169
169
  chunkByteCount,
170
170
  chunkingMode,
171
171
  maxChunkConcurrency,
172
+ fundingMode,
173
+ maxFinalizeMs,
172
174
  });
173
175
  }
174
176
  uploadFile(params) {
@@ -17,13 +17,16 @@ exports.TurboAuthenticatedBaseUploadService = exports.TurboUnauthenticatedUpload
17
17
  * limitations under the License.
18
18
  */
19
19
  const axios_1 = require("axios");
20
+ const bignumber_js_1 = require("bignumber.js");
20
21
  const plimit_lit_1 = require("plimit-lit");
22
+ const types_js_1 = require("../types.js");
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");
24
26
  const chunked_js_1 = require("./chunked.js");
25
27
  const events_js_1 = require("./events.js");
26
28
  const http_js_1 = require("./http.js");
29
+ const index_js_1 = require("./index.js");
27
30
  const logger_js_1 = require("./logger.js");
28
31
  function isTurboUploadFileWithStreamFactoryParams(params) {
29
32
  return 'fileStreamFactory' in params;
@@ -89,14 +92,16 @@ class TurboUnauthenticatedUploadService {
89
92
  exports.TurboUnauthenticatedUploadService = TurboUnauthenticatedUploadService;
90
93
  // NOTE: to avoid redundancy, we use inheritance here - but generally prefer composition over inheritance
91
94
  class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadService {
92
- constructor({ url = exports.defaultUploadServiceURL, retryConfig, signer, logger, token, }) {
95
+ constructor({ url = exports.defaultUploadServiceURL, retryConfig, signer, logger, token, paymentService, }) {
93
96
  super({ url, retryConfig, logger, token });
97
+ this.enabledOnDemandTokens = ['ario', 'solana', 'base-eth'];
94
98
  this.signer = signer;
99
+ this.paymentService = paymentService;
95
100
  }
96
101
  /**
97
102
  * Signs and uploads raw data to the Turbo Upload Service.
98
103
  */
99
- upload({ data, dataItemOpts, signal, events, chunkByteCount, chunkingMode, maxChunkConcurrency, }) {
104
+ upload({ data, dataItemOpts, signal, events, chunkByteCount, chunkingMode, maxChunkConcurrency, fundingMode, maxFinalizeMs, }) {
100
105
  // This function is intended to be usable in both Node and browser environments.
101
106
  if ((0, common_js_1.isBlob)(data)) {
102
107
  const streamFactory = () => data.stream();
@@ -107,6 +112,11 @@ class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadServ
107
112
  signal,
108
113
  dataItemOpts,
109
114
  events,
115
+ chunkByteCount,
116
+ chunkingMode,
117
+ maxChunkConcurrency,
118
+ fundingMode,
119
+ maxFinalizeMs,
110
120
  });
111
121
  }
112
122
  const dataBuffer = (() => {
@@ -127,6 +137,8 @@ class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadServ
127
137
  chunkByteCount,
128
138
  chunkingMode,
129
139
  maxChunkConcurrency,
140
+ fundingMode,
141
+ maxFinalizeMs,
130
142
  });
131
143
  }
132
144
  resolveUploadFileConfig(params) {
@@ -158,7 +170,7 @@ class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadServ
158
170
  };
159
171
  }
160
172
  async uploadFile(params) {
161
- const { signal, dataItemOpts, events, fileStreamFactory, fileSizeFactory } = this.resolveUploadFileConfig(params);
173
+ const { signal, dataItemOpts, events, fileStreamFactory, fileSizeFactory, fundingMode = new types_js_1.ExistingBalanceFunding(), } = this.resolveUploadFileConfig(params);
162
174
  let retries = 0;
163
175
  const maxRetries = this.retryConfig.retries ?? 3;
164
176
  const retryDelay = this.retryConfig.retryDelay ??
@@ -167,6 +179,7 @@ class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadServ
167
179
  let lastStatusCode = undefined; // Store the last status code for throwing
168
180
  const emitter = new events_js_1.TurboEventEmitter(events);
169
181
  // avoid duplicating signing on failures here - these errors will immediately be thrown
182
+ let cryptoFundResult;
170
183
  // TODO: move the retry implementation to the http class, and avoid awaiting here. This will standardize the retry logic across all upload methods.
171
184
  while (retries < maxRetries) {
172
185
  if (signal?.aborted) {
@@ -179,6 +192,14 @@ class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadServ
179
192
  dataItemOpts,
180
193
  emitter,
181
194
  });
195
+ if (fundingMode instanceof types_js_1.OnDemandFunding &&
196
+ cryptoFundResult === undefined) {
197
+ const totalByteCount = dataItemSizeFactory();
198
+ cryptoFundResult = await this.onDemand({
199
+ totalByteCount,
200
+ onDemandFunding: fundingMode,
201
+ });
202
+ }
182
203
  // Now that we have the signed data item, we can upload it using the uploadSignedDataItem method
183
204
  // which will create a new emitter with upload events. We await
184
205
  // this result due to the wrapped retry logic of this method.
@@ -202,7 +223,7 @@ class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadServ
202
223
  signal,
203
224
  events,
204
225
  });
205
- return response;
226
+ return { ...response, cryptoFundResult };
206
227
  }
207
228
  const response = await this.uploadSignedDataItem({
208
229
  dataItemStreamFactory,
@@ -211,7 +232,7 @@ class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadServ
211
232
  signal,
212
233
  events,
213
234
  });
214
- return response;
235
+ return { ...response, cryptoFundResult };
215
236
  }
216
237
  catch (error) {
217
238
  // Store the last encountered error and status for re-throwing after retries
@@ -289,7 +310,7 @@ class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadServ
289
310
  */
290
311
  async uploadFolder(params) {
291
312
  this.logger.debug('Uploading folder...', { params });
292
- const { dataItemOpts, signal, manifestOptions = {}, maxConcurrentUploads = 1, throwOnFailure = true, maxChunkConcurrency, chunkByteCount, chunkingMode, maxFinalizeMs, } = params;
313
+ const { dataItemOpts, signal, manifestOptions = {}, maxConcurrentUploads = 1, throwOnFailure = true, maxChunkConcurrency, chunkByteCount, chunkingMode, fundingMode = new types_js_1.ExistingBalanceFunding(), maxFinalizeMs, } = params;
293
314
  const { disableManifest, indexFile, fallbackFile } = manifestOptions;
294
315
  const paths = {};
295
316
  const response = {
@@ -331,6 +352,16 @@ class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadServ
331
352
  };
332
353
  const files = await this.getFiles(params);
333
354
  const limit = (0, plimit_lit_1.pLimit)(maxConcurrentUploads);
355
+ let cryptoFundResult;
356
+ if (fundingMode instanceof types_js_1.OnDemandFunding) {
357
+ const totalByteCount = files.reduce((acc, file) => {
358
+ return acc + this.getFileSize(file) + 1200; // allow extra per file for ANS-104 headers
359
+ }, 0);
360
+ cryptoFundResult = await this.onDemand({
361
+ totalByteCount,
362
+ onDemandFunding: fundingMode,
363
+ });
364
+ }
334
365
  await Promise.all(files.map((file) => limit(() => uploadFile(file))));
335
366
  this.logger.debug('Finished uploading files', {
336
367
  numFiles: files.length,
@@ -370,6 +401,7 @@ class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadServ
370
401
  ...response,
371
402
  manifest,
372
403
  manifestResponse,
404
+ cryptoFundResult,
373
405
  };
374
406
  }
375
407
  async shareCredits({ approvedAddress, approvedWincAmount, expiresBySeconds, }) {
@@ -424,5 +456,91 @@ class TurboAuthenticatedBaseUploadService extends TurboUnauthenticatedUploadServ
424
456
  }
425
457
  return revokedApprovals;
426
458
  }
459
+ /**
460
+ * Triggers an upload that will top-up the wallet with Credits for the amount before uploading.
461
+ * First, it calculates the expected cost of the upload. Next, it checks the wallet for existing
462
+ * balance. If the balance is insufficient, it will attempt the top-up with the wallet in the specified `token`
463
+ * and await for the balance to be credited.
464
+ * Note: Only `ario`, `solana`, and `base-eth` tokens are currently supported for on-demand uploads.
465
+ */
466
+ async onDemand({ totalByteCount, onDemandFunding, }) {
467
+ const { maxTokenAmount, topUpBufferMultiplier } = onDemandFunding;
468
+ const currentBalance = await this.paymentService.getBalance();
469
+ const wincPriceForOneGiB = (await this.paymentService.getUploadCosts({
470
+ bytes: [2 ** 30],
471
+ }))[0].winc;
472
+ const expectedWincPrice = new bignumber_js_1.BigNumber(wincPriceForOneGiB)
473
+ .multipliedBy(totalByteCount)
474
+ .dividedBy(2 ** 30)
475
+ .toFixed(0, bignumber_js_1.BigNumber.ROUND_UP);
476
+ if ((0, bignumber_js_1.BigNumber)(currentBalance.effectiveBalance).isGreaterThanOrEqualTo(expectedWincPrice)) {
477
+ this.logger.debug('Sufficient balance for on demand upload', {
478
+ currentBalance,
479
+ expectedWincPrice,
480
+ });
481
+ return undefined;
482
+ }
483
+ this.logger.debug('Insufficient balance for on demand upload', {
484
+ currentBalance,
485
+ expectedWincPrice,
486
+ });
487
+ if (!this.enabledOnDemandTokens.includes(this.token)) {
488
+ throw new Error(`On-demand uploads are not supported for token: ${this.token}`);
489
+ }
490
+ const topUpWincAmount = (0, bignumber_js_1.BigNumber)(expectedWincPrice)
491
+ .minus(currentBalance.effectiveBalance)
492
+ .multipliedBy(topUpBufferMultiplier) // add buffer to avoid underpayment
493
+ .toFixed(0, bignumber_js_1.BigNumber.ROUND_UP);
494
+ const wincPriceForOneToken = (await this.paymentService.getWincForToken({
495
+ tokenAmount: index_js_1.tokenToBaseMap[this.token](1),
496
+ })).winc;
497
+ const topUpTokenAmount = new bignumber_js_1.BigNumber(topUpWincAmount)
498
+ .dividedBy(wincPriceForOneToken)
499
+ .multipliedBy(index_js_1.tokenToBaseMap[this.token](1))
500
+ .toFixed(0, bignumber_js_1.BigNumber.ROUND_UP);
501
+ if (maxTokenAmount !== undefined) {
502
+ if (new bignumber_js_1.BigNumber(topUpTokenAmount).isGreaterThan(maxTokenAmount)) {
503
+ throw new Error(`Top up token amount ${new bignumber_js_1.BigNumber(topUpTokenAmount).div(index_js_1.exponentMap[this.token])} is greater than the maximum allowed amount of ${maxTokenAmount}`);
504
+ }
505
+ }
506
+ this.logger.debug(`Topping up wallet with ${topUpTokenAmount} ${this.token} for ${topUpWincAmount} winc`);
507
+ const topUpResponse = await this.paymentService.topUpWithTokens({
508
+ tokenAmount: topUpTokenAmount,
509
+ });
510
+ this.logger.debug('Top up transaction submitted', { topUpResponse });
511
+ const pollingOptions = {
512
+ pollIntervalMs: 3 * 1000, // poll every 3 seconds
513
+ timeoutMs: 120 * 1000, // wait up to 2 minutes
514
+ };
515
+ let tries = 1;
516
+ const maxTries = Math.ceil(pollingOptions.timeoutMs / pollingOptions.pollIntervalMs) - 1; // -1 because we already tried once with the initial request
517
+ while (topUpResponse.status !== 'confirmed' && tries < maxTries) {
518
+ this.logger.debug('Tx not yet confirmed, waiting to poll again', {
519
+ tries,
520
+ maxTries,
521
+ });
522
+ await (0, common_js_1.sleep)(pollingOptions.pollIntervalMs);
523
+ tries++;
524
+ try {
525
+ const submitFundResult = await this.paymentService.submitFundTransaction({
526
+ txId: topUpResponse.id,
527
+ });
528
+ if (submitFundResult.status === 'confirmed') {
529
+ this.logger.debug('Top-up transaction confirmed and balance updated', { submitFundResult });
530
+ topUpResponse.status = 'confirmed';
531
+ break;
532
+ }
533
+ }
534
+ catch (error) {
535
+ this.logger.warn('Error fetching fund transaction during polling', {
536
+ message: error instanceof Error ? error.message : error,
537
+ });
538
+ }
539
+ }
540
+ if (tries >= maxTries) {
541
+ this.logger.warn('Timed out waiting for fund tx to confirm after top-up. Will continue to attempt upload but it may fail if balance is insufficient.');
542
+ }
543
+ return topUpResponse;
544
+ }
427
545
  }
428
546
  exports.TurboAuthenticatedBaseUploadService = TurboAuthenticatedBaseUploadService;
@@ -23,13 +23,14 @@ const stream_1 = require("stream");
23
23
  const upload_js_1 = require("../common/upload.js");
24
24
  const types_js_1 = require("../types.js");
25
25
  class TurboAuthenticatedUploadService extends upload_js_1.TurboAuthenticatedBaseUploadService {
26
- constructor({ url = upload_js_1.defaultUploadServiceURL, retryConfig, signer, logger, token, }) {
26
+ constructor({ url = upload_js_1.defaultUploadServiceURL, retryConfig, signer, logger, token, paymentService, }) {
27
27
  super({
28
28
  url,
29
29
  retryConfig,
30
30
  logger,
31
31
  token,
32
32
  signer,
33
+ paymentService,
33
34
  });
34
35
  }
35
36
  async getAbsoluteFilePathsFromFolder(folderPath) {
package/lib/cjs/types.js CHANGED
@@ -1,11 +1,12 @@
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.tokenTypes = exports.fiatCurrencyTypes = void 0;
3
+ exports.validChunkingModes = exports.isJWK = exports.isWebUploadFolderParams = exports.isNodeUploadFolderParams = exports.multipartFinalizedStatus = exports.multipartFailedStatus = exports.multipartPendingStatus = exports.OnDemandFunding = exports.ExistingBalanceFunding = exports.tokenTypes = exports.fiatCurrencyTypes = void 0;
4
4
  exports.isCurrency = isCurrency;
5
5
  exports.isKyvePrivateKey = isKyvePrivateKey;
6
6
  exports.isEthPrivateKey = isEthPrivateKey;
7
7
  exports.isSolanaWalletAdapter = isSolanaWalletAdapter;
8
8
  exports.isEthereumWalletAdapter = isEthereumWalletAdapter;
9
+ const bignumber_js_1 = require("bignumber.js");
9
10
  exports.fiatCurrencyTypes = [
10
11
  'usd',
11
12
  'eur',
@@ -31,6 +32,24 @@ exports.tokenTypes = [
31
32
  'pol',
32
33
  'base-eth',
33
34
  ];
35
+ class ExistingBalanceFunding {
36
+ }
37
+ exports.ExistingBalanceFunding = ExistingBalanceFunding;
38
+ class OnDemandFunding {
39
+ constructor({ maxTokenAmount, topUpBufferMultiplier = 1.1, }) {
40
+ if (maxTokenAmount !== undefined &&
41
+ new bignumber_js_1.BigNumber(maxTokenAmount).isLessThan(0)) {
42
+ throw new Error('maxTokenAmount must be non-negative');
43
+ }
44
+ this.maxTokenAmount =
45
+ maxTokenAmount !== undefined ? new bignumber_js_1.BigNumber(maxTokenAmount) : undefined;
46
+ if (topUpBufferMultiplier < 1) {
47
+ throw new Error('topUpBufferMultiplier must be >= 1');
48
+ }
49
+ this.topUpBufferMultiplier = topUpBufferMultiplier;
50
+ }
51
+ }
52
+ exports.OnDemandFunding = OnDemandFunding;
34
53
  exports.multipartPendingStatus = [
35
54
  'ASSEMBLING',
36
55
  '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.31.1';
20
+ exports.version = '1.32.0';
@@ -19,8 +19,8 @@ exports.TurboAuthenticatedUploadService = void 0;
19
19
  const upload_js_1 = require("../common/upload.js");
20
20
  const types_js_1 = require("../types.js");
21
21
  class TurboAuthenticatedUploadService extends upload_js_1.TurboAuthenticatedBaseUploadService {
22
- constructor({ url = upload_js_1.defaultUploadServiceURL, retryConfig, signer, logger, token, }) {
23
- super({ url, retryConfig, logger, token, signer });
22
+ constructor({ url = upload_js_1.defaultUploadServiceURL, retryConfig, signer, logger, token, paymentService, }) {
23
+ super({ url, retryConfig, logger, token, signer, paymentService });
24
24
  }
25
25
  getFiles(params) {
26
26
  if (!(0, types_js_1.isWebUploadFolderParams)(params)) {
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { createReadStream, statSync } from 'fs';
17
17
  import { turboCliTags } from '../constants.js';
18
- import { getChunkingOptions, getTagsFromOptions, paidByFromOptions, turboFromOptions, } from '../utils.js';
18
+ import { getChunkingOptions, getTagsFromOptions, onDemandOptionsFromOptions, paidByFromOptions, turboFromOptions, } from '../utils.js';
19
19
  export async function uploadFile(options) {
20
20
  const { filePath } = options;
21
21
  if (filePath === undefined) {
@@ -30,6 +30,7 @@ export async function uploadFile(options) {
30
30
  fileSizeFactory: () => fileSize,
31
31
  dataItemOpts: { tags: [...turboCliTags, ...customTags], paidBy },
32
32
  ...getChunkingOptions(options),
33
+ ...onDemandOptionsFromOptions(options),
33
34
  });
34
35
  console.log('Uploaded file:', JSON.stringify(result, null, 2));
35
36
  }
@@ -14,7 +14,7 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import { turboCliTags } from '../constants.js';
17
- import { getTagsFromOptions, getUploadFolderOptions, paidByFromOptions, turboFromOptions, } from '../utils.js';
17
+ import { getTagsFromOptions, getUploadFolderOptions, onDemandOptionsFromOptions, paidByFromOptions, turboFromOptions, } from '../utils.js';
18
18
  export async function uploadFolder(options) {
19
19
  const turbo = await turboFromOptions(options);
20
20
  const paidBy = await paidByFromOptions(options, turbo);
@@ -33,6 +33,7 @@ export async function uploadFolder(options) {
33
33
  chunkingMode,
34
34
  maxChunkConcurrency,
35
35
  maxFinalizeMs,
36
+ ...onDemandOptionsFromOptions(options),
36
37
  });
37
38
  console.log('Uploaded folder:', JSON.stringify(result, null, 2));
38
39
  }
@@ -173,6 +173,19 @@ export const optionMap = {
173
173
  description: 'Chunking mode to use for the upload. Can be "auto", "force" or "disabled". Defaults to "auto".',
174
174
  default: 'auto',
175
175
  },
176
+ onDemand: {
177
+ alias: '--on-demand',
178
+ description: 'Enable on-demand crypto top-ups during upload if balance is insufficient',
179
+ default: false,
180
+ },
181
+ topUpBufferMultiplier: {
182
+ alias: '--top-up-buffer-multiplier <topUpBufferMultiplier>',
183
+ description: 'Multiplier to apply to the estimated top-up amount to avoid underpayment during on-demand top-ups. Defaults to 1.1 (10% buffer).',
184
+ },
185
+ maxCryptoTopUpValue: {
186
+ alias: '--max-crypto-top-up-value <maxCryptoTopUpValue>',
187
+ description: 'Maximum crypto top-up value to use for the upload. Defaults to no limit.',
188
+ },
176
189
  };
177
190
  export const walletOptions = [
178
191
  optionMap.walletFile,
@@ -190,6 +203,11 @@ export const globalOptions = [
190
203
  optionMap.paymentUrl,
191
204
  optionMap.uploadUrl,
192
205
  ];
206
+ const onDemandOptions = [
207
+ optionMap.onDemand,
208
+ optionMap.topUpBufferMultiplier,
209
+ optionMap.maxCryptoTopUpValue,
210
+ ];
193
211
  export const uploadOptions = [
194
212
  ...walletOptions,
195
213
  optionMap.paidBy,
@@ -200,6 +218,7 @@ export const uploadOptions = [
200
218
  optionMap.maxFinalizeMs,
201
219
  optionMap.chunkByteCount,
202
220
  optionMap.chunkingMode,
221
+ ...onDemandOptions,
203
222
  ];
204
223
  export const uploadFolderOptions = [
205
224
  ...uploadOptions,