@needmoretruth/nmts-cli 0.34.4 → 0.36.1

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 (156) hide show
  1. package/AGENTS.md +11 -12
  2. package/CHANGELOG.md +43 -0
  3. package/README.ko.md +33 -35
  4. package/README.md +21 -22
  5. package/dist/account.js +2 -1
  6. package/dist/arg-options.d.ts +169 -0
  7. package/dist/arg-options.js +10 -0
  8. package/dist/args.d.ts +2 -157
  9. package/dist/args.js +2 -0
  10. package/dist/artifact-about.d.ts +1 -1
  11. package/dist/autonomy.d.ts +3 -10
  12. package/dist/autonomy.js +15 -17
  13. package/dist/bytes.d.ts +15 -0
  14. package/dist/bytes.js +63 -0
  15. package/dist/collision.d.ts +5 -5
  16. package/dist/collision.js +19 -22
  17. package/dist/commands/delete-account.js +1 -1
  18. package/dist/commands/erase.js +1 -1
  19. package/dist/commands/extend.d.ts +2 -0
  20. package/dist/commands/extend.js +8 -1
  21. package/dist/commands/get.js +1 -1
  22. package/dist/commands/listfile.js +1 -1
  23. package/dist/commands/mcp.js +5 -1
  24. package/dist/commands/mode.js +2 -2
  25. package/dist/commands/on-collision.d.ts +1 -1
  26. package/dist/commands/on-collision.js +3 -3
  27. package/dist/commands/platform.d.ts +9 -0
  28. package/dist/commands/platform.js +124 -0
  29. package/dist/commands/pull.js +1 -1
  30. package/dist/commands/push-wallet.js +8 -1
  31. package/dist/commands/push.d.ts +2 -0
  32. package/dist/commands/push.js +6 -4
  33. package/dist/commands/put-payer.d.ts +8 -0
  34. package/dist/commands/put-payer.js +41 -0
  35. package/dist/commands/put-wallet.d.ts +9 -3
  36. package/dist/commands/put-wallet.js +108 -186
  37. package/dist/commands/put.d.ts +5 -7
  38. package/dist/commands/put.js +11 -31
  39. package/dist/commands/receive.js +1 -1
  40. package/dist/commands/settings.js +1 -1
  41. package/dist/commands/support.js +3 -3
  42. package/dist/commands/unlock.js +2 -2
  43. package/dist/commands/wallet-donate.d.ts +3 -0
  44. package/dist/commands/wallet-donate.js +6 -2
  45. package/dist/commands/wallet-hall.d.ts +4 -1
  46. package/dist/commands/wallet-hall.js +16 -8
  47. package/dist/commands/wallet-list.d.ts +18 -0
  48. package/dist/commands/wallet-list.js +116 -0
  49. package/dist/commands/wallet-send.d.ts +4 -0
  50. package/dist/commands/wallet-send.js +6 -2
  51. package/dist/commands/wallet-storage-ops.d.ts +4 -1
  52. package/dist/commands/wallet-storage-ops.js +7 -2
  53. package/dist/commands/wallet-swap.d.ts +3 -0
  54. package/dist/commands/wallet-swap.js +8 -4
  55. package/dist/commands/wallet-use.d.ts +7 -0
  56. package/dist/commands/wallet-use.js +55 -0
  57. package/dist/commands/wallet.d.ts +4 -0
  58. package/dist/commands/wallet.js +20 -8
  59. package/dist/crypto.d.ts +8 -5
  60. package/dist/crypto.js +15 -61
  61. package/dist/download-sink-node.d.ts +49 -0
  62. package/dist/download-sink-node.js +204 -0
  63. package/dist/download-sink.d.ts +0 -48
  64. package/dist/download-sink.js +6 -199
  65. package/dist/download.js +5 -4
  66. package/dist/engine-node.d.ts +19 -0
  67. package/dist/engine-node.js +77 -0
  68. package/dist/env-vars.d.ts +20 -0
  69. package/dist/env-vars.js +27 -0
  70. package/dist/extend-plan.d.ts +2 -0
  71. package/dist/gate.js +1 -1
  72. package/dist/help.js +12 -5
  73. package/dist/host-contract.d.ts +7 -0
  74. package/dist/host-contract.js +95 -0
  75. package/dist/host-node.d.ts +27 -0
  76. package/dist/host-node.js +192 -0
  77. package/dist/host.d.ts +99 -0
  78. package/dist/host.js +59 -0
  79. package/dist/index.d.ts +6 -40
  80. package/dist/index.js +20 -32
  81. package/dist/main.js +11 -10
  82. package/dist/manifest-chunk-cache.d.ts +3 -3
  83. package/dist/manifest-chunk-cache.js +39 -44
  84. package/dist/manifest-chunk-flow.js +15 -14
  85. package/dist/manifest-write.js +1 -1
  86. package/dist/manifest.d.ts +2 -2
  87. package/dist/manifest.js +37 -34
  88. package/dist/mcp-tools/tiers.js +1 -1
  89. package/dist/network.d.ts +1 -1
  90. package/dist/network.js +6 -2
  91. package/dist/platform-sign.d.ts +143 -0
  92. package/dist/platform-sign.js +220 -0
  93. package/dist/portable.d.ts +59 -0
  94. package/dist/portable.js +82 -0
  95. package/dist/product.d.ts +1 -1
  96. package/dist/product.js +1 -1
  97. package/dist/progress-node.d.ts +2 -0
  98. package/dist/progress-node.js +14 -0
  99. package/dist/progress.d.ts +0 -1
  100. package/dist/progress.js +4 -8
  101. package/dist/redact.js +3 -2
  102. package/dist/registration.js +5 -2
  103. package/dist/risk.d.ts +11 -0
  104. package/dist/risk.js +16 -0
  105. package/dist/run-log.d.ts +4 -4
  106. package/dist/run-log.js +29 -36
  107. package/dist/seal.js +5 -4
  108. package/dist/server.d.ts +1 -1
  109. package/dist/server.js +6 -2
  110. package/dist/setup-questions.js +2 -2
  111. package/dist/shared/lib/drive/manifest-ops.d.ts +8 -30
  112. package/dist/shared/lib/drive/manifest-ops.js +7 -39
  113. package/dist/shared/lib/drive/manifest-settings-patch.d.ts +52 -0
  114. package/dist/shared/lib/drive/manifest-settings-patch.js +110 -0
  115. package/dist/shared/lib/drive/manifest-settings.d.ts +55 -4
  116. package/dist/shared/lib/drive/manifest-settings.js +66 -28
  117. package/dist/shared/lib/wallet/discover.d.ts +54 -0
  118. package/dist/shared/lib/wallet/discover.js +66 -0
  119. package/dist/standing-tip.d.ts +3 -0
  120. package/dist/standing-tip.js +1 -0
  121. package/dist/upload-file-node.d.ts +3 -0
  122. package/dist/upload-file-node.js +38 -0
  123. package/dist/upload-file.d.ts +2 -2
  124. package/dist/upload-file.js +15 -45
  125. package/dist/upload-price-node.d.ts +8 -0
  126. package/dist/upload-price-node.js +37 -0
  127. package/dist/upload-price.d.ts +0 -8
  128. package/dist/upload-price.js +4 -33
  129. package/dist/upload-steps.js +3 -3
  130. package/dist/upload-store.d.ts +14 -14
  131. package/dist/upload-store.js +68 -75
  132. package/dist/upload-wallet-chain.js +3 -2
  133. package/dist/upload-wallet-put.d.ts +118 -0
  134. package/dist/upload-wallet-put.js +221 -0
  135. package/dist/upload-wallet.d.ts +3 -0
  136. package/dist/upload-wallet.js +18 -15
  137. package/dist/upload.js +16 -15
  138. package/dist/wallet-list-chain.d.ts +4 -0
  139. package/dist/wallet-list-chain.js +21 -0
  140. package/dist/wallet-pay-index.d.ts +21 -0
  141. package/dist/wallet-pay-index.js +68 -0
  142. package/dist/wallet-sign-seams.d.ts +69 -0
  143. package/dist/wallet-sign-seams.js +16 -0
  144. package/dist/wallet-sign.d.ts +3 -53
  145. package/dist/wallet-sign.js +22 -18
  146. package/dist/wallet.d.ts +13 -13
  147. package/dist/wallet.js +15 -15
  148. package/dist/walrus-write.js +3 -2
  149. package/dist/walrus.d.ts +1 -16
  150. package/dist/walrus.js +11 -22
  151. package/docs/commands/credits.md +1 -1
  152. package/docs/commands/platform.md +14 -0
  153. package/docs/commands/support.md +1 -1
  154. package/docs/commands/trash.md +1 -1
  155. package/docs/commands/wallet.md +26 -8
  156. package/package.json +13 -2
