@needmoretruth/nmts-cli 0.35.0 → 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 +26 -0
- package/README.ko.md +19 -21
- package/README.md +21 -22
- package/dist/account.js +2 -1
- package/dist/arg-options.d.ts +3 -1
- 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/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 +2 -1
- package/dist/commands/push.js +6 -4
- package/dist/commands/put-wallet.js +5 -3
- package/dist/commands/put.js +8 -5
- 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-hall.d.ts +4 -1
- package/dist/commands/wallet-hall.js +16 -8
- 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.d.ts +1 -1
- 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/gate.js +1 -1
- package/dist/help.js +9 -4
- 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 -49
- package/dist/index.js +20 -45
- package/dist/main.js +7 -6
- 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 +7 -0
- package/dist/risk.js +10 -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/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.js +2 -2
- package/dist/upload-wallet.js +16 -15
- package/dist/upload.js +16 -15
- package/dist/wallet-sign-seams.d.ts +12 -0
- package/dist/wallet-sign.js +10 -13
- 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 +11 -7
- package/package.json +13 -2
package/dist/upload-store.js
CHANGED
|
@@ -9,25 +9,21 @@
|
|
|
9
9
|
//
|
|
10
10
|
// So they are written down BEFORE the money moves. That ordering is the whole design.
|
|
11
11
|
//
|
|
12
|
-
// ⛔ WHAT IS
|
|
13
|
-
// about to be handed to a public storage network.
|
|
14
|
-
// public" is about the CONTENT and the
|
|
15
|
-
// account uploaded and when.
|
|
12
|
+
// ⛔ WHAT IS KEPT IS ALREADY PUBLIC. The `.bin` half is the sealed NCF-3 stream — the exact bytes
|
|
13
|
+
// about to be handed to a public storage network. The host still keeps it as privately as it
|
|
14
|
+
// can (0600 on this machine), because "already public" is about the CONTENT and the record's
|
|
15
|
+
// presence would otherwise say which files this account uploaded and when.
|
|
16
16
|
//
|
|
17
|
-
// ⛔ THE
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import { join } from "node:path";
|
|
24
|
-
import { configDir, modesAreEnforced } from "./credentials.js";
|
|
25
|
-
import { chmodSync } from "node:fs";
|
|
17
|
+
// ⛔ THE KEY IS NOT A CONTENT FINGERPRINT. Keying by SHA-256 of the plaintext would leave a store
|
|
18
|
+
// of hashes matchable against published hash sets — the very thing sealing the content hash
|
|
19
|
+
// avoids. The key mixes the account's data key in, so it identifies the file only to somebody
|
|
20
|
+
// who already holds the account.
|
|
21
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
22
|
+
import { fromUtf8, toBase64Url, utf8 } from "./bytes.js";
|
|
26
23
|
import { NmtsError } from "./errors.js";
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
24
|
+
import { host } from "./host.js";
|
|
25
|
+
/** The area of the host's state these records live in. One key per file below it. */
|
|
26
|
+
const AREA = "uploads";
|
|
31
27
|
/**
|
|
32
28
|
* A stable, account-scoped name for one file's upload attempt.
|
|
33
29
|
*
|
|
@@ -71,12 +67,12 @@ export function reservationKeyStreamed(dataKey, plaintextChunks, name, destinati
|
|
|
71
67
|
* what lets both come out of a single pass instead of two reads of a very large file.
|
|
72
68
|
*/
|
|
73
69
|
export function startReservationKey(dataKey) {
|
|
74
|
-
return
|
|
70
|
+
return sha256.create().update(dataKey);
|
|
75
71
|
}
|
|
76
72
|
/** Finish it. The name and the destination go in last, exactly as the one-shot form does. */
|
|
77
73
|
export function finishReservationKey(hash, name, destination) {
|
|
78
|
-
const digest = hash.update(
|
|
79
|
-
return
|
|
74
|
+
const digest = hash.update(utf8(`\u0000${name}\u0000${destination}`)).digest();
|
|
75
|
+
return toBase64Url(digest).slice(0, 32);
|
|
80
76
|
}
|
|
81
77
|
/**
|
|
82
78
|
* The record name for ONE part of a file.
|
|
@@ -89,9 +85,9 @@ export function finishReservationKey(hash, name, destination) {
|
|
|
89
85
|
export function partKey(fileKey, partIndex) {
|
|
90
86
|
return `${fileKey}~p${partIndex}`;
|
|
91
87
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return { json:
|
|
88
|
+
/** The two keys one part's reservation occupies: its record, and the sealed bytes it bought. */
|
|
89
|
+
function keysFor(key) {
|
|
90
|
+
return { json: `${AREA}/${key}.json`, bin: `${AREA}/${key}.bin` };
|
|
95
91
|
}
|
|
96
92
|
function isReservation(value) {
|
|
97
93
|
if (typeof value !== "object" || value === null)
|
|
@@ -114,23 +110,25 @@ function isReservation(value) {
|
|
|
114
110
|
* resume that only needs to commit still reads every byte of a very large upload off the disk.
|
|
115
111
|
* They are fetched separately, by the one step that actually pushes them.
|
|
116
112
|
*/
|
|
117
|
-
export function readReservationRecord(key) {
|
|
118
|
-
const { json, bin } =
|
|
119
|
-
|
|
113
|
+
export async function readReservationRecord(key) {
|
|
114
|
+
const { json, bin } = keysFor(key);
|
|
115
|
+
const state = host().state;
|
|
116
|
+
const held = await state.read(json);
|
|
117
|
+
if (held === undefined || (await state.read(bin)) === undefined)
|
|
120
118
|
return null;
|
|
121
119
|
let parsed;
|
|
122
120
|
try {
|
|
123
|
-
parsed = JSON.parse(
|
|
121
|
+
parsed = JSON.parse(fromUtf8(held));
|
|
124
122
|
}
|
|
125
123
|
catch {
|
|
126
124
|
// ⛔ Unreadable is not the same as absent, and treating it as absent would buy storage twice.
|
|
127
|
-
throw new NmtsError(`
|
|
125
|
+
throw new NmtsError(`The unfinished upload record ${json} could not be read.`, {
|
|
128
126
|
nextStep: "It names storage this account may already have paid for. Move it aside rather than " +
|
|
129
127
|
"deleting it if the upload matters, then try again.",
|
|
130
128
|
});
|
|
131
129
|
}
|
|
132
130
|
if (!isReservation(parsed)) {
|
|
133
|
-
throw new NmtsError(`The unfinished upload record
|
|
131
|
+
throw new NmtsError(`The unfinished upload record ${json} is not in a shape this version knows.`, {
|
|
134
132
|
nextStep: "Move it aside and try again. Nothing was sent.",
|
|
135
133
|
});
|
|
136
134
|
}
|
|
@@ -143,38 +141,36 @@ export function readReservationRecord(key) {
|
|
|
143
141
|
* different blob from the one the treasury registered — the relay refuses them, forever, and
|
|
144
142
|
* the credits are gone.
|
|
145
143
|
*/
|
|
146
|
-
export function readReservationBytes(key) {
|
|
147
|
-
const { bin } =
|
|
148
|
-
|
|
144
|
+
export async function readReservationBytes(key) {
|
|
145
|
+
const { bin } = keysFor(key);
|
|
146
|
+
const held = await host().state.read(bin);
|
|
147
|
+
if (held === undefined) {
|
|
148
|
+
throw new NmtsError(`The sealed bytes of ${bin} are gone.`, {
|
|
149
|
+
nextStep: "Nothing was sent. The storage they bought cannot be filled by re-sealing — run the " +
|
|
150
|
+
"upload again to buy storage for a fresh sealing of the same file.",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return held;
|
|
149
154
|
}
|
|
150
155
|
/** The record and its bytes together, for the callers that need both. */
|
|
151
|
-
export function readReservation(key) {
|
|
152
|
-
const record = readReservationRecord(key);
|
|
156
|
+
export async function readReservation(key) {
|
|
157
|
+
const record = await readReservationRecord(key);
|
|
153
158
|
if (record === null)
|
|
154
159
|
return null;
|
|
155
|
-
return { record, sealed: readReservationBytes(key) };
|
|
160
|
+
return { record, sealed: await readReservationBytes(key) };
|
|
156
161
|
}
|
|
157
162
|
/** Write the record and its sealed bytes. Called BEFORE the reserve, and again after it answers. */
|
|
158
|
-
export function writeReservation(key, record, sealed) {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
atomically(bin, sealed);
|
|
170
|
-
atomically(json, Buffer.from(`${JSON.stringify(record, null, 2)}\n`, "utf8"));
|
|
171
|
-
}
|
|
172
|
-
function atomically(target, bytes) {
|
|
173
|
-
const scratch = `${target}.${process.pid}.tmp`;
|
|
174
|
-
writeFileSync(scratch, bytes, { mode: 0o600 });
|
|
175
|
-
if (modesAreEnforced())
|
|
176
|
-
chmodSync(scratch, 0o600);
|
|
177
|
-
renameSync(scratch, target);
|
|
163
|
+
export async function writeReservation(key, record, sealed) {
|
|
164
|
+
const { json, bin } = keysFor(key);
|
|
165
|
+
// ⛔ THE BYTES GO DOWN FIRST, AND THE RECORD IS WHAT MAKES THE PAIR COUNT. A reader treats a
|
|
166
|
+
// record without its bytes as no reservation at all, so a run that stopped between these two
|
|
167
|
+
// writes leaves nothing that can be resumed — rather than a record pointing at bytes that are
|
|
168
|
+
// not there. Each write on its own is all-or-nothing; that is the host's promise, and the
|
|
169
|
+
// write that matters most is the LAST one, because a half-written record is the only local
|
|
170
|
+
// pointer to a file that is already paid for.
|
|
171
|
+
const state = host().state;
|
|
172
|
+
await state.write(bin, sealed);
|
|
173
|
+
await state.write(json, utf8(`${JSON.stringify(record, null, 2)}\n`));
|
|
178
174
|
}
|
|
179
175
|
/**
|
|
180
176
|
* Forget a reservation.
|
|
@@ -183,11 +179,11 @@ function atomically(target, bytes) {
|
|
|
183
179
|
* upload already succeeded" was not true of every caller. What IS true of all of them is that
|
|
184
180
|
* nothing further depends on the record, which is why it never throws.
|
|
185
181
|
*/
|
|
186
|
-
export function clearReservation(key) {
|
|
187
|
-
const { json, bin } =
|
|
188
|
-
for (const
|
|
182
|
+
export async function clearReservation(key) {
|
|
183
|
+
const { json, bin } = keysFor(key);
|
|
184
|
+
for (const name of [json, bin]) {
|
|
189
185
|
try {
|
|
190
|
-
|
|
186
|
+
await host().state.remove(name);
|
|
191
187
|
}
|
|
192
188
|
catch {
|
|
193
189
|
// ⚠ A record that cannot be removed is left where it is. That is not free -- a record
|
|
@@ -197,8 +193,8 @@ export function clearReservation(key) {
|
|
|
197
193
|
}
|
|
198
194
|
}
|
|
199
195
|
}
|
|
200
|
-
function
|
|
201
|
-
return
|
|
196
|
+
function itemKey(fileKey) {
|
|
197
|
+
return `${AREA}/${fileKey}.item.json`;
|
|
202
198
|
}
|
|
203
199
|
function isItemRecord(value) {
|
|
204
200
|
if (typeof value !== "object" || value === null)
|
|
@@ -209,41 +205,38 @@ function isItemRecord(value) {
|
|
|
209
205
|
return id === undefined || typeof id === "string";
|
|
210
206
|
}
|
|
211
207
|
/** What is known about this file's commit, or `null` when it has not been attempted. */
|
|
212
|
-
export function readItemRecord(fileKey) {
|
|
213
|
-
const path =
|
|
214
|
-
|
|
208
|
+
export async function readItemRecord(fileKey) {
|
|
209
|
+
const path = itemKey(fileKey);
|
|
210
|
+
const held = await host().state.read(path);
|
|
211
|
+
if (held === undefined)
|
|
215
212
|
return null;
|
|
216
213
|
let parsed;
|
|
217
214
|
try {
|
|
218
|
-
parsed = JSON.parse(
|
|
215
|
+
parsed = JSON.parse(fromUtf8(held));
|
|
219
216
|
}
|
|
220
217
|
catch {
|
|
221
218
|
// ⛔ Same reasoning as an unreadable reservation: unreadable is not absent. This file is the
|
|
222
219
|
// only local pointer to storage that is already bought and possibly already committed.
|
|
223
|
-
throw new NmtsError(`
|
|
220
|
+
throw new NmtsError(`The unfinished upload record ${path} could not be read.`, {
|
|
224
221
|
nextStep: "It names a file this account may already have paid for. Move it aside rather than " +
|
|
225
222
|
"deleting it if the upload matters, then try again.",
|
|
226
223
|
});
|
|
227
224
|
}
|
|
228
225
|
if (!isItemRecord(parsed)) {
|
|
229
|
-
throw new NmtsError(`The unfinished upload record
|
|
226
|
+
throw new NmtsError(`The unfinished upload record ${path} is not in a shape this version knows.`, {
|
|
230
227
|
nextStep: "Move it aside and try again. Nothing was sent.",
|
|
231
228
|
});
|
|
232
229
|
}
|
|
233
230
|
return parsed;
|
|
234
231
|
}
|
|
235
232
|
/** Write the file-level record. Called before the commit, and again once it has an id. */
|
|
236
|
-
export function writeItemRecord(fileKey, record) {
|
|
237
|
-
|
|
238
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
239
|
-
if (modesAreEnforced())
|
|
240
|
-
chmodSync(dir, 0o700);
|
|
241
|
-
atomically(itemPath(fileKey), Buffer.from(`${JSON.stringify(record, null, 2)}\n`, "utf8"));
|
|
233
|
+
export async function writeItemRecord(fileKey, record) {
|
|
234
|
+
await host().state.write(itemKey(fileKey), utf8(`${JSON.stringify(record, null, 2)}\n`));
|
|
242
235
|
}
|
|
243
236
|
/** Forget the file-level record. Never throws, for the same reason `clearReservation` does not. */
|
|
244
|
-
export function clearItemRecord(fileKey) {
|
|
237
|
+
export async function clearItemRecord(fileKey) {
|
|
245
238
|
try {
|
|
246
|
-
|
|
239
|
+
await host().state.remove(itemKey(fileKey));
|
|
247
240
|
}
|
|
248
241
|
catch {
|
|
249
242
|
// ⚠ Left where it is. A stale one carrying an item id would be resumed rather than
|
|
@@ -21,6 +21,7 @@ import { bcs } from "@mysten/sui/bcs";
|
|
|
21
21
|
import { SuiJsonRpcClient } from "@mysten/sui/jsonRpc";
|
|
22
22
|
import { Transaction } from "@mysten/sui/transactions";
|
|
23
23
|
import { blobIdFromInt, blobIdToInt, MAINNET_WALRUS_PACKAGE_CONFIG, TESTNET_WALRUS_PACKAGE_CONFIG, walrus, } from "@mysten/walrus";
|
|
24
|
+
import { fromBase64Url } from "./bytes.js";
|
|
24
25
|
import { NmtsError } from "./errors.js";
|
|
25
26
|
import { extendReads, netGasFee, readBlobLease } from "./extend-chain.js";
|
|
26
27
|
import { readOwnedStorage, readStorageType } from "./shared/lib/storage-control/chain.js";
|
|
@@ -104,8 +105,8 @@ export function certifyTransaction(client, input) {
|
|
|
104
105
|
deletable: true,
|
|
105
106
|
certificate: {
|
|
106
107
|
signers: input.certificate.signers,
|
|
107
|
-
serializedMessage:
|
|
108
|
-
signature:
|
|
108
|
+
serializedMessage: fromBase64Url(input.certificate.serialized_message_b64),
|
|
109
|
+
signature: fromBase64Url(input.certificate.signature_b64),
|
|
109
110
|
},
|
|
110
111
|
});
|
|
111
112
|
}
|
|
@@ -197,9 +197,9 @@ export async function walletPut(ctx, file, seams = {}) {
|
|
|
197
197
|
// ⛔ ONLY NOW, AND EVERY PART — the same order the credit rail keeps and for the same reason: a
|
|
198
198
|
// paid-for file the list does not name is invisible, and the records are what let a second
|
|
199
199
|
// call finish the job without signing again.
|
|
200
|
-
clearItemRecord(result.fileKey);
|
|
200
|
+
await clearItemRecord(result.fileKey);
|
|
201
201
|
for (const record of partKeysOf(result.fileKey, result.parts))
|
|
202
|
-
clearReservation(record);
|
|
202
|
+
await clearReservation(record);
|
|
203
203
|
if (added.replaced)
|
|
204
204
|
await setTrashed(ctx.server, ctx.apiKey, added.replaced.id, true);
|
|
205
205
|
return {
|
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)
|
|
@@ -133,14 +134,14 @@ async function buyAndPushPartWithWallet(ctx, input) {
|
|
|
133
134
|
record.registerTxDigest = registered.digest;
|
|
134
135
|
record.blobObjectId = registered.blobObjectId;
|
|
135
136
|
record.endEpoch = registered.endEpoch;
|
|
136
|
-
writeReservation(key, record, sealed);
|
|
137
|
+
await writeReservation(key, record, sealed);
|
|
137
138
|
ctx.onSpend({
|
|
138
139
|
walFrost: quote.writeFrost + (ctx.storage.kind === "buy" ? quote.storageFrost : 0n),
|
|
139
140
|
suiMist: quote.tipMist + (ctx.feeMist ?? 0n),
|
|
140
141
|
});
|
|
141
142
|
const certificate = await push(input, record, registered.digest, registered.blobObjectId, sealed);
|
|
142
143
|
await certify(ctx, input, key, record, certificate);
|
|
143
|
-
return paidPart(readReservationRecord(key) ?? record, false);
|
|
144
|
+
return paidPart((await readReservationRecord(key)) ?? record, false);
|
|
144
145
|
}
|
|
145
146
|
/** The bytes to the relay named in the register transaction. The storage is already bought. */
|
|
146
147
|
async function push(input, record, registerTxDigest, blobObjectId, sealed) {
|
|
@@ -149,7 +150,7 @@ async function push(input, record, registerTxDigest, blobObjectId, sealed) {
|
|
|
149
150
|
return await input.protocol.uploadToRelay({
|
|
150
151
|
blobId: record.blobId,
|
|
151
152
|
bytes: sealed,
|
|
152
|
-
nonce:
|
|
153
|
+
nonce: fromBase64Url(record.nonceB64),
|
|
153
154
|
registerTxDigest,
|
|
154
155
|
blobObjectId,
|
|
155
156
|
});
|
|
@@ -192,7 +193,7 @@ async function certify(ctx, input, key, record, certificate) {
|
|
|
192
193
|
"certifies again, which costs gas and nothing else.",
|
|
193
194
|
});
|
|
194
195
|
}
|
|
195
|
-
writeReservation(key, { ...record, certifyTxDigest: digest }, readReservationBytes(key));
|
|
196
|
+
await writeReservation(key, { ...record, certifyTxDigest: digest }, await readReservationBytes(key));
|
|
196
197
|
// The certify fee is not measured beforehand, so the ledger is told about the tip and the
|
|
197
198
|
// register fee only — the review said so.
|
|
198
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
|
}
|
|
@@ -8,6 +8,14 @@ import type { Network } from "./network.ts";
|
|
|
8
8
|
export type SignMessage = (input: {
|
|
9
9
|
/** ⛔ The NMTS key. It never leaves this machine: it derives the wallet and nothing else. */
|
|
10
10
|
code: string;
|
|
11
|
+
/**
|
|
12
|
+
* Which of this key's wallets proves the name.
|
|
13
|
+
*
|
|
14
|
+
* ⛔ IT IS THE PAYING WALLET BECAUSE THAT IS THE WALLET THE GIFT CAME FROM. A hall entry is an
|
|
15
|
+
* address, and `wallet donate` sends from the account's own number; signing with any other
|
|
16
|
+
* wallet would offer the server a name for an address that has never given anything.
|
|
17
|
+
*/
|
|
18
|
+
wallet: number;
|
|
11
19
|
message: string;
|
|
12
20
|
}) => Promise<string>;
|
|
13
21
|
/** The seam `commands/wallet-send.ts` signs through. Returns the transaction digest. */
|
|
@@ -24,6 +32,8 @@ export type SignSwap = (input: {
|
|
|
24
32
|
network: Network;
|
|
25
33
|
/** ⛔ The NMTS key. It never leaves this machine: it derives the wallet and nothing else. */
|
|
26
34
|
code: string;
|
|
35
|
+
/** Which of this key's wallets swaps — the account's own number (`wallet-pay-index.ts`). */
|
|
36
|
+
wallet: number;
|
|
27
37
|
shape: SwapShape;
|
|
28
38
|
}) => Promise<string>;
|
|
29
39
|
/** The seam the wallet rail registers through. */
|
|
@@ -52,6 +62,8 @@ export type SignBlobCertify = (input: {
|
|
|
52
62
|
export type SignStorageOp = (input: {
|
|
53
63
|
network: Network;
|
|
54
64
|
code: string;
|
|
65
|
+
/** Which of this key's wallets holds the resource — the account's own number. */
|
|
66
|
+
wallet: number;
|
|
55
67
|
shape: StorageOpShape;
|
|
56
68
|
walrusPackageId: string;
|
|
57
69
|
}) => Promise<string>;
|
package/dist/wallet-sign.js
CHANGED
|
@@ -189,10 +189,10 @@ export const signTransfer = async ({ network, code, wallet, shape }) => {
|
|
|
189
189
|
* makes. `signPersonalMessage` and not `sign`: that intent is the domain separator which stops
|
|
190
190
|
* signed text from ever being read as a transaction this wallet authorised.
|
|
191
191
|
*/
|
|
192
|
-
export const signMessage = async ({ code, message }) => {
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
const keypair = await keypairFor(code,
|
|
192
|
+
export const signMessage = async ({ code, wallet, message }) => {
|
|
193
|
+
// The wallet the caller resolved signs it: the hall entry is that wallet's address, and it is
|
|
194
|
+
// the wallet a gift left from, so any other one would name an address that gave nothing.
|
|
195
|
+
const keypair = await keypairFor(code, wallet);
|
|
196
196
|
const { signature } = await keypair.signPersonalMessage(new TextEncoder().encode(message));
|
|
197
197
|
return signature;
|
|
198
198
|
};
|
|
@@ -207,12 +207,9 @@ export const signMessage = async ({ code, message }) => {
|
|
|
207
207
|
*
|
|
208
208
|
* ⚠ A FAILURE HERE IS NOT PROOF THAT NOTHING HAPPENED — the same words as the extension above.
|
|
209
209
|
*/
|
|
210
|
-
export const signSwap = async ({ network, code, shape }) => {
|
|
210
|
+
export const signSwap = async ({ network, code, wallet, shape }) => {
|
|
211
211
|
const client = walrusClient(network);
|
|
212
|
-
|
|
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);
|
|
212
|
+
const keypair = await keypairFor(code, wallet);
|
|
216
213
|
const tx = swapTransaction({ ...shape, network, sender: keypair.toSuiAddress() });
|
|
217
214
|
const result = await client.signAndExecuteTransaction({
|
|
218
215
|
transaction: tx,
|
|
@@ -303,11 +300,11 @@ export const signBlobCertify = async ({ network, code, wallet, relayUrl, ...shap
|
|
|
303
300
|
* so what is signed is what was reviewed. A failed execution still has a digest and still
|
|
304
301
|
* spent its gas, so the status is read and a failure is said as one.
|
|
305
302
|
*/
|
|
306
|
-
export const signStorageOp = async ({ network, code, shape, walrusPackageId }) => {
|
|
303
|
+
export const signStorageOp = async ({ network, code, wallet, shape, walrusPackageId }) => {
|
|
307
304
|
const client = walrusClient(network);
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
const keypair = await keypairFor(code,
|
|
305
|
+
// The resource the review named was read from this wallet's address, so this wallet is the one
|
|
306
|
+
// that can reshape or hand it over.
|
|
307
|
+
const keypair = await keypairFor(code, wallet);
|
|
311
308
|
const tx = storageOpTransaction(shape, { walrusPackageId, sender: keypair.toSuiAddress() });
|
|
312
309
|
const result = await client.signAndExecuteTransaction({ transaction: tx, signer: keypair, options: { showEffects: true } });
|
|
313
310
|
const effects = result.effects;
|
package/dist/walrus-write.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// transaction paid — see the note on `sendTip` below, which is not what its name suggests.
|
|
11
11
|
import { SuiJsonRpcClient } from "@mysten/sui/jsonRpc";
|
|
12
12
|
import { walrus } from "@mysten/walrus";
|
|
13
|
+
import { toBase64Url } from "./bytes.js";
|
|
13
14
|
import { epochClock } from "./expiry.js";
|
|
14
15
|
// ⛔ The re-export below is what callers use; this local name is the same function,
|
|
15
16
|
// imported so the reader above can call it without importing from itself.
|
|
@@ -112,8 +113,8 @@ export function createBlobProtocol(network, bodyBytes, onSent) {
|
|
|
112
113
|
.catch((error) => fail("The upload relay refused the bytes", error));
|
|
113
114
|
return {
|
|
114
115
|
signers: certificate.signers,
|
|
115
|
-
serialized_message_b64:
|
|
116
|
-
signature_b64:
|
|
116
|
+
serialized_message_b64: toBase64Url(certificate.serializedMessage),
|
|
117
|
+
signature_b64: toBase64Url(certificate.signature),
|
|
117
118
|
};
|
|
118
119
|
},
|
|
119
120
|
};
|
package/dist/walrus.d.ts
CHANGED
|
@@ -28,22 +28,7 @@ export declare const RELAY_HOSTS: Readonly<Record<string, readonly string[]>>;
|
|
|
28
28
|
export declare const SUI_RPC_HOSTS: Readonly<Record<string, readonly string[]>>;
|
|
29
29
|
/** How long one host gets before the next is tried. A read that stalls is a read that failed. */
|
|
30
30
|
export declare const READ_TIMEOUT_MS = 60000;
|
|
31
|
-
|
|
32
|
-
* Point reads at somebody else's aggregator, or at a development stack.
|
|
33
|
-
*
|
|
34
|
-
* ⚠ It replaces the list rather than adding to it, and that is deliberate: a run should read from
|
|
35
|
-
* where it was told to read, not from there AND the public hosts. Comma-separated for more
|
|
36
|
-
* than one, tried in the order given.
|
|
37
|
-
*/
|
|
38
|
-
export declare const AGGREGATOR_ENV_VAR = "NMTS_AGGREGATOR";
|
|
39
|
-
/**
|
|
40
|
-
* Push writes through somebody else's relay, or through a development stack.
|
|
41
|
-
*
|
|
42
|
-
* ⚠ ONE host, not a list. Unlike reads there is nothing to fail over to — see `RELAY_HOSTS`.
|
|
43
|
-
*/
|
|
44
|
-
export declare const RELAY_ENV_VAR = "NMTS_RELAY";
|
|
45
|
-
/** Ask a different Sui JSON-RPC node the shard-count question. */
|
|
46
|
-
export declare const SUI_RPC_ENV_VAR = "NMTS_SUI_RPC";
|
|
31
|
+
export { AGGREGATOR_ENV_VAR, RELAY_ENV_VAR, SUI_RPC_ENV_VAR } from "./env-vars.ts";
|
|
47
32
|
/** The relay this run writes through: the environment's if it named one, else the network's. */
|
|
48
33
|
export declare function relayHost(network: string): string;
|
|
49
34
|
/**
|