@needmoretruth/nmts-cli 0.34.4 → 0.35.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.ko.md +15 -15
  3. package/README.md +1 -1
  4. package/dist/arg-options.d.ts +167 -0
  5. package/dist/arg-options.js +10 -0
  6. package/dist/args.d.ts +2 -157
  7. package/dist/args.js +2 -0
  8. package/dist/artifact-about.d.ts +1 -1
  9. package/dist/commands/extend.d.ts +2 -0
  10. package/dist/commands/extend.js +8 -1
  11. package/dist/commands/push-wallet.js +6 -0
  12. package/dist/commands/push.d.ts +2 -0
  13. package/dist/commands/put-payer.d.ts +8 -0
  14. package/dist/commands/put-payer.js +41 -0
  15. package/dist/commands/put-wallet.d.ts +9 -3
  16. package/dist/commands/put-wallet.js +105 -185
  17. package/dist/commands/put.d.ts +5 -7
  18. package/dist/commands/put.js +3 -26
  19. package/dist/commands/wallet-donate.d.ts +3 -0
  20. package/dist/commands/wallet-donate.js +6 -2
  21. package/dist/commands/wallet-list.d.ts +18 -0
  22. package/dist/commands/wallet-list.js +116 -0
  23. package/dist/commands/wallet-send.d.ts +4 -0
  24. package/dist/commands/wallet-send.js +6 -2
  25. package/dist/commands/wallet-use.d.ts +7 -0
  26. package/dist/commands/wallet-use.js +55 -0
  27. package/dist/commands/wallet.d.ts +4 -0
  28. package/dist/commands/wallet.js +20 -8
  29. package/dist/extend-plan.d.ts +2 -0
  30. package/dist/help.js +3 -1
  31. package/dist/index.d.ts +10 -1
  32. package/dist/index.js +14 -1
  33. package/dist/main.js +4 -4
  34. package/dist/product.d.ts +1 -1
  35. package/dist/product.js +1 -1
  36. package/dist/risk.d.ts +4 -0
  37. package/dist/risk.js +6 -0
  38. package/dist/shared/lib/drive/manifest-ops.d.ts +8 -30
  39. package/dist/shared/lib/drive/manifest-ops.js +7 -39
  40. package/dist/shared/lib/drive/manifest-settings-patch.d.ts +52 -0
  41. package/dist/shared/lib/drive/manifest-settings-patch.js +110 -0
  42. package/dist/shared/lib/drive/manifest-settings.d.ts +55 -4
  43. package/dist/shared/lib/drive/manifest-settings.js +66 -28
  44. package/dist/shared/lib/wallet/discover.d.ts +54 -0
  45. package/dist/shared/lib/wallet/discover.js +66 -0
  46. package/dist/standing-tip.d.ts +3 -0
  47. package/dist/standing-tip.js +1 -0
  48. package/dist/upload-wallet-put.d.ts +118 -0
  49. package/dist/upload-wallet-put.js +221 -0
  50. package/dist/upload-wallet.d.ts +3 -0
  51. package/dist/upload-wallet.js +2 -0
  52. package/dist/wallet-list-chain.d.ts +4 -0
  53. package/dist/wallet-list-chain.js +21 -0
  54. package/dist/wallet-pay-index.d.ts +21 -0
  55. package/dist/wallet-pay-index.js +68 -0
  56. package/dist/wallet-sign-seams.d.ts +57 -0
  57. package/dist/wallet-sign-seams.js +16 -0
  58. package/dist/wallet-sign.d.ts +3 -53
  59. package/dist/wallet-sign.js +22 -15
  60. package/dist/wallet.d.ts +13 -13
  61. package/dist/wallet.js +15 -15
  62. package/docs/commands/wallet.md +18 -4
  63. package/package.json +1 -1
