@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.
- package/AGENTS.md +11 -12
- package/CHANGELOG.md +43 -0
- package/README.ko.md +33 -35
- package/README.md +21 -22
- package/dist/account.js +2 -1
- package/dist/arg-options.d.ts +169 -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/autonomy.d.ts +3 -10
- package/dist/autonomy.js +15 -17
- package/dist/bytes.d.ts +15 -0
- package/dist/bytes.js +63 -0
- package/dist/collision.d.ts +5 -5
- package/dist/collision.js +19 -22
- package/dist/commands/delete-account.js +1 -1
- package/dist/commands/erase.js +1 -1
- package/dist/commands/extend.d.ts +2 -0
- package/dist/commands/extend.js +8 -1
- package/dist/commands/get.js +1 -1
- package/dist/commands/listfile.js +1 -1
- package/dist/commands/mcp.js +5 -1
- package/dist/commands/mode.js +2 -2
- package/dist/commands/on-collision.d.ts +1 -1
- package/dist/commands/on-collision.js +3 -3
- package/dist/commands/platform.d.ts +9 -0
- package/dist/commands/platform.js +124 -0
- package/dist/commands/pull.js +1 -1
- package/dist/commands/push-wallet.js +8 -1
- package/dist/commands/push.d.ts +2 -0
- package/dist/commands/push.js +6 -4
- 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 +108 -186
- package/dist/commands/put.d.ts +5 -7
- package/dist/commands/put.js +11 -31
- package/dist/commands/receive.js +1 -1
- package/dist/commands/settings.js +1 -1
- package/dist/commands/support.js +3 -3
- package/dist/commands/unlock.js +2 -2
- package/dist/commands/wallet-donate.d.ts +3 -0
- package/dist/commands/wallet-donate.js +6 -2
- package/dist/commands/wallet-hall.d.ts +4 -1
- package/dist/commands/wallet-hall.js +16 -8
- 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-storage-ops.d.ts +4 -1
- package/dist/commands/wallet-storage-ops.js +7 -2
- package/dist/commands/wallet-swap.d.ts +3 -0
- package/dist/commands/wallet-swap.js +8 -4
- 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/crypto.d.ts +8 -5
- package/dist/crypto.js +15 -61
- package/dist/download-sink-node.d.ts +49 -0
- package/dist/download-sink-node.js +204 -0
- package/dist/download-sink.d.ts +0 -48
- package/dist/download-sink.js +6 -199
- package/dist/download.js +5 -4
- package/dist/engine-node.d.ts +19 -0
- package/dist/engine-node.js +77 -0
- package/dist/env-vars.d.ts +20 -0
- package/dist/env-vars.js +27 -0
- package/dist/extend-plan.d.ts +2 -0
- package/dist/gate.js +1 -1
- package/dist/help.js +12 -5
- package/dist/host-contract.d.ts +7 -0
- package/dist/host-contract.js +95 -0
- package/dist/host-node.d.ts +27 -0
- package/dist/host-node.js +192 -0
- package/dist/host.d.ts +99 -0
- package/dist/host.js +59 -0
- package/dist/index.d.ts +6 -40
- package/dist/index.js +20 -32
- package/dist/main.js +11 -10
- package/dist/manifest-chunk-cache.d.ts +3 -3
- package/dist/manifest-chunk-cache.js +39 -44
- package/dist/manifest-chunk-flow.js +15 -14
- package/dist/manifest-write.js +1 -1
- package/dist/manifest.d.ts +2 -2
- package/dist/manifest.js +37 -34
- package/dist/mcp-tools/tiers.js +1 -1
- package/dist/network.d.ts +1 -1
- package/dist/network.js +6 -2
- package/dist/platform-sign.d.ts +143 -0
- package/dist/platform-sign.js +220 -0
- package/dist/portable.d.ts +59 -0
- package/dist/portable.js +82 -0
- package/dist/product.d.ts +1 -1
- package/dist/product.js +1 -1
- package/dist/progress-node.d.ts +2 -0
- package/dist/progress-node.js +14 -0
- package/dist/progress.d.ts +0 -1
- package/dist/progress.js +4 -8
- package/dist/redact.js +3 -2
- package/dist/registration.js +5 -2
- package/dist/risk.d.ts +11 -0
- package/dist/risk.js +16 -0
- package/dist/run-log.d.ts +4 -4
- package/dist/run-log.js +29 -36
- package/dist/seal.js +5 -4
- package/dist/server.d.ts +1 -1
- package/dist/server.js +6 -2
- package/dist/setup-questions.js +2 -2
- 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-file-node.d.ts +3 -0
- package/dist/upload-file-node.js +38 -0
- package/dist/upload-file.d.ts +2 -2
- package/dist/upload-file.js +15 -45
- package/dist/upload-price-node.d.ts +8 -0
- package/dist/upload-price-node.js +37 -0
- package/dist/upload-price.d.ts +0 -8
- package/dist/upload-price.js +4 -33
- package/dist/upload-steps.js +3 -3
- package/dist/upload-store.d.ts +14 -14
- package/dist/upload-store.js +68 -75
- package/dist/upload-wallet-chain.js +3 -2
- 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 +18 -15
- package/dist/upload.js +16 -15
- 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 +69 -0
- package/dist/wallet-sign-seams.js +16 -0
- package/dist/wallet-sign.d.ts +3 -53
- package/dist/wallet-sign.js +22 -18
- package/dist/wallet.d.ts +13 -13
- package/dist/wallet.js +15 -15
- package/dist/walrus-write.js +3 -2
- package/dist/walrus.d.ts +1 -16
- package/dist/walrus.js +11 -22
- package/docs/commands/credits.md +1 -1
- package/docs/commands/platform.md +14 -0
- package/docs/commands/support.md +1 -1
- package/docs/commands/trash.md +1 -1
- package/docs/commands/wallet.md +26 -8
- package/package.json +13 -2
|
@@ -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
|
+
await clearItemRecord(result.fileKey);
|
|
201
|
+
for (const record of partKeysOf(result.fileKey, result.parts))
|
|
202
|
+
await 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
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// ⛔ THE SIGNATURES ARE A SEAM. `wallet-sign.ts` is loaded by the command, after the agreement,
|
|
19
19
|
// and handed in; the tests hand in recorders and prove that `--dry-run` and a shortfall never
|
|
20
20
|
// reach them.
|
|
21
|
+
import { fromBase64Url, toBase64Url } from "./bytes.js";
|
|
21
22
|
import { NmtsError } from "./errors.js";
|
|
22
23
|
import { readReservationBytes, readReservationRecord, writeReservation } from "./upload-store.js";
|
|
23
24
|
import { UploadError } from "./upload-wire.js";
|
|
@@ -30,14 +31,14 @@ export function walletRail(ctx) {
|
|
|
30
31
|
}
|
|
31
32
|
async function buyAndPushPartWithWallet(ctx, input) {
|
|
32
33
|
const { protocol, key, sealed, onStep } = input;
|
|
33
|
-
const existing = readReservationRecord(key);
|
|
34
|
+
const existing = await readReservationRecord(key);
|
|
34
35
|
if (existing !== null && existing.paidFrom !== "wallet") {
|
|
35
36
|
throw new UploadError({
|
|
36
37
|
phase: "reserve",
|
|
37
38
|
message: "This upload was started with credits paying, and this run would pay from the wallet.",
|
|
38
39
|
paid: existing.ledgerId !== undefined,
|
|
39
|
-
nextStep: "Run it again without --pay wallet to finish it, or
|
|
40
|
-
"
|
|
40
|
+
nextStep: "Run it again without --pay wallet to finish it, or clear the unfinished upload records " +
|
|
41
|
+
"to start over. Nothing was sent.",
|
|
41
42
|
});
|
|
42
43
|
}
|
|
43
44
|
if (existing !== null && (existing.partIndex !== input.part.index || existing.partTotal !== input.part.total)) {
|
|
@@ -46,8 +47,8 @@ async function buyAndPushPartWithWallet(ctx, input) {
|
|
|
46
47
|
message: `This upload was started as part ${existing.partIndex + 1} of ${existing.partTotal} and this ` +
|
|
47
48
|
`run is treating it as part ${input.part.index + 1} of ${input.part.total}.`,
|
|
48
49
|
paid: existing.registerTxDigest !== undefined,
|
|
49
|
-
nextStep: "Run it again with the part size the first attempt used, or
|
|
50
|
-
"
|
|
50
|
+
nextStep: "Run it again with the part size the first attempt used, or clear the unfinished upload " +
|
|
51
|
+
"records to start over. Nothing was sent.",
|
|
51
52
|
});
|
|
52
53
|
}
|
|
53
54
|
// ── already certified: nothing left to sign or send ──
|
|
@@ -58,9 +59,9 @@ async function buyAndPushPartWithWallet(ctx, input) {
|
|
|
58
59
|
// ── registered by an earlier run: push and certify, sign nothing twice ──
|
|
59
60
|
if (existing?.registerTxDigest !== undefined && existing.blobObjectId !== undefined) {
|
|
60
61
|
onStep?.({ step: "resuming", ledgerId: 0, state: "registered" });
|
|
61
|
-
const certificate = await push(input, existing, existing.registerTxDigest, existing.blobObjectId, readReservationBytes(key));
|
|
62
|
+
const certificate = await push(input, existing, existing.registerTxDigest, existing.blobObjectId, await readReservationBytes(key));
|
|
62
63
|
await certify(ctx, input, key, existing, certificate);
|
|
63
|
-
return paidPart(readReservationRecord(key) ?? existing, true);
|
|
64
|
+
return paidPart((await readReservationRecord(key)) ?? existing, true);
|
|
64
65
|
}
|
|
65
66
|
// ── fresh, or interrupted before the signature ──
|
|
66
67
|
onStep?.({ step: "encoding", bytes: sealed.length });
|
|
@@ -68,7 +69,7 @@ async function buyAndPushPartWithWallet(ctx, input) {
|
|
|
68
69
|
try {
|
|
69
70
|
meta = await protocol.computeMetadata({
|
|
70
71
|
bytes: sealed,
|
|
71
|
-
nonce: existing ?
|
|
72
|
+
nonce: existing ? fromBase64Url(existing.nonceB64) : undefined,
|
|
72
73
|
});
|
|
73
74
|
}
|
|
74
75
|
catch (error) {
|
|
@@ -83,8 +84,8 @@ async function buyAndPushPartWithWallet(ctx, input) {
|
|
|
83
84
|
attempt: existing?.attempt ?? 0,
|
|
84
85
|
paidFrom: "wallet",
|
|
85
86
|
blobId: meta.blobId,
|
|
86
|
-
nonceB64:
|
|
87
|
-
rootHashB64:
|
|
87
|
+
nonceB64: toBase64Url(meta.nonce),
|
|
88
|
+
rootHashB64: toBase64Url(meta.rootHash),
|
|
88
89
|
relayUrl: input.relayUrl,
|
|
89
90
|
epochs: input.epochs,
|
|
90
91
|
sealedLen: sealed.length,
|
|
@@ -98,7 +99,7 @@ async function buyAndPushPartWithWallet(ctx, input) {
|
|
|
98
99
|
parentId: input.entry.parentId,
|
|
99
100
|
};
|
|
100
101
|
// ⛔ BEFORE THE SIGNATURE. See the module header.
|
|
101
|
-
writeReservation(key, record, sealed);
|
|
102
|
+
await writeReservation(key, record, sealed);
|
|
102
103
|
onStep?.({ step: "reserving" });
|
|
103
104
|
const quote = ctx.quotes[input.part.index];
|
|
104
105
|
if (quote === undefined)
|
|
@@ -108,6 +109,7 @@ async function buyAndPushPartWithWallet(ctx, input) {
|
|
|
108
109
|
registered = await ctx.signRegister({
|
|
109
110
|
network: ctx.network,
|
|
110
111
|
code: ctx.code,
|
|
112
|
+
wallet: ctx.wallet,
|
|
111
113
|
relayUrl: input.relayUrl,
|
|
112
114
|
epochs: ctx.epochs,
|
|
113
115
|
storage: ctx.storage.kind === "buy" ? { kind: "buy" } : ctx.storage,
|
|
@@ -132,14 +134,14 @@ async function buyAndPushPartWithWallet(ctx, input) {
|
|
|
132
134
|
record.registerTxDigest = registered.digest;
|
|
133
135
|
record.blobObjectId = registered.blobObjectId;
|
|
134
136
|
record.endEpoch = registered.endEpoch;
|
|
135
|
-
writeReservation(key, record, sealed);
|
|
137
|
+
await writeReservation(key, record, sealed);
|
|
136
138
|
ctx.onSpend({
|
|
137
139
|
walFrost: quote.writeFrost + (ctx.storage.kind === "buy" ? quote.storageFrost : 0n),
|
|
138
140
|
suiMist: quote.tipMist + (ctx.feeMist ?? 0n),
|
|
139
141
|
});
|
|
140
142
|
const certificate = await push(input, record, registered.digest, registered.blobObjectId, sealed);
|
|
141
143
|
await certify(ctx, input, key, record, certificate);
|
|
142
|
-
return paidPart(readReservationRecord(key) ?? record, false);
|
|
144
|
+
return paidPart((await readReservationRecord(key)) ?? record, false);
|
|
143
145
|
}
|
|
144
146
|
/** The bytes to the relay named in the register transaction. The storage is already bought. */
|
|
145
147
|
async function push(input, record, registerTxDigest, blobObjectId, sealed) {
|
|
@@ -148,7 +150,7 @@ async function push(input, record, registerTxDigest, blobObjectId, sealed) {
|
|
|
148
150
|
return await input.protocol.uploadToRelay({
|
|
149
151
|
blobId: record.blobId,
|
|
150
152
|
bytes: sealed,
|
|
151
|
-
nonce:
|
|
153
|
+
nonce: fromBase64Url(record.nonceB64),
|
|
152
154
|
registerTxDigest,
|
|
153
155
|
blobObjectId,
|
|
154
156
|
});
|
|
@@ -173,6 +175,7 @@ async function certify(ctx, input, key, record, certificate) {
|
|
|
173
175
|
digest = await ctx.signCertify({
|
|
174
176
|
network: ctx.network,
|
|
175
177
|
code: ctx.code,
|
|
178
|
+
wallet: ctx.wallet,
|
|
176
179
|
relayUrl: record.relayUrl,
|
|
177
180
|
blobId: record.blobId,
|
|
178
181
|
blobObjectId: record.blobObjectId,
|
|
@@ -190,7 +193,7 @@ async function certify(ctx, input, key, record, certificate) {
|
|
|
190
193
|
"certifies again, which costs gas and nothing else.",
|
|
191
194
|
});
|
|
192
195
|
}
|
|
193
|
-
writeReservation(key, { ...record, certifyTxDigest: digest }, readReservationBytes(key));
|
|
196
|
+
await writeReservation(key, { ...record, certifyTxDigest: digest }, await readReservationBytes(key));
|
|
194
197
|
// The certify fee is not measured beforehand, so the ledger is told about the tip and the
|
|
195
198
|
// register fee only — the review said so.
|
|
196
199
|
}
|
package/dist/upload.js
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
// ⛔ EVERY SEAM IS INJECTED. The storage-network protocol and the api are interfaces, so the tests
|
|
27
27
|
// drive the real decisions — including every failure branch — without a network and without
|
|
28
28
|
// spending anything. The seams themselves are in `upload-wire.ts`.
|
|
29
|
+
import { fromBase64Url, toBase64Url } from "./bytes.js";
|
|
29
30
|
import { NmtsError } from "./errors.js";
|
|
30
31
|
import { ServerError } from "./api.js";
|
|
31
32
|
import { readReservationBytes, readReservationRecord, writeReservation, } from "./upload-store.js";
|
|
@@ -53,7 +54,7 @@ function why(error) {
|
|
|
53
54
|
*/
|
|
54
55
|
export async function buyAndPushPart(input) {
|
|
55
56
|
const { api, protocol, key, sealed, onStep } = input;
|
|
56
|
-
const existing = readReservationRecord(key);
|
|
57
|
+
const existing = await readReservationRecord(key);
|
|
57
58
|
// ⛔ THE STORED PLACEMENT WINS THE ARGUMENT, AND A DISAGREEMENT STOPS THE RUN. The bytes on disk
|
|
58
59
|
// were sealed as part i of n and paid for as that; pushing them while this run believes the
|
|
59
60
|
// file splits some other way would file storage under the wrong position in the file. It
|
|
@@ -64,8 +65,8 @@ export async function buyAndPushPart(input) {
|
|
|
64
65
|
phase: "reserve",
|
|
65
66
|
message: "This upload was started with the wallet paying, and this run would pay with credits.",
|
|
66
67
|
paid: existing.registerTxDigest !== undefined,
|
|
67
|
-
nextStep: "Run it again with --pay wallet to finish it, or
|
|
68
|
-
"
|
|
68
|
+
nextStep: "Run it again with --pay wallet to finish it, or clear the unfinished upload records to " +
|
|
69
|
+
"start over with credits. Nothing was sent.",
|
|
69
70
|
});
|
|
70
71
|
}
|
|
71
72
|
if (existing !== null) {
|
|
@@ -76,8 +77,8 @@ export async function buyAndPushPart(input) {
|
|
|
76
77
|
message: `This upload was started as part ${record.partIndex + 1} of ${record.partTotal} and this ` +
|
|
77
78
|
`run is treating it as part ${input.part.index + 1} of ${input.part.total}.`,
|
|
78
79
|
paid: record.ledgerId !== undefined,
|
|
79
|
-
nextStep: "Run it again with the part size the first attempt used, or
|
|
80
|
-
"
|
|
80
|
+
nextStep: "Run it again with the part size the first attempt used, or clear the unfinished upload " +
|
|
81
|
+
"records to start over. Nothing was sent.",
|
|
81
82
|
});
|
|
82
83
|
}
|
|
83
84
|
}
|
|
@@ -111,7 +112,7 @@ export async function buyAndPushPart(input) {
|
|
|
111
112
|
// whatever state it is in — `failed` included. A cleared record means the next run
|
|
112
113
|
// rebuilds the same key, is handed the same dead row, and is told to start over into it.
|
|
113
114
|
// Forever. Counting up is what starting over actually means.
|
|
114
|
-
writeReservation(key, { ...stripReservation(record), attempt: record.attempt + 1 }, readReservationBytes(key));
|
|
115
|
+
await writeReservation(key, { ...stripReservation(record), attempt: record.attempt + 1 }, await readReservationBytes(key));
|
|
115
116
|
throw new UploadError({
|
|
116
117
|
phase: "reserve",
|
|
117
118
|
message: `The credit reservation for this file ended as "${status.state}" and cannot be used.`,
|
|
@@ -131,12 +132,12 @@ export async function buyAndPushPart(input) {
|
|
|
131
132
|
await pushPart(input, {
|
|
132
133
|
ledgerId,
|
|
133
134
|
blobId: record.blobId,
|
|
134
|
-
nonce:
|
|
135
|
+
nonce: fromBase64Url(record.nonceB64),
|
|
135
136
|
registerTxDigest: status.register_tx_digest,
|
|
136
137
|
blobObjectId: status.blob_object_id,
|
|
137
138
|
// ⛔ THE STORED BYTES AND THE STORED RELAY. Not the caller's — see the module header.
|
|
138
139
|
// Read here, and only here: a part that came back certified never needs them at all.
|
|
139
|
-
sealed: readReservationBytes(key),
|
|
140
|
+
sealed: await readReservationBytes(key),
|
|
140
141
|
relayUrl: record.relayUrl,
|
|
141
142
|
});
|
|
142
143
|
return paidPart(record, ledgerId, true);
|
|
@@ -149,7 +150,7 @@ export async function buyAndPushPart(input) {
|
|
|
149
150
|
bytes: sealed,
|
|
150
151
|
// Re-feeding a stored nonce is what makes the retry reproduce the digest a paid tip was
|
|
151
152
|
// computed for. A fresh random one would strand the reservation.
|
|
152
|
-
nonce: existing ?
|
|
153
|
+
nonce: existing ? fromBase64Url(existing.nonceB64) : undefined,
|
|
153
154
|
});
|
|
154
155
|
}
|
|
155
156
|
catch (error) {
|
|
@@ -160,14 +161,14 @@ export async function buyAndPushPart(input) {
|
|
|
160
161
|
nextStep: "Nothing was sent and nothing was spent.",
|
|
161
162
|
});
|
|
162
163
|
}
|
|
163
|
-
const nonceB64 =
|
|
164
|
+
const nonceB64 = toBase64Url(meta.nonce);
|
|
164
165
|
const record = {
|
|
165
166
|
// ⛔ The attempt number is CARRIED FORWARD, not reset. A record that survives a dead
|
|
166
167
|
// reservation is exactly the case that needs a different idempotency key.
|
|
167
168
|
attempt: existing?.attempt ?? 0,
|
|
168
169
|
blobId: meta.blobId,
|
|
169
170
|
nonceB64,
|
|
170
|
-
rootHashB64:
|
|
171
|
+
rootHashB64: toBase64Url(meta.rootHash),
|
|
171
172
|
relayUrl: input.relayUrl,
|
|
172
173
|
epochs: input.epochs,
|
|
173
174
|
sealedLen: sealed.length,
|
|
@@ -181,7 +182,7 @@ export async function buyAndPushPart(input) {
|
|
|
181
182
|
parentId: input.entry.parentId,
|
|
182
183
|
};
|
|
183
184
|
// ⛔ BEFORE THE MONEY. See the module header.
|
|
184
|
-
writeReservation(key, record, sealed);
|
|
185
|
+
await writeReservation(key, record, sealed);
|
|
185
186
|
onStep?.({ step: "reserving" });
|
|
186
187
|
let reply;
|
|
187
188
|
try {
|
|
@@ -196,7 +197,7 @@ export async function buyAndPushPart(input) {
|
|
|
196
197
|
deposit_credits: input.depositCredits,
|
|
197
198
|
relay: {
|
|
198
199
|
host: input.relayUrl,
|
|
199
|
-
blob_digest_b64:
|
|
200
|
+
blob_digest_b64: toBase64Url(meta.blobDigest),
|
|
200
201
|
nonce_b64: nonceB64,
|
|
201
202
|
},
|
|
202
203
|
});
|
|
@@ -217,7 +218,7 @@ export async function buyAndPushPart(input) {
|
|
|
217
218
|
if (!isLive(reply.state)) {
|
|
218
219
|
// Same reason as the resumed branch above: keep the record, count up, so the next run does not
|
|
219
220
|
// ask under a key the server has already settled.
|
|
220
|
-
writeReservation(key, { ...stripReservation(record), attempt: record.attempt + 1 }, sealed);
|
|
221
|
+
await writeReservation(key, { ...stripReservation(record), attempt: record.attempt + 1 }, sealed);
|
|
221
222
|
throw new UploadError({
|
|
222
223
|
phase: "reserve",
|
|
223
224
|
message: `The reservation came back as "${reply.state}", which cannot become storage.`,
|
|
@@ -230,7 +231,7 @@ export async function buyAndPushPart(input) {
|
|
|
230
231
|
record.registerTxDigest = reply.register_tx_digest;
|
|
231
232
|
if (reply.blob_object_id)
|
|
232
233
|
record.blobObjectId = reply.blob_object_id;
|
|
233
|
-
writeReservation(key, record, sealed);
|
|
234
|
+
await writeReservation(key, record, sealed);
|
|
234
235
|
if (reply.state === "certified") {
|
|
235
236
|
return paidPart(record, reply.ledger_id, false);
|
|
236
237
|
}
|
|
@@ -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
|
+
}
|