@needmoretruth/nmts-cli 0.34.3 → 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.
- package/CHANGELOG.md +24 -0
- package/README.ko.md +15 -15
- package/README.md +1 -1
- package/dist/api-advice.js +4 -0
- package/dist/arg-options.d.ts +167 -0
- package/dist/arg-options.js +10 -0
- package/dist/args.d.ts +2 -157
- package/dist/args.js +2 -0
- package/dist/artifact-about.d.ts +1 -1
- package/dist/commands/extend.d.ts +2 -0
- package/dist/commands/extend.js +8 -1
- package/dist/commands/push-wallet.js +6 -0
- package/dist/commands/push.d.ts +2 -0
- package/dist/commands/put-payer.d.ts +8 -0
- package/dist/commands/put-payer.js +41 -0
- package/dist/commands/put-wallet.d.ts +9 -3
- package/dist/commands/put-wallet.js +105 -185
- package/dist/commands/put.d.ts +5 -7
- package/dist/commands/put.js +3 -26
- package/dist/commands/wallet-donate.d.ts +3 -0
- package/dist/commands/wallet-donate.js +6 -2
- package/dist/commands/wallet-list.d.ts +18 -0
- package/dist/commands/wallet-list.js +116 -0
- package/dist/commands/wallet-send.d.ts +4 -0
- package/dist/commands/wallet-send.js +6 -2
- package/dist/commands/wallet-use.d.ts +7 -0
- package/dist/commands/wallet-use.js +55 -0
- package/dist/commands/wallet.d.ts +4 -0
- package/dist/commands/wallet.js +20 -8
- package/dist/extend-plan.d.ts +2 -0
- package/dist/help.js +3 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +14 -1
- package/dist/main.js +4 -4
- package/dist/product.d.ts +1 -1
- package/dist/product.js +1 -1
- package/dist/risk.d.ts +4 -0
- package/dist/risk.js +6 -0
- package/dist/shared/lib/drive/manifest-ops.d.ts +8 -30
- package/dist/shared/lib/drive/manifest-ops.js +7 -39
- package/dist/shared/lib/drive/manifest-settings-patch.d.ts +52 -0
- package/dist/shared/lib/drive/manifest-settings-patch.js +110 -0
- package/dist/shared/lib/drive/manifest-settings.d.ts +55 -4
- package/dist/shared/lib/drive/manifest-settings.js +66 -28
- package/dist/shared/lib/wallet/discover.d.ts +54 -0
- package/dist/shared/lib/wallet/discover.js +66 -0
- package/dist/standing-tip.d.ts +3 -0
- package/dist/standing-tip.js +1 -0
- package/dist/upload-wallet-put.d.ts +118 -0
- package/dist/upload-wallet-put.js +221 -0
- package/dist/upload-wallet.d.ts +3 -0
- package/dist/upload-wallet.js +2 -0
- package/dist/wallet-list-chain.d.ts +4 -0
- package/dist/wallet-list-chain.js +21 -0
- package/dist/wallet-pay-index.d.ts +21 -0
- package/dist/wallet-pay-index.js +68 -0
- package/dist/wallet-sign-seams.d.ts +57 -0
- package/dist/wallet-sign-seams.js +16 -0
- package/dist/wallet-sign.d.ts +3 -53
- package/dist/wallet-sign.js +22 -15
- package/dist/wallet.d.ts +13 -13
- package/dist/wallet.js +15 -15
- package/docs/commands/wallet.md +18 -4
- 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
|
+
}
|
package/dist/upload-wallet.d.ts
CHANGED
|
@@ -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. */
|
package/dist/upload-wallet.js
CHANGED
|
@@ -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 {};
|
package/dist/wallet-sign.d.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import type { SignExtension } from "./extend-plan.ts";
|
|
2
|
-
import {
|
|
3
|
-
|
|
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
|
*
|
package/dist/wallet-sign.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|