@@ -15,6 +15,8 @@ export interface WalletPayOptions {
15
15
  epochs?: string | number | undefined;
16
16
  /** `fit`, `whole`, or a held resource's object id. Absent = buy new storage. */
17
17
  storage?: string | undefined;
18
+ /** `--wallet N`: which wallet pays, this run only. Absent = the account's own number. */
19
+ wallet?: string | undefined;
18
20
  /** The instant the wallet agreement is measured against. */
19
21
  now?: number;
20
22
  /** ⚠ A SEAM, NOT AN OPTION — no flag reaches it. */
@@ -54,16 +56,20 @@ export interface WalletUploadContext {
54
56
  onCollision?: OnCollision | undefined;
55
57
  /** The sealed list's account settings, as read for this run — the standing tip lives in them. */
56
58
  settings?: AccountSettings | undefined;
59
+ /** Which of this key's wallets pays (`wallet-pay-index.ts`). Read from the same list as above. */
60
+ wallet: number;
57
61
  progress: Progress;
58
62
  say: (line: string) => void;
59
63
  json: boolean;
60
64
  }
61
65
  export declare function putWithWallet(target: string | undefined, options: PutWalletOptions): Promise<number>;
62
66
  /**
63
- * Review, agree, upload and record ONE file. Null when `--dry-run` stopped at the review.
67
+ * Review, agree, upload and record ONE file for a person who is watching. Null when `--dry-run`
68
+ * stopped at the review.
64
69
  *
65
- * ⛔ THE SIGNING MODULE IS LOADED AFTER THE AGREEMENT, and only then a dry run and a refusal
66
- * never bring the code that can spend into memory.
70
+ * ⛔ THE AGREEMENT HANDED IN BELOW IS THIS MACHINE'S GRANT, held against the total the review
71
+ * names. It is the one gate between a program and somebody's wallet, and the reason it is
72
+ * handed to the library rather than living inside it: a library call has nobody to ask.
67
73
  */
68
74
  export declare function uploadOneWithWallet(ctx: WalletUploadContext, file: WalletUploadFile, options: WalletPayOptions & {
69
75
  dryRun?: boolean | undefined;
@@ -1,16 +1,14 @@
1
1
  // `nmts put <file> --pay wallet` — one file in, sealed on this machine, its storage bought by the
2
2
  // PERSON'S OWN WALLET on the storage network instead of by credits.
3
3
  //
4
- // ⛔ IT PRICES BEFORE IT SIGNS, ALWAYS, in the order `extend` set: the parts are planned, the chain
5
- // quotes each part in WAL and the relay's tip in SUI, the register transaction is dry-run for
6
- // its fee, both balances are read and only then is the review printed, a known shortfall
7
- // refused, and the `wallet` agreement (scope `storage`) held against the total. `--dry-run`
8
- // stops at the review and never loads the signing module.
4
+ // ⛔ THE UPLOAD ITSELF IS NOT HERE. `upload-wallet-put.ts` prices, refuses, signs, pushes and
5
+ // records; this file is the terminal around it the arguments, the printed review, the wallet
6
+ // agreement this machine holds, the spending ledger and the standing gift. The split is what
7
+ // lets the SDK pay from a wallet without a second copy of the order that keeps money safe.
9
8
  //
10
- // ⛔ THE SERVER IS TOLD THE STORAGE IS THE PERSON'S BY SAYING NOTHING ELSE. `POST /v1/items` forces
11
- // treasury ownership only on a part that names a certified reservation; a wallet-paid part names
12
- // none and carries the blob object and the end epoch instead, exactly as the browser's own
13
- // wallet-paid commit does (`upload-steps.ts`).
9
+ // ⛔ THE REVIEW IS PRINTED BEFORE ANY REFUSAL, and the agreement is asked for only after a known
10
+ // shortfall has already stopped the run so a person who cannot afford it reads the numbers
11
+ // instead of a question. `--dry-run` stops at the review and never loads the signing module.
14
12
  //
15
13
  // ⛔ A HELD RESOURCE IS OFFERED, NEVER CHOSEN FOR THE PERSON — the owner's rule for storage
16
14
  // resources: the leftover is shown in bytes and the person decides. The review names how many
@@ -20,27 +18,22 @@ import { basename, resolve } from "node:path";
20
18
  import { identityOf } from "../account.js";
21
19
  import { requireAccountCode } from "../code-access.js";
22
20
  import { parseAsked } from "../collision.js";
23
- import { DERIVED, loadCrypto } from "../crypto.js";
21
+ import { loadCrypto } from "../crypto.js";
24
22
  import { API_KEY_ENV_VAR, readCredentialsFile, resolveApiKey } from "../credentials.js";
25
23
  import { NmtsError } from "../errors.js";
26
- import { setTrashed } from "../item-trash.js";
27
- import { addEntry } from "../manifest-write.js";
24
+ import { payingWalletIndex } from "../wallet-pay-index.js";
25
+ import { activeWalletOf } from "../shared/lib/drive/manifest-settings.js";
28
26
  import { paddingRuleOf, readFileList } from "../manifest.js";
29
27
  import { resolveNetwork } from "../network.js";
30
28
  import { BINARY_NAME } from "../product.js";
31
29
  import { Progress, silentSink, stderrSink } from "../progress.js";
32
- import { sealedLenFor } from "../seal.js";
33
30
  import { resolveServer } from "../server.js";
34
- import { statusOf } from "../shared/lib/storage-control/plan.js";
35
- import { createUploadApi } from "../upload-api.js";
36
- import { fileSource, partKeysOf, uploadFile } from "../upload-file.js";
37
- import { CREDIT_BYTES, measureLocal, partSizeFor, planAndPrice } from "../upload-price.js";
38
- import { clearItemRecord, clearReservation } from "../upload-store.js";
39
- import { walletRail } from "../upload-wallet.js";
40
- import { chooseUploadEpochs, daysOf, describeUploadReview, parseStorageAsk, pickResource, uploadBudget, uploadShortfallNextStep, walPrice, } from "../upload-wallet-plan.js";
41
- import { coinAmount, walletAddress } from "../wallet.js";
31
+ import { fileSource } from "../upload-file.js";
32
+ import { measureLocal, partSizeFor } from "../upload-price.js";
33
+ import { describeUploadReview } from "../upload-wallet-plan.js";
34
+ import { walletPut } from "../upload-wallet-put.js";
35
+ import { coinAmount } from "../wallet.js";
42
36
  import { recordWalletSpend, requireWalletGrant } from "../wallet-grant.js";
43
- import { createBlobProtocol } from "../walrus-write.js";
44
37
  import { folderIdFor } from "./put.js";
45
38
  export async function putWithWallet(target, options) {
46
39
  const say = options.write ?? ((line) => process.stdout.write(`${line}\n`));
@@ -65,6 +58,12 @@ export async function putWithWallet(target, options) {
65
58
  const asked = parseAsked(options.onCollision);
66
59
  const list = await readFileList(server, key.key, resolved.code, identity.accountId);
67
60
  const rule = paddingRuleOf(list.manifest?.settings);
61
+ // ⛔ WHICH WALLET PAYS, BEFORE ANYTHING IS PRICED — out of the list that was just read, so this
62
+ // costs no second request and the address in the review is the address that signs.
63
+ const wallet = await payingWalletIndex({
64
+ ...options,
65
+ readActiveWallet: async () => activeWalletOf(list.manifest?.settings),
66
+ });
68
67
  const name = options.name ?? basename(localPath);
69
68
  const destination = (options.to ?? "").replace(/^\.?\//, "").replace(/\/$/, "");
70
69
  const parentId = folderIdFor(options.to, list.manifest?.entries ?? []);
@@ -79,6 +78,7 @@ export async function putWithWallet(target, options) {
79
78
  rule,
80
79
  onCollision: asked,
81
80
  settings: list.manifest?.settings,
81
+ wallet,
82
82
  progress: new Progress(options.json === true ? silentSink() : stderrSink(), "uploading"),
83
83
  say,
84
84
  json: options.json === true,
@@ -100,174 +100,75 @@ export async function putWithWallet(target, options) {
100
100
  return 0;
101
101
  }
102
102
  /**
103
- * Review, agree, upload and record ONE file. Null when `--dry-run` stopped at the review.
103
+ * Review, agree, upload and record ONE file for a person who is watching. Null when `--dry-run`
104
+ * stopped at the review.
104
105
  *
105
- * ⛔ THE SIGNING MODULE IS LOADED AFTER THE AGREEMENT, and only then a dry run and a refusal
106
- * never bring the code that can spend into memory.
106
+ * ⛔ THE AGREEMENT HANDED IN BELOW IS THIS MACHINE'S GRANT, held against the total the review
107
+ * names. It is the one gate between a program and somebody's wallet, and the reason it is
108
+ * handed to the library rather than living inside it: a library call has nobody to ask.
107
109
  */
108
110
  export async function uploadOneWithWallet(ctx, file, options) {
109
111
  const { say } = ctx;
110
- const { plan, sealedBytes, sealFor } = planAndPrice(file.size, ctx.partSize, ctx.rule);
111
- const sealedLens = plan.map((range) => sealedLenFor(sealFor(range)));
112
- const storageAsk = parseStorageAsk(options.storage);
113
- if (storageAsk !== null && plan.length > 1) {
114
- throw new NmtsError(`A held storage resource holds one blob, and this file is ${plan.length} parts.`, {
115
- exitCode: 4,
116
- nextStep: `Nothing was signed. Leave --storage off, or raise --part-size so the file is one part.`,
117
- });
118
- }
119
- const protocol = (options.protocol ?? createBlobProtocol)(ctx.network, sealedBytes, (sent, total) => ctx.progress.update(sent, total));
120
- const reads = await (options.readChain ?? defaultReads)(ctx.network, protocol.relayUrl);
121
- const window = await reads.readWindow();
122
- if (window === null) {
123
- throw new NmtsError(`The ${ctx.network} storage network could not be read.`, {
124
- exitCode: 1,
125
- nextStep: `Nothing was signed and nothing was sent. Which epoch the network is in, and how far ahead it will sell, are facts only the chain has — this tool will not spend against a guess.`,
112
+ let outcome;
113
+ try {
114
+ outcome = await walletPut({
115
+ code: ctx.code,
116
+ apiKey: ctx.apiKey,
117
+ server: ctx.server,
118
+ network: ctx.network,
119
+ accountId: ctx.accountId,
120
+ crypt: ctx.crypt,
121
+ wallet: ctx.wallet,
122
+ partSize: ctx.partSize,
123
+ rule: ctx.rule,
124
+ onCollision: ctx.onCollision,
125
+ }, { source: fileSource(file.localPath, file.size), name: file.name, parentId: file.parentId, destination: file.destination }, {
126
+ epochs: options.epochs,
127
+ storage: options.storage,
128
+ dryRun: options.dryRun,
129
+ readChain: options.readChain,
130
+ sign: options.sign,
131
+ protocol: options.protocol,
132
+ api: options.api,
133
+ onProgress: (sent, total) => ctx.progress.update(sent, total),
134
+ onStep: (step) => tell(ctx, file.size, step),
135
+ onReview: (review) => {
136
+ if (ctx.json)
137
+ return;
138
+ describeUploadReview(say, {
139
+ name: review.name,
140
+ bytes: review.bytes,
141
+ parts: review.parts,
142
+ epochs: review.epochs,
143
+ days: review.days,
144
+ endEpoch: review.endEpoch,
145
+ walNeeded: review.budget.walNeededFrost,
146
+ tipMist: review.tipMist,
147
+ storage: review.storage,
148
+ heldResources: review.heldResources,
149
+ }, review.budget);
150
+ },
151
+ agree: (review) => {
152
+ requireWalletGrant("seal", { walFrost: review.budget.walNeededFrost, suiMist: review.budget.suiNeededMist }, new Date(options.now ?? Date.now()));
153
+ },
154
+ onSpend: recordWalletSpend,
126
155
  });
127
156
  }
128
- const epochs = chooseUploadEpochs(options.epochs, window);
129
- const endEpoch = window.clock.current + epochs;
130
- const quotes = await reads.quoteParts(sealedLens, epochs);
131
- const address = await walletAddress(ctx.code);
132
- let storage = { kind: "buy" };
133
- let heldResources = null;
134
- if (storageAsk !== null) {
135
- const firstQuote = quotes[0];
136
- const firstLen = sealedLens[0];
137
- if (firstQuote === undefined || firstLen === undefined)
138
- throw new NmtsError("unreachable: a plan with no part");
139
- let resources;
140
- try {
141
- resources = await reads.readStorage(address);
142
- }
143
- catch (error) {
144
- throw new NmtsError("The storage resources this wallet holds could not be read.", {
145
- exitCode: 1,
146
- nextStep: `Nothing was signed. That is not the same as holding none. Cause: ${error instanceof Error ? error.message : String(error)}`,
147
- });
148
- }
149
- const encoded = await reads.encodedLength(address, firstLen);
150
- storage = pickResource(storageAsk, resources, encoded, window.clock.current, endEpoch, firstQuote.writeFrost);
151
- }
152
- else {
153
- heldResources = await reads.readStorage(address).then((rows) => rows.filter((r) => statusOf(r, window.clock.current) === "usable").length, () => null);
154
- }
155
- const [purse, feeMist] = await Promise.all([
156
- reads.readWallet(address),
157
- reads.estimateRegisterGas({
158
- sender: address,
159
- sealedLen: Math.max(...sealedLens),
160
- epochs,
161
- storage: storage.kind === "buy" ? storage : { kind: "reuse", objectId: storage.objectId, cutToBytes: storage.cutToBytes, writeFrost: storage.writeFrost },
162
- }),
163
- ]);
164
- const budget = uploadBudget({ address, purse, feeMist, quotes, storage });
165
- const tipMist = quotes.reduce((sum, q) => sum + q.tipMist, 0n);
166
- const facts = {
167
- bytes: file.size,
168
- sealedBytes,
169
- parts: plan.length,
170
- epochs,
171
- days: daysOf(window, epochs),
172
- endEpoch,
173
- paidFrom: "wallet",
174
- priceFrost: budget.walNeededFrost.toString(),
175
- priceWal: coinAmount(budget.walNeededFrost),
176
- tipMist: tipMist.toString(),
177
- tipSui: coinAmount(tipMist),
178
- feeMist: feeMist === null ? null : feeMist.toString(),
179
- feeSui: feeMist === null ? null : coinAmount(feeMist),
180
- wallet: address,
181
- walletWal: budget.walFrost === null ? null : coinAmount(budget.walFrost),
182
- walletSui: budget.suiMist === null ? null : coinAmount(budget.suiMist),
183
- storage: storage.kind === "buy"
184
- ? { kind: "buy" }
185
- : { kind: "reuse", objectId: storage.objectId, cutToBytes: storage.cutToBytes, leftoverBytes: storage.leftoverBytes },
186
- };
187
- if (!ctx.json) {
188
- describeUploadReview(say, { name: file.name, bytes: file.size, parts: plan.length, epochs, days: daysOf(window, epochs), endEpoch, walNeeded: walPrice(quotes, storage), tipMist, storage, heldResources }, budget);
157
+ finally {
158
+ ctx.progress.done();
189
159
  }
190
- if (options.dryRun === true) {
160
+ const facts = factsOf(outcome.review);
161
+ if (outcome.kind === "review") {
191
162
  if (ctx.json)
192
- say(JSON.stringify({ dryRun: true, name: file.name, ...facts, signed: false }));
163
+ say(JSON.stringify({ dryRun: true, name: outcome.review.name, ...facts, signed: false }));
193
164
  else {
194
165
  say(``);
195
- if (budget.shortfall !== null)
196
- say(` ⚠ ${budget.shortfall} As it stands, it would be refused.`);
166
+ if (outcome.review.budget.shortfall !== null)
167
+ say(` ⚠ ${outcome.review.budget.shortfall} As it stands, it would be refused.`);
197
168
  say(` Nothing was signed and nothing was sent. Run the same command without --dry-run to upload it.`);
198
169
  }
199
170
  return null;
200
171
  }
201
- // ⛔ A WALLET KNOWN TO BE SHORT IS REFUSED BEFORE THE AGREEMENT IS ASKED FOR (`extend-budget.ts`).
202
- if (budget.shortfall !== null) {
203
- throw new NmtsError(budget.shortfall, { exitCode: 4, nextStep: uploadShortfallNextStep(budget) });
204
- }
205
- // ⛔ THE ONE GATE BETWEEN A PROGRAM AND SOMEBODY'S WALLET. Everything above is a read.
206
- requireWalletGrant("seal", { walFrost: budget.walNeededFrost, suiMist: budget.suiNeededMist }, new Date(options.now ?? Date.now()));
207
- const sign = options.sign ?? (await signers());
208
- const derived = ctx.crypt.kdf_derive(ctx.crypt.account_code_parse(ctx.code));
209
- const dataKey = derived.slice(DERIVED.dataKey[0], DERIVED.dataKey[1]);
210
- derived.fill(0);
211
- let result;
212
- try {
213
- result = await uploadFile({
214
- api: options.api ?? createUploadApi(ctx.server, ctx.apiKey),
215
- protocol,
216
- crypt: ctx.crypt,
217
- dataKey,
218
- source: fileSource(file.localPath, file.size),
219
- name: file.name,
220
- parentId: file.parentId,
221
- destination: file.destination,
222
- relayUrl: protocol.relayUrl,
223
- epochs,
224
- currentEpoch: window.clock.current,
225
- partSize: ctx.partSize,
226
- padding: { rule: ctx.rule, unitBytes: CREDIT_BYTES },
227
- onStep: (step) => tell(ctx, file.size, step),
228
- buy: walletRail({
229
- network: ctx.network,
230
- code: ctx.code,
231
- relayUrl: protocol.relayUrl,
232
- epochs,
233
- storage,
234
- quotes,
235
- feeMist,
236
- signRegister: sign.register,
237
- signCertify: sign.certify,
238
- onSpend: recordWalletSpend,
239
- }),
240
- });
241
- }
242
- finally {
243
- ctx.progress.done();
244
- dataKey.fill(0);
245
- }
246
- const now = Date.now();
247
- const added = await addEntry({
248
- server: ctx.server,
249
- apiKey: ctx.apiKey,
250
- code: ctx.code,
251
- accountId: ctx.accountId,
252
- ...(ctx.onCollision !== undefined ? { onCollision: ctx.onCollision } : {}),
253
- entry: {
254
- id: result.itemId,
255
- parentId: file.parentId,
256
- kind: 1,
257
- name: result.entry.name,
258
- size: result.entry.plaintextLen,
259
- createdAt: now,
260
- updatedAt: now,
261
- dekWrapped: result.entry.dekWrapped,
262
- contentHashCt: result.entry.contentHashCt,
263
- },
264
- });
265
- // ⛔ ONLY NOW, AND EVERY PART — the same order `put.ts` keeps and for the same reason.
266
- clearItemRecord(result.fileKey);
267
- for (const record of partKeysOf(result.fileKey, result.parts))
268
- clearReservation(record);
269
- if (added.replaced)
270
- await setTrashed(ctx.server, ctx.apiKey, added.replaced.id, true);
271
172
  // ⛔ THE STANDING SHARE, AFTER THE PAYMENT AND NEVER INSIDE IT: a gift that fails must not fail
272
173
  // the upload. A resumed run paid nothing now, so it owes nothing now. In --json the tip speaks
273
174
  // on stderr — stdout carries the machine's one answer and nothing else.
@@ -276,11 +177,37 @@ export async function uploadOneWithWallet(ctx, file, options) {
276
177
  network: ctx.network,
277
178
  code: ctx.code,
278
179
  settings: ctx.settings,
279
- paidWalFrost: result.resumed ? 0n : budget.walNeededFrost,
180
+ wallet: ctx.wallet,
181
+ paidWalFrost: outcome.resumed ? 0n : outcome.review.budget.walNeededFrost,
280
182
  say: ctx.json ? (line) => void process.stderr.write(`${line}\n`) : say,
281
183
  ...(options.tip ?? {}),
282
184
  });
283
- return { itemId: result.itemId, savedAs: added.name, replaced: added.replaced?.id ?? null, seq: added.seq, resumed: result.resumed, facts };
185
+ return { itemId: outcome.itemId, savedAs: outcome.savedAs, replaced: outcome.replaced, seq: outcome.fileListVersion, resumed: outcome.resumed, facts };
186
+ }
187
+ /** The review as `--json` prints it: base units as strings, with the coin amounts beside them. */
188
+ function factsOf(review) {
189
+ const { budget, storage } = review;
190
+ return {
191
+ bytes: review.bytes,
192
+ sealedBytes: review.sealedBytes,
193
+ parts: review.parts,
194
+ epochs: review.epochs,
195
+ days: review.days,
196
+ endEpoch: review.endEpoch,
197
+ paidFrom: "wallet",
198
+ priceFrost: budget.walNeededFrost.toString(),
199
+ priceWal: coinAmount(budget.walNeededFrost),
200
+ tipMist: review.tipMist.toString(),
201
+ tipSui: coinAmount(review.tipMist),
202
+ feeMist: budget.feeMist === null ? null : budget.feeMist.toString(),
203
+ feeSui: budget.feeMist === null ? null : coinAmount(budget.feeMist),
204
+ wallet: budget.address,
205
+ walletWal: budget.walFrost === null ? null : coinAmount(budget.walFrost),
206
+ walletSui: budget.suiMist === null ? null : coinAmount(budget.suiMist),
207
+ storage: storage.kind === "buy"
208
+ ? { kind: "buy" }
209
+ : { kind: "reuse", objectId: storage.objectId, cutToBytes: storage.cutToBytes, leftoverBytes: storage.leftoverBytes },
210
+ };
284
211
  }
285
212
  /** Each step, for a person watching. The signatures are named as what they are. */
286
213
  function tell(ctx, size, step) {
@@ -307,10 +234,3 @@ function tell(ctx, size, step) {
307
234
  if (step.step === "committing")
308
235
  say(` saving to the drive`);
309
236
  }
310
- async function defaultReads(network, relayUrl) {
311
- return (await import("../upload-wallet-chain.js")).walletUploadReads(network, relayUrl);
312
- }
313
- async function signers() {
314
- const signing = await import("../wallet-sign.js");
315
- return { register: signing.signBlobRegister, certify: signing.signBlobCertify };
316
- }
@@ -35,16 +35,14 @@ export interface PutOptions {
35
35
  epochs?: string | number | undefined;
36
36
  /** `--pay wallet`: a held storage resource to use — `fit`, `whole`, or its object id. */
37
37
  storage?: string | undefined;
38
+ /** `--pay wallet`: which of this key's wallets pays, this run only. Absent = the account's own. */
39
+ wallet?: string | undefined;
38
40
  json?: boolean;
39
41
  write?: (line: string) => void;
40
42
  }
41
- /** Who pays, or a refusal for a payer this tool does not know. */
42
- export declare function payerOf(pay: string | undefined): "credits" | "wallet";
43
- /** The two options that only mean something when the wallet pays, refused when it does not. */
44
- export declare function refuseWalletOnlyOptions(options: {
45
- epochs?: string | number | undefined;
46
- storage?: string | undefined;
47
- }): void;
43
+ /** Who pays, and the options that lose their meaning under that answer — the rule is in `put-payer.ts`; this is its one road. */
44
+ import { payerOf, refuseWalletOnlyOptions } from "./put-payer.ts";
45
+ export { payerOf, refuseWalletOnlyOptions };
48
46
  /**
49
47
  * The folder id `--to` names, or null for the root. Refuses rather than guessing.
50
48
  *
@@ -31,32 +31,9 @@ import { createUploadApi } from "../upload-api.js";
31
31
  import { fileSource, partKeysOf, uploadFile } from "../upload-file.js";
32
32
  import { CREDIT_BYTES, creditsFor, measureLocal, partSizeFor, planAndPrice, UPLOAD_EPOCHS, } from "../upload-price.js";
33
33
  import { createBlobProtocol, readCurrentEpoch } from "../walrus-write.js";
34
- /** Who pays, or a refusal for a payer this tool does not know. */
35
- export function payerOf(pay) {
36
- if (pay === undefined || pay === "credits")
37
- return "credits";
38
- if (pay === "wallet")
39
- return "wallet";
40
- throw new NmtsError(`--pay takes credits or wallet, not "${pay}".`, {
41
- exitCode: 2,
42
- nextStep: `Nothing was sent. --pay wallet buys the storage from the wallet this NMTS key derives; without it credits pay.`,
43
- });
44
- }
45
- /** The two options that only mean something when the wallet pays, refused when it does not. */
46
- export function refuseWalletOnlyOptions(options) {
47
- if (options.epochs !== undefined) {
48
- throw new NmtsError("--epochs only applies with --pay wallet: one credit buys a fixed term.", {
49
- exitCode: 2,
50
- nextStep: `Nothing was sent. Add --pay wallet to choose the term, or leave --epochs off to pay with credits.`,
51
- });
52
- }
53
- if (options.storage !== undefined) {
54
- throw new NmtsError("--storage only applies with --pay wallet: credits buy storage from the treasury.", {
55
- exitCode: 2,
56
- nextStep: `Nothing was sent. Add --pay wallet to use a storage resource this wallet holds.`,
57
- });
58
- }
59
- }
34
+ /** Who pays, and the options that lose their meaning under that answer — the rule is in `put-payer.ts`; this is its one road. */
35
+ import { payerOf, refuseWalletOnlyOptions } from "./put-payer.js";
36
+ export { payerOf, refuseWalletOnlyOptions };
60
37
  /**
61
38
  * The folder id `--to` names, or null for the root. Refuses rather than guessing.
62
39
  *
@@ -15,7 +15,10 @@ export interface WalletDonateOptions {
15
15
  yes?: boolean;
16
16
  dryRun?: boolean;
17
17
  feeCap?: string | undefined;
18
+ /** `--wallet N`: which wallet the gift comes from, this run only. Absent = the account's number. */
19
+ wallet?: string | undefined;
18
20
  /** ⚠ SEAMS, NOT OPTIONS — no flag reaches them. */
21
+ readActiveWallet?: () => Promise<number>;
19
22
  readDonation?: (server: string) => Promise<DonationConfig>;
20
23
  readChain?: (network: Network) => SendReads | Promise<SendReads>;
21
24
  sign?: SignTransfer;
@@ -25,6 +25,7 @@ import { THANKS_EN, THANKS_KO } from "../standing-tip.js";
25
25
  import { explorerTxUrl } from "../shared/lib/wallet/activity.js";
26
26
  import { clampGasBudgetMist, isValidSuiAddress, parseTokenAmountToBaseUnits, validateSendForm, } from "../shared/lib/wallet/send-rules.js";
27
27
  import { coinAmount, walCoinType, walletAddress } from "../wallet.js";
28
+ import { payingWalletIndex } from "../wallet-pay-index.js";
28
29
  /** The four facts and the promise, as the screens say them, in the order a person reads them. */
29
30
  export const GIFT_NOTICES = [
30
31
  "This is a voluntary gift. Nothing is provided in return, it is non-refundable, and it cannot be undone once sent.",
@@ -57,7 +58,10 @@ export async function walletDonate(operands, options = {}) {
57
58
  throw new NmtsError("Say how much.", { exitCode: 2, nextStep: `\`${BINARY_NAME} wallet donate ${coin} <amount> --yes\`` });
58
59
  }
59
60
  const resolved = await requireAccountCode();
60
- const address = await walletAddress(resolved.code);
61
+ // A gift leaves the same wallet storage is paid from, and it is resolved before the review is
62
+ // priced — so the address a person reads is the address the coins leave.
63
+ const wallet = await payingWalletIndex(options);
64
+ const address = await walletAddress(resolved.code, wallet);
61
65
  const stored = resolved.source === "file" || resolved.source === "file-locked" ? readCredentialsFile() : null;
62
66
  const server = resolveServer(options.server ?? stored?.server);
63
67
  const network = resolveNetwork(server, options.network ?? stored?.network);
@@ -143,7 +147,7 @@ export async function walletDonate(operands, options = {}) {
143
147
  });
144
148
  }
145
149
  const sign = options.sign ?? (await import("../wallet-sign.js")).signTransfer;
146
- const digest = await sign({ network, code: resolved.code, shape });
150
+ const digest = await sign({ network, code: resolved.code, wallet, shape });
147
151
  if (options.json) {
148
152
  say(JSON.stringify({ ...facts, signed: true, digest, explorerUrl: explorerTxUrl(digest, network) }));
149
153
  return 0;
@@ -0,0 +1,18 @@
1
+ import { type Network } from "../network.ts";
2
+ import { type ChainReader } from "../wallet.ts";
3
+ export interface WalletListOptions {
4
+ server?: string | undefined;
5
+ network?: string | undefined;
6
+ json?: boolean;
7
+ write?: (line: string) => void;
8
+ /** ⚠ SEAMS, NOT OPTIONS — no flag reaches either. A live chain cannot be asked to fail on demand. */
9
+ openChain?: (network: Network, address: string) => Promise<ChainReader> | ChainReader;
10
+ /** Has this address any transaction at all? A wallet that has been emptied is still in use. */
11
+ hasHistory?: (network: Network, address: string) => Promise<boolean>;
12
+ /** The account's own numbers, out of the sealed list. */
13
+ readWalletSettings?: () => Promise<{
14
+ active: number;
15
+ count: number;
16
+ }>;
17
+ }
18
+ export declare function walletList(options?: WalletListOptions): Promise<number>;
@@ -0,0 +1,116 @@
1
+ // `nmts wallet list` — every wallet this NMTS key has, what is in it, and which one pays.
2
+ //
3
+ // ⛔ IT READS. Nothing here signs, and nothing here writes the account's choice: `wallet use N`
4
+ // does that. Running this repeatedly costs nothing but questions to a public node.
5
+ //
6
+ // ⛔ WHICH WALLETS EXIST IS NOT A LIST ANYBODY KEEPS. One key derives a wallet at every index
7
+ // (NCF-3 §1.3), so "how many do I have" is answered by WALKING — ask about a number, then the
8
+ // next, and stop after twenty unused ones in a row. That rule is the browser's own, copied
9
+ // byte-for-byte (`shared/lib/wallet/discover.ts`), so the same key shows the same wallets in
10
+ // both programs. ⚠ A wallet funded further out than the gap is not found by the walk; it is not
11
+ // lost — `--wallet N` and `wallet use N` reach any number directly.
12
+ //
13
+ // ⛔ A BALANCE THAT COULD NOT BE READ IS PRINTED AS THAT, NEVER AS ZERO, and the run exits
14
+ // non-zero — the same rule as `nmts wallet`, for the same reason: an empty wallet and an
15
+ // unanswered question look identical on a screen, and one of them is a reason to stop.
16
+ import { requireAccountCode } from "../code-access.js";
17
+ import { readCredentialsFile } from "../credentials.js";
18
+ import { readFileList } from "../manifest.js";
19
+ import { resolveNetwork } from "../network.js";
20
+ import { BINARY_NAME } from "../product.js";
21
+ import { resolveServer } from "../server.js";
22
+ import { openSession } from "../session.js";
23
+ import { activeWalletOf, walletCountOf } from "../shared/lib/drive/manifest-settings.js";
24
+ import { discoverWallets } from "../shared/lib/wallet/discover.js";
25
+ import { coinAmount, readBalances, walCoinType, walletAddress, SUI_COIN_TYPE, } from "../wallet.js";
26
+ /** The table's column widths. A Sui address is 66 characters and gets two spaces after it. */
27
+ const NUMBER_W = 8;
28
+ const ADDRESS_W = 68;
29
+ const AMOUNT_W = 21;
30
+ export async function walletList(options = {}) {
31
+ const say = options.write ?? ((line) => process.stdout.write(`${line}\n`));
32
+ const resolved = await requireAccountCode();
33
+ const stored = resolved.source === "file" || resolved.source === "file-locked" ? readCredentialsFile() : null;
34
+ const server = resolveServer(options.server ?? stored?.server);
35
+ const network = resolveNetwork(server, options.network ?? stored?.network);
36
+ const walType = walCoinType(network);
37
+ const settings = await (options.readWalletSettings ?? (() => walletSettings(options)))();
38
+ const open = options.openChain ??
39
+ (async (net, addr) => (await import("../wallet-chain.js")).chainReader(net, addr));
40
+ const history = options.hasHistory ??
41
+ (async (net, addr) => (await import("../wallet-list-chain.js")).hasHistory(net, addr));
42
+ // Every number the walk asked about, so nothing is derived or read twice.
43
+ const seen = new Map();
44
+ const scan = await discoverWallets(async (index) => {
45
+ const address = await walletAddress(resolved.code, index);
46
+ const balances = await readBalances(await open(network, address), walType);
47
+ seen.set(index, { index, address, balances });
48
+ const held = (b) => b.read && b.baseUnits > 0n;
49
+ if (held(balances.sui) || held(balances.wal))
50
+ return true;
51
+ return history(network, address);
52
+ }, { count: settings.count });
53
+ // What is printed: the wallets this account has made, plus any the walk found beyond them. The
54
+ // rest of the walk is the gap — empty wallets nobody has asked for.
55
+ const rows = [...seen.values()]
56
+ .filter((row) => row.index < settings.count || scan.used.includes(row.index))
57
+ .sort((a, b) => a.index - b.index);
58
+ const unread = rows.some((row) => !row.balances.sui.read || !row.balances.wal.read);
59
+ if (options.json === true) {
60
+ say(JSON.stringify({
61
+ network,
62
+ active: settings.active,
63
+ scanned: scan.scanned,
64
+ wallets: rows.map((row) => ({
65
+ index: row.index,
66
+ address: row.address,
67
+ pays: row.index === settings.active,
68
+ sui: asJson(row.balances.sui, SUI_COIN_TYPE),
69
+ wal: asJson(row.balances.wal, walType),
70
+ })),
71
+ }));
72
+ return unread ? 1 : 0;
73
+ }
74
+ // ⚠ The columns are laid out with the same widths the rows use, from the same constants — a
75
+ // header written out by hand drifts one space at a time until nothing lines up.
76
+ say(`${"Wallet".padEnd(NUMBER_W)}${"Address".padEnd(ADDRESS_W)}${"SUI".padEnd(AMOUNT_W)}WAL`);
77
+ for (const row of rows) {
78
+ const pays = row.index === settings.active ? " pays" : "";
79
+ say(`${String(row.index).padEnd(NUMBER_W)}${row.address.padEnd(ADDRESS_W)}` +
80
+ `${inWords(row.balances.sui).padEnd(AMOUNT_W)}${inWords(row.balances.wal)}${pays}`);
81
+ }
82
+ say(``);
83
+ say(` Every wallet above comes from this NMTS key, at the number in the first column. The one`);
84
+ say(` marked "pays" is the one storage is paid from — \`${BINARY_NAME} wallet use <number>\` moves it,`);
85
+ say(` on this account rather than on this machine. Numbers come from the key, so a wallet`);
86
+ say(` cannot be deleted.`);
87
+ say(` The list stops after twenty unused wallets in a row; a wallet further out than that is`);
88
+ say(` reached by its number.`);
89
+ if (unread) {
90
+ say(` A balance that could not be read is printed as that and never as zero, and this command`);
91
+ say(` exits non-zero when it happens.`);
92
+ }
93
+ return unread ? 1 : 0;
94
+ }
95
+ /** The account's own numbers, read out of the sealed file list. */
96
+ async function walletSettings(options) {
97
+ const session = await openSession({ server: options.server, network: options.network });
98
+ const list = await readFileList(session.server, session.apiKey, session.code, session.accountId);
99
+ const settings = list.manifest?.settings;
100
+ return { active: activeWalletOf(settings), count: walletCountOf(settings) };
101
+ }
102
+ /** One balance, for a person. The same two sentences `nmts wallet` prints. */
103
+ function inWords(balance) {
104
+ return balance.read ? coinAmount(balance.baseUnits) : `⛔ could not be read`;
105
+ }
106
+ /** One balance, for a program. `baseUnits` is a string: a balance outruns a JSON number. */
107
+ function asJson(balance, coinType) {
108
+ if (!balance.read)
109
+ return { coinType, read: false, error: balance.why };
110
+ return {
111
+ coinType,
112
+ read: true,
113
+ baseUnits: balance.baseUnits.toString(),
114
+ amount: coinAmount(balance.baseUnits),
115
+ };
116
+ }
@@ -12,6 +12,10 @@ export interface WalletSendOptions {
12
12
  dryRun?: boolean;
13
13
  /** A ceiling on the fee, in SUI ("0.01"). Clamped to the usable range, never refused. */
14
14
  feeCap?: string | undefined;
15
+ /** `--wallet N`: which wallet pays, this run only. Absent = the account's own number. */
16
+ wallet?: string | undefined;
17
+ /** ⚠ A SEAM, NOT AN OPTION — where that number is read from (`wallet-pay-index.ts`). */
18
+ readActiveWallet?: () => Promise<number>;
15
19
  /** The instant to measure the wallet agreement against. */
16
20
  now?: number;
17
21
  /** ⚠ A SEAM, NOT AN OPTION — no flag reaches it. */