@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
@@ -0,0 +1,69 @@
1
+ import type { StorageOpShape } from "./storage-control-chain.ts";
2
+ import type { SwapShape } from "./wallet-swap-chain.ts";
3
+ import type { TransferShape } from "./wallet-send-chain.ts";
4
+ import type { RegisterShape } from "./upload-wallet-plan.ts";
5
+ import type { Certificate } from "./upload-wire.ts";
6
+ import type { Network } from "./network.ts";
7
+ /** The seam `commands/wallet-hall.ts` signs through. Returns the base64 signature, nothing else. */
8
+ export type SignMessage = (input: {
9
+ /** ⛔ The NMTS key. It never leaves this machine: it derives the wallet and nothing else. */
10
+ code: string;
11
+ /**
12
+ * Which of this key's wallets proves the name.
13
+ *
14
+ * ⛔ IT IS THE PAYING WALLET BECAUSE THAT IS THE WALLET THE GIFT CAME FROM. A hall entry is an
15
+ * address, and `wallet donate` sends from the account's own number; signing with any other
16
+ * wallet would offer the server a name for an address that has never given anything.
17
+ */
18
+ wallet: number;
19
+ message: string;
20
+ }) => Promise<string>;
21
+ /** The seam `commands/wallet-send.ts` signs through. Returns the transaction digest. */
22
+ export type SignTransfer = (input: {
23
+ network: string;
24
+ /** ⛔ The NMTS key. It never leaves this machine: it derives the wallet and nothing else. */
25
+ code: string;
26
+ /** Which of this key's wallets pays — the account's own number (`wallet-pay-index.ts`). */
27
+ wallet: number;
28
+ shape: TransferShape;
29
+ }) => Promise<string>;
30
+ /** The seam `commands/wallet-swap.ts` signs through. Returns the transaction digest. */
31
+ export type SignSwap = (input: {
32
+ network: Network;
33
+ /** ⛔ The NMTS key. It never leaves this machine: it derives the wallet and nothing else. */
34
+ code: string;
35
+ /** Which of this key's wallets swaps — the account's own number (`wallet-pay-index.ts`). */
36
+ wallet: number;
37
+ shape: SwapShape;
38
+ }) => Promise<string>;
39
+ /** The seam the wallet rail registers through. */
40
+ export type SignBlobRegister = (input: {
41
+ network: Network;
42
+ code: string;
43
+ wallet: number;
44
+ relayUrl: string;
45
+ } & RegisterShape) => Promise<{
46
+ digest: string;
47
+ blobObjectId: string;
48
+ endEpoch: number;
49
+ }>;
50
+ /** The seam the wallet rail certifies through. Returns the transaction digest. */
51
+ export type SignBlobCertify = (input: {
52
+ network: Network;
53
+ code: string;
54
+ /** The same wallet that registered the part — it is the one that owns the blob object. */
55
+ wallet: number;
56
+ relayUrl: string;
57
+ blobId: string;
58
+ blobObjectId: string;
59
+ certificate: Certificate;
60
+ }) => Promise<string>;
61
+ /** How a storage-resource operation is signed; the shape carries what the review priced. */
62
+ export type SignStorageOp = (input: {
63
+ network: Network;
64
+ code: string;
65
+ /** Which of this key's wallets holds the resource — the account's own number. */
66
+ wallet: number;
67
+ shape: StorageOpShape;
68
+ walrusPackageId: string;
69
+ }) => Promise<string>;
@@ -0,0 +1,16 @@
1
+ // The shapes the signing module is reached through — one per thing this tool can sign.
2
+ //
3
+ // ⛔ THEY ARE SEAMS BECAUSE A TEST MUST BE ABLE TO PROVE THAT NOTHING SIGNED. Every command that
4
+ // spends prints a review first and stops there without `--yes`; handing it a function that
5
+ // fails the test if it is ever called is the only way to hold that promise, and a signature
6
+ // that reached a chain in a test would cost money every time the suite ran.
7
+ //
8
+ // ⛔ EACH ONE CARRIES **WHICH WALLET** WHERE THE MONEY COMES FROM IT (2026-09-16). One
9
+ // NMTS key opens a wallet at every index and the account says which one pays (`activeWallet`
10
+ // in the sealed file list); the command resolves that number BEFORE it prices anything, so the
11
+ // address in the review is the address that signs. A seam that let the number be omitted would
12
+ // let a review be printed for one wallet and a transaction signed by another.
13
+ //
14
+ // ⚠ They moved out of `wallet-sign.ts` on 2026-09-16 — that file is what signs, and it has a
15
+ // ceiling. Re-exported from there, so no caller spells a new path.
16
+ export {};
@@ -1,10 +1,6 @@
1
1
  import type { SignExtension } from "./extend-plan.ts";
