@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
package/dist/upload-steps.js
CHANGED
|
@@ -70,14 +70,14 @@ export async function commitItem(input, fileKey, parts) {
|
|
|
70
70
|
// Advisory only — the chain is the authority on a blob's life. 0 when this machine could not
|
|
71
71
|
// read the epoch clock: a number we do not have is not a number to invent.
|
|
72
72
|
const expiryEpoch = input.currentEpoch === null ? 0 : input.currentEpoch + input.epochs;
|
|
73
|
-
const previous = readItemRecord(fileKey);
|
|
73
|
+
const previous = await readItemRecord(fileKey);
|
|
74
74
|
// ⛔ ALREADY COMMITTED IS NOT COMMITTED AGAIN. The record outlives the commit precisely so a run
|
|
75
75
|
// that died before writing the file list does not make a second file out of storage that is
|
|
76
76
|
// already named.
|
|
77
77
|
if (previous?.itemId !== undefined)
|
|
78
78
|
return previous.itemId;
|
|
79
79
|
const attempt = previous?.attempt ?? 0;
|
|
80
|
-
writeItemRecord(fileKey, { attempt });
|
|
80
|
+
await writeItemRecord(fileKey, { attempt });
|
|
81
81
|
let view;
|
|
82
82
|
try {
|
|
83
83
|
view = await input.api.createItem({
|
|
@@ -100,7 +100,7 @@ export async function commitItem(input, fileKey, parts) {
|
|
|
100
100
|
// ⛔ Written down before returning: from here on the file EXISTS and is paid for, and the only
|
|
101
101
|
// thing still missing is the account's own list. Losing the record now would make it
|
|
102
102
|
// unreachable.
|
|
103
|
-
writeItemRecord(fileKey, { attempt, itemId: view.id });
|
|
103
|
+
await writeItemRecord(fileKey, { attempt, itemId: view.id });
|
|
104
104
|
return view.id;
|
|
105
105
|
}
|
|
106
106
|
/**
|
package/dist/upload-store.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
/**
|
|
3
|
-
export
|
|
1
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
2
|
+
/** The running hash a reservation key is built in. */
|
|
3
|
+
export type ReservationHash = ReturnType<typeof sha256.create>;
|
|
4
4
|
/**
|
|
5
5
|
* A stable, account-scoped name for one file's upload attempt.
|
|
6
6
|
*
|
|
@@ -36,9 +36,9 @@ export declare function reservationKeyStreamed(dataKey: Uint8Array, plaintextChu
|
|
|
36
36
|
* the SHA-256 the account checks its contents against; handing the caller the running hash is
|
|
37
37
|
* what lets both come out of a single pass instead of two reads of a very large file.
|
|
38
38
|
*/
|
|
39
|
-
export declare function startReservationKey(dataKey: Uint8Array):
|
|
39
|
+
export declare function startReservationKey(dataKey: Uint8Array): ReservationHash;
|
|
40
40
|
/** Finish it. The name and the destination go in last, exactly as the one-shot form does. */
|
|
41
|
-
export declare function finishReservationKey(hash:
|
|
41
|
+
export declare function finishReservationKey(hash: ReservationHash, name: string, destination: string): string;
|
|
42
42
|
/**
|
|
43
43
|
* The record name for ONE part of a file.
|
|
44
44
|
*
|
|
@@ -131,7 +131,7 @@ export interface Reservation {
|
|
|
131
131
|
* resume that only needs to commit still reads every byte of a very large upload off the disk.
|
|
132
132
|
* They are fetched separately, by the one step that actually pushes them.
|
|
133
133
|
*/
|
|
134
|
-
export declare function readReservationRecord(key: string): Reservation | null
|
|
134
|
+
export declare function readReservationRecord(key: string): Promise<Reservation | null>;
|
|
135
135
|
/**
|
|
136
136
|
* The sealed bytes a reservation bought.
|
|
137
137
|
*
|
|
@@ -139,14 +139,14 @@ export declare function readReservationRecord(key: string): Reservation | null;
|
|
|
139
139
|
* different blob from the one the treasury registered — the relay refuses them, forever, and
|
|
140
140
|
* the credits are gone.
|
|
141
141
|
*/
|
|
142
|
-
export declare function readReservationBytes(key: string): Uint8Array
|
|
142
|
+
export declare function readReservationBytes(key: string): Promise<Uint8Array>;
|
|
143
143
|
/** The record and its bytes together, for the callers that need both. */
|
|
144
|
-
export declare function readReservation(key: string): {
|
|
144
|
+
export declare function readReservation(key: string): Promise<{
|
|
145
145
|
record: Reservation;
|
|
146
146
|
sealed: Uint8Array;
|
|
147
|
-
} | null
|
|
147
|
+
} | null>;
|
|
148
148
|
/** Write the record and its sealed bytes. Called BEFORE the reserve, and again after it answers. */
|
|
149
|
-
export declare function writeReservation(key: string, record: Reservation, sealed: Uint8Array): void
|
|
149
|
+
export declare function writeReservation(key: string, record: Reservation, sealed: Uint8Array): Promise<void>;
|
|
150
150
|
/**
|
|
151
151
|
* Forget a reservation.
|
|
152
152
|
*
|
|
@@ -154,7 +154,7 @@ export declare function writeReservation(key: string, record: Reservation, seale
|
|
|
154
154
|
* upload already succeeded" was not true of every caller. What IS true of all of them is that
|
|
155
155
|
* nothing further depends on the record, which is why it never throws.
|
|
156
156
|
*/
|
|
157
|
-
export declare function clearReservation(key: string): void
|
|
157
|
+
export declare function clearReservation(key: string): Promise<void>;
|
|
158
158
|
/**
|
|
159
159
|
* The FILE-level half of an unfinished upload: what happened after every part was paid for.
|
|
160
160
|
*
|
|
@@ -175,8 +175,8 @@ export interface ItemRecord {
|
|
|
175
175
|
attempt: number;
|
|
176
176
|
}
|
|
177
177
|
/** What is known about this file's commit, or `null` when it has not been attempted. */
|
|
178
|
-
export declare function readItemRecord(fileKey: string): ItemRecord | null
|
|
178
|
+
export declare function readItemRecord(fileKey: string): Promise<ItemRecord | null>;
|
|
179
179
|
/** Write the file-level record. Called before the commit, and again once it has an id. */
|
|
180
|
-
export declare function writeItemRecord(fileKey: string, record: ItemRecord): void
|
|
180
|
+
export declare function writeItemRecord(fileKey: string, record: ItemRecord): Promise<void>;
|
|
181
181
|
/** Forget the file-level record. Never throws, for the same reason `clearReservation` does not. */
|
|
182
|
-
export declare function clearItemRecord(fileKey: string): void
|
|
182
|
+
export declare function clearItemRecord(fileKey: string): Promise<void>;
|
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
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { OnCollision } from "./collision.ts";
|
|
2
|
+
import { type CryptoGlue } from "./crypto.ts";
|
|
3
|
+
import type { Network } from "./network.ts";
|
|
4
|
+
import type { PaddingRule } from "./shared/lib/crypto/size-padding.ts";
|
|
5
|
+
import { type FileUploadStep, type PlaintextSource } from "./upload-file.ts";
|
|
6
|
+
import { type StorageChoice, type UploadBudget, type WalletUploadReads } from "./upload-wallet-plan.ts";
|
|
7
|
+
import type { BlobProtocol, UploadApi } from "./upload-wire.ts";
|
|
8
|
+
import type { Spend } from "./wallet-grant.ts";
|
|
9
|
+
import type { SignBlobCertify, SignBlobRegister } from "./wallet-sign.ts";
|
|
10
|
+
/** The account this upload belongs to, and how it seals. */
|
|
11
|
+
export interface WalletPutContext {
|
|
12
|
+
/** ⛔ The NMTS key. It seals the file AND derives the wallet that pays. It is never written down. */
|
|
13
|
+
code: string;
|
|
14
|
+
apiKey: string;
|
|
15
|
+
server: string;
|
|
16
|
+
network: Network;
|
|
17
|
+
accountId: string;
|
|
18
|
+
crypt: CryptoGlue;
|
|
19
|
+
/** How much of the file goes into one part. */
|
|
20
|
+
partSize: number;
|
|
21
|
+
/** The rounding rule from the account's sealed list — it changes the stored size, so the price. */
|
|
22
|
+
rule: PaddingRule;
|
|
23
|
+
/** What to do about a name already in use. Absent = the machine's setting, as `addEntry` reads it. */
|
|
24
|
+
onCollision?: OnCollision | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Which of this key's wallets pays, by index (0 = the first one).
|
|
27
|
+
*
|
|
28
|
+
* ⛔ THE CALLER RESOLVES IT, BEFORE ANYTHING IS PRICED. It is the account's own number, kept in
|
|
29
|
+
* the sealed file list (`wallet-pay-index.ts`), and a library that guessed at it here would
|
|
30
|
+
* price one address and sign with another.
|
|
31
|
+
*/
|
|
32
|
+
wallet: number;
|
|
33
|
+
}
|
|
34
|
+
/** One file and where it goes — already resolved, because a library resolves nothing by asking. */
|
|
35
|
+
export interface WalletPutFile {
|
|
36
|
+
source: PlaintextSource;
|
|
37
|
+
name: string;
|
|
38
|
+
parentId: string | null;
|
|
39
|
+
/** The destination AS TYPED — part of the reservation key, so both runs of a resume agree. */
|
|
40
|
+
destination: string;
|
|
41
|
+
}
|
|
42
|
+
/** The chain, the signatures, the wire and the four things a caller may be told. */
|
|
43
|
+
export interface WalletPutSeams {
|
|
44
|
+
/** How many of the storage network's epochs to buy. Default `DEFAULT_UPLOAD_EPOCHS`. */
|
|
45
|
+
epochs?: string | number | undefined;
|
|
46
|
+
/** `fit`, `whole`, or a held resource's object id. Absent = buy new storage. */
|
|
47
|
+
storage?: string | undefined;
|
|
48
|
+
/** Stop at the review. Nothing is sealed, signed or sent, and no signer is even loaded. */
|
|
49
|
+
dryRun?: boolean | undefined;
|
|
50
|
+
/** ⚠ A SEAM, NOT AN OPTION — what the chain is asked. */
|
|
51
|
+
readChain?: ((network: Network, relayUrl: string) => WalletUploadReads | Promise<WalletUploadReads>) | undefined;
|
|
52
|
+
/** ⛔ SEPARATE FROM THE READS so a caller can prove the review stops before this. */
|
|
53
|
+
sign?: {
|
|
54
|
+
register: SignBlobRegister;
|
|
55
|
+
certify: SignBlobCertify;
|
|
56
|
+
} | undefined;
|
|
57
|
+
/** ⚠ SEAMS, NOT OPTIONS — the storage-network protocol and the server calls. */
|
|
58
|
+
protocol?: ((network: Network, bodyBytes: number, onSent: (sent: number, total: number) => void) => BlobProtocol & {
|
|
59
|
+
relayUrl: string;
|
|
60
|
+
}) | undefined;
|
|
61
|
+
api?: UploadApi | undefined;
|
|
62
|
+
/** Told as sealed bytes leave for the relay. */
|
|
63
|
+
onProgress?: ((sent: number, total: number) => void) | undefined;
|
|
64
|
+
/** Told about each step as it starts. */
|
|
65
|
+
onStep?: ((step: FileUploadStep) => void) | undefined;
|
|
66
|
+
/** Told the numbers the moment they are known — before the dry run returns and before a refusal. */
|
|
67
|
+
onReview?: ((review: WalletPutReview) => void) | undefined;
|
|
68
|
+
/** ⛔ THE GATE. Throwing here stops the upload with nothing signed. See the module header. */
|
|
69
|
+
agree?: ((review: WalletPutReview) => void) | undefined;
|
|
70
|
+
/** Told what left the wallet, after each signature. */
|
|
71
|
+
onSpend?: ((spend: Spend) => void) | undefined;
|
|
72
|
+
}
|
|
73
|
+
/** What this upload would buy, in the order somebody deciding needs it. Base units throughout. */
|
|
74
|
+
export interface WalletPutReview {
|
|
75
|
+
name: string;
|
|
76
|
+
/** Plaintext bytes. */
|
|
77
|
+
bytes: number;
|
|
78
|
+
/** Bytes the storage network holds, padding and sealing included. */
|
|
79
|
+
sealedBytes: number;
|
|
80
|
+
parts: number;
|
|
81
|
+
epochs: number;
|
|
82
|
+
/** The term in days, read from the network's own clock. */
|
|
83
|
+
days: string;
|
|
84
|
+
/** The epoch the storage runs to. */
|
|
85
|
+
endEpoch: number;
|
|
86
|
+
/** The relay's tip over every part, in MIST. Paid inside the register signatures. */
|
|
87
|
+
tipMist: bigint;
|
|
88
|
+
/** Where the storage comes from. */
|
|
89
|
+
storage: StorageChoice;
|
|
90
|
+
/** Free storage resources the wallet holds when none was asked for; null when unreadable. */
|
|
91
|
+
heldResources: number | null;
|
|
92
|
+
/** The wallet against the price: what is needed, what is held, and the shortfall if there is one. */
|
|
93
|
+
budget: UploadBudget;
|
|
94
|
+
}
|
|
95
|
+
/** A dry run's answer, or a finished upload's. The review is in both, so both report one price. */
|
|
96
|
+
export type WalletPutOutcome = {
|
|
97
|
+
kind: "review";
|
|
98
|
+
review: WalletPutReview;
|
|
99
|
+
} | {
|
|
100
|
+
kind: "uploaded";
|
|
101
|
+
review: WalletPutReview;
|
|
102
|
+
itemId: string;
|
|
103
|
+
/** The name it got — numbered if the one asked for was taken. */
|
|
104
|
+
savedAs: string;
|
|
105
|
+
/** The id of the file this one displaced into the trash, when the machine overwrites. */
|
|
106
|
+
replaced: string | null;
|
|
107
|
+
/** The file-list version this write produced. */
|
|
108
|
+
fileListVersion: number;
|
|
109
|
+
/** True when every part was already signed for by an earlier run: this call spent nothing. */
|
|
110
|
+
resumed: boolean;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Price, agree, sign, upload and record ONE file, paid from the wallet the NMTS key derives.
|
|
114
|
+
*
|
|
115
|
+
* ⛔ THE SIGNING MODULE IS LOADED ONLY AFTER `agree` HAS RETURNED. A dry run and a refusal never
|
|
116
|
+
* bring the code that can spend into memory.
|
|
117
|
+
*/
|
|
118
|
+
export declare function walletPut(ctx: WalletPutContext, file: WalletPutFile, seams?: WalletPutSeams): Promise<WalletPutOutcome>;
|