@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
@@ -0,0 +1,221 @@
1
+ // A WALLET-PAID upload with no terminal in it: one file sealed here, its storage bought by the
2
+ // wallet the NMTS key derives instead of by credits, the sealed bytes pushed, the file named in
3
+ // the account's sealed list.
4
+ //
5
+ // ⛔ WHY THIS IS NOT IN `commands/`. Paying for storage from somebody's own coins existed only as
6
+ // `nmts put --pay wallet`, and a command cannot be called from a library: it parses arguments,
7
+ // prints, asks a person and exits. Everything below takes its inputs as values and reports
8
+ // through a return value and thrown `NmtsError`s, which is what lets the SDK — and somebody's
9
+ // own server — pay for an upload from their own coins without inheriting a terminal.
10
+ //
11
+ // ⛔ IT PRICES BEFORE IT SIGNS, ALWAYS, in the order `extend` set: the parts are planned, the chain
12
+ // quotes each part in WAL and the relay's tip in SUI, the register transaction is dry-run for
13
+ // its fee, both balances are read — and only then is `onReview` told the numbers, a known
14
+ // shortfall refused, and `agree` given its say. A dry run stops at the review and never loads
15
+ // the signing module.
16
+ //
17
+ // ⛔ `agree` IS THE ONE GATE BETWEEN A PROGRAM AND SOMEBODY'S WALLET, and it is a parameter
18
+ // because who may say yes differs: the command holds the review against this machine's wallet
19
+ // agreement, and a library call IS the agreement, made by the caller who wrote the call.
20
+ // Everything before it is a read.
21
+ //
22
+ // ⛔ THE SERVER IS TOLD THE STORAGE IS THE PERSON'S BY SAYING NOTHING ELSE. `POST /v1/items` forces
23
+ // treasury ownership only on a part that names a certified reservation; a wallet-paid part names
24
+ // none and carries the blob object and the end epoch instead, exactly as the browser's own
25
+ // wallet-paid commit does (`upload-steps.ts`).
26
+ //
27
+ // ⛔ A HELD RESOURCE IS OFFERED, NEVER CHOSEN FOR THE CALLER — the owner's rule for storage
28
+ // resources. Without `storage` this buys new storage and the review counts the free resources
29
+ // the wallet holds; with it the review says in bytes what is cut free or bound with the file.
30
+ //
31
+ // ⚠ NO SPENDING LEDGER AND NO GIFT LIVE HERE. Both are the command's: the ledger counts what one
32
+ // machine signed under one agreement, and a standing gift is a choice a person made in their own
33
+ // account. A library that kept either would be deciding for a caller who never asked it to.
34
+ import { DERIVED } from "./crypto.js";
35
+ import { NmtsError } from "./errors.js";
36
+ import { setTrashed } from "./item-trash.js";
37
+ import { addEntry } from "./manifest-write.js";
38
+ import { sealedLenFor } from "./seal.js";
39
+ import { statusOf } from "./shared/lib/storage-control/plan.js";
40
+ import { createUploadApi } from "./upload-api.js";
41
+ import { partKeysOf, uploadFile } from "./upload-file.js";
42
+ import { CREDIT_BYTES, planAndPrice } from "./upload-price.js";
43
+ import { clearItemRecord, clearReservation } from "./upload-store.js";
44
+ import { walletRail } from "./upload-wallet.js";
45
+ import { chooseUploadEpochs, daysOf, parseStorageAsk, pickResource, uploadBudget, uploadShortfallNextStep, } from "./upload-wallet-plan.js";
46
+ import { walletAddress } from "./wallet.js";
47
+ import { createBlobProtocol } from "./walrus-write.js";
48
+ /**
49
+ * Price, agree, sign, upload and record ONE file, paid from the wallet the NMTS key derives.
50
+ *
51
+ * ⛔ THE SIGNING MODULE IS LOADED ONLY AFTER `agree` HAS RETURNED. A dry run and a refusal never
52
+ * bring the code that can spend into memory.
53
+ */
54
+ export async function walletPut(ctx, file, seams = {}) {
55
+ const { plan, sealedBytes, sealFor } = planAndPrice(file.source.size, ctx.partSize, ctx.rule);
56
+ const sealedLens = plan.map((range) => sealedLenFor(sealFor(range)));
57
+ const storageAsk = parseStorageAsk(seams.storage);
58
+ if (storageAsk !== null && plan.length > 1) {
59
+ throw new NmtsError(`A held storage resource holds one blob, and this file is ${plan.length} parts.`, {
60
+ exitCode: 4,
61
+ nextStep: `Nothing was signed. Leave the storage resource off, or raise the part size so the file is one part.`,
62
+ });
63
+ }
64
+ const protocol = (seams.protocol ?? createBlobProtocol)(ctx.network, sealedBytes, (sent, total) => seams.onProgress?.(sent, total));
65
+ const reads = await (seams.readChain ?? defaultReads)(ctx.network, protocol.relayUrl);
66
+ const window = await reads.readWindow();
67
+ if (window === null) {
68
+ throw new NmtsError(`The ${ctx.network} storage network could not be read.`, {
69
+ exitCode: 1,
70
+ 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 will not spend against a guess.`,
71
+ });
72
+ }
73
+ const epochs = chooseUploadEpochs(seams.epochs, window);
74
+ const endEpoch = window.clock.current + epochs;
75
+ const quotes = await reads.quoteParts(sealedLens, epochs);
76
+ // ⛔ THE ADDRESS THAT IS PRICED IS THE ADDRESS THAT SIGNS — the same number the rail carries.
77
+ const address = await walletAddress(ctx.code, ctx.wallet);
78
+ let storage = { kind: "buy" };
79
+ let heldResources = null;
80
+ if (storageAsk !== null) {
81
+ const firstQuote = quotes[0];
82
+ const firstLen = sealedLens[0];
83
+ if (firstQuote === undefined || firstLen === undefined)
84
+ throw new NmtsError("unreachable: a plan with no part");
85
+ let resources;
86
+ try {
87
+ resources = await reads.readStorage(address);
88
+ }
89
+ catch (error) {
90
+ throw new NmtsError("The storage resources this wallet holds could not be read.", {
91
+ exitCode: 1,
92
+ nextStep: `Nothing was signed. That is not the same as holding none. Cause: ${error instanceof Error ? error.message : String(error)}`,
93
+ });
94
+ }
95
+ const encoded = await reads.encodedLength(address, firstLen);
96
+ storage = pickResource(storageAsk, resources, encoded, window.clock.current, endEpoch, firstQuote.writeFrost);
97
+ }
98
+ else {
99
+ // ⚠ A read that fails is null, not zero: this number only offers a choice, and losing it must
100
+ // not lose the upload.
101
+ heldResources = await reads.readStorage(address).then((rows) => rows.filter((r) => statusOf(r, window.clock.current) === "usable").length, () => null);
102
+ }
103
+ const [purse, feeMist] = await Promise.all([
104
+ reads.readWallet(address),
105
+ reads.estimateRegisterGas({
106
+ sender: address,
107
+ sealedLen: Math.max(...sealedLens),
108
+ epochs,
109
+ storage: storage.kind === "buy"
110
+ ? storage
111
+ : { kind: "reuse", objectId: storage.objectId, cutToBytes: storage.cutToBytes, writeFrost: storage.writeFrost },
112
+ }),
113
+ ]);
114
+ const budget = uploadBudget({ address, purse, feeMist, quotes, storage });
115
+ const review = {
116
+ name: file.name,
117
+ bytes: file.source.size,
118
+ sealedBytes,
119
+ parts: plan.length,
120
+ epochs,
121
+ days: daysOf(window, epochs),
122
+ endEpoch,
123
+ tipMist: quotes.reduce((sum, q) => sum + q.tipMist, 0n),
124
+ storage,
125
+ heldResources,
126
+ budget,
127
+ };
128
+ seams.onReview?.(review);
129
+ if (seams.dryRun === true)
130
+ return { kind: "review", review };
131
+ // ⛔ A WALLET KNOWN TO BE SHORT IS REFUSED BEFORE THE AGREEMENT IS ASKED FOR (`extend-budget.ts`).
132
+ if (budget.shortfall !== null) {
133
+ throw new NmtsError(budget.shortfall, { exitCode: 4, nextStep: uploadShortfallNextStep(budget) });
134
+ }
135
+ seams.agree?.(review);
136
+ const sign = seams.sign ?? (await signers());
137
+ const onSpend = seams.onSpend ?? (() => undefined);
138
+ const derived = ctx.crypt.kdf_derive(ctx.crypt.account_code_parse(ctx.code));
139
+ const dataKey = derived.slice(DERIVED.dataKey[0], DERIVED.dataKey[1]);
140
+ derived.fill(0);
141
+ let result;
142
+ try {
143
+ result = await uploadFile({
144
+ api: seams.api ?? createUploadApi(ctx.server, ctx.apiKey),
145
+ protocol,
146
+ crypt: ctx.crypt,
147
+ dataKey,
148
+ source: file.source,
149
+ name: file.name,
150
+ parentId: file.parentId,
151
+ destination: file.destination,
152
+ relayUrl: protocol.relayUrl,
153
+ epochs,
154
+ currentEpoch: window.clock.current,
155
+ partSize: ctx.partSize,
156
+ padding: { rule: ctx.rule, unitBytes: CREDIT_BYTES },
157
+ ...(seams.onStep === undefined ? {} : { onStep: seams.onStep }),
158
+ buy: walletRail({
159
+ network: ctx.network,
160
+ code: ctx.code,
161
+ wallet: ctx.wallet,
162
+ relayUrl: protocol.relayUrl,
163
+ epochs,
164
+ storage,
165
+ quotes,
166
+ feeMist,
167
+ signRegister: sign.register,
168
+ signCertify: sign.certify,
169
+ onSpend,
170
+ }),
171
+ });
172
+ }
173
+ finally {
174
+ dataKey.fill(0);
175
+ }
176
+ const now = Date.now();
177
+ // ⛔ FROM `result.entry`, NOT FROM THIS CALL. The key that opens the stored bytes is the key they
178
+ // were sealed with, which on a resume belongs to the call that sealed them.
179
+ const added = await addEntry({
180
+ server: ctx.server,
181
+ apiKey: ctx.apiKey,
182
+ code: ctx.code,
183
+ accountId: ctx.accountId,
184
+ ...(ctx.onCollision !== undefined ? { onCollision: ctx.onCollision } : {}),
185
+ entry: {
186
+ id: result.itemId,
187
+ parentId: file.parentId,
188
+ kind: 1,
189
+ name: result.entry.name,
190
+ size: result.entry.plaintextLen,
191
+ createdAt: now,
192
+ updatedAt: now,
193
+ dekWrapped: result.entry.dekWrapped,
194
+ contentHashCt: result.entry.contentHashCt,
195
+ },
196
+ });
197
+ // ⛔ ONLY NOW, AND EVERY PART — the same order the credit rail keeps and for the same reason: a
198
+ // paid-for file the list does not name is invisible, and the records are what let a second
199
+ // call finish the job without signing again.
200
+ clearItemRecord(result.fileKey);
201
+ for (const record of partKeysOf(result.fileKey, result.parts))
202
+ clearReservation(record);
203
+ if (added.replaced)
204
+ await setTrashed(ctx.server, ctx.apiKey, added.replaced.id, true);
205
+ return {
206
+ kind: "uploaded",
207
+ review,
208
+ itemId: result.itemId,
209
+ savedAs: added.name,
210
+ replaced: added.replaced?.id ?? null,
211
+ fileListVersion: added.seq,
212
+ resumed: result.resumed,
213
+ };
214
+ }
215
+ async function defaultReads(network, relayUrl) {
216
+ return (await import("./upload-wallet-chain.js")).walletUploadReads(network, relayUrl);
217
+ }
218
+ async function signers() {
219
+ const signing = await import("./wallet-sign.js");
220
+ return { register: signing.signBlobRegister, certify: signing.signBlobCertify };
221
+ }
@@ -7,6 +7,9 @@ export interface WalletRailContext {
7
7
  network: Network;
8
8
  /** ⛔ The NMTS key. Held for the signatures and never written anywhere. */
9
9
  code: string;
10
+ /** Which of this key's wallets pays — the account's own number, resolved before anything was
11
+ * priced (`wallet-pay-index.ts`), so the address in the review is the address that signs. */
12
+ wallet: number;
10
13
  relayUrl: string;
11
14
  epochs: number;
12
15
  /** Where the storage comes from. A held resource serves one blob, so it applies to a one-part file. */
@@ -108,6 +108,7 @@ async function buyAndPushPartWithWallet(ctx, input) {
108
108
  registered = await ctx.signRegister({
109
109
  network: ctx.network,
110
110
  code: ctx.code,
111
+ wallet: ctx.wallet,
111
112
  relayUrl: input.relayUrl,
112
113
  epochs: ctx.epochs,
113
114
  storage: ctx.storage.kind === "buy" ? { kind: "buy" } : ctx.storage,
@@ -173,6 +174,7 @@ async function certify(ctx, input, key, record, certificate) {
173
174
  digest = await ctx.signCertify({
174
175
  network: ctx.network,
175
176
  code: ctx.code,
177
+ wallet: ctx.wallet,
176
178
  relayUrl: record.relayUrl,
177
179
  blobId: record.blobId,
178
180
  blobObjectId: record.blobObjectId,
@@ -0,0 +1,4 @@
1
+ import type { Network } from "./network.ts";
2
+ /** How long the two questions get together. A read that stalls is a read that failed. */
3
+ export declare const HISTORY_TIMEOUT_MS = 20000;
4
+ export declare function hasHistory(network: Network, address: string): Promise<boolean>;
@@ -0,0 +1,21 @@
1
+ // The live read behind `nmts wallet list`'s walk: has this address ever been in a transaction?
2
+ //
3
+ // ⛔ READ ONLY, and one question in each direction. The RPC filter has no OR, so "from" and "to"
4
+ // are asked separately — a wallet that only ever RECEIVED is in use too, and a walk that asked
5
+ // about balances alone would report a wallet somebody emptied as one that does not exist.
6
+ //
7
+ // ⚠ ONE transaction is enough to answer, so one is what is asked for. This runs once per wallet
8
+ // the walk looks at, which is why it asks for as little as the chain will sell.
9
+ import { SuiJsonRpcClient } from "@mysten/sui/jsonRpc";
10
+ import { suiRpcTransport } from "./sui-rpc.js";
11
+ /** How long the two questions get together. A read that stalls is a read that failed. */
12
+ export const HISTORY_TIMEOUT_MS = 20_000;
13
+ export async function hasHistory(network, address) {
14
+ const client = new SuiJsonRpcClient({ network, transport: suiRpcTransport(network) });
15
+ const signal = AbortSignal.timeout(HISTORY_TIMEOUT_MS);
16
+ const sent = await client.queryTransactionBlocks({ filter: { FromAddress: address }, limit: 1, signal });
17
+ if (sent.data.length > 0)
18
+ return true;
19
+ const received = await client.queryTransactionBlocks({ filter: { ToAddress: address }, limit: 1, signal });
20
+ return received.data.length > 0;
21
+ }
@@ -0,0 +1,21 @@
1
+ export interface PayingWalletInput {
2
+ server?: string | undefined;
3
+ network?: string | undefined;
4
+ /** `--wallet N` — this run only, and it never writes the account's choice. */
5
+ wallet?: string | undefined;
6
+ /**
7
+ * ⚠ A SEAM, NOT AN OPTION: how the account's number is read. No flag reaches it. It exists
8
+ * because the alternative is a test that signs in and reads a live file list to check that a
9
+ * review was printed.
10
+ */
11
+ readActiveWallet?: () => Promise<number>;
12
+ }
13
+ /**
14
+ * A wallet number as a person typed it.
15
+ *
16
+ * ⛔ REFUSED, NEVER ROUNDED. `--wallet 1.5` and `--wallet -1` are command lines to correct; taking
17
+ * either of them to a neighbouring wallet would spend from an address nobody named.
18
+ */
19
+ export declare function walletIndexOf(raw: string): number;
20
+ /** Which wallet this run pays from: the flag if it was given, otherwise the account's own number. */
21
+ export declare function payingWalletIndex(input: PayingWalletInput): Promise<number>;
@@ -0,0 +1,68 @@
1
+ // WHICH WALLET PAYS — the number this account keeps in its sealed file list, and the one flag that
2
+ // overrides it for a single run.
3
+ //
4
+ // ⛔ WHY IT IS NOT A CONSTANT ANY MORE (2026-09-16). One NMTS key derives a wallet at
5
+ // every index (NCF-3 §1.3) and the account chooses which of them pays, so that a phone, a
6
+ // laptop and this tool all spend from the same address. That choice lives where no server can
7
+ // read it: inside the sealed list, beside the padding rule.
8
+ //
9
+ // ⛔ AND IT REFUSES RATHER THAN GUESSING. If the list cannot be read, this tool does not fall back
10
+ // to the first wallet: that would sign with a wallet the person may not have funded, print a
11
+ // review naming an address nobody chose, and — with `--yes` — spend from it. "I do not know
12
+ // which wallet should pay" is an answer; wallet 0 is not.
13
+ //
14
+ // ⚠ THE NUMBER IS RESOLVED BEFORE ANYTHING IS PRICED. Every command that spends prints a review
15
+ // with the paying address in it, so the address that is read, priced and shown has to be the
16
+ // address that signs. Asking for the number at signing time would let those two differ.
17
+ import { NmtsError } from "./errors.js";
18
+ import { readFileList } from "./manifest.js";
19
+ import { BINARY_NAME } from "./product.js";
20
+ import { openSession } from "./session.js";
21
+ import { activeWalletOf, WALLET_INDEX_LIMIT } from "./shared/lib/drive/manifest-settings.js";
22
+ /**
23
+ * A wallet number as a person typed it.
24
+ *
25
+ * ⛔ REFUSED, NEVER ROUNDED. `--wallet 1.5` and `--wallet -1` are command lines to correct; taking
26
+ * either of them to a neighbouring wallet would spend from an address nobody named.
27
+ */
28
+ export function walletIndexOf(raw) {
29
+ const text = raw.trim();
30
+ const value = Number(text);
31
+ if (!/^[0-9]+$/u.test(text) || !Number.isSafeInteger(value) || value >= WALLET_INDEX_LIMIT) {
32
+ throw new NmtsError(`A wallet number is a whole number from 0 to ${WALLET_INDEX_LIMIT - 1}, not "${raw}".`, {
33
+ exitCode: 2,
34
+ nextStep: `\`${BINARY_NAME} wallet list\` shows this key's wallets and their numbers.`,
35
+ });
36
+ }
37
+ return value;
38
+ }
39
+ /** Which wallet this run pays from: the flag if it was given, otherwise the account's own number. */
40
+ export async function payingWalletIndex(input) {
41
+ const said = input.wallet;
42
+ if (said !== undefined && said !== "")
43
+ return walletIndexOf(said);
44
+ const read = input.readActiveWallet ?? (() => fromSealedList(input));
45
+ try {
46
+ return await read();
47
+ }
48
+ catch (error) {
49
+ // ⛔ A refusal this tool already worded (no API key, a locked code) is passed on as it stands —
50
+ // it names the thing to fix, and wrapping it would bury that under a second sentence.
51
+ if (error instanceof NmtsError)
52
+ throw error;
53
+ throw new NmtsError(`Which wallet should pay is written in this account's file list, and it could not be read: ` +
54
+ `${error instanceof Error ? error.message : String(error)}`, {
55
+ exitCode: 1,
56
+ nextStep: `Nothing was signed. Try again when the list can be read, or name the wallet for this ` +
57
+ `one run with \`--wallet N\` — \`${BINARY_NAME} wallet list\` shows the numbers.`,
58
+ });
59
+ }
60
+ }
61
+ /** The account's own number, out of the sealed file list. One request, and no flag reaches it. */
62
+ async function fromSealedList(input) {
63
+ const session = await openSession({ server: input.server, network: input.network });
64
+ const list = await readFileList(session.server, session.apiKey, session.code, session.accountId);
65
+ // An account that has never written a setting pays from the first wallet — that is a READ
66
+ // answer, not a fallback: the list opened, and it said nothing about a wallet.
67
+ return activeWalletOf(list.manifest?.settings);
68
+ }
@@ -0,0 +1,57 @@
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
+ message: string;
12
+ }) => Promise<string>;
13
+ /** The seam `commands/wallet-send.ts` signs through. Returns the transaction digest. */
14
+ export type SignTransfer = (input: {
15
+ network: string;
16
+ /** ⛔ The NMTS key. It never leaves this machine: it derives the wallet and nothing else. */
17
+ code: string;
18
+ /** Which of this key's wallets pays — the account's own number (`wallet-pay-index.ts`). */
19
+ wallet: number;
20
+ shape: TransferShape;
21
+ }) => Promise<string>;
22
+ /** The seam `commands/wallet-swap.ts` signs through. Returns the transaction digest. */
23
+ export type SignSwap = (input: {
24
+ network: Network;
25
+ /** ⛔ The NMTS key. It never leaves this machine: it derives the wallet and nothing else. */
26
+ code: string;
27
+ shape: SwapShape;
28
+ }) => Promise<string>;
29
+ /** The seam the wallet rail registers through. */
30
+ export type SignBlobRegister = (input: {
31
+ network: Network;
32
+ code: string;
33
+ wallet: number;
34
+ relayUrl: string;
35
+ } & RegisterShape) => Promise<{
36
+ digest: string;
37
+ blobObjectId: string;
38
+ endEpoch: number;
39
+ }>;
40
+ /** The seam the wallet rail certifies through. Returns the transaction digest. */
41
+ export type SignBlobCertify = (input: {
42
+ network: Network;
43
+ code: string;
44
+ /** The same wallet that registered the part — it is the one that owns the blob object. */
45
+ wallet: number;
46
+ relayUrl: string;
47
+ blobId: string;
48
+ blobObjectId: string;
49
+ certificate: Certificate;
50
+ }) => Promise<string>;
51
+ /** How a storage-resource operation is signed; the shape carries what the review priced. */
52
+ export type SignStorageOp = (input: {
53
+ network: Network;
54
+ code: string;
55
+ shape: StorageOpShape;
56
+ walrusPackageId: string;
57
+ }) => 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,
@@ -190,7 +190,9 @@ export const signTransfer = async ({ network, code, shape }) => {
190
190
  * signed text from ever being read as a transaction this wallet authorised.
191
191
  */
192
192
  export const signMessage = async ({ code, message }) => {
193
- const keypair = await keypairFor(code);
193
+ // THE FIRST WALLET SIGNS THE NAME — the address that stands in the hall is that wallet's, and
194
+ // the account's choice of paying wallet does not reach this command yet.
195
+ const keypair = await keypairFor(code, BUILT_IN_WALLET_INDEX);
194
196
  const { signature } = await keypair.signPersonalMessage(new TextEncoder().encode(message));
195
197
  return signature;
196
198
  };
@@ -207,7 +209,10 @@ export const signMessage = async ({ code, message }) => {
207
209
  */
208
210
  export const signSwap = async ({ network, code, shape }) => {
209
211
  const client = walrusClient(network);
210
- const keypair = await keypairFor(code);
212
+ // SWAPS FROM THE FIRST WALLET — the coins being swapped are that wallet's, and the account's
213
+ // choice of paying wallet does not reach this command yet. The day it does, this line and the
214
+ // review above it carry the same number.
215
+ const keypair = await keypairFor(code, BUILT_IN_WALLET_INDEX);
211
216
  const tx = swapTransaction({ ...shape, network, sender: keypair.toSuiAddress() });
212
217
  const result = await client.signAndExecuteTransaction({
213
218
  transaction: tx,
@@ -247,9 +252,9 @@ function refusedBecause(result) {
247
252
  * ⚠ A FAILURE HERE IS NOT PROOF THAT NOTHING HAPPENED — the same words as the extension above.
248
253
  * The caller keeps its record and re-reads it rather than registering again.
249
254
  */
250
- export const signBlobRegister = async ({ network, code, relayUrl, ...shape }) => {
255
+ export const signBlobRegister = async ({ network, code, wallet, relayUrl, ...shape }) => {
251
256
  const client = payingClient(network, relayUrl);
252
- const keypair = await keypairFor(code);
257
+ const keypair = await keypairFor(code, wallet);
253
258
  const tx = await registerTransaction(client, network, { ...shape, sender: keypair.toSuiAddress() });
254
259
  const result = await client.signAndExecuteTransaction({
255
260
  transaction: tx,
@@ -270,9 +275,9 @@ export const signBlobRegister = async ({ network, code, relayUrl, ...shape }) =>
270
275
  return { digest: result.digest, ...blob };
271
276
  };
272
277
  /** 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 }) => {
278
+ export const signBlobCertify = async ({ network, code, wallet, relayUrl, ...shape }) => {
274
279
  const client = payingClient(network, relayUrl);
275
- const keypair = await keypairFor(code);
280
+ const keypair = await keypairFor(code, wallet);
276
281
  const tx = certifyTransaction(client, shape);
277
282
  tx.setSender(keypair.toSuiAddress());
278
283
  const result = await client.signAndExecuteTransaction({
@@ -300,7 +305,9 @@ export const signBlobCertify = async ({ network, code, relayUrl, ...shape }) =>
300
305
  */
301
306
  export const signStorageOp = async ({ network, code, shape, walrusPackageId }) => {
302
307
  const client = walrusClient(network);
303
- const keypair = await keypairFor(code);
308
+ // RESHAPES THE FIRST WALLET'S STORAGE — that wallet holds the resource; the paying-wallet choice
309
+ // does not reach this command yet.
310
+ const keypair = await keypairFor(code, BUILT_IN_WALLET_INDEX);
304
311
  const tx = storageOpTransaction(shape, { walrusPackageId, sender: keypair.toSuiAddress() });
305
312
  const result = await client.signAndExecuteTransaction({ transaction: tx, signer: keypair, options: { showEffects: true } });
306
313
  const effects = result.effects;