2
- import { type TransferShape } from "./wallet-send-chain.ts";
3
- import { type StorageOpShape } from "./storage-control-chain.ts";
4
- import { type SwapShape } from "./wallet-swap-chain.ts";
5
- import type { Network } from "./network.ts";
6
- import type { RegisterShape } from "./upload-wallet-plan.ts";
7
- import type { Certificate } from "./upload-wire.ts";
2
+ import type { SignBlobCertify, SignBlobRegister, SignMessage, SignStorageOp, SignSwap, SignTransfer } from "./wallet-sign-seams.ts";
3
+ export type { SignBlobCertify, SignBlobRegister, SignMessage, SignStorageOp, SignSwap, SignTransfer, } from "./wallet-sign-seams.ts";
8
4
  /**
9
5
  * The address this tool would sign as.
10
6
  *
@@ -14,7 +10,7 @@ import type { Certificate } from "./upload-wire.ts";
14
10
  * part is silent — a signature from an address with nothing in it, or worse, money sent to an
15
11
  * address that signs nothing. A test compares them, offline, for free.
16
12
  */
17
- export declare function signerAddress(code: string): Promise<string>;
13
+ export declare function signerAddress(code: string, index?: number): Promise<string>;
18
14
  /**
19
15
  * Extend every listed blob by `epochs`, in ONE transaction, signed by the account's own wallet.
20
16
  *
@@ -50,19 +46,6 @@ export declare const signTransfer: SignTransfer;
50
46
  * signed text from ever being read as a transaction this wallet authorised.
51
47
  */
52
48
  export declare const signMessage: SignMessage;
53
- /** The seam `commands/wallet-hall.ts` signs through. Returns the base64 signature, nothing else. */
54
- export type SignMessage = (input: {
55
- /** ⛔ The NMTS key. It never leaves this machine: it derives the wallet and nothing else. */
56
- code: string;
57
- message: string;
58
- }) => Promise<string>;
59
- /** The seam `commands/wallet-send.ts` signs through. Returns the transaction digest. */
60
- export type SignTransfer = (input: {
61
- network: string;
62
- /** ⛔ The NMTS key. It never leaves this machine: it derives the wallet and nothing else. */
63
- code: string;
64
- shape: TransferShape;
65
- }) => Promise<string>;
66
49
  /**
67
50
  * Swap SUI for WAL or WAL for SUI on the named venue, in ONE transaction, signed by the account's
68
51
  * own wallet. Every output goes back to the signer — DeepBook's three coins are sent there by this
@@ -75,13 +58,6 @@ export type SignTransfer = (input: {
75
58
  * ⚠ A FAILURE HERE IS NOT PROOF THAT NOTHING HAPPENED — the same words as the extension above.
76
59
  */
77
60
  export declare const signSwap: SignSwap;
78
- /** The seam `commands/wallet-swap.ts` signs through. Returns the transaction digest. */
79
- export type SignSwap = (input: {
80
- network: Network;
81
- /** ⛔ The NMTS key. It never leaves this machine: it derives the wallet and nothing else. */
82
- code: string;
83
- shape: SwapShape;
84
- }) => Promise<string>;
85
61
  /**
86
62
  * Register ONE part's blob, signed by the account's own wallet: the relay's tip, then the storage
87
63
  * — bought for `epochs`, or a resource the wallet already holds, cut to fit first if asked.
@@ -97,32 +73,6 @@ export type SignSwap = (input: {
97
73
  export declare const signBlobRegister: SignBlobRegister;
98
74
  /** Certify ONE registered part from the relay's certificate. Gas only; nothing else leaves the wallet. */
99
75
  export declare const signBlobCertify: SignBlobCertify;
100
- /** The seam the wallet rail registers through. */
101
- export type SignBlobRegister = (input: {
102
- network: Network;
103
- code: string;
104
- relayUrl: string;
105
- } & RegisterShape) => Promise<{
106
- digest: string;
107
- blobObjectId: string;
108
- endEpoch: number;
109
- }>;
110
- /** The seam the wallet rail certifies through. Returns the transaction digest. */
111
- export type SignBlobCertify = (input: {
112
- network: Network;
113
- code: string;
114
- relayUrl: string;
115
- blobId: string;
116
- blobObjectId: string;
117
- certificate: Certificate;
118
- }) => Promise<string>;
119
- /** How a storage-resource operation is signed; the shape carries what the review priced. */
120
- export type SignStorageOp = (input: {
121
- network: Network;
122
- code: string;
123
- shape: StorageOpShape;
124
- walrusPackageId: string;
125
- }) => Promise<string>;
126
76
  /**
127
77
  * Cut, join or hand over a storage resource, in ONE transaction, signed by the account's own wallet.
128
78
  *
@@ -45,7 +45,7 @@ import { BUILT_IN_WALLET_INDEX } from "./wallet.js";
45
45
  * ⛔ NOT EXPORTED. A caller that could hold this could sign anything, and the whole of this tool's
46
46
  * story about the wallet is that one command signs one shape of transaction.
47
47
  */
