@needmoretruth/nmts-cli 0.34.2 → 0.34.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,22 @@
3
3
  Each version's entry is what changed for the person or the program using `nmts`. The product's own
4
4
  update history, which covers the site and the server too, is at https://nmts.me/updates.
5
5
 
6
+ ## 0.34.4 — 2026-09-07
7
+
8
+ - The advice for `SPONSORED_IDEM_MISMATCH`: the server now refuses a credit reservation repeated
9
+ under the same key with a different blob (the piece was re-encrypted after it was reserved).
10
+ Repeating the call cannot work; the advice says to start the upload again so new keys are
11
+ derived, and that nothing was charged for the refused call.
12
+
13
+ ## 0.34.3 — 2026-09-07
14
+
15
+ - Security. `nmts wallet swap` on Bluefin now calls only the contract package pinned in this
16
+ release. Before, it read the "current" package from a public Sui node and preferred that answer,
17
+ so a node giving a false answer could have sent the swap into someone else's code with your coin
18
+ as the argument. The pinned package is still checked against the chain before a quote is shown;
19
+ if that check refuses it, the Bluefin venue is unavailable until a release bumps the pin. DeepBook
20
+ was never affected. No such node was observed and no loss is known.
21
+
6
22
  ## 0.34.2 — 2026-09-07
7
23
 
8
24
  - The README and the agent document open with what the name stands for — NMTS is
package/README.ko.md CHANGED
@@ -51,7 +51,7 @@ nmts --help
51
51
 
52
52
  ```sh
53
53
  npm install -g github:needmoretruth/nmts-cli # 기본 브랜치
54
- npm install -g github:needmoretruth/nmts-cli#v0.34.2 # 버전을 고정할 때
54
+ npm install -g github:needmoretruth/nmts-cli#v0.34.4 # 버전을 고정할 때
55
55
  npm install -g https://github.com/needmoretruth/nmts-cli/releases/latest/download/nmts.tgz
56
56
  ```
57
57
 
package/README.md CHANGED
@@ -53,7 +53,7 @@ default branch, from a pinned version, or from the tarball attached to the
53
53
 
54
54
  ```sh
55
55
  npm install -g github:needmoretruth/nmts-cli # the default branch
56
- npm install -g github:needmoretruth/nmts-cli#v0.34.2 # a pinned version
56
+ npm install -g github:needmoretruth/nmts-cli#v0.34.4 # a pinned version
57
57
  npm install -g https://github.com/needmoretruth/nmts-cli/releases/latest/download/nmts.tgz
58
58
  ```
59
59
 