@@ -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,24 @@ 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
- import { Progress, silentSink, stderrSink } from "../progress.js";
32
- import { sealedLenFor } from "../seal.js";
29
+ import { Progress, silentSink } from "../progress.js";
30
+ import { stderrSink } from "../progress-node.js";
33
31
  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";
32
+ import { fileSource } from "../upload-file-node.js";
33
+ import { partSizeFor } from "../upload-price.js";
34
+ import { measureLocal } from "../upload-price-node.js";
35
+ import { describeUploadReview } from "../upload-wallet-plan.js";
36
+ import { walletPut } from "../upload-wallet-put.js";
37
+ import { coinAmount } from "../wallet.js";
42
38
  import { recordWalletSpend, requireWalletGrant } from "../wallet-grant.js";
43
- import { createBlobProtocol } from "../walrus-write.js";
44
39
  import { folderIdFor } from "./put.js";
45
40
  export async function putWithWallet(target, options) {
46
41
  const say = options.write ?? ((line) => process.stdout.write(`${line}\n`));
@@ -65,6 +60,12 @@ export async function putWithWallet(target, options) {
65
60
  const asked = parseAsked(options.onCollision);
66
61
  const list = await readFileList(server, key.key, resolved.code, identity.accountId);
67
62
  const rule = paddingRuleOf(list.manifest?.settings);
63
+ // ⛔ WHICH WALLET PAYS, BEFORE ANYTHING IS PRICED — out of the list that was just read, so this
64
+ // costs no second request and the address in the review is the address that signs.
65
+ const wallet = await payingWalletIndex({
66
+ ...options,
67
+ readActiveWallet: async () => activeWalletOf(list.manifest?.settings),
68
+ });
68
69
  const name = options.name ?? basename(localPath);
69
70
  const destination = (options.to ?? "").replace(/^\.?\//, "").replace(/\/$/, "");
70
71
  const parentId = folderIdFor(options.to, list.manifest?.entries ?? []);
@@ -79,6 +80,7 @@ export async function putWithWallet(target, options) {
79
80
  rule,
80
81
  onCollision: asked,
81
82
  settings: list.manifest?.settings,
83
+ wallet,
82
84
  progress: new Progress(options.json === true ? silentSink() : stderrSink(), "uploading"),
83
85
  say,
84
86
  json: options.json === true,
@@ -100,174 +102,75 @@ export async function putWithWallet(target, options) {
100
102
  return 0;
101
103
  }
102
104
  /**
103
- * Review, agree, upload and record ONE file. Null when `--dry-run` stopped at the review.
105
+ * Review, agree, upload and record ONE file for a person who is watching. Null when `--dry-run`
106
+ * stopped at the review.
104
107
  *
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.
108
+ * ⛔ THE AGREEMENT HANDED IN BELOW IS THIS MACHINE'S GRANT, held against the total the review
109
+ * names. It is the one gate between a program and somebody's wallet, and the reason it is
110
+ * handed to the library rather than living inside it: a library call has nobody to ask.
107
111
  */
108
112
  export async function uploadOneWithWallet(ctx, file, options) {
109
113
  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.`,
114
+ let outcome;
115
+ try {
116
+ outcome = await walletPut({
117
+ code: ctx.code,
118
+ apiKey: ctx.apiKey,
119
+ server: ctx.server,
120
+ network: ctx.network,
121
+ accountId: ctx.accountId,
122
+ crypt: ctx.crypt,
123
+ wallet: ctx.wallet,
124
+ partSize: ctx.partSize,
125
+ rule: ctx.rule,
126
+ onCollision: ctx.onCollision,
127
+ }, { source: fileSource(file.localPath, file.size), name: file.name, parentId: file.parentId, destination: file.destination }, {
128
+ epochs: options.epochs,
129
+ storage: options.storage,
130
+ dryRun: options.dryRun,
131
+ readChain: options.readChain,
132
+ sign: options.sign,
133
+ protocol: options.protocol,
134
+ api: options.api,
135
+ onProgress: (sent, total) => ctx.progress.update(sent, total),
136
+ onStep: (step) => tell(ctx, file.size, step),
137
+ onReview: (review) => {
138
+ if (ctx.json)
139
+ return;
140
+ describeUploadReview(say, {
141
+ name: review.name,
142
+ bytes: review.bytes,
143
+ parts: review.parts,
144
+ epochs: review.epochs,
145
+ days: review.days,
146
+ endEpoch: review.endEpoch,
147
+ walNeeded: review.budget.walNeededFrost,
148
+ tipMist: review.tipMist,
149
+ storage: review.storage,
150
+ heldResources: review.heldResources,
151
+ }, review.budget);
152
+ },
153
+ agree: (review) => {
154
+ requireWalletGrant("seal", { walFrost: review.budget.walNeededFrost, suiMist: review.budget.suiNeededMist }, new Date(options.now ?? Date.now()));
155
+ },
156
+ onSpend: recordWalletSpend,
126
157
  });
127
158
  }
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);
159
+ finally {
160
+ ctx.progress.done();
189
161
  }
190
- if (options.dryRun === true) {
162
+ const facts = factsOf(outcome.review);
163
+ if (outcome.kind === "review") {
191
164
  if (ctx.json)
192
- say(JSON.stringify({ dryRun: true, name: file.name, ...facts, signed: false }));
165
+ say(JSON.stringify({ dryRun: true, name: outcome.review.name, ...facts, signed: false }));
193
166
  else {
194
167
  say(``);
195
- if (budget.shortfall !== null)
196
- say(` ⚠ ${budget.shortfall} As it stands, it would be refused.`);
168
+ if (outcome.review.budget.shortfall !== null)
169
+ say(` ⚠ ${outcome.review.budget.shortfall} As it stands, it would be refused.`);
197
170
  say(` Nothing was signed and nothing was sent. Run the same command without --dry-run to upload it.`);
198
171
  }
199
172
  return null;
200
173
  }
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
174
  // ⛔ THE STANDING SHARE, AFTER THE PAYMENT AND NEVER INSIDE IT: a gift that fails must not fail
272
175
  // the upload. A resumed run paid nothing now, so it owes nothing now. In --json the tip speaks
273
176
  // on stderr — stdout carries the machine's one answer and nothing else.
@@ -276,11 +179,37 @@ export async function uploadOneWithWallet(ctx, file, options) {
276
179
  network: ctx.network,
277
180
  code: ctx.code,
278
181
  settings: ctx.settings,
279
- paidWalFrost: result.resumed ? 0n : budget.walNeededFrost,
182
+ wallet: ctx.wallet,
183
+ paidWalFrost: outcome.resumed ? 0n : outcome.review.budget.walNeededFrost,
280
184
  say: ctx.json ? (line) => void process.stderr.write(`${line}\n`) : say,
281
185
  ...(options.tip ?? {}),
282
186
  });
283
- return { itemId: result.itemId, savedAs: added.name, replaced: added.replaced?.id ?? null, seq: added.seq, resumed: result.resumed, facts };
187
+ return { itemId: outcome.itemId, savedAs: outcome.savedAs, replaced: outcome.replaced, seq: outcome.fileListVersion, resumed: outcome.resumed, facts };
188
+ }
189
+ /** The review as `--json` prints it: base units as strings, with the coin amounts beside them. */
190
+ function factsOf(review) {
191
+ const { budget, storage } = review;
192
+ return {
193
+ bytes: review.bytes,
194
+ sealedBytes: review.sealedBytes,
195
+ parts: review.parts,
196
+ epochs: review.epochs,
197
+ days: review.days,
198
+ endEpoch: review.endEpoch,
199
+ paidFrom: "wallet",
200
+ priceFrost: budget.walNeededFrost.toString(),
201
+ priceWal: coinAmount(budget.walNeededFrost),
202
+ tipMist: review.tipMist.toString(),
203
+ tipSui: coinAmount(review.tipMist),
204
+ feeMist: budget.feeMist === null ? null : budget.feeMist.toString(),
205
+ feeSui: budget.feeMist === null ? null : coinAmount(budget.feeMist),
206
+ wallet: budget.address,
207
+ walletWal: budget.walFrost === null ? null : coinAmount(budget.walFrost),
208
+ walletSui: budget.suiMist === null ? null : coinAmount(budget.suiMist),
209
+ storage: storage.kind === "buy"
210
+ ? { kind: "buy" }
211
+ : { kind: "reuse", objectId: storage.objectId, cutToBytes: storage.cutToBytes, leftoverBytes: storage.leftoverBytes },
212
+ };
284
213
  }
285
214
  /** Each step, for a person watching. The signatures are named as what they are. */
286
215
  function tell(ctx, size, step) {
@@ -307,10 +236,3 @@ function tell(ctx, size, step) {
307
236
  if (step.step === "committing")
308
237
  say(` saving to the drive`);
309
238
  }
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
  *
@@ -19,7 +19,8 @@ import { depositDefaultOf, depositLines, parseDeposit, refuseDepositWithWallet }
19
19
  import { DERIVED, loadCrypto } from "../crypto.js";
20
20
  import { buildIndex, fullPathOf, isLive, KIND_FOLDER, normalisePath } from "../drive-paths.js";
21
21
  import { NmtsError } from "../errors.js";
22
- import { Progress, silentSink, stderrSink } from "../progress.js";
22
+ import { Progress, silentSink } from "../progress.js";
23
+ import { stderrSink } from "../progress-node.js";
23
24
  import { setTrashed } from "../item-trash.js";
24
25
  import { addEntry } from "../manifest-write.js";
25
26
  import { paddingRuleOf, readFileList } from "../manifest.js";
@@ -28,35 +29,14 @@ import { BINARY_NAME } from "../product.js";
28
29
  import { resolveServer } from "../server.js";
29
30
  import { clearItemRecord, clearReservation } from "../upload-store.js";
30
31
  import { createUploadApi } from "../upload-api.js";
31
- import { fileSource, partKeysOf, uploadFile } from "../upload-file.js";
32
- import { CREDIT_BYTES, creditsFor, measureLocal, partSizeFor, planAndPrice, UPLOAD_EPOCHS, } from "../upload-price.js";
32
+ import { partKeysOf, uploadFile } from "../upload-file.js";
33
+ import { fileSource } from "../upload-file-node.js";
34
+ import { CREDIT_BYTES, creditsFor, partSizeFor, planAndPrice, UPLOAD_EPOCHS } from "../upload-price.js";
35
+ import { measureLocal } from "../upload-price-node.js";
33
36
  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
- }
37
+ /** Who pays, and the options that lose their meaning under that answer — the rule is in `put-payer.ts`; this is its one road. */
38
+ import { payerOf, refuseWalletOnlyOptions } from "./put-payer.js";
39
+ export { payerOf, refuseWalletOnlyOptions };
60
40
  /**
61
41
  * The folder id `--to` names, or null for the root. Refuses rather than guessing.
62
42
  *
@@ -283,9 +263,9 @@ export async function put(target, options = {}) {
283
263
  // ⛔ ONLY NOW, AND EVERY PART. Until the entry is in the list the file is paid for and invisible,
284
264
  // and the records are the only thing that lets a second run finish the job without spending
285
265
  // again. Clearing the file-level one first would leave a run able to commit a second time.
286
- clearItemRecord(result.fileKey);
266
+ await clearItemRecord(result.fileKey);
287
267
  for (const record of partKeysOf(result.fileKey, result.parts))
288
- clearReservation(record);
268
+ await clearReservation(record);
289
269
  // ⛔ THE DISPLACED FILE IS TOLD TO THE SERVER ONLY NOW, and only after the new one is in the
290
270
  // list. Until this line the person still had the file they started with.
291
271
  // ⚠ A failure here leaves it hidden in this account's trash while the server still counts it as
@@ -14,7 +14,7 @@ import { resolve } from "node:path";
14
14
  import { request } from "../api.js";
15
15
  import { loadCrypto } from "../crypto.js";
16
16
  import { fetchWithKey } from "../download.js";
17
- import { fileSink, stdoutSink } from "../download-sink.js";
17
+ import { fileSink, stdoutSink } from "../download-sink-node.js";
18
18
  import { NmtsError } from "../errors.js";
19
19
  import { destinationFor } from "../safe-path.js";
20
20
  import { resolveNetwork } from "../network.js";
@@ -39,5 +39,5 @@ export async function runSettings(command, args) {
39
39
  return await mode(args.operands[0], args.operands[1], { json: args.json });
40
40
  }
41
41
  const { onCollision } = await import("./on-collision.js");
42
- return onCollision(args.operands[0], { json: args.json });
42
+ return await onCollision(args.operands[0], { json: args.json });
43
43
  }
@@ -57,7 +57,7 @@ async function sendReport(options) {
57
57
  const clean = (text) => omitLiterals(redact(text), omit);
58
58
  const message = clean(await readMessage(options)).trim();
59
59
  requireLength(message, MAX_MESSAGE_CHARS, "message");
60
- const attachment = packLog(options.attachLog, clean);
60
+ const attachment = await packLog(options.attachLog, clean);
61
61
  const total = Buffer.byteLength(message, "utf8") + (attachment?.bytes ?? 0);
62
62
  // ⛔ THE MACHINE-READABLE PREVIEW IS THE ONLY THING PRINTED ON THAT PATH, and it is printed
63
63
  // INSTEAD of sending rather than before it. Two JSON objects on one stream would let a caller
@@ -169,7 +169,7 @@ function standardInput() {
169
169
  });
170
170
  }
171
171
  /** Read the newest runs and pack them, or `null` when the option was not given. */
172
- function packLog(asked, clean) {
172
+ async function packLog(asked, clean) {
173
173
  if (asked === undefined)
174
174
  return null;
175
175
  const count = asked === "" ? DEFAULT_ATTACHED_RUNS : Number(asked);
@@ -180,7 +180,7 @@ function packLog(asked, clean) {
180
180
  }
181
181
  if (count === 0)
182
182
  return null;
183
- const runs = readRuns(count);
183
+ const runs = await readRuns(count);
184
184
  if (runs.length === 0)
185
185
  return null;
186
186
  return buildLogAttachment(runs, clean, MAX_ATTACHMENT_BYTES);
@@ -67,7 +67,7 @@ export async function unlock(action, target, options = {}) {
67
67
  }
68
68
  if (action === "unlock") {
69
69
  const key = keyOrFail(target);
70
- if (options.readLine === undefined && !stdinIsATerminal() && currentMode() !== "skip-permissions") {
70
+ if (options.readLine === undefined && !stdinIsATerminal() && (await currentMode()) !== "skip-permissions") {
71
71
  throw new NmtsError(`A person unlocks, at a terminal — stdin here is not one.`, {
72
72
  exitCode: 5,
73
73
  nextStep: `If you are an agent: do not unlock things. Show the person what \`${BINARY_NAME} unlock\` ` +
@@ -80,7 +80,7 @@ export async function unlock(action, target, options = {}) {
80
80
  say(c.what);
81
81
  say(` ${c.risk}`);
82
82
  say(` ${c.limit}`);
83
- if (currentMode() !== "skip-permissions") {
83
+ if ((await currentMode()) !== "skip-permissions") {
84
84
  const ask = options.readLine ?? promptLine;
85
85
  const answer = (await ask(`Unlock ${key} on this machine? [y/N] `)).trim();
86
86
  if (answer !== "y" && answer !== "Y") {
@@ -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;
@@ -6,7 +6,10 @@ export interface WalletHallOptions {
6
6
  /** `--name`: be listed under this. `--remove`: go back to a shortened address. */
7
7
  name?: string | undefined;
8
8
  remove?: boolean;
9
- /** A SEAM, NOT AN OPTION no flag reaches it. It exists so a test never signs for real. */
9
+ /** `--wallet N`: which wallet signs the name, this run only. Absent = the account's own number. */
10
+ wallet?: string | undefined;
11
+ /** ⚠ SEAMS, NOT OPTIONS — no flag reaches them. They exist so a test never signs for real. */
12
+ readActiveWallet?: () => Promise<number>;
10
13
  sign?: SignMessage;
11
14
  }
12
15
  /** One row of the hall, as this tool needs it. Amounts are base units, exactly as they arrived. */
@@ -6,9 +6,12 @@
6
6
  // taken off is the NAME, which puts the entry back to a shortened address.
7
7
  //
8
8
  // ⛔ A NAME IS PUBLISHED, AND THAT IS WHY IT IS SIGNED. The proof that an address is yours is a
9
- // signature by that address, so `--name` signs a short message with the wallet this NMTS
10
- // key derives (`wallet-sign.ts`) and sends the signature. No session, no API key, no account
11
- // id goes with it the server is told an address, a name and a signature, and nothing else.
9
+ // signature by that address, so `--name` signs a short message with the wallet this account
10
+ // pays from (`wallet-pay-index.ts`, `wallet-sign.ts`) and sends the signature. That is the
11
+ // wallet a gift left from, so it is the address the hall holds an entry for. Reading WHICH
12
+ // wallet that is opens this account's own file list, so `--name` needs an API key where
13
+ // reading the hall needs none; what is SENT is unchanged — no session, no API key, no account
14
+ // id, just an address, a name and a signature.
12
15
  //
13
16
  // ⛔ THE MESSAGE IS BUILT IN ONE PLACE, `hallMessage` BELOW, because the server rebuilds the same
14
17
  // bytes from the fields it was sent and compares. A space added on either side makes every
@@ -21,6 +24,7 @@ import { isRecord } from "../guards.js";
21
24
  import { BINARY_NAME, HOME_URL } from "../product.js";
22
25
  import { resolveServer } from "../server.js";
23
26
  import { coinAmount, walletAddress } from "../wallet.js";
27
+ import { payingWalletIndex } from "../wallet-pay-index.js";
24
28
  /** How many rows a terminal gets. The page at the site carries the rest. */
25
29
  const SHOWN = 10;
26
30
  /**
@@ -93,7 +97,7 @@ const REFUSALS = {
93
97
  signature_invalid: {
94
98
  line: "The server did not accept the signature.",
95
99
  exitCode: 1,
96
- nextStep: `Nothing was published. \`${BINARY_NAME} wallet address\` shows the wallet this run signs as.`,
100
+ nextStep: `Nothing was published. \`${BINARY_NAME} wallet list\` shows this key's wallets and which one this run signs as.`,
97
101
  },
98
102
  };
99
103
  /** The server the sealed sign-in points at, when there is one this run can read. */
@@ -180,14 +184,17 @@ async function readHall(server) {
180
184
  /** `--name` and `--remove`: sign the message, send it, and say what the hall shows now. */
181
185
  async function setName(server, say, options) {
182
186
  const resolved = await requireAccountCode();
183
- // ⛔ The address is derived here rather than taken from a flag: a signature only proves ownership
184
- // of the address it was made by, so the two must come from the same NMTS key.
185
- const address = await walletAddress(resolved.code);
187
+ // ⛔ WHICH WALLET, BEFORE THE ADDRESS the entry belongs to the wallet the gift came from, which
188
+ // is the account's own number, and the address is then derived here rather than taken from a
189
+ // flag: a signature only proves ownership of the address it was made by, so the address, the
190
+ // message and the key that signs it have to be the same wallet.
191
+ const wallet = await payingWalletIndex({ ...options, server });
192
+ const address = await walletAddress(resolved.code, wallet);
186
193
  const name = options.remove === true ? null : (options.name ?? null);
187
194
  const issuedAt = new Date(Date.now()).toISOString();
188
195
  const message = hallMessage(address, name, issuedAt);
189
196
  const sign = options.sign ?? (await import("../wallet-sign.js")).signMessage;
190
- const signature = await sign({ code: resolved.code, message });
197
+ const signature = await sign({ code: resolved.code, wallet, message });
191
198
  let answer;
192
199
  try {
193
200
  answer = await request(server, "/v1/gifts/listing", {
@@ -207,6 +214,7 @@ async function setName(server, say, options) {
207
214
  }
208
215
  const label = isRecord(answer) ? text(answer["label"]) : "";
209
216
  say(`Listed as ${label}`);
217
+ say(` from ${address} (wallet ${wallet})`);
210
218
  say(``);
211
219
  say(` The hall of fame is at ${HOME_URL}/hall. Run \`${BINARY_NAME} wallet hall --remove\` to go`);
212
220
  say(` back to a shortened address; the gifts themselves stay on the chain either way.`);