48
- async function keypairFor(code) {
48
+ async function keypairFor(code, index) {
49
49
  // The one refusal text for a malformed code lives in `account.ts`, so a typo fails here the same
50
50
  // way it fails everywhere else in this tool rather than as an engine error.
51
51
  await assertUsableCode(code);
@@ -61,7 +61,7 @@ async function keypairFor(code) {
61
61
  derived = glue.kdf_derive(bytes);
62
62
  const [from, to] = DERIVED.walletRoot;
63
63
  root = derived.slice(from, to);
64
- seed = glue.wallet_seed_for(root, BUILT_IN_WALLET_INDEX);
64
+ seed = glue.wallet_seed_for(root, index);
65
65
  return Ed25519Keypair.fromSecretKey(seed);
66
66
  }
67
67
  catch (error) {
@@ -86,8 +86,8 @@ async function keypairFor(code) {
86
86
  * part is silent — a signature from an address with nothing in it, or worse, money sent to an
87
87
  * address that signs nothing. A test compares them, offline, for free.
88
88
  */
89
- export async function signerAddress(code) {
90
- return (await keypairFor(code)).toSuiAddress();
89
+ export async function signerAddress(code, index = BUILT_IN_WALLET_INDEX) {
90
+ return (await keypairFor(code, index)).toSuiAddress();
91
91
  }
92
92
  /**
93
93
  * Extend every listed blob by `epochs`, in ONE transaction, signed by the account's own wallet.
@@ -104,7 +104,7 @@ export async function signerAddress(code) {
104
104
  * was executed. The caller re-reads the chain rather than offering a second attempt against
105
105
  * numbers it read before.
106
106
  */
107
- export const signExtension = async ({ network, code, objectIds, epochs }) => {
107
+ export const signExtension = async ({ network, code, wallet, objectIds, epochs }) => {
108
108
  const unique = [...new Set(objectIds)];
109
109
  if (unique.length === 0) {
110
110
  throw new NmtsError("There is nothing on this file that can be extended.", { exitCode: 4 });
@@ -113,7 +113,7 @@ export const signExtension = async ({ network, code, objectIds, epochs }) => {
113
113
  throw new NmtsError("An extension must be a positive whole number of epochs.", { exitCode: 2 });
114
114
  }
115
115
  const client = walrusClient(network);
116
- const keypair = await keypairFor(code);
116
+ const keypair = await keypairFor(code, wallet);
117
117
  const tx = new Transaction();
118
118
  // The sender must be set before the fragments resolve: paying with the SDK's default coin
119
119
  // selection picks the WAL coins from the sender's own address.
@@ -158,9 +158,9 @@ export const signExtension = async ({ network, code, objectIds, epochs }) => {
158
158
  *
159
159
  * ⚠ A FAILURE HERE IS NOT PROOF THAT NOTHING HAPPENED — the same words as the extension above.
160
160
  */
161
- export const signTransfer = async ({ network, code, shape }) => {
161
+ export const signTransfer = async ({ network, code, wallet, shape }) => {
162
162
  const client = walrusClient(network);
163
- const keypair = await keypairFor(code);
163
+ const keypair = await keypairFor(code, wallet);
164
164
  const tx = transferTransaction({ ...shape, sender: keypair.toSuiAddress() });
165
165
  const result = await client.signAndExecuteTransaction({
166
166
  transaction: tx,
@@ -189,8 +189,10 @@ export const signTransfer = async ({ network, code, shape }) => {
189
189
  * makes. `signPersonalMessage` and not `sign`: that intent is the domain separator which stops
190
190
  * signed text from ever being read as a transaction this wallet authorised.
191
191
  */
192
- export const signMessage = async ({ code, message }) => {
193
- const keypair = await keypairFor(code);
192
+ export const signMessage = async ({ code, wallet, message }) => {
193
+ // The wallet the caller resolved signs it: the hall entry is that wallet's address, and it is
194
+ // the wallet a gift left from, so any other one would name an address that gave nothing.
195
+ const keypair = await keypairFor(code, wallet);
194
196
  const { signature } = await keypair.signPersonalMessage(new TextEncoder().encode(message));
195
197
  return signature;
196
198
  };
@@ -205,9 +207,9 @@ export const signMessage = async ({ code, message }) => {
205
207
  *
206
208
  * ⚠ A FAILURE HERE IS NOT PROOF THAT NOTHING HAPPENED — the same words as the extension above.
207
209
  */
208
- export const signSwap = async ({ network, code, shape }) => {
210
+ export const signSwap = async ({ network, code, wallet, shape }) => {
209
211
  const client = walrusClient(network);
210
- const keypair = await keypairFor(code);
212
+ const keypair = await keypairFor(code, wallet);
211
213
  const tx = swapTransaction({ ...shape, network, sender: keypair.toSuiAddress() });
212
214
  const result = await client.signAndExecuteTransaction({
213
215
  transaction: tx,
@@ -247,9 +249,9 @@ function refusedBecause(result) {
247
249
  * ⚠ A FAILURE HERE IS NOT PROOF THAT NOTHING HAPPENED — the same words as the extension above.
248
250
  * The caller keeps its record and re-reads it rather than registering again.
249
251
  */
250
- export const signBlobRegister = async ({ network, code, relayUrl, ...shape }) => {
252
+ export const signBlobRegister = async ({ network, code, wallet, relayUrl, ...shape }) => {
251
253
  const client = payingClient(network, relayUrl);
252
- const keypair = await keypairFor(code);
254
+ const keypair = await keypairFor(code, wallet);
253
255
  const tx = await registerTransaction(client, network, { ...shape, sender: keypair.toSuiAddress() });
254
256
  const result = await client.signAndExecuteTransaction({
255
257
  transaction: tx,
@@ -270,9 +272,9 @@ export const signBlobRegister = async ({ network, code, relayUrl, ...shape }) =>
270
272
  return { digest: result.digest, ...blob };
271
273
  };
272
274
  /** Certify ONE registered part from the relay's certificate. Gas only; nothing else leaves the wallet. */
273
- export const signBlobCertify = async ({ network, code, relayUrl, ...shape }) => {
275
+ export const signBlobCertify = async ({ network, code, wallet, relayUrl, ...shape }) => {
274
276
  const client = payingClient(network, relayUrl);
275
- const keypair = await keypairFor(code);
277
+ const keypair = await keypairFor(code, wallet);
276
278
  const tx = certifyTransaction(client, shape);
277
279
  tx.setSender(keypair.toSuiAddress());
278
280
  const result = await client.signAndExecuteTransaction({
@@ -298,9 +300,11 @@ export const signBlobCertify = async ({ network, code, relayUrl, ...shape }) =>
298
300
  * so what is signed is what was reviewed. A failed execution still has a digest and still
299
301
  * spent its gas, so the status is read and a failure is said as one.
300
302
  */
301
- export const signStorageOp = async ({ network, code, shape, walrusPackageId }) => {
303
+ export const signStorageOp = async ({ network, code, wallet, shape, walrusPackageId }) => {
302
304
  const client = walrusClient(network);
303
- const keypair = await keypairFor(code);
305
+ // The resource the review named was read from this wallet's address, so this wallet is the one
306
+ // that can reshape or hand it over.
307
+ const keypair = await keypairFor(code, wallet);
304
308
  const tx = storageOpTransaction(shape, { walrusPackageId, sender: keypair.toSuiAddress() });
305
309
  const result = await client.signAndExecuteTransaction({ transaction: tx, signer: keypair, options: { showEffects: true } });
306
310
  const effects = result.effects;
package/dist/wallet.d.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  import { type CryptoGlue } from "./crypto.ts";
2
2
  import type { Network } from "./network.ts";
3
3
  /**
4
- * The wallet this NMTS key opens by itself.
4
+ * The wallet this NMTS key opens by itself, when nobody has said which one.
5
5
  *
6
- * ⛔ IT IS WALLET 0 BECAUSE THAT IS THE ONE THE BROWSER OPENS. Every wallet, including this one,
7
- * comes out of `wallet_seed_for` — there is no special case for the first — so the index is the
8
- * whole of the difference between "the account's wallet" and somebody else's.
6
+ * ⛔ IT IS WALLET 0 BECAUSE THAT IS THE ONE EVERY ACCOUNT STARTS WITH. Every wallet, including this
7
+ * one, comes out of `wallet_seed_for` — there is no special case for the first — so the index is
8
+ * the whole of the difference between one wallet of this key and another.
9
9
  *
10
- * EXPORTED SO THE SIGNER CANNOT PICK ITS OWN. `extend-sign.ts` derives a keypair from the same
11
- * root and has to reach the SAME wallet as the address printed here; a second literal `0` over
12
- * there would be a second answer to a question with one right one, and the failure is silent —
13
- * a signature from an address nobody funded. `extend-sign.test.ts` compares the two.
10
+ * IT IS THE OFFLINE ANSWER, NOT THE PAYING ONE (2026-09-16). `nmts wallet address` asks
11
+ * nothing of anybody and so cannot know which wallet the account pays from; every command that
12
+ * SPENDS reads that number out of the sealed file list first (`wallet-pay-index.ts`) and refuses
13
+ * rather than falling back here — paying from the wrong wallet is not a thing to guess at.
14
14
  */
15
15
  export declare const BUILT_IN_WALLET_INDEX = 0;
16
16
  /** The chain's own coin. Its type is fixed by the chain itself and takes no network. */
@@ -50,12 +50,12 @@ export declare function walCoinType(network: Network): string;
50
50
  */
51
51
  export declare function addressFromSeed(seed: Uint8Array): string;
52
52
  /**
53
- * The address of the wallet this NMTS key derives. Offline: nothing is asked of anybody.
53
+ * The address of one of the wallets this NMTS key derives. Offline: nothing is asked of anybody.
54
54
  *
55
- * The same address on every network — an account has one wallet, and which chain it is looked up
56
- * on is a separate question from what it is called.
55
+ * The same address on every network — a wallet is the same wallet on every chain, and which chain
56
+ * it is looked up on is a separate question from what it is called.
57
57
  */
58
- export declare function walletAddress(code: string): Promise<string>;
58
+ export declare function walletAddress(code: string, index?: number): Promise<string>;
59
59
  /**
60
60
  * The engine functions this module uses. Narrowed to the one it actually calls.
61
61
  *
@@ -73,7 +73,7 @@ export type WalletGlue = Pick<CryptoGlue, "wallet_seed_for">;
73
73
  * engine expands from it — are wiped before it returns, on the failing path as well as the
74
74
  * good one. Neither one leaves.
75
75
  */
76
- export declare function addressFromDerived(glue: WalletGlue, derived: Uint8Array): string;
76
+ export declare function addressFromDerived(glue: WalletGlue, derived: Uint8Array, index?: number): string;
77
77
  /**
78
78
  * How much of one coin an address holds, exactly as the chain reported it — or why nobody knows.
79
79
  *
package/dist/wallet.js CHANGED
@@ -19,16 +19,16 @@ import { assertUsableCode } from "./account.js";
19
19
  import { DERIVED, loadCrypto } from "./crypto.js";
20
20
  import { NmtsError } from "./errors.js";
21
21
  /**
22
- * The wallet this NMTS key opens by itself.
22
+ * The wallet this NMTS key opens by itself, when nobody has said which one.
23
23
  *
24
- * ⛔ IT IS WALLET 0 BECAUSE THAT IS THE ONE THE BROWSER OPENS. Every wallet, including this one,
25
- * comes out of `wallet_seed_for` — there is no special case for the first — so the index is the
26
- * whole of the difference between "the account's wallet" and somebody else's.
24
+ * ⛔ IT IS WALLET 0 BECAUSE THAT IS THE ONE EVERY ACCOUNT STARTS WITH. Every wallet, including this
25
+ * one, comes out of `wallet_seed_for` — there is no special case for the first — so the index is
26
+ * the whole of the difference between one wallet of this key and another.
27
27
  *
28
- * EXPORTED SO THE SIGNER CANNOT PICK ITS OWN. `extend-sign.ts` derives a keypair from the same
29
- * root and has to reach the SAME wallet as the address printed here; a second literal `0` over
30
- * there would be a second answer to a question with one right one, and the failure is silent —
31
- * a signature from an address nobody funded. `extend-sign.test.ts` compares the two.
28
+ * IT IS THE OFFLINE ANSWER, NOT THE PAYING ONE (2026-09-16). `nmts wallet address` asks
29
+ * nothing of anybody and so cannot know which wallet the account pays from; every command that
30
+ * SPENDS reads that number out of the sealed file list first (`wallet-pay-index.ts`) and refuses
31
+ * rather than falling back here — paying from the wrong wallet is not a thing to guess at.
32
32
  */
33
33
  export const BUILT_IN_WALLET_INDEX = 0;
34
34
  /**
@@ -83,12 +83,12 @@ export function addressFromSeed(seed) {
83
83
  return Ed25519Keypair.fromSecretKey(seed).toSuiAddress();
84
84
  }
85
85
  /**
86
- * The address of the wallet this NMTS key derives. Offline: nothing is asked of anybody.
86
+ * The address of one of the wallets this NMTS key derives. Offline: nothing is asked of anybody.
87
87
  *
88
- * The same address on every network — an account has one wallet, and which chain it is looked up
89
- * on is a separate question from what it is called.
88
+ * The same address on every network — a wallet is the same wallet on every chain, and which chain
89
+ * it is looked up on is a separate question from what it is called.
90
90
  */
91
- export async function walletAddress(code) {
91
+ export async function walletAddress(code, index = BUILT_IN_WALLET_INDEX) {
92
92
  // ⛔ The one refusal text for a malformed code lives in `account.ts`. Checking here means a typo
93
93
  // fails the same way it fails everywhere else in this tool rather than as an engine error.
94
94
  await assertUsableCode(code);
@@ -108,7 +108,7 @@ export async function walletAddress(code) {
108
108
  // everything sliced out of it, on the failing paths as well as the good one.
109
109
  const derived = glue.kdf_derive(bytes);
110
110
  try {
111
- return addressFromDerived(glue, derived);
111
+ return addressFromDerived(glue, derived, index);
112
112
  }
113
113
  finally {
114
114
  derived.fill(0);
@@ -123,12 +123,12 @@ export async function walletAddress(code) {
123
123
  * engine expands from it — are wiped before it returns, on the failing path as well as the
124
124
  * good one. Neither one leaves.
125
125
  */
126
- export function addressFromDerived(glue, derived) {
126
+ export function addressFromDerived(glue, derived, index = BUILT_IN_WALLET_INDEX) {
127
127
  const [from, to] = DERIVED.walletRoot;
128
128
  const root = derived.slice(from, to);
129
129
  let seed = null;
130
130
  try {
131
- seed = glue.wallet_seed_for(root, BUILT_IN_WALLET_INDEX);
131
+ seed = glue.wallet_seed_for(root, index);
132
132
  return addressFromSeed(seed);
133
133
  }
134
134
  finally {
@@ -10,6 +10,7 @@
10
10
  // transaction paid — see the note on `sendTip` below, which is not what its name suggests.
11
11
  import { SuiJsonRpcClient } from "@mysten/sui/jsonRpc";
12
12
  import { walrus } from "@mysten/walrus";
13
+ import { toBase64Url } from "./bytes.js";
13
14
  import { epochClock } from "./expiry.js";
14
15
  // ⛔ The re-export below is what callers use; this local name is the same function,
15
16
  // imported so the reader above can call it without importing from itself.
@@ -112,8 +113,8 @@ export function createBlobProtocol(network, bodyBytes, onSent) {
112
113
  .catch((error) => fail("The upload relay refused the bytes", error));
113
114
  return {
114
115
  signers: certificate.signers,
115
- serialized_message_b64: Buffer.from(certificate.serializedMessage).toString("base64url"),
116
- signature_b64: Buffer.from(certificate.signature).toString("base64url"),
116
+ serialized_message_b64: toBase64Url(certificate.serializedMessage),
117
+ signature_b64: toBase64Url(certificate.signature),
117
118
  };
118
119
  },
119
120
  };
package/dist/walrus.d.ts CHANGED
@@ -28,22 +28,7 @@ export declare const RELAY_HOSTS: Readonly<Record<string, readonly string[]>>;
28
28
  export declare const SUI_RPC_HOSTS: Readonly<Record<string, readonly string[]>>;
29
29
  /** How long one host gets before the next is tried. A read that stalls is a read that failed. */
30
30
  export declare const READ_TIMEOUT_MS = 60000;
31
- /**
32
- * Point reads at somebody else's aggregator, or at a development stack.
33
- *
34
- * ⚠ It replaces the list rather than adding to it, and that is deliberate: a run should read from
35
- * where it was told to read, not from there AND the public hosts. Comma-separated for more
36
- * than one, tried in the order given.
37
- */
38
- export declare const AGGREGATOR_ENV_VAR = "NMTS_AGGREGATOR";
39
- /**
40
- * Push writes through somebody else's relay, or through a development stack.
41
- *
42
- * ⚠ ONE host, not a list. Unlike reads there is nothing to fail over to — see `RELAY_HOSTS`.
43
- */
44
- export declare const RELAY_ENV_VAR = "NMTS_RELAY";
45
- /** Ask a different Sui JSON-RPC node the shard-count question. */
46
- export declare const SUI_RPC_ENV_VAR = "NMTS_SUI_RPC";
31
+ export { AGGREGATOR_ENV_VAR, RELAY_ENV_VAR, SUI_RPC_ENV_VAR } from "./env-vars.ts";
47
32
  /** The relay this run writes through: the environment's if it named one, else the network's. */
48
33
  export declare function relayHost(network: string): string;
49
34
  /**
package/dist/walrus.js CHANGED
@@ -10,7 +10,9 @@
10
10
  // them: here, the browser build, and the standalone recovery tool. One copy going stale shows
11
11
  // up as "file not found", which reads as "the file is gone" — so the machine holds them level
12
12
  // rather than a person remembering to.
13
+ import { AGGREGATOR_ENV_VAR, RELAY_ENV_VAR, SUI_RPC_ENV_VAR } from "./env-vars.js";
13
14
  import { NmtsError } from "./errors.js";
15
+ import { host as runtime } from "./host.js";
14
16
  /** Curated Walrus aggregator (read) endpoints per network, preference order. */
15
17
  export const AGGREGATOR_HOSTS = {
16
18
  testnet: ["https://aggregator.walrus-testnet.walrus.space"],
@@ -50,34 +52,21 @@ export const SUI_RPC_HOSTS = {
50
52
  };
51
53
  /** How long one host gets before the next is tried. A read that stalls is a read that failed. */
52
54
  export const READ_TIMEOUT_MS = 60_000;
53
- /**
54
- * Point reads at somebody else's aggregator, or at a development stack.
55
- *
56
- * ⚠ It replaces the list rather than adding to it, and that is deliberate: a run should read from
57
- * where it was told to read, not from there AND the public hosts. Comma-separated for more
58
- * than one, tried in the order given.
59
- */
60
- export const AGGREGATOR_ENV_VAR = "NMTS_AGGREGATOR";
61
- /**
62
- * Push writes through somebody else's relay, or through a development stack.
63
- *
64
- * ⚠ ONE host, not a list. Unlike reads there is nothing to fail over to — see `RELAY_HOSTS`.
65
- */
66
- export const RELAY_ENV_VAR = "NMTS_RELAY";
67
- /** Ask a different Sui JSON-RPC node the shard-count question. */
68
- export const SUI_RPC_ENV_VAR = "NMTS_SUI_RPC";
55
+ // Re-exported so every caller still finds them here; the names themselves live in a module with no
56
+ // imports, because `nmts --help` prints them (`env-vars.ts`).
57
+ export { AGGREGATOR_ENV_VAR, RELAY_ENV_VAR, SUI_RPC_ENV_VAR } from "./env-vars.js";
69
58
  /** The relay this run writes through: the environment's if it named one, else the network's. */
70
59
  export function relayHost(network) {
71
- const named = process.env[RELAY_ENV_VAR]?.trim();
60
+ const named = runtime().env(RELAY_ENV_VAR)?.trim();
72
61
  if (named)
73
62
  return named;
74
- const host = RELAY_HOSTS[network]?.[0];
75
- if (host === undefined) {
63
+ const known = RELAY_HOSTS[network]?.[0];
64
+ if (known === undefined) {
76
65
  throw new NmtsError(`No upload relay is known for the ${network} storage network.`, {
77
66
  nextStep: `Name one in ${RELAY_ENV_VAR} to upload anyway.`,
78
67
  });
79
68
  }
80
- return host;
69
+ return known;
81
70
  }
82
71
  /**
83
72
  * Every Sui JSON-RPC node this run may ask, in order.
@@ -88,7 +77,7 @@ export function relayHost(network) {
88
77
  * they did not choose.
89
78
  */
90
79
  export function suiRpcHosts(network) {
91
- const named = process.env[SUI_RPC_ENV_VAR]?.trim();
80
+ const named = runtime().env(SUI_RPC_ENV_VAR)?.trim();
92
81
  if (named)
93
82
  return [named];
94
83
  const hosts = SUI_RPC_HOSTS[network];
@@ -104,7 +93,7 @@ export function suiRpcHost(network) {
104
93
  return suiRpcHosts(network)[0] ?? "";
105
94
  }
106
95
  function fromEnvironment() {
107
- const raw = process.env[AGGREGATOR_ENV_VAR];
96
+ const raw = runtime().env(AGGREGATOR_ENV_VAR);
108
97
  if (raw === undefined)
109
98
  return null;
110
99
  const hosts = raw.split(",").map((h) => h.trim()).filter((h) => h !== "");
@@ -27,7 +27,7 @@ is spent by moving credits, and running the same command the other way puts them
27
27
  The credits keep the expiry they already had. The move drains the soonest-expiring lots first, so
28
28
  sending 100 may arrive as several grants with different dates, and moving them never renews them.
29
29
 
30
- Four refusals, four different remedies, and none of them is a retry:
30
+ Four refusals, none of them worth retrying:
31
31
  `CREDIT_TRANSFER_OUTSIDE_FAMILY` (name an account of your own) · `CREDIT_TRANSFER_SELF` (that is
32
32
  the account sending) · `CREDIT_TRANSFER_INSUFFICIENT` (send less — the refusal carries what was
33
33
  needed and what the account can spend) · `CREDIT_TRANSFER_ZERO` (send some). Nothing is moved and
@@ -0,0 +1,14 @@
1
+ # nmts platform — the key pair a business signs with
2
+
3
+ Commands: platform
4
+ Tiers: platform=none · platform.keygen=medium
5
+
6
+ `platform keygen` makes an Ed25519 key pair and writes both halves to `nmts-business-key.json`, or
7
+ to `--out <file>`, with mode 0600 (Windows applies no file mode, and the command says so). It
8
+ refuses a name that already exists, whatever `--force` says, and prints only the public half.
9
+ `platform register` cannot register it: that needs a signed-in browser session at nmts.me, under
10
+ Settings › Developer › Platform, and the command prints that path and exits 2.
11
+
12
+ The private half is what signs every request a business makes and every delegation token it mints
13
+ for one of its users. Nothing can derive it again, and replacing the registered key invalidates every
14
+ token the old one signed.
@@ -13,4 +13,4 @@ English. One report per problem.
13
13
 
14
14
  In the default mode it prints what it would send and asks (or takes `--yes`); in an auto mode it
15
15
  sends after printing. Replies arrive in the thread: `support list`, `support show <code>`,
16
- `support reply <code> --message`. If the CLI itself cannot run, the second door is nmts@nmts.me.
16
+ `support reply <code> --message`. If the CLI itself cannot run, write to nmts@nmts.me.
@@ -1,4 +1,4 @@
1
- # nmts rm, restore, sweep, erase — the trash, and the one way past it
1
+ # nmts rm, restore, sweep, erase — the trash, and erasing for good
2
2
 
3
3
  Commands: rm, restore, sweep, erase
4
4
  Tiers: rm=low · restore=low · sweep=medium · erase=ultra-high · erase.release=ultra-high(release-storage)
@@ -1,12 +1,28 @@
1
1
  # nmts wallet — the wallet your NMTS key derives
2
2
 
3
3
  Commands: wallet
4
- Tiers: wallet=none · wallet.send=high(wallet) · wallet.swap=high(wallet) · wallet.donate=high(donate) · wallet.storage.reshape=high(wallet) · wallet.storage.give=high(wallet) · wallet.hall=none · wallet.hall.set=medium
4
+ Tiers: wallet=none · wallet.use=low · wallet.send=high(wallet) · wallet.swap=high(wallet) · wallet.donate=high(donate) · wallet.storage.reshape=high(wallet) · wallet.storage.give=high(wallet) · wallet.hall=none · wallet.hall.set=medium
5
5
 
6
6
  `wallet` shows the address and its SUI and WAL balances; `wallet address` derives the address
7
- offline (`--qr` draws it); `wallet activity` lists recent transactions, named only where the chain
8
- proves it; `wallet storage` lists storage resources (size × time) held outside any file. None of
9
- these signs.
7
+ offline (`--qr` draws it, `--index N` derives another of this key's wallets); `wallet activity`
8
+ lists recent transactions, named only where the chain proves it; `wallet storage` lists storage
9
+ resources (size × time) held outside any file. None of these signs.
10
+
11
+ ## One key, many wallets
12
+
13
+ Your NMTS key derives a wallet at every number from 0 upwards, and one of them pays. `wallet list`
14
+ walks them — it shows each number, its address and its balances, marks the one that pays, and stops
15
+ after twenty unused wallets in a row. `wallet use <number>` moves which one pays: the number is
16
+ kept inside your sealed file list, so the browser, this tool and every other device agree on it.
17
+ Numbers come from the key, so a wallet cannot be deleted, and a wallet you leave keeps whatever is
18
+ in it.
19
+
20
+ `put`, `push`, `extend`, `wallet send` and `wallet donate` all pay from that wallet. The same
21
+ number decides the rest: `wallet swap` swaps its coins, `wallet storage split|merge|transfer`
22
+ reshapes the resources it holds, and `wallet hall --name` signs as its address. Each takes
23
+ `--wallet <number>` to use a different one for that run only, without changing the setting.
24
+ ⛔ If the file list cannot be read, they refuse rather than falling back to wallet 0 — signing with
25
+ a wallet nobody chose is not a thing to guess at.
10
26
 
11
27
  ## Where SUI comes from, and why it is SUI
12
28
 
@@ -14,8 +30,8 @@ You can store without any coin here: credits pay the network for you (the weekly
14
30
  nmts.me, or credits somebody gave you), and `nmts balance` shows them. The wallet matters only when
15
31
  you pay the network yourself.
16
32
 
17
- The files live on Walrus, and Walrus is paid on the Sui chain in WAL, with gas in SUI. That is why
18
- this wallet holds SUI and WAL and nothing else — not a preference, the chain the storage runs on.
33
+ The files live on Walrus, and Walrus is paid on the Sui chain in WAL, with gas in SUI, so
34
+ this wallet holds SUI and WAL and nothing else.
19
35
  Fiat is not taken because taking it would make NMTS hold your money, which it never does.
20
36
 
21
37
  Getting SUI (as of 2026-09-06): buy it on an exchange that lists it (Binance and Upbit did on that
@@ -63,5 +79,7 @@ One more reads the gifts back, and one flag on it publishes a name:
63
79
  - `wallet hall` prints the gift hall of fame — the developer, then the ten largest senders as read
64
80
  from the public chain, with the rest of the list at nmts.me/hall; reading it signs nothing and
65
81
  needs no NMTS key. `--name <name>` (1 to 24 characters, no links, not an address) signs a
66
- short message with this account's wallet so the server shows that name beside the address, and
67
- `--remove` puts the entry back to a shortened address.
82
+ short message with the wallet this account pays from the wallet a gift left from so the
83
+ server shows that name beside its address, and `--remove` puts the entry back to a shortened
84
+ address. Reading which wallet that is opens the file list, so naming yourself needs an API key
85
+ where reading the hall needs none.