permaweb-deploy 3.4.2 → 3.4.3

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.
@@ -0,0 +1,930 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { TurboFactory, ETHToTokenAmount, ARIOToTokenAmount, OnDemandFunding } from '@ardrive/turbo-sdk';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import { l as loadCache, u as uploadFile, c as cleanupCache, s as saveCache, a as uploadFolder } from './uploader-DDS_d-O_.js';
7
+ import { createHash } from 'node:crypto';
8
+ import { createRequire } from 'node:module';
9
+ import { Readable } from 'node:stream';
10
+ import { createDataItemSigner, message } from '@permaweb/aoconnect';
11
+ import { expandPath } from '../utils/path.js';
12
+ import { createSigner } from '../utils/signer.js';
13
+
14
+ class HyperbalanceError extends Error {
15
+ cause;
16
+ constructor(message, cause) {
17
+ super(message);
18
+ this.cause = cause;
19
+ this.name = "HyperbalanceError";
20
+ }
21
+ }
22
+ class MissingDiscoveryError extends HyperbalanceError {
23
+ attemptedUrls;
24
+ constructor(attemptedUrls) {
25
+ super(`No hyperbalance discovery endpoint found: ${attemptedUrls.join(", ")}`);
26
+ this.attemptedUrls = attemptedUrls;
27
+ this.name = "MissingDiscoveryError";
28
+ }
29
+ }
30
+ class PaymentRequiredError extends HyperbalanceError {
31
+ required;
32
+ available;
33
+ constructor(message, required, available) {
34
+ super(message);
35
+ this.required = required;
36
+ this.available = available;
37
+ this.name = "PaymentRequiredError";
38
+ }
39
+ }
40
+
41
+ const DEFAULT_DISCOVERY_PATHS = ["/.well-known/hyperbalance"];
42
+ function normalizeNodeUrl(nodeUrl) {
43
+ return nodeUrl.replace(/\/+$/, "");
44
+ }
45
+ async function discoverPaymentProfile(nodeUrl, options = {}) {
46
+ const fetcher = options.fetch ?? globalThis.fetch;
47
+ const base = normalizeNodeUrl(nodeUrl);
48
+ const paths = options.paths ?? DEFAULT_DISCOVERY_PATHS;
49
+ const attemptedUrls = [];
50
+ for (const path of paths) {
51
+ const url = `${base}${path.startsWith("/") ? path : `/${path}`}`;
52
+ attemptedUrls.push(url);
53
+ const response = await fetcher(url, { headers: { accept: "application/json" } }).catch(() => undefined);
54
+ if (!response?.ok)
55
+ continue;
56
+ const profile = (await response.json());
57
+ validateProfile(profile);
58
+ return profile;
59
+ }
60
+ throw new MissingDiscoveryError(attemptedUrls);
61
+ }
62
+ function validateProfile(profile) {
63
+ if (!profile || typeof profile !== "object") {
64
+ throw new Error("Invalid hyperbalance profile: expected object");
65
+ }
66
+ if (!Array.isArray(profile.ledgers)) {
67
+ throw new Error("Invalid hyperbalance profile: ledgers must be an array");
68
+ }
69
+ if (!Array.isArray(profile.tokens)) {
70
+ throw new Error("Invalid hyperbalance profile: tokens must be an array");
71
+ }
72
+ }
73
+
74
+ function getLedger(profile, ledgerId) {
75
+ const ledger = profile.ledgers.find((candidate) => candidate.id === ledgerId);
76
+ if (!ledger) {
77
+ throw new Error(`Ledger not found in payment profile: ${ledgerId}`);
78
+ }
79
+ return ledger;
80
+ }
81
+ function getToken(profile, tokenId) {
82
+ const token = profile.tokens.find((candidate) => candidate.id === tokenId);
83
+ if (!token) {
84
+ throw new Error(`Token not found in payment profile: ${tokenId}`);
85
+ }
86
+ return token;
87
+ }
88
+
89
+ async function parseBalanceResponse(response) {
90
+ const contentType = response.headers.get("content-type") ?? "";
91
+ const raw = await response.text();
92
+ if (contentType.includes("application/json")) {
93
+ const parsed = JSON.parse(raw);
94
+ return parseBalanceValue(parsed);
95
+ }
96
+ return BigInt(raw.trim());
97
+ }
98
+ async function parseQuoteResponse(response) {
99
+ const contentType = response.headers.get("content-type") ?? "";
100
+ const raw = await response.text();
101
+ if (contentType.includes("application/json")) {
102
+ const parsed = JSON.parse(raw);
103
+ const quote = parseQuoteValue(parsed);
104
+ return { ...quote, raw: parsed };
105
+ }
106
+ return { amount: BigInt(raw.trim()), raw };
107
+ }
108
+ function parseBalanceValue(value) {
109
+ if (typeof value === "bigint")
110
+ return value;
111
+ if (typeof value === "number")
112
+ return BigInt(value);
113
+ if (typeof value === "string")
114
+ return BigInt(value);
115
+ if (value && typeof value === "object") {
116
+ const record = value;
117
+ for (const key of ["balance", "value", "amount", "ok"]) {
118
+ if (key in record)
119
+ return parseBalanceValue(record[key]);
120
+ }
121
+ }
122
+ throw new Error("Could not parse ledger balance response");
123
+ }
124
+ function parseQuoteValue(value) {
125
+ if (typeof value === "bigint" || typeof value === "number" || typeof value === "string") {
126
+ return { amount: BigInt(value) };
127
+ }
128
+ if (value && typeof value === "object") {
129
+ const record = value;
130
+ const amountKey = ["amount", "price", "quote", "value", "ok"].find((key) => key in record);
131
+ if (!amountKey) {
132
+ throw new Error("Could not parse quote amount response");
133
+ }
134
+ const quote = parseQuoteValue(record[amountKey]);
135
+ const ledgerId = typeof record.ledgerId === "string" ? record.ledgerId : undefined;
136
+ const tokenId = typeof record.tokenId === "string" ? record.tokenId : undefined;
137
+ return {
138
+ amount: quote.amount,
139
+ ...(ledgerId !== undefined && { ledgerId }),
140
+ ...(tokenId !== undefined && { tokenId }),
141
+ };
142
+ }
143
+ throw new Error("Could not parse quote amount response");
144
+ }
145
+
146
+ function applyTemplate(template, values) {
147
+ return template.replaceAll(/\{([A-Za-z0-9_-]+)\}/g, (_match, key) => {
148
+ const value = values[key];
149
+ if (value === undefined) {
150
+ throw new Error(`Missing template value: ${key}`);
151
+ }
152
+ return encodeURIComponent(String(value));
153
+ });
154
+ }
155
+ function applyTemplateMap(templates, values) {
156
+ if (!templates)
157
+ return {};
158
+ return Object.fromEntries(Object.entries(templates).map(([key, template]) => [key, applyTemplate(template, values)]));
159
+ }
160
+
161
+ class HyperbalanceClient {
162
+ fetch;
163
+ nodeUrl;
164
+ constructor(options) {
165
+ this.fetch = options.fetch ?? globalThis.fetch;
166
+ this.nodeUrl = normalizeNodeUrl(options.nodeUrl);
167
+ }
168
+ discover(options = {}) {
169
+ return discoverPaymentProfile(this.nodeUrl, { ...options, fetch: this.fetch });
170
+ }
171
+ async getBalance(request) {
172
+ const ledger = getLedger(request.profile, request.ledgerId);
173
+ const path = applyTemplate(ledger.balancePath, { address: request.address });
174
+ const response = await this.fetch(this.absoluteUrl(path), {
175
+ headers: { accept: "application/json, text/plain" },
176
+ });
177
+ if (response.status === 404) {
178
+ return {
179
+ address: request.address,
180
+ ledger,
181
+ value: 0n,
182
+ };
183
+ }
184
+ if (!response.ok) {
185
+ throw new Error(`Balance request failed: ${response.status} ${response.statusText}`);
186
+ }
187
+ return {
188
+ address: request.address,
189
+ ledger,
190
+ value: await parseBalanceResponse(response),
191
+ };
192
+ }
193
+ async ensureCredit(request) {
194
+ const token = getToken(request.profile, request.tokenId);
195
+ const ledger = getLedger(request.profile, request.ledgerId);
196
+ const before = await this.getBalance({
197
+ address: request.recipient,
198
+ ledgerId: ledger.id,
199
+ profile: request.profile,
200
+ });
201
+ if (before.value >= request.minimumBalance) {
202
+ return {
203
+ after: before,
204
+ before,
205
+ shortfall: 0n,
206
+ };
207
+ }
208
+ const shortfall = request.minimumBalance - before.value;
209
+ const depositAddress = token.depositAddress ?? request.profile.node?.operator;
210
+ if (!depositAddress) {
211
+ throw new PaymentRequiredError("Payment is required, but the node did not advertise a deposit address", request.minimumBalance, before.value);
212
+ }
213
+ if (token.transfer?.kind && token.transfer.kind !== request.transferAdapter.kind) {
214
+ throw new Error(`Transfer adapter kind mismatch: token expects ${token.transfer.kind}, got ${request.transferAdapter.kind}`);
215
+ }
216
+ const sender = await request.transferAdapter.inferSender?.();
217
+ const transferRequest = {
218
+ amount: shortfall,
219
+ depositAddress,
220
+ recipient: request.recipient,
221
+ token,
222
+ };
223
+ if (sender !== undefined)
224
+ transferRequest.sender = sender;
225
+ const transfer = await request.transferAdapter.transfer(transferRequest);
226
+ const importRequest = {
227
+ amount: shortfall,
228
+ ledger,
229
+ profile: request.profile,
230
+ recipient: request.recipient,
231
+ token,
232
+ transfer,
233
+ };
234
+ const importSender = transfer.sender ?? sender;
235
+ if (importSender !== undefined)
236
+ importRequest.sender = importSender;
237
+ const imported = await this.importDeposit(importRequest);
238
+ const after = await this.getBalance({
239
+ address: request.recipient,
240
+ ledgerId: ledger.id,
241
+ profile: request.profile,
242
+ });
243
+ return {
244
+ after,
245
+ before,
246
+ imported,
247
+ shortfall,
248
+ transfer,
249
+ };
250
+ }
251
+ async ensureCreditAuto(request) {
252
+ const profile = request.profile ?? (await this.discover());
253
+ const targetOptions = {
254
+ transferKind: request.transferAdapter.kind,
255
+ };
256
+ if (request.ledgerId !== undefined)
257
+ targetOptions.ledgerId = request.ledgerId;
258
+ if (request.tokenId !== undefined)
259
+ targetOptions.tokenId = request.tokenId;
260
+ const { ledger, token } = selectFundingTarget(profile, targetOptions);
261
+ return this.ensureCredit({
262
+ ledgerId: ledger.id,
263
+ minimumBalance: request.minimumBalance,
264
+ profile,
265
+ recipient: request.recipient,
266
+ tokenId: token.id,
267
+ transferAdapter: request.transferAdapter,
268
+ });
269
+ }
270
+ async quote(request) {
271
+ const descriptor = request.profile.pricing?.find((candidate) => candidate.action === request.action);
272
+ if (!descriptor?.quotePath) {
273
+ throw new Error(`No quote path advertised for action: ${request.action}`);
274
+ }
275
+ const values = request.params ?? {};
276
+ const url = new URL(this.absoluteUrl(applyTemplate(descriptor.quotePath, values)));
277
+ for (const [key, value] of Object.entries(applyTemplateMap(descriptor.query, values))) {
278
+ url.searchParams.set(key, value);
279
+ }
280
+ const bodyValues = applyTemplateMap(descriptor.body, values);
281
+ const hasBody = Object.keys(bodyValues).length > 0;
282
+ const init = {
283
+ headers: { accept: "application/json, text/plain" },
284
+ method: descriptor.method ?? (hasBody ? "POST" : "GET"),
285
+ };
286
+ if (hasBody) {
287
+ init.body = JSON.stringify(bodyValues);
288
+ init.headers = { ...init.headers, "content-type": "application/json" };
289
+ }
290
+ const response = await this.fetch(url, init);
291
+ if (!response.ok) {
292
+ throw new Error(`Quote request failed: ${response.status} ${response.statusText}`);
293
+ }
294
+ return parseQuoteResponse(response);
295
+ }
296
+ async quoteAuto(request) {
297
+ const quoteRequest = {
298
+ action: request.action,
299
+ profile: request.profile ?? (await this.discover()),
300
+ };
301
+ if (request.params !== undefined)
302
+ quoteRequest.params = request.params;
303
+ return this.quote(quoteRequest);
304
+ }
305
+ async importDeposit(request) {
306
+ const descriptor = request.token.import;
307
+ if (!descriptor) {
308
+ throw new Error(`Token does not advertise a deposit import flow: ${request.token.id}`);
309
+ }
310
+ const values = {
311
+ ledgerId: request.ledger.id,
312
+ messageId: request.transfer.messageId,
313
+ quantity: request.amount,
314
+ recipient: request.recipient,
315
+ sender: request.sender,
316
+ slot: request.transfer.slot,
317
+ tokenId: request.token.id,
318
+ };
319
+ const url = new URL(this.absoluteUrl(descriptor.path));
320
+ for (const [key, value] of Object.entries(applyTemplateMap(descriptor.query, values))) {
321
+ url.searchParams.set(key, value);
322
+ }
323
+ const bodyValues = applyTemplateMap(descriptor.body, values);
324
+ const hasBody = Object.keys(bodyValues).length > 0;
325
+ const init = {
326
+ method: descriptor.method ?? "POST",
327
+ };
328
+ if (hasBody) {
329
+ init.body = JSON.stringify(bodyValues);
330
+ init.headers = { "content-type": "application/json" };
331
+ }
332
+ const response = await this.fetch(url, init);
333
+ if (!response.ok) {
334
+ throw new Error(`Deposit import failed: ${response.status} ${response.statusText}`);
335
+ }
336
+ const text = await response.text();
337
+ if (!text.trim())
338
+ return undefined;
339
+ try {
340
+ return JSON.parse(text);
341
+ }
342
+ catch {
343
+ return text;
344
+ }
345
+ }
346
+ absoluteUrl(pathOrUrl) {
347
+ if (/^https?:\/\//.test(pathOrUrl))
348
+ return pathOrUrl;
349
+ return `${this.nodeUrl}${pathOrUrl.startsWith("/") ? pathOrUrl : `/${pathOrUrl}`}`;
350
+ }
351
+ }
352
+ function selectFundingTarget(profile, options = {}) {
353
+ const token = options.tokenId
354
+ ? getToken(profile, options.tokenId)
355
+ : profile.tokens.find((candidate) => (!options.transferKind || candidate.transfer?.kind === options.transferKind) &&
356
+ (!options.ledgerId || candidate.ledgerId === options.ledgerId));
357
+ if (!token) {
358
+ throw new Error("No matching funding token found in payment profile");
359
+ }
360
+ const ledgerId = options.ledgerId ?? token.ledgerId;
361
+ if (!ledgerId) {
362
+ throw new Error(`Funding token does not specify a ledger: ${token.id}`);
363
+ }
364
+ return {
365
+ ledger: getLedger(profile, ledgerId),
366
+ token,
367
+ };
368
+ }
369
+
370
+ class AoTokenTransferAdapter {
371
+ options;
372
+ kind = "ao";
373
+ inferSender;
374
+ constructor(options) {
375
+ this.options = options;
376
+ if (options.inferSender) {
377
+ this.inferSender = options.inferSender;
378
+ }
379
+ }
380
+ async transfer(request) {
381
+ const transfer = request.token.transfer;
382
+ assertAoTransferDescriptor(request.token, transfer);
383
+ const tags = buildAoTransferTags(request, transfer);
384
+ const messageId = await this.options.message({
385
+ data: "",
386
+ process: transfer.processId,
387
+ tags,
388
+ });
389
+ const slot = await this.options.waitForAssignmentSlot?.(messageId, {
390
+ processId: transfer.processId,
391
+ });
392
+ const result = { messageId };
393
+ if (request.sender !== undefined)
394
+ result.sender = request.sender;
395
+ if (slot !== undefined)
396
+ result.slot = slot;
397
+ return result;
398
+ }
399
+ }
400
+ async function waitForAoAssignmentSlot(options) {
401
+ const fetcher = options.fetch ?? globalThis.fetch;
402
+ const stateUrl = (options.stateUrl ?? "https://state.forward.computer").replace(/\/+$/, "");
403
+ const pollMs = options.pollMs ?? 5000;
404
+ const timeoutMs = options.timeoutMs ?? 360000;
405
+ const fromSlot = Math.max(0, (await currentSlot(fetcher, stateUrl, options.processId)) - 5);
406
+ const deadline = Date.now() + timeoutMs;
407
+ while (Date.now() < deadline) {
408
+ const toSlot = (await currentSlot(fetcher, stateUrl, options.processId)) + 20;
409
+ const url = `${stateUrl}/${options.processId}~process@1.0/schedule` +
410
+ `?from=${fromSlot}&to=${toSlot}&accept=application/aos-2`;
411
+ const response = await fetcher(url);
412
+ if (!response.ok) {
413
+ throw new Error(`AO schedule request failed: ${response.status} ${response.statusText}`);
414
+ }
415
+ const schedule = (await response.json());
416
+ for (const edge of schedule.edges ?? []) {
417
+ if (edge.node?.message?.Id === options.messageId) {
418
+ const slot = edge.node.assignment?.Tags?.find((tag) => tag.name === "Nonce")?.value;
419
+ if (!slot) {
420
+ throw new Error(`AO assignment is missing Nonce for message ${options.messageId}`);
421
+ }
422
+ return slot;
423
+ }
424
+ }
425
+ await sleep(pollMs);
426
+ }
427
+ throw new Error(`AO message did not appear in schedule: ${options.messageId}`);
428
+ }
429
+ async function currentSlot(fetcher, stateUrl, processId) {
430
+ const response = await fetcher(`${stateUrl}/${processId}~process@1.0/slot/current`);
431
+ if (!response.ok) {
432
+ throw new Error(`AO current slot request failed: ${response.status} ${response.statusText}`);
433
+ }
434
+ const text = await response.text();
435
+ const match = text.match(/\d+/);
436
+ if (!match) {
437
+ throw new Error(`Could not parse AO current slot: ${text}`);
438
+ }
439
+ return Number(match[0]);
440
+ }
441
+ function sleep(ms) {
442
+ return new Promise((resolve) => setTimeout(resolve, ms));
443
+ }
444
+ function buildAoTransferTags(request, transfer) {
445
+ const tags = applyTemplateMap(transfer.tags, {
446
+ depositAddress: request.depositAddress,
447
+ quantity: request.amount,
448
+ recipient: request.recipient,
449
+ sender: request.sender,
450
+ tokenId: request.token.id,
451
+ });
452
+ return Object.entries(tags).map(([name, value]) => ({ name, value }));
453
+ }
454
+ function assertAoTransferDescriptor(token, transfer) {
455
+ if (!transfer) {
456
+ throw new Error(`AO token does not advertise a transfer flow: ${token.id}`);
457
+ }
458
+ if (transfer.kind !== "ao") {
459
+ throw new Error(`Token transfer kind is not AO: ${transfer.kind}`);
460
+ }
461
+ if (!transfer.processId) {
462
+ throw new Error(`AO token is missing transfer.processId: ${token.id}`);
463
+ }
464
+ }
465
+
466
+ const DEFAULT_AO_TOKEN_ID = "0syT13r0s0tgPmIed95bJnuSqaD29HQNN8D3ElLSrsc";
467
+ const HYPERBEAM_AO_BUNDLER_QUOTE_ACTION = "hyperbeam-upload";
468
+ const HYPERBEAM_DEFAULT_LEDGER_ID = "default";
469
+ const HYPERBEAM_DEFAULT_LEDGER_ROUTE = "/ledger~node-process@1.0";
470
+ async function discoverHyperbeamAoBundlerProfile(options) {
471
+ const fetcher = options.fetch ?? globalThis.fetch;
472
+ const nodeUrl = normalizeNodeUrl(options.nodeUrl);
473
+ const ledgerId = options.ledgerId ?? HYPERBEAM_DEFAULT_LEDGER_ID;
474
+ const ledgerRoute = options.ledgerRoute ?? HYPERBEAM_DEFAULT_LEDGER_ROUTE;
475
+ const tokenId = options.tokenId ?? DEFAULT_AO_TOKEN_ID;
476
+ const depositAddress = await fetchHyperbeamOperatorAddress(fetcher, nodeUrl);
477
+ return {
478
+ ledgers: [
479
+ {
480
+ balancePath: `${ledgerRoute}/now/balance/{address}`,
481
+ id: ledgerId,
482
+ route: ledgerRoute,
483
+ type: "process-ledger@1.0",
484
+ unit: "AO",
485
+ },
486
+ ],
487
+ node: {
488
+ operator: depositAddress,
489
+ url: nodeUrl,
490
+ },
491
+ pricing: [
492
+ {
493
+ action: HYPERBEAM_AO_BUNDLER_QUOTE_ACTION,
494
+ query: {
495
+ amount: "{bytes}",
496
+ resource: "arweave-bytes",
497
+ },
498
+ quotePath: "/~metering@1.0/quote",
499
+ },
500
+ ],
501
+ tokens: [
502
+ {
503
+ decimals: 12,
504
+ depositAddress,
505
+ id: tokenId,
506
+ import: {
507
+ method: "POST",
508
+ path: "/~ao-payment@1.0/ingest",
509
+ query: {
510
+ "message-id": "{messageId}",
511
+ quantity: "{quantity}",
512
+ recipient: "{recipient}",
513
+ sender: "{sender}",
514
+ slot: "{slot}",
515
+ token: "{tokenId}",
516
+ },
517
+ },
518
+ ledgerId,
519
+ network: "ao",
520
+ ticker: "AO",
521
+ transfer: {
522
+ kind: "ao",
523
+ processId: tokenId,
524
+ tags: {
525
+ Action: "Transfer",
526
+ Quantity: "{quantity}",
527
+ Recipient: "{depositAddress}",
528
+ },
529
+ },
530
+ },
531
+ ],
532
+ version: "hyperbalance@0.1",
533
+ };
534
+ }
535
+ async function fetchHyperbeamOperatorAddress(fetcher, nodeUrl) {
536
+ const response = await fetcher(`${nodeUrl}/~meta@1.0/info/address`, {
537
+ headers: { accept: "text/plain" },
538
+ });
539
+ if (!response.ok) {
540
+ throw new Error(`HyperBEAM operator address request failed: ${response.status} ${response.statusText}`);
541
+ }
542
+ const address = (await response.text()).trim();
543
+ if (!address) {
544
+ throw new Error("HyperBEAM operator address response was empty");
545
+ }
546
+ return address;
547
+ }
548
+
549
+ const require$1 = createRequire(import.meta.url);
550
+ const { ArweaveSigner, DataItem, createData } = require$1("@dha-team/arbundles");
551
+ async function readableToBuffer(stream) {
552
+ const chunks = [];
553
+ for await (const chunk of stream) {
554
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
555
+ }
556
+ return Buffer.concat(chunks);
557
+ }
558
+ async function streamToBuffer(stream) {
559
+ if (Buffer.isBuffer(stream)) {
560
+ return stream;
561
+ }
562
+ if (stream instanceof Uint8Array) {
563
+ return Buffer.from(stream);
564
+ }
565
+ if (stream instanceof Readable) {
566
+ return readableToBuffer(stream);
567
+ }
568
+ if (stream && typeof stream.getReader === "function") {
569
+ return readableToBuffer(Readable.fromWeb(stream));
570
+ }
571
+ throw new Error("Unsupported upload stream type");
572
+ }
573
+ function toBase64Url(value) {
574
+ if (typeof value === "string") {
575
+ return value;
576
+ }
577
+ return Buffer.from(value).toString("base64url");
578
+ }
579
+ function normalizeUploadUrl(base, uploadPath) {
580
+ const normalizedBase = base.endsWith("/") ? base : `${base}/`;
581
+ const cleanPath = uploadPath.startsWith("/") ? uploadPath.slice(1) : uploadPath;
582
+ return new URL(cleanPath, normalizedBase).toString();
583
+ }
584
+ function arweaveAddressFromJwk(jwk) {
585
+ if (typeof jwk.n !== "string") {
586
+ throw new TypeError('Arweave JWK is missing modulus field "n"');
587
+ }
588
+ return createHash("sha256").update(Buffer.from(jwk.n, "base64url")).digest("base64url");
589
+ }
590
+ function parseHyperbeamFundAmount(value) {
591
+ if (!/^[1-9]\d*$/.test(value)) {
592
+ throw new Error("--hyperbeam-fund-amount must be a positive integer in token base units");
593
+ }
594
+ return BigInt(value);
595
+ }
596
+ async function ensureHyperbeamCredit(options, profile) {
597
+ const jwk = JSON.parse(Buffer.from(options.deployKey, "base64").toString("utf8"));
598
+ const recipient = arweaveAddressFromJwk(jwk);
599
+ const signer = createDataItemSigner(jwk);
600
+ const client = new HyperbalanceClient({ nodeUrl: options.uploader });
601
+ const adapter = new AoTokenTransferAdapter({
602
+ async inferSender() {
603
+ return recipient;
604
+ },
605
+ async message(input) {
606
+ return message({
607
+ data: input.data ?? "",
608
+ process: input.process,
609
+ signer,
610
+ tags: input.tags
611
+ });
612
+ },
613
+ async waitForAssignmentSlot(messageId, context) {
614
+ return waitForAoAssignmentSlot({
615
+ messageId,
616
+ pollMs: options.aoPollMs,
617
+ processId: context.processId,
618
+ stateUrl: options.aoStateUrl,
619
+ timeoutMs: options.aoTimeoutMs
620
+ });
621
+ }
622
+ });
623
+ return client.ensureCreditAuto({
624
+ ledgerId: options.ledgerId,
625
+ minimumBalance: options.minimumBalance,
626
+ profile,
627
+ recipient,
628
+ tokenId: options.tokenId ?? DEFAULT_AO_TOKEN_ID,
629
+ transferAdapter: adapter
630
+ });
631
+ }
632
+ async function autoFundQuotedHyperbeamLedger(options) {
633
+ const profile = await discoverHyperbeamAoBundlerProfile({
634
+ ledgerId: options.ledgerId,
635
+ nodeUrl: options.uploader,
636
+ tokenId: options.tokenId
637
+ });
638
+ const client = new HyperbalanceClient({ nodeUrl: options.uploader });
639
+ let { ledgerId } = options;
640
+ let { minimumBalance } = options;
641
+ let { tokenId } = options;
642
+ if (minimumBalance === void 0) {
643
+ const quote = await client.quoteAuto({
644
+ action: options.quoteAction ?? "hyperbeam-upload",
645
+ params: { bytes: options.signedBytes },
646
+ profile
647
+ });
648
+ minimumBalance = quote.amount;
649
+ ledgerId ??= quote.ledgerId;
650
+ tokenId ??= quote.tokenId;
651
+ }
652
+ return ensureHyperbeamCredit(
653
+ {
654
+ aoPollMs: options.aoPollMs,
655
+ aoStateUrl: options.aoStateUrl,
656
+ aoTimeoutMs: options.aoTimeoutMs,
657
+ deployKey: options.deployKey,
658
+ ledgerId,
659
+ minimumBalance,
660
+ tokenId,
661
+ uploader: options.uploader
662
+ },
663
+ profile
664
+ );
665
+ }
666
+ function hyperbeamBundlerLink(uploader, id) {
667
+ const normalizedBase = uploader.endsWith("/") ? uploader : `${uploader}/`;
668
+ return new URL(`~arweave@2.9/raw=${encodeURIComponent(id)}`, normalizedBase).toString();
669
+ }
670
+ function responseId(headers, body) {
671
+ const headerId = headers.get("id");
672
+ if (headerId) {
673
+ return headerId;
674
+ }
675
+ try {
676
+ const parsed = JSON.parse(body);
677
+ return parsed.id || parsed.body?.id;
678
+ } catch {
679
+ return void 0;
680
+ }
681
+ }
682
+ class HyperbeamBundlerClient {
683
+ autoFund;
684
+ signer;
685
+ uploader;
686
+ uploadUrl;
687
+ constructor({ autoFund, deployKey, uploadPath, uploader }) {
688
+ const jwk = JSON.parse(Buffer.from(deployKey, "base64").toString("utf8"));
689
+ this.autoFund = autoFund;
690
+ this.signer = new ArweaveSigner(jwk);
691
+ this.uploader = uploader;
692
+ this.uploadUrl = normalizeUploadUrl(uploader, uploadPath);
693
+ }
694
+ async uploadFile(args) {
695
+ const data = args.file ? typeof args.file === "string" ? fs.readFileSync(args.file) : args.file : await streamToBuffer(args.fileStreamFactory?.() ?? Readable.from([]));
696
+ const tags = args.dataItemOpts?.tags ?? [];
697
+ const item = createData(data, this.signer, { tags });
698
+ await item.sign(this.signer);
699
+ const raw = Buffer.from(item.getRaw());
700
+ const localId = item.id || toBase64Url(new DataItem(raw).id);
701
+ if (this.autoFund) {
702
+ await autoFundQuotedHyperbeamLedger({
703
+ ...this.autoFund,
704
+ signedBytes: raw.length
705
+ });
706
+ }
707
+ const res = await fetch(this.uploadUrl, {
708
+ body: raw,
709
+ headers: {
710
+ accept: "application/json, text/plain, */*",
711
+ "content-type": "application/octet-stream"
712
+ },
713
+ method: "POST"
714
+ });
715
+ const body = await res.text();
716
+ if (!res.ok) {
717
+ const preview = body.replaceAll(/\s+/g, " ").trim().slice(0, 300);
718
+ const paymentHint = res.status === 402 ? await this.paymentHint() : void 0;
719
+ throw new Error(
720
+ [
721
+ `HyperBEAM bundler upload failed with HTTP ${res.status}${preview ? `: ${preview}` : ""}`,
722
+ paymentHint
723
+ ].filter(Boolean).join("\n\n")
724
+ );
725
+ }
726
+ return { id: responseId(res.headers, body) || localId };
727
+ }
728
+ async paymentHint() {
729
+ try {
730
+ return hyperbeamAoFundingHint(
731
+ await discoverHyperbeamAoBundlerProfile({ nodeUrl: this.uploader })
732
+ );
733
+ } catch {
734
+ return void 0;
735
+ }
736
+ }
737
+ }
738
+ function hyperbeamAoFundingHint(profile) {
739
+ const lines = profile.tokens.map((token) => {
740
+ const depositAddress = token.depositAddress ?? profile.node?.operator;
741
+ if (!depositAddress) return;
742
+ const label = token.ticker ? `${token.ticker} (${token.id})` : token.id;
743
+ const ledger = token.ledgerId ? profile.ledgers.find((candidate) => candidate.id === token.ledgerId) : void 0;
744
+ const ledgerInfo = ledger ? ` Local ledger: ${ledger.id}${ledger.route ? ` at ${ledger.route}` : ""}.` : "";
745
+ return `- ${label}: send funds to ${depositAddress}.${ledgerInfo}`;
746
+ }).filter(Boolean);
747
+ if (lines.length === 0) return void 0;
748
+ return [
749
+ "The HyperBEAM node requires AO in its local ledger:",
750
+ ...lines,
751
+ "Use --hyperbeam-auto-fund to transfer AO and import the credit automatically before upload."
752
+ ].join("\n");
753
+ }
754
+
755
+ function getFolderSize(folderPath) {
756
+ let totalSize = 0;
757
+ for (const item of fs.readdirSync(folderPath)) {
758
+ const fullPath = path.join(folderPath, item);
759
+ const stats = fs.statSync(fullPath);
760
+ totalSize += stats.isDirectory() ? getFolderSize(fullPath) : stats.size;
761
+ }
762
+ return totalSize;
763
+ }
764
+ async function runUploadWorkflow(deployKey, config, io) {
765
+ const spinner = ora();
766
+ const uploaderType = config["uploader-type"] ?? "turbo";
767
+ let uploadClient;
768
+ let turbo;
769
+ if (uploaderType === "hyperbeam") {
770
+ if (config["sig-type"] !== "arweave") {
771
+ io.error("HyperBEAM uploads require --sig-type arweave");
772
+ }
773
+ if (!config.uploader) {
774
+ io.error("HyperBEAM uploads require --uploader <node-url>");
775
+ }
776
+ if (config["on-demand"]) {
777
+ io.error("HyperBEAM uploads do not support Turbo --on-demand payments");
778
+ }
779
+ let autoFund;
780
+ if (config["hyperbeam-auto-fund"]) {
781
+ autoFund = {
782
+ deployKey,
783
+ uploader: config.uploader
784
+ };
785
+ if (config["hyperbeam-ao-state-url"]) autoFund.aoStateUrl = config["hyperbeam-ao-state-url"];
786
+ if (config["hyperbeam-ledger-id"]) autoFund.ledgerId = config["hyperbeam-ledger-id"];
787
+ if (config["hyperbeam-token-id"]) autoFund.tokenId = config["hyperbeam-token-id"];
788
+ if (config["hyperbeam-fund-amount"]) {
789
+ autoFund.minimumBalance = parseHyperbeamFundAmount(config["hyperbeam-fund-amount"]);
790
+ }
791
+ }
792
+ spinner.start("Initializing HyperBEAM bundler");
793
+ uploadClient = new HyperbeamBundlerClient({
794
+ autoFund,
795
+ deployKey,
796
+ uploadPath: config["hyperbeam-upload-path"] ?? "/~bundler@1.0/item?codec-device=ans104@1.0",
797
+ uploader: config.uploader
798
+ });
799
+ spinner.succeed(`HyperBEAM bundler initialized (${chalk.cyan(config.uploader)})`);
800
+ } else {
801
+ spinner.start("Creating signer");
802
+ const { signer, token } = createSigner(config["sig-type"], deployKey);
803
+ spinner.succeed(`Signer created (${chalk.cyan(config["sig-type"])})`);
804
+ spinner.start("Initializing Turbo");
805
+ const turboFactoryArgs = { signer, token };
806
+ if (config.uploader) {
807
+ turboFactoryArgs.uploadServiceConfig = { url: config.uploader };
808
+ }
809
+ turbo = TurboFactory.authenticated(turboFactoryArgs);
810
+ uploadClient = turbo;
811
+ spinner.succeed("Turbo initialized");
812
+ }
813
+ let fundingMode;
814
+ if (config["on-demand"] && config["max-token-amount"]) {
815
+ const tokenType = config["on-demand"];
816
+ const maxAmount = Number.parseFloat(config["max-token-amount"]);
817
+ let maxTokenAmount;
818
+ switch (tokenType) {
819
+ case "ario": {
820
+ maxTokenAmount = ARIOToTokenAmount(maxAmount);
821
+ break;
822
+ }
823
+ case "base-eth": {
824
+ maxTokenAmount = ETHToTokenAmount(maxAmount);
825
+ break;
826
+ }
827
+ default: {
828
+ throw new Error(`Unsupported on-demand token type: ${tokenType}`);
829
+ }
830
+ }
831
+ fundingMode = new OnDemandFunding({
832
+ maxTokenAmount,
833
+ topUpBufferMultiplier: 1.1
834
+ });
835
+ }
836
+ if (!fundingMode && turbo) {
837
+ spinner.start("Checking Turbo credits for upload");
838
+ try {
839
+ const uploadBytes = config["deploy-file"] ? (() => {
840
+ const filePath = expandPath(config["deploy-file"]);
841
+ return fs.statSync(filePath).size;
842
+ })() : (() => {
843
+ const folderPath = expandPath(config["deploy-folder"]);
844
+ return getFolderSize(folderPath);
845
+ })();
846
+ const FREE_THRESHOLD_BYTES = 107520;
847
+ if (uploadBytes >= FREE_THRESHOLD_BYTES) {
848
+ const [uploadCost] = await turbo.getUploadCosts({ bytes: [uploadBytes] });
849
+ const balance = await turbo.getBalance();
850
+ const requiredWinc = BigInt(uploadCost.winc);
851
+ const currentWinc = BigInt(balance.winc);
852
+ if (requiredWinc > currentWinc) {
853
+ spinner.fail("Insufficient Turbo credits");
854
+ io.error(
855
+ [
856
+ "Insufficient Turbo credits for this upload.",
857
+ `Required: ${requiredWinc.toString()} winc, available: ${currentWinc.toString()} winc.`,
858
+ "",
859
+ "Top up your Turbo balance (or re-run with --on-demand and --max-token-amount)."
860
+ ].join(" ")
861
+ );
862
+ }
863
+ }
864
+ spinner.succeed("Turbo credits check passed");
865
+ } catch (balanceError) {
866
+ spinner.fail("Failed to check Turbo credits");
867
+ const errorMessage = balanceError instanceof Error ? balanceError.message : String(balanceError);
868
+ io.error(`Failed to check Turbo credits: ${errorMessage}`);
869
+ }
870
+ }
871
+ let txOrManifestId;
872
+ try {
873
+ if (config["deploy-file"]) {
874
+ const filePath = expandPath(config["deploy-file"]);
875
+ spinner.start(`Uploading file ${chalk.yellow(config["deploy-file"])}`);
876
+ let cache = config["dedupe-cache-max-entries"] > 0 ? loadCache() : {};
877
+ const uploadResult = await uploadFile(uploadClient, filePath, { cache, fundingMode });
878
+ if (!uploadResult.transactionId) {
879
+ spinner.fail("File upload failed: no transaction ID returned");
880
+ io.error("File upload failed: no transaction ID returned");
881
+ }
882
+ txOrManifestId = uploadResult.transactionId;
883
+ if (uploadResult.updatedCache && config["dedupe-cache-max-entries"] > 0) {
884
+ cache = cleanupCache(uploadResult.updatedCache, config["dedupe-cache-max-entries"]);
885
+ saveCache(cache);
886
+ }
887
+ if (uploadResult.cacheHit) {
888
+ spinner.succeed(`File cache hit - reusing transaction ${chalk.green(txOrManifestId)}`);
889
+ } else {
890
+ const cacheMsg = config["dedupe-cache-max-entries"] > 0 ? chalk.gray("(cached for future uploads)") : "";
891
+ spinner.succeed(`File uploaded: ${chalk.green(txOrManifestId)} ${cacheMsg}`.trim());
892
+ }
893
+ } else {
894
+ const folderPath = expandPath(config["deploy-folder"]);
895
+ spinner.start(`Uploading folder ${chalk.yellow(config["deploy-folder"])}`);
896
+ let cache = config["dedupe-cache-max-entries"] > 0 ? loadCache() : {};
897
+ const uploadResult = await uploadFolder(uploadClient, folderPath, {
898
+ cache,
899
+ concurrency: config["hyperbeam-auto-fund"] ? 1 : void 0,
900
+ fundingMode,
901
+ throwOnFailure: true
902
+ });
903
+ if (!uploadResult.transactionId) {
904
+ spinner.fail("Folder upload failed: no transaction ID returned");
905
+ io.error("Folder upload failed: no transaction ID returned");
906
+ }
907
+ txOrManifestId = uploadResult.transactionId;
908
+ if (uploadResult.updatedCache && config["dedupe-cache-max-entries"] > 0) {
909
+ cache = cleanupCache(uploadResult.updatedCache, config["dedupe-cache-max-entries"]);
910
+ saveCache(cache);
911
+ }
912
+ const { cacheHits, totalFiles, uploaded } = uploadResult;
913
+ const statsMsg = cacheHits > 0 ? chalk.gray(` (${cacheHits}/${totalFiles} files cached, ${uploaded} uploaded)`) : "";
914
+ if (uploadResult.cacheHit) {
915
+ spinner.succeed(`All ${totalFiles} files cached - manifest: ${chalk.green(txOrManifestId)}`);
916
+ } else {
917
+ const cacheMsg = config["dedupe-cache-max-entries"] > 0 ? chalk.gray(" (files cached for future uploads)") : "";
918
+ spinner.succeed(`Folder uploaded: ${chalk.green(txOrManifestId)}${statsMsg}${cacheMsg}`);
919
+ }
920
+ }
921
+ } catch (uploadError) {
922
+ spinner.fail("Upload failed");
923
+ const errorMessage = uploadError instanceof Error ? uploadError.message : String(uploadError);
924
+ io.error(`Upload failed: ${errorMessage}`);
925
+ }
926
+ return txOrManifestId;
927
+ }
928
+
929
+ export { hyperbeamBundlerLink as h, runUploadWorkflow as r };
930
+ //# sourceMappingURL=upload-workflow-zlELdPNp.js.map