@needmoretruth/nmts-cli 0.38.1 → 0.40.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/AGENTS.md +14 -35
- package/CHANGELOG.md +16 -0
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/dist/artifact-about.d.ts +1 -1
- package/dist/commands/erase.js +12 -107
- package/dist/commands/extend.d.ts +3 -3
- package/dist/commands/extend.js +52 -161
- package/dist/commands/wallet-storage-ops.d.ts +2 -2
- package/dist/commands/wallet-storage-ops.js +108 -134
- package/dist/commands/wallet-storage.d.ts +3 -8
- package/dist/commands/wallet-storage.js +16 -37
- package/dist/drive-erase.d.ts +83 -0
- package/dist/drive-erase.js +161 -0
- package/dist/platform-sign.d.ts +9 -1
- package/dist/platform-sign.js +9 -1
- package/dist/product.d.ts +1 -1
- package/dist/product.js +1 -1
- package/dist/storage-control/extend.d.ts +124 -0
- package/dist/storage-control/extend.js +189 -0
- package/dist/storage-control/hints.d.ts +20 -0
- package/dist/storage-control/hints.js +11 -0
- package/dist/storage-control/list.d.ts +55 -0
- package/dist/storage-control/list.js +71 -0
- package/dist/storage-control/reshape.d.ts +120 -0
- package/dist/storage-control/reshape.js +188 -0
- package/dist/storage-control-chain.d.ts +1 -1
- package/dist/storage-control.d.ts +13 -0
- package/dist/storage-control.js +24 -0
- package/dist/wallet-storage-chain.d.ts +1 -1
- package/package.json +9 -1
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// `nmts wallet storage split|merge|transfer` — reshaping the storage resources the wallet holds,
|
|
2
|
-
// and handing one over. ⛔ EACH SIGNS.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// and handing one over. ⛔ EACH SIGNS. What the contract allows, what a change would leave behind,
|
|
3
|
+
// the dry run that prices it and the signature are `storage-control.ts`, which the SDK calls as
|
|
4
|
+
// well; this file is the terminal over it: the words typed, the review read, and `--yes`.
|
|
5
5
|
//
|
|
6
|
-
// ⛔ THE ORDER IS THE SAFETY,
|
|
7
|
-
// the named ones must be this wallet's ② the rules judge the request and say why not ③ the
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
6
|
+
// ⛔ THE ORDER IS THE SAFETY, and it is kept over there: ① the resources are READ from the chain and
|
|
7
|
+
// the named ones must be this wallet's ② the rules judge the request and say why not ③ the exact
|
|
8
|
+
// transaction is dry-run for its fee, and a refusal there ends the run ④ the review ⑤ without
|
|
9
|
+
// `--yes` that is the end ⑥ the wallet unlock is held against the fee — scope `storage` for
|
|
10
|
+
// cutting and joining, scope `all` for handing over ⑦ only then the signature. Steps ⑤ and ⑥ are
|
|
11
|
+
// this file's, handed in as the gate.
|
|
11
12
|
//
|
|
12
13
|
// ⛔ HANDING OVER MOVES NO FILE. The owner's sentence for the browser holds here: what goes is size
|
|
13
14
|
// and remaining time; a file's bytes are sealed with keys the NMTS key derives and stay
|
|
@@ -18,20 +19,12 @@ import { NmtsError } from "../errors.js";
|
|
|
18
19
|
import { resolveNetwork } from "../network.js";
|
|
19
20
|
import { BINARY_NAME } from "../product.js";
|
|
20
21
|
import { resolveServer } from "../server.js";
|
|
21
|
-
import {
|
|
22
|
+
import { reshapeStorage, } from "../storage-control.js";
|
|
22
23
|
import { explorerTxUrl } from "../shared/lib/wallet/activity.js";
|
|
23
|
-
import { isValidSuiAddress } from "../shared/lib/wallet/send-rules.js";
|
|
24
|
-
import { storageOpsReads } from "../storage-control-chain.js";
|
|
25
24
|
import { coinAmount, walletAddress } from "../wallet.js";
|
|
26
25
|
import { payingWalletIndex } from "../wallet-pay-index.js";
|
|
27
26
|
import { recordWalletSpend, requireWalletGrant } from "../wallet-grant.js";
|
|
28
27
|
import { formatBytes } from "./wallet-storage.js";
|
|
29
|
-
const FUSE_WHY = {
|
|
30
|
-
same: "those are the same resource.",
|
|
31
|
-
differentSize: "their periods differ and so do their sizes — the contract joins sizes only over an identical period, and periods only at an identical size.",
|
|
32
|
-
differentPeriod: "their periods differ.",
|
|
33
|
-
notAdjacent: "their sizes are equal but their periods do not touch — one has to end where the other starts.",
|
|
34
|
-
};
|
|
35
28
|
export async function walletStorageOps(op, rest, options = {}) {
|
|
36
29
|
const say = options.write ?? ((line) => process.stdout.write(`${line}\n`));
|
|
37
30
|
const resolved = await requireAccountCode();
|
|
@@ -42,57 +35,43 @@ export async function walletStorageOps(op, rest, options = {}) {
|
|
|
42
35
|
const stored = resolved.source === "file" || resolved.source === "file-locked" ? readCredentialsFile() : null;
|
|
43
36
|
const server = resolveServer(options.server ?? stored?.server);
|
|
44
37
|
const network = resolveNetwork(server, options.network ?? stored?.network);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
exitCode:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
38
|
+
// What was typed, as values. Everything past this is judged against the chain's own answer.
|
|
39
|
+
const { ask, command } = asked(op, rest, options);
|
|
40
|
+
// ⛔ WITHOUT `--yes` THE RUN STOPS AT THE REVIEW, exactly as `--dry-run` does — the two differ
|
|
41
|
+
// only in what is said afterwards and in the exit code, which is this file's business.
|
|
42
|
+
const stopAtReview = options.dryRun === true || options.yes !== true;
|
|
43
|
+
const outcome = await reshapeStorage({ network, code: resolved.code, wallet, address }, ask, {
|
|
44
|
+
reads: options.storageReads,
|
|
45
|
+
sign: options.signStorage,
|
|
46
|
+
dryRun: stopAtReview,
|
|
47
|
+
// ⚠ THE SENTENCES A PERSON READS ARE THIS FILE'S. The library judges; what a refusal calls
|
|
48
|
+
// the thing that was wrong — a flag, another command — belongs where somebody typed it.
|
|
49
|
+
hints: {
|
|
50
|
+
cannotRead: "Nothing was signed.",
|
|
51
|
+
notHeld: `\`${BINARY_NAME} wallet storage\` lists the ones it holds. A resource bound inside a file is not free.`,
|
|
52
|
+
cut: (what, limit) => new NmtsError(what === "size"
|
|
53
|
+
? `--size must be above 0 and below the resource's ${limit}.`
|
|
54
|
+
: `--epochs must be a whole number above 0 and below the resource's ${limit} epochs.`, { exitCode: 2 }),
|
|
55
|
+
},
|
|
56
|
+
onReview: (review) => {
|
|
57
|
+
if (!options.json)
|
|
58
|
+
describe(say, review, wallet);
|
|
59
|
+
},
|
|
60
|
+
// ⑥ The unlock, held against the fee this review measured. ⛔ Throwing here signs nothing.
|
|
61
|
+
agree: (review) => {
|
|
62
|
+
requireWalletGrant(review.plan.action, spendOf(review), new Date(options.now ?? Date.now()));
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const { review } = outcome;
|
|
66
|
+
const facts = {
|
|
67
|
+
op,
|
|
68
|
+
network,
|
|
69
|
+
address,
|
|
70
|
+
shape: review.plan.shape,
|
|
71
|
+
feeSui: review.feeMist === null ? null : coinAmount(review.feeMist),
|
|
72
|
+
currentEpoch: review.currentEpoch,
|
|
66
73
|
};
|
|
67
|
-
|
|
68
|
-
const { shape, action, lines, command } = plan(op, rest, options, held);
|
|
69
|
-
const walrusPackageId = await reads.walrusPackageId();
|
|
70
|
-
// ③ The dry run: the fee, or the contract's own refusal.
|
|
71
|
-
const verdict = await reads.dryRun(shape, address);
|
|
72
|
-
if (verdict.refusal !== null) {
|
|
73
|
-
throw new NmtsError(`The chain would refuse this: ${verdict.refusal}`, {
|
|
74
|
-
exitCode: 4,
|
|
75
|
-
nextStep: "Nothing was signed and no fee was spent.",
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
const feeMist = verdict.feeMist;
|
|
79
|
-
const facts = { op, network, address, shape, feeSui: feeMist === null ? null : coinAmount(feeMist), currentEpoch: read.currentEpoch };
|
|
80
|
-
// ④ The review.
|
|
81
|
-
if (!options.json) {
|
|
82
|
-
say(`Would ${lines.what}`);
|
|
83
|
-
say(` from ${address} (wallet ${wallet})`);
|
|
84
|
-
for (const l of lines.detail)
|
|
85
|
-
say(` ${l}`);
|
|
86
|
-
say(feeMist === null
|
|
87
|
-
? ` Chain fee (SUI): could not be measured just now; it is charged with the signature.`
|
|
88
|
-
: ` Chain fee about ${facts.feeSui} SUI, measured by a dry run just now.`);
|
|
89
|
-
if (op === "transfer") {
|
|
90
|
-
say(` ⛔ No file goes with it. What goes is size and remaining time; the files stay sealed with this`);
|
|
91
|
-
say(` account's keys. It cannot be undone, and NMTS cannot recall it. Check the address once more.`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
// ⑤ Without --yes, that is the end.
|
|
95
|
-
if (options.dryRun === true || options.yes !== true) {
|
|
74
|
+
if (outcome.kind === "review") {
|
|
96
75
|
if (options.json) {
|
|
97
76
|
say(JSON.stringify({ ...facts, signed: false, dryRun: options.dryRun === true }));
|
|
98
77
|
return options.dryRun === true ? 0 : 4;
|
|
@@ -107,61 +86,86 @@ export async function walletStorageOps(op, rest, options = {}) {
|
|
|
107
86
|
nextStep: `Read the review above, then: \`${command} --yes\``,
|
|
108
87
|
});
|
|
109
88
|
}
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
requireWalletGrant(action, spend, new Date(options.now ?? Date.now()));
|
|
113
|
-
const sign = options.signStorage ?? (await import("../wallet-sign.js")).signStorageOp;
|
|
114
|
-
const digest = await sign({ network, code: resolved.code, wallet, shape, walrusPackageId });
|
|
115
|
-
recordWalletSpend(spend);
|
|
89
|
+
// What left the wallet is added to this machine's ledger as the dry run measured it.
|
|
90
|
+
recordWalletSpend(spendOf(review));
|
|
116
91
|
if (options.json) {
|
|
117
|
-
say(JSON.stringify({ ...facts, signed: true, digest, explorerUrl: explorerTxUrl(digest, network) }));
|
|
92
|
+
say(JSON.stringify({ ...facts, signed: true, digest: outcome.digest, explorerUrl: explorerTxUrl(outcome.digest, network) }));
|
|
118
93
|
return 0;
|
|
119
94
|
}
|
|
120
95
|
say(``);
|
|
121
|
-
say(` Done. Transaction ${digest}`);
|
|
122
|
-
say(` ${explorerTxUrl(digest, network)}`);
|
|
96
|
+
say(` Done. Transaction ${outcome.digest}`);
|
|
97
|
+
say(` ${explorerTxUrl(outcome.digest, network)}`);
|
|
123
98
|
say(` \`${BINARY_NAME} wallet storage\` lists what the wallet holds now.`);
|
|
124
99
|
return 0;
|
|
125
100
|
}
|
|
126
|
-
/**
|
|
127
|
-
function
|
|
101
|
+
/** What one change costs this wallet: no WAL, and the fee the dry run measured. */
|
|
102
|
+
function spendOf(review) {
|
|
103
|
+
return { walFrost: 0n, suiMist: review.feeMist ?? 0n };
|
|
104
|
+
}
|
|
105
|
+
/** The review a person reads, in the order somebody deciding needs it. */
|
|
106
|
+
function describe(say, review, wallet) {
|
|
107
|
+
const { plan } = review;
|
|
108
|
+
say(`Would ${what(plan)}`);
|
|
109
|
+
say(` from ${review.address} (wallet ${wallet})`);
|
|
110
|
+
for (const line of detail(plan))
|
|
111
|
+
say(` ${line}`);
|
|
112
|
+
say(review.feeMist === null
|
|
113
|
+
? ` Chain fee (SUI): could not be measured just now; it is charged with the signature.`
|
|
114
|
+
: ` Chain fee about ${coinAmount(review.feeMist)} SUI, measured by a dry run just now.`);
|
|
115
|
+
if (plan.op === "transfer") {
|
|
116
|
+
say(` ⛔ No file goes with it. What goes is size and remaining time; the files stay sealed with this`);
|
|
117
|
+
say(` account's keys. It cannot be undone, and NMTS cannot recall it. Check the address once more.`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** The one-line "what this would do", from the resources as the chain has them. */
|
|
121
|
+
function what(plan) {
|
|
122
|
+
const named = (at) => {
|
|
123
|
+
const r = plan.named[at];
|
|
124
|
+
return r === undefined ? "" : `${formatBytes(r.sizeBytes)} · epoch ${r.startEpoch} to ${r.endEpoch} (${r.objectId})`;
|
|
125
|
+
};
|
|
126
|
+
const result = plan.result;
|
|
127
|
+
if (result.kind === "split")
|
|
128
|
+
return `cut ${named(0)} by ${plan.shape.kind === "splitSize" ? "size" : "period"}.`;
|
|
129
|
+
if (result.kind === "merge")
|
|
130
|
+
return `join ${named(1)} into ${named(0)}.`;
|
|
131
|
+
return `hand ${named(0)} to ${result.to}.`;
|
|
132
|
+
}
|
|
133
|
+
/** What it would leave behind, said from the numbers the plan worked out. */
|
|
134
|
+
function detail(plan) {
|
|
135
|
+
const result = plan.result;
|
|
136
|
+
if (result.kind === "split") {
|
|
137
|
+
const { keeps, creates } = result;
|
|
138
|
+
return plan.shape.kind === "splitSize"
|
|
139
|
+
? [`It keeps ${formatBytes(keeps.sizeBytes)}; a new resource of ${formatBytes(creates.sizeBytes)} over the same period appears in this wallet.`]
|
|
140
|
+
: [`It keeps epoch ${keeps.startEpoch} to ${keeps.endEpoch}; a new resource of the same size over epoch ${creates.startEpoch} to ${creates.endEpoch} appears in this wallet.`];
|
|
141
|
+
}
|
|
142
|
+
if (result.kind === "merge") {
|
|
143
|
+
const b = result.becomes;
|
|
144
|
+
return [
|
|
145
|
+
result.how === "amount"
|
|
146
|
+
? `Same period, so the sizes add: the first becomes ${formatBytes(b.sizeBytes)} and the second is gone.`
|
|
147
|
+
: `Same size and touching periods, so the periods join: the first spans epoch ${b.startEpoch} to ${b.endEpoch} and the second is gone.`,
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
return [`Afterwards this wallet no longer holds it.`];
|
|
151
|
+
}
|
|
152
|
+
/** What was typed, as the values the library judges — and the line to run to go through with it. */
|
|
153
|
+
function asked(op, rest, options) {
|
|
128
154
|
const [a, b] = rest;
|
|
129
|
-
const resource = (r) => `${formatBytes(r.sizeBytes)} · epoch ${r.startEpoch} to ${r.endEpoch} (${r.objectId})`;
|
|
130
155
|
if (op === "split") {
|
|
131
156
|
if (a === undefined)
|
|
132
157
|
throw usage("split <resource id> --size <bytes> | --epochs <n>");
|
|
133
|
-
const r = held(a);
|
|
134
158
|
if (options.size !== undefined && options.epochs !== undefined)
|
|
135
159
|
throw usage("split takes --size OR --epochs, not both");
|
|
136
160
|
if (options.size !== undefined) {
|
|
137
|
-
const keep = parseBytes(options.size);
|
|
138
|
-
if (keep <= 0 || keep >= r.sizeBytes) {
|
|
139
|
-
throw new NmtsError(`--size must be above 0 and below the resource's ${formatBytes(r.sizeBytes)}.`, { exitCode: 2 });
|
|
140
|
-
}
|
|
141
161
|
return {
|
|
142
|
-
|
|
143
|
-
action: "reshape",
|
|
144
|
-
lines: {
|
|
145
|
-
what: `cut ${resource(r)} by size.`,
|
|
146
|
-
detail: [`It keeps ${formatBytes(keep)}; a new resource of ${formatBytes(r.sizeBytes - keep)} over the same period appears in this wallet.`],
|
|
147
|
-
},
|
|
162
|
+
ask: { kind: "splitSize", objectId: a, keepBytes: parseBytes(options.size) },
|
|
148
163
|
command: `${BINARY_NAME} wallet storage split ${a} --size ${options.size}`,
|
|
149
164
|
};
|
|
150
165
|
}
|
|
151
166
|
if (options.epochs !== undefined) {
|
|
152
|
-
const n = Number(options.epochs);
|
|
153
|
-
const span = r.endEpoch - r.startEpoch;
|
|
154
|
-
if (!Number.isSafeInteger(n) || n <= 0 || n >= span) {
|
|
155
|
-
throw new NmtsError(`--epochs must be a whole number above 0 and below the resource's ${span} epochs.`, { exitCode: 2 });
|
|
156
|
-
}
|
|
157
|
-
const at = r.startEpoch + n;
|
|
158
167
|
return {
|
|
159
|
-
|
|
160
|
-
action: "reshape",
|
|
161
|
-
lines: {
|
|
162
|
-
what: `cut ${resource(r)} by period.`,
|
|
163
|
-
detail: [`It keeps epoch ${r.startEpoch} to ${at}; a new resource of the same size over epoch ${at} to ${r.endEpoch} appears in this wallet.`],
|
|
164
|
-
},
|
|
168
|
+
ask: { kind: "splitEpochs", objectId: a, keepEpochs: Number(options.epochs) },
|
|
165
169
|
command: `${BINARY_NAME} wallet storage split ${a} --epochs ${options.epochs}`,
|
|
166
170
|
};
|
|
167
171
|
}
|
|
@@ -170,41 +174,11 @@ function plan(op, rest, options, held) {
|
|
|
170
174
|
if (op === "merge") {
|
|
171
175
|
if (a === undefined || b === undefined)
|
|
172
176
|
throw usage("merge <resource id> <resource id>");
|
|
173
|
-
|
|
174
|
-
const second = held(b);
|
|
175
|
-
const verdict = canFuse(first, second);
|
|
176
|
-
if (!verdict.can) {
|
|
177
|
-
throw new NmtsError(`These two cannot be joined: ${FUSE_WHY[verdict.why]}`, {
|
|
178
|
-
exitCode: 4,
|
|
179
|
-
nextStep: `Nothing was signed. \`${BINARY_NAME} wallet storage\` shows each one's size and period.`,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
return {
|
|
183
|
-
shape: { kind: "fuse", first: a, second: b, how: verdict.kind },
|
|
184
|
-
action: "reshape",
|
|
185
|
-
lines: {
|
|
186
|
-
what: `join ${resource(second)} into ${resource(first)}.`,
|
|
187
|
-
detail: [
|
|
188
|
-
verdict.kind === "amount"
|
|
189
|
-
? `Same period, so the sizes add: the first becomes ${formatBytes(first.sizeBytes + second.sizeBytes)} and the second is gone.`
|
|
190
|
-
: `Same size and touching periods, so the periods join: the first spans epoch ${Math.min(first.startEpoch, second.startEpoch)} to ${Math.max(first.endEpoch, second.endEpoch)} and the second is gone.`,
|
|
191
|
-
],
|
|
192
|
-
},
|
|
193
|
-
command: `${BINARY_NAME} wallet storage merge ${a} ${b}`,
|
|
194
|
-
};
|
|
177
|
+
return { ask: { kind: "merge", first: a, second: b }, command: `${BINARY_NAME} wallet storage merge ${a} ${b}` };
|
|
195
178
|
}
|
|
196
179
|
if (a === undefined || b === undefined)
|
|
197
180
|
throw usage("transfer <resource id> <address>");
|
|
198
|
-
|
|
199
|
-
if (!isValidSuiAddress(b)) {
|
|
200
|
-
throw new NmtsError("That is not a Sui address: it is 0x followed by 64 hexadecimal characters.", { exitCode: 2 });
|
|
201
|
-
}
|
|
202
|
-
return {
|
|
203
|
-
shape: { kind: "transfer", objectId: a, to: b },
|
|
204
|
-
action: "give",
|
|
205
|
-
lines: { what: `hand ${resource(r)} to ${b}.`, detail: [`Afterwards this wallet no longer holds it.`] },
|
|
206
|
-
command: `${BINARY_NAME} wallet storage transfer ${a} ${b}`,
|
|
207
|
-
};
|
|
181
|
+
return { ask: { kind: "transfer", objectId: a, to: b }, command: `${BINARY_NAME} wallet storage transfer ${a} ${b}` };
|
|
208
182
|
}
|
|
209
183
|
function usage(form) {
|
|
210
184
|
return new NmtsError(`\`${BINARY_NAME} wallet storage ${form}\``, { exitCode: 2 });
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { type Network } from "../network.ts";
|
|
2
|
-
import type
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
items: readonly StorageResource[];
|
|
6
|
-
currentEpoch: number | null;
|
|
7
|
-
}
|
|
2
|
+
import { type StorageRead } from "../storage-control.ts";
|
|
3
|
+
export type { StorageRead } from "../storage-control.ts";
|
|
4
|
+
export { formatBytes } from "../storage-control.ts";
|
|
8
5
|
export interface WalletStorageOptions {
|
|
9
6
|
server?: string | undefined;
|
|
10
7
|
network?: string | undefined;
|
|
@@ -14,5 +11,3 @@ export interface WalletStorageOptions {
|
|
|
14
11
|
readStorage?: (network: Network, address: string) => Promise<StorageRead>;
|
|
15
12
|
}
|
|
16
13
|
export declare function walletStorage(options?: WalletStorageOptions): Promise<number>;
|
|
17
|
-
/** Bytes for a person: binary units, two decimals, whole bytes below a KiB. */
|
|
18
|
-
export declare function formatBytes(bytes: number): string;
|
|
@@ -6,16 +6,16 @@
|
|
|
6
6
|
// fusing and spending a resource need a signature and are not here.
|
|
7
7
|
//
|
|
8
8
|
// ⛔ "NONE" AND "COULD NOT READ" ARE DIFFERENT SENTENCES — holding no resource is normal; failing to
|
|
9
|
-
// read is a reason to look again. The
|
|
10
|
-
//
|
|
9
|
+
// read is a reason to look again. The reading, the ordering and that refusal are
|
|
10
|
+
// `storage-control.ts`, which the SDK calls as well; this file is the terminal over it.
|
|
11
11
|
import { requireAccountCode } from "../code-access.js";
|
|
12
12
|
import { readCredentialsFile } from "../credentials.js";
|
|
13
|
-
import { NmtsError } from "../errors.js";
|
|
14
13
|
import { resolveNetwork } from "../network.js";
|
|
15
14
|
import { BINARY_NAME } from "../product.js";
|
|
15
|
+
import { formatBytes, listStorage, } from "../storage-control.js";
|
|
16
16
|
import { resolveServer } from "../server.js";
|
|
17
|
-
import { statusOf, totalUsableBytes, usableFirst } from "../shared/lib/storage-control/plan.js";
|
|
18
17
|
import { walletAddress } from "../wallet.js";
|
|
18
|
+
export { formatBytes } from "../storage-control.js";
|
|
19
19
|
export async function walletStorage(options = {}) {
|
|
20
20
|
const say = options.write ?? ((line) => process.stdout.write(`${line}\n`));
|
|
21
21
|
const resolved = await requireAccountCode();
|
|
@@ -23,33 +23,25 @@ export async function walletStorage(options = {}) {
|
|
|
23
23
|
const stored = resolved.source === "file" || resolved.source === "file-locked" ? readCredentialsFile() : null;
|
|
24
24
|
const server = resolveServer(options.server ?? stored?.server);
|
|
25
25
|
const network = resolveNetwork(server, options.network ?? stored?.network);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
throw new NmtsError("The storage resources could not be read from the chain.", {
|
|
34
|
-
exitCode: 1,
|
|
35
|
-
nextStep: `That is not the same as holding none. \`${BINARY_NAME} env\` says which network was asked. ` +
|
|
36
|
-
`Cause: ${error instanceof Error ? error.message : String(error)}`,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
const epoch = got.currentEpoch;
|
|
40
|
-
const items = epoch === null ? [...got.items] : usableFirst(got.items, epoch);
|
|
26
|
+
// ⚠ THE SENTENCE A PERSON READS IS THIS FILE'S, not the library's: `nmts env` is a command at a
|
|
27
|
+
// prompt and means nothing inside somebody else's program.
|
|
28
|
+
const listed = await listStorage({ network, address }, options.readStorage, {
|
|
29
|
+
cannotRead: `That is not the same as holding none. \`${BINARY_NAME} env\` says which network was asked.`,
|
|
30
|
+
});
|
|
31
|
+
const epoch = listed.currentEpoch;
|
|
32
|
+
const items = listed.items;
|
|
41
33
|
if (options.json) {
|
|
42
34
|
say(JSON.stringify({
|
|
43
35
|
address,
|
|
44
36
|
network,
|
|
45
37
|
currentEpoch: epoch,
|
|
46
|
-
usableBytes:
|
|
38
|
+
usableBytes: listed.usableBytes,
|
|
47
39
|
resources: items.map((r) => ({
|
|
48
40
|
objectId: r.objectId,
|
|
49
41
|
sizeBytes: r.sizeBytes,
|
|
50
42
|
startEpoch: r.startEpoch,
|
|
51
43
|
endEpoch: r.endEpoch,
|
|
52
|
-
status:
|
|
44
|
+
status: r.status,
|
|
53
45
|
})),
|
|
54
46
|
}));
|
|
55
47
|
return 0;
|
|
@@ -61,10 +53,10 @@ export async function walletStorage(options = {}) {
|
|
|
61
53
|
say(` This wallet holds no free storage resource. Anything bound inside a file is not listed here.`);
|
|
62
54
|
}
|
|
63
55
|
else {
|
|
64
|
-
if (
|
|
65
|
-
say(` ${formatBytes(
|
|
56
|
+
if (listed.usableBytes !== null)
|
|
57
|
+
say(` ${formatBytes(listed.usableBytes)} usable now, in ${items.length} resource${items.length === 1 ? "" : "s"}.`);
|
|
66
58
|
for (const r of items) {
|
|
67
|
-
const status =
|
|
59
|
+
const status = r.status === null ? "" : ` ${statusWord(r.status)}`;
|
|
68
60
|
say(` ${formatBytes(r.sizeBytes).padStart(11)} · epoch ${r.startEpoch} to ${r.endEpoch}${status}`);
|
|
69
61
|
say(` ${r.objectId}`);
|
|
70
62
|
}
|
|
@@ -80,16 +72,3 @@ export async function walletStorage(options = {}) {
|
|
|
80
72
|
function statusWord(status) {
|
|
81
73
|
return status === "usable" ? "usable now" : status === "notYet" ? "not started" : "ended";
|
|
82
74
|
}
|
|
83
|
-
/** Bytes for a person: binary units, two decimals, whole bytes below a KiB. */
|
|
84
|
-
export function formatBytes(bytes) {
|
|
85
|
-
const units = ["KiB", "MiB", "GiB", "TiB"];
|
|
86
|
-
let value = bytes;
|
|
87
|
-
let unit = "B";
|
|
88
|
-
for (const next of units) {
|
|
89
|
-
if (value < 1024)
|
|
90
|
-
break;
|
|
91
|
-
value /= 1024;
|
|
92
|
-
unit = next;
|
|
93
|
-
}
|
|
94
|
-
return unit === "B" ? `${bytes} B` : `${value.toFixed(2)} ${unit}`;
|
|
95
|
-
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type ListEditInput } from "./manifest-write.ts";
|
|
2
|
+
/** What the erasing underneath takes: where to talk, what opens the list, and whose list it is. */
|
|
3
|
+
export interface EraseInput extends ListEditInput {
|
|
4
|
+
/**
|
|
5
|
+
* The account code's own proof for this one run, base64url — what the two permanent doors ask
|
|
6
|
+
* for beside the credential (`x-nmts-account-proof`).
|
|
7
|
+
*
|
|
8
|
+
* ⛔ BUILT BY THE CALLER AND KEPT BY NOBODY. It is not the NMTS key and opens no file; what it
|
|
9
|
+
* proves is possession of the code, which is exactly the question these two doors ask.
|
|
10
|
+
*/
|
|
11
|
+
accountProof: string;
|
|
12
|
+
}
|
|
13
|
+
export interface EraseOptions {
|
|
14
|
+
/** Also destroy the treasury's storage under credit-paid files, before erasing them. */
|
|
15
|
+
releaseStorage?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** One thing this run acts on: its id, and where it sits in the list as it was read. */
|
|
18
|
+
export interface ErasePath {
|
|
19
|
+
id: string;
|
|
20
|
+
path: string;
|
|
21
|
+
}
|
|
22
|
+
/** What one run will destroy, worked out before anything is sent. */
|
|
23
|
+
export interface ErasePlan {
|
|
24
|
+
/** Every FILE going: the ones named, and every file under a folder that was named. */
|
|
25
|
+
readonly files: readonly ErasePath[];
|
|
26
|
+
/**
|
|
27
|
+
* The ids leaving the sealed list — the files above and the folders that were named.
|
|
28
|
+
*
|
|
29
|
+
* ⚠ WIDER THAN `files` ON PURPOSE. A named folder has no server row of its own, so nothing is
|
|
30
|
+
* erased for it; its entry still has to go, or the list keeps a folder whose contents are gone.
|
|
31
|
+
*/
|
|
32
|
+
readonly going: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
/** What one file's storage release came back with. */
|
|
35
|
+
export interface StorageRelease {
|
|
36
|
+
path: string;
|
|
37
|
+
released: number;
|
|
38
|
+
alreadyReleased: number;
|
|
39
|
+
failed: number;
|
|
40
|
+
/**
|
|
41
|
+
* Credits the release actually cost, and where they came from.
|
|
42
|
+
*
|
|
43
|
+
* ⛔ TWO FIELDS, NOT ONE, because "it cost nothing" and "it cost nothing OUT OF THE BALANCE" are
|
|
44
|
+
* different answers and only one of them is true. A release paid out of the file's own
|
|
45
|
+
* deposit charges the balance nothing; a file with no deposit pays twice the fee from the
|
|
46
|
+
* balance, and somebody watching their credits needs to be able to tell which happened.
|
|
47
|
+
*/
|
|
48
|
+
feeCredits: number;
|
|
49
|
+
fromDeposit: boolean;
|
|
50
|
+
/** The server's typed refusal when the storage was not the treasury's to destroy. */
|
|
51
|
+
refused: string | null;
|
|
52
|
+
}
|
|
53
|
+
/** What one run did. */
|
|
54
|
+
export interface EraseOutcome {
|
|
55
|
+
/** How many server rows went. Lower than what was asked for when one was already gone. */
|
|
56
|
+
erased: number;
|
|
57
|
+
/** The files it acted on, with the paths they had. */
|
|
58
|
+
files: ErasePath[];
|
|
59
|
+
/** One per file whose storage was asked about — empty unless `releaseStorage` was asked for. */
|
|
60
|
+
releases: StorageRelease[];
|
|
61
|
+
/** The version of the sealed list after the entries left it. */
|
|
62
|
+
seq: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Work out what erasing these paths would destroy, without destroying anything.
|
|
66
|
+
*
|
|
67
|
+
* ⛔ ITS OWN STEP BECAUSE ONE CALLER HAS TO SHOW THE LIST BEFORE IT ASKS. `nmts erase` prints every
|
|
68
|
+
* file it is about to destroy and then waits for a typed sentence; folding this into the act
|
|
69
|
+
* would leave the terminal with nothing to print, and reading the list twice would leave the
|
|
70
|
+
* two reads free to disagree about what is in it.
|
|
71
|
+
*/
|
|
72
|
+
export declare function planErase(input: ListEditInput, paths: readonly string[]): Promise<ErasePlan>;
|
|
73
|
+
/**
|
|
74
|
+
* Destroy what the plan names. ⛔ Irreversible, and nothing below asks whether the caller meant it.
|
|
75
|
+
*/
|
|
76
|
+
export declare function eraseFiles(input: EraseInput, plan: ErasePlan, options?: EraseOptions): Promise<EraseOutcome>;
|
|
77
|
+
/**
|
|
78
|
+
* Resolve these paths and destroy what they name, in one call.
|
|
79
|
+
*
|
|
80
|
+
* ⚠ FOR A CALLER THAT HAS ALREADY DECIDED. Anything that shows a person what is about to go should
|
|
81
|
+
* use `planErase` first, so what it shows is what it then destroys.
|
|
82
|
+
*/
|
|
83
|
+
export declare function erasePaths(input: EraseInput, paths: readonly string[], options?: EraseOptions): Promise<EraseOutcome>;
|