@needmoretruth/nmts-cli 0.39.0 → 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.
@@ -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. The storage-control half a terminal can do: the browser's
3
- // rules (`shared/lib/storage-control/plan.ts`, copied byte-for-byte) say what the contract allows;
4
- // the dry run is the final judge; the review comes before the signature; `--yes` is the answer.
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, as in `wallet-send.ts`: ① 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
8
- // exact transaction is dry-run for its fee, and a refusal there is printed and ends the run
9
- // ④ the review ⑤ without `--yes` that is the end ⑥ the wallet unlock is held against the fee —
10
- // scope `storage` for cutting and joining, scope `all` for handing over ⑦ only then the signature.
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 { canFuse, statusOf } from "../shared/lib/storage-control/plan.js";
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
- const reads = (options.storageReads ?? storageOpsReads)(network);
46
- // The resources, and which epoch it is.
47
- let read;
48
- try {
49
- read = await reads.readStorage(address);
50
- }
51
- catch (error) {
52
- throw new NmtsError("The storage resources could not be read from the chain.", {
53
- exitCode: 1,
54
- nextStep: `Nothing was signed. Cause: ${error instanceof Error ? error.message : String(error)}`,
55
- });
56
- }
57
- const held = (id) => {
58
- const found = read.items.find((r) => r.objectId === id);
59
- if (found === undefined) {
60
- throw new NmtsError(`This wallet holds no free storage resource ${id}.`, {
61
- exitCode: 4,
62
- nextStep: `\`${BINARY_NAME} wallet storage\` lists the ones it holds. A resource bound inside a file is not free.`,
63
- });
64
- }
65
- return found;
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
- // The request, judged.
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
- // The unlock, held against the fee. The signature.
111
- const spend = { walFrost: 0n, suiMist: feeMist ?? 0n };
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
- /** The shape one request is, the unlock action it needs, and the review's words. */
127
- function plan(op, rest, options, held) {
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
- shape: { kind: "splitSize", objectId: a, keepBytes: keep },
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
- shape: { kind: "splitEpoch", objectId: a, splitEpoch: at },
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
- const first = held(a);
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
- const r = held(a);
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 { StorageResource } from "../shared/lib/storage-control/chain.ts";
3
- /** What the chain said: the resources, and which epoch it is (null when the clock could not be read). */
4
- export interface StorageRead {
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 reader (`shared/lib/storage-control/chain.ts`, the
10
- // browser's own, copied byte-for-byte) throws rather than answering an empty list.
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
- const read = options.readStorage ??
27
- (async (net, addr) => (await import("../wallet-storage-chain.js")).readWalletStorage(net, addr));
28
- let got;
29
- try {
30
- got = await read(network, address);
31
- }
32
- catch (error) {
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: epoch === null ? null : totalUsableBytes(items, epoch),
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: epoch === null ? null : statusOf(r, epoch),
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 (epoch !== null)
65
- say(` ${formatBytes(totalUsableBytes(items, epoch))} usable now, in ${items.length} resource${items.length === 1 ? "" : "s"}.`);
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 = epoch === null ? "" : ` ${statusWord(statusOf(r, epoch))}`;
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
- }
package/dist/product.d.ts CHANGED
@@ -9,7 +9,7 @@ export declare const BINARY_NAME = "nmts";
9
9
  * beside it. Here rather than in `main.ts` because the MCP server has to say it too, and a
10
10
  * command importing the entry point is a cycle waiting to bite.
11
11
  */
12
- export declare const VERSION = "0.39.0";
12
+ export declare const VERSION = "0.40.0";
13
13
  /** Where the product lives, for messages that need to send somebody somewhere real. */
14
14
  export declare const HOME_URL = "https://nmts.me";
15
15
  /** The source, so a person holding only the built program can find what it was built from. */
package/dist/product.js CHANGED
@@ -18,7 +18,7 @@ export const BINARY_NAME = "nmts";
18
18
  * beside it. Here rather than in `main.ts` because the MCP server has to say it too, and a
19
19
  * command importing the entry point is a cycle waiting to bite.
20
20
  */
21
- export const VERSION = "0.39.0";
21
+ export const VERSION = "0.40.0";
22
22
  /** Where the product lives, for messages that need to send somebody somewhere real. */
23
23
  export const HOME_URL = "https://nmts.me";
24
24
  /** The source, so a person holding only the built program can find what it was built from. */
@@ -0,0 +1,124 @@
1
+ import { type Budget } from "../extend-budget.ts";
2
+ import { type DaysLeft, type ExpiryStage } from "../expiry.ts";
3
+ import { type ExtendPreview, type ExtendReads, type SignExtension } from "../extend-plan.ts";
4
+ import type { StorageHints } from "./hints.ts";
5
+ import { type AccountSettings } from "../shared/lib/drive/manifest-settings.ts";
6
+ /** Where to talk, what opens the list, and whose list it is. */
7
+ export interface ExtendInput {
8
+ server: string;
9
+ apiKey: string;
10
+ /** ⛔ The NMTS key. It opens the list and derives the wallet that pays; it goes nowhere else. */
11
+ code: string;
12
+ accountId: string;
13
+ network: string;
14
+ }
15
+ /**
16
+ * Everything one run worked out, in one shape.
17
+ *
18
+ * ⛔ ONE OBJECT SO TWO ANSWERS CANNOT DISAGREE. The words a person reads and the JSON a program
19
+ * reads are built from this and from nothing else; when they were assembled separately the day
20
+ * count in one of them was the count before the extension and in the other the count after.
21
+ */
22
+ export interface ExtendFacts {
23
+ file: string;
24
+ itemId: string;
25
+ network: string;
26
+ epoch: number;
27
+ endEpoch: number;
28
+ epochs: number;
29
+ newEndEpoch: number;
30
+ daysLeft: DaysLeft;
31
+ daysLeftAfter: DaysLeft;
32
+ blobs: number;
33
+ /** ⚠ A STRING: base units run past what a JSON number keeps without losing digits. */
34
+ priceFrost: string;
35
+ priceWal: string;
36
+ /** ⛔ Said in the machine-readable answer too. A program spending WAL should not have to infer it. */
37
+ paidFrom: "wallet";
38
+ filesOnTheSameBlobs: number;
39
+ partsThatCannotBeExtended: number;
40
+ /** The address that would sign. */
41
+ wallet: string;
42
+ /** What it holds, as amounts — null when the chain could not say. ⛔ Never zero for unread. */
43
+ walletWal: string | null;
44
+ walletSui: string | null;
45
+ /** The chain fee a dry run measured — null when it could not be measured. */
46
+ feeMist: string | null;
47
+ feeSui: string | null;
48
+ }
49
+ /** What one extension would buy, and everything the decision to buy it rests on. */
50
+ export interface ExtendPlan {
51
+ facts: ExtendFacts;
52
+ itemId: string;
53
+ /** The file's full path in the account, as the list spells it. */
54
+ path: string;
55
+ /** The blobs the transaction would name. */
56
+ objectIds: readonly string[];
57
+ epochs: number;
58
+ /**
59
+ * ⚠ THE WALLET'S NUMBER. `facts.wallet` is that wallet's address — the one the price was measured
60
+ * against, and the one that signs.
61
+ */
62
+ wallet: number;
63
+ /**
64
+ * How close this file is to running out.
65
+ *
66
+ * ⛔ `later` IS NOT A REFUSAL HERE. Extending early loses nothing — the epochs are added to what
67
+ * is left — so whether to spend now for time a file does not need yet is the caller's to
68
+ * decide, and each surface names its own way of saying yes.
69
+ */
70
+ stage: ExpiryStage;
71
+ budget: Budget;
72
+ /** The sealed list's settings, as read for this price. */
73
+ settings: AccountSettings | undefined;
74
+ }
75
+ /** The chain reads. ⚠ A SEAM, NOT AN OPTION — no flag and no caller argument reaches it. */
76
+ export interface ExtendPlanSeams {
77
+ readChain?: ((network: string) => Promise<ExtendReads> | ExtendReads) | undefined;
78
+ /** How many of the storage network's epochs to add. Default `DEFAULT_EXTEND_EPOCHS`. */
79
+ epochs?: string | number | undefined;
80
+ /** Which of this key's wallets pays. Absent = the account's own number, out of the list read here. */
81
+ wallet?: number | undefined;
82
+ /** The instant to measure against. Passed in so one run reports one moment. */
83
+ now: number;
84
+ /** What this caller wants said in a refusal instead of the neutral sentence. */
85
+ hints?: StorageHints | undefined;
86
+ }
87
+ /**
88
+ * Work out what extending this file would buy and what it would cost, without signing anything.
89
+ *
90
+ * ⛔ NO KEY IS DERIVED FOR SIGNING AND NO SIGNER IS LOADED. Everything here is a read, which is
91
+ * what lets a price be asked for without an agreement being asked for first.
92
+ */
93
+ export declare function planExtension(input: ExtendInput, target: string, seams: ExtendPlanSeams): Promise<ExtendPlan>;
94
+ /** What one extension did. ⛔ By the time this exists, the storage IS extended and paid for. */
95
+ export interface ExtendOutcome {
96
+ /** The transaction digest — what the server records as the replay guard. */
97
+ digest: string;
98
+ /** Whether the server wrote the new date down. */
99
+ recorded: boolean;
100
+ /** True when the server had already recorded this digest, so nothing was written twice. */
101
+ replay: boolean;
102
+ /**
103
+ * Why the date was not written down, in the words the failure gave. Null when it was.
104
+ *
105
+ * ⛔ IT IS NOT AN EXCEPTION, and that is the whole point: the money is already spent, so a caller
106
+ * that saw a throw here would reasonably try again — and trying again pays again.
107
+ */
108
+ notRecorded: string | null;
109
+ }
110
+ /** The signature, and the one moment a caller may want between it and the server. ⚠ Seams. */
111
+ export interface ExtendApplySeams {
112
+ sign?: SignExtension | undefined;
113
+ /** Told the instant the signature exists — before the server is asked to write the date down. */
114
+ onSigned?: ((digest: string) => void) | undefined;
115
+ }
116
+ /**
117
+ * Buy it. ⛔ THIS SIGNS, and nothing below this line can be undone by anybody, NMTS included.
118
+ *
119
+ * Nothing here asks whether the caller meant it: the price, both balances and any shortfall are in
120
+ * the plan, and calling this is the answer.
121
+ */
122
+ export declare function applyExtension(input: ExtendInput, plan: ExtendPlan, seams?: ExtendApplySeams): Promise<ExtendOutcome>;
123
+ /** Why a file has nothing to extend, said as the two different things it can be. */
124
+ export declare function nothingToExtend(preview: ExtendPreview): string;