@@ -168,6 +168,10 @@ export function adviseFor(code) {
168
168
  case "RELEASE_NOT_SPONSORED":
169
169
  return ("This file's storage was not paid for with credits, so it is not the server's to release. " +
170
170
  "Storage bought from a wallet is released by that wallet.");
171
+ case "SPONSORED_IDEM_MISMATCH":
172
+ return ("This reservation key already names a different blob: the piece was re-encrypted after it " +
173
+ "was reserved. Repeating the call under this key cannot work. Start the upload again so it " +
174
+ "derives new keys; nothing was charged for this call.");
171
175
  case "SPONSORED_STATE":
172
176
  return ("The upload is not at the step that call belongs to — the steps have an order and one was " +
173
177
  "skipped or already done. Start the upload again rather than repeating this call.");
@@ -11,7 +11,7 @@ export declare const CRYPTO_SPEC_URL = "https://github.com/needmoretruth/nmts-re
11
11
  * is the field's own contract — what the writer says about itself — and a person holding two
12
12
  * copies of one account's artefacts can then tell which program made each.
13
13
  */
14
- export declare const WRITTEN_BY = "nmts-cli 0.34.2";
14
+ export declare const WRITTEN_BY = "nmts-cli 0.34.4";
15
15
  /** Which of the three artefacts a wrapper is. A reader holding several can sort them. */
16
16
  export type ArtifactKind = "recovery-list" | "file-list" | "recovery-kit";
17
17
  /**
package/dist/product.d.ts CHANGED
@@ -9,7 +9,7 @@ export declare const BINARY_NAME = "nmts";
9
9
  * beside it. Here rather than in `main.ts` because the MCP server has to say it too, and a
10
10
  * command importing the entry point is a cycle waiting to bite.
11
11
  */
12
- export declare const VERSION = "0.34.2";
12
+ export declare const VERSION = "0.34.4";
13
13
  /** Where the product lives, for messages that need to send somebody somewhere real. */
14
14
  export declare const HOME_URL = "https://nmts.me";
15
15
  /** The source, so a person holding only the built program can find what it was built from. */
package/dist/product.js CHANGED
@@ -18,7 +18,7 @@ export const BINARY_NAME = "nmts";
18
18
  * beside it. Here rather than in `main.ts` because the MCP server has to say it too, and a
19
19
  * command importing the entry point is a cycle waiting to bite.
20
20
  */
21
- export const VERSION = "0.34.2";
21
+ export const VERSION = "0.34.4";
22
22
  /** Where the product lives, for messages that need to send somebody somewhere real. */
23
23
  export const HOME_URL = "https://nmts.me";
24
24
  /** The source, so a person holding only the built program can find what it was built from. */
@@ -5,7 +5,7 @@ import { type WalletBalances } from "./wallet.ts";
5
5
  import { type VenueQuote } from "./wallet-swap-quote.ts";
6
6
  /** Where a swap runs: one of the two mainnet venues, or the official testnet facility. */
7
7
  export type SwapRail = SwapVenue | "exchange";
8
- /** Bluefin's addresses, with the package RESOLVED and version-checked on chain, never assumed. */
8
+ /** Bluefin's addresses: the package PINNED in this release, confirmed on chain before it is used. */
9
9
  export interface BluefinBinding {
10
10
  packageId: string;
11
11
  globalConfigId: string;
@@ -40,7 +40,7 @@ export declare function swapTransaction(input: SwapShape & {
40
40
  /** What `commands/wallet-swap.ts` reads before it prints a review. */
41
41
  export interface SwapReads {
42
42
  readWallet(address: string): Promise<WalletBalances>;
43
- /** Bluefin's current package, version-checked. Throws when no known package passes. */
43
+ /** Bluefin's pinned package, version-checked. Throws when the chain refuses that package. */
44
44
  resolveBluefin(): Promise<BluefinBinding>;
45
45
  /** The testnet facility and its rate. Throws off testnet, or when the object cannot be read. */
46
46
  readExchange(): Promise<ExchangeFacility>;
@@ -49,4 +49,18 @@ export interface SwapReads {
49
49
  /** The fee in MIST measured by dry-running this exact swap, or null when it could not be. */
50
50
  estimateFee(shape: SwapShape, sender: string): Promise<bigint | null>;
51
51
  }
52
+ /** All this path uses of an RPC: one devInspect and its `error`. Narrow on purpose — the type itself
53
+ * has no place for a chain-supplied ADDRESS to enter, and a test can hand it a fake. */
54
+ export interface BluefinVersionRpc {
55
+ devInspectTransactionBlock(input: {
56
+ sender: string;
57
+ transactionBlock: Transaction;
58
+ }): Promise<{
59
+ error?: string | null;
60
+ }>;
61
+ }
62
+ /** The pinned addresses, returned only once the chain confirms the pinned package still verifies.
63
+ * A refusal ends it: this function has no other address to return. Exported so the test can hold an
64
+ * RPC that names a different package and watch it change nothing. */
65
+ export declare function checkedBluefinBinding(pinned: BluefinBinding, rpc: BluefinVersionRpc): Promise<BluefinBinding>;
52
66
  export declare function swapReads(network: Network): SwapReads;
@@ -1,6 +1,7 @@
1
- // The swap transaction and its live reads: the wallet's balances, Bluefin's current package, the
2
- // testnet facility's rate, and the fee a dry run measures. The quotes live next door in
3
- // `wallet-swap-quote.ts`; both files read, and only `wallet-sign.ts` signs.
1
+ // The swap transaction and its live reads: the wallet's balances, the version check on Bluefin's
2
+ // PINNED package, the testnet facility's rate, and the fee a dry run measures. Bluefin's package id
3
+ // itself is NOT a live read see the block above `checkedBluefinBinding`. The quotes live next door
4
+ // in `wallet-swap-quote.ts`; both files read, and only `wallet-sign.ts` signs.
4
5
  //
5
6
  // ⛔ ONE BUILDER FOR THE FEE AND THE SIGNATURE. `estimateFee` and `wallet-sign.ts` both call
6
7
  // `swapTransaction`, so the fee printed is the fee of the transaction that is then signed.
@@ -20,7 +21,7 @@ import { coinWithBalance, Transaction } from "@mysten/sui/transactions";
20
21
  import { TESTNET_WALRUS_PACKAGE_CONFIG } from "@mysten/walrus";
21
22
  import { walrusClient, netGasFee } from "./extend-chain.js";
22
23
  import { isRecord } from "./guards.js";
23
- import { BLUEFIN_GLOBAL_CONFIG_IDS, BLUEFIN_MAX_SQRT_PRICE, BLUEFIN_MIN_SQRT_PRICE, BLUEFIN_PACKAGE_IDS, BLUEFIN_UPGRADE_CAP_IDS, BLUEFIN_WAL_SUI_POOLS, DEEP_COIN_TYPES, DEEPBOOK_PACKAGE_IDS, DEEPBOOK_WAL_SUI_POOLS, } from "./shared/lib/wallet/venue-ids.js";
24
+ import { BLUEFIN_GLOBAL_CONFIG_IDS, BLUEFIN_MAX_SQRT_PRICE, BLUEFIN_MIN_SQRT_PRICE, BLUEFIN_PACKAGE_IDS, BLUEFIN_WAL_SUI_POOLS, DEEP_COIN_TYPES, DEEPBOOK_PACKAGE_IDS, DEEPBOOK_WAL_SUI_POOLS, } from "./shared/lib/wallet/venue-ids.js";
24
25
  import { readBalances, SUI_COIN_TYPE, walCoinType } from "./wallet.js";
25
26
  import { chainReader } from "./wallet-chain.js";
26
27
  import { quoteVenue, QUOTE_SENDER } from "./wallet-swap-quote.js";
@@ -86,38 +87,36 @@ export function swapTransaction(input) {
86
87
  tx.setGasBudget(input.gasBudgetMist);
87
88
  return tx;
88
89
  }
89
- const FULL_ADDRESS = /^0x[0-9a-f]{64}$/;
90
90
  /** `fields.<name>` of a Move object's content, as a string, or null when the shape differs. */
91
91
  function fieldOf(content, name) {
92
92
  const fields = isRecord(content) ? content["fields"] : undefined;
93
93
  return isRecord(fields) ? fields[name] : undefined;
94
94
  }
95
+ /** Does the chain still accept this package? `config::verify_version` under devInspect: no signature,
96
+ * no gas. False when the chain refuses AND when the RPC cannot answer — both mean "no Bluefin". */
97
+ async function versionPasses(rpc, packageId, globalConfigId) {
98
+ try {
99
+ const tx = new Transaction();
100
+ tx.moveCall({ target: `${packageId}::config::verify_version`, arguments: [tx.object(globalConfigId)] });
101
+ const res = await rpc.devInspectTransactionBlock({ sender: QUOTE_SENDER, transactionBlock: tx });
102
+ return !res.error;
103
+ }
104
+ catch {
105
+ return false;
106
+ }
107
+ }
108
+ /** The pinned addresses, returned only once the chain confirms the pinned package still verifies.
109
+ * A refusal ends it: this function has no other address to return. Exported so the test can hold an
110
+ * RPC that names a different package and watch it change nothing. */
111
+ export async function checkedBluefinBinding(pinned, rpc) {
112
+ if (!(await versionPasses(rpc, pinned.packageId, pinned.globalConfigId))) {
113
+ throw new Error("Bluefin's on-chain version check refused the package address this tool pins.");
114
+ }
115
+ return pinned;
116
+ }
95
117
  export function swapReads(network) {
96
118
  const client = walrusClient(network);
97
119
  let bluefin = null;
98
- async function livePackage(upgradeCapId) {
99
- try {
100
- const obj = await client.getObject({ id: upgradeCapId, options: { showContent: true, showType: true } });
101
- if (obj.data?.type !== "0x2::package::UpgradeCap")
102
- return null;
103
- const pkg = fieldOf(obj.data.content, "package");
104
- return typeof pkg === "string" && FULL_ADDRESS.test(pkg) ? pkg : null;
105
- }
106
- catch {
107
- return null;
108
- }
109
- }
110
- async function versionPasses(packageId, globalConfigId) {
111
- try {
112
- const tx = new Transaction();
113
- tx.moveCall({ target: `${packageId}::config::verify_version`, arguments: [tx.object(globalConfigId)] });
114
- const res = await client.devInspectTransactionBlock({ sender: QUOTE_SENDER, transactionBlock: tx });
115
- return !res.error;
116
- }
117
- catch {
118
- return false;
119
- }
120
- }
121
120
  return {
122
121
  async readWallet(address) {
123
122
  return readBalances(chainReader(network, address), walCoinType(network));
@@ -127,19 +126,13 @@ export function swapReads(network) {
127
126
  if (bluefin !== null)
128
127
  return bluefin;
129
128
  const pending = (async () => {
130
- const pinned = BLUEFIN_PACKAGE_IDS[network];
131
- const capId = BLUEFIN_UPGRADE_CAP_IDS[network];
129
+ const packageId = BLUEFIN_PACKAGE_IDS[network];
132
130
  const globalConfigId = BLUEFIN_GLOBAL_CONFIG_IDS[network];
133
131
  const poolId = BLUEFIN_WAL_SUI_POOLS[network];
134
- if (pinned === null || capId === null || globalConfigId === null || poolId === null) {
132
+ if (packageId === null || globalConfigId === null || poolId === null) {
135
133
  throw new Error(`This tool knows no Bluefin WAL/SUI pool on ${network}.`);
136
134
  }
137
- const live = await livePackage(capId);
138
- for (const packageId of live === null ? [pinned] : [live, pinned]) {
139
- if (await versionPasses(packageId, globalConfigId))
140
- return { packageId, globalConfigId, poolId };
141
- }
142
- throw new Error("Bluefin's on-chain version check refused every package address this tool knows.");
135
+ return checkedBluefinBinding({ packageId, globalConfigId, poolId }, client);
143
136
  })();
144
137
  bluefin = pending;
145
138
  pending.catch(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needmoretruth/nmts-cli",
3
- "version": "0.34.2",
3
+ "version": "0.34.4",
4
4
  "description": "Command-line and MCP access to NMTS (NeedMoreTruthStorage, https://nmts.me): open-source, end-to-end encrypted storage on the Walrus network. No fee to NMTS; you pay the network from your own wallet. For people and for their agents.",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {