@needmoretruth/nmts-cli 0.17.2
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/AGENTS.md +591 -0
- package/LICENSE +202 -0
- package/LICENSING.md +49 -0
- package/README.ko.md +641 -0
- package/README.md +657 -0
- package/dist/account-proof.js +78 -0
- package/dist/account.js +75 -0
- package/dist/api-key.js +310 -0
- package/dist/api.js +271 -0
- package/dist/args.js +137 -0
- package/dist/artifact-about.js +70 -0
- package/dist/autonomy.js +98 -0
- package/dist/code-access.js +76 -0
- package/dist/code-vault.js +233 -0
- package/dist/collision.js +133 -0
- package/dist/commands/balance.js +93 -0
- package/dist/commands/consent.js +77 -0
- package/dist/commands/create.js +356 -0
- package/dist/commands/env.js +136 -0
- package/dist/commands/expiring.js +167 -0
- package/dist/commands/extend.js +236 -0
- package/dist/commands/get.js +130 -0
- package/dist/commands/kit.js +136 -0
- package/dist/commands/listfile.js +105 -0
- package/dist/commands/login.js +335 -0
- package/dist/commands/logout.js +33 -0
- package/dist/commands/ls.js +202 -0
- package/dist/commands/marks.js +176 -0
- package/dist/commands/mcp.js +139 -0
- package/dist/commands/mode.js +54 -0
- package/dist/commands/on-collision.js +45 -0
- package/dist/commands/organise.js +296 -0
- package/dist/commands/public-code.js +89 -0
- package/dist/commands/pull.js +185 -0
- package/dist/commands/push.js +278 -0
- package/dist/commands/put.js +282 -0
- package/dist/commands/rebuild.js +160 -0
- package/dist/commands/receive.js +125 -0
- package/dist/commands/recovery-list.js +104 -0
- package/dist/commands/recovery.js +291 -0
- package/dist/commands/s3.js +241 -0
- package/dist/commands/settings.js +28 -0
- package/dist/commands/share.js +276 -0
- package/dist/commands/sweep.js +240 -0
- package/dist/commands/trash.js +288 -0
- package/dist/commands/trial.js +264 -0
- package/dist/commands/update.js +169 -0
- package/dist/commands/usage.js +84 -0
- package/dist/commands/verify.js +331 -0
- package/dist/commands/wallet.js +122 -0
- package/dist/commands/whoami.js +33 -0
- package/dist/consent.js +200 -0
- package/dist/credentials.js +311 -0
- package/dist/crypto-surface.js +54 -0
- package/dist/crypto.js +137 -0
- package/dist/download-part.js +144 -0
- package/dist/download-sink.js +213 -0
- package/dist/download.js +163 -0
- package/dist/drive-paths.js +170 -0
- package/dist/environment.js +190 -0
- package/dist/errors.js +71 -0
- package/dist/exit.js +107 -0
- package/dist/expiry.js +115 -0
- package/dist/extend-chain.js +137 -0
- package/dist/extend-plan.js +149 -0
- package/dist/extend-sign.js +130 -0
- package/dist/guards.js +15 -0
- package/dist/help.js +192 -0
- package/dist/human-check.js +54 -0
- package/dist/item-trash.js +25 -0
- package/dist/kit-file.js +133 -0
- package/dist/list-file.js +71 -0
- package/dist/list-view-find.js +71 -0
- package/dist/list-view-order.js +96 -0
- package/dist/main.js +394 -0
- package/dist/manifest-create.js +81 -0
- package/dist/manifest-write.js +244 -0
- package/dist/manifest.js +213 -0
- package/dist/mark-render.js +45 -0
- package/dist/mcp-args.js +154 -0
- package/dist/mcp-tools/context.js +50 -0
- package/dist/mcp-tools/files.js +133 -0
- package/dist/mcp-tools/organise.js +128 -0
- package/dist/mcp-tools/reads.js +89 -0
- package/dist/mcp-tools/share.js +61 -0
- package/dist/mcp.js +142 -0
- package/dist/net-retry.js +84 -0
- package/dist/network.js +43 -0
- package/dist/notice.js +45 -0
- package/dist/product.js +38 -0
- package/dist/progress.js +110 -0
- package/dist/prompt.js +145 -0
- package/dist/rebuild.js +255 -0
- package/dist/recovery-assemble.js +148 -0
- package/dist/recovery-build.js +260 -0
- package/dist/recovery-map-file.js +114 -0
- package/dist/recovery-map.js +191 -0
- package/dist/recovery-release.js +229 -0
- package/dist/recovery-seq.js +63 -0
- package/dist/recovery-source.js +143 -0
- package/dist/registration.js +83 -0
- package/dist/s3/listing.js +134 -0
- package/dist/s3/multipart.js +104 -0
- package/dist/s3/response-sink.js +41 -0
- package/dist/s3/same-file.js +117 -0
- package/dist/s3/server.js +252 -0
- package/dist/s3/sigv4.js +168 -0
- package/dist/s3/staging.js +87 -0
- package/dist/s3/xml.js +80 -0
- package/dist/safe-path.js +89 -0
- package/dist/seal.js +206 -0
- package/dist/secret-reader.js +112 -0
- package/dist/server.js +33 -0
- package/dist/session.js +49 -0
- package/dist/setup-questions.js +31 -0
- package/dist/share.js +195 -0
- package/dist/shared/lib/crypto/size-padding.js +126 -0
- package/dist/shared/lib/drive/manifest-codec.js +287 -0
- package/dist/shared/lib/drive/manifest-index.js +235 -0
- package/dist/shared/lib/drive/manifest-ops.js +267 -0
- package/dist/shared/lib/drive/manifest-settings.js +65 -0
- package/dist/shared/lib/drive/name-conflict.js +121 -0
- package/dist/shared/lib/drive/unique-name.js +48 -0
- package/dist/shared/lib/extend/epochs.js +256 -0
- package/dist/shared/lib/net/retry-budget.js +95 -0
- package/dist/shared/lib/share/shared-file-info.js +65 -0
- package/dist/shared/lib/storage-network.js +64 -0
- package/dist/shared/lib/upload/part-plan.js +28 -0
- package/dist/stdout.js +109 -0
- package/dist/trash-sweep.js +123 -0
- package/dist/units.js +19 -0
- package/dist/update-check.js +164 -0
- package/dist/update-source.js +143 -0
- package/dist/upload-api.js +98 -0
- package/dist/upload-file.js +242 -0
- package/dist/upload-price.js +117 -0
- package/dist/upload-steps.js +111 -0
- package/dist/upload-store.js +252 -0
- package/dist/upload-wire.js +25 -0
- package/dist/upload.js +294 -0
- package/dist/usage-report.js +54 -0
- package/dist/wallet-chain.js +76 -0
- package/dist/wallet.js +209 -0
- package/dist/walrus-write.js +188 -0
- package/dist/walrus.js +156 -0
- package/package.json +56 -0
- package/vendor/nmts-crypto/nmts_crypto_wasm.d.ts +469 -0
- package/vendor/nmts-crypto/nmts_crypto_wasm.js +1300 -0
- package/vendor/nmts-crypto/nmts_crypto_wasm_bg.wasm +0 -0
- package/vendor/nmts-crypto/nmts_crypto_wasm_bg.wasm.d.ts +55 -0
- package/vendor/nmts-crypto/package.json +17 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// The real chain reads behind `nmts wallet` — the only file in this command that loads the Sui SDK.
|
|
2
|
+
//
|
|
3
|
+
// ⛔ SPLIT OUT SO THE COMMAND CAN BE TESTED. `wallet.ts` decides what a balance means and what is
|
|
4
|
+
// said when there is none; this only knows how to ask. Keeping the SDK out of the deciding half
|
|
5
|
+
// is what lets `node --test` drive every branch — including the ones a live chain would only
|
|
6
|
+
// produce by being broken.
|
|
7
|
+
//
|
|
8
|
+
// ⛔ IT READS AND CANNOT DO ANYTHING ELSE. No signer is built here, no transaction is assembled,
|
|
9
|
+
// and the two calls below are the whole of this file's contact with the network: what one
|
|
10
|
+
// address holds, and whether a coin type exists. A balance is public data — anybody can ask the
|
|
11
|
+
// same question about the same address — so nothing about this run leaves that was not already
|
|
12
|
+
// published by the chain itself.
|
|
13
|
+
import { SuiJsonRpcClient } from "@mysten/sui/jsonRpc";
|
|
14
|
+
import { suiRpcHost } from "./walrus.js";
|
|
15
|
+
/**
|
|
16
|
+
* How long one balance question gets.
|
|
17
|
+
*
|
|
18
|
+
* A read that stalls is a read that failed, and the caller has something honest to print either
|
|
19
|
+
* way. Long enough for a slow public node, short enough that an agent waiting on this does not
|
|
20
|
+
* conclude the tool has hung.
|
|
21
|
+
*/
|
|
22
|
+
export const BALANCE_TIMEOUT_MS = 20_000;
|
|
23
|
+
/**
|
|
24
|
+
* Read balances for one address on one network.
|
|
25
|
+
*
|
|
26
|
+
* ⚠ THE NODE IS NOT TRUSTED WITH ANYTHING, and it does not have to be: what it answers is printed
|
|
27
|
+
* as a number and nothing is decided on it. A node that lies about a balance produces a wrong
|
|
28
|
+
* number on a screen — where its owner will see it disagree with every other wallet they own —
|
|
29
|
+
* and it cannot produce a wrong address, because the address never came from it.
|
|
30
|
+
*/
|
|
31
|
+
export function chainReader(network, address) {
|
|
32
|
+
// The network name reaches the SDK as well as the URL, which is what keeps a mirror pointed at
|
|
33
|
+
// the wrong chain from being discovered later, as an answer that quietly made no sense.
|
|
34
|
+
const client = new SuiJsonRpcClient({ network, url: suiRpcHost(network) });
|
|
35
|
+
return {
|
|
36
|
+
async totalOf(coinType) {
|
|
37
|
+
const { totalBalance } = await client.getBalance({
|
|
38
|
+
owner: address,
|
|
39
|
+
coinType,
|
|
40
|
+
signal: AbortSignal.timeout(BALANCE_TIMEOUT_MS),
|
|
41
|
+
});
|
|
42
|
+
return baseUnitsOf(totalBalance);
|
|
43
|
+
},
|
|
44
|
+
async knowsCoinType(coinType) {
|
|
45
|
+
// Null is the answer for a type that does not exist — measured against the public mainnet
|
|
46
|
+
// node this tool reads from, 2026-08-24, which is also where the balance-of-a-made-up-coin
|
|
47
|
+
// measurement in `wallet.ts` comes from.
|
|
48
|
+
const metadata = await client.getCoinMetadata({
|
|
49
|
+
coinType,
|
|
50
|
+
signal: AbortSignal.timeout(BALANCE_TIMEOUT_MS),
|
|
51
|
+
});
|
|
52
|
+
return metadata !== null;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A total off the wire, as base units.
|
|
58
|
+
*
|
|
59
|
+
* ⛔ A STRING, AND IT STAYS EXACT. Balances run past what a JavaScript number holds without losing
|
|
60
|
+
* digits, so the value is never converted through one. Anything that is not a whole,
|
|
61
|
+
* non-negative count of base units is refused rather than rounded into something printable:
|
|
62
|
+
* this tool would rather say it could not read the balance than print a number the chain did
|
|
63
|
+
* not say.
|
|
64
|
+
*/
|
|
65
|
+
function baseUnitsOf(total) {
|
|
66
|
+
let units;
|
|
67
|
+
try {
|
|
68
|
+
units = BigInt(total);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new Error("the chain answered with a balance this tool cannot read as a number");
|
|
72
|
+
}
|
|
73
|
+
if (units < 0n)
|
|
74
|
+
throw new Error("the chain answered with a negative balance, which is not a balance");
|
|
75
|
+
return units;
|
|
76
|
+
}
|
package/dist/wallet.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// The wallet an account code derives: its address, and what a chain says it holds.
|
|
2
|
+
//
|
|
3
|
+
// ⛔ NOTHING HERE SIGNS OR SPENDS, AND NOTHING HERE HANDS OUT A KEY. The only secret this module
|
|
4
|
+
// touches is the 32-byte wallet seed; it lives for the length of one address computation and is
|
|
5
|
+
// wiped on every path out, failures included. No function returns a seed, a private key or a
|
|
6
|
+
// keypair, so a later caller cannot reach one without deciding to add one.
|
|
7
|
+
//
|
|
8
|
+
// ⛔ THE DERIVATION IS THE ENGINE'S, NOT THIS FILE'S. `wallet_seed_for` is the same WebAssembly
|
|
9
|
+
// function the browser app calls, off the same wallet root, at the same index — so the address
|
|
10
|
+
// printed here is the address the account already has. A second derivation written in
|
|
11
|
+
// TypeScript would be free to drift from the first, and the way that surfaces is somebody
|
|
12
|
+
// funding an address their account cannot spend from.
|
|
13
|
+
//
|
|
14
|
+
// ⛔ A BALANCE THAT COULD NOT BE READ IS NOT ZERO. On a screen the two look identical; they mean
|
|
15
|
+
// opposite things — "this wallet is empty" and "nobody here knows". Every result below says
|
|
16
|
+
// which of the two it is, and a caller has to open the result to get at a number at all.
|
|
17
|
+
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
|
|
18
|
+
import { assertUsableCode } from "./account.js";
|
|
19
|
+
import { DERIVED, loadCrypto } from "./crypto.js";
|
|
20
|
+
import { NmtsError } from "./errors.js";
|
|
21
|
+
/**
|
|
22
|
+
* The wallet this account code opens by itself.
|
|
23
|
+
*
|
|
24
|
+
* ⛔ IT IS WALLET 0 BECAUSE THAT IS THE ONE THE BROWSER OPENS. Every wallet, including this one,
|
|
25
|
+
* comes out of `wallet_seed_for` — there is no special case for the first — so the index is the
|
|
26
|
+
* whole of the difference between "the account's wallet" and somebody else's.
|
|
27
|
+
*
|
|
28
|
+
* ⛔ EXPORTED SO THE SIGNER CANNOT PICK ITS OWN. `extend-sign.ts` derives a keypair from the same
|
|
29
|
+
* root and has to reach the SAME wallet as the address printed here; a second literal `0` over
|
|
30
|
+
* there would be a second answer to a question with one right one, and the failure is silent —
|
|
31
|
+
* a signature from an address nobody funded. `extend-sign.test.ts` compares the two.
|
|
32
|
+
*/
|
|
33
|
+
export const BUILT_IN_WALLET_INDEX = 0;
|
|
34
|
+
/**
|
|
35
|
+
* How many base units make one coin. SUI counts in MIST and WAL counts in FROST; both are 1e9.
|
|
36
|
+
*
|
|
37
|
+
* The two constants are written as digits and scale rather than as one number so they cannot
|
|
38
|
+
* disagree with each other.
|
|
39
|
+
*/
|
|
40
|
+
const BASE_UNIT_DIGITS = 9;
|
|
41
|
+
const BASE_UNITS_PER_COIN = 10n ** BigInt(BASE_UNIT_DIGITS);
|
|
42
|
+
/** The chain's own coin. Its type is fixed by the chain itself and takes no network. */
|
|
43
|
+
export const SUI_COIN_TYPE = "0x2::sui::SUI";
|
|
44
|
+
/**
|
|
45
|
+
* The storage network's coin, per chain.
|
|
46
|
+
*
|
|
47
|
+
* ⛔ A WRONG VALUE HERE READS AS AN EMPTY WALLET RATHER THAN AS A FAULT. Measured against the
|
|
48
|
+
* public mainnet node this tool reads from, 2026-08-24: asking for a coin type that does not
|
|
49
|
+
* exist answers `totalBalance: "0"` — the same answer a real empty balance gives. That
|
|
50
|
+
* measurement is why `readBalances` never reports a zero WAL balance without first asking the
|
|
51
|
+
* chain whether it has heard of the type at all; see `ChainReader.knowsCoinType`.
|
|
52
|
+
*
|
|
53
|
+
* ⚠ THIS IS A SECOND COPY. The browser app carries the same two strings for the same reason, and
|
|
54
|
+
* no machine holds the two level — this package deliberately imports nothing from that tree.
|
|
55
|
+
* What keeps them honest is the check above: a copy that went stale stops answering zero and
|
|
56
|
+
* starts saying it could not be read.
|
|
57
|
+
*/
|
|
58
|
+
export const WAL_COIN_TYPES = {
|
|
59
|
+
testnet: "0x8270feb7375eee355e64fdb69c50abb6b5f9393a722883c1cf45f8e26048810a::wal::WAL",
|
|
60
|
+
mainnet: "0x356a26eb9e012a68958082340d4c4116e7f55615cf27affcff209cf0ae544f59::wal::WAL",
|
|
61
|
+
};
|
|
62
|
+
/** The WAL coin type for one network. */
|
|
63
|
+
export function walCoinType(network) {
|
|
64
|
+
return WAL_COIN_TYPES[network];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The Sui address of one 32-byte wallet seed.
|
|
68
|
+
*
|
|
69
|
+
* ⛔ IT IS THE SDK'S COMPUTATION, NOT ONE WRITTEN HERE. An address is BLAKE2b-256 over a scheme
|
|
70
|
+
* byte and the public key; hand-rolling that would put a second answer in the world for a
|
|
71
|
+
* question that must have exactly one, and the failure would be silent — a well-formed address
|
|
72
|
+
* nobody can spend from.
|
|
73
|
+
*
|
|
74
|
+
* ⚠ THE KEYPAIR HOLDS THE SECRET AND CANNOT BE WIPED. `@mysten/sui` keeps the key inside its own
|
|
75
|
+
* object, with no method that clears it; the same is true in the browser's worker. What is
|
|
76
|
+
* controlled here is lifetime: it is built, asked one question, and dropped, and the caller's
|
|
77
|
+
* copy of the seed is zeroed. Nothing retains it.
|
|
78
|
+
*
|
|
79
|
+
* Exported because the vectors that hold this to the other implementations of the same derivation
|
|
80
|
+
* are (seed, address) pairs, and a test cannot check them through the account-code entry point.
|
|
81
|
+
*/
|
|
82
|
+
export function addressFromSeed(seed) {
|
|
83
|
+
return Ed25519Keypair.fromSecretKey(seed).toSuiAddress();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The address of the wallet this account code derives. Offline: nothing is asked of anybody.
|
|
87
|
+
*
|
|
88
|
+
* The same address on every network — an account has one wallet, and which chain it is looked up
|
|
89
|
+
* on is a separate question from what it is called.
|
|
90
|
+
*/
|
|
91
|
+
export async function walletAddress(code) {
|
|
92
|
+
// ⛔ The one refusal text for a malformed code lives in `account.ts`. Checking here means a typo
|
|
93
|
+
// fails the same way it fails everywhere else in this tool rather than as an engine error.
|
|
94
|
+
await assertUsableCode(code);
|
|
95
|
+
const glue = await loadCrypto();
|
|
96
|
+
let bytes;
|
|
97
|
+
try {
|
|
98
|
+
bytes = glue.account_code_parse(code);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// ⛔ Unreachable in practice — the same parser accepted this input a line ago. It is caught
|
|
102
|
+
// anyway because the alternative is an engine message going out verbatim, and an engine
|
|
103
|
+
// message about a code can carry the code (see `errors.ts`).
|
|
104
|
+
throw new NmtsError("The account code could not be read on this machine.", { exitCode: 1 });
|
|
105
|
+
}
|
|
106
|
+
// ⛔ THIS BUFFER IS EVERY KEY IN THE ACCOUNT, not just the wallet root: the sign-in secret, the
|
|
107
|
+
// key that opens the files, the key that opens the file list. It is wiped below along with
|
|
108
|
+
// everything sliced out of it, on the failing paths as well as the good one.
|
|
109
|
+
const derived = glue.kdf_derive(bytes);
|
|
110
|
+
try {
|
|
111
|
+
return addressFromDerived(glue, derived);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
derived.fill(0);
|
|
115
|
+
bytes.fill(0);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The built-in wallet's address, from a buffer the caller already holds.
|
|
120
|
+
*
|
|
121
|
+
* ⛔ IT TAKES KEY MATERIAL AND RETURNS A STRING. `derived` belongs to the caller and is left
|
|
122
|
+
* alone; the two secrets this function makes — the wallet root it slices out and the seed the
|
|
123
|
+
* engine expands from it — are wiped before it returns, on the failing path as well as the
|
|
124
|
+
* good one. Neither one leaves.
|
|
125
|
+
*/
|
|
126
|
+
export function addressFromDerived(glue, derived) {
|
|
127
|
+
const [from, to] = DERIVED.walletRoot;
|
|
128
|
+
const root = derived.slice(from, to);
|
|
129
|
+
let seed = null;
|
|
130
|
+
try {
|
|
131
|
+
seed = glue.wallet_seed_for(root, BUILT_IN_WALLET_INDEX);
|
|
132
|
+
return addressFromSeed(seed);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
root.fill(0);
|
|
136
|
+
seed?.fill(0);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Why a coin came back unread, from whatever was thrown. */
|
|
140
|
+
function reasonOf(error) {
|
|
141
|
+
return error instanceof Error ? error.message : String(error);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Read both balances, keeping "could not be read" apart from "empty" in every branch.
|
|
145
|
+
*
|
|
146
|
+
* ⚠ THE REASON A READ FAILED IS SAFE TO PRINT, and that is worth saying because in this tool it
|
|
147
|
+
* usually is not (`errors.ts`). Nothing secret is in one of these requests: what goes out is a
|
|
148
|
+
* public address and a coin type, so what comes back cannot quote a secret.
|
|
149
|
+
*
|
|
150
|
+
* ⛔ ONE COIN'S FAILURE DOES NOT TAKE THE OTHER DOWN. They are separate requests about separate
|
|
151
|
+
* coins, and an answer for one of them is worth more than a matching pair of silences.
|
|
152
|
+
*/
|
|
153
|
+
export async function readBalances(reader, walType) {
|
|
154
|
+
const [sui, wal] = await Promise.all([readSui(reader), readWal(reader, walType)]);
|
|
155
|
+
return { sui, wal };
|
|
156
|
+
}
|
|
157
|
+
async function readSui(reader) {
|
|
158
|
+
// ⛔ NO COIN-TYPE CHECK FOR THIS ONE, and the asymmetry is deliberate: `0x2::sui::SUI` is the
|
|
159
|
+
// chain's own coin, named by the chain rather than by this build, so a zero here is a zero.
|
|
160
|
+
try {
|
|
161
|
+
return { read: true, baseUnits: await reader.totalOf(SUI_COIN_TYPE) };
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
return { read: false, why: reasonOf(error) };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function readWal(reader, walType) {
|
|
168
|
+
let total;
|
|
169
|
+
try {
|
|
170
|
+
total = await reader.totalOf(walType);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
return { read: false, why: reasonOf(error) };
|
|
174
|
+
}
|
|
175
|
+
// A positive balance is its own proof that the coin type is real; only zero is ambiguous.
|
|
176
|
+
if (total > 0n)
|
|
177
|
+
return { read: true, baseUnits: total };
|
|
178
|
+
try {
|
|
179
|
+
if (await reader.knowsCoinType(walType))
|
|
180
|
+
return { read: true, baseUnits: total };
|
|
181
|
+
return {
|
|
182
|
+
read: false,
|
|
183
|
+
why: "this chain does not know the WAL coin type this version of the tool was built with, so a zero balance here would mean nothing",
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
// ⛔ A FAILED CHECK IS NOT A CONFIRMED ZERO. Reporting the zero anyway would be reporting a
|
|
188
|
+
// number whose one supporting question went unanswered.
|
|
189
|
+
return { read: false, why: `the balance came back as zero and could not be confirmed: ${reasonOf(error)}` };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Base units as a person reads them — the exact value, never rounded.
|
|
194
|
+
*
|
|
195
|
+
* ⛔ NOT `toFixed`, NOT A SHORTENED FORM. A wallet holding 0.000000004 SUI is not holding "0.00",
|
|
196
|
+
* and money that reads as zero when it is not is the one rounding error nobody forgives. The
|
|
197
|
+
* fractional part is trimmed of trailing zeros only, which removes nothing that was there.
|
|
198
|
+
*
|
|
199
|
+
* Callers hand it a total the chain reported; a negative one is refused where it is read, so this
|
|
200
|
+
* never sees one.
|
|
201
|
+
*/
|
|
202
|
+
export function coinAmount(baseUnits) {
|
|
203
|
+
const whole = baseUnits / BASE_UNITS_PER_COIN;
|
|
204
|
+
const fraction = baseUnits % BASE_UNITS_PER_COIN;
|
|
205
|
+
if (fraction === 0n)
|
|
206
|
+
return whole.toString();
|
|
207
|
+
const digits = fraction.toString().padStart(BASE_UNIT_DIGITS, "0").replace(/0+$/, "");
|
|
208
|
+
return `${whole}.${digits}`;
|
|
209
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// The real Walrus protocol client — the ONE file in this tool that loads the storage network's SDK.
|
|
2
|
+
//
|
|
3
|
+
// ⛔ SPLIT OUT FROM `upload.ts` ON PURPOSE. That file decides what gets bought and when; this one
|
|
4
|
+
// only knows how to talk to the network. Keeping the SDK out of it is what lets `node --test`
|
|
5
|
+
// drive every spending decision, including all of its failure branches, against fakes.
|
|
6
|
+
//
|
|
7
|
+
// ⛔ THERE IS NO SIGNER HERE AND NOTHING IS BUILT AS A TRANSACTION. On the credit rail the server
|
|
8
|
+
// registers the blob and pays the relay's tip out of the treasury; this tool computes the id
|
|
9
|
+
// locally and hands over the bytes. The one thing it must get right is telling the relay WHICH
|
|
10
|
+
// transaction paid — see the note on `sendTip` below, which is not what its name suggests.
|
|
11
|
+
import { SuiJsonRpcClient } from "@mysten/sui/jsonRpc";
|
|
12
|
+
import { walrus } from "@mysten/walrus";
|
|
13
|
+
import { epochClock } from "./expiry.js";
|
|
14
|
+
// ⛔ The re-export below is what callers use; this local name is the same function,
|
|
15
|
+
// imported so the reader above can call it without importing from itself.
|
|
16
|
+
import { epochStartedMs as readEpochStart } from "./shared/lib/extend/epochs.js";
|
|
17
|
+
import { NmtsError } from "./errors.js";
|
|
18
|
+
import { countingFetch } from "./progress.js";
|
|
19
|
+
import { relayHost, suiRpcHost } from "./walrus.js";
|
|
20
|
+
/**
|
|
21
|
+
* How long the relay gets for one blob PUT, sized to the body.
|
|
22
|
+
*
|
|
23
|
+
* The SDK's own default is 30 seconds, which a multi-megabyte body cannot finish on an ordinary
|
|
24
|
+
* connection — and a timeout here happens AFTER the storage is paid for.
|
|
25
|
+
*/
|
|
26
|
+
export function relayTimeoutMs(bodyBytes) {
|
|
27
|
+
const perMiB = 20_000;
|
|
28
|
+
const floor = 60_000;
|
|
29
|
+
return Math.max(floor, Math.ceil((bodyBytes / 2 ** 20) * perMiB));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* ⛔ `sendTip` IS A FLAG HERE, NOT A BUDGET. The SDK decides whether to tell the relay which
|
|
33
|
+
* transaction paid its tip by asking `!!config.uploadRelay.sendTip` — with the field absent it
|
|
34
|
+
* sends the bytes WITHOUT the nonce and the transaction id, and the relay refuses them because
|
|
35
|
+
* as far as it can tell nobody paid. On this rail the treasury already paid, inside the
|
|
36
|
+
* register transaction; this tool holds no wallet and cannot pay anything whatever this number
|
|
37
|
+
* says. It is a ceiling on a payment that has no way of happening.
|
|
38
|
+
*
|
|
39
|
+
* ⚠ Which also means it does NOT have to agree with the browser's ceiling, and no gate holds the
|
|
40
|
+
* two level: the browser's number bounds a real payment made by a real wallet.
|
|
41
|
+
*/
|
|
42
|
+
const TIP_CEILING_UNUSED_MIST = 1;
|
|
43
|
+
function extend(network, relayUrl, bodyBytes, onSent) {
|
|
44
|
+
const base = new SuiJsonRpcClient({
|
|
45
|
+
// ⛔ The network name reaches the SDK as well as the URL. A mirror pointed at the wrong chain
|
|
46
|
+
// would otherwise be discovered as a blob the storage nodes refuse, after the money moved.
|
|
47
|
+
network: network === "mainnet" ? "mainnet" : "testnet",
|
|
48
|
+
url: suiRpcHost(network),
|
|
49
|
+
});
|
|
50
|
+
return base.$extend(walrus({
|
|
51
|
+
uploadRelay: {
|
|
52
|
+
host: relayUrl,
|
|
53
|
+
sendTip: { max: TIP_CEILING_UNUSED_MIST },
|
|
54
|
+
timeout: relayTimeoutMs(bodyBytes),
|
|
55
|
+
// ⛔ THE ONLY WAY TO SEE AN UPLOAD MOVE. The SDK does not report progress, and the relay
|
|
56
|
+
// PUT is the one step of an upload that takes real time — a person watching a large
|
|
57
|
+
// file with no feedback cannot tell a slow upload from a hung one, and neither can an
|
|
58
|
+
// agent deciding whether to give up.
|
|
59
|
+
...(onSent ? { fetch: countingFetch(onSent) } : {}),
|
|
60
|
+
},
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
function fail(what, error) {
|
|
64
|
+
throw new NmtsError(`${what}: ${error instanceof Error ? error.message : String(error)}`);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Build a protocol client bound to ONE relay, for ONE upload of a known size.
|
|
68
|
+
*
|
|
69
|
+
* `onSent` is called as the request body leaves — that is the honest measure of an upload, and it
|
|
70
|
+
* is the only phase of one that anything can report on.
|
|
71
|
+
*/
|
|
72
|
+
export function createBlobProtocol(network, bodyBytes, onSent) {
|
|
73
|
+
const relayUrl = relayHost(network);
|
|
74
|
+
const client = extend(network, relayUrl, bodyBytes, onSent);
|
|
75
|
+
return {
|
|
76
|
+
relayUrl,
|
|
77
|
+
async computeMetadata({ bytes, nonce }) {
|
|
78
|
+
// ⛔ The shard count is NOT supplied. It is a property of the live storage network, the
|
|
79
|
+
// erasure coding depends on it, and a number written down here would be right until the
|
|
80
|
+
// day it was not — at which point every id this tool computed would be refused after the
|
|
81
|
+
// credits had moved. The SDK reads it from the chain.
|
|
82
|
+
const meta = await client.walrus
|
|
83
|
+
.computeBlobMetadata({
|
|
84
|
+
bytes,
|
|
85
|
+
// Omitted rather than passed as undefined so the SDK's own "make a random one" path runs
|
|
86
|
+
// on a first encode; supplied on a retry so the digest reproduces bit for bit.
|
|
87
|
+
...(nonce ? { nonce } : {}),
|
|
88
|
+
})
|
|
89
|
+
.catch((error) => fail("The storage network's encoder could not prepare this file", error));
|
|
90
|
+
return {
|
|
91
|
+
blobId: meta.blobId,
|
|
92
|
+
rootHash: meta.rootHash,
|
|
93
|
+
nonce: meta.nonce,
|
|
94
|
+
// The SDK hands this back as a thunk, because computing it costs a hash of the encoded
|
|
95
|
+
// blob. Awaited once here so nothing downstream has to know it was ever lazy.
|
|
96
|
+
blobDigest: await meta.blobDigest(),
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
async uploadToRelay({ blobId, bytes, nonce, registerTxDigest, blobObjectId }) {
|
|
100
|
+
const { certificate } = await client.walrus
|
|
101
|
+
.writeBlobToUploadRelay({
|
|
102
|
+
blobId,
|
|
103
|
+
blob: bytes,
|
|
104
|
+
nonce,
|
|
105
|
+
txDigest: registerTxDigest,
|
|
106
|
+
blobObjectId,
|
|
107
|
+
// Not a choice made here: the chain service hard-codes it and refuses a registration
|
|
108
|
+
// that says otherwise, so a different value would make the relay reject these bytes.
|
|
109
|
+
deletable: true,
|
|
110
|
+
})
|
|
111
|
+
.catch((error) => fail("The upload relay refused the bytes", error));
|
|
112
|
+
return {
|
|
113
|
+
signers: certificate.signers,
|
|
114
|
+
serialized_message_b64: Buffer.from(certificate.serializedMessage).toString("base64url"),
|
|
115
|
+
signature_b64: Buffer.from(certificate.signature).toString("base64url"),
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The storage network's current epoch, or `null` when it could not be read.
|
|
122
|
+
*
|
|
123
|
+
* ⚠ ADVISORY ONLY. It becomes the `expiry_epoch` the server records beside the file, which the
|
|
124
|
+
* chain — not this number — is the authority on. `null` is written as 0, meaning "not recorded",
|
|
125
|
+
* which is honest; inventing a number would put a date in the drive that nothing stands behind.
|
|
126
|
+
*/
|
|
127
|
+
export async function readCurrentEpoch(network) {
|
|
128
|
+
try {
|
|
129
|
+
const base = new SuiJsonRpcClient({
|
|
130
|
+
network: network === "mainnet" ? "mainnet" : "testnet",
|
|
131
|
+
url: suiRpcHost(network),
|
|
132
|
+
}).$extend(walrus({}));
|
|
133
|
+
// The epoch lives on the COMMITTEE, not beside it: the system state describes capacity and
|
|
134
|
+
// the deny lists as well, and only the committee is stamped with which epoch it serves.
|
|
135
|
+
const state = await base.walrus.systemState();
|
|
136
|
+
const epoch = state.committee.epoch;
|
|
137
|
+
return typeof epoch === "number" ? epoch : null;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The storage network's epoch clock: which epoch, how long one lasts, and when this one began.
|
|
145
|
+
*
|
|
146
|
+
* ⛔ THE EPOCH LENGTH IS READ, NEVER ASSUMED. It is one day on one network and fourteen on the
|
|
147
|
+
* other, so a constant borrowed from either would turn "fourteen days left" into "196 days
|
|
148
|
+
* left" on the wrong one — beside a sentence about a file being deleted.
|
|
149
|
+
*
|
|
150
|
+
* ⚠ `startedMs` is usually ABSENT and that is normal, not a failure: the network only carries the
|
|
151
|
+
* moment an epoch settled while it is not changing epochs. Everything downstream treats its
|
|
152
|
+
* absence as "the day count is a lower bound", which is the safe direction.
|
|
153
|
+
*
|
|
154
|
+
* `null` means the clock could not be read at all. ⛔ The caller must say so rather than draw a
|
|
155
|
+
* drive with nothing expiring — an unread clock and an account in no danger look identical from
|
|
156
|
+
* the outside and are the opposite of each other.
|
|
157
|
+
*/
|
|
158
|
+
export async function readEpochWindow(network) {
|
|
159
|
+
try {
|
|
160
|
+
const base = new SuiJsonRpcClient({
|
|
161
|
+
network: network === "mainnet" ? "mainnet" : "testnet",
|
|
162
|
+
url: suiRpcHost(network),
|
|
163
|
+
}).$extend(walrus({}));
|
|
164
|
+
const [system, staking] = await Promise.all([base.walrus.systemState(), base.walrus.stakingState()]);
|
|
165
|
+
// Both numbers can arrive as strings — Sui reports 64-bit values that way — so they are
|
|
166
|
+
// converted here and the constructor below refuses whatever did not survive it.
|
|
167
|
+
const epoch = system.committee.epoch;
|
|
168
|
+
const duration = staking.epoch_duration;
|
|
169
|
+
return epochClock(Number(epoch), Number(duration), readEpochStart(staking.epoch_state));
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* When the current epoch began, or null.
|
|
177
|
+
*
|
|
178
|
+
* ⛔ THE NARROWING ITSELF IS NOT WRITTEN HERE (2026-08-25). It used to be, and it accepted a single
|
|
179
|
+
* enum case while the network sits in a different one for nearly all of every epoch — so the
|
|
180
|
+
* anchor was thrown away almost always and this tool reported "N days or more" where it could
|
|
181
|
+
* have reported a date. The browser had the same bug in its own copy of the same judgement,
|
|
182
|
+
* which is the point: two narrowings are two answers. It now lives beside the arithmetic that
|
|
183
|
+
* depends on it, in the file both programs copy from.
|
|
184
|
+
*
|
|
185
|
+
* ⛔ STILL EXPORTED FROM HERE. `extend-chain.ts` reads the same state for a different reason and
|
|
186
|
+
* imports this name; re-exporting keeps one import path for callers in this package.
|
|
187
|
+
*/
|
|
188
|
+
export { epochStartedMs } from "./shared/lib/extend/epochs.js";
|
package/dist/walrus.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Reading bytes back from the Walrus storage network.
|
|
2
|
+
//
|
|
3
|
+
// ⛔ THE AGGREGATOR IS NOT TRUSTED, and nothing here pretends otherwise. Whatever these hosts hand
|
|
4
|
+
// back goes straight into the NCF-3 stream decryptor, which authenticates every chunk under the
|
|
5
|
+
// file's own key. A host that returns the wrong bytes — by mistake or on purpose — produces a
|
|
6
|
+
// decryption failure, never a quietly wrong file. What an aggregator CAN do is refuse to serve,
|
|
7
|
+
// or serve nothing, which is why more than one is listed.
|
|
8
|
+
//
|
|
9
|
+
// ⛔ THE HOST LIST IS WRITTEN IN THREE LANGUAGES and `deploy/check-walrus-hosts.mjs` set-compares
|
|
10
|
+
// them: here, the browser build, and the standalone recovery tool. One copy going stale shows
|
|
11
|
+
// up as "file not found", which reads as "the file is gone" — so the machine holds them level
|
|
12
|
+
// rather than a person remembering to.
|
|
13
|
+
import { NmtsError } from "./errors.js";
|
|
14
|
+
/** Curated Walrus aggregator (read) endpoints per network, preference order. */
|
|
15
|
+
export const AGGREGATOR_HOSTS = {
|
|
16
|
+
testnet: ["https://aggregator.walrus-testnet.walrus.space"],
|
|
17
|
+
mainnet: ["https://aggregator.walrus-mainnet.walrus.space"],
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Curated Walrus upload-relay (write) endpoints per network, preference order.
|
|
21
|
+
*
|
|
22
|
+
* ⛔ A RELAY IS NOT AN AGGREGATOR, and the difference is money. The relay is named inside the
|
|
23
|
+
* register transaction the server pays a tip in, so the bytes have to go to the SAME host the
|
|
24
|
+
* reservation was made for. That is why a write picks its host BEFORE the storage is bought and
|
|
25
|
+
* then never moves: failing over to a second relay would push bytes nobody paid that relay for.
|
|
26
|
+
*/
|
|
27
|
+
export const RELAY_HOSTS = {
|
|
28
|
+
testnet: ["https://upload-relay.testnet.walrus.space"],
|
|
29
|
+
mainnet: ["https://upload-relay.mainnet.walrus.space"],
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Sui JSON-RPC endpoints per network — public mirrors, because the official full nodes retired
|
|
33
|
+
* JSON-RPC on both networks (browser measurements 2026-07-29 testnet, 2026-08-03 mainnet).
|
|
34
|
+
*
|
|
35
|
+
* ⛔ READ-ONLY, AND NOT TRUSTED WITH ANYTHING. This tool asks one question here: how many shards
|
|
36
|
+
* the storage network currently has, which the erasure coding needs. A wrong answer produces a
|
|
37
|
+
* blob id the storage network refuses, so it fails loudly at the relay rather than quietly
|
|
38
|
+
* storing something unreadable. No key is ever sent to one of these, and nothing is signed.
|
|
39
|
+
*/
|
|
40
|
+
export const SUI_RPC_HOSTS = {
|
|
41
|
+
testnet: "https://rpc-testnet.suiscan.xyz",
|
|
42
|
+
mainnet: "https://rpc-mainnet.suiscan.xyz",
|
|
43
|
+
};
|
|
44
|
+
/** How long one host gets before the next is tried. A read that stalls is a read that failed. */
|
|
45
|
+
export const READ_TIMEOUT_MS = 60_000;
|
|
46
|
+
/**
|
|
47
|
+
* Point reads at somebody else's aggregator, or at a development stack.
|
|
48
|
+
*
|
|
49
|
+
* ⚠ It replaces the list rather than adding to it, and that is deliberate: a run should read from
|
|
50
|
+
* where it was told to read, not from there AND the public hosts. Comma-separated for more
|
|
51
|
+
* than one, tried in the order given.
|
|
52
|
+
*/
|
|
53
|
+
export const AGGREGATOR_ENV_VAR = "NMTS_AGGREGATOR";
|
|
54
|
+
/**
|
|
55
|
+
* Push writes through somebody else's relay, or through a development stack.
|
|
56
|
+
*
|
|
57
|
+
* ⚠ ONE host, not a list. Unlike reads there is nothing to fail over to — see `RELAY_HOSTS`.
|
|
58
|
+
*/
|
|
59
|
+
export const RELAY_ENV_VAR = "NMTS_RELAY";
|
|
60
|
+
/** Ask a different Sui JSON-RPC node the shard-count question. */
|
|
61
|
+
export const SUI_RPC_ENV_VAR = "NMTS_SUI_RPC";
|
|
62
|
+
/** The relay this run writes through: the environment's if it named one, else the network's. */
|
|
63
|
+
export function relayHost(network) {
|
|
64
|
+
const named = process.env[RELAY_ENV_VAR]?.trim();
|
|
65
|
+
if (named)
|
|
66
|
+
return named;
|
|
67
|
+
const host = RELAY_HOSTS[network]?.[0];
|
|
68
|
+
if (host === undefined) {
|
|
69
|
+
throw new NmtsError(`No upload relay is known for the ${network} storage network.`, {
|
|
70
|
+
nextStep: `Name one in ${RELAY_ENV_VAR} to upload anyway.`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return host;
|
|
74
|
+
}
|
|
75
|
+
/** The Sui JSON-RPC node this run asks about shard count. */
|
|
76
|
+
export function suiRpcHost(network) {
|
|
77
|
+
const named = process.env[SUI_RPC_ENV_VAR]?.trim();
|
|
78
|
+
if (named)
|
|
79
|
+
return named;
|
|
80
|
+
const host = SUI_RPC_HOSTS[network];
|
|
81
|
+
if (host === undefined) {
|
|
82
|
+
throw new NmtsError(`No Sui RPC endpoint is known for the ${network} network.`, {
|
|
83
|
+
nextStep: `Name one in ${SUI_RPC_ENV_VAR} to upload anyway.`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return host;
|
|
87
|
+
}
|
|
88
|
+
function fromEnvironment() {
|
|
89
|
+
const raw = process.env[AGGREGATOR_ENV_VAR];
|
|
90
|
+
if (raw === undefined)
|
|
91
|
+
return null;
|
|
92
|
+
const hosts = raw.split(",").map((h) => h.trim()).filter((h) => h !== "");
|
|
93
|
+
return hosts.length > 0 ? hosts : null;
|
|
94
|
+
}
|
|
95
|
+
function hostsFor(network, options) {
|
|
96
|
+
if (options.hosts !== undefined && options.hosts.length > 0)
|
|
97
|
+
return options.hosts;
|
|
98
|
+
const chosen = fromEnvironment();
|
|
99
|
+
if (chosen !== null)
|
|
100
|
+
return chosen;
|
|
101
|
+
const known = AGGREGATOR_HOSTS[network];
|
|
102
|
+
if (known === undefined) {
|
|
103
|
+
throw new NmtsError(`No storage-network hosts are known for "${network}".`, {
|
|
104
|
+
exitCode: 2,
|
|
105
|
+
nextStep: "Use --network mainnet or --network testnet.",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return known;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Fetch one stored object, trying each host in order.
|
|
112
|
+
*
|
|
113
|
+
* ⛔ It reports what it tried. A read that fails everywhere is either a blob that is gone, a
|
|
114
|
+
* network that is wrong, or hosts that are all down, and those need different next steps — a
|
|
115
|
+
* bare "not found" would send somebody looking for the wrong one.
|
|
116
|
+
*/
|
|
117
|
+
async function readFrom(network, pathOf, what, options) {
|
|
118
|
+
const hosts = hostsFor(network, options);
|
|
119
|
+
const tried = [];
|
|
120
|
+
for (const host of hosts) {
|
|
121
|
+
const url = pathOf(host.replace(/\/$/, ""));
|
|
122
|
+
const timer = AbortSignal.timeout(options.timeoutMs ?? READ_TIMEOUT_MS);
|
|
123
|
+
const signal = options.signal === undefined ? timer : AbortSignal.any([timer, options.signal]);
|
|
124
|
+
try {
|
|
125
|
+
const response = await fetch(url, { signal, redirect: "follow" });
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
tried.push(`${host} → ${response.status}`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
// ⛔ The reason is kept, not flattened to "failed". A timeout and a refused connection mean
|
|
134
|
+
// different things, and the last host's reason is what the person reads.
|
|
135
|
+
tried.push(`${host} → ${error instanceof Error ? error.message : "no answer"}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
throw new NmtsError(`${what} could not be read from the ${network} storage network.`, {
|
|
139
|
+
exitCode: 5,
|
|
140
|
+
nextStep: `Tried: ${tried.join(" · ")}. If every host answered 404, either the bytes are gone or ` +
|
|
141
|
+
`this is the wrong network — the same identifier does not exist on both.`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/** Whole-blob read: `GET {aggregator}/v1/blobs/{blobId}`. */
|
|
145
|
+
export function readBlob(network, blobId, options = {}) {
|
|
146
|
+
return readFrom(network, (host) => `${host}/v1/blobs/${encodeURIComponent(blobId)}`, `Blob ${blobId}`, options);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Quilt-patch read: `GET {aggregator}/v1/blobs/by-quilt-patch-id/{patchId}`.
|
|
150
|
+
*
|
|
151
|
+
* A quilt is one stored blob holding many small files; the patch id addresses one of them. Files
|
|
152
|
+
* under 64 MiB share a quilt, so this is the common path rather than the exotic one.
|
|
153
|
+
*/
|
|
154
|
+
export function readQuiltPatch(network, patchId, options = {}) {
|
|
155
|
+
return readFrom(network, (host) => `${host}/v1/blobs/by-quilt-patch-id/${encodeURIComponent(patchId)}`, `Quilt patch ${patchId}`, options);
|
|
156
|
+
}
|