@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.
- package/README.md +28 -0
- package/bundles/web.bundle.min.js +437 -258
- package/lib/cjs/cli/commands/uploadFile.js +1 -0
- package/lib/cjs/cli/commands/uploadFolder.js +1 -0
- package/lib/cjs/cli/options.js +19 -0
- package/lib/cjs/cli/utils.js +25 -0
- package/lib/cjs/common/factory.js +1 -0
- package/lib/cjs/common/payment.js +1 -1
- package/lib/cjs/common/token/ario.js +1 -1
- package/lib/cjs/common/token/arweave.js +1 -1
- package/lib/cjs/common/token/baseEth.js +17 -1
- package/lib/cjs/common/token/ethereum.js +13 -4
- package/lib/cjs/common/token/kyve.js +1 -1
- package/lib/cjs/common/token/solana.js +1 -1
- package/lib/cjs/common/turbo.js +3 -1
- package/lib/cjs/common/upload.js +124 -6
- package/lib/cjs/node/upload.js +2 -1
- package/lib/cjs/types.js +20 -1
- package/lib/cjs/version.js +1 -1
- package/lib/cjs/web/upload.js +2 -2
- package/lib/esm/cli/commands/uploadFile.js +2 -1
- package/lib/esm/cli/commands/uploadFolder.js +2 -1
- package/lib/esm/cli/options.js +19 -0
- package/lib/esm/cli/utils.js +25 -1
- package/lib/esm/common/factory.js +1 -0
- package/lib/esm/common/payment.js +1 -1
- package/lib/esm/common/token/ario.js +1 -1
- package/lib/esm/common/token/arweave.js +1 -1
- package/lib/esm/common/token/baseEth.js +17 -1
- package/lib/esm/common/token/ethereum.js +13 -4
- package/lib/esm/common/token/kyve.js +1 -1
- package/lib/esm/common/token/solana.js +1 -1
- package/lib/esm/common/turbo.js +3 -1
- package/lib/esm/common/upload.js +124 -6
- package/lib/esm/node/upload.js +2 -1
- package/lib/esm/types.js +17 -0
- package/lib/esm/version.js +1 -1
- package/lib/esm/web/upload.js +2 -2
- package/lib/types/cli/commands/uploadFile.d.ts.map +1 -1
- package/lib/types/cli/commands/uploadFolder.d.ts.map +1 -1
- package/lib/types/cli/options.d.ts +43 -0
- package/lib/types/cli/options.d.ts.map +1 -1
- package/lib/types/cli/types.d.ts +4 -0
- package/lib/types/cli/types.d.ts.map +1 -1
- package/lib/types/cli/utils.d.ts +4 -1
- package/lib/types/cli/utils.d.ts.map +1 -1
- package/lib/types/common/factory.d.ts +4 -1
- package/lib/types/common/factory.d.ts.map +1 -1
- package/lib/types/common/token/ario.d.ts +1 -1
- package/lib/types/common/token/ario.d.ts.map +1 -1
- package/lib/types/common/token/arweave.d.ts +1 -1
- package/lib/types/common/token/arweave.d.ts.map +1 -1
- package/lib/types/common/token/baseEth.d.ts +1 -0
- package/lib/types/common/token/baseEth.d.ts.map +1 -1
- package/lib/types/common/token/ethereum.d.ts +2 -1
- package/lib/types/common/token/ethereum.d.ts.map +1 -1
- package/lib/types/common/token/kyve.d.ts +1 -1
- package/lib/types/common/token/kyve.d.ts.map +1 -1
- package/lib/types/common/token/solana.d.ts +1 -1
- package/lib/types/common/token/solana.d.ts.map +1 -1
- package/lib/types/common/turbo.d.ts +2 -2
- package/lib/types/common/turbo.d.ts.map +1 -1
- package/lib/types/common/upload.d.ts +16 -3
- package/lib/types/common/upload.d.ts.map +1 -1
- package/lib/types/node/factory.d.ts +4 -1
- package/lib/types/node/factory.d.ts.map +1 -1
- package/lib/types/node/upload.d.ts +4 -1
- package/lib/types/node/upload.d.ts.map +1 -1
- package/lib/types/types.d.ts +19 -4
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/version.d.ts +1 -1
- package/lib/types/version.d.ts.map +1 -1
- package/lib/types/web/factory.d.ts +4 -1
- package/lib/types/web/factory.d.ts.map +1 -1
- package/lib/types/web/upload.d.ts +4 -1
- package/lib/types/web/upload.d.ts.map +1 -1
- 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
|
}
|
package/lib/cjs/cli/options.js
CHANGED
|
@@ -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,
|
package/lib/cjs/cli/utils.js
CHANGED
|
@@ -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)) {
|
|
@@ -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.
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
57
|
-
if (
|
|
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(
|
|
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
|
|
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
|
|
77
|
+
async pollTxAvailability({ txId }) {
|
|
78
78
|
const { maxAttempts, pollingIntervalMs, initialBackoffMs } = this.pollingOptions;
|
|
79
79
|
this.logger.debug('Polling for transaction...', {
|
|
80
80
|
txId,
|
package/lib/cjs/common/turbo.js
CHANGED
|
@@ -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) {
|
package/lib/cjs/common/upload.js
CHANGED
|
@@ -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;
|
package/lib/cjs/node/upload.js
CHANGED
|
@@ -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',
|
package/lib/cjs/version.js
CHANGED
package/lib/cjs/web/upload.js
CHANGED
|
@@ -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
|
}
|
package/lib/esm/cli/options.js
CHANGED
|
@@ -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,
|