@needmoretruth/nmts-cli 0.17.2 → 0.34.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 +124 -516
- package/CHANGELOG.md +98 -0
- package/README.ko.md +374 -467
- package/README.md +384 -465
- package/dist/account-proof.d.ts +29 -0
- package/dist/account-proof.js +6 -6
- package/dist/account.d.ts +18 -0
- package/dist/account.js +5 -5
- package/dist/agent-host.d.ts +71 -0
- package/dist/agent-host.js +169 -0
- package/dist/api-advice.d.ts +2 -0
- package/dist/api-advice.js +210 -0
- package/dist/api-key.d.ts +79 -0
- package/dist/api-key.js +7 -7
- package/dist/api-refusal.d.ts +14 -0
- package/dist/api-refusal.js +8 -0
- package/dist/api.d.ts +114 -0
- package/dist/api.js +59 -162
- package/dist/args.d.ts +160 -0
- package/dist/args.js +118 -30
- package/dist/artifact-about.d.ts +49 -0
- package/dist/autonomy.d.ts +39 -0
- package/dist/autonomy.js +110 -32
- package/dist/code-access.d.ts +29 -0
- package/dist/code-access.js +3 -3
- package/dist/code-vault.d.ts +38 -0
- package/dist/code-vault.js +8 -8
- package/dist/collision.d.ts +67 -0
- package/dist/collision.js +1 -1
- package/dist/commands/accept-terms.d.ts +11 -0
- package/dist/commands/accept-terms.js +106 -0
- package/dist/commands/balance.d.ts +7 -0
- package/dist/commands/balance.js +54 -1
- package/dist/commands/create-code-file.d.ts +24 -0
- package/dist/commands/create-code-file.js +93 -0
- package/dist/commands/create-link.d.ts +10 -0
- package/dist/commands/create-link.js +232 -0
- package/dist/commands/create.d.ts +20 -0
- package/dist/commands/create.js +43 -102
- package/dist/commands/credits.d.ts +9 -0
- package/dist/commands/credits.js +110 -0
- package/dist/commands/delete-account.d.ts +11 -0
- package/dist/commands/delete-account.js +63 -0
- package/dist/commands/deposit.d.ts +7 -0
- package/dist/commands/deposit.js +85 -0
- package/dist/commands/devices.d.ts +12 -0
- package/dist/commands/devices.js +144 -0
- package/dist/commands/documents.d.ts +35 -0
- package/dist/commands/documents.js +212 -0
- package/dist/commands/env.d.ts +5 -0
- package/dist/commands/env.js +10 -2
- package/dist/commands/erase.d.ts +13 -0
- package/dist/commands/erase.js +193 -0
- package/dist/commands/expiring.d.ts +19 -0
- package/dist/commands/extend.d.ts +33 -0
- package/dist/commands/extend.js +54 -15
- package/dist/commands/get.d.ts +18 -0
- package/dist/commands/key-manage.d.ts +11 -0
- package/dist/commands/key-manage.js +162 -0
- package/dist/commands/key.d.ts +28 -0
- package/dist/commands/key.js +249 -0
- package/dist/commands/kit.d.ts +11 -0
- package/dist/commands/kit.js +7 -7
- package/dist/commands/listfile.d.ts +13 -0
- package/dist/commands/listfile.js +12 -5
- package/dist/commands/login.d.ts +18 -0
- package/dist/commands/login.js +19 -19
- package/dist/commands/logout.d.ts +1 -0
- package/dist/commands/logout.js +1 -1
- package/dist/commands/losses.d.ts +12 -0
- package/dist/commands/losses.js +180 -0
- package/dist/commands/ls.d.ts +23 -0
- package/dist/commands/marks-dispatch.d.ts +2 -0
- package/dist/commands/marks-dispatch.js +51 -0
- package/dist/commands/marks.d.ts +46 -0
- package/dist/commands/marks.js +80 -0
- package/dist/commands/mcp.d.ts +24 -0
- package/dist/commands/mcp.js +70 -5
- package/dist/commands/mode.d.ts +8 -0
- package/dist/commands/mode.js +60 -24
- package/dist/commands/on-collision.d.ts +6 -0
- package/dist/commands/organise.d.ts +46 -0
- package/dist/commands/padding.d.ts +7 -0
- package/dist/commands/padding.js +114 -0
- package/dist/commands/public-code.d.ts +9 -0
- package/dist/commands/public-code.js +8 -8
- package/dist/commands/pull.d.ts +27 -0
- package/dist/commands/push-wallet.d.ts +19 -0
- package/dist/commands/push-wallet.js +69 -0
- package/dist/commands/push.d.ts +47 -0
- package/dist/commands/push.js +37 -7
- package/dist/commands/put-wallet.d.ts +77 -0
- package/dist/commands/put-wallet.js +316 -0
- package/dist/commands/put.d.ts +56 -0
- package/dist/commands/put.js +53 -6
- package/dist/commands/rebuild.d.ts +18 -0
- package/dist/commands/rebuild.js +108 -10
- package/dist/commands/receive.d.ts +24 -0
- package/dist/commands/recovery-dispatch.d.ts +2 -0
- package/dist/commands/recovery-dispatch.js +34 -0
- package/dist/commands/recovery-list.d.ts +11 -0
- package/dist/commands/recovery-list.js +6 -6
- package/dist/commands/recovery.d.ts +25 -0
- package/dist/commands/recovery.js +2 -2
- package/dist/commands/rollback.d.ts +9 -0
- package/dist/commands/rollback.js +118 -0
- package/dist/commands/s3.d.ts +23 -0
- package/dist/commands/s3.js +6 -6
- package/dist/commands/settings.d.ts +5 -0
- package/dist/commands/settings.js +21 -6
- package/dist/commands/share-dispatch.d.ts +2 -0
- package/dist/commands/share-dispatch.js +46 -0
- package/dist/commands/share.d.ts +14 -0
- package/dist/commands/share.js +13 -3
- package/dist/commands/shares-sent.d.ts +18 -0
- package/dist/commands/shares-sent.js +95 -0
- package/dist/commands/support.d.ts +24 -0
- package/dist/commands/support.js +321 -0
- package/dist/commands/sweep.d.ts +11 -0
- package/dist/commands/tip.d.ts +11 -0
- package/dist/commands/tip.js +86 -0
- package/dist/commands/trash.d.ts +17 -0
- package/dist/commands/trash.js +1 -1
- package/dist/commands/trial.d.ts +7 -0
- package/dist/commands/trial.js +16 -18
- package/dist/commands/unlock.d.ts +16 -0
- package/dist/commands/unlock.js +159 -0
- package/dist/commands/update.d.ts +42 -0
- package/dist/commands/update.js +1 -1
- package/dist/commands/usage.d.ts +8 -0
- package/dist/commands/verify.d.ts +18 -0
- package/dist/commands/verify.js +10 -6
- package/dist/commands/wallet-activity.d.ts +26 -0
- package/dist/commands/wallet-activity.js +117 -0
- package/dist/commands/wallet-donate.d.ts +26 -0
- package/dist/commands/wallet-donate.js +163 -0
- package/dist/commands/wallet-hall.d.ts +35 -0
- package/dist/commands/wallet-hall.js +214 -0
- package/dist/commands/wallet-send.d.ts +22 -0
- package/dist/commands/wallet-send.js +153 -0
- package/dist/commands/wallet-storage-ops.d.ts +24 -0
- package/dist/commands/wallet-storage-ops.js +215 -0
- package/dist/commands/wallet-storage.d.ts +18 -0
- package/dist/commands/wallet-storage.js +95 -0
- package/dist/commands/wallet-swap.d.ts +32 -0
- package/dist/commands/wallet-swap.js +287 -0
- package/dist/commands/wallet.d.ts +36 -0
- package/dist/commands/wallet.js +57 -15
- package/dist/commands/whoami.d.ts +10 -0
- package/dist/commands/whoami.js +24 -6
- package/dist/consent.d.ts +140 -0
- package/dist/consent.js +89 -30
- package/dist/credentials.d.ts +146 -0
- package/dist/credentials.js +10 -10
- package/dist/crypto-surface.d.ts +197 -0
- package/dist/crypto.d.ts +81 -0
- package/dist/crypto.js +13 -0
- package/dist/deposit.d.ts +25 -0
- package/dist/deposit.js +78 -0
- package/dist/download-part.d.ts +40 -0
- package/dist/download-sink.d.ts +76 -0
- package/dist/download.d.ts +79 -0
- package/dist/download.js +3 -3
- package/dist/drive-paths.d.ts +73 -0
- package/dist/environment.d.ts +62 -0
- package/dist/environment.js +29 -9
- package/dist/errors.d.ts +22 -0
- package/dist/errors.js +3 -3
- package/dist/exit.d.ts +44 -0
- package/dist/expiry.d.ts +58 -0
- package/dist/extend-budget.d.ts +35 -0
- package/dist/extend-budget.js +81 -0
- package/dist/extend-chain.d.ts +43 -0
- package/dist/extend-chain.js +40 -3
- package/dist/extend-plan.d.ts +148 -0
- package/dist/extend-plan.js +1 -1
- package/dist/gate.d.ts +18 -0
- package/dist/gate.js +100 -0
- package/dist/guards.d.ts +2 -0
- package/dist/help.d.ts +20 -0
- package/dist/help.js +199 -33
- package/dist/human-check.d.ts +23 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +49 -0
- package/dist/item-trash.d.ts +1 -0
- package/dist/kit-file.d.ts +56 -0
- package/dist/kit-file.js +10 -10
- package/dist/list-file.d.ts +88 -0
- package/dist/list-file.js +90 -7
- package/dist/list-view-find.d.ts +15 -0
- package/dist/list-view-order.d.ts +32 -0
- package/dist/main.d.ts +12 -0
- package/dist/main.js +112 -113
- package/dist/manifest-chunk-cache.d.ts +14 -0
- package/dist/manifest-chunk-cache.js +103 -0
- package/dist/manifest-chunk-flow.d.ts +81 -0
- package/dist/manifest-chunk-flow.js +307 -0
- package/dist/manifest-create.d.ts +23 -0
- package/dist/manifest-create.js +26 -30
- package/dist/manifest-write.d.ts +161 -0
- package/dist/manifest-write.js +42 -33
- package/dist/manifest.d.ts +82 -0
- package/dist/manifest.js +69 -21
- package/dist/mark-render.d.ts +26 -0
- package/dist/mcp-args.d.ts +17 -0
- package/dist/mcp-ask.d.ts +43 -0
- package/dist/mcp-ask.js +100 -0
- package/dist/mcp-tools/context.d.ts +46 -0
- package/dist/mcp-tools/credits.d.ts +3 -0
- package/dist/mcp-tools/credits.js +61 -0
- package/dist/mcp-tools/files.d.ts +3 -0
- package/dist/mcp-tools/files.js +34 -0
- package/dist/mcp-tools/organise.d.ts +3 -0
- package/dist/mcp-tools/organise.js +82 -2
- package/dist/mcp-tools/reads.d.ts +3 -0
- package/dist/mcp-tools/reads.js +132 -4
- package/dist/mcp-tools/share.d.ts +3 -0
- package/dist/mcp-tools/share.js +26 -7
- package/dist/mcp-tools/support.d.ts +3 -0
- package/dist/mcp-tools/support.js +140 -0
- package/dist/mcp-tools/tiers.d.ts +31 -0
- package/dist/mcp-tools/tiers.js +147 -0
- package/dist/mcp.d.ts +80 -0
- package/dist/mcp.js +110 -9
- package/dist/net-retry.d.ts +38 -0
- package/dist/network.d.ts +10 -0
- package/dist/notice.d.ts +7 -0
- package/dist/notice.js +7 -7
- package/dist/product.d.ts +29 -0
- package/dist/product.js +1 -1
- package/dist/progress.d.ts +37 -0
- package/dist/prompt.d.ts +31 -0
- package/dist/prompt.js +1 -1
- package/dist/rebuild-key-check.d.ts +26 -0
- package/dist/rebuild-key-check.js +90 -0
- package/dist/rebuild.d.ts +87 -0
- package/dist/rebuild.js +26 -4
- package/dist/recovery-assemble.d.ts +40 -0
- package/dist/recovery-assemble.js +1 -1
- package/dist/recovery-build.d.ts +40 -0
- package/dist/recovery-map-file.d.ts +53 -0
- package/dist/recovery-map-file.js +4 -4
- package/dist/recovery-map.d.ts +197 -0
- package/dist/recovery-release.d.ts +104 -0
- package/dist/recovery-seq.d.ts +10 -0
- package/dist/recovery-source.d.ts +50 -0
- package/dist/redact.d.ts +70 -0
- package/dist/redact.js +212 -0
- package/dist/registration.d.ts +30 -0
- package/dist/registration.js +5 -5
- package/dist/risk.d.ts +380 -0
- package/dist/risk.js +206 -0
- package/dist/run-log.d.ts +78 -0
- package/dist/run-log.js +301 -0
- package/dist/s3/listing.d.ts +47 -0
- package/dist/s3/multipart.d.ts +21 -0
- package/dist/s3/response-sink.d.ts +8 -0
- package/dist/s3/same-file.d.ts +54 -0
- package/dist/s3/server.d.ts +58 -0
- package/dist/s3/server.js +2 -2
- package/dist/s3/sigv4.d.ts +46 -0
- package/dist/s3/sigv4.js +1 -1
- package/dist/s3/staging.d.ts +10 -0
- package/dist/s3/xml.d.ts +28 -0
- package/dist/safe-path.d.ts +41 -0
- package/dist/seal.d.ts +120 -0
- package/dist/secret-reader.d.ts +28 -0
- package/dist/server.d.ts +9 -0
- package/dist/session.d.ts +32 -0
- package/dist/session.js +1 -1
- package/dist/setup-questions.d.ts +7 -0
- package/dist/share.d.ts +108 -0
- package/dist/share.js +1 -1
- package/dist/shared/lib/crypto/size-padding.d.ts +55 -0
- package/dist/shared/lib/crypto/size-padding.js +5 -0
- package/dist/shared/lib/drive/manifest-chunks.d.ts +95 -0
- package/dist/shared/lib/drive/manifest-chunks.js +252 -0
- package/dist/shared/lib/drive/manifest-codec.d.ts +214 -0
- package/dist/shared/lib/drive/manifest-codec.js +7 -7
- package/dist/shared/lib/drive/manifest-index.d.ts +98 -0
- package/dist/shared/lib/drive/manifest-ops.d.ts +167 -0
- package/dist/shared/lib/drive/manifest-ops.js +9 -4
- package/dist/shared/lib/drive/manifest-pack.d.ts +65 -0
- package/dist/shared/lib/drive/manifest-pack.js +287 -0
- package/dist/shared/lib/drive/manifest-settings.d.ts +113 -0
- package/dist/shared/lib/drive/manifest-settings.js +87 -6
- package/dist/shared/lib/drive/name-conflict.d.ts +76 -0
- package/dist/shared/lib/drive/rebuild-verify.d.ts +73 -0
- package/dist/shared/lib/drive/rebuild-verify.js +112 -0
- package/dist/shared/lib/drive/unique-name.d.ts +9 -0
- package/dist/shared/lib/drive/zstd.d.ts +27 -0
- package/dist/shared/lib/drive/zstd.js +74 -0
- package/dist/shared/lib/extend/epochs.d.ts +205 -0
- package/dist/shared/lib/net/retry-budget.d.ts +79 -0
- package/dist/shared/lib/share/shared-file-info.d.ts +21 -0
- package/dist/shared/lib/storage-control/chain.d.ts +57 -0
- package/dist/shared/lib/storage-control/chain.js +82 -0
- package/dist/shared/lib/storage-control/plan.d.ts +40 -0
- package/dist/shared/lib/storage-control/plan.js +72 -0
- package/dist/shared/lib/storage-control/reuse.d.ts +72 -0
- package/dist/shared/lib/storage-control/reuse.js +95 -0
- package/dist/shared/lib/storage-network.d.ts +25 -0
- package/dist/shared/lib/upload/part-plan.d.ts +24 -0
- package/dist/shared/lib/wallet/activity.d.ts +138 -0
- package/dist/shared/lib/wallet/activity.js +208 -0
- package/dist/shared/lib/wallet/send-rules.d.ts +68 -0
- package/dist/shared/lib/wallet/send-rules.js +116 -0
- package/dist/shared/lib/wallet/swap-rules.d.ts +115 -0
- package/dist/shared/lib/wallet/swap-rules.js +199 -0
- package/dist/shared/lib/wallet/tip.d.ts +14 -0
- package/dist/shared/lib/wallet/tip.js +34 -0
- package/dist/shared/lib/wallet/venue-ids.d.ts +23 -0
- package/dist/shared/lib/wallet/venue-ids.js +53 -0
- package/dist/standing-tip.d.ts +24 -0
- package/dist/standing-tip.js +52 -0
- package/dist/stdout.d.ts +38 -0
- package/dist/storage-control-chain.d.ts +38 -0
- package/dist/storage-control-chain.js +78 -0
- package/dist/sui-rpc.d.ts +15 -0
- package/dist/sui-rpc.js +59 -0
- package/dist/support-attach.d.ts +24 -0
- package/dist/support-attach.js +72 -0
- package/dist/support-copy.d.ts +42 -0
- package/dist/support-copy.js +109 -0
- package/dist/support-wire.d.ts +49 -0
- package/dist/support-wire.js +194 -0
- package/dist/trash-sweep.d.ts +55 -0
- package/dist/units.d.ts +2 -0
- package/dist/update-check.d.ts +50 -0
- package/dist/update-source.d.ts +63 -0
- package/dist/upload-api.d.ts +3 -0
- package/dist/upload-file.d.ts +97 -0
- package/dist/upload-file.js +3 -2
- package/dist/upload-price.d.ts +50 -0
- package/dist/upload-steps.d.ts +26 -0
- package/dist/upload-steps.js +29 -9
- package/dist/upload-store.d.ts +182 -0
- package/dist/upload-wallet-chain.d.ts +54 -0
- package/dist/upload-wallet-chain.js +203 -0
- package/dist/upload-wallet-plan.d.ts +128 -0
- package/dist/upload-wallet-plan.js +184 -0
- package/dist/upload-wallet.d.ts +24 -0
- package/dist/upload-wallet.js +208 -0
- package/dist/upload-wire.d.ts +244 -0
- package/dist/upload.d.ts +18 -0
- package/dist/upload.js +10 -0
- package/dist/usage-report.d.ts +35 -0
- package/dist/wallet-activity-chain.d.ts +5 -0
- package/dist/wallet-activity-chain.js +31 -0
- package/dist/wallet-chain.d.ts +19 -0
- package/dist/wallet-chain.js +2 -2
- package/dist/wallet-grant.d.ts +50 -0
- package/dist/wallet-grant.js +218 -0
- package/dist/wallet-send-chain.d.ts +24 -0
- package/dist/wallet-send-chain.js +49 -0
- package/dist/wallet-sign.d.ts +133 -0
- package/dist/wallet-sign.js +317 -0
- package/dist/wallet-storage-chain.d.ts +3 -0
- package/dist/wallet-storage-chain.js +20 -0
- package/dist/wallet-swap-chain.d.ts +52 -0
- package/dist/wallet-swap-chain.js +186 -0
- package/dist/wallet-swap-quote.d.ts +22 -0
- package/dist/wallet-swap-quote.js +126 -0
- package/dist/wallet.d.ts +135 -0
- package/dist/wallet.js +5 -5
- package/dist/walrus-write.d.ts +56 -0
- package/dist/walrus-write.js +5 -4
- package/dist/walrus.d.ts +86 -0
- package/dist/walrus.js +48 -10
- package/dist/zstd-node.d.ts +11 -0
- package/dist/zstd-node.js +71 -0
- package/docs/commands/accept-terms.md +15 -0
- package/docs/commands/create.md +17 -0
- package/docs/commands/credits.md +34 -0
- package/docs/commands/delete-account.md +14 -0
- package/docs/commands/deposit.md +27 -0
- package/docs/commands/devices.md +9 -0
- package/docs/commands/documents.md +10 -0
- package/docs/commands/env.md +21 -0
- package/docs/commands/extend.md +27 -0
- package/docs/commands/folders.md +10 -0
- package/docs/commands/get.md +14 -0
- package/docs/commands/key.md +14 -0
- package/docs/commands/login.md +23 -0
- package/docs/commands/logout.md +11 -0
- package/docs/commands/losses.md +13 -0
- package/docs/commands/ls.md +16 -0
- package/docs/commands/marks.md +9 -0
- package/docs/commands/mcp.md +33 -0
- package/docs/commands/mode.md +29 -0
- package/docs/commands/on-collision.md +13 -0
- package/docs/commands/padding.md +13 -0
- package/docs/commands/public-code.md +9 -0
- package/docs/commands/pull.md +9 -0
- package/docs/commands/push.md +12 -0
- package/docs/commands/put.md +36 -0
- package/docs/commands/rebuild.md +13 -0
- package/docs/commands/recovery.md +18 -0
- package/docs/commands/s3.md +17 -0
- package/docs/commands/share.md +15 -0
- package/docs/commands/support.md +16 -0
- package/docs/commands/tip.md +21 -0
- package/docs/commands/trash.md +25 -0
- package/docs/commands/trial.md +12 -0
- package/docs/commands/unlock.md +25 -0
- package/docs/commands/update.md +14 -0
- package/docs/commands/usage.md +14 -0
- package/docs/commands/verify.md +18 -0
- package/docs/commands/wallet.md +67 -0
- package/docs/commands/whoami.md +12 -0
- package/package.json +67 -8
- package/server.json +28 -0
- package/vendor/nmts-crypto/nmts_crypto_wasm.d.ts +23 -2
- package/vendor/nmts-crypto/nmts_crypto_wasm.js +44 -2
- package/vendor/nmts-crypto/nmts_crypto_wasm_bg.wasm +0 -0
- package/vendor/nmts-crypto/nmts_crypto_wasm_bg.wasm.d.ts +1 -0
- package/vendor/nmts-crypto/package.json +1 -1
- package/dist/commands/consent.js +0 -77
- package/dist/extend-sign.js +0 -130
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// What a WALLET-PAID upload would buy, worked out before anything is sealed or signed.
|
|
2
|
+
//
|
|
3
|
+
// ⛔ NO NETWORK, NO SDK, NO KEY. Everything here is arithmetic over numbers somebody else read, so
|
|
4
|
+
// `node --test` drives every branch — the shortfalls, the resource that does not fit, the length
|
|
5
|
+
// the network will not sell. The reads live in `upload-wallet-chain.ts`, the signatures in
|
|
6
|
+
// `wallet-sign.ts`, and neither can be reached from here.
|
|
7
|
+
//
|
|
8
|
+
// ⛔ THE PRICE HAS THREE PARTS AND THE REVIEW SAYS ALL THREE. Storage and the write are WAL; the
|
|
9
|
+
// relay's tip and the chain fee are SUI. The browser's payment sheet itemises exactly these, and
|
|
10
|
+
// a person comparing the two programs should see the same numbers under the same names.
|
|
11
|
+
//
|
|
12
|
+
// ⛔ AN UNREAD BALANCE IS NOT A SHORTFALL — the rule `extend-budget.ts` states, kept here where it
|
|
13
|
+
// costs the most: a zero would refuse an upload the wallet can afford.
|
|
14
|
+
import { NmtsError } from "./errors.js";
|
|
15
|
+
import { BINARY_NAME } from "./product.js";
|
|
16
|
+
import { fits, leftoverBytes, statusOf, usableFirst } from "./shared/lib/storage-control/plan.js";
|
|
17
|
+
import { coinAmount } from "./wallet.js";
|
|
18
|
+
/** The browser's default rung, and the term one credit buys. A number of EPOCHS, not days. */
|
|
19
|
+
export const DEFAULT_UPLOAD_EPOCHS = 2;
|
|
20
|
+
/**
|
|
21
|
+
* How many epochs to buy: what was asked for, or the browser's default, held against the ceiling.
|
|
22
|
+
*
|
|
23
|
+
* ⛔ IT REFUSES RATHER THAN CLAMPS, for the reason `extend-plan.ts` gives: quietly buying fewer
|
|
24
|
+
* epochs spends money on something nobody asked for.
|
|
25
|
+
*/
|
|
26
|
+
export function chooseUploadEpochs(asked, window) {
|
|
27
|
+
const epochs = asked === undefined ? DEFAULT_UPLOAD_EPOCHS : typeof asked === "number" ? asked : Number(asked.trim());
|
|
28
|
+
if (!Number.isSafeInteger(epochs) || epochs <= 0) {
|
|
29
|
+
throw new NmtsError(`A storage term is a whole number of epochs: ${String(asked)}.`, {
|
|
30
|
+
exitCode: 2,
|
|
31
|
+
nextStep: `Nothing was signed. \`--epochs 4\` buys four of the storage network's epochs — ${daysOf(window, 1)} each.`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
if (epochs > window.maxAhead) {
|
|
35
|
+
throw new NmtsError(`The storage network sells at most ${window.maxAhead} epochs ahead.`, {
|
|
36
|
+
exitCode: 4,
|
|
37
|
+
nextStep: `Nothing was signed. Ask for ${window.maxAhead} or fewer: \`${BINARY_NAME} put <file> --pay wallet --epochs ${window.maxAhead}\`.`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return epochs;
|
|
41
|
+
}
|
|
42
|
+
/** A number of epochs as days, read from the network's own clock. */
|
|
43
|
+
export function daysOf(window, epochs) {
|
|
44
|
+
const days = (epochs * window.clock.durationMs) / 86_400_000;
|
|
45
|
+
const rounded = Number.isInteger(days) ? String(days) : days.toFixed(1);
|
|
46
|
+
return `${rounded} day${days === 1 ? "" : "s"}`;
|
|
47
|
+
}
|
|
48
|
+
export function parseStorageAsk(text) {
|
|
49
|
+
if (text === undefined || text === "")
|
|
50
|
+
return null;
|
|
51
|
+
if (text === "fit" || text === "whole")
|
|
52
|
+
return { mode: text };
|
|
53
|
+
if (/^0x[0-9a-fA-F]{64}$/.test(text))
|
|
54
|
+
return { mode: "object", objectId: text.toLowerCase() };
|
|
55
|
+
throw new NmtsError(`--storage takes fit, whole, or a storage resource's object id, not "${text}".`, {
|
|
56
|
+
exitCode: 2,
|
|
57
|
+
nextStep: `\`${BINARY_NAME} wallet storage\` lists the resources this wallet holds.`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Which held resource supplies this part's storage, and how much of it.
|
|
62
|
+
*
|
|
63
|
+
* ⛔ THE LEFTOVER IS A NUMBER THE PERSON SEES — the owner's rule: registration binds the WHOLE resource,
|
|
64
|
+
* so `whole` says how many bytes go in with the file, and `fit` cuts first and says what stays
|
|
65
|
+
* free. `fit` and `whole` pick the smallest usable resource that fits; an object id names one.
|
|
66
|
+
*/
|
|
67
|
+
export function pickResource(ask, resources, encodedBytes, currentEpoch, needUntilEpoch, writeFrost) {
|
|
68
|
+
const usable = usableFirst(resources, currentEpoch).filter((r) => statusOf(r, currentEpoch) === "usable");
|
|
69
|
+
let chosen;
|
|
70
|
+
if (ask.mode === "object") {
|
|
71
|
+
chosen = resources.find((r) => r.objectId === ask.objectId);
|
|
72
|
+
if (chosen === undefined) {
|
|
73
|
+
throw new NmtsError(`This wallet holds no free storage resource ${ask.objectId}.`, {
|
|
74
|
+
exitCode: 4,
|
|
75
|
+
nextStep: `Nothing was signed. \`${BINARY_NAME} wallet storage\` lists what it holds.`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (!fits(chosen, encodedBytes, needUntilEpoch) || statusOf(chosen, currentEpoch) !== "usable") {
|
|
79
|
+
throw new NmtsError(`Resource ${ask.objectId} cannot hold this part until epoch ${needUntilEpoch}.`, {
|
|
80
|
+
exitCode: 4,
|
|
81
|
+
nextStep: `Nothing was signed. It holds ${chosen.sizeBytes} bytes from epoch ${chosen.startEpoch} to ` +
|
|
82
|
+
`${chosen.endEpoch}; this part is ${encodedBytes} bytes once encoded. Leave --storage off to buy new storage.`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// Smallest first among those that fit: the one that wastes least when bound whole.
|
|
88
|
+
chosen = usable.filter((r) => fits(r, encodedBytes, needUntilEpoch)).sort((a, b) => a.sizeBytes - b.sizeBytes)[0];
|
|
89
|
+
if (chosen === undefined) {
|
|
90
|
+
throw new NmtsError(`No free storage resource in this wallet holds ${encodedBytes} encoded bytes until epoch ${needUntilEpoch}.`, {
|
|
91
|
+
exitCode: 4,
|
|
92
|
+
nextStep: `Nothing was signed. \`${BINARY_NAME} wallet storage\` lists what it holds; leave --storage off ` +
|
|
93
|
+
`to buy new storage, or ask for fewer epochs.`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const cut = ask.mode === "fit";
|
|
98
|
+
return {
|
|
99
|
+
kind: "reuse",
|
|
100
|
+
objectId: chosen.objectId,
|
|
101
|
+
cutToBytes: cut ? encodedBytes : null,
|
|
102
|
+
writeFrost,
|
|
103
|
+
leftoverBytes: leftoverBytes(chosen, encodedBytes),
|
|
104
|
+
resource: chosen,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function held(coin) {
|
|
108
|
+
return coin.read ? coin.baseUnits : null;
|
|
109
|
+
}
|
|
110
|
+
export function uploadBudget(input) {
|
|
111
|
+
const { address, purse, feeMist, quotes, storage } = input;
|
|
112
|
+
const walNeededFrost = walPrice(quotes, storage);
|
|
113
|
+
const tips = quotes.reduce((sum, q) => sum + q.tipMist, 0n);
|
|
114
|
+
const suiNeededMist = tips + (feeMist ?? 0n) * BigInt(quotes.length);
|
|
115
|
+
const walFrost = held(purse.wal);
|
|
116
|
+
const suiMist = held(purse.sui);
|
|
117
|
+
const unread = [];
|
|
118
|
+
if (!purse.wal.read)
|
|
119
|
+
unread.push(`WAL: ${purse.wal.why}`);
|
|
120
|
+
if (!purse.sui.read)
|
|
121
|
+
unread.push(`SUI: ${purse.sui.why}`);
|
|
122
|
+
let shortfall = null;
|
|
123
|
+
if (walFrost !== null && walFrost < walNeededFrost) {
|
|
124
|
+
shortfall = `The wallet holds ${coinAmount(walFrost)} WAL and this upload costs ${coinAmount(walNeededFrost)} WAL.`;
|
|
125
|
+
}
|
|
126
|
+
else if (suiMist !== null && suiMist < suiNeededMist) {
|
|
127
|
+
shortfall =
|
|
128
|
+
`The wallet holds ${coinAmount(suiMist)} SUI and this upload needs about ${coinAmount(suiNeededMist)} SUI ` +
|
|
129
|
+
`for the relay tip and the chain fees.`;
|
|
130
|
+
}
|
|
131
|
+
return { address, walNeededFrost, suiNeededMist, walFrost, suiMist, feeMist, unread, shortfall };
|
|
132
|
+
}
|
|
133
|
+
/** WAL for the whole file: storage and write when buying, the write alone on a held resource. */
|
|
134
|
+
export function walPrice(quotes, storage) {
|
|
135
|
+
return quotes.reduce((sum, q) => sum + q.writeFrost + (storage.kind === "buy" ? q.storageFrost : 0n), 0n);
|
|
136
|
+
}
|
|
137
|
+
/** The next step when the wallet is short: where to send what, said once. */
|
|
138
|
+
export function uploadShortfallNextStep(b) {
|
|
139
|
+
const coin = b.walFrost !== null && b.walFrost < b.walNeededFrost ? "WAL" : "SUI";
|
|
140
|
+
return (`Nothing was signed and nothing was sent. Send ${coin} to ${b.address} — ` +
|
|
141
|
+
`\`${BINARY_NAME} wallet\` shows the address and both balances — and run this again.`);
|
|
142
|
+
}
|
|
143
|
+
/** The numbers, for a person, in the order somebody deciding needs them. */
|
|
144
|
+
export function describeUploadReview(say, facts, budget) {
|
|
145
|
+
say(`${facts.name} ${facts.bytes} bytes → ${coinAmount(facts.walNeeded)} WAL from the wallet`);
|
|
146
|
+
if (facts.parts > 1)
|
|
147
|
+
say(` in ${facts.parts} parts — each one is registered and certified with its own signatures`);
|
|
148
|
+
say(` Stored for ${facts.epochs} epoch${facts.epochs === 1 ? "" : "s"} — ${facts.days} — until epoch ${facts.endEpoch}.`);
|
|
149
|
+
if (facts.storage.kind === "reuse") {
|
|
150
|
+
const r = facts.storage.resource;
|
|
151
|
+
say(` Storage comes from resource ${r.objectId} (${r.sizeBytes} bytes, until epoch ${r.endEpoch}); the WAL above is the write alone.`);
|
|
152
|
+
say(facts.storage.cutToBytes === null
|
|
153
|
+
? ` Used whole: ${facts.storage.leftoverBytes} bytes beyond this file are bound with it and cannot be taken back.`
|
|
154
|
+
: ` Cut to fit: ${facts.storage.leftoverBytes} bytes stay free as a resource of their own.`);
|
|
155
|
+
}
|
|
156
|
+
else if (facts.heldResources !== null && facts.heldResources > 0) {
|
|
157
|
+
say(` This wallet also holds ${facts.heldResources} free storage resource${facts.heldResources === 1 ? "" : "s"} — \`${BINARY_NAME} wallet storage\` lists them; --storage fit or whole tries one.`);
|
|
158
|
+
}
|
|
159
|
+
say(` Relay tip ${coinAmount(facts.tipMist)} SUI, paid inside the register signature${facts.parts > 1 ? "s" : ""}.`);
|
|
160
|
+
if (budget.feeMist === null) {
|
|
161
|
+
say(` The chain fee (SUI) of a register signature could not be measured just now; it is charged with each signature.`);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
say(` Chain fee about ${coinAmount(budget.feeMist)} SUI per register signature, measured by a dry run just now; ` +
|
|
165
|
+
`each certify signature pays its own smaller fee on top.`);
|
|
166
|
+
}
|
|
167
|
+
const wal = budget.walFrost === null ? "WAL not read" : `${coinAmount(budget.walFrost)} WAL`;
|
|
168
|
+
const sui = budget.suiMist === null ? "SUI not read" : `${coinAmount(budget.suiMist)} SUI`;
|
|
169
|
+
say(` Wallet ${budget.address} holds ${wal} and ${sui}.`);
|
|
170
|
+
if (budget.walFrost !== null && budget.suiMist !== null) {
|
|
171
|
+
const walAfter = budget.walFrost - budget.walNeededFrost;
|
|
172
|
+
const suiAfter = budget.suiMist - budget.suiNeededMist;
|
|
173
|
+
say(` Afterwards it holds about ${coinAmount(walAfter < 0n ? 0n : walAfter)} WAL and ${coinAmount(suiAfter < 0n ? 0n : suiAfter)} SUI.`);
|
|
174
|
+
}
|
|
175
|
+
for (const why of budget.unread) {
|
|
176
|
+
say(` A balance could not be read — ${why}. That is not zero: the chain decides at signing.`);
|
|
177
|
+
}
|
|
178
|
+
say(``);
|
|
179
|
+
say(` This is paid in WAL and SUI from the wallet this NMTS key derives — not from credits, which`);
|
|
180
|
+
say(` is what \`${BINARY_NAME} put\` spends without --pay wallet. Nobody, NMTS included, can reverse it.`);
|
|
181
|
+
say(` The storage is the wallet's own: NMTS records the file and does not hold it. This upload does`);
|
|
182
|
+
say(` not carry the recovery list's storage-network copy, whatever the account's switch says —`);
|
|
183
|
+
say(` only the browser's small-file uploads do.`);
|
|
184
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Network } from "./network.ts";
|
|
2
|
+
import type { PartQuote, StorageChoice } from "./upload-wallet-plan.ts";
|
|
3
|
+
import { type PaidPart, type UploadInput } from "./upload-wire.ts";
|
|
4
|
+
import type { Spend } from "./wallet-grant.ts";
|
|
5
|
+
import type { SignBlobCertify, SignBlobRegister } from "./wallet-sign.ts";
|
|
6
|
+
export interface WalletRailContext {
|
|
7
|
+
network: Network;
|
|
8
|
+
/** ⛔ The NMTS key. Held for the signatures and never written anywhere. */
|
|
9
|
+
code: string;
|
|
10
|
+
relayUrl: string;
|
|
11
|
+
epochs: number;
|
|
12
|
+
/** Where the storage comes from. A held resource serves one blob, so it applies to a one-part file. */
|
|
13
|
+
storage: StorageChoice;
|
|
14
|
+
/** What each part was quoted, in part order — what the grant ledger is told after each signature. */
|
|
15
|
+
quotes: readonly PartQuote[];
|
|
16
|
+
/** The measured register fee, added to the ledger with the tip; null adds the tip alone. */
|
|
17
|
+
feeMist: bigint | null;
|
|
18
|
+
signRegister: SignBlobRegister;
|
|
19
|
+
signCertify: SignBlobCertify;
|
|
20
|
+
/** Told what left the wallet, after each signature. */
|
|
21
|
+
onSpend: (spend: Spend) => void;
|
|
22
|
+
}
|
|
23
|
+
/** The rail: what `uploadFile` calls once per part. */
|
|
24
|
+
export declare function walletRail(ctx: WalletRailContext): (input: UploadInput) => Promise<PaidPart>;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// The WALLET rail for ONE part: register (signature one), push the bytes at the relay, certify
|
|
2
|
+
// (signature two) — with the same written-down records the credit rail keeps.
|
|
3
|
+
//
|
|
4
|
+
// ⛔ THE ORDER IS WHAT STANDS BETWEEN AN INTERRUPTION AND MONEY THAT BOUGHT NOTHING, as in
|
|
5
|
+
// `upload.ts`:
|
|
6
|
+
//
|
|
7
|
+
// encode → WRITE IT DOWN → sign register (WAL and the tip leave here) → write it down again
|
|
8
|
+
// → relay → sign certify (gas) → write it down again … once per part …
|
|
9
|
+
//
|
|
10
|
+
// Before the signature, so a retry has the sealed bytes and the tip nonce that reproduce the
|
|
11
|
+
// blob the registration named. After it, so the transaction the relay checks its tip in, and
|
|
12
|
+
// the blob object the certification names, are not lost with the process.
|
|
13
|
+
//
|
|
14
|
+
// ⛔ NOTHING IS SIGNED TWICE FOR ONE PART. A record carrying a register digest skips the register;
|
|
15
|
+
// one carrying a certify digest skips everything. That is what makes running the same command
|
|
16
|
+
// again cost nothing more — the same promise the credit rail makes with a reservation.
|
|
17
|
+
//
|
|
18
|
+
// ⛔ THE SIGNATURES ARE A SEAM. `wallet-sign.ts` is loaded by the command, after the agreement,
|
|
19
|
+
// and handed in; the tests hand in recorders and prove that `--dry-run` and a shortfall never
|
|
20
|
+
// reach them.
|
|
21
|
+
import { NmtsError } from "./errors.js";
|
|
22
|
+
import { readReservationBytes, readReservationRecord, writeReservation } from "./upload-store.js";
|
|
23
|
+
import { UploadError } from "./upload-wire.js";
|
|
24
|
+
function why(error) {
|
|
25
|
+
return error instanceof Error ? error.message : String(error);
|
|
26
|
+
}
|
|
27
|
+
/** The rail: what `uploadFile` calls once per part. */
|
|
28
|
+
export function walletRail(ctx) {
|
|
29
|
+
return (input) => buyAndPushPartWithWallet(ctx, input);
|
|
30
|
+
}
|
|
31
|
+
async function buyAndPushPartWithWallet(ctx, input) {
|
|
32
|
+
const { protocol, key, sealed, onStep } = input;
|
|
33
|
+
const existing = readReservationRecord(key);
|
|
34
|
+
if (existing !== null && existing.paidFrom !== "wallet") {
|
|
35
|
+
throw new UploadError({
|
|
36
|
+
phase: "reserve",
|
|
37
|
+
message: "This upload was started with credits paying, and this run would pay from the wallet.",
|
|
38
|
+
paid: existing.ledgerId !== undefined,
|
|
39
|
+
nextStep: "Run it again without --pay wallet to finish it, or move the records in the uploads " +
|
|
40
|
+
"directory aside to start over. Nothing was sent.",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (existing !== null && (existing.partIndex !== input.part.index || existing.partTotal !== input.part.total)) {
|
|
44
|
+
throw new UploadError({
|
|
45
|
+
phase: "reserve",
|
|
46
|
+
message: `This upload was started as part ${existing.partIndex + 1} of ${existing.partTotal} and this ` +
|
|
47
|
+
`run is treating it as part ${input.part.index + 1} of ${input.part.total}.`,
|
|
48
|
+
paid: existing.registerTxDigest !== undefined,
|
|
49
|
+
nextStep: "Run it again with the part size the first attempt used, or move the records in the " +
|
|
50
|
+
"uploads directory aside to start over. Nothing was sent.",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// ── already certified: nothing left to sign or send ──
|
|
54
|
+
if (existing?.certifyTxDigest !== undefined) {
|
|
55
|
+
onStep?.({ step: "resuming", ledgerId: 0, state: "certified" });
|
|
56
|
+
return paidPart(existing, true);
|
|
57
|
+
}
|
|
58
|
+
// ── registered by an earlier run: push and certify, sign nothing twice ──
|
|
59
|
+
if (existing?.registerTxDigest !== undefined && existing.blobObjectId !== undefined) {
|
|
60
|
+
onStep?.({ step: "resuming", ledgerId: 0, state: "registered" });
|
|
61
|
+
const certificate = await push(input, existing, existing.registerTxDigest, existing.blobObjectId, readReservationBytes(key));
|
|
62
|
+
await certify(ctx, input, key, existing, certificate);
|
|
63
|
+
return paidPart(readReservationRecord(key) ?? existing, true);
|
|
64
|
+
}
|
|
65
|
+
// ── fresh, or interrupted before the signature ──
|
|
66
|
+
onStep?.({ step: "encoding", bytes: sealed.length });
|
|
67
|
+
let meta;
|
|
68
|
+
try {
|
|
69
|
+
meta = await protocol.computeMetadata({
|
|
70
|
+
bytes: sealed,
|
|
71
|
+
nonce: existing ? new Uint8Array(Buffer.from(existing.nonceB64, "base64url")) : undefined,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
throw new UploadError({
|
|
76
|
+
phase: "encoding",
|
|
77
|
+
message: `Could not prepare this file for the storage network: ${why(error)}`,
|
|
78
|
+
paid: false,
|
|
79
|
+
nextStep: "Nothing was sent and nothing was signed.",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const record = {
|
|
83
|
+
attempt: existing?.attempt ?? 0,
|
|
84
|
+
paidFrom: "wallet",
|
|
85
|
+
blobId: meta.blobId,
|
|
86
|
+
nonceB64: Buffer.from(meta.nonce).toString("base64url"),
|
|
87
|
+
rootHashB64: Buffer.from(meta.rootHash).toString("base64url"),
|
|
88
|
+
relayUrl: input.relayUrl,
|
|
89
|
+
epochs: input.epochs,
|
|
90
|
+
sealedLen: sealed.length,
|
|
91
|
+
plaintextLen: input.entry.plaintextLen,
|
|
92
|
+
partPlaintextLen: input.part.plaintextLen,
|
|
93
|
+
partIndex: input.part.index,
|
|
94
|
+
partTotal: input.part.total,
|
|
95
|
+
dekWrapped: input.entry.dekWrapped,
|
|
96
|
+
contentHashCt: input.entry.contentHashCt,
|
|
97
|
+
name: input.entry.name,
|
|
98
|
+
parentId: input.entry.parentId,
|
|
99
|
+
};
|
|
100
|
+
// ⛔ BEFORE THE SIGNATURE. See the module header.
|
|
101
|
+
writeReservation(key, record, sealed);
|
|
102
|
+
onStep?.({ step: "reserving" });
|
|
103
|
+
const quote = ctx.quotes[input.part.index];
|
|
104
|
+
if (quote === undefined)
|
|
105
|
+
throw new NmtsError("unreachable: a part with no quote");
|
|
106
|
+
let registered;
|
|
107
|
+
try {
|
|
108
|
+
registered = await ctx.signRegister({
|
|
109
|
+
network: ctx.network,
|
|
110
|
+
code: ctx.code,
|
|
111
|
+
relayUrl: input.relayUrl,
|
|
112
|
+
epochs: ctx.epochs,
|
|
113
|
+
storage: ctx.storage.kind === "buy" ? { kind: "buy" } : ctx.storage,
|
|
114
|
+
part: { sealedLen: sealed.length, blobId: meta.blobId, rootHash: meta.rootHash, nonce: meta.nonce, blobDigest: meta.blobDigest },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
throw new UploadError({
|
|
119
|
+
phase: "reserve",
|
|
120
|
+
message: why(error),
|
|
121
|
+
// ⛔ A refusal did not spend more than gas. A request that never got an answer MIGHT have
|
|
122
|
+
// registered — the record is on disk either way, and the next run reads the chain's
|
|
123
|
+
// answer through the same signature path rather than guessing here.
|
|
124
|
+
paid: false,
|
|
125
|
+
nextStep: error instanceof NmtsError && error.nextStep !== null
|
|
126
|
+
? error.nextStep
|
|
127
|
+
: "If the transaction reached the chain the storage may be bought; running the same " +
|
|
128
|
+
"command again finds the record and does not sign a second registration for a part " +
|
|
129
|
+
"that has one.",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
record.registerTxDigest = registered.digest;
|
|
133
|
+
record.blobObjectId = registered.blobObjectId;
|
|
134
|
+
record.endEpoch = registered.endEpoch;
|
|
135
|
+
writeReservation(key, record, sealed);
|
|
136
|
+
ctx.onSpend({
|
|
137
|
+
walFrost: quote.writeFrost + (ctx.storage.kind === "buy" ? quote.storageFrost : 0n),
|
|
138
|
+
suiMist: quote.tipMist + (ctx.feeMist ?? 0n),
|
|
139
|
+
});
|
|
140
|
+
const certificate = await push(input, record, registered.digest, registered.blobObjectId, sealed);
|
|
141
|
+
await certify(ctx, input, key, record, certificate);
|
|
142
|
+
return paidPart(readReservationRecord(key) ?? record, false);
|
|
143
|
+
}
|
|
144
|
+
/** The bytes to the relay named in the register transaction. The storage is already bought. */
|
|
145
|
+
async function push(input, record, registerTxDigest, blobObjectId, sealed) {
|
|
146
|
+
input.onStep?.({ step: "uploading", relayUrl: record.relayUrl, bytes: sealed.length });
|
|
147
|
+
try {
|
|
148
|
+
return await input.protocol.uploadToRelay({
|
|
149
|
+
blobId: record.blobId,
|
|
150
|
+
bytes: sealed,
|
|
151
|
+
nonce: new Uint8Array(Buffer.from(record.nonceB64, "base64url")),
|
|
152
|
+
registerTxDigest,
|
|
153
|
+
blobObjectId,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
throw new UploadError({
|
|
158
|
+
phase: "uploading",
|
|
159
|
+
message: `Uploading to the storage network failed: ${why(error)}`,
|
|
160
|
+
paid: true,
|
|
161
|
+
nextStep: "The storage is already bought and the tip is paid. Running the same command again pushes " +
|
|
162
|
+
"the same bytes to the same relay and signs nothing more for it.",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** The second signature. Gas only. Written down before returning so a rerun never certifies twice. */
|
|
167
|
+
async function certify(ctx, input, key, record, certificate) {
|
|
168
|
+
if (record.blobObjectId === undefined)
|
|
169
|
+
throw new NmtsError("unreachable: certifying a part with no blob object");
|
|
170
|
+
input.onStep?.({ step: "certifying" });
|
|
171
|
+
let digest;
|
|
172
|
+
try {
|
|
173
|
+
digest = await ctx.signCertify({
|
|
174
|
+
network: ctx.network,
|
|
175
|
+
code: ctx.code,
|
|
176
|
+
relayUrl: record.relayUrl,
|
|
177
|
+
blobId: record.blobId,
|
|
178
|
+
blobObjectId: record.blobObjectId,
|
|
179
|
+
certificate,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
throw new UploadError({
|
|
184
|
+
phase: "certify",
|
|
185
|
+
message: why(error),
|
|
186
|
+
paid: true,
|
|
187
|
+
nextStep: error instanceof NmtsError && error.nextStep !== null
|
|
188
|
+
? error.nextStep
|
|
189
|
+
: "The bytes are on the network and the storage is bought. Running the same command again " +
|
|
190
|
+
"certifies again, which costs gas and nothing else.",
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
writeReservation(key, { ...record, certifyTxDigest: digest }, readReservationBytes(key));
|
|
194
|
+
// The certify fee is not measured beforehand, so the ledger is told about the tip and the
|
|
195
|
+
// register fee only — the review said so.
|
|
196
|
+
}
|
|
197
|
+
/** What the commit needs about a finished part — off the RECORD, never off the run. */
|
|
198
|
+
function paidPart(record, resumed) {
|
|
199
|
+
return {
|
|
200
|
+
partIndex: record.partIndex,
|
|
201
|
+
ledgerId: null,
|
|
202
|
+
blobId: record.blobId,
|
|
203
|
+
sealedLen: record.sealedLen,
|
|
204
|
+
resumed,
|
|
205
|
+
...(record.blobObjectId !== undefined ? { suiObjectId: record.blobObjectId } : {}),
|
|
206
|
+
...(record.endEpoch !== undefined ? { endEpoch: record.endEpoch } : {}),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { NmtsError } from "./errors.ts";
|
|
2
|
+
/** What the storage network's encoder says about one sealed blob, before anything is bought. */
|
|
3
|
+
export interface BlobMeta {
|
|
4
|
+
/** The network's own id for these bytes. Deterministic in (bytes, nonce). */
|
|
5
|
+
blobId: string;
|
|
6
|
+
/** Merkle root of the encoded blob. Handed to the chain service verbatim. */
|
|
7
|
+
rootHash: Uint8Array;
|
|
8
|
+
/** The relay tip nonce. Random on a first encode, re-fed on a retry so the digest repeats. */
|
|
9
|
+
nonce: Uint8Array;
|
|
10
|
+
/** What the relay checks the paid tip against. */
|
|
11
|
+
blobDigest: Uint8Array;
|
|
12
|
+
}
|
|
13
|
+
/** What the storage nodes signed. Passed to the server unread — this tool never interprets it. */
|
|
14
|
+
export interface Certificate {
|
|
15
|
+
signers: number[];
|
|
16
|
+
serialized_message_b64: string;
|
|
17
|
+
signature_b64: string;
|
|
18
|
+
}
|
|
19
|
+
/** The Walrus protocol seam. The real one is in `walrus-write.ts`; the tests supply a fake. */
|
|
20
|
+
export interface BlobProtocol {
|
|
21
|
+
computeMetadata(input: {
|
|
22
|
+
bytes: Uint8Array;
|
|
23
|
+
nonce?: Uint8Array | undefined;
|
|
24
|
+
}): Promise<BlobMeta>;
|
|
25
|
+
uploadToRelay(input: {
|
|
26
|
+
blobId: string;
|
|
27
|
+
bytes: Uint8Array;
|
|
28
|
+
nonce: Uint8Array;
|
|
29
|
+
registerTxDigest: string;
|
|
30
|
+
blobObjectId: string;
|
|
31
|
+
}): Promise<Certificate>;
|
|
32
|
+
}
|
|
33
|
+
/** The reservation as the server describes it. */
|
|
34
|
+
export interface ReserveReply {
|
|
35
|
+
ledger_id: number;
|
|
36
|
+
state: string;
|
|
37
|
+
blob_object_id?: string;
|
|
38
|
+
register_tx_digest?: string;
|
|
39
|
+
credits_spent: number;
|
|
40
|
+
}
|
|
41
|
+
/** The resume surface. */
|
|
42
|
+
export interface StatusReply {
|
|
43
|
+
ledger_id: number;
|
|
44
|
+
state: string;
|
|
45
|
+
blob_object_id?: string;
|
|
46
|
+
register_tx_digest?: string;
|
|
47
|
+
}
|
|
48
|
+
/** The api calls this rail makes. */
|
|
49
|
+
export interface UploadApi {
|
|
50
|
+
reserve(body: {
|
|
51
|
+
idempotency_key: string;
|
|
52
|
+
blob_id: string;
|
|
53
|
+
root_hash_b64: string;
|
|
54
|
+
size: number;
|
|
55
|
+
epochs: number;
|
|
56
|
+
/**
|
|
57
|
+
* How many credits this file sets aside as a deposit, 0 to 64.
|
|
58
|
+
*
|
|
59
|
+
* ⛔ ALWAYS SENT, NEVER LEFT TO THE SERVER'S DEFAULT. The number is a choice — the upload's
|
|
60
|
+
* own `--deposit`, or the account's sealed default — and the price this tool printed named
|
|
61
|
+
* it. A request that omitted it would let a server default decide an amount the person had
|
|
62
|
+
* already been told, which is how a screen and a ledger come to disagree.
|
|
63
|
+
*/
|
|
64
|
+
deposit_credits: number;
|
|
65
|
+
relay: {
|
|
66
|
+
host: string;
|
|
67
|
+
blob_digest_b64: string;
|
|
68
|
+
nonce_b64: string;
|
|
69
|
+
};
|
|
70
|
+
}): Promise<ReserveReply>;
|
|
71
|
+
status(ledgerId: number): Promise<StatusReply>;
|
|
72
|
+
uploaded(ledgerId: number, certificate: Certificate): Promise<unknown>;
|
|
73
|
+
createItem(body: {
|
|
74
|
+
size: number;
|
|
75
|
+
dek_wrapped: string;
|
|
76
|
+
content_hash_ct: string;
|
|
77
|
+
visibility: number;
|
|
78
|
+
parts: readonly Record<string, unknown>[];
|
|
79
|
+
}, idempotencyKey: string): Promise<{
|
|
80
|
+
id: string;
|
|
81
|
+
}>;
|
|
82
|
+
}
|
|
83
|
+
/** Where an upload stopped. The phase is what makes the message say the right true thing. */
|
|
84
|
+
export type UploadPhase = "encoding" | "reserve" | "uploading" | "certify" | "committing";
|
|
85
|
+
/** Told about each step, so a person watching a slow upload sees it moving. */
|
|
86
|
+
export type UploadStep = {
|
|
87
|
+
step: "encoding";
|
|
88
|
+
bytes: number;
|
|
89
|
+
} | {
|
|
90
|
+
step: "reserving";
|
|
91
|
+
} | {
|
|
92
|
+
step: "uploading";
|
|
93
|
+
relayUrl: string;
|
|
94
|
+
bytes: number;
|
|
95
|
+
} | {
|
|
96
|
+
step: "certifying";
|
|
97
|
+
} | {
|
|
98
|
+
step: "committing";
|
|
99
|
+
} | {
|
|
100
|
+
step: "resuming";
|
|
101
|
+
ledgerId: number;
|
|
102
|
+
state: string;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* A failure that names its phase — and, crucially, whether the account has already paid.
|
|
106
|
+
*
|
|
107
|
+
* ⛔ `paid` IS NOT COSMETIC. Before the reserve, a failure costs nothing and "try again" is honest
|
|
108
|
+
* advice. After it, the credits are gone and the storage exists; the honest advice is that the
|
|
109
|
+
* same command will FINISH it rather than buy it again, and that saying otherwise would send
|
|
110
|
+
* somebody to spend twice.
|
|
111
|
+
*/
|
|
112
|
+
export declare class UploadError extends NmtsError {
|
|
113
|
+
readonly phase: UploadPhase;
|
|
114
|
+
readonly paid: boolean;
|
|
115
|
+
constructor(input: {
|
|
116
|
+
phase: UploadPhase;
|
|
117
|
+
message: string;
|
|
118
|
+
paid: boolean;
|
|
119
|
+
nextStep?: string | null;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* What making the FILE needs — everything that is true of the whole upload rather than one part.
|
|
124
|
+
*
|
|
125
|
+
* ⛔ THE ENTRY IS NOT REBUILT PER RUN. What is stored on the network is one particular sealing of
|
|
126
|
+
* the file, under one particular file key. A resumed run that wrote its own freshly generated
|
|
127
|
+
* key into the list would produce a file that is paid for, present, correctly named — and
|
|
128
|
+
* impossible to open, because the key in the list would not be the key the bytes were sealed
|
|
129
|
+
* with. It comes off the record that was written before the first reservation.
|
|
130
|
+
*/
|
|
131
|
+
export interface CommitInput {
|
|
132
|
+
api: UploadApi;
|
|
133
|
+
/** Storage term in storage-network epochs. */
|
|
134
|
+
epochs: number;
|
|
135
|
+
/** The current storage-network epoch, or null when it could not be read. */
|
|
136
|
+
currentEpoch: number | null;
|
|
137
|
+
/** Everything the file list will need, carried through so a resumed run writes the same entry. */
|
|
138
|
+
entry: {
|
|
139
|
+
name: string;
|
|
140
|
+
parentId: string | null;
|
|
141
|
+
plaintextLen: number;
|
|
142
|
+
dekWrapped: string;
|
|
143
|
+
contentHashCt: string;
|
|
144
|
+
};
|
|
145
|
+
onStep?: (step: UploadStep) => void;
|
|
146
|
+
}
|
|
147
|
+
export interface UploadInput extends CommitInput {
|
|
148
|
+
protocol: BlobProtocol;
|
|
149
|
+
/** The stable, account-scoped name for THIS PART — see `upload-store.ts`. */
|
|
150
|
+
key: string;
|
|
151
|
+
/**
|
|
152
|
+
* The sealed NCF-3 stream for this part. These exact bytes are what gets stored.
|
|
153
|
+
*
|
|
154
|
+
* ⛔ ON A RESUME THE CALLER MUST PASS THE STORED BYTES, not a fresh sealing. Sealing is
|
|
155
|
+
* non-deterministic — a new file key and a new nonce every time — so re-sealing produces a
|
|
156
|
+
* DIFFERENT blob id, and the relay refuses bytes that are not the blob the treasury paid to
|
|
157
|
+
* register. `upload-store.ts` keeps them for exactly this.
|
|
158
|
+
*/
|
|
159
|
+
sealed: Uint8Array;
|
|
160
|
+
/** How many credits this file sets aside as a deposit, 0 to 64. Passed straight to the reserve. */
|
|
161
|
+
depositCredits: number;
|
|
162
|
+
/**
|
|
163
|
+
* The relay this run writes through when the reservation is new.
|
|
164
|
+
*
|
|
165
|
+
* ⚠ IGNORED ON A RESUME. The tip was paid to whichever relay the first attempt chose, and it is
|
|
166
|
+
* recorded; pushing to a different one is pushing bytes nobody tipped that relay for.
|
|
167
|
+
*/
|
|
168
|
+
relayUrl: string;
|
|
169
|
+
/**
|
|
170
|
+
* Where these sealed bytes sit in the file. A file that fits in one blob is part 0 of 1.
|
|
171
|
+
*
|
|
172
|
+
* ⛔ IT IS ALREADY SEALED INTO THE BYTES. NCF-3 writes both numbers into the part's header, so
|
|
173
|
+
* this is not where the placement is decided — it is a copy the record keeps so a resumed run
|
|
174
|
+
* can check it is about to push the same piece of the same file.
|
|
175
|
+
*/
|
|
176
|
+
part: {
|
|
177
|
+
index: number;
|
|
178
|
+
total: number;
|
|
179
|
+
plaintextLen: number;
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* ONE part that is bought, pushed and certified — everything the commit needs to name it.
|
|
184
|
+
*
|
|
185
|
+
* ⛔ IT IS NOT A FINISHED FILE. Nothing in the account can see these bytes yet: a file becomes
|
|
186
|
+
* real when `POST /v1/items` names every one of its parts at once. A part that stopped here is
|
|
187
|
+
* storage that is paid for and filled, waiting for the rest of the file.
|
|
188
|
+
*/
|
|
189
|
+
export interface PaidPart {
|
|
190
|
+
/** Its position in the file. */
|
|
191
|
+
partIndex: number;
|
|
192
|
+
/**
|
|
193
|
+
* The reservation row that paid for it — or null when the person's OWN WALLET paid.
|
|
194
|
+
*
|
|
195
|
+
* ⛔ THE ONE FIELD THAT SAYS WHO PAID. The server forces treasury ownership only on a part that
|
|
196
|
+
* names a certified reservation; a part without one is the person's, and the commit says
|
|
197
|
+
* nothing else about it (`upload-steps.ts`).
|
|
198
|
+
*/
|
|
199
|
+
ledgerId: number | null;
|
|
200
|
+
/** The storage network's id for its sealed bytes. */
|
|
201
|
+
blobId: string;
|
|
202
|
+
/** How many sealed bytes it is — what the credits were charged on. */
|
|
203
|
+
sealedLen: number;
|
|
204
|
+
/** True when this run did not have to spend on it, because a previous one already had. */
|
|
205
|
+
resumed: boolean;
|
|
206
|
+
/** Wallet rail only: the on-chain blob object, recorded for the recovery list. */
|
|
207
|
+
suiObjectId?: string;
|
|
208
|
+
/** Wallet rail only: the epoch the bought storage ends at, read from the blob object. */
|
|
209
|
+
endEpoch?: number;
|
|
210
|
+
}
|
|
211
|
+
/** What finished. The caller writes the file list, THEN clears the records. */
|
|
212
|
+
export interface UploadResult {
|
|
213
|
+
itemId: string;
|
|
214
|
+
/** True when no part of this run had to spend, because a previous one already had. */
|
|
215
|
+
resumed: boolean;
|
|
216
|
+
/** The reservation rows, for a message that can be checked against the account screen. Empty on the wallet rail. */
|
|
217
|
+
ledgerIds: readonly number[];
|
|
218
|
+
/**
|
|
219
|
+
* The account-scoped name this upload's records were filed under, and how many parts it had.
|
|
220
|
+
*
|
|
221
|
+
* ⛔ HANDED BACK RATHER THAN RECOMPUTED. Working it out again needs the account's data key and
|
|
222
|
+
* another full read of the file, and the caller wipes that key the moment the upload returns.
|
|
223
|
+
* A caller that could not name the records could not clear them, and an uncleared record is
|
|
224
|
+
* resumed by the next run instead of being overwritten.
|
|
225
|
+
*/
|
|
226
|
+
fileKey: string;
|
|
227
|
+
parts: number;
|
|
228
|
+
/**
|
|
229
|
+
* What the FILE LIST must record — taken from the reservation, not from this run.
|
|
230
|
+
*
|
|
231
|
+
* ⛔ THE CALLER MUST WRITE THESE AND NOT ITS OWN. What is stored on the network is one
|
|
232
|
+
* particular sealing of the file, under one particular file key. A resumed run that wrote its
|
|
233
|
+
* own freshly generated key into the list would produce a file that is paid for, present,
|
|
234
|
+
* correctly named — and impossible to open, because the key in the list would not be the key
|
|
235
|
+
* the stored bytes were sealed with. Returning them is what makes that mistake unavailable.
|
|
236
|
+
*/
|
|
237
|
+
entry: {
|
|
238
|
+
name: string;
|
|
239
|
+
parentId: string | null;
|
|
240
|
+
plaintextLen: number;
|
|
241
|
+
dekWrapped: string;
|
|
242
|
+
contentHashCt: string;
|
|
243
|
+
};
|
|
244
|
+
}
|