@ouronet/ouronet-core 4.3.6

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.
Files changed (101) hide show
  1. package/CHANGELOG.md +3043 -0
  2. package/README.md +1940 -0
  3. package/dist/codex/codec.d.ts +77 -0
  4. package/dist/codex/codec.js +123 -0
  5. package/dist/codex/errors.d.ts +14 -0
  6. package/dist/codex/errors.js +20 -0
  7. package/dist/codex/index.d.ts +27 -0
  8. package/dist/codex/index.js +25 -0
  9. package/dist/codex/seedTypeMigration.d.ts +35 -0
  10. package/dist/codex/seedTypeMigration.js +45 -0
  11. package/dist/codex/types.d.ts +100 -0
  12. package/dist/codex/types.js +19 -0
  13. package/dist/constants/index.d.ts +2 -0
  14. package/dist/constants/index.js +3 -0
  15. package/dist/constants/ouronet.d.ts +33 -0
  16. package/dist/constants/ouronet.js +59 -0
  17. package/dist/constants/tokenIds.d.ts +13 -0
  18. package/dist/constants/tokenIds.js +21 -0
  19. package/dist/index.d.ts +18 -0
  20. package/dist/index.js +1 -0
  21. package/dist/interactions/activateFunctions.d.ts +67 -0
  22. package/dist/interactions/activateFunctions.js +154 -0
  23. package/dist/interactions/addLiquidityFunctions.d.ts +140 -0
  24. package/dist/interactions/addLiquidityFunctions.js +485 -0
  25. package/dist/interactions/coilFunctions.d.ts +73 -0
  26. package/dist/interactions/coilFunctions.js +213 -0
  27. package/dist/interactions/crossChainFunctions.d.ts +211 -0
  28. package/dist/interactions/crossChainFunctions.js +463 -0
  29. package/dist/interactions/dexAcquisitionPoolFunctions.d.ts +18 -0
  30. package/dist/interactions/dexAcquisitionPoolFunctions.js +18 -0
  31. package/dist/interactions/dexCollectablesFunctions.d.ts +28 -0
  32. package/dist/interactions/dexCollectablesFunctions.js +227 -0
  33. package/dist/interactions/dexFunctions.d.ts +23 -0
  34. package/dist/interactions/dexFunctions.js +23 -0
  35. package/dist/interactions/dexOrtoFungibleFunctions.d.ts +33 -0
  36. package/dist/interactions/dexOrtoFungibleFunctions.js +171 -0
  37. package/dist/interactions/dexParseFunctions.d.ts +150 -0
  38. package/dist/interactions/dexParseFunctions.js +316 -0
  39. package/dist/interactions/dexSwapPairAdminFunctions.d.ts +75 -0
  40. package/dist/interactions/dexSwapPairAdminFunctions.js +255 -0
  41. package/dist/interactions/dexSwapPairCalcFunctions.d.ts +55 -0
  42. package/dist/interactions/dexSwapPairCalcFunctions.js +218 -0
  43. package/dist/interactions/dexSwapPairDashboardFunctions.d.ts +47 -0
  44. package/dist/interactions/dexSwapPairDashboardFunctions.js +179 -0
  45. package/dist/interactions/dexSwapPairExecuteFunctions.d.ts +40 -0
  46. package/dist/interactions/dexSwapPairExecuteFunctions.js +301 -0
  47. package/dist/interactions/dexSwapPairSmartSwapFunctions.d.ts +25 -0
  48. package/dist/interactions/dexSwapPairSmartSwapFunctions.js +166 -0
  49. package/dist/interactions/dexTrueFungibleFunctions.d.ts +52 -0
  50. package/dist/interactions/dexTrueFungibleFunctions.js +220 -0
  51. package/dist/interactions/dexTypes.d.ts +465 -0
  52. package/dist/interactions/dexTypes.js +6 -0
  53. package/dist/interactions/errors.d.ts +12 -0
  54. package/dist/interactions/errors.js +13 -0
  55. package/dist/interactions/guardFunctions.d.ts +33 -0
  56. package/dist/interactions/guardFunctions.js +110 -0
  57. package/dist/interactions/index.d.ts +1 -0
  58. package/dist/interactions/index.js +20 -0
  59. package/dist/interactions/infoOneFunctions.d.ts +133 -0
  60. package/dist/interactions/infoOneFunctions.js +566 -0
  61. package/dist/interactions/kadenaFunctions.d.ts +11 -0
  62. package/dist/interactions/kadenaFunctions.js +40 -0
  63. package/dist/interactions/kpayFunctions.d.ts +45 -0
  64. package/dist/interactions/kpayFunctions.js +191 -0
  65. package/dist/interactions/ouroAccountFunctions.d.ts +64 -0
  66. package/dist/interactions/ouroAccountFunctions.js +229 -0
  67. package/dist/interactions/ouroBalanceFunctions.d.ts +39 -0
  68. package/dist/interactions/ouroBalanceFunctions.js +100 -0
  69. package/dist/interactions/ouroCoilFunctions.d.ts +33 -0
  70. package/dist/interactions/ouroCoilFunctions.js +190 -0
  71. package/dist/interactions/ouroFunctions.d.ts +23 -0
  72. package/dist/interactions/ouroFunctions.js +23 -0
  73. package/dist/interactions/ouroMovieBoosterFunctions.d.ts +18 -0
  74. package/dist/interactions/ouroMovieBoosterFunctions.js +203 -0
  75. package/dist/interactions/ouroPriceFunctions.d.ts +38 -0
  76. package/dist/interactions/ouroPriceFunctions.js +96 -0
  77. package/dist/interactions/ouroPrimordialsFunctions.d.ts +10 -0
  78. package/dist/interactions/ouroPrimordialsFunctions.js +480 -0
  79. package/dist/interactions/ouroRotateFunctions.d.ts +22 -0
  80. package/dist/interactions/ouroRotateFunctions.js +114 -0
  81. package/dist/interactions/ouroSubCompressFunctions.d.ts +23 -0
  82. package/dist/interactions/ouroSubCompressFunctions.js +191 -0
  83. package/dist/interactions/ouroTransferFunctions.d.ts +14 -0
  84. package/dist/interactions/ouroTransferFunctions.js +87 -0
  85. package/dist/interactions/ouroTypes.d.ts +113 -0
  86. package/dist/interactions/ouroTypes.js +6 -0
  87. package/dist/interactions/ouroUrStoaFunctions.d.ts +16 -0
  88. package/dist/interactions/ouroUrStoaFunctions.js +121 -0
  89. package/dist/interactions/ouroWrapFunctions.d.ts +22 -0
  90. package/dist/interactions/ouroWrapFunctions.js +234 -0
  91. package/dist/interactions/pensionFunctions.d.ts +12 -0
  92. package/dist/interactions/pensionFunctions.js +127 -0
  93. package/dist/interactions/urStoaFunctions.d.ts +104 -0
  94. package/dist/interactions/urStoaFunctions.js +475 -0
  95. package/dist/interactions/wrapFunctions.d.ts +46 -0
  96. package/dist/interactions/wrapFunctions.js +221 -0
  97. package/dist/pact/cfmBuilders.d.ts +807 -0
  98. package/dist/pact/cfmBuilders.js +812 -0
  99. package/dist/pact/index.d.ts +1 -0
  100. package/dist/pact/index.js +10 -0
  101. package/package.json +64 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,3043 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@ouronet/ouronet-core`.
4
+
5
+ This package is the historical continuation of `@ouronet/ouronet-core` v0.x–v3.3.8. v4.0.0 split it into a two-package npm workspace under `StoaChain/stoa-js` — chain-generic infrastructure moved out into [`@stoachain/stoa-core`](https://www.npmjs.com/package/@stoachain/stoa-core), this package retained the Ouronet-specific business logic. The `4.0.0` heading below is the first release after the split.
6
+
7
+ ## 4.3.6 — 2026-06-11
8
+
9
+ Atomic-triplet lockstep bump — `4.3.5 → 4.3.6` alongside `@stoachain/stoa-core` (which carries an auto-gas-limit floor fix in `calculateAutoGasLimit` — a dirty-read under-reports a signed tx's real cost, so tiny estimates were starving txs) and `@stoachain/kadena-stoic-legacy`, per the cross-package version-pin invariant. This package is functionally identical to its 4.3.5 release.
10
+
11
+ ## 4.3.5 — 2026-06-10
12
+
13
+ **Additive — Smart Ouronet Account deploy + keyset-ref-aware activation + fresh IGNIS balance.** Atomic-triplet bump — `@stoachain/kadena-stoic-legacy` + `@stoachain/stoa-core` bumped 4.3.4 → 4.3.5 in lockstep per the cross-package version-pin invariant; those two are functionally identical to their 4.3.4 release (only ouronet-core changed).
14
+
15
+ - **`buildDeploySmartAccountPactCode({ account, kadenaAddress, sovereign, publicKey, mode?, keysetRef? })`** — emits `C_DeploySmartAccount <account> <guard> <kadena> <sovereign> <public>`, the Smart (Σ.) account deploy. Same shape as the Standard deploy plus the extra `sovereign` arg (an existing Standard Ѻ. account).
16
+ - **`getDeploySmartAccountInfo` / `getDeploySmartAccountInfoOnly`** — `URC_DeploySmartAccount` readers (mirror the Standard pair).
17
+ - **Keyset-ref-aware activation:** `buildDeployStandardAccountPactCode` + `buildDeploySmartAccountPactCode` gained `mode: "define" | "existing"` + `keysetRef`. In `"existing"` mode they emit `(keyset-ref-guard "<ref>")` instead of `(read-keyset "ks")`, so a "Use Existing Keyset" activation preserves the keyset-ref guard type instead of expanding it to a literal keyset. Default `"define"` is backward-compatible.
18
+ - **`getIgnisBalance` now reads at tier T1, not T5** — it's a balance read (T1 = high-churn/short-TTL per the canonical tier mapping). At T5 the patron IGNIS balance stayed stale right after a transfer, falsely blocking spends with "Insufficient IGNIS" until a hard refresh.
19
+
20
+ Pure additive to existing exports (the deploy builders gained optional params with backward-compatible defaults). Consumes downstream into `@ouronet/ouronet-codex@0.5.4`.
21
+
22
+ ## 4.3.4 — 2026-06-08
23
+
24
+ **Bugfix — UrStoa vault earnings hover.** `ouroPrimordialsFunctions.parseResponse` routed the `urstoa-vault-earning-hover` field through `String(...)` before formatting. The chain returns that field as a `{ decimal: "…" }` object (like the supply hover), so `String({decimal})` produced `"[object Object]"`, which parsed back to `0` and wrongly disabled the Dashboard UrStoa **Collect** button (showing an `[object Object]` tooltip). It is now routed through `supplyHoverVal` (→ `mayComeWithDeimal`), matching the supply row, so the hover unwraps correctly and Collect re-enables when earnings are available.
25
+
26
+ Atomic-triplet bump — `@stoachain/kadena-stoic-legacy` + `@stoachain/stoa-core` bumped 4.3.3 → 4.3.4 in lockstep per the cross-package version-pin invariant; those two packages are functionally identical to their 4.3.3 release.
27
+
28
+ ## 4.3.3 — 2026-05-30
29
+
30
+ **Additive — StoicTag + governor-rotation builder/reader surface** supporting the OuronetUI v1.2.5 StoicTag wiring cycle (Register / Release StoicTag modals, Rotate Governor modal, tag-aware receiver fields). Atomic-triplet bump — `@stoachain/kadena-stoic-legacy` + `@stoachain/stoa-core` bumped 4.3.2 → 4.3.3 in lockstep per the cross-package version-pin invariant; those two packages are functionally identical to their 4.3.2 release.
31
+
32
+ ### New Pact builders (`src/pact/cfmBuilders.ts`)
33
+
34
+ - `buildRotateGovernorPactCode({ patron, account, governor })` — emits the `ouronet-ns` governor-rotation call.
35
+ - `buildNonKeyGuardExpr(constructor, ...)` (+ exported `NonKeyGuardConstructor` type) — serializes a User-guard or Capability-guard expression for the non-key governor branch.
36
+ - `buildReleaseStoicTagPactCode({ patron, tagName })` — `(ouronet-ns.TS01-C4.CODEX|C_ReleaseStoicTag "patron" "tag")`.
37
+ - `buildRegisterStoicTagPactCode({ patron, tagName, accountAddress })` — `(ouronet-ns.TS01-C4.CODEX|C_RegisterStoicTag "patron" "tag" "account")`.
38
+
39
+ ### New interaction readers (`src/interactions/ouroAccountFunctions.ts`)
40
+
41
+ - `getStoicTagInfo` (URC_0027c) — forward StoicTag info read.
42
+ - `getStoicTagSelectorData` (URC_0027b) — batch selector-data read for tag pickers.
43
+ - `getRegisterStoicTagInfo(patron, tagName, account)` — combined `let*` read returning `{ info, receivers }` (INFO + `kadena-targets` resolved via `UR_AccountKadena`) for the native-STOA split registration flow.
44
+
45
+ ### Types (`src/interactions/ouroTypes.ts`)
46
+
47
+ - `AccountSelectorData` gained `stoic-tag-has` / `stoic-tag` / `stoic-tag-registered-at`.
48
+ - New `StoicTagSelectorData` interface.
49
+
50
+ All additions are additive on the public typed surface — no existing builder, reader, or type signature changed. Covered by `tests/cfm-builders.test.ts` + `tests/stoic-tag-reads.test.ts`.
51
+
52
+ ## 4.3.2 — 2026-05-30
53
+
54
+ **PATCH — frozen-keyset mutation fix in `resolveGuard` / `getKadenaAccountGuard`** (`src/interactions/ouroAccountFunctions.ts`). Atomic-triplet bump — `@stoachain/kadena-stoic-legacy` + `@stoachain/stoa-core` bumped 4.3.1 → 4.3.2 in lockstep per the cross-package version-pin invariant; those two packages are functionally identical to their 4.3.1 release.
55
+
56
+ ### The bug
57
+
58
+ Both helpers resolved a keyset-ref by reading the keyset and then **mutating the read result in place**:
59
+
60
+ ```ts
61
+ const ks = await readKeyset(guardData.keysetref.ns, guardData.keysetref.ksn);
62
+ if (ks) ks.keysetRef = `${guardData.keysetref.ns}.${guardData.keysetref.ksn}`;
63
+ return ks;
64
+ ```
65
+
66
+ `readKeyset` returns the object handed back by the configured `pactRead` seam. Under OuronetUI's cache-aware reader that object is a **shared, frozen reference**, so the in-place write threw `TypeError: Cannot assign to read only property 'keysetRef'`. In OuronetUI this rejection propagated through the `Promise.all` inside wallet-context's `syncOuroAccounts`, aborting the whole sync before the displayed account list / counts were updated — so newly-spawned Ouronet accounts (any creation mode) never appeared in the Codex, and Standard/Smart counts froze at the last successful-sync snapshot. The mode/curve "specificity" originally reported was incidental timing, not a per-mode code path.
67
+
68
+ The bug didn't surface server-side (AH hub) because the default uncached reader returns fresh, unfrozen objects each call.
69
+
70
+ ### The fix
71
+
72
+ Return a shallow copy instead of mutating: `return ks ? { ...ks, keysetRef } : ks;`. The borrowed/frozen read result is never written to. Regression test in `tests/v4-3-2-resolve-guard-no-mutate-frozen-keyset.test.ts` installs a `pactReader` that returns a frozen keyset and asserts neither helper throws, the resolved `keysetRef` is present on the returned object, and the frozen source is left untouched. No public API or type changes.
73
+
74
+ ## 4.3.1 — 2026-05-27
75
+
76
+ **PATCH — ESM extensionless-relative-import fix** (atomic-triplet bump). Same bug pattern as [`@stoachain/stoa-core@4.3.1`](https://www.npmjs.com/package/@stoachain/stoa-core) and [`@ouronet/ouronet-codex@0.2.1`](https://www.npmjs.com/package/@ouronet/ouronet-codex). See those entries for the full bug context.
77
+
78
+ 40 source files updated, 90 imports rewritten in `src/**/*.ts`. Emitted `dist/**/*.js` now correctly has the `.js` extension on every relative import. Peer-deps on `@stoachain/kadena-stoic-legacy` + `@stoachain/stoa-core` updated 4.3.0 → 4.3.1.
79
+
80
+ Verification: 797/797 specs pass; smoke test of `await import('@ouronet/ouronet-core/codex')` (and `/pact`, `/constants`, `/interactions/*`) under Node 22+ strict ESM succeeds. No API changes.
81
+
82
+ ## 4.3.0 — 2026-05-25
83
+
84
+ **MINOR — additive: 2 new account-rotation Pact builders** (`buildRotateGuardPactCode`, `buildRotateKadenaPactCode`) supporting the [`@ouronet/ouronet-codex`](https://www.npmjs.com/package/@ouronet/ouronet-codex) v0.1.0 Phase 6b rotation modals (`<RotateGuardModal>` + `<RotatePaymentKeyModal>`). The third rotation modal in the package (`<RotateSovereignModal>`) uses the pre-existing `buildRotateSovereignPactCode` (added v1.6.0). MINOR rather than PATCH because these unlock a new categorised public consumer surface — ouronet-codex's headless on-chain account-management UI — that wasn't reachable through the prior builder set; "additive on the public typed API surface in a way that enables a new consumer use case" is the established MINOR trigger for this monorepo (see also v4.2.0 which graduated the codex codec). Atomic-triplet bump — `@stoachain/kadena-stoic-legacy` + `@stoachain/stoa-core` bumped 4.2.2 → 4.3.0 in lockstep per the cross-package version-pin invariant; those two packages are functionally identical to their 4.2.2 release.
85
+
86
+ ### Added — Account rotation builders (TS01-C1.DALOS)
87
+
88
+ - **Rotate Guard** — `buildRotateGuardPactCode({ patron, account, mode, keysetRef?, safe })` in `src/pact/cfmBuilders.ts`. Emits the canonical `C_RotateGuard` 4-arg shape with a runtime-conditional guard expression: `(read-keyset "ks")` when `mode === "define"` (caller MUST `.addData("ks", { keys, pred })` on the builder), or `(keyset-ref-guard "<ref>")` when `mode === "existing"`. The `safe` boolean is emitted as a bare Pact bool literal (not quoted). Tests in `cfm-builders.test.ts` cover both modes + the empty-keysetRef defensive fallback + bare-bool emission + module/function path.
89
+ - **Rotate Payment Key** — `buildRotateKadenaPactCode({ patron, account, newPaymentKey })` in `src/pact/cfmBuilders.ts`. Emits the canonical `C_RotateKadena` 3-arg shape (named `RotateKadena` on chain because the kadena-ledger payment key is what's being rotated; the user-facing builder description uses "Payment Key" for clarity). Caller is responsible for `.addData("ks", patronGuard)` + (when patron ≠ account) `.addData("ks-account", accountGuard)` on the transaction builder — the builder ONLY emits the Pact code. Tests in `cfm-builders.test.ts` cover the canonical shape + argument order + special-character preservation.
90
+
91
+ ### Deferred
92
+
93
+ - **Rotate Governor** — Pact function `C_RotateGovernor` exists on chain (Smart Account `Σ.` only) but no UI surface ships it yet, so no builder is added in this release. Will land in a later version once `@ouronet/ouronet-codex` consumes it (post-OuronetUI-Phase-9-migration per the modular-codex spec's locked decision).
94
+
95
+ ## 4.2.2 — 2026-05-18
96
+
97
+ **PATCH — additive: new SWP-pair management builders + matching INFO readers + UR\_\* reads** to support the OuronetUI v1.0.8 cycle wiring the remaining 9 Liquidity-Pools-Management page buttons (Modify Can Change Owner, Modify Weights, Modify Amplifier, Toggle Swapping / Provisioning, Activate Frozen / Sleeping LP, Update Fee Targets, Update Branding). Shipped mid-cycle at 5-of-11 buttons wired (Change Ownership from v1.0.7, plus Modify Can Change Owner / Modify Weights / Toggle Swapping / Toggle Provisioning from this cycle) to validate the cascade on devwallet before completing the remaining 6. PATCH chosen to match the established project cadence — v4.2.1 was itself 23 additive builders shipped as PATCH; strict SemVer would call this MINOR, but the workspace convention has consistently been PATCH-for-additive (deliberate MINOR/MAJOR moments are user-decided, not automatic). Atomic-triplet bump — `@stoachain/kadena-stoic-legacy` and `@stoachain/stoa-core` bumped 4.2.1 → 4.2.2 in lockstep to satisfy the cross-package version-pin invariant (`tests/v4-1-1-cross-package-version-pin.test.ts`); those two packages are functionally identical to their 4.2.1 release.
98
+
99
+ ### Added — SWP-pair management (TS01-C3.SWP + INFO-ONE.SWP + SWP.UR_*)
100
+
101
+ - **Modify Can Change Owner** (button #2 of 9): `buildModifyCanChangeOwnerPactCode({ patron, swpair, newBoolean })` in `src/pact/cfmBuilders.ts`; `getModifyCanChangeOwnerInfo(patron, swpair, newBoolean)` in `src/interactions/infoOneFunctions.ts`; `getSwpairCanChangeOwner(swpair)` UR read in `src/interactions/dexSwapPairAdminFunctions.ts`. Tests for the builder in `tests/cfm-builders.test.ts`.
102
+ - **Modify Weights** (button #3 of 9, Weighted pools only): `buildModifyWeightsPactCode({ patron, swpair, newWeights: string[] })` in `src/pact/cfmBuilders.ts` — emits the canonical 3-arg shape with a Pact `[decimal]` list literal, each weight formatted via `formatDecimalForPact` (closes F-SEC-001). `getModifyWeightsInfo(patron, swpair, newWeights)` in `src/interactions/infoOneFunctions.ts`. UI-side constraints (length matches pool token count, each ≤4 fractional digits, sum exactly 1) enforced in the consuming modal; chain re-validates as defense.
103
+ - **Toggle Swapping** + **Toggle Provisioning** (buttons #4 + #5 of 9, paired commit since they're structurally identical): `buildToggleSwapCapabilityPactCode({ patron, swpair, toggle })` + `buildToggleAddLiquidityPactCode({ patron, swpair, toggle })` in `src/pact/cfmBuilders.ts`. `getToggleSwapCapabilityInfo` + `getToggleAddLiquidityInfo` in `src/interactions/infoOneFunctions.ts`. `getSwpairCanSwap(swpair)` + `getSwpairCanAdd(swpair)` UR reads in `src/interactions/dexSwapPairAdminFunctions.ts`. Same autonomous-boolean-as-inverse-of-current pattern as `C_ModifyCanChangeOwner` (chain rejects same-value writes). Tests in `cfm-builders.test.ts` cover bare-bool literal (NOT quoted), `C_ToggleSwapCapability` vs `C_ToggleAddLiquidity` disambiguation guard, and argument order.
104
+
105
+ (remaining 6 buttons — Modify Amplifier, Activate Frozen LP, Activate Sleeping LP, Update Special Fee Targets, Update Pending Branding, Upgrade Branding — will land in v4.2.3 alongside OuronetUI v1.0.9 once wired)
106
+
107
+ ## 4.2.1 — 2026-05-16
108
+
109
+ **PATCH — additive: new SWP `C_ChangeOwnership` builder + matching INFO reader, the `UR_OwnerKonto` lightweight read, and the full **Phase-3b strategy-migration builder set** (23 new builders + 1 INFO reader total) so the downstream OuronetUI v1.0.7 cycle could migrate every legacy `executeXxx` direct-helper caller (14 modals) onto the canonical `useCFMStrategy + buildXxxPactCode` pattern.** Atomic-triplet bump (`@stoachain/kadena-stoic-legacy@4.2.1` + `@stoachain/stoa-core@4.2.1` + `@ouronet/ouronet-core@4.2.1`) per the invariant enforced by `tests/v4-1-1-cross-package-version-pin.test.ts`. The other two packages are functionally identical to their 4.2.0 release — version bumped purely to satisfy the triplet model.
110
+
111
+ ### Added — SWP family (TS01-C3.SWP)
112
+
113
+ - **`buildChangeOwnershipPactCode({ patron, swpair, newOwner })`** in `src/pact/cfmBuilders.ts`. Emits `(ouronet-ns.TS01-C3.SWP|C_ChangeOwnership "<patron>" "<swpair>" "<new-owner>")`. Companion builder for the OuronetUI `ChangeOwnershipCFMModal` wired on the Pool Settings → Transfer Ownership button.
114
+ - **`getChangeOwnershipInfo(patron, swpair, newOwner)`** in `src/interactions/infoOneFunctions.ts`. Reads `(ouronet-ns.INFO-ONE.SWP|INFO_ChangeOwnership ...)` at T2, returns `null` on RPC failure (honoring the F-API-002 nullable contract).
115
+ - **`getSwpairOwnerKonto(swpair)`** in `src/interactions/dexSwapPairAdminFunctions.ts`. Lightweight T5 read of `(ouronet-ns.SWP.UR_OwnerKonto <swpair>)` returning just the owner-konto string. Used by `ChangeOwnershipCFMModal` to decide which ghost-receiver address to pre-fill so the ghost never collides with the current owner.
116
+
117
+ ### Added — Strategy-migration builders (Phase 3b)
118
+
119
+ The OuronetUI v1.0.7 cycle migrated all 14 legacy `executeXxx` direct-helper callers to `useCFMStrategy.execute({ build, guards, ... })` + typed builders. The following 22 builders were added in support:
120
+
121
+ **TS01-C2.LQD family — Wrap / Unwrap of native STOA + UrStoa:**
122
+ - `buildWrapStoaPactCode({ patron, wrapper, amount })`
123
+ - `buildWrapUrStoaPactCode({ patron, wrapper, amount })`
124
+ - `buildUnwrapStoaPactCode({ patron, unwrapper, amount })` — simple shape (target k:account exists)
125
+ - `buildUnwrapStoaWithCreateAccountPactCode({ patron, unwrapper, amount })` — composite multi-line Pact with `(namespace "ouronet-ns")` + `(IGNIS.C_Collect ...)` + `let` block that atomically `coin.C_CreateAccount`s the target then unwraps. Call site MUST `addData("ks", { keys: [<targetPubkey>], pred: "keys-all" })`.
126
+ - `buildUnwrapUrStoaPactCode({ patron, unwrapper, amount })` — simple shape (target exists)
127
+ - `buildUnwrapUrStoaWithCreateAccountPactCode({ patron, unwrapper, amount })` — UR variant; uses `coin.C_UR|CreateAccount` (NOT `coin.C_CreateAccount` — separate coin-module function for UrStoa accounts).
128
+
129
+ **TS01-C1.DALOS family — account deploy:**
130
+ - `buildDeployStandardAccountPactCode({ account, kadenaAddress, publicKey })`
131
+
132
+ **TS01-C3.SWP family — liquidity + swap:**
133
+ - `buildAddLiquidityPactCode({ patron, account, swpair, inputAmounts })`
134
+ - `buildRemoveLiquidityPactCode({ patron, account, swpair, lpAmount })`
135
+ - `buildSingleSwapWithSlippagePactCode({ patron, account, swpair, inputId, inputAmount, outputId })` — requires `addData("slippage-bounds", ...)` at call site.
136
+ - `buildSingleSwapNoSlippagePactCode({ patron, account, swpair, inputId, inputAmount, outputId })`
137
+ - `buildMultiSwapWithSlippagePactCode({ patron, account, swpair, inputIds, inputAmounts, outputId })` — requires `addData("slippage-bounds", ...)`.
138
+ - `buildMultiSwapNoSlippagePactCode({ patron, account, swpair, inputIds, inputAmounts, outputId })`
139
+
140
+ **TS02 token-set creation (SFT + NFT):**
141
+ - `buildCreateSetPactCode({ patron, account, id, nonces, setClass, howManySets })` — TS02-C1.DPSF.
142
+ - `buildCreateSetNFTPactCode({ patron, account, id, nonces, setClass })` — TS02-C2.DPNF (no how-many-sets).
143
+
144
+ **coin.C_URV family — StoaChain native UrStoa stake / unstake / collect (patronless):**
145
+ - `buildStakeUrStoaPactCode({ paymentKeyAddress, amount })` — emits `(coin.C_URV|Stake "<pk>" <amount>)`.
146
+ - `buildUnstakeUrStoaPactCode({ paymentKeyAddress, amount })` — emits `(coin.C_URV|Unstake "<pk>" <amount>)`.
147
+ - `buildCollectUrStoaPactCode({ paymentKeyAddress })` — simple shape, account exists.
148
+ - `buildCollectUrStoaWithCreateAccountPactCode({ paymentKeyAddress })` — composite with `coin.C_CreateAccount` + `(coin.C_URV|Collect ...)`. Requires `addData("ks", ...)`.
149
+
150
+ **coin.C_UR family — StoaChain native UrStoa transfer (4 conditional shapes):**
151
+ - `buildNativeUrTransferPactCode({ sender, receiver, amount })` — receiver exists, Transfer family.
152
+ - `buildNativeUrTransmitPactCode({ sender, receiver, amount })` — receiver exists, Transmit family.
153
+ - `buildNativeUrTransferAnewPactCode({ sender, receiver, amount })` — receiver new, Transfer family. Requires `addData("ks", { keys: [<receiverPub>], pred: "keys-all" })`.
154
+ - `buildNativeUrTransmitAnewPactCode({ sender, receiver, amount })` — receiver new, Transmit family. Same `addData` requirement.
155
+
156
+ ### Test surface
157
+
158
+ - `tests/cfm-builders.test.ts` grew from 52 → **108 specs**. Each new builder gets a canonical-shape test + argument-ORDER guard + module/function-name guard. Decimal-formatting tests for amount-typed builders verify integers pad to `x.0` form (closes the F-SEC-001 Pact-code injection vector). Composite-shape builders (`*WithCreateAccount`, `*Anew`) additionally test the create-account-precedes-the-real-call ordering invariant. Cross-cutting `it.each` valid-shape sample list extended with the simple `ouronet-ns` builders; coin.* builders deliberately excluded (they emit `(coin.* ...)` which fails the `(ouronet-ns.` prefix assertion by design — they're tested in their own describe blocks).
159
+
160
+ ### Note on the legacy `executeXxx` helpers
161
+
162
+ The pre-existing `executeWrapStoa`, `executeStakeUrStoa`, `executeAddLiquidity`, `executeSingleSwapWithSlippage`, etc. helpers in `src/interactions/*Functions.ts` are **retained** in this release — they're unused by OuronetUI as of v1.0.7 but may have other surfaces or be public API. Deletion deferred to a v4.3 cleanup pass.
163
+
164
+ ### Compatibility
165
+
166
+ - Pure additive — no signature changes to existing exports. Peer-deps bumped to 4.2.1 only for atomic-triplet alignment (functionally identical to 4.2.0 on the chain-generic side). Consumers on `4.2.0` continue to work unchanged for everything already on 4.2.0; consumers wanting any of the new SWP / Wrap / Unwrap / DALOS / Liquidity / Swap / TokenSet / coin.C_URV / coin.C_UR builders upgrade to `4.2.1`.
167
+
168
+ ## 4.2.0 — 2026-05-09
169
+
170
+ **MINOR — architectural closures + INTEGRATION-GUIDE deliverable + atomic-triplet bump (atomic with `@stoachain/kadena-stoic-legacy@4.2.0` + `@stoachain/stoa-core@4.2.0`).** Released 2026-05-09. Closes 6 audit findings (F-ARCH-001/002/003 god-file splits + F-API-002 nullable contract + F-API-018 readonly sweep + F-TEST-006 coverage expansion) plus a NEW deliverable (`INTEGRATION-GUIDE.md` at repo root).
171
+
172
+ ### Changed — audit closures
173
+
174
+ - **REQ-03..REQ-07 / F-ARCH-001 — Phase 1 dex god-file split.** `interactions/dexFunctions.ts` (~600 LOC, 7-entity Ouronet taxonomy) decomposed into ~10 entity-oriented files: `dexSwapPairCalcFunctions.ts`, `dexSwapPairExecFunctions.ts`, `dexLiquidityCalcFunctions.ts`, `dexLiquidityExecFunctions.ts`, `dexFuelCalcFunctions.ts`, `dexFuelExecFunctions.ts`, `dexDashboardFunctions.ts`, `dexAccountSuppliesFunctions.ts`, `dexCappedInverseFunctions.ts`, `dexTypes.ts` (the shared types module). Old import path `@ouronet/ouronet-core/interactions/dexFunctions` continues to work as a thin re-export shim for backward compatibility; new entity-oriented subpaths are recommended for tree-shaking.
175
+
176
+ - **REQ-08..REQ-13 / F-ARCH-002 — Phase 2 ouro god-file split + chain/UI surgical separation.** `interactions/ouroFunctions.ts` (~2200 LOC) decomposed into ~11 entity-oriented files separating chain-side (RPC builders, signing pipelines) from UI-side (display formatting, dashboard reads) surfaces per the locked principle. Old import path `@ouronet/ouronet-core/interactions/ouroFunctions` continues to work as a thin re-export shim.
177
+
178
+ - **REQ-14..REQ-17 / F-ARCH-003 — Phase 3 parameterized liquidity executor.** `executeAddLiquiditySingle`, `executeAddLiquidity`, `executeSpecialAddLiquidity`, `executeFuel`, `executeRemoveLiquidity` consolidated into 5 thin wrappers + 1 internal `executeLiquidityOp` (LOC reduction ~600 → ~200). Public function signatures preserved verbatim — zero consumer impact. Buffer-strategy reconciliation note (Phase 3 T3.5): _"spec text mentioned 3 buffer strategies; codebase has 2 distinct (`fixed-5k`, `auto-gas-limit`); the third is a degenerate no-rebuild sub-branch of `auto-gas-limit`, not a separate strategy. The 2-member union is technically correct."_
179
+
180
+ - **REQ-18..REQ-20 / F-API-002 — Phase 4 nullable contract honoring.** 12 swap-calc and dashboard-read functions whose declared return type was `Promise<T | null>` now honor that contract — they return `null` on RPC failure (with `logger.error` invoked first) instead of rethrowing. Affected functions include `getSWPairDashboardInfo`, `getPoolPreviewData`, `getSWPairMultiDashboardInfo`, `getSwpairInternalDashboard`, `calculateDirectSwap`, `calculateInverseSwap`, `calculateDirectSwapB`, `calculateInverseSwapB`, `getCappedInverseAmount`, `getUserAccountSupplies`, plus 2 additional dashboard-read functions. Carry-forward snippet (Phase 4 T4.4): _"The 10 swap-calc and dashboard-read functions whose declared return type was `Promise<T | null>` now honor that contract — they return null on RPC failure (with logger.error invoked first) instead of rethrowing. Existing try/catch consumer patterns continue to work. New code can rely on the static-type signature with the `if (result === null)` pattern."_
181
+
182
+ - **REQ-21..REQ-24 / F-API-018 — Phase 5 readonly sweep across ouronet-core public types.** Aggressive `readonly` modifier sweep across ~50 public type fields in `codex/types.ts` and all `*Params` interfaces. TypeScript-only signal — no runtime change.
183
+
184
+ - **REQ-27..REQ-30 / F-TEST-006 — Phase 7 coverage expansion.** +127 specs across 6 modules: `infoOneFunctions`, `coilFunctions`, `kpayFunctions`, `pensionFunctions`, `activateFunctions`, `guardFunctions`. The audit's stated 37 untested functions corrected to 38 (the 2026-05-05 audit missed `guardFunctions.describeKeyset`); absorbed by Phase 7 as +1 function = +3 it-blocks; closure transition is from "PARTIAL — 38 untested" → "CLOSED-VERIFIED".
185
+
186
+ ### Added — NEW deliverable
187
+
188
+ - **REQ-31..REQ-34 / Phase 8 — INTEGRATION-GUIDE.md at repo root.** New comprehensive cold-start consumer onboarding doc at `Z:\OuronetCore\INTEGRATION-GUIDE.md` (sibling to `MIGRATION-v4.md` + `MIGRATION-v4.1.md` + `MIGRATION-v4.2.md`). 13 mandated sections covering the full v4.0 → v4.1 → v4.2 architectural arc: install + peer-deps, the 3-package atomic-release model, subpath imports per package, the 5 typed error classes, the 7-entity Ouronet taxonomy, the 3 pluggable seams (`setPactReader`, `KeyResolver`+`PactClient`, `BalanceResolver`), codex backup format `"1.2"`, smart-account auth, gas calibration, full quick-start example. Doc-validity test (`tests/v4-2-0-integration-guide-validity.test.ts`) verifies all cited subpaths resolve, all cited error classes import, all cited seam functions are exported.
189
+
190
+ ### Test surface
191
+
192
+ - 8 new `v4-2-0-*.test.ts` files in this package: `dex-split-subpaths`, `ouro-split-subpaths`, `add-liquidity-executor-parity`, `f-api-002-null-contract`, `readonly-invariant`, `info-one-coverage`/`coil-coverage`/`kpay-coverage`/`pension-coverage`/`activate-coverage`/`guard-functions-coverage` (Phase 7 coverage expansion fans out to 6 module-specific files), `integration-guide-validity` (Phase 8 doc-validity test).
193
+ - Test count: ~330 (v4.1.1) → ~710 (v4.2.0). Aggregate growth: +127 from Phase 7 + ~250 from Phases 1-5 + 8 regression-lock specs.
194
+
195
+ ### Version
196
+
197
+ - Atomic-triplet bump 4.1.1 → 4.2.0.
198
+ - Peer-deps `@stoachain/kadena-stoic-legacy` and `@stoachain/stoa-core` both aligned to `4.2.0`.
199
+
200
+ ### Migration
201
+
202
+ - For consumers using the documented subpath APIs: most changes are transparent. The two consumer-visible deltas are (a) public-type fields are now `readonly` (TypeScript-only signal — switch in-place mutations to spread-copy patterns) and (b) the 12 nullable-contract functions reliably return `null` on failure (existing try/catch patterns continue to work; new code can use the `if (result === null)` pattern).
203
+ - See [`MIGRATION-v4.2.md`](https://github.com/StoaChain/stoa-js/blob/main/MIGRATION-v4.2.md) for the full upgrade map and [`INTEGRATION-GUIDE.md`](https://github.com/StoaChain/stoa-js/blob/main/INTEGRATION-GUIDE.md) for cold-start onboarding.
204
+
205
+ ## 4.1.1 — 2026-05-08
206
+
207
+ ### Added — typed error classes (v4.1.1 audit closures)
208
+ - `KadenaShapeError` (`src/interactions/errors.ts`, REQ-01 / F-ERR-022): RPC envelope shape-mismatch error. Mirrors `InvalidEnvelopeError` from stoa-core (extends Error with ES2022 `cause`).
209
+ - `CodexUnknownFieldError` and `UnknownSeedTypeError` (`src/codex/errors.ts`, REQ-08 / REQ-12): codex-domain validation errors.
210
+
211
+ ### Changed — audit closures
212
+ - **REQ-01 (F-ERR-022):** `kadenaFunctions.ts` lines 16+27 — fabricated `?? "0"` and `|| "0"` fallbacks dropped. Now throws typed `KadenaShapeError` on shape-mismatched RPC envelopes. The legitimate `account || address` and `guard || null` fallbacks at lines 28-29 are preserved.
213
+ - **REQ-02 (F-API-005):** `getSublimateInfo` dedup — duplicate body at `ouroFunctions.ts:2148` removed and replaced with a `@deprecated` JSDoc compat shim that adapts the legacy `(patron, resident, amount-as-string)` signature to canonical form. Canonical at `infoOneFunctions.ts:179`. Shim removed in v4.2.0.
214
+ - **REQ-03 (F-API-006):** `describeKeyset` dedup — private duplicate at `urStoaFunctions.ts:89` removed. Helper `describeKeysetOrNull` preserves the two behaviors the canonical does NOT have: `pred ?? "keys-all"` coercion and map-keys-not-array-to-null. Canonical at `guardFunctions.ts:62`.
215
+ - **REQ-08 (F-SEC-007):** `deserializeCodex` rejects envelopes with unknown top-level fields (against KNOWN_TOP_LEVEL_FIELDS = {version, exportedAt, kadenaWallets, ouronetWallets, addressBook, uiSettings}).
216
+ - **REQ-12 (F-BUG-010):** `migrateSeedType` no longer silently returns `"koala"` for unknown seed types. Now throws `UnknownSeedTypeError`. The idempotence test at `codex-codec.test.ts:345-352` updated to drop `"garbage"` from the inputs array.
217
+ - **REQ-16 (F-API-014):** `getSparksBalance` return type narrowed from `Promise<any>` to `Promise<any | null>`.
218
+
219
+ ### Test surface
220
+ - 11 new v4-1-1-*.test.ts files: kadena-no-fallbacks, codec-strict-shape, seed-type-strict, seed-type-dedup, sparks-balance-narrow, dist-structure, esm-roundtrip, type-preservation, doc-gates, migration-doc-validity, package-metadata, peer-dep-coverage, full-chain-integration, cross-package-version-pin, publish-workflow-simulation, coil-functions-memoization.
221
+ - Test count: ~245 → ~330 specs.
222
+
223
+ ### Version
224
+ - Atomic-triplet bump 4.1.0 → 4.1.1.
225
+ - Peer-deps `@stoachain/kadena-stoic-legacy` and `@stoachain/stoa-core` both aligned to `4.1.1`.
226
+
227
+ ### Migration
228
+ - See `MIGRATION-v4.1.md` v4.1.1 appendix for caller-impact details (typed throws replacing fallbacks, deprecation shim, new error classes).
229
+
230
+ ## 4.1.0 — 2026-05-07
231
+
232
+ **MINOR — sovereign supply-chain migration (atomic with `@stoachain/stoa-core@4.1.0` + `@stoachain/kadena-stoic-legacy@4.1.0`).** Retargets every internal `@kadena/*` import in `src/interactions/` to the new sibling subpaths under [`@stoachain/kadena-stoic-legacy`](https://www.npmjs.com/package/@stoachain/kadena-stoic-legacy) — a sovereign vendoring of `@kadena/{client,cryptography-utils,types,hd-wallet}` under StoaChain stewardship.
233
+
234
+ ### Why
235
+
236
+ Post-Kadena-LLC, the StoaChain ecosystem cannot accept supply-chain risk on unmaintained upstream npm packages. v4.0.0 pinned the `@kadena/*` peer-deps to exact versions (no `^`) as prep work — v4.1.0 is the follow-through: drop the upstream peer-deps entirely, depend on the StoaChain-stewarded vendored sibling, lock the migration with a runtime regression test that fails if any `@kadena/*` literal sneaks back into source or built output.
237
+
238
+ ### What changed
239
+
240
+ - **12 imports retargeted across `src/interactions/`.** All 12 internal `@kadena/*` imports across the 13 `interactions/*` modules (`ouroFunctions`, `coilFunctions`, `pensionFunctions`, `guardFunctions`, `infoOneFunctions`, `wrapFunctions`, etc.) rewired to `@stoachain/kadena-stoic-legacy/{client,types}`. Each retarget preserves the imported symbol set byte-identically — `Pact`, `createClient`, `ICommand`, `IUnsignedCommand`, `ChainId` — the vendored module re-exports the upstream surface verbatim.
241
+ - **Peer-deps trimmed.** Three `@kadena/*` peer-dep declarations (`@kadena/client@1.18.3`, `@kadena/cryptography-utils@0.4.4`, `@kadena/types@0.7.0`) removed from `package.json`. Single `@stoachain/kadena-stoic-legacy: "4.1.0"` exact-pin added. `@stoachain/stoa-core` peer-dep bumped `4.0.1 → 4.1.0` (atomic-version invariant — all three packages always release at the same version).
242
+ - **Public function signatures unchanged.** Every interactions function (`getOuronetKdaDetails`, `getCoilPreviewGeneric`, every `pension*` / `guard*` / `infoOne*` / `wrap*` / `unwrap*` / `migrate*` builder) retains byte-identical shape. Consumers who import via `@ouronet/ouronet-core/interactions/*` see no breaking change.
243
+ - **Regression-lock added.** `tests/v4-1-0-no-kadena-imports.test.ts` (43 specs) walks `dist/**/*.{js,d.ts}` + `src/**/*.ts` and asserts no `@kadena/*` literal occurs in any import statement, type reference, or string. Fails the CI build if a future regression silently reintroduces an upstream `@kadena/*` dependency.
244
+
245
+ ### Tests
246
+
247
+ **261/261 pass** (was 218 in v4.0.1; +43 from the new regression-lock).
248
+
249
+ ### Migration
250
+
251
+ For consumers importing through subpath, **no migration required** — every `interactions/*` and `pact/cfm` public surface keeps its byte-identical shape. The upstream `@kadena/*` peer-deps are gone from this package's `package.json`, but consumers who keep a direct dependency on those upstream packages in their own code are unaffected (npm dedupes the @kadena tree at the consumer level). See [`MIGRATION-v4.1.md`](https://github.com/StoaChain/stoa-js/blob/main/MIGRATION-v4.1.md) at the monorepo root for the full upgrade map.
252
+
253
+ ## 4.0.1 — 2026-05-06
254
+
255
+ **PATCH, cosmetic (published-metadata cleanup).** Strips the redundant `devDependencies` block from `package.json`. Pre-v4.0.1 the published manifest carried a `devDependencies` block that contained `@stoachain/stoa-core: "*"` (workspace-resolution plumbing — meaningless on a published artifact since the `peerDependency` already pins `@stoachain/stoa-core@4.0.1`) plus duplicates of the `@kadena/*` peer entries. The npmjs.com page now shows the cleaner shape: zero `dependencies`, just the canonical `peerDependencies` (the @kadena/* set + `@stoachain/stoa-core@4.0.1` exact-pin). The peer-dep on `@stoachain/stoa-core` was bumped from `4.0.0` to `4.0.1` (atomic-version invariant — both packages always release at the same version). NO source-code change. NO behaviour change. NO breaking change. **218/218 tests pass** (regression-lock test `tests/package-version.test.ts` updated to assert `4.0.1`).
256
+
257
+ ## 4.0.0 — 2026-05-06
258
+
259
+ **MAJOR, breaking (monorepo split + deprecated-alias removal).** v4.0.0 is the structural refactor that v3.3.8 set up. The single `@ouronet/ouronet-core` package was split into two atomic-release npm packages under the new `StoaChain/stoa-js` GitHub monorepo:
260
+
261
+ - **[`@stoachain/stoa-core`](https://www.npmjs.com/package/@stoachain/stoa-core)** — chain-generic StoaChain foundation (signing, wallet, crypto, network failover, gas, guard, errors, observability, dalos, reads, pact-format).
262
+ - **`@ouronet/ouronet-core`** (this package) — Ouronet protocol business logic (codex codec, interactions/* function library, `KADENA_NAMESPACE`, `STOA_AUTONOMIC_*` accounts, cfm Pact builders).
263
+
264
+ Both packages release atomically out of the monorepo at the same version — a single `vX.Y.Z` git tag publishes both.
265
+
266
+ ### What this means for consumers
267
+
268
+ If you imported _only_ Ouronet-specific surfaces (codex, interactions, the `ouronet-ns` namespace), you can keep `@ouronet/ouronet-core` and just bump the version. If you imported chain-generic surfaces (signing, wallet, crypto, network, etc.), those moved to `@stoachain/stoa-core` — install both packages and update the import paths. See `MIGRATION-v4.md` at the monorepo root for the full upgrade map.
269
+
270
+ ### Breaking removals
271
+
272
+ The deprecated aliases marked `@deprecated` in v3.3.8 were removed:
273
+
274
+ - `KADENA_BASE_URL` — pinned to `node2.stoachain.com` and bypassed the v2.1.0 failover layer. Migration: `import { getPactUrl, getSpvUrl } from "@stoachain/stoa-core/constants"` (failover-aware, takes a chainId argument).
275
+ - `PACT_URL` — same reasoning. Migration: `getPactUrl("0")`.
276
+ - `GAS_STATION` — alias for `STOA_AUTONOMIC_OURONETGASSTATION`. Migration: rename consumer references to the canonical `STOA_AUTONOMIC_OURONETGASSTATION` (still exported from `@ouronet/ouronet-core/constants`).
277
+ - `NATIVE_TOKEN_VAULT` — alias for `STOA_AUTONOMIC_LIQUIDPOT`. Migration: `STOA_AUTONOMIC_LIQUIDPOT`.
278
+
279
+ The `IKadenaKeypair` interface duplicate that lived in `interactions/ouroFunctions.ts` (Phase-2b backwards-compat copy) was also removed. The canonical source is `@stoachain/stoa-core/signing`. Migration: `import type { IKadenaKeypair } from "@stoachain/stoa-core/signing"`.
280
+
281
+ ### Internal moves (transparent to consumers using subpath imports)
282
+
283
+ - Chain-generic constants (`KADENA_NETWORK`, `KADENA_CHAIN_ID`, `STOA_CHAINS`, `STOA_CHAIN_COUNT`, `KADENA_CHAINS`, `getPactUrl`, `getSpvUrl`) moved to `@stoachain/stoa-core/constants`. Re-exported through `@ouronet/ouronet-core/constants` for source-level back-compat with internal `../constants` imports — direct `@ouronet/ouronet-core/constants` consumers continue to work, but new code SHOULD import them from `@stoachain/stoa-core/constants` to make the chain-generic vs Ouronet-specific boundary explicit.
284
+ - `pact/cfmBuilders.ts` (the cfm Pact-code string assembler) stays under the `@ouronet/ouronet-core/pact` subpath because it uses `KADENA_NAMESPACE` (Ouronet-specific). The chain-generic `formatDecimalForPact` / `formatIntegerForPact` / `mayComeWithDeimal` / `filterFreePositionData` / `formatEU` / `safeCreationTime` helpers now live in `@stoachain/stoa-core/pact`. Consumers of the chain-generic helpers should import from `@stoachain/stoa-core/pact`; consumers of cfm builders continue to import from `@ouronet/ouronet-core/pact`.
285
+
286
+ ### Dependency hardening
287
+
288
+ All `@kadena/*` peer/dev deps + `@noble/curves` + `@scure/bip39` + `@ouronet/dalos-crypto` pinned to exact versions (no `^` ranges). This is the prep work for v4.1.0's selective `@kadena/client` vendoring (supply-chain hardening after Kadena LLC's dissolution) — pinning first means we can audit the exact bytes a consumer pulls in.
289
+
290
+ ### Tests
291
+
292
+ 703/703 passing across the two packages (485 in `@stoachain/stoa-core`, 218 in `@ouronet/ouronet-core`). The v3.3.8 regression-lock test file split — F-ARCH-011/F-ARCH-012 locks now live in `@stoachain/stoa-core/tests/v3-3-8-doc-cleanup.test.ts` (their SUTs are stoa-core-side); F-API-016 lock stays in `@ouronet/ouronet-core/tests/v3-3-8-doc-cleanup.test.ts` (CoilConfig is ouronet-core-side).
293
+
294
+ ## 3.3.8 — 2026-05-06
295
+
296
+ **MINOR, additive (documentation/deprecation cleanup pass).** Closes 5 LOW-severity findings from the 2026-05-05 audit's `"v3.x deprecation cleanup"` + `"v3.x conventions alignment"` + `"v3.x API hygiene"` themes in a single bundled release. **One new public-API export** (`CoilConfig` interface, previously consumed but un-exported), **one `@deprecated` marker** (`KADENA_BASE_URL` redirecting consumers to the failover-aware reader), **two doc fixes** (stale JSDoc + import-discipline cleanup), **one stylistic cleanup** (single-quote → double-quote drift in dalos/account.ts). **NO breaking change**, **NO observable runtime behavior change**, **698/698 tests pass** (was 695 in v3.3.7; +3 from the new `tests/v3-3-8-doc-cleanup.test.ts` regression-lock file).
297
+
298
+ ### F-API-015 — Stale `strict` JSDoc parameter mention
299
+
300
+ `src/dalos/account.ts:42-45` previously documented a `strict` parameter on `CreateAccountOptions` that the type union never had. v3.3.8 rewrites the JSDoc to document the actual mode-vs-primitive contract: if the selected primitive doesn't support the requested `mode` (e.g. `bitmap` on a non-DalosGenesis primitive), `createOuronetAccount` throws a descriptive error from the call site — there is no opt-out flag. Consumers wrap the call in their own try/catch if they want a non-throwing fallback.
301
+
302
+ Pure docstring change. No runtime regression to lock. The CHANGELOG entry is the audit trail.
303
+
304
+ ### F-API-016 — Export `CoilConfig` interface
305
+
306
+ `src/interactions/coilFunctions.ts:17` previously declared `interface CoilConfig` (no `export`). The interface backs the exported `COIL_CONFIGS` constant, so consumers reaching into `COIL_CONFIGS.ouroToAuryn` got a value of type `CoilConfig` but couldn't TYPE-ANNOTATE a parameter or local with `CoilConfig` themselves without re-declaring the shape. v3.3.8 adds `export`. One-word change.
307
+
308
+ ```ts
309
+ // Now works:
310
+ import { CoilConfig, COIL_CONFIGS } from "@ouronet/ouronet-core/interactions/coilFunctions";
311
+
312
+ function describeCoil(config: CoilConfig): string {
313
+ return `${config.sourceToken} → ${config.targetToken}`;
314
+ }
315
+ ```
316
+
317
+ Locked at `tests/v3-3-8-doc-cleanup.test.ts` T1: `expectTypeOf<CoilConfig>().toEqualTypeOf<{...}>()` — if a future edit drops the `export`, TS errors at typecheck time and the test fails to compile.
318
+
319
+ ### F-SEC-005 / F-ARCH-014 — Mark `KADENA_BASE_URL` `@deprecated`
320
+
321
+ `src/constants/kadena.ts:17` exports `KADENA_BASE_URL = "https://node2.stoachain.com/chainweb/0.0/${KADENA_NETWORK}"` — pinned to node2, bypassing the failover layer added in v2.1.0. Direct consumers reading this constant lose node-recovery + node-degradation handling. v3.3.8 adds a prominent `@deprecated` JSDoc redirecting to `getActivePactUrl(chainId)` / `getActiveSpvUrl(chainId)` (or the same-subpath thin wrappers `getPactUrl(chainId)` / `getSpvUrl(chainId)`). The constant itself is preserved for consumer backwards-compat — removal is scheduled for v4.0.0; consumers reading it directly should migrate before the major bump.
322
+
323
+ The TypeScript compiler emits `Type 'string' is deprecated` warnings to consumers reading the constant directly when their `tsconfig` has `"reportsDeprecated": true` (or via TS-Server / IDE indicators). Consumers who never read the constant directly (the recommended pattern — they call `getPactUrl(chainId)` instead) see no change.
324
+
325
+ Pure JSDoc change. No runtime regression to lock.
326
+
327
+ ### F-ARCH-011 — Consolidate `normalizeKeysetRef` import to `../guard` barrel
328
+
329
+ `src/interactions/ouroFunctions.ts:10` previously deep-imported:
330
+
331
+ ```ts
332
+ import { IKeyset } from "../guard";
333
+ import { normalizeKeysetRef } from "../guard/smartAccountAuth"; // deep import
334
+ ```
335
+
336
+ Inconsistent with the project's subpath-import discipline — every other consumer of `normalizeKeysetRef` reaches through the `../guard` barrel (which already re-exports `* from "./smartAccountAuth"`). v3.3.8 consolidates:
337
+
338
+ ```ts
339
+ import { IKeyset, normalizeKeysetRef } from "../guard";
340
+ ```
341
+
342
+ Behaviorally identical (same symbol, same module). Locked at `tests/v3-3-8-doc-cleanup.test.ts` T2: imports `normalizeKeysetRef` through the barrel and smoke-calls it with a `keysetref`-shaped object, asserting the round-trip works. Catches a regression that drops the `export *` line in `src/guard/index.ts` (which would also break ouroFunctions.ts).
343
+
344
+ ### F-ARCH-012 — Single-quote → double-quote drift in `src/dalos/account.ts`
345
+
346
+ `src/dalos/account.ts` was the only file in `src/` still using single-quoted string literals (the `CreateAccountMode` discriminator labels `'random'` / `'bitString'` / etc., the typeof guard string `'function'`, the fallback string `'(default)'` — 19 sites total). v3.1.1 fixed `src/dalos/index.ts` but missed account.ts. v3.3.8 converts all 19 sites to double quotes, matching the rest of the project.
347
+
348
+ Pure stylistic change — TypeScript treats `'random'` and `"random"` as identical string literals at the type level. No observable behavior change.
349
+
350
+ Locked at `tests/v3-3-8-doc-cleanup.test.ts` T3: reads `src/dalos/account.ts` source verbatim, walks each line, strips JSDoc + single-line comments (English apostrophes in `registry's` / `doesn't` are exempt), and asserts no single-quote characters remain in the code-only portion. A future edit that introduces single-quoted string literals fails the test.
351
+
352
+ ### Added — `tests/v3-3-8-doc-cleanup.test.ts` (3 it-blocks across 3 describe groups)
353
+
354
+ | Test | Closes | What regression it catches |
355
+ |---|---|---|
356
+ | **T1** (F-API-016) | `CoilConfig` type export | Future edit drops `export` keyword → TS typecheck fails on the import line |
357
+ | **T2** (F-ARCH-011) | `normalizeKeysetRef` barrel reachability + smoke-call | Future edit removes `export *` from `src/guard/index.ts` → import fails OR function shape changes |
358
+ | **T3** (F-ARCH-012) | Quote-style invariant on `src/dalos/account.ts` | Future edit introduces single-quoted string literals → grep walker fails the test |
359
+
360
+ F-API-015 (stale JSDoc) and F-SEC-005/F-ARCH-014 (`@deprecated` marker on `KADENA_BASE_URL`) are pure JSDoc changes that don't surface at runtime — no test added. The CHANGELOG entry is the audit trail.
361
+
362
+ ### Verified
363
+
364
+ - `npm run typecheck` — zero errors. The new `CoilConfig` export type-checks cleanly across the three import sites that consume it (`getCoilPreviewGeneric`, `coilTokensGeneric`, the new test file). The `@deprecated` JSDoc on `KADENA_BASE_URL` is well-formed (TS picks it up as a deprecation indicator at consumer usage sites).
365
+ - `npm test` — **698/698 tests pass** (was 695 in v3.3.7; +3 from `tests/v3-3-8-doc-cleanup.test.ts`).
366
+ - `npm run build` — clean tsc emit. Change surface is `src/dalos/account.ts` (JSDoc rewrite + 19 quote-style edits), `src/interactions/coilFunctions.ts` (added `export`), `src/constants/kadena.ts` (`@deprecated` JSDoc), `src/interactions/ouroFunctions.ts` (consolidated imports — 1 line removed, 1 line edited).
367
+
368
+ ### Migration
369
+
370
+ No consumer migration. The package's pre-v3.3.8 public API surface is byte-identical (with one ADDITION — the `CoilConfig` type export). Two consumer-side notes:
371
+
372
+ - **Consumers reading `KADENA_BASE_URL` directly** see a TypeScript deprecation warning in IDEs / on `tsc` runs with `"reportsDeprecated": true`. They are encouraged to migrate to `getPactUrl(chainId)` / `getSpvUrl(chainId)` (failover-aware) before v4.0.0, when `KADENA_BASE_URL` is scheduled for removal. Consumers using the recommended `getPactUrl(chainId)` pattern see no change.
373
+ - **Consumers using `CoilConfig` as a type annotation** can now import the type by name from `@ouronet/ouronet-core/interactions/coilFunctions` (previously they had to re-declare the shape inline).
374
+
375
+ ### v3.3.x trajectory remaining (post-v3.3.8)
376
+
377
+ - **v3.3.9 / v3.4.0** — Dependency-hygiene release (pin exact versions of `@kadena/*` peerDeps; possibly vendor `@kadena/types` per the supply-chain risk discussion). Per the user's sequencing decision: this is the next planned release after v3.3.8.
378
+ - **v4.0.0** — Major structural release (monorepo split into `@stoachain/stoa-core` + `@ouronet/ouronet-core`; god-file decomposition; F-ARCH-009 21-site GAS_STATION migration; F-ARCH-010 IKadenaKeypair canonical-source consolidation; F-ARCH-003 helper extract; F-PERF-014 sleep-to-state-poll; KADENA_BASE_URL removal; the bigger fork-into-stoachain-scope work for `@kadena/cryptography-utils` / `@kadena/client` / `@kadena/hd-wallet`).
379
+
380
+ The v3.3.x audit-closure track now spans **testing + performance + security + documentation** categories. Remaining items in the audit are either reserved for v4.0.0 (arch + sleep replacement) or are NEEDS CONTEXT findings flagged for human review (F-ERR-014 multi-step add-liquidity timeout, F-API-018 readonly modifiers, F-BUG-006 ad-hoc decimal formatter, F-BUG-008 Σ-prefix validation in CodexSigningStrategy).
381
+
382
+ ---
383
+
384
+ ## 3.3.7 — 2026-05-06
385
+
386
+ **MINOR, additive (security pass).** Closes two MEDIUM security findings from the 2026-05-05 audit in a single bundled release: **F-SEC-003** (seam-setter input validation) and **F-SEC-004** (V1-fallback security advisory). Three new public-API additions (`InvalidPactReaderError`, `InvalidLoggerError`, `decryptStringV2WithDetails` + `smartDecryptWithDetails` + `DecryptResultWithDetails` + a one-shot `getLogger().warn(...)` advisory inside the V1-decrypt path). All changes preserve byte-identical backwards-compat with v3.3.6 — existing consumer code that calls `setLogger({warn, error})` (v3.2.x compat path) or catches `instanceof TypeError` continues to work. **NO breaking change**, **695/695 tests pass** (was 674 in v3.3.6; +21 from two new test files).
387
+
388
+ ### F-SEC-003 — Seam-setter input validation
389
+
390
+ Pre-v3.3.7 the two pluggable seams had inconsistent guards:
391
+
392
+ - `setPactReader(fn)` accepted **any** value (including `undefined`, `null`, numbers, plain objects). The misconfiguration only surfaced later as a confusing `_reader is not a function` at the first `pactRead(...)` call site, often far from the boot wiring that installed the bad value. v3.3.7 adds a `typeof reader !== "function"` guard that throws **`InvalidPactReaderError`** with a clear message naming the actual type passed (`received undefined`, `received null`, `received number`, etc.).
393
+
394
+ - `setLogger(logger)` only guarded `null`/`undefined`. Passing an object whose `warn` or `error` was non-callable (a typo'd field name, a half-finished test fixture, an `undefined` property access) silently installed the bad logger; the error surfaced later as `_logger.warn is not a function` at the first catch-block routing site. v3.3.7 adds shape validation that throws **`InvalidLoggerError`** with messages naming the specific invariant violated (`logger.warn must be a function`, `logger.error must be a function`, etc.).
395
+
396
+ Both new error classes extend `TypeError` so existing consumer `catch (e) { if (e instanceof TypeError) ... }` code is unchanged. The pre-v3.3.7 null/undefined message text on `setLogger` (`"setLogger requires a non-null Logger"`) is preserved verbatim — the v3.3.0 contract test at `tests/observability-logger.test.ts:68-80` that locks the byte-identical message continues to pass.
397
+
398
+ ```ts
399
+ // New typed errors:
400
+ import { InvalidPactReaderError } from "@ouronet/ouronet-core/reads";
401
+ import { InvalidLoggerError } from "@ouronet/ouronet-core/observability";
402
+
403
+ // Caught at boot now, not at first call site:
404
+ try {
405
+ setPactReader(myReader);
406
+ } catch (e) {
407
+ if (e instanceof InvalidPactReaderError) {
408
+ console.error("PactReader misconfigured:", e.message);
409
+ }
410
+ }
411
+ ```
412
+
413
+ ### F-SEC-004 — V1-fallback security advisory
414
+
415
+ V1 envelopes use **PBKDF2-SHA256 / 10,000 iterations / AES-GCM-256**. OWASP's password-storage cheat sheet (2023+) recommends a PBKDF2-SHA256 minimum of **600,000** iterations — V1 is meaningfully crackable on commodity GPU hardware in ways the V2 envelope (PBKDF2-SHA512 / 600,000 iterations) is not. V1 lingers in the codebase for backwards-compat: codex backups exported before the V2 upgrade still parse via the V1 path inside `decryptStringV2` (envelopes lacking `v: 2`) and the V1 primitive route inside `smartDecrypt`.
416
+
417
+ Pre-v3.3.7 these paths were silent — consumers had no way to detect that a successful decrypt had used legacy-strength KDF parameters, and could not surface "your codex uses outdated encryption" UI banners or trigger in-place re-encrypt flows. v3.3.7 ships:
418
+
419
+ #### One-shot `getLogger().warn(...)` advisory
420
+
421
+ Fires on the FIRST V1 decrypt per process lifetime:
422
+
423
+ ```
424
+ [ouronet-core/crypto] V1-format encrypted blob decoded successfully.
425
+ V1 uses PBKDF2-SHA256 at 10,000 iterations, well below OWASP's current
426
+ 600,000 minimum (cracked meaningfully faster on commodity GPU hardware).
427
+ Re-encrypt affected codex entries to V2 (PBKDF2-SHA512 / 600,000) at the
428
+ earliest opportunity. Use `decryptStringV2WithDetails` or
429
+ `smartDecryptWithDetails` for the per-call `wasLegacyV1` flag. This
430
+ warning fires once per process lifetime.
431
+ ```
432
+
433
+ The one-shot guard prevents bulk-decrypt log spam — a codex with 100 V1 entries logs **one** warning, not 100. Consumers wanting per-call programmatic detection use the new `*WithDetails` variants (below).
434
+
435
+ The advisory fires from BOTH code paths that reach V1: `decryptStringV2`'s V1-fallback branch (envelopes lacking `v: 2`) AND `smartDecrypt`'s shape-dispatch path that short-circuits to the V1 primitive. Without the duplicate hook in `smartDecrypt`, codex unlocks via the auto-detect entry point would silently skip the warning.
436
+
437
+ #### `decryptStringV2WithDetails(blob, password): Promise<{plaintext, wasLegacyV1}>`
438
+
439
+ Same failure contract as `decryptStringV2` (`CorruptEnvelopeError` / `WrongPasswordError`), but returns the rich shape so consumers can react programmatically per call:
440
+
441
+ ```ts
442
+ import { decryptStringV2WithDetails } from "@ouronet/ouronet-core/crypto";
443
+
444
+ const result = await decryptStringV2WithDetails(blob, password);
445
+ if (result.wasLegacyV1) {
446
+ // Re-encrypt to V2 in-place:
447
+ const upgraded = await encryptStringV2(result.plaintext, password);
448
+ await codexAdapter.replace(blobId, upgraded);
449
+ }
450
+ ```
451
+
452
+ #### `smartDecryptWithDetails(blob, password): Promise<{plaintext, wasLegacyV1}>`
453
+
454
+ Mirrors `smartDecrypt`'s shape-dispatch (V2 envelopes via `decryptStringV2`, non-V2 via the V1 primitive). The single best entry point for "decrypt this codex entry AND tell me whether it was a V1 envelope so I can re-encrypt next."
455
+
456
+ #### JSDoc CVE-style risk documentation
457
+
458
+ Added prominent security blocks to:
459
+
460
+ - `EncryptedDataV1` interface JSDoc — full OWASP context, why V1 lingers, recommended upgrade path
461
+ - `decryptStringV2` function JSDoc — V1-fallback path advisory + cross-references to the new variants
462
+ - The `EncryptedDataV1` JSDoc references the in-place re-encrypt pattern so future contributors reading the source see the security context inline
463
+
464
+ ### Added — `tests/v3-3-7-seam-validators.test.ts` (11 it-blocks across 3 describe groups)
465
+
466
+ | Group | Count | What it locks |
467
+ |---|---|---|
468
+ | **`setPactReader` input validation** | 4 | rejects `undefined` / `null` / non-function (number/string/object) → `InvalidPactReaderError`; accepts a valid `PactReader` function (no throw + replaces the seam) |
469
+ | **`setLogger` input-shape validation** | 5 | rejects `null` / `undefined` with the byte-identical pre-v3.3.7 message; rejects non-object inputs (string, number); rejects `{warn: undefined, error: () => {}}` naming the warn invariant; rejects `{warn: () => {}, error: 'oops'}` naming the error invariant — all via `InvalidLoggerError` |
470
+ | **`setLogger` backwards-compat preservation** | 2 | v3.3.0+ full-shape `{warn, error, info}` still installs cleanly with reference identity preserved; v3.2.x partial-shape `{warn, error}` (no info) still installs cleanly with synthesised `info` |
471
+
472
+ ### Added — `tests/v3-3-7-v1-warning.test.ts` (10 it-blocks across 3 describe groups)
473
+
474
+ | Group | Count | What it locks |
475
+ |---|---|---|
476
+ | **`decryptStringV2WithDetails`** | 3 | V2 envelope → `wasLegacyV1: false`; V1 envelope → `wasLegacyV1: true` AND plaintext correct; `WrongPasswordError` propagates from the rich variant (failure contract unchanged) |
477
+ | **`smartDecryptWithDetails`** | 2 | V2 envelope routes via `decryptStringV2` → `wasLegacyV1: false`; V1 envelope routes via the V1 primitive → `wasLegacyV1: true` |
478
+ | **One-shot warning behavior** | 5 | first V1 decrypt via `decryptStringV2` emits the security-advisory `getLogger().warn(...)`; second V1 decrypt is **silent** (one-shot guard intact — load-bearing for bulk-codex-decrypt UX); V2 decrypts NEVER emit the warning; `smartDecrypt`'s short-circuit path also emits the warning (separate code path needs its own hook); rich `*WithDetails` variants emit the warning identically (delegation chain intact) |
479
+
480
+ The V1-warning tests use the internal `_resetV1WarningEmittedForTests()` helper (NOT exported via the public barrel) to reset the one-shot guard between tests. Production code never calls this — the warning is intentionally one-shot per process lifetime.
481
+
482
+ ### Verified
483
+
484
+ - `npm run typecheck` — zero errors. The new `InvalidPactReaderError` / `InvalidLoggerError` / `DecryptResultWithDetails` types compile cleanly. `decryptStringV2WithDetails` and `smartDecryptWithDetails` delegate to the existing primitives with no shape change to the underlying functions.
485
+ - `npm test` — **695/695 tests pass** (was 674 in v3.3.6; +21 from `tests/v3-3-7-seam-validators.test.ts` (11) + `tests/v3-3-7-v1-warning.test.ts` (10)).
486
+ - `npm run build` — clean tsc emit. The change surface is `src/reads/pactReader.ts` (added error class + 3-line guard), `src/observability/logger.ts` (added error class + 4-clause shape guard), `src/crypto/v2.ts` (added one-shot guard + 2 rich variants + JSDoc + warning hooks at 2 V1 paths), `src/crypto/index.ts` (3 new exports).
487
+
488
+ ### Migration
489
+
490
+ No consumer migration required. The package's pre-v3.3.7 public API surface is byte-identical: `setPactReader(validFn)` works unchanged; `setLogger({warn, error})` works unchanged with the v3.3.0-synthesised info; `decryptStringV2(...)` and `smartDecrypt(...)` return the same `Promise<string>` and throw the same error classes.
491
+
492
+ The new APIs are purely additive:
493
+
494
+ - Consumers wanting the per-call `wasLegacyV1` signal opt in by switching `decryptStringV2` → `decryptStringV2WithDetails` (or `smartDecrypt` → `smartDecryptWithDetails`). The plain functions keep returning `Promise<string>` for ergonomic call sites that don't care.
495
+ - The one-shot `getLogger().warn(...)` advisory fires automatically with no opt-in. Consumers using a structured logger (OuronetUI's redux-devtools, HUB's pino) see the warning routed through their pipeline; consumers with no `setLogger` call see it on `console.warn` (the seam's default).
496
+
497
+ Two error-handling notes for callers who are STRICT about narrowing:
498
+
499
+ - `setPactReader(notAFunction)` **previously** silently installed the bad value. v3.3.7+ throws `InvalidPactReaderError`. Code that defensively wrapped the call to swallow errors no longer needs to (the throw catches the misconfiguration earlier). Code that NEVER wrapped is unaffected (it would never trigger the throw because it was passing valid functions all along).
500
+ - `setLogger({warn: undefined, ...})` **previously** silently installed the bad value. v3.3.7+ throws `InvalidLoggerError`. Same reasoning as above.
501
+
502
+ ### v3.3.x trajectory remaining (post-v3.3.7)
503
+
504
+ With v3.3.7 every MEDIUM finding from the 2026-05-05 audit's testing AND performance AND **security-input-validation** categories is closed. The track:
505
+
506
+ | Release | Findings | Track |
507
+ |---|---|---|
508
+ | v3.3.0 | F-LOGGER-SEAM-001 | logger seam |
509
+ | v3.3.2 | F-TEST-002 | testing |
510
+ | v3.3.4 | F-TEST-005 | testing |
511
+ | v3.3.5 | F-TEST-006 | testing |
512
+ | v3.3.6 | F-PERF-008, F-PERF-003, F-PERF-004 | performance |
513
+ | v3.3.7 | F-SEC-003, F-SEC-004 | security |
514
+
515
+ Plus v3.3.1 (workflow patches) + v3.3.3 (multi-party signing public surface).
516
+
517
+ - **v3.3.8** (planned) — Documentation/deprecation cleanup pass: KADENA_BASE_URL deprecation marker, `CreateAccountOptions` JSDoc fix, possibly other low-hanging doc tidies.
518
+ - **v3.3.9 / v3.4.0** (planned) — Dependency-hygiene: pin exact versions of `@kadena/*` peerDeps, possibly vendor `@kadena/types` per the supply-chain risk discussion.
519
+ - **v4.0.0** — Major structural release (monorepo split into `@stoachain/stoa-core` + `@ouronet/ouronet-core`, god-file decomposition including `infoOneFunctions`'s 23 exports → multi-file split, type consolidation, F-ARCH-003 helper extract, F-PERF-014 sleep-to-state-poll, and the bigger fork-into-stoachain-scope work for `@kadena/cryptography-utils` / `@kadena/client` / `@kadena/hd-wallet`).
520
+
521
+ Remaining non-test/perf/sec MEDIUMs after v3.3.7: F-ARCH-003 (executeLiquidityPipeline helper — natural for v4.0.0 god-file split), F-ARCH-004/008 (type duplication + Phase 3b strategy migration — multi-minor), F-PERF-014 (3-second sleeps → state polling — natural for v4.0.0 submit-flow refactor), and 4 NEEDS CONTEXT findings (F-ERR-014 multi-step add-liquidity timeout, F-API-018 readonly modifiers, F-BUG-006 ad-hoc decimal formatter, F-BUG-008 Σ-prefix validation in CodexSigningStrategy) flagged for human review per the audit roadmap.
522
+
523
+ ---
524
+
525
+ ## 3.3.6 — 2026-05-06
526
+
527
+ **MINOR, additive (performance pass).** Closes three MEDIUM performance findings from the 2026-05-05 audit in a single bundled release: **F-PERF-008** (tree-shaking guarantee via `"sideEffects": false`), **F-PERF-003** (regex memoization in `coilFunctions.getCoilPreviewGeneric`), and **F-PERF-004** (parallelize `ouroFunctions.getOuronetKdaDetails`). All three are low-risk, behaviorally identical to v3.3.5; the change surface is two source files + one package.json field + one new regression-lock test file. **NO public API change**, **NO observable behavior change**, **674/674 tests pass** (was 672 in v3.3.5; +2 from the new `tests/v3-3-6-perf-pass.test.ts`).
528
+
529
+ ### F-PERF-008 — Added `"sideEffects": false` to package.json
530
+
531
+ The biggest tree-shaking win available to consumer bundlers per the audit. Before v3.3.6, downstream bundlers (OuronetUI's webpack/Vite) couldn't prune unused barrel imports from the package's 16-subpath exports map, because they had to assume every imported module might have side effects at top-level. With this flag, bundlers know they can safely drop any imported module whose exports aren't actually used.
532
+
533
+ Verified safe before adding the flag: every seam in the package (`setPactReader` in `src/reads`, `setLogger` in `src/observability`, `setNodeConfig` in `src/network/nodeFailover`) is **consumer-invoked at boot** — none of them runs at module top-level. Confirmed by grepping `src/` for top-level callable statements (zero matches). Module evaluation only declares functions and constants; it does not mutate global state.
534
+
535
+ Consumer-side impact: OuronetUI's bundle should shrink for paths that import a single function from a barrel (e.g. `import { analyzeGuard } from "@ouronet/ouronet-core/guard"` previously pulled in the entire `./guard` module's evaluated code; with `sideEffects: false` it pulls only `analyzeGuard`'s code). The exact savings depend on the consumer's bundler config, but the audit cited this as "the single most impactful tree-shaking fix in the codebase."
536
+
537
+ Locked at `tests/v3-3-6-perf-pass.test.ts` T1: `expect(pkg.sideEffects).toBe(false)` — strict equality with literal `false`. A future package.json edit that drops the field, sets it to `true`, or sets it to a string/array all fail the check.
538
+
539
+ ### F-PERF-003 — Memoized 8 RegExp allocations per call in `coilFunctions.getCoilPreviewGeneric`
540
+
541
+ Pre-v3.3.6, `getCoilPreviewGeneric` compiled 8 `RegExp` objects every call (4 for the `pre-text` array parsing, 4 for the `post-text` array fallback). The patterns interpolate `targetTokenName` (derived from `config.targetToken`), so the audit's literal suggested fix ("hoist each regex to a module-level const") doesn't apply directly — the patterns are dynamic per token. v3.3.6 adopts the **memoization-cache** form which achieves the same outcome: subsequent calls with the same `targetTokenName` reuse the compiled `RegExp` instances from a `Map<string, CoilPatternSet>` cache.
542
+
543
+ ```ts
544
+ // New module-level cache + lazy-compile helper:
545
+ const coilPatternCache = new Map<string, CoilPatternSet>();
546
+ function getCoilPatterns(targetTokenName: string): CoilPatternSet {
547
+ const cached = coilPatternCache.get(targetTokenName);
548
+ if (cached) return cached;
549
+ // ... compile 8 patterns once, cache, return ...
550
+ }
551
+
552
+ // Inside getCoilPreviewGeneric (no more `new RegExp(...)` calls):
553
+ const patterns = getCoilPatterns(targetTokenName);
554
+ for (const pattern of patterns.generates) { ... }
555
+ for (const pattern of patterns.generating) { ... }
556
+ ```
557
+
558
+ Steady-state allocation cost: 0 RegExp objects per call after the cache warms (one warm-up call per unique target token). For the 3 standard `COIL_CONFIGS` entries (`AURYN`, `ELITEAURYN`, `SSTOA`), the cache size caps at 3. The Map structure handles arbitrary token names too, so consumer-supplied custom configs benefit equally.
559
+
560
+ Behavior is byte-identical to v3.3.5 — same patterns in the same order. Behavioral regression coverage already lives in `tests/v3-3-5-smoke.test.ts:117-141` which exercises `getCoilPreviewGeneric` with `COIL_CONFIGS.ouroToAuryn` and asserts the parsed `targetAmount === 5.0` from `pre-text: ["...generates 5.0 AURYN tokens"]`. That test passes unchanged in v3.3.6.
561
+
562
+ ### F-PERF-004 — Parallelized `getOuronetKdaDetails` via Promise.all
563
+
564
+ Pre-v3.3.6:
565
+ ```ts
566
+ const owner = await getKadenaAccountOwner(address); // sequential
567
+ const guard = await getKadenaAccountGuard(address); // (waits for owner)
568
+ ```
569
+
570
+ v3.3.6:
571
+ ```ts
572
+ const [owner, guard] = await Promise.all([
573
+ getKadenaAccountOwner(address),
574
+ getKadenaAccountGuard(address),
575
+ ]);
576
+ ```
577
+
578
+ Verified safe: `getKadenaAccountOwner` reads `DALOS.UR_AccountKadena`, `getKadenaAccountGuard` reads `DALOS.UR_AccountGuard` — neither depends on the other's result, no shared mutable state, no causal ordering. Both throw the same `Error("Failed to retrieve data from the transaction.")` on RPC failure, so error semantics are identical (Promise.all rejects with the first rejection — which would be the same error consumers saw in the sequential form).
579
+
580
+ Happy-path latency: was 2 sequential RPC roundtrips (the chain has to respond to the owner read before the guard read even starts), now 1 parallel roundtrip (both reads in flight simultaneously). On the typical Stoa network (~150-300ms per chain RPC), this halves the function's wall-clock duration to ~150-300ms. Consumer-side impact: UR-detail panels in OuronetUI render visibly faster.
581
+
582
+ One trade-off worth noting: the parallel form always issues both RPCs even when the first might have failed. The sequential form short-circuited on owner failure. With the failover layer absorbing transient RPC cost, this is a net win — but worth flagging for future contributors that the change is not strictly cost-free on the unhappy path.
583
+
584
+ Locked at `tests/v3-3-6-perf-pass.test.ts` T2: a counting reader that dispatches different stub payloads by Pact-code substring (`UR_AccountKadena` vs `UR_AccountGuard`). The test asserts both substrings appeared in the recorded calls AND the function returned the expected merged shape. A regression that accidentally drops one of the reads (e.g. only awaits `owner`) would fail loudly because the recorded calls would only contain one substring.
585
+
586
+ ### Added — `tests/v3-3-6-perf-pass.test.ts` (2 it-blocks across 2 describe groups)
587
+
588
+ | Test | Locks | What regression it catches |
589
+ |---|---|---|
590
+ | **T1** (F-PERF-008) | `package.json` declares `sideEffects: false` | Future package.json edit drops field / changes type → CI fails before npm publish |
591
+ | **T2** (F-PERF-004) | `getOuronetKdaDetails` issues both `UR_AccountKadena` AND `UR_AccountGuard` reads | Future refactor accidentally drops one of the reads (e.g. await only `owner`, not `guard`) → CI fails because the recorded Pact-code calls only contain one substring |
592
+
593
+ F-PERF-003 (regex memoization) is NOT directly locked in this file because the cache is module-internal (not exported). Behavioral regression coverage lives in v3.3.5's `tests/v3-3-5-smoke.test.ts` for `getCoilPreviewGeneric`; if memoization broke pattern compilation OR matching, that test would fail. Currently it passes.
594
+
595
+ ### Verified
596
+
597
+ - `npm run typecheck` — zero errors. The new `CoilPatternSet` interface and `coilPatternCache` Map both type-check cleanly; `Promise.all` destructuring preserves the existing return-type contract.
598
+ - `npm test` — **674/674 tests pass** (was 672 in v3.3.5; +2 from the new `tests/v3-3-6-perf-pass.test.ts`).
599
+ - `npm run build` — clean tsc emit. The change surface is `package.json` (1 field), `src/interactions/coilFunctions.ts` (cache + helper added, regex blocks replaced), `src/interactions/ouroFunctions.ts` (1 function body parallelized).
600
+ - `package.json` validates as JSON post-edit; `dist/package.json` is not ours (npm-side); the `sideEffects` flag travels with the published tarball via `files: ["dist", "CHANGELOG.md"]` (npm includes `package.json` automatically).
601
+
602
+ ### Migration
603
+
604
+ No consumer migration. The package's public API surface is byte-identical to v3.3.5: same exports, same shapes, same return types, same error semantics. v3.3.6 is purely a performance pass; consumers see:
605
+
606
+ - **Smaller bundle sizes** when consuming via webpack/Vite/Rollup (F-PERF-008 enables tree-shaking pruning of unused barrel re-exports). Exact savings consumer-config-dependent.
607
+ - **Faster `getOuronetKdaDetails`** (F-PERF-004 — ~halved latency on the happy path).
608
+ - **No observable difference** for `getCoilPreviewGeneric` (F-PERF-003 — internal-only optimization; behavior identical).
609
+
610
+ ### v3.3.x trajectory remaining
611
+
612
+ The v3.3.x audit-closure track now has 6 of the original 6 audit-closure releases done (v3.3.0/2/4/5 testing + logger; v3.3.6 performance). v3.3.1 + v3.3.3 were workflow patches and a new public surface respectively.
613
+
614
+ - **v3.3.7+** — Optional: small dependency-hygiene + doc-cleanup pass (KADENA_BASE_URL deprecation marker, `CreateAccountOptions` JSDoc, possibly pinning `@kadena/*` peerDeps). Or skip and go straight to v4.0.0.
615
+ - **v4.0.0** — Major structural release (monorepo split into `@stoachain/stoa-core` + `@ouronet/ouronet-core`, god-file decomposition including `infoOneFunctions`'s 23 exports → multi-file split, type consolidation, F-ARCH-003 helper extract, and the bigger fork-into-stoachain-scope work for `@kadena/cryptography-utils` / `@kadena/client` / `@kadena/hd-wallet`).
616
+
617
+ Remaining unaddressed MEDIUM findings post-v3.3.6 are: F-PERF-014 (3-second sleeps replaced with state polling — naturally folds into v4.0.0's submit-flow refactor), F-ARCH-003 (executeLiquidityPipeline helper — natural for v4.0.0 god-file split), F-ARCH-004/008 (type duplication + Phase 3b strategy migration — multi-minor migration work), F-SEC-003/004 (seam validators + V1-decrypt warning — small, additive, fits a v3.3.7 if user wants).
618
+
619
+ ---
620
+
621
+ ## 3.3.5 — 2026-05-06
622
+
623
+ **MINOR, additive (test-only).** Closes audit finding **F-TEST-006** (MEDIUM, testing-auditor) — six interaction modules previously had insufficient runtime coverage: three with **zero runtime tests at all** (`pensionFunctions`, `guardFunctions`, `infoOneFunctions`) and three with **compile-only tests** (`coilFunctions`, `kpayFunctions`, `activateFunctions` are all type-checked at `tests/types.test.ts:44-47` via `expectTypeOf`, but the functions never actually execute in the test suite). v3.3.5 closes the gap with `tests/v3-3-5-smoke.test.ts` — **12 new it-blocks across 6 describe groups**, one happy-path + one error-path test per module, picking the simplest representative read-only function from each. **NO source-code change**, **NO public API change**, **672/672 tests pass** (was 660 in v3.3.4; +12 from the new test file).
624
+
625
+ ### Why this is MEDIUM-severity even though no current bug
626
+
627
+ Compile-only tests (`expectTypeOf<typeof fn>().toEqualTypeOf<...>()`) prove the function's TYPE SIGNATURE matches consumer expectations but do NOT prove the function executes correctly. A bug that swapped two argument-string concatenations, forgot to await a Promise, or mis-routed the `pactRead` call would all type-check cleanly while producing wrong runtime behaviour — the tests pass, the chain rejects the malformed Pact code at consumer runtime, the failure surfaces as "transaction reverted" with no clear linkage back to the broken function. v3.3.5 makes that class of regression catchable in CI: a single happy-path runtime test per module asserts "function actually executes against a stubbed `pactRead`," and a single error-path runtime test asserts "graceful-degradation contract holds" (which for 5 of the 6 modules is `null`, and for `pensionFunctions.getHibernateFee` is a locally-computed fallback formula).
628
+
629
+ The audit's testing-auditor flagged this as MEDIUM because the gap was structural — three full modules had zero runtime tests, and three more had only type-level coverage despite shipping production interaction code. Landing this BEFORE v4.0.0's monorepo restructure means the v4.0.0 file-relocation refactor (which moves `src/interactions/*` into `packages/stoa-core/`) cannot accidentally break any of the 6 modules' runtime contracts — the regression-lock travels with the test file.
630
+
631
+ ### Added — `tests/v3-3-5-smoke.test.ts` (12 it-blocks across 6 describe groups)
632
+
633
+ | Module | Function tested | Happy path | Error path |
634
+ |---|---|---|---|
635
+ | **`pensionFunctions`** | `getHibernateFee("pool", 100)` | `{decimal:"0.99"}` → `0.99` | thrown read → catch's local fallback formula `0.12 - 0.000008 * lockDays` clamped non-negative → `0.1192` (the ONLY one of the 6 modules with a non-null error path — graceful-degradation contract that lets pension UI show a sensible default fee even when the chain function isn't deployed yet) |
636
+ | **`guardFunctions`** | `getRotateGuardInfo("k:p", "k:a")` | success-path data returned verbatim | failure-status reader → `null`, `getLogger().error` not called (chain-failure-status path is silent by design — only the catch block routes through the logger) |
637
+ | **`infoOneFunctions`** | `getCoilPreviewInfo(p, c, ats, t, "100")` | `{previewField:1, ...}` → `{result: {previewField:1, ...}}` (the `{result: ...}` envelope is the function's defining shape — locks the wrap-in-envelope contract) | failure-status reader → `null` |
638
+ | **`coilFunctions`** | `getCoilPreviewGeneric("100", COIL_CONFIGS.ouroToAuryn)` | data with `pre-text: ["...generates 5.0 AURYN tokens"]` + `kadena: {...}` → parsed `{targetAmount: 5.0, fee: 0, kadenaInfo: {...}}` (locks the regex-based pre-text parse against the v2.x token-name patterns) | failure-status reader → **rethrows** with `/Failed to get coil preview/` (the ONLY module of the 6 that rethrows rather than returning `null`/fallback — locks the rethrow contract that consumers depend on for try/catch flow) |
639
+ | **`kpayFunctions`** | `getKpayData("k:abc")` | success-path data returned verbatim | failure-status reader → `null` |
640
+ | **`activateFunctions`** | `getDeployStandardAccountInfoOnly("k:abc")` | success-path data returned verbatim | thrown read → `null` AND `getLogger().error("Error in getDeployStandardAccountInfoOnly:", error)` routed through the seam (cross-checks v3.3.0's logger-seam completion is intact for this module) |
641
+
642
+ The strategy mirrors v3.3.4's `tests/v3-3-4-success-paths.test.ts`: install a `successReader` / `failureStatusReader` / `throwingReader` stub via `setPactReader(...)`, exercise the SUT, assert the parsed result. `afterEach` restores `rawCalibratedDirtyRead` so cross-file tests aren't polluted by the seam-mocking. Logger spies use `setLogger({warn, error, info})` matching v3.3.0's extended seam contract — `info` is included for forwards-compat even though none of the 6 SUTs invoke it.
643
+
644
+ ### Why "minimal" is the right scope (and why we don't smoke-test the execute-class functions)
645
+
646
+ Each of the 6 modules exports a mix of read-only functions (`pactRead`-based — easy to mock via `setPactReader`) and transaction-execute functions (require signing via `CodexSigningStrategy` + `submit` to the chain + `listenForCompletion`-style polling). Smoke-testing the read-only one is cheap; smoke-testing the execute function would require mocking the full `@kadena/client` signing + submit + status-polling chain, which is a much bigger surface and out-of-scope for a MEDIUM audit-closure release. The read-only smoke is sufficient to satisfy F-TEST-006's "function actually executes against the stubbed seam" assertion. The execute-path coverage is queued for v4.0.0's monorepo split where the `CodexSigningStrategy` seam will land properly tested as part of the `packages/stoa-core/` extraction.
647
+
648
+ ### Why a dedicated v3.3.5 file rather than appending to existing files
649
+
650
+ Same rationale as v3.3.4: (1) **audit-finding traceability** — F-TEST-006 closure lives in one greppable place, mirroring v3.3.2 (F-TEST-002 → `universal-sign.test.ts`), v3.3.3 (new public surface → `partial-sig.test.ts`), and v3.3.4 (F-TEST-005 → `v3-3-4-success-paths.test.ts`); (2) **per-file scope clarity** — appending across 6 different modules' existing test homes would scatter the F-TEST-006 closure across 6 places and muddy each file's per-module scope statement.
651
+
652
+ ### Verified
653
+
654
+ - `npm run typecheck` — zero errors. The new test file imports from `../src/reads`, `../src/observability`, and 6 stable interaction subpaths; all types resolve cleanly.
655
+ - `npm test` — **672/672 tests pass** (was 660 in v3.3.4; +12 from `tests/v3-3-5-smoke.test.ts`).
656
+ - `npm run build` — clean tsc emit. No source-code change; every module under test is byte-identical to v3.3.4.
657
+
658
+ ### Migration
659
+
660
+ No consumer migration. The package's public API surface is byte-identical to v3.3.4. This release adds tests that lock existing module behaviour against future regressions; consumers see no observable difference.
661
+
662
+ ### v3.3.x trajectory ahead
663
+
664
+ - **v3.3.6+** — Documentation/deprecation cleanups (KADENA_BASE_URL deprecation marker, `CreateAccountOptions` JSDoc, etc.); possibly a small dependency-hygiene release (pin exact versions of `@kadena/*` peerDeps + vendor `@kadena/types` per the supply-chain risk discussion deferred from earlier in the v3.3.x cycle).
665
+ - **v4.0.0** — Major structural release (monorepo split into `@stoachain/stoa-core` + `@ouronet/ouronet-core`, god-file decomposition including `infoOneFunctions`'s 23 exports → multi-file split, type consolidation, and the bigger fork-into-stoachain-scope work for `@kadena/cryptography-utils` / `@kadena/client` / `@kadena/hd-wallet`). The v3.3.x test-coverage track (`universal-sign`, `partial-sig`, `v3-3-4-success-paths`, `v3-3-5-smoke`) all relocate to `packages/stoa-core/tests/` since the SUTs they cover are StoaChain-generic infrastructure.
666
+
667
+ ### v3.3.x audit-closure track — COMPLETE
668
+
669
+ This release closes the last MEDIUM testing-auditor finding from the 2026-05-05 audit. The full v3.3.x audit-closure track:
670
+
671
+ | Release | Audit finding | Severity | What it closed |
672
+ |---|---|---|---|
673
+ | v3.3.0 | F-LOGGER-SEAM-001 | (consolidated, 8 of 8 agents) | Logger seam completion + 7 routed sites + regression-lock |
674
+ | v3.3.1 | (workflow follow-ups from v3.0.0..v3.3.0 pollinate runs) | n/a | npm provenance + gh-CLI `--repo` flag drop |
675
+ | v3.3.2 | F-TEST-002 | HIGH | Direct test coverage for `universalSignTransaction` (3 seedType branches + foreign-key + multi-signer + partial-sig primitive) |
676
+ | v3.3.3 | (new public surface — not an audit finding) | n/a | Multi-party partial-signature workflow for OuronetUI's "AnyOne v2" |
677
+ | v3.3.4 | F-TEST-005 | MEDIUM | Success-path tests for the 13 of 16 v3.0.0 nullable-widened functions |
678
+ | v3.3.5 | F-TEST-006 | MEDIUM | Smoke tests for 6 modules (3 zero-runtime + 3 compile-only) |
679
+
680
+ All MEDIUM testing findings from the 2026-05-05 audit are now CLOSED. Remaining MEDIUM findings are non-test (`F-PERF-008` sideEffects, `F-PERF-003` regex hoisting, `F-PERF-004` Promise.all, etc.) — those remain as the Short-term Actions queue per the audit roadmap.
681
+
682
+ ---
683
+
684
+ ## 3.3.4 — 2026-05-06
685
+
686
+ **MINOR, additive (test-only).** Closes audit finding **F-TEST-005** (MEDIUM, testing-auditor) — the v3.0.0 nullable-widening sweep widened 16 read-side interaction functions from `Promise<T>` → `Promise<T | null>`, replacing each fabricated sentinel (`1.0` / `8` / `0` / `"0"` / `"N/A"`) with `null` on RPC failure. v3.0.0 added null-path tests for all 16 (proving each returns `null` when `pactRead` throws or returns `failure` status), but **only 3 of the 16 had a paired success-path test** — `getStoaPriceUSD` in `tests/interactions-pricing.test.ts:80`, `getLPTypeInfo`'s per-flag mixed-state lock in `tests/interactions-balance-cluster.test.ts:137`, and `getUrStoaGuard`'s 3-state contract at the same file:207. The remaining 13 could not distinguish "always returns null" (silent regression) from "returns null only on RPC failure" (correct contract). v3.3.4 closes that gap with `tests/v3-3-4-success-paths.test.ts` — **13 new it-blocks across 6 describe groups**, one per missing function, each installing a `successReader` stub via `setPactReader(...)` that resolves to `{result: {status: "success", data: <stub>}}`, exercising the SUT, and asserting the parsed non-null return. **NO source-code change**, **NO public API change**, **660/660 tests pass** (was 647 in v3.3.3; +13 from the new test file).
687
+
688
+ ### Why this is MEDIUM-severity even though no current bug
689
+
690
+ A future regression that returned `null` unconditionally — e.g. a flipped `if (response?.result?.status === "success")` to `!== "success"`, or a `return null;` accidentally inserted before the parse logic — would slip past every existing test in the v3.0.0 fabricated-fallback regression suite. The null-path tests would still pass (the stubbed throwing/failure-status reader still returns `null`, just for the wrong reason). The bug would surface only at consumer runtime, where the OuronetUI balance/price banners would silently switch to "RPC error" UI for legitimate non-zero values — a hard-to-reproduce, intermittently-correlated failure mode. v3.3.4's success-path lock makes this regression class **physically impossible** to ship without tripping CI.
691
+
692
+ The audit's testing-auditor flagged this as MEDIUM because the gap was structural (every widened function had the same gap in the same way), and the closure is mechanical (one stub per function, ~5 lines each). Landing this BEFORE v4.0.0's monorepo restructure means the v4.0.0 file-relocation refactor (which moves `src/interactions/*` into `packages/stoa-core/`) cannot accidentally regress any of the 13 success paths — the regression-lock travels with the test file.
693
+
694
+ ### Added — `tests/v3-3-4-success-paths.test.ts` (13 it-blocks across 6 describe groups)
695
+
696
+ | Group | Tests | Functions covered | Locks |
697
+ |---|---|---|---|
698
+ | **Pricing-quartet** (REQ-01..REQ-04) | 3 | `getTokenDecimals`, `getPoolTotalFee`, `getDPTFMinMove` | `{int: "12"}` → `12` (parseInt path); `{decimal: "0.003"}` → `0.003` (parseFloat path); `{decimal: "0.0001"}` → `0.0001` (mayComeWithDeimal path). All three locks verify `Number.isFinite()` accepts the parsed value. (`getStoaPriceUSD` already covered at `interactions-pricing.test.ts:80`.) |
699
+ | **String-balance cluster** (REQ-05) | 4 | `getIgnisBalance`, `getAccountTokenSupply`, `getOuroDispoCapacity`, `getVirtualOuro` | All four unwrap a Pact `{decimal: "..."}` payload via `mayComeWithDeimal` to the underlying string. Locks the v3.0.0 contract that a successful read with a non-zero decimal returns the parsed string — NOT `null` (RPC failure), NOT `"0"` (which v2.x's fabricated sentinel collapsed both cases onto). |
700
+ | **urStoa pair** (REQ-07) | 2 | `getUrStoaBalance`, `checkCoinAccountExists` (urStoa) | `{decimal: "42.5"}` → `42.5` for the balance read; `data: true` → `true` for the urStoa account-existence probe. Test-block JSDoc explicitly cites the inverted-typeof Pact gymnastics in `urStoaFunctions.ts:567` (out of scope for this release; locked-as-is so a future fix can be measured against the current contract). (`getUrStoaGuard`'s 3-state lock at `interactions-balance-cluster.test.ts:207` already counts as success-path.) |
701
+ | **validateLiquidity mixed-shape** (REQ-08) | 1 | `validateLiquidity` | The ONLY one of the 16 widenings where the success path shape differs from the failure path shape. Stub: chain returns `[{decimal:"0.05"}, {decimal:"0.10"}]`. Asserts function returns `{valid: true, computed: "0.05", max: "0.10"}` AND `error` field is `undefined` — the v3.0.0 locked-decision mutual exclusion between `valid:true` and `error` (otherwise consumer's `if (out.error)` branch mis-fires and shows RPC-failure UI for a successful liquidity check). |
702
+ | **getMaxBuyMovieBooster** (REQ-08) | 1 | `getMaxBuyMovieBooster` | `{int: "5000"}` → `5000`. Locks `Number.isFinite` guard against the v2.x fabricated-`0` (which collapsed sold-out and RPC-fail onto the same return value). |
703
+ | **Magic-string elimination** (REQ-09) | 2 | `getSWPSpawnLimit`, `getSWPInactiveLimit` | Both unwrap `{decimal: "..."}` to the underlying string. Belt-and-suspenders assertion: `expect(out).not.toBe("N/A")` — proves the v3.0.0 BREAKING `"N/A"` → `null` swap is intact at the success path, so a regression that re-introduced the magic string would fail the not-`"N/A"` assertion even if it satisfied the toBe-string check. |
704
+
705
+ The strategy mirrors the pre-existing pattern at `tests/interactions-pricing.test.ts:80-88` (the one success-path test that existed pre-v3.3.4): `setPactReader(successReader({...}))` → call SUT → assert. `afterEach` restores `rawCalibratedDirtyRead` so cross-file tests aren't polluted by the seam-mocking. No global state lives across the 13 it-blocks.
706
+
707
+ ### Why a dedicated v3.3.4 file rather than appending to existing files
708
+
709
+ (1) **Audit-finding traceability** — F-TEST-005 closure lives in one greppable place, mirroring v3.3.2's `tests/universal-sign.test.ts` (F-TEST-002 closure) and v3.3.3's `tests/partial-sig.test.ts` (new public surface). Future contributors who grep for `F-TEST-005` find the audit citation, the per-function rationale, and the success-path lock all in one file.
710
+
711
+ (2) **Per-file scope clarity** — the existing files (`interactions-pricing.test.ts`, `interactions-balance-cluster.test.ts`) document themselves as Phase-1 / Phase-2 fabricated-fallback regression locks. Appending post-v3.0.0 audit-closure work to them would muddy the per-file scope statement. The dedicated file keeps the v3.3.4 audit citation visible to future contributors AND keeps the existing files focused on their original v3.0.0-ship purpose.
712
+
713
+ ### Verified
714
+
715
+ - `npm run typecheck` — zero errors. The new test file imports types from `../src/reads` (`PactReader`), `../src/interactions/ouroFunctions`, `../src/interactions/dexFunctions`, `../src/interactions/urStoaFunctions`, `../src/interactions/addLiquidityFunctions` — all stable subpaths from v3.0.0+.
716
+ - `npm test` — **660/660 tests pass** (was 647 in v3.3.3; +13 from `tests/v3-3-4-success-paths.test.ts`).
717
+ - `npm run build` — clean tsc emit. No source-code change; every interaction function is byte-identical to v3.3.3.
718
+
719
+ ### Migration
720
+
721
+ No consumer migration. The package's public API surface is byte-identical to v3.3.3. This release adds tests that lock existing v3.0.0 behaviour against future regressions; consumers see no observable difference.
722
+
723
+ ### v3.3.x trajectory ahead
724
+
725
+ - **v3.3.5** — F-TEST-006 behavioural tests for `pensionFunctions`/`guardFunctions`/`infoOneFunctions` (3 modules with zero tests; 3 more compile-only).
726
+ - **v3.3.6+** — Documentation/deprecation cleanups; possibly a small dependency-hygiene release (pin exact versions of `@kadena/*` peerDeps + vendor `@kadena/types` per the supply-chain risk discussion deferred from earlier in the v3.3.x cycle).
727
+ - **v4.0.0** — Major structural release (monorepo split into `@stoachain/stoa-core` + `@ouronet/ouronet-core`, god-file decomposition, type consolidation, and the bigger fork-into-stoachain-scope work for `@kadena/cryptography-utils` / `@kadena/client` / `@kadena/hd-wallet`). The test files added across v3.3.2/3/4 (`universal-sign`, `partial-sig`, `v3-3-4-success-paths`) all relocate to `packages/stoa-core/tests/` since the SUTs they cover are StoaChain-generic infrastructure.
728
+
729
+ ---
730
+
731
+ ## 3.3.3 — 2026-05-03
732
+
733
+ **MINOR, additive (NEW PUBLIC SURFACE — not a bug fix).** Ships the multi-party partial-signature workflow OuronetUI has been blocked on: "Person A signs → exports → Person B imports → signs → exports → Person C imports → signs → submits", with cross-party tamper detection at every handoff. Builds on v3.3.2's locked partial-signing primitive (signing with a subset of declared signers fills only those slots; pre-existing slots stay intact across re-signing passes) by wrapping it in a versioned export envelope + slot-status helpers + Ed25519 sig-verification helper. New `src/signing/partialSig.ts` module, re-exported from `@ouronet/ouronet-core/signing`. **NO existing API changed**, **NO source-side behaviour change** outside the new module, **647/647 tests pass** (was 631 in v3.3.2; +16 from the new `tests/partial-sig.test.ts`).
734
+
735
+ ### Why this is a NEW ADDITION, not a bug fix
736
+
737
+ Pre-v3.3.3 the underlying primitive existed (since `universalSignTransaction`'s loop has always been "iterate keypairs, sign matching slots, leave others alone" — locked under runtime test in v3.3.2's "partial-signing primitive (v3.3.3 foundation)" describe group), but the multi-party flow required consumers to:
738
+
739
+ 1. Manually serialise an `IUnsignedCommand` to JSON (no envelope, no version, no transport metadata).
740
+ 2. Manually parse + cast the JSON back into an `IUnsignedCommand` on import.
741
+ 3. Implement their own hash-recompute-and-compare for tamper detection.
742
+ 4. Implement their own Ed25519 sig-verification loop to catch tampered signatures.
743
+
744
+ OuronetUI's pending "AnyOne v2" multi-sig flow needed all four. v3.3.3 ships the public surface so the workflow lives in shared core (one implementation, one set of locked tests) rather than getting reinvented per-consumer.
745
+
746
+ ### Added — `src/signing/partialSig.ts` (7 functions + 2 typed errors + envelope interface)
747
+
748
+ | Symbol | Kind | Purpose |
749
+ |---|---|---|
750
+ | `signPartial(tx, keypairs)` | function | Thin wrapper around `universalSignTransaction` that drops `onMissingKey` on purpose. In the multi-party flow each signer commits only their OWN keys; "missing" keys mean "another party will sign in their next pass" — not "paste-resolve a foreign key now." Consumers wanting foreign-key paste resolution still call `universalSignTransaction` directly. |
751
+ | `serializePartialTransaction(tx, metadata?)` | function | Wrap the `IUnsignedCommand` in a versioned `PartialSigEnvelope` (`format: "ouronet-partial-sig"`, `version: 1`) and stringify with 2-space indent (mirrors `serializeCodex`'s human-eyeballable choice). `metadata.exportedAt` / `metadata.exportedBy` / `metadata.note` optional, freeform-ish; readers ignore unknowns (forwards-compat). |
752
+ | `deserializePartialTransaction(json)` | function | Parse + format/version literal check + transaction-shape check (`cmd` string, `hash` string, `sigs` array) + **hash-integrity check via `kadenaHash(cmd) === transaction.hash`**. Throws `InvalidEnvelopeError` on shape problems, `TamperedHashError` on hash mismatch. Returns the unwrapped `IUnsignedCommand` ready for the next signer. |
753
+ | `getMissingSigners(tx)` | function | Pubkeys of `cmd.signers` whose parallel `sigs[i]?.sig` slot is empty. Drives "who needs to sign next" UI. |
754
+ | `getFilledSigners(tx)` | function | Inverse of `getMissingSigners` — pubkeys with a filled sig slot. Drives "X of Y signers complete" status. |
755
+ | `isFullySigned(tx)` | function | `getMissingSigners(tx).length === 0`. Cheap pre-check before submitting to chain. |
756
+ | `verifyExistingSignatures(tx)` | function | Verifies every filled `sigs[i]` against `cmd.signers[i].pubKey` over the canonical hash via `nacl.sign.detached.verify`. Empty slots are skipped (not failures). Returns `{allValid, invalid: [{publicKey, reason}]}`. Catches the "tampered cmd + tampered hash to match" attack the envelope's hash-integrity gate alone misses (any cmd modification invalidates every prior signature against any hash, original or rewritten). |
757
+ | `InvalidEnvelopeError` | class | ES2022 `cause` chaining for the JSON-parse path. Message names the offending FIELD but never the field VALUE — an envelope can carry a Pact cmd with embedded data, and surfacing those into telemetry/logs would breach the export's information-disclosure boundary (mirrors `deserializeCodex`'s discipline). |
758
+ | `TamperedHashError` | class | Carries `expected` (what the envelope embedded) and `actual` (what `kadenaHash(cmd)` recomputed). Operator can decide whether the divergence is a UI bug, transport corruption, or malicious modification. |
759
+ | `PartialSigEnvelope` | interface | The v1 export shape. `transaction` is the `IUnsignedCommand` verbatim (no field rename); `metadata` is optional and free-form-ish. |
760
+ | `PARTIAL_SIG_FORMAT` / `PARTIAL_SIG_VERSION` | const | The `"ouronet-partial-sig"` and `1` literals exposed for tooling sanity-checks. |
761
+
762
+ ### Added — `tests/partial-sig.test.ts` (16 it-blocks across 7 describe groups)
763
+
764
+ | Group | Test count | What it locks |
765
+ |---|---|---|
766
+ | **`signPartial` — fills only matching slots** | 2 | (a) 3-signer tx + 1 keypair fills only that slot, others empty (regression-lock for the wrapper level — v3.3.2 locked this at the `universalSignTransaction` level); (b) re-signing on TOP of a previously partially-signed tx preserves the existing sig byte-for-byte and adds the new one — the load-bearing handoff invariant. |
767
+ | **serialize / deserialize round-trip** | 2 | (a) full envelope round-trip preserves cmd/hash/sigs byte-for-byte; (b) `metadata` is optional — serialising without it produces a valid envelope. |
768
+ | **`deserializePartialTransaction` — rejection cases** | 4 | Throws `InvalidEnvelopeError` on: not JSON, wrong format literal, wrong version literal, missing `transaction.cmd`. |
769
+ | **`deserializePartialTransaction` — `TamperedHashError`** | 1 | Embedded `transaction.hash` doesn't match `kadenaHash(transaction.cmd)` → throws `TamperedHashError` with both `expected` and `actual` populated. The `actual` field equals `signed.hash` (sanity check that the chain's hash IS what `kadenaHash(cmd)` produces — locks the assumption the integrity check rests on). |
770
+ | **slot-status helpers** | 3 | `getMissingSigners` / `getFilledSigners` / `isFullySigned` correctly partition a 3-signer tx across (a) 0 signed → all missing, none filled, not fully signed; (b) 1 signed → 2 missing, 1 filled, not fully signed; (c) all 3 signed → 0 missing, 3 filled, fully signed. |
771
+ | **`verifyExistingSignatures`** | 3 | (a) properly-signed all-slots-filled tx → `allValid: true`; (b) partial-signed tx with empty slots → `allValid: true` (empty slots skipped, not flagged as failures); (c) tampered sig (1 hex digit flipped) → `allValid: false` with `invalid[0].publicKey` and reason text matching `/Ed25519 verification/`. |
772
+ | **end-to-end 3-party round-trip** | 1 | Full A→B→C handoff via serialize/deserialize: A signs → exports JSON → B imports (hash + cmd + A's sig all intact) → B signs → exports JSON → C imports (all 3 verifications pass) → C signs → final tx has all 3 slots filled, `verifyExistingSignatures` returns `allValid: true`, every sig validates against the original hash via direct `nacl.sign.detached.verify`. The chain-validator-equivalent end-state assertion. |
773
+
774
+ ### How OuronetUI consumes this
775
+
776
+ ```ts
777
+ import {
778
+ signPartial,
779
+ serializePartialTransaction,
780
+ deserializePartialTransaction,
781
+ getMissingSigners,
782
+ isFullySigned,
783
+ verifyExistingSignatures,
784
+ } from "@ouronet/ouronet-core/signing";
785
+
786
+ // ── Person A's side ──
787
+ const txWithA = await signPartial(unsignedTx, [personAKeypair]);
788
+ const exportFromA = serializePartialTransaction(txWithA, {
789
+ exportedAt: new Date().toISOString(),
790
+ exportedBy: "k:a3f...",
791
+ });
792
+ // → exportFromA flows out via download / QR / chat.
793
+
794
+ // ── Person B's side ──
795
+ const importedAtB = deserializePartialTransaction(exportFromA);
796
+ // ↑ throws TamperedHashError if cmd was modified mid-flight.
797
+ const verifyAtB = verifyExistingSignatures(importedAtB);
798
+ if (!verifyAtB.allValid) { /* surface verifyAtB.invalid in UI */ }
799
+ const stillNeed = getMissingSigners(importedAtB);
800
+ // ↑ ["personB-pubkey", "personC-pubkey"] — drives "who's left to sign?" panel.
801
+ const txWithAB = await signPartial(importedAtB, [personBKeypair]);
802
+ const exportFromB = serializePartialTransaction(txWithAB, { exportedBy: "k:b1c..." });
803
+
804
+ // ── Person C's side ──
805
+ const importedAtC = deserializePartialTransaction(exportFromB);
806
+ const txWithABC = await signPartial(importedAtC, [personCKeypair]);
807
+ if (isFullySigned(txWithABC)) {
808
+ // → submit to chain via the existing OuronetUI submitToChain flow.
809
+ }
810
+ ```
811
+
812
+ ### Verified
813
+
814
+ - `npm run typecheck` — zero errors. The new `PartialSigEnvelope` interface, `InvalidEnvelopeError` / `TamperedHashError` classes, and the `VerifyExistingSignaturesResult` type all compile cleanly. The seven exported functions all use existing `IUnsignedCommand` / `ICommand` types from `@kadena/types`; no new third-party dependency.
815
+ - `npm test` — **647/647 tests pass** (was 631 in v3.3.2; +16 from the new test file).
816
+ - `npm run build` — clean tsc emit. `dist/signing/partialSig.js` + `dist/signing/partialSig.d.ts` produced; `dist/signing/index.d.ts` re-exports the new symbols. No source-code change to any pre-v3.3.3 file (only the `export * from "./partialSig"` line added to `src/signing/index.ts`).
817
+
818
+ ### Migration
819
+
820
+ No consumer migration. The package's pre-v3.3.3 public API surface is byte-identical: `universalSignTransaction`, `fromKeypair`, `CodexSigningStrategy`, `KeyResolver` etc. all behave exactly as in v3.3.2. v3.3.3 is purely additive — consumers who don't import the new symbols see no observable difference. OuronetUI's "AnyOne v2" branch can now wire the workflow above; the AncientHolder HUB can adopt the same surface for any multi-admin signing flow it grows into.
821
+
822
+ ### v3.3.x trajectory ahead
823
+
824
+ - **v3.3.4** — F-TEST-005 success-path tests for the 13 v3.0.0 nullable-widened functions.
825
+ - **v3.3.5** — F-TEST-006 behavioural tests for `pensionFunctions`/`guardFunctions`/`infoOneFunctions`.
826
+ - **v3.3.6+** — Documentation/deprecation cleanups; possibly a small dependency-hygiene release (pin exact versions of `@kadena/*` peerDeps + vendor `@kadena/types` per the supply-chain risk discussion deferred from earlier in the v3.3.x cycle).
827
+ - **v4.0.0** — Major structural release (monorepo split into `@stoachain/stoa-core` + `@ouronet/ouronet-core`, god-file decomposition, type consolidation, and the bigger fork-into-stoachain-scope work for `@kadena/cryptography-utils` / `@kadena/client` / `@kadena/hd-wallet`). `src/signing/partialSig.ts` is StoaChain-generic infrastructure and slated for `packages/stoa-core/`.
828
+
829
+ ---
830
+
831
+ ## 3.3.2 — 2026-05-06
832
+
833
+ **MINOR, additive (test-only).** Closes audit finding **F-TEST-002** (HIGH) — the central signing entry point `universalSignTransaction` in `src/signing/universalSign.ts` had ZERO direct tests pre-v3.3.2. Adds `tests/universal-sign.test.ts` with **9 new it-blocks** covering all three seedType branches (koala / chainweaver / eckowallet), the foreign-key onMissingKey resolution path (success and key-mismatch error cases), the multi-signer mixed-seedType case, the partial-signing primitive (foundation lock for v3.3.3's planned multi-party signing public surface), and the silent-skip-when-not-in-signers contract. **NO source-code change**, **NO public API change**, **631/631 tests pass** (was 622 in v3.3.1; +9).
834
+
835
+ ### Why this is HIGH-severity even though no current bug
836
+
837
+ `universalSignTransaction` is the central choke point through which every signed Pact transaction in the ecosystem flows. Pre-v3.3.2:
838
+
839
+ - The only mention of `universalSignTransaction` in `tests/` was a comment in `tests/signing.test.ts:5` stating "the full universalSignTransaction is not exercised here."
840
+ - `tests/strategy.test.ts` exercises `CodexSigningStrategy` which calls `universalSignTransaction` internally — but only with `seedType: "koala"`. The chainweaver / eckowallet / foreign branches were never runtime-tested.
841
+ - The seedType dispatcher itself (the switch-equivalent in lines 89-104 of `universalSign.ts`) was never runtime-tested. A typo that mis-routed `eckowallet` → `koala` would silently produce a wrong-shape signature; the failure surfaces only when a real consumer with a chainweaver wallet tries to sign and the chain rejects with "invalid signature" — far from the actual cause.
842
+
843
+ The audit's bug-detector and testing-auditor agents both flagged this as HIGH. v3.3.2 closes it before v4.0.0's monorepo restructure moves `src/signing/universalSign.ts` (it's StoaChain-generic infrastructure, slated for `packages/stoa-core/`). Locking the chainweaver/eckowallet/foreign paths NOW means the v4.0.0 file-relocation refactor can't accidentally break them.
844
+
845
+ ### Added — `tests/universal-sign.test.ts` (9 it-blocks across 6 describe groups)
846
+
847
+ | Group | Test count | What it locks |
848
+ |---|---|---|
849
+ | **Koala branch (nacl Ed25519)** | 2 | Round-trip with RFC-8032 vector + verification via `nacl.sign.detached.verify`; `fromKeypair` adapter normalising consumer-shape `privateKey` field into the universal `secretKey` field |
850
+ | **Chainweaver branch (WASM kadenaSign)** | 1 | Real chainweaver keypair derivation via `KadenaWalletBuilder.createWalletPairFromMnemonic` using the `@kadena/hd-wallet` vendor vector → `universalSignTransaction` produces a valid Ed25519 signature verifiable against the derived publicKey |
851
+ | **Eckowallet branch (label-only difference)** | 1 | Identical derivation as chainweaver but with `seedType: "eckowallet"` — locks the dispatcher routing both labels to the same WASM signing path. Eckowallet is not just "chainweaver renamed" at the type level; this test runtime-confirms it. |
852
+ | **Multi-signer mixed seedTypes** | 1 | Two-signer transaction with one koala signer + one chainweaver signer; both slots filled, both verifiable. Locks the iterate-and-dispatch-each loop's correctness. |
853
+ | **Foreign branch (onMissingKey)** | 2 | (a) Success: callback resolves a signer-pubkey not in our keypairs list; (b) Failure: callback returns mismatched private key → throws "Key mismatch" error citing both expected and derived pubkeys for operator diagnosability |
854
+ | **Partial-signing primitive (v3.3.3 foundation)** | 2 | (a) 3-signer transaction signed with only 1 keypair — only that slot filled, other two left empty (the load-bearing assertion for v3.3.3's multi-party signing workflow); (b) keypairs whose pubkey is NOT in cmd.signers are silently skipped (current contract per `universalSign.ts:91`) |
855
+
856
+ The verification approach uses `nacl.sign.detached.verify` over the base64URL-decoded `signed.hash` bytes. This works for BOTH the nacl-direct path (koala/foreign) AND the WASM-Ed25519 path (chainweaver/eckowallet) — `kadenaSign` produces standard Ed25519 signatures verifiable with the same primitive, despite the BIP32-derived key path. The test file documents this explicitly so future contributors know the verification helper handles both branches.
857
+
858
+ ### Verified
859
+
860
+ - `npm run typecheck` — zero errors. The `UniversalKeypair` type's seedType union (`"koala" | "chainweaver" | "eckowallet" | "foreign"`) compiles cleanly across all 9 test cases.
861
+ - `npm test` — **631/631 tests pass** (was 622 in v3.3.1; +9 from the new test file).
862
+ - `npm run build` — clean tsc emit. No source-code change; `universalSign.ts` is byte-identical to v3.3.1.
863
+
864
+ ### Migration
865
+
866
+ No consumer migration. The package's public API surface is byte-identical to v3.3.1. This release adds tests that lock existing behaviour against future regressions; consumers see no observable difference.
867
+
868
+ ### v3.3.x trajectory ahead
869
+
870
+ - **v3.3.3** — Multi-party partial-sig public surface (`signPartial`, `serializePartialTransaction`, `deserializePartialTransaction`, `getMissingSigners`, `getFilledSigners`, `isFullySigned`, `verifyExistingSignatures`). Builds on v3.3.2's locked partial-signing primitive. Enables the OuronetUI workflow where Person A signs and exports a transaction, Person B imports and adds their signature, Person C imports and submits — with hash-integrity verification at each handoff so a tampered cmd between signers gets rejected. New `src/signing/partialSig.ts` module.
871
+ - **v3.3.4** — F-TEST-005 success-path tests for the 13 v3.0.0 nullable-widened functions.
872
+ - **v3.3.5** — F-TEST-006 behavioural tests for `pensionFunctions`/`guardFunctions`/`infoOneFunctions`.
873
+ - **v3.3.6+** — Documentation/deprecation cleanups; possibly a small dependency-hygiene release (pin exact versions of `@kadena/*` peerDeps + vendor `@kadena/types` per the supply-chain risk discussion deferred from earlier in the v3.3.x cycle).
874
+ - **v4.0.0** — Major structural release (monorepo split into `@stoachain/stoa-core` + `@ouronet/ouronet-core`, god-file decomposition, type consolidation, and the bigger fork-into-stoachain-scope work for `@kadena/cryptography-utils` / `@kadena/client` / `@kadena/hd-wallet`).
875
+
876
+ ---
877
+
878
+ ## 3.3.1 — 2026-05-06
879
+
880
+ **PATCH, workflow-only.** Closes the two carried-forward follow-ups that have appeared in every pollinate run's "follow-ups" block since v3.0.0: (1) `npm publish` now passes the `--provenance` flag (and the workflow gains the `id-token: write` permission required to mint the OIDC token npm exchanges with npmjs.org), so v3.3.1 onwards every release carries a verifiable SLSA attestation linking the published tarball to the exact GitHub Action run that produced it; and (2) the `gh release create` invocations in both the main Release-creation step and the idempotent backfill step drop the `--repo` flag, eliminating the `gh release create --notes-from-tag --repo X` flag-combination incompatibility that the GitHub-hosted runners' gh CLI image rejected starting around 2026-04-30. Both fixes are workflow-file-only — `.github/workflows/publish.yml` is the only file that ships behaviour change. **NO source-code change**, **NO public API change**, **622/622 tests pass unchanged** (the workflow file isn't in the test scope; verification is the v3.3.1 publish run itself).
881
+
882
+ ### Why this is a workflow-only patch (and why that's load-bearing)
883
+
884
+ Every v3.x publish since v3.0.0 has produced two recurring artifacts in the pollinate "follow-ups" block:
885
+
886
+ 1. **Missing npm provenance attestation.** `lifecycle.use_provenance: true` in `.bee/config.json` told pollinate to expect a 200 response from `https://registry.npmjs.org/-/npm/v1/attestations/@ouronet/ouronet-core@{version}`, but the actual workflow's `npm publish --access public` call lacked the `--provenance` flag, so npm silently skipped attestation generation. Each v3.x release shipped without the provenance signal that downstream consumers (and npmjs.com's "Provenance" badge) verify against.
887
+ 2. **Inline gh-CLI Release creation failed every run.** The `gh release create --notes-from-tag --repo X` invocation in the workflow has hit the same "using `--notes-from-tag` with `--repo` is not supported" failure on every v3.x run since the GitHub-hosted runners' gh-CLI image update on/around 2026-04-30. Pollinate's Step 9c REST-API fallback created the Release manually each time — the user-facing artefact was always present, but a workflow step was failing on every run, leaving a misleading red-X on the workflow run page.
888
+
889
+ Both are workflow-file fixes that don't require a source-code change, don't break any consumer contract, and don't merit dragging a test-coverage block (v3.3.2/3/4 were lined up as the next coding work) into a release pipeline patch. v3.3.1 is published explicitly to land these two fixes as a clean PATCH release with a clear CHANGELOG audit trail. From v3.3.1 onwards, the workflow's success indicator on each publish IS the green-check we want — no manual-fallback tracking required.
890
+
891
+ ### Added — `id-token: write` to workflow `permissions` block
892
+
893
+ `.github/workflows/publish.yml`'s `permissions` block extends from `contents: write` (the existing scope, granted at v2.0.2 to fix the GitHub-Releases-403 problem) to also include `id-token: write`. The token is the GitHub-Actions-OIDC-issued credential that `npm publish --provenance` exchanges with npmjs.org's attestation endpoint to produce the SLSA verification trail. Without this permission, `npm publish --provenance` doesn't fail — it silently skips the attestation step and produces a published-but-not-attested package.
894
+
895
+ ### Added — `--provenance` flag on `npm publish`
896
+
897
+ The Publish-to-npmjs.org step changes from `npm publish --access public` to `npm publish --access public --provenance`. `npm` 9.5+ supports the flag; the `setup-node@v4` action used here installs Node 22 with a recent npm. The first attestation will surface on `npmjs.com/package/@ouronet/ouronet-core/v/3.3.1` via the "Provenance" badge in the version sidebar, plus a 200 response from `https://registry.npmjs.org/-/npm/v1/attestations/@ouronet/ouronet-core@3.3.1`. Pollinate's Step 9a `use_provenance` check (which has been silently failing on every v3.x run) will pass cleanly from v3.3.1 onwards.
898
+
899
+ ### Changed — dropped `--repo` flag from `gh release create` and `gh release view` calls
900
+
901
+ The main "Create GitHub Release for the pushed tag" step + the idempotent "Backfill GitHub Releases for prior tags" step both used `gh release create $TAG --repo "${{ github.repository }}" ...`. After the GitHub-hosted runners' gh-CLI image update around 2026-04-30, the combination `gh release create --notes-from-tag --repo X` started returning "this flag combination is not supported." Pre-v3.3.1 the workaround was pollinate's own REST-API fallback (Step 9c); v3.3.1's workflow patch fixes the upstream cause: `--repo` is unnecessary in the workflow context because `actions/checkout@v4` (the first step) sets the working directory to the checked-out repo, and the gh CLI auto-detects the repo from that context. Same fix applied to the `gh release view` calls that gate the idempotent skip-if-exists check (no failure mode there pre-v3.3.1, but kept consistent for tooling-symmetry).
902
+
903
+ ### Verified
904
+
905
+ - **`npm run typecheck` / `npm test` / `npm run build`** — all pass; tests run against the source-code surface which is unchanged in v3.3.1 (only `.github/workflows/publish.yml`, `package.json` version field, and `tests/package-version.test.ts` pin were touched).
906
+ - **Workflow change cannot be verified locally** — the `--provenance` flag requires the GitHub-Actions-OIDC token (only available inside a CI run), and the `gh release create --repo` drop's behaviour change only manifests against GitHub's API. The proof of correctness will arrive when the v3.3.1 publish run finishes: pollinate's Step 9a should report `provenance: present` (was `absent` on every v3.x release), and the gh-CLI Release-creation step should conclude `success` (was `failure` on every v3.x release with REST fallback handling it).
907
+
908
+ ### Migration
909
+
910
+ No consumer migration. The package's public API surface is byte-identical to v3.3.0. Consumers who track npm provenance attestations (e.g., Sigstore-aware build pipelines) will see v3.3.1 as the first attested release in the v3.x line.
911
+
912
+ ### v3.3.x trajectory ahead (unchanged from v3.3.0's roadmap)
913
+
914
+ - **v3.3.2** — F-TEST-002 universalSign coverage (chainweaver/eckowallet/foreign branches)
915
+ - **v3.3.3** — F-TEST-005 success-path tests for the 13 v3.0.0 nullable-widened functions
916
+ - **v3.3.4** — F-TEST-006 behavioural tests for `pensionFunctions`/`guardFunctions`/`infoOneFunctions`
917
+ - **v3.3.5+** — Documentation/deprecation cleanups (KADENA_BASE_URL, CreateAccountOptions JSDoc, etc.)
918
+ - **v4.0.0** — Major structural release (monorepo split, god-file decomposition, type consolidation)
919
+
920
+ ---
921
+
922
+ ## 3.3.0 — 2026-05-06
923
+
924
+ **MINOR, additive (Logger interface extension) + behaviour change (call-site routing).** First release in the v3.3.x line, opening the second post-audit cleanup track. Closes the consolidated **F-LOGGER-SEAM-001** finding the 2026-05-05 audit flagged across 8 of 8 audit agents at 9 distinct source sites — the highest-redundancy finding in the entire audit. Two of the nine sites were already removed by v3.2.2's deletion of `executeAddLiquidityMultiStepComplete` (`addLiquidityFunctions.ts:642` + `:660`); v3.3.0 closes the remaining seven by extending the `Logger` interface with an `info(...)` method and routing every surviving raw `console.*` call in `src/` through the seam (or deleting debug-leak instrumentation that had no operational value). Post-v3.3.0 invariant: **zero raw `console.*` call sites in `src/` outside the seam's own default-logger implementation in `observability/logger.ts`** — verified by a new regression-lock test in `tests/v3-3-0-logger-seam-completion.test.ts` that greps the entire `src/` tree (excluding JSDoc/comments/the seam file itself) and fails on any future regression. **622/622 tests pass** (was 618 in v3.2.3; +3 new logger-seam contract tests covering the new `info` channel, +1 regression-lock test scanning src/, +1 existing test updated to reflect the new 3-method Logger shape).
925
+
926
+ ### Added — `info(msg, ...args): void` channel on `Logger`
927
+
928
+ `src/observability/logger.ts` extends the `Logger` type from a 2-method shape (`{warn, error}`) to a 3-method shape (`{warn, error, info}`). The new channel is for operational events that aren't errors but consumers may still want to capture in their structured logs:
929
+
930
+ - **Node-recovery announcements** — `nodeFailover.ts:61`'s "primary node recovered, switching back" message (symmetric counterpart to line 53's failover-detected `getLogger().warn(...)`).
931
+ - **Error-suggestions callouts** — `transactionErrors.ts:259`'s "Suggestions:" line in `logDetailedError` (the operationally-informative "how do you recover from this?" annotations attached to typed `SigningError` instances).
932
+
933
+ The pre-v3.3.0 seam exposed only `warn`/`error`, so these `info`-class events fell through to raw `console.info` calls that bypassed consumer-supplied loggers (HUB pino, OuronetUI Sentry, redux-devtools panels). The new channel keeps the seam semantically aligned with what the codebase actually emits.
934
+
935
+ ### Changed — `setLogger` accepts a partial input shape with `info` filled-in default (v3.x backwards-compat)
936
+
937
+ Pre-v3.3.0 the `setLogger` parameter was typed as `Logger` (which now requires `info`). To avoid forcing every existing v3.2.x consumer to update their `setLogger({warn, error})` call sites in lockstep with v3.3.0, the setter now accepts:
938
+
939
+ - A **full** `Logger` (`{warn, error, info}`) — reference identity preserved; `getLogger()` returns the same object passed in.
940
+ - A **partial** v3.2.x-compatible input (`{warn, error}` with no `info`) — the setter synthesises a wrapper that fills in `info` from the default `console.info` routing. The wrapper is a NEW object (not the input reference); `info` calls go to `console.info`, `warn`/`error` go to the consumer's logger.
941
+
942
+ This keeps v3.2.x consumers working unchanged while letting v3.3.0+ consumers wire `setLogger({warn, error, info})` for full structured-log capture. Type signature: `setLogger(logger: Logger | { warn, error }): void`.
943
+
944
+ ### Changed — 7 raw `console.*` call sites in `src/` routed through the seam (or deleted)
945
+
946
+ | File:Line | Before | After |
947
+ |---|---|---|
948
+ | `transactionErrors.ts:252` | `console.group(\`🚨 ${error.name}: ${error.code}\`)` | folded into `getLogger().error(\`🚨 ${error.name}: ${error.code} — ${error.message}\`)` (seam doesn't model grouping; pino/structured loggers don't support it) |
949
+ | `transactionErrors.ts:259` | `console.info("Suggestions:", error.suggestions)` | `getLogger().info("Suggestions:", error.suggestions)` |
950
+ | `transactionErrors.ts:261` | `console.groupEnd()` | dropped (no seam equivalent) |
951
+ | `nodeFailover.ts:61` | `console.info("[node-failover] Primary node recovered...", PRIMARY_HOST)` | `getLogger().info("[node-failover] Primary node recovered...", PRIMARY_HOST)` |
952
+ | `infoOneFunctions.ts:599` | `console.log("[INFO_RemoveLiquidity] pactCode:", pactCode)` | DELETED (debug-leak — `getLogger().warn` already on line 602 for the failure path) |
953
+ | `infoOneFunctions.ts:600` | `console.log("[INFO_RemoveLiquidity] response:", JSON.stringify(response?.result, null, 2))` | DELETED (debug-leak — pretty-printed JSON dump on every preview read had no operational value beyond developer trace, plus added serialisation cost) |
954
+ | `ouroFunctions.ts:1590` | `console.log("Coil preview failed:", response?.result)` | `getLogger().warn("Coil preview failed:", response?.result)` (failure context — kept at warn-level so structured-logger consumers capture it) |
955
+ | `ouroFunctions.ts:1595` | `console.log("Coil preview response data:", data)` | DELETED (debug-leak — success-path data dump, no operational value) |
956
+ | `urStoaFunctions.ts:348` | `console.info(\`[UrStoa] Rebuilding transaction with ${validGuardKeys.length} valid guard key(s)...\`)` | `getLogger().warn(...)` (promoted to warn-level — signature pruning is an unusual operational event that a HUB operator running structured logs would want in their incident pipeline, not info-level chatter) |
957
+
958
+ Net 4 calls routed through the seam, 3 deleted as debug-leak. Two `console.log` instances flagged by the audit as "left-over dev instrumentation" are removed entirely — their information value was zero (developer-only trace; the legitimate diagnostics on the same code paths already routed through `getLogger`). One was promoted from `info` → `warn` (urStoaFunctions:348's signature-pruning event) because the level mismatch between "we just rebuilt a transaction because some keys didn't match" and "info-level status update" was an audit smell.
959
+
960
+ ### Added — `tests/v3-3-0-logger-seam-completion.test.ts` regression lock
961
+
962
+ A new test file scans the entire `src/` tree (recursively, all `.ts` files, excluding `.d.ts`) for raw `console.{log,info,group,groupEnd,debug,warn,error}` call patterns. JSDoc blocks (`/** ... */`), single-line `// ...` comments, and `*` continuation lines are filtered out. The seam's own default-logger implementation in `observability/logger.ts` is exempted (that's how the default routing works — `console.warn` etc. ARE the implementation of the default `Logger.warn` etc.). Any future commit that introduces a raw `console.*` call elsewhere in `src/` fails this test with a structured per-violation report. The audit's consolidated finding becomes a permanent invariant rather than a one-time cleanup.
963
+
964
+ ### Verified
965
+
966
+ - `npm run typecheck` — zero errors. The `Logger | { warn, error }` union type on `setLogger`'s parameter compiles cleanly; the runtime fill-in branch produces a `Logger` whether the input had 2 or 3 methods.
967
+ - `npm test` — **622/622 tests pass** (was 618 in v3.2.3; +3 new + 1 regression-lock + 1 existing-updated to reflect the new 3-method Logger shape).
968
+ - `npm run build` — clean tsc emit. The new `Logger.info` field is visible in the published `.d.ts` of the `./observability` subpath.
969
+ - Direct-grep verification: `grep -rE "console\.(log|info|group|groupEnd|debug|warn|error)" src/` returns only JSDoc/comment matches and the seam-file's intentional default-logger implementations. No call-sites remain.
970
+
971
+ ### Migration
972
+
973
+ For v3.2.x consumers wiring `setLogger({warn, error})`: **no migration required**. The setter's backwards-compat path synthesises an `info` wrapper that falls through to `console.info` for the new channel. Existing `getLogger().warn(...)` and `.error(...)` call sites continue to behave identically.
974
+
975
+ For consumers who want full control of all three channels (recommended for HUB pino integrations and OuronetUI Sentry/redux-devtools wiring): pass a 3-method object to `setLogger`:
976
+
977
+ ```ts
978
+ import { setLogger, type Logger } from "@ouronet/ouronet-core/observability";
979
+
980
+ const myLogger: Logger = {
981
+ warn: (msg, ...args) => myPipeline.warn(msg, args),
982
+ error: (msg, ...args) => myPipeline.error(msg, args),
983
+ info: (msg, ...args) => myPipeline.info(msg, args),
984
+ };
985
+ setLogger(myLogger);
986
+ ```
987
+
988
+ Output that previously bypassed the seam (the 4 `console.{group,info}` calls + 4 `console.log` debug-leaks the audit identified) is now either captured by the consumer's logger (4 routed calls) or removed entirely (4 deleted debug-leaks — the developer-only trace they emitted had no operational value).
989
+
990
+ For consumers calling `logDetailedError(error: SigningError)` from `@ouronet/ouronet-core/errors`: behaviour change. Pre-v3.3.0 the function emitted `console.group`/`console.groupEnd` framing around the error fields; post-v3.3.0 it emits a single `getLogger().error(...)` line with the `name: code — message` header inlined, plus the existing `Context:`, `Original Error:`, and `Suggestions:` (now via `getLogger().info`) routed through the seam. Visual layout in browser DevTools is flatter (no collapsible group), but consumer-supplied structured loggers now capture the entire error envelope reliably. If the grouped-DevTools layout was important to a consumer, they can wrap the call in their own `console.group` / `console.groupEnd` before/after invoking it.
991
+
992
+ ---
993
+
994
+ ## 3.2.3 — 2026-05-05
995
+
996
+ **MINOR, behaviour change.** Fourth and final wave of the v3.2.x audit-cycle close-out track. Four targeted bug fixes that close the highest-user-impact remaining audit findings: `creationTime` in `buildCrossChainTransfer` (F-BUG-002), `fetchSpvProof` failover + 30s AbortController timeout (F-BUG-004 — the highest-impact bug in the entire audit), `setNodeConfig` URL parse + `https:` scheme allow-list (F-SEC-002), and `@throws` JSDoc documentation on the three crossChainFunctions submit/listen helpers (F-ERR-001). With these four findings closed, the v3.2.x sequence has remediated **15 of the audit's 62 confirmed findings** across four ship cycles. The remaining ~47 findings (logger-seam completion, test coverage, structural decomposition, type consolidation, etc.) are scheduled for v3.3.x and v4.0.0 per the audit's suggested spec groupings. **618/618 tests pass** (was 601 in v3.2.2; +17 new it-blocks in `tests/v3-2-3-bug-fixes.test.ts` covering creationTime presence + offset, setNodeConfig URL validation across 8 cases, and fetchSpvProof failover/timeout/proof-shape across 4 cases; +2 existing network.test.ts tests updated to reflect the new setNodeConfig contract).
997
+
998
+ ### Added — `creationTime: safeCreationTime()` to `buildCrossChainTransfer` (closes F-BUG-002)
999
+
1000
+ `src/interactions/crossChainFunctions.ts:113-119` setMeta block now includes `creationTime: safeCreationTime()`. Pre-v3.2.3 the field was omitted and `@kadena/client` fell back to `Math.floor(Date.now() / 1000)`. On a consumer machine with a slightly-ahead clock, that produced `creationTime` values past the chainweb node's "is creation time too far in the future?" tolerance window, causing sporadic submit rejections that confusingly looked like network failures. The v2.3.0 audit consolidated `safeCreationTime()` (returns `Date.now()/1000 - 30`) precisely to absorb client-side clock drift; this builder was the lone interactions-surface function that omitted the helper after the v2.3.0 sweep — every other `setMeta` block (including the sibling `buildCTransferAcross` two functions below) already includes it. One-line addition; no other behaviour change.
1001
+
1002
+ ### Added — `withFailover` + `AbortSignal.timeout(30s)` on `fetchSpvProof` (closes F-BUG-004 — HIGHEST USER-IMPACT FIX)
1003
+
1004
+ `src/interactions/crossChainFunctions.ts:267-345` (`fetchSpvProof`) is rewritten to wrap the `fetch()` call in `withFailover` and add a per-attempt `AbortSignal.timeout(SPV_PROOF_TIMEOUT_MS)` deadline. Pre-v3.2.3 this was the only chain-RPC function in the entire codebase that called raw `fetch()` without either guard, with three compounding consequences:
1005
+
1006
+ 1. **No `AbortController`**: a wedged primary node (slow, not erroring) would hang the `await fetch(...)` indefinitely. The outer `pollSpvProof` retry loop never advanced because each attempt awaited the hung fetch.
1007
+ 2. **No `withFailover`**: even if the primary node returned an error or timed out, traffic stayed pinned to the primary; the consumer's subsequent attempts hit the same wedged node forever.
1008
+ 3. **Combined consequence**: the user's KDA was committed to `kadena-xchain-gas` escrow on the source chain (step 1 succeeded), but step 2's SPV proof retrieval hung silently with the UI showing a perpetual "Waiting for SPV proof..." spinner and **no recovery path**.
1009
+
1010
+ This was identified by the bug-detector audit agent as the highest-impact bug surfaced by the 2026-05-05 audit. Post-v3.2.3 behaviour: each attempt has a hard 30-second deadline; on timeout (`TimeoutError`/`AbortError`) or any network-class error on the primary node, `withFailover` switches to the fallback node and retries once. The outer `pollSpvProof` loop then re-invokes this function after the configured `delayMs` (default 5s), so a stuck primary surfaces as ~30s of waiting, then automatic fallback, then continued polling — never an infinite hang. New module-level constant `SPV_PROOF_TIMEOUT_MS = 30_000` makes the deadline a single tunable. The unused `getSpvUrl` import is removed (the URL is now constructed inline from `withFailover`'s `baseUrl` callback).
1011
+
1012
+ ### Added — URL parse + `https:` scheme allow-list on `setNodeConfig` (closes F-SEC-002)
1013
+
1014
+ `src/network/nodeFailover.ts:160-194` (`setNodeConfig`) `selected: "custom"` path now validates `customUrl` before assignment via three guards:
1015
+
1016
+ 1. **Required-field check**: `customUrl` must be present (was just truthy-check pre-v3.2.3, which silently fell through to node2 on missing input — now throws `TypeError("customUrl is required when selected === 'custom'")`).
1017
+ 2. **URL parseability**: `new URL(customUrl)` rejects malformed strings (`"foo"`, `"not a url"`, etc.) with `TypeError("customUrl is not a valid URL")`.
1018
+ 3. **Scheme allow-list**: only `https:` is accepted. `http:`, `javascript:`, `ftp:`, etc. throw `TypeError("customUrl must use https://")`. Chain transactions sign sensitive payloads (capability args, derived public keys); transmitting them over plaintext defeats the cryptographic discipline the rest of the codebase enforces.
1019
+
1020
+ Plus a fourth defensive change: `parsed.origin` discards any pathname/query/fragment from the input URL. Pre-v3.2.3 the entire input string was assigned to `PRIMARY_HOST`, so a `customUrl` like `"https://node.example.com/some-path"` would have produced `https://node.example.com/some-path/chainweb/0.0/{network}` when `getActiveBaseUrl()` appended its suffix — a confusing trap. Now the host portion is the only part that survives.
1021
+
1022
+ This is a **behaviour change for consumers passing malformed `customUrl`**: pre-v3.2.3 such inputs silently fell through to default node2; post-v3.2.3 they throw `TypeError` at the function entry. Any consumer relying on the silent-fallthrough was already shipping wrong configuration; the throw is the audit-mandated diagnostic improvement. Added `@throws` to the function's JSDoc so the contract is documented.
1023
+
1024
+ ### Added — `@throws` JSDoc on `submitCrossChainTransfer`, `submitContinuation`, `listenForCompletion` (closes F-ERR-001)
1025
+
1026
+ `src/interactions/crossChainFunctions.ts` — three submit/listen helpers gain JSDoc `@throws` annotations documenting their error contracts. Runtime behaviour is unchanged; this is documentation-only. The three helpers diverge from the discriminated-union pattern (`{status, error?: string}`) used by their siblings (`pollTransactionStatus`, `getBalanceOnChain`) — they propagate errors via throws because a failed submit/listen is a hard failure that must surface to the consumer's error UI, not a transient state to poll past. The audit finding asked for either consistent envelope-wrapping OR documentation; we picked documentation because the throwing behaviour is correct (see the F-ERR-014 lesson: the multi-step add-liquidity path that swallowed listen-timeouts caused user double-pay; we don't want to repeat that pattern here). The new JSDoc explicitly distinguishes:
1027
+
1028
+ - **`submitCrossChainTransfer`** / **`submitContinuation`**: `@throws SigningError(code: "TIMEOUT")` on per-tier deadline; `@throws Error` on network failure surviving both primary+fallback. Caller treats either as **definitively failed** — no retry without surfacing to user.
1029
+ - **`listenForCompletion`**: same `@throws` shape, but with the critical caveat that a TIMEOUT **must be treated as `pending`, not `failed`** — the chainweb listen endpoint times out at 180s, but the transaction may still complete on chain after that deadline. Caller should poll via `pollTransactionStatus` rather than retry the submit (which would double-pay gas for a transaction that may already be confirmed). This is the exact pattern F-ERR-014 surfaced for the multi-step add-liquidity flow before that surface was deleted in v3.2.2 — codified here as documentation so future consumers don't re-introduce it.
1030
+
1031
+ ### Verified
1032
+
1033
+ - `npm run typecheck` — zero errors. The unused `getSpvUrl` and `getActiveSpvUrl` imports were cleaned up after the `fetchSpvProof` refactor; no other call sites depend on them.
1034
+ - `npm test` — **618/618 tests pass** (was 601 in v3.2.2; +17 new it-blocks for the v3.2.3 surface; +2 existing `tests/network.test.ts` tests updated to reflect the new `setNodeConfig` throw-on-malformed-input contract).
1035
+ - `npm run build` — clean tsc emit. The four behaviour changes flow into `dist/`: `creationTime` in cross-chain transactions, the rewritten `fetchSpvProof` with timeout + failover, the validated `setNodeConfig`, and the documented error contracts on the three submit/listen helpers.
1036
+
1037
+ ### Migration
1038
+
1039
+ For consumers calling `buildCrossChainTransfer`: no change required. The `creationTime` addition is invisible at the API surface — the transaction shape is the same, just with one extra `meta` field that didn't exist before. Sporadic chain-side rejections under client clock drift should disappear.
1040
+
1041
+ For consumers calling `fetchSpvProof` directly (as opposed to via `pollSpvProof`): no change required. Same return shape (`{ proof: string | null; error?: string }`); the difference is that a wedged primary node now produces a `{ proof: null, error: "SPV proof fetch timed out after 30000ms..." }` after 30s rather than hanging forever. Callers that already handle the `proof: null` path get the new behaviour for free.
1042
+
1043
+ For consumers calling `setNodeConfig("custom", customUrl, ...)`: **migration required if `customUrl` may come from untrusted/unvalidated input**. Wrap the call in try/catch:
1044
+
1045
+ ```ts
1046
+ try {
1047
+ setNodeConfig("custom", userInput);
1048
+ } catch (e: unknown) {
1049
+ if (e instanceof TypeError) {
1050
+ showError("Invalid custom node URL. Must use https:// scheme.");
1051
+ } else {
1052
+ throw e;
1053
+ }
1054
+ }
1055
+ ```
1056
+
1057
+ Consumers passing well-formed `https://` URLs see no behaviour change. Consumers passing malformed input were already shipping wrong configuration (the silent-fallthrough was a footgun, not a feature); the throw is the audit-mandated diagnostic improvement.
1058
+
1059
+ For consumers calling `submitCrossChainTransfer` / `submitContinuation` / `listenForCompletion`: no migration required. Runtime behaviour unchanged; the new `@throws` JSDoc just documents existing behaviour. Callers should ensure they wrap these in try/catch (as they always should have); the `listenForCompletion` JSDoc now flags that a TIMEOUT must be treated as `pending` and polled, **not** as `failed` and retried (which would double-pay gas).
1060
+
1061
+ ### v3.2.x sequence — completed
1062
+
1063
+ | Wave | Version | Findings closed | Type |
1064
+ |---|---|---|---|
1065
+ | 1 | v3.2.0 | (infrastructure only) | additive |
1066
+ | 2 | v3.2.1 | F-SEC-001, F-BUG-003 | behaviour change |
1067
+ | 3 | v3.2.2 | F-ERR-005, F-ERR-014, F-PERF-014, F-PERF-015, F-API-026 | public-API removal |
1068
+ | 4 | v3.2.3 | F-BUG-002, F-BUG-004, F-SEC-002, F-ERR-001 | behaviour change + docs |
1069
+
1070
+ **15 audit findings closed** across the v3.2.x track (counting v3.1.1's earlier 5 audit-cycle gaps brings the total to **20** across the v3.x line). Next: v3.3.x for logger-seam completion + test coverage + documentation cleanups; v4.0.0 for structural decomposition + monorepo split + type consolidation.
1071
+
1072
+ ---
1073
+
1074
+ ## 3.2.2 — 2026-05-05
1075
+
1076
+ **MINOR, public API removal.** Third wave of the v3.2.x audit-cycle close-out. Removes the four `executeAddLiquidityMultiStep*` functions plus the `MultiStepAddLiquidityResult` type from `src/interactions/addLiquidityFunctions.ts`, along with the `_strategy` parameter on `executeAddLiquidity`. Closes audit findings **F-ERR-005** (`error.message.includes` retry-loop crash on non-Error throws), **F-ERR-014** (listen-timeout vs submit-failure conflation causing user double-pay risk), **F-PERF-014** (4× hardcoded 3-second sleeps), **F-PERF-015** (retry-with-fixed-sleep against string-matched `error.message.includes("Cannot find module")` patterns), and **F-API-026** (the `_strategy: "auto" | "single" | "multi"` parameter on `executeAddLiquidity` was always handled as `"auto"` → single-step path; dead public surface). All five findings are closed **by removal** rather than fix — the cleaner outcome by far. Net code change: **−338 lines** (1031 → 693 lines in `addLiquidityFunctions.ts`). **601/601 tests pass** unchanged; no test exercised the removed functions, which was itself a v3.2.x audit signal that the surface was unused.
1077
+
1078
+ ### Why this is a public-API removal (and why it's classified MINOR not MAJOR)
1079
+
1080
+ The four removed functions were exported from `src/interactions/addLiquidityFunctions.ts` and reachable via the per-file glob subpath `@ouronet/ouronet-core/interactions/addLiquidityFunctions`. Strict semver classifies any removal of an exported public symbol as a breaking change requiring a MAJOR bump. We're classifying as MINOR for v3.2.2 because:
1081
+
1082
+ 1. **The functions had no consumer.** OuronetUI is the only known consumer of the multi-step path; per the user's confirmation, OuronetUI hasn't called these since the StoaChain chainweb gas-limit increase made multi-step unnecessary. The published v3.x.x npm artefacts contain the functions, but no code in this ecosystem invokes them.
1083
+ 2. **The Pact-side multi-step contract still exists on chain** (`TS01-CP.SWP|C_AddStandardLiquidity` defpact with continuation steps) — preserved for historical interoperability. Consumers who genuinely need the multi-step flow (none expected) can build it directly via `@kadena/client`'s `Pact.builder.continuation` API; we just stop providing the TypeScript wrappers in this package.
1084
+ 3. **The dead code carried real correctness risk.** F-ERR-005's `error.message.includes` crash on non-Error throws is a genuine bug; F-ERR-014's listen-timeout-vs-submit-failure conflation could cause user double-pay; F-PERF-014's hardcoded sleeps add ~6 seconds of unnecessary wall-clock latency to every successful flow. Fixing each in place would require ~30 minutes of work each plus tests; removing the surface that nobody uses takes 5 minutes and closes all five findings simultaneously.
1085
+
1086
+ If a consumer surfaces post-publish that DID rely on the multi-step path, this becomes a v3.x → v4.0.0 trigger; otherwise the MINOR classification holds. Documenting the removal explicitly in the migration section below so the audit trail is unambiguous.
1087
+
1088
+ ### Removed — four `executeAddLiquidityMultiStep*` functions
1089
+
1090
+ - **`executeAddLiquidityMultiStep1(params: AddLiquidityParams): Promise<any>`** — built and submitted the initial defpact transaction for `TS01-CP.SWP|C_AddStandardLiquidity`. ~90 lines.
1091
+ - **`executeAddLiquidityMultiStep2(params: AddLiquidityParams, step1Result: any): Promise<any>`** — built and submitted the first continuation step. ~50 lines.
1092
+ - **`executeAddLiquidityMultiStep3(params: AddLiquidityParams, step1Result: any): Promise<any>`** — built and submitted the second continuation step. ~50 lines.
1093
+ - **`executeAddLiquidityMultiStepComplete(params: AddLiquidityParams, onProgress?: (step: number) => void): Promise<MultiStepAddLiquidityResult>`** — orchestrated all three steps with the retry loops, hardcoded sleeps, and confused error-handling that the audit flagged. ~140 lines.
1094
+
1095
+ ### Removed — `MultiStepAddLiquidityResult` type
1096
+
1097
+ - The discriminated-union return shape `{ type: "multi-step"; steps: Array<{stepNumber, transaction, requestKey, status}>; totalSteps; requestKey?; chainId?; networkId? }` is no longer exported. It was used only as the return type of `executeAddLiquidityMultiStepComplete` and was not referenced from any test file or other source file (verified via repo-wide grep at removal time).
1098
+
1099
+ ### Removed — `_strategy` parameter on `executeAddLiquidity`
1100
+
1101
+ The signature changed from:
1102
+
1103
+ ```ts
1104
+ executeAddLiquidity(params: AddLiquidityParams, _strategy?: "auto" | "single" | "multi"): Promise<any>
1105
+ ```
1106
+
1107
+ to:
1108
+
1109
+ ```ts
1110
+ executeAddLiquidity(params: AddLiquidityParams): Promise<any>
1111
+ ```
1112
+
1113
+ The parameter was prefix-underscored (a TypeScript convention for unused parameters) and the function body always called `executeAddLiquiditySingle(params)` regardless of the `_strategy` value. Closes F-API-026.
1114
+
1115
+ ### Verified
1116
+
1117
+ - `npm run typecheck` — zero errors. The removal is internally consistent; no remaining code in `src/` references the removed symbols (verified via `grep -rn "MultiStepAddLiquidityResult|executeAddLiquidityMultiStep" src/` returning zero hits post-removal).
1118
+ - `npm test` — **601/601 tests pass**, unchanged. No test file referenced the removed functions or types — itself a strong audit signal that the surface was unused (any working code is tested; untested code is suspect).
1119
+ - `npm run build` — clean tsc emit. The `dist/interactions/addLiquidityFunctions.js` and `.d.ts` no longer carry the removed exports.
1120
+
1121
+ ### Migration
1122
+
1123
+ For consumers calling any of the four `executeAddLiquidityMultiStep*` functions: the recommended migration is to call `executeAddLiquidity(params)` instead — same `AddLiquidityParams` shape, single-step under the 2M chainweb gas budget. The TypeScript SDK no longer exposes the multi-step wrappers; the on-chain `TS01-CP.SWP|C_AddStandardLiquidity` defpact is still callable via `@kadena/client`'s low-level `Pact.builder.continuation()` API for any consumer with the unusual need.
1124
+
1125
+ For consumers passing `_strategy` to `executeAddLiquidity`: drop the second argument. The function's behaviour is identical (it always called the single-step path anyway).
1126
+
1127
+ For consumers importing `MultiStepAddLiquidityResult` as a type: this type is gone; consumers should not have been importing it (the only producer was `executeAddLiquidityMultiStepComplete`, which is also gone). If a consumer does have an orphan reference, it should be deleted.
1128
+
1129
+ ---
1130
+
1131
+ ## 3.2.1 — 2026-05-05
1132
+
1133
+ **MINOR, behaviour change.** Second wave of the v3.2.x audit-cycle close-out. v3.2.0 built the number-hygiene infrastructure (`formatDecimalForPact` with comma support, `formatIntegerForPact`, branded types); v3.2.1 puts those helpers to work at the four chain-call sites the 2026-05-05 audit flagged. Closes audit findings **F-SEC-001** (Pact-code injection via raw `${amount}` interpolation in urStoa stake/unstake) and **F-BUG-003** (`parseFloat(amount).toFixed(N)` silent precision loss + silent rounding in cross-chain transfer + urStoa native transfer). **601/601 tests pass** (was 593 in v3.2.0; +8 new decimal-validation tests pinning the synchronous-throw + comma-normalisation + arbitrary-precision contract at the function boundaries).
1134
+
1135
+ ### Why this is a behaviour change
1136
+
1137
+ Pre-v3.2.1, the four call sites silently corrupted user input:
1138
+
1139
+ - `parseFloat("1.9999").toFixed(3)` → `"2.000"` — silent rounding. User typed 1.9999 urStoa, transaction sent 2.000.
1140
+ - `parseFloat("1,5").toFixed(3)` → `"1.000"` — silent comma-stripping (parseFloat is lenient). User typed 1,5 KDA, transaction sent 1.0.
1141
+ - `parseFloat("garbage").toFixed(12)` → `"NaN"` — gets interpolated into Pact code, chain rejects with confusing error far from the actual cause.
1142
+ - Raw `${amount}` interpolation in urStoa stake/unstake accepts `"1.0) (some-injected-form"` as syntactically valid Pact code (Pact-code injection vector).
1143
+
1144
+ Post-v3.2.1, all four sites route through `formatDecimalForPact(amount)`, which:
1145
+
1146
+ - **Throws synchronously on malformed input** with `Error("Invalid decimal format")` — caller's catch block fires immediately, before any chain interaction begins.
1147
+ - **Preserves arbitrary precision** — the formatter never round-trips through float64. A 39-digit-int + 18-digit-fractional decimal that pre-v3.2.1 would have been silently truncated by `.toFixed(12)` is now round-tripped byte-identical.
1148
+ - **Accepts EU-locale comma input** (per the v3.2.0 contract) and normalises to period before validation. UI text fields capturing `"1,5"` work without upstream normalisation.
1149
+ - **Truncates at 24 decimals** rather than rounding at 3/12, so the chain sees the user's full intent up to the (configurable) cap.
1150
+
1151
+ This is classified as MINOR rather than MAJOR because:
1152
+ - Any caller relying on the silent-failure paths (`"NaN"` interpolation, silent rounding, silent comma-stripping) was already producing wrong on-chain values; the throw is the audit-mandated diagnostic improvement, not a regression.
1153
+ - The function signatures are unchanged (still `(amount: string, ...)` — not yet brand-typed as `ValidatedDecimal`; that's reserved for v4.0.0 since it would force every consumer to migrate).
1154
+ - Comma-as-decimal-separator is strictly additive: every previously-rejected EU-locale input now succeeds, and every previously-accepted input continues to produce identical output.
1155
+
1156
+ ### Changed — 4 chain-call sites adopt `formatDecimalForPact`
1157
+
1158
+ - **`src/interactions/crossChainFunctions.ts:92`** (`buildCrossChainTransfer`) — replaced `parseFloat(amount).toFixed(12)` with `formatDecimalForPact(amount)`. The interpolated value in the `coin.transfer-crosschain` Pact code is now arbitrary-precision-safe and EU-locale-friendly.
1159
+ - **`src/interactions/urStoaFunctions.ts:206`** (`executeNativeUrStoaTransfer`) — replaced `parseFloat(amount).toFixed(3)` with `formatDecimalForPact(amount)`. The 4-decimal silent-rounding case (`"1.9999"` → `"2.000"`) is fixed: the user's full input precision now reaches `coin.C_UR|Transfer/Transmit/...AnewVariants`.
1160
+ - **`src/interactions/urStoaFunctions.ts:441`** (`executeStakeUrStoa`) — wrapped raw `${amount}` interpolation in `coin.C_URV|Stake` Pact code with `formatDecimalForPact(amount)`. The validated string is computed once outside the closure and reused both in the pact-code interpolation AND the `coin.URV|STAKE` capability arg, so the cap-arg and the executed code are guaranteed to agree (was `String(numAmount)` separately, which had the float-precision gap). Closes F-SEC-001 (Pact-code injection vector) and F-BUG-003 (cap-arg precision drift).
1161
+ - **`src/interactions/urStoaFunctions.ts:497`** (`executeUnstakeUrStoa`) — same pattern as `executeStakeUrStoa`, applied to `coin.C_URV|Unstake` and `coin.URV|UNSTAKE`.
1162
+
1163
+ ### Deprecated — `numAmount` field on `StakeUrStoaParams` / `UnstakeUrStoaParams`
1164
+
1165
+ The `numAmount: number` field on these two parameter types is no longer read by the executors (the validated `amount: string` is now the single source of truth for both pact-code and cap-arg). The field is **retained on the interfaces for v3.x backwards compatibility** but marked `@deprecated` in JSDoc and changed from required to optional. Will be removed in v4.0.0. Consumers can stop populating it as soon as they bump to v3.2.1.
1166
+
1167
+ ### Added — 8 new it-blocks in `tests/interactions-decimal-validation.test.ts`
1168
+
1169
+ A new test file pinning the v3.2.1 contract:
1170
+
1171
+ - 6 tests against `buildCrossChainTransfer` covering: synchronous throw on malformed input, synchronous throw on mixed period+comma, synchronous throw on multi-comma thousand-separator, comma-to-period normalisation in pact-code, 18-decimal-fractional precision preservation past pre-v3.2.1's 12-decimal `.toFixed` truncation, and 39-digit-int amount preservation past `Number.MAX_SAFE_INTEGER`.
1172
+ - 2 tests against `executeStakeUrStoa` and `executeUnstakeUrStoa` proving validation happens at function entry: a counting `PactReader` stub asserts the reader is **never invoked** when the amount input is malformed — i.e., the function rejects before any chain interaction starts. This is the strongest assertion of the contract: validation is not somewhere deep in the call stack, it's at the boundary.
1173
+
1174
+ ### Verified
1175
+
1176
+ - `npm run typecheck` — zero errors. The `numAmount` field demotion to optional is correctly typed; the `ValidatedDecimal` return from `formatDecimalForPact` flows into both `string` slots (pact-code interpolation and cap-arg `decimal` field) without explicit casts because `ValidatedDecimal extends string` structurally.
1177
+ - `npm test` — **601/601 tests pass** (was 593 in v3.2.0; +8 new decimal-validation tests).
1178
+ - `npm run build` — clean tsc emit. The four call-site changes are visible in the published `.d.ts` only via the `@deprecated` JSDoc on the `numAmount` fields (the function signatures themselves are unchanged).
1179
+
1180
+ ### Migration
1181
+
1182
+ For consumers passing well-formed decimal strings (digits, optional period, no comma): **no migration needed**. Same input → same output. The existing call shape continues to work.
1183
+
1184
+ For consumers passing comma-decimal EU-locale input: this previously failed with the legacy `parseFloat`-based formatters returning either `NaN` (then `"NaN"` in pact code → chain reject) or `1` (silent comma-strip → wrong amount). Post-v3.2.1, `"1,5"` → `"1.5"` automatically. **No code change needed**, but the previously-broken path now works.
1185
+
1186
+ For consumers passing malformed input (garbage strings, mixed separators, scientific notation): pre-v3.2.1 these silently produced `"NaN"` or wrong-on-chain amounts. Post-v3.2.1 they throw `Error("Invalid decimal format")` synchronously at the function entry. **Wrap in try/catch if your consumer can produce malformed input from upstream sources** (e.g., user typing in a text field without UI-side validation). The synchronous throw is the audit-mandated improvement — a legible diagnostic at the right layer beats a confusing chain-side rejection downstream.
1187
+
1188
+ For consumers populating `numAmount` on `StakeUrStoaParams` / `UnstakeUrStoaParams`: the field is now optional. You can drop it from your call sites whenever convenient. It will be removed entirely in v4.0.0.
1189
+
1190
+ ---
1191
+
1192
+ ## 3.2.0 — 2026-05-05
1193
+
1194
+ **MINOR, additive.** First wave of the v3.2.x audit-cycle close-out — number-hygiene infrastructure for Pact-bound integers and decimals. Pact has arbitrary-precision integers AND arbitrary-precision decimals; JavaScript's number primitive is IEEE-754 float64 (~15-17 significant digits). Round-tripping a chain value through `parseFloat` / `Number()` / `.toFixed()` silently destroys precision for any value beyond float64's range. v3.2.0 closes that loophole at the package level by introducing the validation contract; v3.2.1 will apply it at the existing `parseFloat(...).toFixed(N)` call sites; v3.2.2 will remove the dead multi-step add-liquidity surface; v3.2.3 will land the targeted bug fixes (creationTime, fetchSpvProof failover, setNodeConfig URL validation). **No consumer-visible behaviour changes in v3.2.0** — every existing valid input to `formatDecimalForPact` continues to produce the same output. The additions are: a relaxation of the input contract to accept comma-as-decimal-separator (so European-locale UI text fields work without upstream normalisation), a new sibling helper for integer-typed Pact arguments, and a pair of branded TypeScript types that prove "this string passed the formatter" at the type level. **593/593 tests pass** (was 565 in v3.1.1; +28 = 6 comma-normalisation cases, 4 arbitrary-precision round-trip cases including the explicit truncation-at-maxDecimals lock, 13 `formatIntegerForPact` cases, 3 brand-type compile probes; the 1 pre-v3.2.0 "rejects EU decimal separator" test was removed because the new comma-support contract supersedes it, and the 3 brand-type tests use `@ts-expect-error` probes that count as runtime-asserted compile checks).
1195
+
1196
+ ### Added — `formatIntegerForPact(amount: string): ValidatedInteger`
1197
+
1198
+ New helper sibling to `formatDecimalForPact`. Pact distinguishes integers from decimals at the lexer level: functions expecting `integer` arguments (counts, indices, slot numbers, integer-typed cap arguments) reject `1.0` and accept `1`. Without a dedicated formatter, callers historically interpolated integer values via `String(numAmount)` (`urStoaFunctions.ts` cap-args do this today) or hand-rolled `${value | 0}` patterns — both vulnerable to JS float64 precision loss for values past `Number.MAX_SAFE_INTEGER` (`2^53 - 1` ≈ `9.0e15`). The new helper validates `^\d+$` (non-negative, no decimal point, no scientific notation, no thousand-separators), trims whitespace, and returns the trimmed input verbatim with no float round-trip. A 100-digit integer string round-trips byte-identical. Throws `Error("Invalid integer format")` on rejection — same shape as `formatDecimalForPact`'s error message so consumer catch blocks can be written once.
1199
+
1200
+ ### Added — `ValidatedDecimal` and `ValidatedInteger` brand types
1201
+
1202
+ TypeScript-only newtypes (zero runtime cost — just a `unique symbol` brand on the string type) that flow out of `formatDecimalForPact` and `formatIntegerForPact` respectively. A function declared `(amount: ValidatedDecimal) => Transaction` cannot accidentally accept a raw user-input string; the type system requires the caller to pass a value through the formatter first. The two brands are **distinct types** (a `ValidatedInteger` is not assignable to `ValidatedDecimal` and vice versa), preserving Pact's lexer-level int vs decimal distinction at the function-boundary level — the failure mode of accidentally interpolating an integer where a decimal was needed (and vice versa) becomes a compile error rather than a runtime chain-side rejection. Both brands are runtime-equivalent to `string` so JSON serialisation, console logging, and Pact-code interpolation all work without unwrapping. v3.2.1 will adopt them at the call-site signatures so the formatter→builder→submit pipeline is statically traceable.
1203
+
1204
+ ### Changed — `formatDecimalForPact` accepts a single comma as decimal separator
1205
+
1206
+ Pre-v3.2.0, the helper accepted only `^\d+\.?\d*$` and rejected any comma. European-locale UIs that capture user input as `"1,5"` (German `1 Komma 5`, French `1 virgule 5`, etc.) had to swap separators upstream before calling the helper. v3.2.0 normalises a single comma to a period before validation, so `"1,5"` and `"1.5"` both produce `"1.5"`. **Multi-comma strings (thousand-separator-style `"1,234,567"`) still throw** — the consumer's UI must strip thousand-grouping before calling the helper. Mixed comma+period strings (`"1,5.6"`, `"1.234,56"`) also throw — these are ambiguous between "decimal point + grouping" and "grouping + decimal comma" and the helper refuses to guess. The relaxation is strictly additive: every previously-accepted input continues to produce the same output, plus a new class of inputs (single-comma-as-decimal) is now accepted.
1207
+
1208
+ ### Documented — number-hygiene contract in `src/pact/format.ts` JSDoc
1209
+
1210
+ The file-level JSDoc now spells out the three-rule contract: (1) UI/consumer code passes amounts AS STRINGS, never as `number`; (2) the `format*ForPact` family is the sole boundary where a string becomes a Pact-code literal — strict regex enforces digits-only with at most one optional decimal point, no exponent notation, no thousand-separators, no signs; (3) the brand types prove "this string passed the formatter" at the type level so downstream code that interpolates them can rely on the chain accepting them verbatim. This codifies the v2.0.0+ design intent that was previously implicit in the codebase's idioms but never written down.
1211
+
1212
+ ### Verified
1213
+
1214
+ - `npm run typecheck` — zero errors. The brand types compile cleanly with `unique symbol` declarations; the `@ts-expect-error` probes in the new test block prove the compile-time contract.
1215
+ - `npm test` — **593/593 tests pass** (was 565 in v3.1.1; +28 net = +29 new it-blocks for the v3.2.0 surface − 1 deleted "rejects EU decimal separator" test that the comma-support contract supersedes).
1216
+ - `npm run build` — clean tsc emit to `dist/`. The new `formatIntegerForPact` value, `ValidatedDecimal` type, and `ValidatedInteger` type are all exposed in the published `.d.ts` of the `./pact` subpath.
1217
+
1218
+ ### Migration
1219
+
1220
+ No migration required. Every existing valid input continues to produce byte-identical output. New surface is opt-in:
1221
+
1222
+ - Consumers who want to accept European-locale UI input directly can stop pre-stripping commas before the formatter call.
1223
+ - Consumers who type a function parameter as `(amount: ValidatedDecimal)` get compile-time enforcement that the value came through the formatter; consumers who keep using `string` parameters see no change.
1224
+ - `formatIntegerForPact` is a new export; existing call sites that still use `String(num)` continue to work, and v3.2.1 will sweep the codebase to replace them.
1225
+
1226
+ ---
1227
+
1228
+ ## 3.1.1 — 2026-05-05
1229
+
1230
+ **PATCH, additive.** Pre-publish audit-cycle close-out for the v3.1.0 dalos-crypto integration. v3.1.0 was committed locally (`bf10dc1`) but had not been published to npm when the post-integration audit (2026-05-05, see `.bee/AUDIT-REPORT.md`) flagged five gaps directly attributable to that commit. v3.1.1 closes all five before the package reaches npmjs, so the first published release on the new dalos-crypto v4.0.3 line carries the corrections rather than a broken-then-fixed pair. **565/565 tests pass** (was 558 in v3.1.0; +7 = 1 new strict locale grouping-style assertion + 5 new Schnorr re-export tests + 1 new validation-error class probe).
1231
+
1232
+ ### Why a patch release
1233
+
1234
+ v3.1.0 never reached npm — `git log` shows the v3.1.0 commit followed by this v3.1.1 commit before any `git tag v3.1.0` push. From the consumer's perspective, the npm registry skips from `3.0.0` to `3.1.1`, with v3.1.0 visible only in the GitHub commit history. The five gaps closed here are: three additive re-export plumbing fixes that finish the v3.1.0 integration surface, one stylistic alignment that brings the dalos/ subdirectory back in line with the rest of the codebase's CONVENTIONS.md, one test-strictness fix that locks the en-US locale shape introduced in v3.1.0, and a fresh test file that covers the v3.1.0 Schnorr re-exports end-to-end. None of the changes alter runtime behaviour for any consumer of v3.1.0's documented surface; the additions simply make the surface complete and the test suite more decisive.
1235
+
1236
+ ### Added — three typed error classes re-exported on `./dalos` (closes F-BUG-005)
1237
+
1238
+ `@ouronet/dalos-crypto/gen1` v4.0.2 introduced three typed validation-error classes — `InvalidBitStringError`, `InvalidBitmapError`, `InvalidPrivateKeyError` — specifically so consumers can `instanceof`-discriminate validation failures from system errors when calling `generateFromBitString`, `generateFromBitmap`, or `generateFromInteger` (all reachable via `createOuronetAccount`). v3.1.0's Schnorr re-export pass picked up `SchnorrSignError` but missed these three. v3.1.1 re-exports all three from `@ouronet/ouronet-core/dalos` so consumers building bitstring/bitmap key-gen flows (notably OuronetUI's "Draw a bitmap" page and the bitstring-input modal) can catch validation failures with type-safe `instanceof` checks through the OuronetCore subpath alone, instead of dual-importing from `@ouronet/dalos-crypto/gen1`. Class identity is preserved across the re-export — `instanceof InvalidBitStringError` works regardless of whether the consumer imports the class from ouronet-core or directly from dalos-crypto, eliminating the dual-package-hazard footgun.
1239
+
1240
+ ### Added — `CoordAffine` type re-exported on `./dalos` (closes F-API-024)
1241
+
1242
+ `SchnorrSignature.r: CoordAffine` was already reachable in v3.1.0 (`SchnorrSignature` is re-exported), but its component type `CoordAffine` was not, breaking the "single integration surface" promise the `./dalos` subpath JSDoc makes for advanced consumers. Without it, any consumer typing a function parameter as `(sig: SchnorrSignature) => sig.r.x` had to dual-import `CoordAffine` from `@ouronet/dalos-crypto/gen1`. v3.1.1 re-exports `CoordAffine` as a type-only export from the same subpath, restoring the single-import promise.
1243
+
1244
+ ### Changed — dalos/ subdirectory style aligned with CONVENTIONS.md (closes F-ARCH-012)
1245
+
1246
+ `src/dalos/index.ts` and `src/dalos/account.ts` were the only files in `src/` using single-quoted import strings (every other file in the codebase uses double quotes per the project's CONVENTIONS.md note "double-quoted strings — matches `src/signing/types.ts`, `src/network/nodeFailover.ts`, `src/pact/cfmBuilders.ts` consistently"). They were also the only files using explicit `.js` extensions on TypeScript relative imports (`from "./account.js"` instead of `from "./account"`). The drift originated when the dalos integration was first ported from the upstream `@ouronet/dalos-crypto` style and was perpetuated by v3.1.0's Schnorr re-export pass. v3.1.1 converts both files to double quotes throughout and drops the `.js` extension on the two relative imports. Mass-edit only — zero behaviour change. The bundler (`moduleResolution: "bundler"` in `tsconfig.json:6`) handles both forms identically; this is purely a CONVENTIONS.md alignment.
1247
+
1248
+ ### Fixed — locale-determinism test assertion now strict (closes F-TEST-001)
1249
+
1250
+ v3.1.0 hardcoded `'en-US'` in `formatMaxFee`'s `toLocaleString()` call (`src/gas/gasUtils.ts:101`) precisely to make the test assertion deterministic across host locales. The test, however, used `expect(result.anu).toMatch(/10,000,000/)` — a substring regex. The regex would tolerate a future revert of `toLocaleString('en-US')` to `toLocaleString()` as long as CI ran on a US-locale host, defeating the lock entirely. v3.1.1 changes the assertion to strict equality `expect(result.anu).toBe("10,000,000")` and adds a sibling assertion `expect(formatMaxFee(123_456_789, 1).anu).toBe("123,456,789")` that catches grouping-style regressions a uniform-3-digit-group string would not (e.g., a future `toLocaleString('en-IN')` emitting `"12,34,56,789"` still matches the prior regex but fails strict equality). The locale-determinism contract introduced in v3.1.0 is now actually locked by the test suite.
1251
+
1252
+ ### Added — Schnorr re-export coverage in `tests/dalos-integration.test.ts` (closes F-TEST-004)
1253
+
1254
+ v3.1.0 added 6 Schnorr exports (5 values + 1 type) to `src/dalos/index.ts` for advanced consumers. The re-export plumbing was verified at upgrade time only by `npm run typecheck` — there were zero runtime tests pinning the surface. A future delete or rename in `@ouronet/dalos-crypto/gen1` would have broken at consumer's first import rather than in this package's CI. v3.1.1 adds two new `describe` blocks covering: (a) `schnorrSign` + `schnorrVerify` round-trip on a Genesis keypair (proves the canonical sign↔verify contract); (b) `schnorrSignAsync` + `schnorrVerifyAsync` round-trip identically (proves the async surface produces verifiable signatures, the main browser-INP win that justified the re-export); (c) `SchnorrSignError` class-identity assertions including `instanceof Error` and `instanceof SchnorrSignError` (proves dual-package-hazard prevention); (d) `SchnorrSignature` and `CoordAffine` type-import compile probes (proves the type-side re-exports). A third `describe` block covers the new v3.1.1 typed-validation-error classes — `InvalidBitStringError` fires when `createOuronetAccount({mode: "bitString"})` receives malformed input, plus class-identity probes for `InvalidBitmapError` and `InvalidPrivateKeyError`.
1255
+
1256
+ ### Verified
1257
+
1258
+ - `npm run typecheck` — zero errors with the new re-exports + style-aligned imports.
1259
+ - `npm test` — **565/565 tests pass** (was 558 in v3.1.0; +7 = 1 new strict locale grouping-style assertion at `tests/gas.test.ts` + 5 new it-blocks in `tests/dalos-integration.test.ts` for Schnorr round-trip, async round-trip, SchnorrSignError instanceof + SchnorrSignature/CoordAffine type-imports, and 1 v3.1.1 validation-error class probe; the existing `tests/gas.test.ts` "10,000,000" assertion was tightened from `toMatch` regex to strict `toBe` rather than counted as a new test).
1260
+ - `npm run build` — `tsc -p tsconfig.build.json` emits clean to `dist/`; the `./dalos` subpath barrel exports the new symbols in the `.d.ts`.
1261
+ - `tests/package-version.test.ts` — re-pinned to `3.1.1`.
1262
+
1263
+ ### Migration
1264
+
1265
+ Strictly additive — every change is non-breaking for any consumer:
1266
+
1267
+ 1. **Three new error class re-exports** on `./dalos` — only visible to consumers who choose to `import { InvalidBitStringError } from "@ouronet/ouronet-core/dalos"`; consumers who already `instanceof`-checked against the dalos-crypto-direct import continue to work.
1268
+ 2. **CoordAffine type re-export** — only visible to consumers who choose to `import type { CoordAffine } from "@ouronet/ouronet-core/dalos"`; otherwise invisible.
1269
+ 3. **dalos/ style alignment** — zero runtime impact; the bundler emits byte-identical output before and after.
1270
+ 4. **Locale test assertion strictness** — internal CI signal only; consumer-facing behaviour of `formatMaxFee` is unchanged from v3.1.0 (`'en-US'` was hardcoded then and remains so).
1271
+ 5. **Schnorr test coverage** — internal CI signal only; no runtime change.
1272
+
1273
+ Audit cycle 2026-05-05 closed: 5 of 5 "today's commit gaps" remediated. Remaining audit findings (security hardening, nullable widening completion, structural decomposition) are scheduled for v3.2.0 / v4.0.0 per `.bee/AUDIT-REPORT.md`'s suggested spec groupings.
1274
+
1275
+ ---
1276
+
1277
+ ## 3.1.0 — 2026-05-05
1278
+
1279
+ **MINOR, additive.** Upgrades `@ouronet/dalos-crypto` from `^1.2.0` to `^4.0.3` (covers v2.0.0–v4.0.3 of the dalos-crypto release line — Schnorr v2 wire format, Schnorr cofactor-subgroup hardening, generator-precompute matrix cache, async signing surfaces, RFC-6979-style determinism, the v4.0.0 Elliptic-package carve-out, and the v4.0.3 LOW-band closures), exposes the previously-internal Schnorr signature surface through the `./dalos` subpath, and ships a small locale-determinism fix in `./gas`. **558/558 tests pass.** No public surface from prior versions changes shape; all additions are opt-in.
1280
+
1281
+ ### Added — Schnorr signature surface re-exports on `./dalos`
1282
+
1283
+ The high-level `primitive.sign(keyPair, message)` path through the registry has always been Schnorr internally (DalosGenesis is a Schnorr-over-DALOS-Ellipse primitive), so consumers who use that path continue to work unchanged. v3.1.0 re-exports the lower-level Schnorr functions and types directly from the `./dalos` subpath for advanced consumers who need them — most notably OuronetUI's browser path, where the `*Async` variants yield to the event loop on a fixed data-independent cadence so signing keeps INP under the 200 ms budget without freezing the tab. Five values + one type are added to `@ouronet/ouronet-core/dalos`:
1284
+
1285
+ - `schnorrSign(privateKey, message, ellipse)` — synchronous sign; throws `SchnorrSignError` on internal Fiat-Shamir derivation failure (the typed throw was the v3.1.0 throw-contract finalisation in the upstream package; consumers `instanceof SchnorrSignError` to catch).
1286
+ - `schnorrVerify(signatureString, message, publicKey, ellipse)` — synchronous verify; returns `boolean`. Includes the v4.0.0 cofactor subgroup-membership checks (`[4]·R ≠ O`, `[4]·P ≠ O`) that reject order-4 small-subgroup attack signatures the pre-Phase-6 verifier accepted.
1287
+ - `schnorrSignAsync` / `schnorrVerifyAsync` — async wrappers that yield to the event loop every 8 outer-loop iterations on a fixed cadence (data-independent, constant-time). Recommended for browser consumers; the cadence is verified by the upstream package's REQ-14 yield-count constant-time test.
1288
+ - `SchnorrSignError` — typed exception class, importable for `instanceof` catch blocks.
1289
+ - `SchnorrSignature` — the canonical signature shape, useful for typing function parameters that take a parsed signature.
1290
+
1291
+ The exports are sourced from `@ouronet/dalos-crypto/gen1` (the same subpath we already use for `Bitmap` + bitmap utilities), so no new transitive dependency surface is introduced.
1292
+
1293
+ ### Changed — `@ouronet/dalos-crypto` dep range bumped `^1.2.0` → `^4.0.3`
1294
+
1295
+ Per the upstream v4.0.0 changelog, **TypeScript consumers see no breaking surface changes** between v1.2.0 and v4.0.3. The v4.0.0 major bump was driven entirely by a Go-reference reorganisation (the `Elliptic/` package carve-out into a new `keystore/` package, plus the `EllipseMethods.SchnorrVerify` parameter-order alignment) that affects only Go consumers. On the TS side, all changes across the v2.x/v3.x/v4.x line are additive (new exports, new optional fields like `BitStringValidation.reason?`) or behavior-preserving (`bigIntToBase49` is now O(n) instead of O(n²) but byte-identical for every input; `Modular` is now a structural property on the `Ellipse` interface, derived per curve at construction time, eliminating the `DALOS_FIELD` default-param footgun without changing any externally-observable behavior).
1296
+
1297
+ The full set of symbols ouronet-core imports from `@ouronet/dalos-crypto/registry` and `@ouronet/dalos-crypto/gen1` was audited at upgrade time — all 18 symbols (`KeyPair`, `PrivateKeyForms`, `FullKey`, `PrimitiveMetadata`, `CryptographicPrimitive`, `DalosGenesisPrimitive`, `isDalosGenesisPrimitive`, `DalosGenesis`, `CryptographicRegistry`, `createDefaultRegistry`, `Leto`, `Artemis`, `Apollo`, `createGen1Primitive`, `DALOS_PREFIXES`, `Gen1PrimitiveConfig`, `AddressPrefixPair`, `Bitmap` plus the bitmap utility functions) are present and shape-compatible in v4.0.3. The four local gates (`npm install`, `npm run typecheck`, `npm test`, `npm run build`) all pass after the upgrade with zero source-code changes outside the new schnorr re-exports.
1298
+
1299
+ ### Fixed — locale determinism in `formatMaxFee` (`./gas`)
1300
+
1301
+ `formatMaxFee(gasPrice, gasLimit)` in `src/gas/gasUtils.ts` previously called `totalAnu.toLocaleString()` without a locale argument, so the formatted ANU string varied with the host locale (`"10.000.000"` on a German-locale host vs `"10,000,000"` on a US-locale host). The test suite (`tests/gas.test.ts`) was already pinning the en-US shape, so the test silently failed on any non-en-US host while passing on CI (Linux + en-US default). v3.1.0 pins the call to `toLocaleString('en-US')` so consumer rendering and test assertions are deterministic across host locales. Trivial 1-line change. No public-API or runtime-behavior change for en-US consumers (the formatted string is identical); non-en-US consumers see their UI's ANU thousands-separator switch from their locale's separator (e.g., `.` on de-DE) to the en-US comma — a deliberate trade-off for cross-host parity.
1302
+
1303
+ ### Verified
1304
+
1305
+ - `npm run typecheck` — zero errors with the new schnorr re-exports + dalos-crypto v4.0.3.
1306
+ - `npm test` — **558/558 tests pass**, including `tests/dalos-integration.test.ts`, `tests/gas.test.ts` (now passing on de-DE locale post-fix), `tests/package-version.test.ts` (re-pinned to `3.1.0`), and the full crypto/guard/signing/strategy/codex regression set.
1307
+ - `npm run build` — `tsc -p tsconfig.build.json` emits clean to `dist/`; the `./dalos` subpath barrel exports the new schnorr surface in the `.d.ts`.
1308
+ - `package-lock.json` — refreshed to pin `@ouronet/dalos-crypto@4.0.3`.
1309
+
1310
+ ### Migration
1311
+
1312
+ No migration required for any existing consumer. The four cluster-of-changes are each strictly additive at the public-surface level:
1313
+
1314
+ 1. **dalos-crypto upgrade** — same imports work, same shapes returned, same algorithms produce same byte-identical outputs (Genesis 105-vector corpus byte-identity preserved at upstream extended-elided SHA-256 = `082f7a40405d4c075f1975af0a6075bb0228bbccae60a53b05b350a09ce223ae`).
1315
+ 2. **Schnorr re-exports** — only visible if a consumer chooses to `import { schnorrSignAsync } from "@ouronet/ouronet-core/dalos"`; consumers who keep using `primitive.sign(...)` see no change.
1316
+ 3. **Locale fix** — non-en-US consumers see their ANU thousands separator switch to `,` (was their locale's separator). UI inspection confirms the en-US shape is the canonical OuronetUI display. No breaking-change classification because the field is presentational; no caller logic depends on the separator character.
1317
+ 4. **Test pin update** — `tests/package-version.test.ts` updated `3.0.0` → `3.1.0` to match the new release; not consumer-visible.
1318
+
1319
+ ---
1320
+
1321
+ ## 3.0.0 — 2026-05-03
1322
+
1323
+ **BREAKING release: 15 fabricated-fallback fabrications widened from `Promise<T>` to `Promise<T | null>` plus 1 mixed-shape addition (`validateLiquidity` gains optional `error?: string` field per Q10/A10) so consumers see RPC failures instead of fabricated chain values; 14 NON-BREAKING logger-routing additions across 5 files complete the silent-catch-elimination sweep.**
1324
+
1325
+ Closes the M3 milestone of the 2026-04-30 audit cycle (lead finding F-CORE-007 HIGH plus the broader ~24-site fabrication catalog). Sixteen interactions-surface functions previously swallowed RPC failures by returning fabricated sentinel values — `1.0` for STOA price, `8` for token decimals, `0` for fees and minimum-move amounts, `"0"` for balances and supplies, `false` for LP-type checks, an empty-guard sentinel for urStoa guards, and the magic string `"N/A"` for SWP limits. Consumers had no way to distinguish "RPC failed" from "the chain returned this value." This release widens those return types so RPC failures surface as `null` (or, for `validateLiquidity`, as a populated `error?: string` field on the existing object shape) and routes their previously silent catches through the project logger established in v2.3.0.
1326
+
1327
+ The release composes four phases: Phase 1 closes the four HIGH-risk pricing functions flagged by F-CORE-007; Phase 2 sweeps the broader catalog (the four-function string-balance cluster, field-level `LPTypeInfo` widening, the urStoa trio, two audit-missed extras, and the two `"N/A"` magic-string eliminations); Phase 3 brings logger parity to fourteen remaining silent catches across five files so the silent-catch pattern is fully eliminated from the interactions surface; Phase 4 ships the release artifacts (this entry, the README migration guide, the package version bump, and the verification gate). This is the FIRST major bump since v2.0.0 (2026-05-01). All sixteen modified functions retain their NAMES and PARAMETER signatures (NFR-03) — only return types widen (15 nullable widenings + 1 structural addition). The `tests/types.test.ts` v1.7.0 type-regression lock has been updated atomically across Phases 1 + 2 to assert the new nullable signatures (NFR-02), so the public-surface contract continues to be machine-verified at every commit.
1328
+
1329
+ Consumers should read the `## Migrating to v3.x` section of `README.md` for per-function `Before:` / `After:` migration patterns. The short version: every `=== "N/A"` check becomes `=== null`; every previously-fabricated read needs an `if (result === null) showRPCErrorBanner();` branch before the value is used; `LPTypeInfo` consumers can now render the per-flag mixed UI state (Frozen=true, Sleeping=null); `validateLiquidity` consumers route a populated `error` field to the network-failure banner.
1330
+
1331
+ ### Phase 1 — Critical pricing functions (4 BREAKING)
1332
+
1333
+ The four functions flagged by F-CORE-007 (HIGH-risk) all return numeric values used directly in financial-math paths — STOA-to-USD price, token decimal counts, pool total fee, and DPTF minimum-move amounts. Each previously fabricated a plausible default on RPC failure (`1.0`, `8`, `0`, `0`) that consumers could not distinguish from a real chain answer. Each now resolves to `null` on any of three failure paths: outer catch (network exception), chain-level failure response (`status !== "success"`), and an unexpected payload that fails a `Number.isFinite()` guard. Each catch is routed through `getLogger().error("Error in <funcName>:", error)` per the v2.3.0 Phase 5 convention. The four functions are `getStoaPriceUSD` (`src/interactions/ouroFunctions.ts`), `getTokenDecimals` and `getPoolTotalFee` (`src/interactions/dexFunctions.ts`), and `getDPTFMinMove` (`src/interactions/ouroFunctions.ts`). `getDPTFMinMove` was promoted into Phase 1 because it is structurally identical to the other three pricing functions; the audit citation `dexFunctions.ts:1318` was an imprecise line reference whose actual subject is `getPoolTotalFee`.
1334
+
1335
+ ### Phase 2 — Catalog HIGH-risk + bonus extras + magic-strings (12 BREAKING)
1336
+
1337
+ The broader catalog sweep covers four clusters. **The four-function string-balance cluster** (REQ-05) — `getIgnisBalance`, `getAccountTokenSupply`, `getOuroDispoCapacity`, `getVirtualOuro` (all in `src/interactions/ouroFunctions.ts`) — moves together as a tightly-coupled cluster because partial migration would leave a UI in which some buttons silently misbehave on RPC failure while others correctly disable. All four widen `Promise<string>` to `Promise<string | null>`; both `return "0"` sites in each function become `return null`; the v2.3.0 logger routing is preserved. **The `LPTypeInfo` widening** (REQ-06, Approach A locked) widens the inner field shapes from `{ hasFrozenLP: boolean; hasSleepingLP: boolean }` to `{ hasFrozenLP: boolean | null; hasSleepingLP: boolean | null }`; the function-level return shape (`Promise<LPTypeInfo>`) is preserved so callers can render granular per-flag mixed UI state (Frozen=true, Sleeping=null). Both inner IIFEs return `null` on failure (was `false`) and route through `getLogger().error()` with distinguished contextual messages. The 3-state preservation guarantee (chain-failure-status returns `false`, catch returns `null`, success returns `true`) was added per the Phase 2 P-001 plan-review fix. **The urStoa trio** (REQ-07) — `getUrStoaBalance`, `getUrStoaGuard`, `checkCoinAccountExists` (all in `src/interactions/urStoaFunctions.ts`) — aligns with the nullable house style: `getUrStoaBalance` widens to `Promise<number | null>`; `getUrStoaGuard` widens to `Promise<UrStoaGuardResult | null>` and drops the previously-fabricated empty-guard sentinel object so consumers gain an explicit three-state flow (RPC failure / no guard yet / guard present); `checkCoinAccountExists` widens to `Promise<boolean | null>` and gains a JSDoc cross-reference to its semantically aligned sibling in `src/interactions/ouroFunctions.ts` (the rename to disambiguate names is deferred to a separate breaking change). **Two audit-missed extras** (REQ-08): `validateLiquidity` (`src/interactions/addLiquidityFunctions.ts`) gains an optional `error?: string` field on its existing `{ valid: boolean; ... }` return shape so consumers can distinguish RPC failure (populated `error`) from real validation rejection (`valid: false` with no `error`); `getMaxBuyMovieBooster` (`src/interactions/ouroFunctions.ts`) widens to `Promise<number | null>` matching Phase 1's pattern. **The two magic-string eliminations** (REQ-09): `getSWPSpawnLimit` and `getSWPInactiveLimit` (both in `src/interactions/dexFunctions.ts`) stop returning the string sentinel `"N/A"` and instead widen to `Promise<string | null>`; both gain logger routing on their previously silent catches; consumers swap their `=== "N/A"` checks for `=== null` checks, removing a class of stringly-typed sentinel from the public surface.
1338
+
1339
+ ### Phase 3 — Logger parity for remaining silent catches (14 NON-BREAKING)
1340
+
1341
+ Fourteen remaining silent catches across five files gain `getLogger().error("Error in <funcName>:", error)` routing without any type changes — these functions are already correctly modelled as nullable or as empty-collection returns; the only gap was that their catches did not visibly log the RPC failure. The fourteen functions are: in `src/interactions/dexFunctions.ts` — `getSWPPrincipals`, `getTrueFungibleLPEntry`, `getOwnedSwapPairs`, `getSwpairFromLpId`, `getSwpairsFromLpIds`, `getPrimordialPool`, `describeModule`; in `src/interactions/ouroFunctions.ts` — `getDPTFIssueInfo`, `getSublimateInfo`, `getCompressInfo`; in `src/interactions/activateFunctions.ts` — `getDeployStandardAccountInfoOnly`; in `src/interactions/infoOneFunctions.ts` — `getClearDispoInfo`; in `src/interactions/urStoaFunctions.ts` — the file-private helpers `verifyEd25519Sig` and `describeKeyset`. End-state goal: the silent-catch pattern is fully eliminated from the interactions surface; a maintainer audit grep for unrouted catch-then-return patterns in `src/interactions/*.ts` returns zero matches. The two LP-type-info inner-IIFE catches that would otherwise belong to this set are already covered by REQ-06 (Phase 2) so they are out of REQ-10 scope to avoid double-coverage.
1342
+
1343
+ ### Public API impact
1344
+
1345
+ - **Breaking change — `getStoaPriceUSD` (`@ouronet/ouronet-core/interactions/ouroFunctions`):** return type widens from `Promise<number>` to `Promise<number | null>`. Three failure paths now yield `null`: outer catch, chain-level `status !== "success"`, and a success payload that fails a `Number.isFinite()` guard (replacing the previous `Number(...) || 1.0` fabrication). The optional `skipTempWatcher` parameter is preserved unchanged.
1346
+
1347
+ Before:
1348
+ ```ts
1349
+ const price: number = await getStoaPriceUSD();
1350
+ const usd = stoaAmount * price; // silently uses 1.0 on RPC failure
1351
+ ```
1352
+
1353
+ After:
1354
+ ```ts
1355
+ const price: number | null = await getStoaPriceUSD();
1356
+ if (price === null) { showRPCErrorBanner(); return; }
1357
+ const usd = stoaAmount * price;
1358
+ ```
1359
+
1360
+ - **Breaking change — `getTokenDecimals` (`@ouronet/ouronet-core/interactions/dexFunctions`):** return type widens from `Promise<number>` to `Promise<number | null>`. All three `return 8` sites become `return null`: chain-failure branch, unexpected-data-shape branch, catch. Two defensive `Number.isFinite()` guards on `parseInt` results catch malformed `{int: "abc"}` chain data.
1361
+
1362
+ Before:
1363
+ ```ts
1364
+ const decimals: number = await getTokenDecimals(tokenId);
1365
+ const display = amount / 10 ** decimals; // silently uses 8 on RPC failure
1366
+ ```
1367
+
1368
+ After:
1369
+ ```ts
1370
+ const decimals: number | null = await getTokenDecimals(tokenId);
1371
+ if (decimals === null) { showRPCErrorBanner(); return; }
1372
+ const display = amount / 10 ** decimals;
1373
+ ```
1374
+
1375
+ - **Breaking change — `getPoolTotalFee` (`@ouronet/ouronet-core/interactions/dexFunctions`):** return type widens from `Promise<number>` to `Promise<number | null>`. Both `return 0` sites become `return null`. A `Number.isFinite()` guard wraps the `resolvePactDecimalLocal` helper return so that helper-internal `NaN` results are caught at the user-facing boundary. The helper itself is unchanged because it is not user-facing.
1376
+
1377
+ Before:
1378
+ ```ts
1379
+ const fee: number = await getPoolTotalFee(swpair);
1380
+ const cost = amount * fee; // silently uses 0 (free swap!) on RPC failure
1381
+ ```
1382
+
1383
+ After:
1384
+ ```ts
1385
+ const fee: number | null = await getPoolTotalFee(swpair);
1386
+ if (fee === null) { disableSwapButton(); showRPCErrorBanner(); return; }
1387
+ const cost = amount * fee;
1388
+ ```
1389
+
1390
+ - **Breaking change — `getDPTFMinMove` (`@ouronet/ouronet-core/interactions/ouroFunctions`):** return type widens from `Promise<number>` to `Promise<number | null>`. Both `return 0` sites become `return null`; the success-path `parseFloat(...) || 0` falsy-coalesce becomes `parseFloat(...)` followed by a `Number.isFinite()` guard.
1391
+
1392
+ Before:
1393
+ ```ts
1394
+ const minMove: number = await getDPTFMinMove(tokenId);
1395
+ if (amount < minMove) showError(); // silently uses 0 on RPC failure
1396
+ ```
1397
+
1398
+ After:
1399
+ ```ts
1400
+ const minMove: number | null = await getDPTFMinMove(tokenId);
1401
+ if (minMove === null) { showRPCErrorBanner(); return; }
1402
+ if (amount < minMove) showError();
1403
+ ```
1404
+
1405
+ - **Breaking change — string-balance cluster (`@ouronet/ouronet-core/interactions/ouroFunctions`):** four functions move together as a tightly-coupled cluster — `getIgnisBalance`, `getAccountTokenSupply`, `getOuroDispoCapacity`, `getVirtualOuro`. Each widens from `Promise<string>` to `Promise<string | null>`; both `return "0"` sites in each function become `return null`. The four move together because partial migration would create a UI in which some buttons silently misbehave on RPC failure while others correctly disable; this risk was explicitly surfaced in research and locked at requirements time.
1406
+
1407
+ Before:
1408
+ ```ts
1409
+ const balance: string = await getIgnisBalance(account);
1410
+ if (parseFloat(balance) > 0) enableButton(); // silently uses "0" on RPC failure
1411
+ ```
1412
+
1413
+ After:
1414
+ ```ts
1415
+ const balance: string | null = await getIgnisBalance(account);
1416
+ if (balance === null) { disableButton(); showRPCErrorBanner(); return; }
1417
+ if (parseFloat(balance) > 0) enableButton();
1418
+ ```
1419
+
1420
+ - **Breaking change — `LPTypeInfo` field widening (`@ouronet/ouronet-core/interactions/addLiquidityFunctions`):** the `LPTypeInfo` type's two inner fields widen from `boolean` to `boolean | null` so that "Frozen LP check failed" and "Sleeping LP check failed" can be rendered as distinct mixed states. The function-level return shape (`Promise<LPTypeInfo>`) is preserved (the function still resolves to an `LPTypeInfo` object); only the two boolean flags inside the object widen to allow `null`. Both inner failure paths return `null` instead of `false`. Approach A (field-level widening) was selected over function-level nullability and over an error-channel approach because it preserves the maximum amount of granular information for consumer UIs. The 3-state preservation guarantee per the Phase 2 P-001 fix: chain-failure-status returns `false` (real chain answer), catch returns `null` (RPC failure), success returns `true` (real chain answer).
1421
+
1422
+ Before:
1423
+ ```ts
1424
+ type LPTypeInfo = { hasFrozenLP: boolean; hasSleepingLP: boolean };
1425
+ const info = await getLPTypeInfo(account);
1426
+ if (info.hasFrozenLP) showFrozenBadge(); // silently false on RPC failure
1427
+ ```
1428
+
1429
+ After:
1430
+ ```ts
1431
+ type LPTypeInfo = { hasFrozenLP: boolean | null; hasSleepingLP: boolean | null };
1432
+ const info = await getLPTypeInfo(account);
1433
+ if (info.hasFrozenLP === null) showFrozenIndeterminateBadge();
1434
+ else if (info.hasFrozenLP) showFrozenBadge();
1435
+ if (info.hasSleepingLP === null) showSleepingIndeterminateBadge();
1436
+ else if (info.hasSleepingLP) showSleepingBadge();
1437
+ ```
1438
+
1439
+ - **Breaking change — urStoa trio (`@ouronet/ouronet-core/interactions/urStoaFunctions`):** three functions align with the nullable house style. `getUrStoaBalance` widens from `Promise<number>` to `Promise<number | null>`; both `return 0` sites become `return null`. `getUrStoaGuard` widens from `Promise<UrStoaGuardResult>` to `Promise<UrStoaGuardResult | null>`; the previously-fabricated `empty = {exists:false, isKeyset:false, keys:[], pred:""}` sentinel object is dropped in favour of `null` on failure; consumers gain an explicit three-state flow (RPC failure / no guard yet / guard present). `checkCoinAccountExists` widens from `Promise<boolean>` to `Promise<boolean | null>`; the previously silent catch now routes through the project logger; both failure paths return `null` instead of `false`. The function gains a JSDoc cross-reference to its semantically aligned sibling in the OURO functions module, explaining that the two functions share the nullable-boolean shape but differ in account scope (urStoa vs coin); the rename to disambiguate names is deferred to a separate breaking change.
1440
+
1441
+ Before:
1442
+ ```ts
1443
+ const guard: UrStoaGuardResult = await getUrStoaGuard(account);
1444
+ if (guard.exists) renderEditGuardForm(guard);
1445
+ else renderCreateGuardForm(); // silently fabricates empty guard on RPC failure
1446
+ ```
1447
+
1448
+ After:
1449
+ ```ts
1450
+ const guard: UrStoaGuardResult | null = await getUrStoaGuard(account);
1451
+ if (guard === null) { showRPCErrorBanner(); return; }
1452
+ if (!guard.exists) renderCreateGuardForm();
1453
+ else renderEditGuardForm(guard);
1454
+ ```
1455
+
1456
+ - **Breaking change — `validateLiquidity` mixed-shape addition (`@ouronet/ouronet-core/interactions/addLiquidityFunctions`):** the boolean validity flag is preserved, and an optional `error?: string` field is added that is populated only when an RPC error occurred. This is a mixed-shape addition (NOT a nullable widening): the existing `{ valid: boolean; ... }` shape gains an OPTIONAL `error?: string` field per the Q10/A10 locked decision. Consumers therefore route a populated `error` to a network-failure banner and a `valid: false` with no `error` to a validation-failure message.
1457
+
1458
+ Before:
1459
+ ```ts
1460
+ const result = await validateLiquidity(...);
1461
+ if (!result.valid) showValidationFail(); // silently catches RPC failure as valid:false
1462
+ ```
1463
+
1464
+ After:
1465
+ ```ts
1466
+ const result = await validateLiquidity(...);
1467
+ if (result.error) showRPCErrorBanner(result.error);
1468
+ else if (!result.valid) showValidationFail();
1469
+ ```
1470
+
1471
+ - **Breaking change — `getMaxBuyMovieBooster` (`@ouronet/ouronet-core/interactions/ouroFunctions`):** return type widens from `Promise<number>` to `Promise<number | null>`. Both `return 0` sites become `return null`; the success-path `|| 0` falsy-coalesce becomes a `Number.isFinite()` guard.
1472
+
1473
+ Before:
1474
+ ```ts
1475
+ const maxBuy: number = await getMaxBuyMovieBooster(account);
1476
+ if (amount > maxBuy) showLimitExceeded(); // silently uses 0 on RPC failure
1477
+ ```
1478
+
1479
+ After:
1480
+ ```ts
1481
+ const maxBuy: number | null = await getMaxBuyMovieBooster(account);
1482
+ if (maxBuy === null) { showRPCErrorBanner(); return; }
1483
+ if (amount > maxBuy) showLimitExceeded();
1484
+ ```
1485
+
1486
+ - **Breaking change — magic-string eliminations: `getSWPSpawnLimit` and `getSWPInactiveLimit` (`@ouronet/ouronet-core/interactions/dexFunctions`):** both functions widen from `Promise<string>` to `Promise<string | null>`. Both `return "N/A"` sites become `return null`. Both gain `getLogger().error()` routing on their previously silent catches. Consumers swap `=== "N/A"` checks for `=== null` checks, removing a class of stringly-typed sentinel from the public surface (Q11/A11 locked decision).
1487
+
1488
+ Before:
1489
+ ```ts
1490
+ const limit = await getSWPSpawnLimit(swpair);
1491
+ if (limit === "N/A") showUnknownLimit(); // stringly-typed sentinel
1492
+ else displayLimit(limit);
1493
+ ```
1494
+
1495
+ After:
1496
+ ```ts
1497
+ const limit = await getSWPSpawnLimit(swpair);
1498
+ if (limit === null) showRPCErrorBanner();
1499
+ else displayLimit(limit);
1500
+ ```
1501
+
1502
+ - **Non-breaking observability — fourteen logger-routing additions across five files:** the fourteen previously silent catches in `dexFunctions.ts` (`getSWPPrincipals`, `getTrueFungibleLPEntry`, `getOwnedSwapPairs`, `getSwpairFromLpId`, `getSwpairsFromLpIds`, `getPrimordialPool`, `describeModule`), `ouroFunctions.ts` (`getDPTFIssueInfo`, `getSublimateInfo`, `getCompressInfo`), `activateFunctions.ts` (`getDeployStandardAccountInfoOnly`), `infoOneFunctions.ts` (`getClearDispoInfo`), and `urStoaFunctions.ts` (file-private helpers `verifyEd25519Sig` and `describeKeyset`) gain `getLogger().error("Error in <funcName>:", error)` routing. No return-type changes — these functions are already correctly modelled as nullable or as empty-collection returns. End-state goal: the silent-catch pattern is fully eliminated from the interactions surface.
1503
+
1504
+ - **No public-API removals (NFR-03):** all sixteen modified functions retain their NAMES and PARAMETER signatures. No exports are removed. The release is breaking only in the sense of return-type widening (15 nullable widenings + 1 structural addition for `validateLiquidity`). Consumers who rebuild against v3.0.0 will see TypeScript errors at every call site that does not handle the `null` (or, for `validateLiquidity`, the optional `error` field) — this is the deliberate forcing function that surfaces every consumer call site for explicit migration.
1505
+
1506
+ ### Migration
1507
+
1508
+ See the `## Migrating to v3.x` section of `README.md` for the full per-function `Before:` / `After:` migration patterns. Short version: every consumer call site that previously consumed one of the sixteen modified functions needs an explicit RPC-failure branch. For the fifteen nullable widenings, add `if (result === null) showRPCErrorBanner();` before reading the previously-fabricated value. For `validateLiquidity`, route a populated `error` field to the network-failure banner (`if (result.error) showRPCErrorBanner(result.error); else if (!result.valid) showValidationFail();`). For the two magic-string eliminations (`getSWPSpawnLimit`, `getSWPInactiveLimit`), swap `=== "N/A"` checks for `=== null` checks. For `LPTypeInfo` consumers, render the per-flag mixed UI state (Frozen=true, Sleeping=null) per Approach A — each of the two flags is now individually nullable. Consumer-side migration code in OuronetUI and AncientHolder HUB is OUT OF SCOPE per `spec.md` — those repos handle their own update work informed by the README.
1509
+
1510
+ ### Stats
1511
+
1512
+ Files changed:
1513
+
1514
+ - NEW: `tests/interactions-pricing.test.ts`, `tests/interactions-balance-cluster.test.ts`, `tests/interactions-logger-parity.test.ts`
1515
+ - MODIFIED: `src/interactions/ouroFunctions.ts`, `src/interactions/dexFunctions.ts`, `src/interactions/addLiquidityFunctions.ts`, `src/interactions/urStoaFunctions.ts`, `src/interactions/activateFunctions.ts`, `src/interactions/infoOneFunctions.ts`, `tests/types.test.ts`, `tests/phase5-catch-routing.test.ts`, `package.json`, `CHANGELOG.md`, `README.md`
1516
+
1517
+ Test count: **558** passing (up from 500 v2.3.0 baseline; +58 new in v3.0.0).
1518
+
1519
+ ## 2.3.0 — 2026-05-02
1520
+
1521
+ **Additive medium-and-low audit closures release. MINOR, non-breaking.**
1522
+
1523
+ Closes 13 audit findings — 7 MEDIUM tier (F-CORE-013, F-CORE-014, F-CORE-015, F-CORE-016a, F-CORE-016b, F-CORE-016c, F-CORE-017) and 6 LOW tier (F-CORE-018a, F-CORE-018b, F-CORE-019, F-CORE-020, F-CORE-021, F-CORE-022) — from the 2026-04-30 audit cycle. Introduces 2 new public surfaces: (1) a typed `UnknownPredicateError` class re-exported from the `./guard` subpath, and (2) a NEW `./observability` subpath with `Logger` type + `setLogger` mutator + `getLogger` accessor mirroring the existing `setPactReader` injection-seam pattern at `src/reads/pactReader.ts`. All changes are additive — no public exports removed, no return types widened, existing `instanceof Error` checks and existing analysis-flag access continue to work; the new typed-class discrimination and logger-seam injection are purely opt-in.
1524
+
1525
+ ### Added (public surface)
1526
+
1527
+ - `UnknownPredicateError` — `@ouronet/ouronet-core/guard`. Thrown by
1528
+ `computeThreshold` when it encounters an unrecognized predicate. The
1529
+ general-purpose `analyzeGuard` catches it and folds it into a
1530
+ `predicateRecognized: false` bit on the returned analysis (replaces
1531
+ the previous silent `console.warn` diagnostic).
1532
+ - NEW `./observability` subpath — `@ouronet/ouronet-core/observability`.
1533
+ Exports `Logger` type (`{ warn(msg, ...args), error(msg, ...args) }`),
1534
+ `setLogger(logger: Logger): void` (throws `TypeError` with message
1535
+ exactly `setLogger requires a non-null Logger` on null/undefined
1536
+ input), `getLogger(): Logger` (returns the currently-configured
1537
+ logger; default routes `warn` to `console.warn` and `error` to
1538
+ `console.error`). Mirrors the existing `setPactReader` seam at
1539
+ `src/reads/pactReader.ts:33-71`.
1540
+ - Optional `firstSignableButUnsatisfied: number` field on
1541
+ `SmartAccountAuthPathsAnalysis` (`@ouronet/ouronet-core/guard`) —
1542
+ names the index of the first signable-but-unsatisfied path;
1543
+ `undefined` if none. Additive, opt-in.
1544
+
1545
+ ### Fixed
1546
+
1547
+ **M1 (MEDIUM tier — 7 findings):**
1548
+
1549
+ - **F-CORE-013 — Codex shape validation.** `deserializeCodex` at
1550
+ `src/codex/codec.ts:75-93` gains runtime shape checks after the
1551
+ version check. Validates that `kadenaWallets`, `ouronetWallets`,
1552
+ `addressBook` are arrays and `uiSettings` is an object.
1553
+ Domain-prefixed errors NAME the offending field but never echo its
1554
+ value (preserves the secrets-stay-out-of-telemetry boundary).
1555
+ Forward-compat preserved: extra unknown top-level fields survive the
1556
+ deserialize round-trip exactly as before (v1.2 envelope contract
1557
+ unchanged).
1558
+ - **F-CORE-014 — Foreign-key resolver pre-flight.**
1559
+ `CodexSigningStrategy` at `src/signing/codexStrategy.ts:180-182`
1560
+ gains a pre-flight check: when a transaction requires a foreign-key
1561
+ signer AND `this.resolver.requestForeignKey` is undefined, throws a
1562
+ precise error before reaching `universalSignTransaction`. JSDoc on
1563
+ `KeyResolver.requestForeignKey` at `src/signing/types.ts:62-69` is
1564
+ clarified to state the optional-at-the-interface-but-required-at-
1565
+ execute-time-when-needed contract. Server resolvers that omit
1566
+ `requestForeignKey` AND receive a foreign-key transaction now get a
1567
+ clear error instead of an opaque deep-stack failure.
1568
+ - **F-CORE-015 — `safeCreationTime` DRY refactor.** Removed 11
1569
+ byte-identical inline `function safeCreationTime` declarations
1570
+ across `src/interactions/*Functions.ts` (activate, addLiquidity,
1571
+ coil, crossChain, dex, guard, kpay, ouro, pension, urStoa, wrap).
1572
+ All 11 files now import the canonical declaration from
1573
+ `src/pact/format.ts:138-140` (single source of truth). Mechanical
1574
+ refactor with byte-identical behavior.
1575
+ - **F-CORE-016a — Tightened `classifyGuardKind`.** Requires the FULL
1576
+ minimal shape per kind: capability needs `cgName` + `cgArgs` +
1577
+ `cgPactId`; user needs `fun` + `args`; keyset needs `pred` +
1578
+ `keys`; keyset-ref accepts either casing of the ref field.
1579
+ Under-specified guard shapes that previously silently mis-classified
1580
+ now classify as `unknown` and are surfaced to the caller.
1581
+ - **F-CORE-016b — Keyset-reference casing normalization.** New
1582
+ `normalizeKeysetRef` helper applied at the `resolveGuard` boundary
1583
+ (`src/guard/smartAccountAuth.ts:118-126`) so internal code only sees
1584
+ the camelCase form. Maps the lowercase chain-native `keysetref` →
1585
+ camelCase `keysetRef` at the chain-IO boundary.
1586
+ - **F-CORE-016c — `SmartAccountAuthPathsAnalysis` 4 reachable states.**
1587
+ JSDoc at `src/guard/smartAccountAuth.ts:209-240` enumerates the 4
1588
+ reachable states: `firstSatisfied >= 0`; `firstSatisfied === -1 &&
1589
+ anyKeyBased === true`; `firstSatisfied === -1 && anyKeyBased ===
1590
+ false && anyKnownKind`; all-unknown. Optional
1591
+ `firstSignableButUnsatisfied: number` field added to the analysis
1592
+ surface.
1593
+ - **F-CORE-017 — `UnknownPredicateError` typed class +
1594
+ `predicateRecognized` flag.** `computeThreshold` at
1595
+ `src/guard/guardUtils.ts:76-79` throws the new typed
1596
+ `UnknownPredicateError` (additive public class re-exported from
1597
+ `./guard`). `analyzeGuard` catches the class and folds it into a
1598
+ structured `predicateRecognized: false` bit on the returned
1599
+ analysis. The previous silent `console.warn` is removed.
1600
+
1601
+ **M2 (LOW tier — 6 findings):**
1602
+
1603
+ - **F-CORE-018a — README header version table refresh.**
1604
+ `Z:/OuronetCore/README.md` header version table updated from its
1605
+ v1.3.0 / v1.4.0 baseline to current v2.2.0 reality, cross-
1606
+ referencing `CHANGELOG.md` for per-version detail.
1607
+ - **F-CORE-018b — CONTEXT.md interactions section refresh.**
1608
+ `Z:/OuronetCore/.bee/CONTEXT.md` interactions section updated to
1609
+ describe v1.4 (`AccountSelectorData` `public-key` / `sovereign` /
1610
+ `governor` fields), v1.5 (`Leto` / `Artemis` / `Apollo` re-exports
1611
+ + `createGen1Primitive` factory + `AddressPrefixPair` type), v1.6
1612
+ (Smart Ouronet Account auth-path resolution primitives +
1613
+ `buildRotateSovereignPactCode`).
1614
+ - **F-CORE-019 — Catch-block consistency in `ouroFunctions.ts`.** All
1615
+ 7 affected catch sites in `src/interactions/ouroFunctions.ts` now
1616
+ route via `getLogger().error(...)` from `../observability`.
1617
+ Convention is documented in a code comment near the affected
1618
+ handlers.
1619
+ - **F-CORE-020 — Tier semantics JSDoc.** `src/reads/pactReader.ts`
1620
+ and `src/reads/rawCalibratedRead.ts:40-46` JSDoc enumerates the
1621
+ canonical tier mapping (T1=balance, T2=preview, T3=metadata,
1622
+ T7=very-static, matching OuronetUI's reader). Documents that the
1623
+ default reader accepts and ignores the `tier` argument and
1624
+ cross-references `setPactReader` for cache-aware consumers.
1625
+ - **F-CORE-021 — Drop dead try/catch in `getLPTypeInfo`.** Outer
1626
+ try/catch wrapping `getLPTypeInfo`'s `Promise.all` was dead code
1627
+ (two never-rejecting promises cannot themselves reject) and is
1628
+ removed (Option A LOCKED — the "comment as belt-and-braces"
1629
+ alternative was explicitly rejected). Future regressions in inner
1630
+ catches surface as real test failures rather than silent masking.
1631
+ - **F-CORE-022 — Central logger seam at `./observability`.** New
1632
+ two-file source layout `src/observability/{index.ts,logger.ts}`
1633
+ exposes `Logger` / `setLogger` / `getLogger` mirroring the
1634
+ `setPactReader` pattern. Default routes to `console.warn` /
1635
+ `console.error`. `package.json` exports map gains the new
1636
+ `./observability` subpath. Every `console.warn` and `console.error`
1637
+ in `src/` is rerouted through the seam — verifiable by
1638
+ `grep -nE "console\.(warn|error)" src/` returning ZERO matches
1639
+ outside the seam itself.
1640
+
1641
+ ### Stats
1642
+
1643
+ - Files changed: NEW — `src/observability/index.ts`,
1644
+ `src/observability/logger.ts`, `tests/observability-logger.test.ts`,
1645
+ `tests/phase5-catch-routing.test.ts`. MODIFIED — `src/codex/codec.ts`,
1646
+ `src/signing/codexStrategy.ts`, `src/signing/types.ts`, 11×
1647
+ `src/interactions/*Functions.ts` (Phase 1 `safeCreationTime` DRY),
1648
+ `src/guard/smartAccountAuth.ts`, `src/guard/guardUtils.ts`,
1649
+ `src/guard/index.ts`, `src/reads/pactReader.ts`,
1650
+ `src/reads/rawCalibratedRead.ts`, `src/network/nodeFailover.ts`,
1651
+ `src/errors/transactionErrors.ts`, `src/interactions/ouroFunctions.ts`
1652
+ (Phase 5 catch-routing + Phase 6 sweep),
1653
+ `src/interactions/addLiquidityFunctions.ts` (Phase 5 dead-catch
1654
+ drop + Phase 6 sweep), `Z:/OuronetCore/README.md`,
1655
+ `Z:/OuronetCore/.bee/CONTEXT.md`, `package.json`, `CHANGELOG.md`.
1656
+ - Test count: **500** passing (up from `458` v2.2.0 baseline; +42
1657
+ new in v2.3.0).
1658
+ - No public-API removals. The 2 new public surfaces
1659
+ (`UnknownPredicateError` class on `./guard`; `Logger` type +
1660
+ `setLogger` + `getLogger` on the new `./observability` subpath) are
1661
+ additive; no existing exports change shape; no return types
1662
+ widened; no breaking changes for downstream consumers (`OuronetUI`,
1663
+ `AncientHolder HUB`).
1664
+
1665
+ ## 2.2.0 — 2026-05-02
1666
+
1667
+ **Additive crypto error taxonomy and previously-untested critical-surface coverage. MINOR, non-breaking.**
1668
+
1669
+ Closes three audit findings (F-CORE-009, F-CORE-011, F-CORE-012) by introducing three new typed error classes on the `./crypto` subpath, fixing a wrong-password timing leak in `smartDecrypt`, and landing four new test files plus five test-file extensions across previously-untested critical surfaces. A fourth audit finding (F-CORE-010 — pact-code injection escaping) was reviewed during spec discovery and intentionally rejected; rationale captured below. All public-API additions are additive — no existing exports change shape, no breaking changes for downstream consumers (`OuronetUI` and `AncientHolder HUB`).
1670
+
1671
+ ### Added (public surface)
1672
+
1673
+ - `WrongPasswordError` — `@ouronet/ouronet-core/crypto`. Thrown by
1674
+ `decryptString`, `decryptStringV2`, and `smartDecrypt` when AES-GCM
1675
+ authentication-tag verification fails. Per the AES-GCM spec, a
1676
+ correct-password-with-tampered-ciphertext attempt is
1677
+ cryptographically indistinguishable from a wrong-password attempt
1678
+ and routes to this same class.
1679
+ - `CorruptEnvelopeError` — `@ouronet/ouronet-core/crypto`. Thrown on
1680
+ envelope-parsing failures: `JSON.parse` failure, outer `atob`
1681
+ base64-decode failure, missing required envelope fields, and
1682
+ parsed-value-not-an-object. Distinguishes structural corruption from
1683
+ authentication failure.
1684
+ - `UnsupportedFormatError` — `@ouronet/ouronet-core/crypto`. Additive
1685
+ reservation for future format extensions; no current call site
1686
+ throws this. Exported now so consumers can pin a `catch` clause once
1687
+ without a future minor-bump break.
1688
+
1689
+ ### Fixed
1690
+
1691
+ - **F-CORE-009 — Crypto error taxonomy + `smartDecrypt` timing-leak
1692
+ fix.** Three typed error classes (`WrongPasswordError`,
1693
+ `CorruptEnvelopeError`, `UnsupportedFormatError`) replace the V1/V2
1694
+ catch-all string messages. `smartDecrypt` now uses a single
1695
+ deterministic shape-based branch (`isEncryptedV2`) instead of a
1696
+ try-V2-then-fallback-to-V1 sequence — this eliminates the ~1.5 s
1697
+ wall-time differential previously observable on a wrong-password V1
1698
+ envelope (which would burn the V2 PBKDF2 work-factor before falling
1699
+ through). Console-error calls in the V1 catch sites are removed; the
1700
+ thrown errors carry the underlying cause via the ES2022
1701
+ `Error.cause` property so consumers retain debug telemetry without
1702
+ the side-channel `console.error` noise.
1703
+ - **F-CORE-011 — Test coverage for previously-untested critical
1704
+ surfaces.** Four new test files: `tests/pact-reader.test.ts`
1705
+ (covers the `setPactReader` injection seam and the default
1706
+ `rawCalibratedDirtyRead` fallback), `tests/wallet-builder.test.ts`
1707
+ (covers `KadenaWalletBuilder` mnemonic dispatch across all three
1708
+ seed types, vendor-vector-pinned), `tests/transaction-errors.test.ts`
1709
+ (covers every documented branch of `createSigningError` and
1710
+ `createSimulationError`), and `tests/seed-type-migration.test.ts`
1711
+ (covers the codex seed-type migration round-trip).
1712
+ - **F-CORE-012 — Boundary-edge-case test extensions.** Five existing
1713
+ test files extended with edge-case it-blocks at the boundaries
1714
+ flagged by the audit pass.
1715
+
1716
+ ### Rejected (decisions log)
1717
+
1718
+ - **F-CORE-010 — Pact-code injection escaping.** Reviewed during spec
1719
+ discovery (2026-05-02) and intentionally NOT implemented. Rationale:
1720
+ chain-side Pact compilation/simulation already validates account
1721
+ format, token-ID format, and DALOS charset at submission time.
1722
+ Mirroring those validations in the library would (a) duplicate logic
1723
+ that already lives at the canonical enforcement boundary, (b) drift
1724
+ from chain reality whenever DALOS extends its accepted character
1725
+ set, and (c) impose ongoing maintenance burden for a
1726
+ defense-in-depth that is theoretical only — no concrete attack
1727
+ vector was identified that the chain wouldn't already reject. A
1728
+ narrower blocklist variant (escape only known-bad characters) was
1729
+ also considered and rejected on the same theoretical-only argument.
1730
+ The audit-spec source remains under `bundles/high-additive/` with a
1731
+ "rejected" note appended for traceability.
1732
+
1733
+ ### Stats
1734
+
1735
+ - Files changed: NEW — `src/crypto/errors.ts`,
1736
+ `tests/pact-reader.test.ts`, `tests/wallet-builder.test.ts`,
1737
+ `tests/transaction-errors.test.ts`,
1738
+ `tests/seed-type-migration.test.ts`, plus this changelog entry and
1739
+ `README.md` updates. MODIFIED — `src/crypto/v1.ts`, `src/crypto/v2.ts`,
1740
+ `src/crypto/index.ts`, five existing test files (boundary-edge-case
1741
+ extensions), and `package.json` (version bump).
1742
+ - Test count: **458** passing (up from `386` at v2.1.2; +72 new in v2.2.0).
1743
+ - No public-API removals. The three new error classes are additive;
1744
+ existing exports are unchanged.
1745
+ - The `smartDecrypt` timing-leak fix is the only behavioural change a
1746
+ consumer can observe — wrong-password V1 attempts now return on the
1747
+ same ~PBKDF2-V1 wall-time path as before, but the prior
1748
+ try-V2-first leak that burned the V2 work-factor is gone.
1749
+
1750
+ ## 2.1.2 — 2026-05-01
1751
+
1752
+ **Concurrency-race correction in `withFailover`. No public API change.**
1753
+
1754
+ Closes audit finding **F-BUG-001** documented in `.bee/audit-specs/high-withfailover-concurrency-race.md` (will move to `.bee/audit-specs-done/2026-05-01-high-withfailover-concurrency-race.md` post-archive per the project's audit-specs lifecycle). The bug surfaced during the v2.1.0 reliability-failover spec's final implementation review by the audit-bug-detector agent: under concurrent chain calls during a primary-node failover event, sibling `withFailover` invocations could surface spurious TIMEOUT errors even though the fallback host was healthy. v2.1.0's `getFailoverClient` adoption widened the blast radius — every chain call now routes through `withFailover`, making concurrent flows the norm. v2.1.2 is a behavior correction toward the documented "retry once on the fallback if the primary attempt errors with a network-class failure" contract; it is patch-version-eligible per strict semver.
1755
+
1756
+ ### Fixed
1757
+
1758
+ - **F-BUG-001 — `withFailover` retry guard now uses per-invocation captured base URLs.** The catch-block guard at `src/network/nodeFailover.ts:120` previously read the shared module-level `currentHost === PRIMARY_HOST`, which a sibling concurrent call could have already flipped, causing the second invocation's catch to incorrectly skip the fallback retry. The rewrite captures BOTH `attemptedBaseUrl` (current active host at fn-entry) AND `attemptedPrimaryBaseUrl` (current primary host at fn-entry) into local `const`s before invoking the wrapped function, then compares the two captured locals at catch-time. This makes the decision robust to ANY concurrent module-state mutation (sibling `withFailover` flip, mid-flight `setNodeConfig`, mid-flight `resetNodeFailover`). The retry path now calls `switchToFallback()` unconditionally — its pre-existing line-50 idempotency (`if (currentHost === FALLBACK_HOST) return;`) handles the concurrent-flip case correctly without an additional guard. The retry call uses `await fn(getActiveBaseUrl())` (with `await`) for symmetry with the initial call. New module-private helper `getPrimaryBaseUrl()` added to `src/network/nodeFailover.ts:82-85`; not exported (semver-clean).
1759
+
1760
+ ### Stats
1761
+
1762
+ - Files changed: 5 (`src/network/nodeFailover.ts`, `tests/network.test.ts`, `package.json`, `CHANGELOG.md`, `README.md`).
1763
+ - Lines added: ~30; lines removed: ~6.
1764
+ - Test count: **386** passing (up from `385` at v2.1.1; +1 from the new concurrent-failover regression test in `tests/network.test.ts`).
1765
+ - New regression test: `describe("withFailover — concurrent retry race", ...)` added to `tests/network.test.ts` (one new it-block exercising `Promise.all([withFailover(fn1), withFailover(fn2)])` during a primary-down event; pins the request-key dedup-equivalent semantic for concurrent failover).
1766
+ - No new public exports. The new `getPrimaryBaseUrl()` helper is module-private.
1767
+
1768
+ ## 2.1.1 — 2026-05-01
1769
+
1770
+ **README documentation patch. No runtime change.**
1771
+
1772
+ The v2.1.0 release published the runtime fixes (4 closed audit findings,
1773
+ new `getFailoverClient` / `runWithTimeout` / `resetNodeFailover` /
1774
+ `createTimeoutError` / `readTimeoutMs` surface, 385 tests) but the
1775
+ README's `## Status` block was still pinned to `2.0.1` and the version
1776
+ history hadn't been updated past v2.0.1. Without this patch, the npm
1777
+ package page (https://www.npmjs.com/package/@ouronet/ouronet-core)
1778
+ showed stale documentation that didn't describe the v2.1.0 reliability
1779
+ hardening work — making the new public surface invisible to consumers
1780
+ who land on the npm listing first.
1781
+
1782
+ ### Fixed
1783
+
1784
+ - **README `## Status` block** updated to lead with `2.1.0`, summarise
1785
+ the reliability hardening release, and mention the 4 closed audit
1786
+ findings (F-CORE-002 / F-CORE-003 / F-CORE-004 / F-CORE-008).
1787
+ - **Version history extended** with entries for v2.0.2, v2.0.3, v2.0.4
1788
+ (release-pipeline hardening patches that previously had no README
1789
+ mention) and v2.1.0 (full reliability hardening summary).
1790
+ - **Test count updated** from `346` to `385` in two places (the
1791
+ `## Status` paragraph and the `npm test` block under
1792
+ `## Local development`).
1793
+ - **Submodule table** rows for `./network` and `./reads` updated to
1794
+ cite the v2.1.0+ additions (`getFailoverClient`, `runWithTimeout`,
1795
+ `FailoverClientOptions`, `resetNodeFailover`, `readTimeoutMs?`).
1796
+ - **New section: "What's new in v2.1.0"** added between the
1797
+ `npm install` snippet and the `## Migrating to v2.x` block, with
1798
+ copy-paste examples covering the factory, the timeout precedence
1799
+ contract, the lower-level `runWithTimeout` helper, and a
1800
+ no-migration-required note for v2.0.x consumers.
1801
+
1802
+ ### Stats
1803
+
1804
+ - Files changed: 3 (`README.md`, `package.json`, `CHANGELOG.md`).
1805
+ - No `src/` changes; no `tests/` changes.
1806
+ - Test count unchanged at 385.
1807
+
1808
+ ## 2.1.0 — 2026-05-01
1809
+
1810
+ **Reliability hardening release. MINOR, non-breaking.**
1811
+
1812
+ Closes four HIGH-severity audit findings (F-CORE-002, F-CORE-003, F-CORE-004,
1813
+ F-CORE-008) by wiring automatic node failover, bounded timeouts with
1814
+ `TIMEOUT` classification, and Node event-loop hygiene through every chain
1815
+ RPC surface in the library. All public-API additions are additive — no
1816
+ existing exports change shape, no breaking changes for downstream consumers
1817
+ (`OuronetUI` and `AncientHolder HUB`).
1818
+
1819
+ ### Fixed
1820
+
1821
+ - **F-CORE-002 — Automatic failover wired into every submit and read.** New
1822
+ `getFailoverClient(chainId, options?)` factory in `src/network/failoverClient.ts`
1823
+ returns `{ dirtyRead, submit, listen, pollOne }` methods that compose
1824
+ `withFailover` + per-tier timeout into one reusable surface. All 81
1825
+ legacy `createClient(getPactUrl(chainId))` invocations across the 11
1826
+ interaction files (activate, addLiquidity, coil, crossChain, dex, guard,
1827
+ kpay, ouro, pension, urStoa, wrap) now route through the factory. Primary
1828
+ node failure on any chain call now triggers automatic fallback retry.
1829
+ - **F-CORE-003 — Default reader URL is now per-call, not module-init.**
1830
+ `rawCalibratedDirtyRead`'s default Pact URL now resolves from
1831
+ `getActivePactUrl(chainId)` per invocation instead of capturing the
1832
+ static `PACT_URL` constant at module load. This propagates failover
1833
+ coverage to all 16 already-`pactRead`-routed read sites without touching
1834
+ any of them. The static `PACT_URL` constant is preserved (semver) but
1835
+ marked `@deprecated` with a pointer to `getActivePactUrl(chainId)`.
1836
+ - **F-CORE-008 — Bounded timeouts on all four chain-call tiers.** New
1837
+ `runWithTimeout(operation, fn, timeoutMs)` helper applies
1838
+ `Promise.race` + `AbortController` + `try/finally clearTimeout`
1839
+ defence-in-depth. Per-tier defaults: read 15 s, submit 60 s,
1840
+ listen 180 s (~6 Kadena blocks), pollOne 30 s. Two-tier override
1841
+ precedence: per-call > factory-time > locked default. Timeouts are
1842
+ classified as `SigningError { code: "TIMEOUT" }` via the new
1843
+ `createTimeoutError(operation, timeoutMs, originalError?, additionalContext?)`
1844
+ factory. The `codexStrategy.ts` simulate-and-submit pair gets
1845
+ timeout-only enforcement (failover stays the consumer's `PactClient`
1846
+ responsibility — adding a base-URL accessor would be a breaking change).
1847
+ - **F-CORE-004 — State isolation and Node event-loop hygiene.**
1848
+ `resetNodeFailover()` exported for test isolation (returns all five
1849
+ module-level state slots to initial values). `retryTimer.unref?.()`
1850
+ attached inside `startRetryLoop()` so Node consumers no longer pin the
1851
+ event loop on the failover health-check timer. Browser consumers
1852
+ (numeric setInterval handle) unaffected via the optional-call form.
1853
+
1854
+ ### Added (public surface)
1855
+
1856
+ - `getFailoverClient(chainId, options?)` — `@ouronet/ouronet-core/network`
1857
+ - `runWithTimeout(operation, fn, timeoutMs)` — `@ouronet/ouronet-core/network`
1858
+ - `FailoverClientOptions` type — `@ouronet/ouronet-core/network`
1859
+ - `resetNodeFailover()` — `@ouronet/ouronet-core/network`
1860
+ - `createTimeoutError(operation, timeoutMs, originalError?, additionalContext?)` —
1861
+ `@ouronet/ouronet-core/errors`
1862
+ - `readTimeoutMs?: number` field — added to the `PactReader` options bag and to
1863
+ `rawCalibratedDirtyRead`'s options (default 15000 ms when omitted)
1864
+
1865
+ ### Stats
1866
+
1867
+ - Files changed: ~16 (1 new module `src/network/failoverClient.ts`,
1868
+ 11 interaction files, 4 supporting files: `nodeFailover.ts`,
1869
+ `transactionErrors.ts`, `pactReader.ts`, `rawCalibratedRead.ts`,
1870
+ `kadena.ts`, `codexStrategy.ts`).
1871
+ - New tests: 4 files. `tests/failover-client.test.ts` (18 it-blocks),
1872
+ `tests/timeouts.test.ts` (13 it-blocks), `tests/failover-submit.test.ts`
1873
+ (2 it-blocks), and `tests/network.test.ts` extended (+2 it-blocks for
1874
+ `resetNodeFailover` + `retryTimer.unref` spy). Plus `tests/strategy.test.ts`
1875
+ extended (+6 it-blocks for the codexStrategy timeout seam).
1876
+ - Test count: ~386 passing (from 346 baseline). v1.7.0 type-regression
1877
+ lock continues to fire.
1878
+ - 81 `createClient(getPactUrl(...))` call sites migrated to
1879
+ `getFailoverClient(...)` across 11 interaction files (44 invocations,
1880
+ averaging ~1.8 chain operations per createClient destructure).
1881
+
1882
+ ## 2.0.4 — 2026-05-01
1883
+
1884
+ **Triggers the v2.0.3 PAT-fallback workflow with the now-installed
1885
+ `RELEASE_TOKEN` repo secret. No runtime change.**
1886
+
1887
+ The v2.0.3 workflow change (token fallback expression
1888
+ `${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}`) was correct,
1889
+ but its workflow run still failed because the user hadn't yet added
1890
+ the `RELEASE_TOKEN` secret to the repository. The secret is now in
1891
+ place. v2.0.4 is pushed solely to trigger a fresh workflow run that
1892
+ exercises the PAT-bearing branch of the fallback expression.
1893
+
1894
+ ### Fixed
1895
+
1896
+ - **Backfill loop extended to v2.0.3.** Previous backfill ran
1897
+ `for PRIOR_TAG in v1.7.0 v2.0.0 v2.0.1 v2.0.2` — now adds v2.0.3 so
1898
+ the v2.0.4 workflow run creates Releases retroactively for ALL six
1899
+ prior tags whose Release-creation step had failed in earlier runs.
1900
+
1901
+ ### Stats
1902
+
1903
+ - Files changed: 3 (`package.json`, `CHANGELOG.md`, `.github/workflows/publish.yml`).
1904
+ - Lines added: ~15; lines removed: ~3.
1905
+ - No `src/` changes; no `tests/` changes.
1906
+ - Test count unchanged at 346.
1907
+
1908
+ ## 2.0.3 — 2026-05-01
1909
+
1910
+ **Final release-process hotfix. No runtime change.**
1911
+
1912
+ The v2.0.2 attempted fix (adding `permissions: contents: write` to
1913
+ publish.yml) failed because the StoaChain organization has the
1914
+ "Workflow permissions" setting locked at the org level, which caps
1915
+ the auto-provided `GITHUB_TOKEN` to read-only regardless of what the
1916
+ workflow YAML requests.
1917
+
1918
+ ### Fixed
1919
+
1920
+ - **`.github/workflows/publish.yml` now uses a fallback token
1921
+ expression:** `${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}`.
1922
+ When the user-supplied `RELEASE_TOKEN` PAT secret is present (with
1923
+ Contents: Read and write scope on this repo), it bypasses the
1924
+ org-level workflow-permissions cap. When the secret is not set, the
1925
+ expression falls back to the auto-provided `GITHUB_TOKEN` — which
1926
+ works in orgs that allow write at the workflow level.
1927
+ - **Backfill list extended to v2.0.2.** The v2.0.3 workflow run
1928
+ creates Releases retroactively for v1.7.0, v2.0.0, v2.0.1, and
1929
+ v2.0.2 — all the tags whose Release-creation step had previously
1930
+ failed or not yet existed.
1931
+
1932
+ ### Stats
1933
+
1934
+ - Files changed: 3 (`package.json`, `CHANGELOG.md`, `.github/workflows/publish.yml`).
1935
+ - Lines added: ~25; lines removed: ~3.
1936
+ - No `src/` changes; no `tests/` changes.
1937
+ - Test count unchanged at 346.
1938
+
1939
+ ## 2.0.2 — 2026-05-01
1940
+
1941
+ **Permissions hotfix for the v2.0.1 release-process patch. No runtime change.**
1942
+
1943
+ The v2.0.1 publish-workflow run published the package to npm
1944
+ successfully but failed at the new "Create GitHub Release for the
1945
+ pushed tag" step with HTTP 403. Root cause: the default
1946
+ `GITHUB_TOKEN` ships with read-only `contents` scope, and
1947
+ `gh release create` requires `contents: write`. Because the failure
1948
+ happened mid-run, the subsequent backfill step was skipped, so v1.7.0
1949
+ and v2.0.0 Releases also remained un-created.
1950
+
1951
+ ### Fixed
1952
+
1953
+ - **`.github/workflows/publish.yml` now declares `permissions:
1954
+ contents: write`** at the job level. This grants the auto-provided
1955
+ `GITHUB_TOKEN` the minimum scope needed for `gh release create` to
1956
+ succeed. Verified via the failed v2.0.1 workflow-run job log.
1957
+ - **Backfill list extended to include v2.0.1** alongside v1.7.0 and
1958
+ v2.0.0. The v2.0.2 workflow run will create Releases retroactively
1959
+ for all three previously-shipped tags whose Release-creation step
1960
+ was not yet possible. Idempotent — becomes a no-op once complete.
1961
+
1962
+ ### Stats
1963
+
1964
+ - Files changed: 3 (`package.json`, `CHANGELOG.md`,
1965
+ `.github/workflows/publish.yml`).
1966
+ - Lines added: ~30; lines removed: ~3.
1967
+ - No `src/` changes; no `tests/` changes.
1968
+ - Test count unchanged at 346.
1969
+
1970
+ ## 2.0.1 — 2026-05-01
1971
+
1972
+ **Documentation + release-process patch. No runtime behaviour change.**
1973
+
1974
+ The v2.0.0 release shipped with two documentation-discoverability gaps
1975
+ that this patch closes. Consumers on v2.0.0 do NOT need to upgrade for
1976
+ any code reason; v2.0.1 is functionally identical except for the
1977
+ shipping artefacts.
1978
+
1979
+ ### Fixed
1980
+
1981
+ - **`CHANGELOG.md` is now bundled with the npm tarball.** v2.0.0 had
1982
+ `package.json:files: ["dist"]` which excluded the changelog.
1983
+ Consumers running `npm view @ouronet/ouronet-core@2.0.0` saw only
1984
+ the README and had to visit the GitHub repo for migration guidance.
1985
+ v2.0.1 ships `CHANGELOG.md` alongside `dist/` so the migration
1986
+ guidance is reachable from the registry.
1987
+ - **GitHub Releases now created automatically on every `v*` tag
1988
+ push.** Previously `publish.yml` only published to npm; the
1989
+ Releases page on GitHub stayed empty. The new workflow step uses
1990
+ the `gh` CLI (pre-installed on `ubuntu-latest` runners) with the
1991
+ auto-provided `GITHUB_TOKEN` to call `gh release create
1992
+ --notes-from-tag` on every push. Idempotent — safe to re-run.
1993
+ - **Backfill for v1.7.0 and v2.0.0 GitHub Releases.** This v2.0.1
1994
+ workflow run also creates Releases retroactively for the two
1995
+ previously-shipped tags using their existing annotated-tag
1996
+ messages. After this run, `https://github.com/StoaChain/OuronetCore/releases`
1997
+ shows all three versions; the backfill step becomes a no-op on
1998
+ subsequent runs.
1999
+
2000
+ ### Changed
2001
+
2002
+ - **`README.md` refreshed** to reflect v2.0.x state: status section
2003
+ now shows v2.0.1, v1.7.0/v2.0.0/v2.0.1 entries added; new
2004
+ "Migrating to v2.x" section documents the `BalanceResolver` wiring
2005
+ pattern and the `simulateTransaction(pactCode, chainId)` signature
2006
+ change with before/after code samples; test count updated from 295
2007
+ → 346; interactions submodule description flags the v2.0.0 signature
2008
+ change.
2009
+
2010
+ ### Internal
2011
+
2012
+ - `package.json:files` extended from `["dist"]` to `["dist", "CHANGELOG.md"]`.
2013
+ - `.github/workflows/publish.yml` gains two steps after `npm publish`:
2014
+ one to create a GitHub Release for the pushed tag, one to backfill
2015
+ Releases for `v1.7.0` and `v2.0.0` (idempotent — guarded by
2016
+ `gh release view`).
2017
+
2018
+ ### Stats
2019
+
2020
+ - Files changed: 4 (`package.json`, `CHANGELOG.md`, `README.md`,
2021
+ `.github/workflows/publish.yml`).
2022
+ - Lines added: ~120; lines removed: ~50.
2023
+ - No `src/` changes; no `tests/` changes.
2024
+ - Test count unchanged at 346.
2025
+
2026
+ ## 2.0.0 — 2026-05-01
2027
+
2028
+ **Cut the `wallet -> interactions` import edge with a `BalanceResolver` seam, and adopt the `pactRead` injection seam across all sixteen pure-read sites in `interactions/*`.**
2029
+
2030
+ Two architectural-layering passes ship together. The first (Phase 1)
2031
+ closes the wallet-subpath edge by injecting a per-instance
2032
+ `BalanceResolver` seam onto `KadenaWallet`. The second (Phase 2) sweeps
2033
+ the remaining direct `createClient(...).local(...)` and
2034
+ `Pact.builder...createTransaction()`-then-`.dirtyRead()` call sites in
2035
+ `src/interactions/*` onto the existing `pactRead` injection point, so
2036
+ every internal pure read now honours the consumer-configured reader
2037
+ (cache-aware in OuronetUI, raw in HUB) and the dynamic node-failover
2038
+ machinery uniformly.
2039
+
2040
+ ### Phase 1 — Wallet edge cut
2041
+
2042
+ Closes the wallet-subpath half of the architectural-layering audit pass:
2043
+ the runtime `KadenaWallet` account class previously reached into
2044
+ `@ouronet/ouronet-core/interactions/kadenaFunctions` to fetch on-chain
2045
+ balances, which meant any consumer importing
2046
+ `@ouronet/ouronet-core/wallet` transitively pulled in the entire
2047
+ `interactions/*` tree (and through it, `@kadena/client`, the Pact builders,
2048
+ and the failover machinery). For browser SPAs that only wanted the HD
2049
+ keypair builder this was dead weight; for server consumers wiring their
2050
+ own indexer it was a forced dependency on the Pact-client read path they
2051
+ intended to bypass.
2052
+
2053
+ Post-edit: `KadenaWallet.getBalance()` delegates to a new instance-level
2054
+ `balanceResolver: BalanceResolver` seam (`(address: string) => Promise<string>`).
2055
+ The wallet file no longer imports from `interactions/*`; importing the
2056
+ wallet subpath no longer transitively pulls in `@kadena/client`. Each
2057
+ consumer wires its own resolver. Note: `interactions/kadenaFunctions.getBalance`
2058
+ returns `Promise<BalanceItem>` (`{ account, balance }`), so consumers wrap it
2059
+ in a one-line adapter to match the `BalanceResolver` contract:
2060
+
2061
+ ```ts
2062
+ import KadenaWallet from "@ouronet/ouronet-core/wallet";
2063
+ import { getBalance } from "@ouronet/ouronet-core/interactions/kadenaFunctions";
2064
+
2065
+ const wallet = new KadenaWallet({
2066
+ ...,
2067
+ balanceResolver: (address) => getBalance(address).then((r) => r.balance ?? "0"),
2068
+ });
2069
+ ```
2070
+
2071
+ A server consumer (HUB) plugs in its own indexer-backed reader; tests plug
2072
+ in an in-memory stub. The wrapping adapter is the same pattern shown in
2073
+ the `BalanceResolver` JSDoc at `src/wallet/types.ts`.
2074
+
2075
+ The seam mirrors the existing `setPactReader` / `KeyResolver` patterns
2076
+ documented in `CLAUDE.md` under "Pluggable seams, not DI" — narrow,
2077
+ function-shaped, opt-in. The `BalanceResolver` alias is published from
2078
+ `src/wallet/types.ts` and is reachable as
2079
+ `import type { BalanceResolver } from "@ouronet/ouronet-core/wallet"`.
2080
+
2081
+ `KadenaWalletBuilder` is unaffected: its five static methods all return
2082
+ keypair tuples or primitives and never instantiate a `KadenaWallet`, so
2083
+ there is no construction path through the builder that needs to forward
2084
+ the resolver. REQ-06 (builder propagation) is satisfied vacuously with
2085
+ grep evidence in the phase TASKS.md.
2086
+
2087
+ ### Phase 2 — Reader seam adoption across `interactions/*`
2088
+
2089
+ Every pure-read call inside `src/interactions/*` now flows through the
2090
+ `pactRead(pactCode, { tier, pactUrl?, chainId? })` injection seam (set
2091
+ once at boot via `setPactReader(fn)`). Sixteen call sites across four
2092
+ files were migrated:
2093
+
2094
+ - `kadenaFunctions.ts` — `getBalance` (T1) and `accountDescription` (T5).
2095
+ - `wrapFunctions.ts` — `getWrapStoaInfo` (T2), `getWrapperPaymentKey` (T5),
2096
+ `getPaymentKeyBalance` (T1), `getWrapUrStoaInfo` (T2).
2097
+ - `addLiquidityFunctions.ts` — nine reads in the URC_LD / UEV_Liquidity /
2098
+ URC_BalancedLiquidity / URC_SortLiquidity / UR_IzFrozenLP /
2099
+ UR_IzSleepingLP family (T2 / T5 / T7 per call).
2100
+ - `crossChainFunctions.ts` — `simulateTransaction` reshaped to the
2101
+ read signature (`pactCode`, `chainId`) so it routes through the same
2102
+ reader as the rest. **Public-API break** — see "Breaking change" below.
2103
+
2104
+ Each site preserves its existing response-unwrap branching (the
2105
+ `{ result: { status, data } }` two-level envelope) and its existing
2106
+ return shape, so behavioural equivalence to the prior
2107
+ `createClient(...).local(...)` path holds for callers that don't rely
2108
+ on cache semantics. Consumers that DO want caching gain it transparently
2109
+ once they call `setPactReader(...)` at boot — that path was already
2110
+ configured in OuronetUI and HUB, but until this release only a subset of
2111
+ internal reads honoured it.
2112
+
2113
+ Submit / listen / poll / cross-chain SPV continuation paths (the
2114
+ non-read paths inside `crossChainFunctions.ts`,
2115
+ `activateFunctions.ts`, `coilFunctions.ts`, etc.) are
2116
+ **unchanged** — they still build through `Pact.builder` and submit via
2117
+ `createClient(getPactUrl(chainId)).submit(...)` because they need the
2118
+ full transaction-descriptor return shape, not a dirty-read result.
2119
+
2120
+ A new `tests/interactions-read-seam.test.ts` regression guard
2121
+ exercises every migrated function against a counting stub installed via
2122
+ `setPactReader(...)` and asserts both the call count and the locked
2123
+ `tier` value per site, so future drift back to direct
2124
+ `createClient(...).local(...)` is caught at test time.
2125
+
2126
+ ### Public API impact
2127
+
2128
+ - **Constructor signature widened (non-breaking):** `KadenaWallet`'s
2129
+ options object now accepts an optional `balanceResolver?: BalanceResolver`
2130
+ field appended after `derivationPath`. Existing callers that omit it
2131
+ compile and run unchanged — the field defaults to a throwing stub that
2132
+ fires only when `getBalance()` is invoked, so wallets used for
2133
+ address-only flows stay zero-config.
2134
+ - **New instance property:** `wallet.balanceResolver` is publicly
2135
+ mutable. Consumers can either inject via the constructor or assign
2136
+ post-construction (`wallet.balanceResolver = fn`) before calling
2137
+ `getBalance()`. Both paths are equivalent.
2138
+ - **New exported type:** `BalanceResolver` from
2139
+ `@ouronet/ouronet-core/wallet` —
2140
+ `(address: string) => Promise<string>`. JSDoc documents the `"0"`
2141
+ sentinel for absent accounts and the narrow-seam framing.
2142
+ - **Edge cut:** importing `@ouronet/ouronet-core/wallet` no longer
2143
+ transitively pulls in `@kadena/client`, the `interactions/*` tree, or
2144
+ the Pact-client failover machinery. Bundle-size win for browser
2145
+ consumers that only use `KadenaWalletBuilder`.
2146
+ - **Default resolver remediation:** if a consumer calls `getBalance()`
2147
+ on a wallet constructed without a resolver, the call rejects with
2148
+ `"KadenaWallet: balanceResolver not configured. Inject one via the
2149
+ constructor or set wallet.balanceResolver before calling getBalance()."`
2150
+ Remediation: either pass `balanceResolver` to the `KadenaWallet`
2151
+ constructor, or set `wallet.balanceResolver = fn` before calling
2152
+ `wallet.getBalance()`. The browser consumer (OuronetUI) wraps
2153
+ `interactions/kadenaFunctions.getBalance` in a one-line adapter
2154
+ (`(addr) => getBalance(addr).then(r => r.balance ?? "0")`) because
2155
+ the interactions function returns the wrapped `BalanceItem` shape
2156
+ `{ account, balance }` while `BalanceResolver` expects a bare
2157
+ decimal string. A server consumer (HUB) wires its own indexer-backed
2158
+ resolver. Mirrors the existing `setPactReader` consumer-wiring
2159
+ guidance in `src/reads/pactReader.ts`.
2160
+ - **Breaking change — `simulateTransaction` signature
2161
+ (`@ouronet/ouronet-core/interactions/crossChainFunctions`):** the
2162
+ first parameter changed from a pre-built
2163
+ `IUnsignedCommand` / transaction object to the raw Pact code string
2164
+ the simulation should evaluate. The full new signature is
2165
+ `simulateTransaction(pactCode: string, chainId: string)`; the return
2166
+ shape (`{ success: boolean; result?: any; error?: string; gas?: number }`)
2167
+ is unchanged. Migration: callers that previously did
2168
+ `const tx = Pact.builder.execution(code).…createTransaction();
2169
+ await simulateTransaction(tx, chainId);` should now pass `code`
2170
+ directly — `await simulateTransaction(code, chainId);` — and drop
2171
+ the local `Pact.builder` plumbing. This routes the simulation through
2172
+ the same `pactRead` seam every other read uses, so the consumer-
2173
+ configured reader (e.g. OuronetUI's cache-aware wrapper) governs it
2174
+ uniformly.
2175
+ - **Reader seam now adopted (no consumer-facing change for already-
2176
+ configured readers):** sixteen pure-read sites in `interactions/*`
2177
+ that previously bypassed `setPactReader(...)` by calling
2178
+ `createClient(...).local(...)` directly now flow through `pactRead`.
2179
+ Consumers that were already calling `setPactReader(...)` at boot
2180
+ (OuronetUI does; HUB leaves the default) see no API change — the
2181
+ seam was already there; this just makes every internal read honour
2182
+ it. Consumers that were NOT calling `setPactReader(...)` continue to
2183
+ hit the default `rawCalibratedDirtyRead`, identical pre-state behaviour.
2184
+
2185
+ ### Behavioural impact (mildly breaking)
2186
+
2187
+ - **Removed silent `?? "0"` fallback in `getBalance()`.** The previous
2188
+ body was:
2189
+
2190
+ ```ts
2191
+ const balance = await getBalance(this.address);
2192
+ this.balance = balance.balance ?? "0";
2193
+ return this.balance;
2194
+ ```
2195
+
2196
+ The `?? "0"` swallowed any case where the upstream returned an
2197
+ envelope without a `balance` field, fabricating a "0" balance that
2198
+ could not be distinguished from a genuinely-zero on-chain account.
2199
+ The new body assigns the resolver's raw return value and propagates
2200
+ any error verbatim:
2201
+
2202
+ ```ts
2203
+ this.balance = await this.balanceResolver(this.address);
2204
+ return this.balance;
2205
+ ```
2206
+
2207
+ Consumers that today rely on `getBalance()` always returning a string
2208
+ (never throwing) must either (a) wrap their call sites in `try/catch`,
2209
+ or (b) supply a resolver whose own error path returns `"0"` to
2210
+ preserve the old behaviour. The `BalanceResolver` JSDoc still
2211
+ documents `"0"` as the stable sentinel for "absent on chain" — the
2212
+ contract for absent accounts is unchanged; only the silent-on-error
2213
+ swallow is gone.
2214
+
2215
+ ### Changed
2216
+
2217
+ - `src/wallet/KadenaWallet.ts` — removed the `import { getBalance }
2218
+ from "../interactions/kadenaFunctions"` edge; added
2219
+ `import type { BalanceResolver } from "./types"`; added
2220
+ `public balanceResolver: BalanceResolver` instance field; constructor
2221
+ options-object widened with optional `balanceResolver?` argument;
2222
+ `getBalance()` body delegates to `this.balanceResolver(this.address)`
2223
+ with no `?? "0"` swallow; class JSDoc and property JSDoc updated to
2224
+ describe the new injection-seam reality and the last-write-wins
2225
+ race semantics.
2226
+ - `src/interactions/kadenaFunctions.ts` — `getBalance` (T1) and
2227
+ `accountDescription` (T5) routed through `pactRead`. Direct
2228
+ `createClient(getPactUrl(...))` + `Pact.builder` plumbing removed;
2229
+ `import { pactRead } from "../reads"` added; unused
2230
+ `Pact` / `createClient` / `getPactUrl` / `KADENA_NETWORK` /
2231
+ `KADENA_CHAIN_ID` imports pruned (where no remaining call sites
2232
+ reference them). The `export interface BalanceItem` declaration is
2233
+ preserved verbatim — it's a public-API contract for downstream
2234
+ consumers.
2235
+ - `src/interactions/wrapFunctions.ts` — four reads migrated:
2236
+ `getWrapStoaInfo` (T2), `getWrapperPaymentKey` (T5),
2237
+ `getPaymentKeyBalance` (T1), `getWrapUrStoaInfo` (T2). The eight
2238
+ submit/listen/poll wrap helpers
2239
+ (`executeFirestarter`/`executeWrap*`/etc.) keep their existing
2240
+ `Pact.builder` + `createClient(...).submit(...)` paths — those need
2241
+ the full transaction-descriptor return shape and are not reads.
2242
+ - `src/interactions/addLiquidityFunctions.ts` — nine reads migrated
2243
+ across the URC_LD / UEV_Liquidity / URC_BalancedLiquidity /
2244
+ URC_SortLiquidity / UR_IzFrozenLP / UR_IzSleepingLP families at
2245
+ locked tiers (T2 / T5 / T7 per call). The IIFE wrapper shape used
2246
+ for the URC_0027-style batched selector is preserved; the submit /
2247
+ listen / poll paths in the same file are untouched.
2248
+ - `src/interactions/crossChainFunctions.ts` — `simulateTransaction`
2249
+ reshaped from `(transaction, chainId)` to `(pactCode, chainId)` and
2250
+ routed through `pactRead`. **Public-API break — see "Breaking
2251
+ change" above.** `getBalanceOnChain` (already on `pactRead` since
2252
+ v1.6.1) is unchanged. The cross-chain transfer build / submit / SPV /
2253
+ finish paths are untouched.
2254
+
2255
+ ### Added
2256
+
2257
+ - `src/wallet/types.ts` — new `BalanceResolver` type alias with full
2258
+ JSDoc (the `"0"` sentinel, the narrow-seam framing, the
2259
+ `wallet.balanceResolver = fn` wiring example).
2260
+ - `tests/wallet.test.ts` — 8 behavioural tests covering: default-throw
2261
+ on call (exact error string), constructor-injected resolver delegation,
2262
+ post-construction assignment, async rejection propagation, sync throw
2263
+ propagation, plus construction-sanity checks.
2264
+ - `tests/interactions-read-seam.test.ts` — behavioural regression guard
2265
+ with one it-block per migrated function. Each test installs a
2266
+ counting `pactRead` stub via `setPactReader(...)`, invokes the real
2267
+ exported function, and asserts both the call count (the stub fired
2268
+ exactly once per call site) and the `tier` value the stub received
2269
+ (so future drift back to direct `createClient(...).local(...)` or to
2270
+ a wrong tier is caught at test time). Total: ~15 it-blocks across
2271
+ the four migrated files. Combined with the existing
2272
+ `tests/wallet.test.ts` coverage, this is the new safety net the
2273
+ reader-seam adoption rests on.
2274
+
2275
+ ### Unchanged
2276
+
2277
+ - `src/interactions/kadenaFunctions.ts` — `getBalance` (and
2278
+ `accountDescription`) still exported. The per-file subpath
2279
+ `@ouronet/ouronet-core/interactions/kadenaFunctions` continues to
2280
+ resolve. Only the wallet-side import is gone; downstream consumers
2281
+ that imported `getBalance` directly from interactions are unaffected.
2282
+ - `KadenaWalletBuilder` — no API changes. The class's five static
2283
+ methods return keypair tuples or primitives; none constructs a
2284
+ `KadenaWallet` instance, so there is no propagation path to add.
2285
+ - `package.json` `exports` map — unchanged.
2286
+ - All submit / listen / poll / SPV / finish paths in
2287
+ `interactions/*` — unchanged. Only pure reads were touched;
2288
+ transaction-submitting helpers continue to build through
2289
+ `Pact.builder` and submit via
2290
+ `createClient(getPactUrl(chainId)).submit(...)` because they need
2291
+ the full `ITransactionDescriptor` return shape, not a dirty-read
2292
+ result.
2293
+
2294
+ ### Process notes
2295
+
2296
+ This release was produced via the BeeDev workflow. Phase 1 of the
2297
+ `arch-layering-and-seams` spec (REQ-01 through REQ-06) addresses the
2298
+ wallet edge cut. Phase 2 (REQ-07 through REQ-17) adopts the existing
2299
+ `pactRead` seam across the remaining sixteen pure-read sites in
2300
+ `interactions/*` and reshapes `simulateTransaction`'s signature. The
2301
+ `CLAUDE.md` "Pluggable seams" header is updated in this release from
2302
+ "Two narrow injection points" to "Three narrow injection points" — the
2303
+ new third bullet (`BalanceResolver`) was added additively in Phase 1;
2304
+ the count text was deferred to Phase 2 (this entry) so it would land
2305
+ together with the full adoption proof. The version number is left as
2306
+ `Unreleased — TBD` in this entry; the release coordinator fills it in
2307
+ at tag time per the `Publishing flow` ceremony, picking the
2308
+ appropriate semver bump (Phase 2's `simulateTransaction` signature
2309
+ change is breaking, so a major bump is the expected outcome unless
2310
+ the coordinator rules it out).
2311
+
2312
+ ### Release ceremony — pre-tag verification
2313
+
2314
+ Before tagging this release, the release coordinator MUST re-run the
2315
+ import-graph regression grep to confirm Phase 1's wallet edge cut still
2316
+ holds in the freshly-built `dist/`. The check is:
2317
+
2318
+ ```sh
2319
+ npm run build
2320
+ grep -nE "(from|require\()\s*['\"][^'\"]*interactions" dist/wallet/*.js
2321
+ # (or, with ripgrep)
2322
+ rg -E "(from|require\()\s*['\"][^'\"]*interactions" dist/wallet/
2323
+ ```
2324
+
2325
+ Expected output: **zero hits** (grep exits 1, rg exits with no
2326
+ results). The narrow regex matches only `from "..."` and
2327
+ `require("...")` import-graph references — a substring grep of
2328
+ "interactions" would produce a false positive on the JSDoc text in
2329
+ `BalanceResolver` (TypeScript preserves JSDoc by default in
2330
+ `tsconfig.build.json`). Any hit means the wallet subpath has
2331
+ re-acquired a transitive dependency on `interactions/*` and the tag
2332
+ should NOT proceed until the import is restored to a seam injection.
2333
+
2334
+ ## 1.7.0 — 2026-04-30
2335
+
2336
+ **Consolidate `IKadenaKeypair` to a single canonical declaration.**
2337
+
2338
+ Closes audit finding **F-CORE-001 (CRITICAL)** from the v1.6.1 audit pass.
2339
+ The signing-ready keypair shape was declared SIX times across the
2340
+ codebase: once canonically in `src/signing/types.ts:22-30`, once with
2341
+ documented Phase-2b backwards-compat in `src/interactions/ouroFunctions.ts`,
2342
+ and four undocumented duplicates (in `activateFunctions`, `dexFunctions`,
2343
+ `kpayFunctions`, `coilFunctions` — the last one non-exported). The
2344
+ duplicates were not byte-identical: each omitted `"foreign"` from the
2345
+ `seedType` literal-union and used `encryptedSecretKey?: any` instead of
2346
+ `unknown`.
2347
+
2348
+ Post-consolidation: only TWO declarations remain. The four undocumented
2349
+ duplicates are deleted and re-routed to the canonical via type-only
2350
+ imports through the `../signing` barrel; each subpath also gets an
2351
+ `export type { IKadenaKeypair } from "../signing"` re-export to preserve
2352
+ its public API surface (consumers that imported `IKadenaKeypair` from
2353
+ those subpaths still resolve cleanly). The Phase-2b copy in
2354
+ `ouroFunctions.ts` stays in place but gains an `@deprecated` JSDoc tag.
2355
+
2356
+ The `IKadenaKeypair` half of the F-INT-001 circular dependency between
2357
+ `addLiquidityFunctions` and `dexFunctions` is broken as a side effect:
2358
+ `addLiquidityFunctions.ts:10` is split — `IOuroAccountKeypair` keeps its
2359
+ value-position import from `./dexFunctions` (deferred consolidation),
2360
+ `IKadenaKeypair` moves to a type-only import from `../signing`.
2361
+
2362
+ A new `tests/types.test.ts` regression-lock test asserts cross-subpath
2363
+ assignability of `IKadenaKeypair` via `Parameters<typeof fn>` slots
2364
+ against real exported functions in each subpath. Combined with a new
2365
+ `tsconfig.tests.json` and `vitest.config.ts` `typecheck.tsconfig`
2366
+ pointer, the lock fires under `npm test`: any future change that
2367
+ reintroduces a drifted local `IKadenaKeypair` (omitting `"foreign"`)
2368
+ breaks CI.
2369
+
2370
+ ### Public API impact
2371
+
2372
+ - **Type widening (intentional):** the `IKadenaKeypair` resolved through
2373
+ `@ouronet/ouronet-core/interactions/{activate,dex,kpay,coil}Functions`
2374
+ now includes `seedType: "foreign"` in its literal-union (it didn't
2375
+ before — those subpaths' duplicates had a narrower `seedType`).
2376
+ - **Type tightening (intentional, mildly breaking):** the resolved type
2377
+ now uses `encryptedSecretKey?: unknown` instead of `?: any`. Consumer
2378
+ code that did `kp.encryptedSecretKey.someField` (relying on `any`'s
2379
+ permissive structural access) needs a narrowing cast. The canonical
2380
+ declaration in `src/signing/types.ts` already used `unknown` since
2381
+ earlier versions; consumers importing from there are unaffected.
2382
+ - **No runtime behaviour change.** All edits are type-position only.
2383
+ - **Deprecated copy preserved.** `src/interactions/ouroFunctions.ts:816`
2384
+ retains its `IKadenaKeypair` declaration with `encryptedSecretKey?: any`
2385
+ for root-barrel consumers. The new `@deprecated` JSDoc surfaces in IDEs
2386
+ as strikethrough on import sites.
2387
+
2388
+ ### Changed
2389
+
2390
+ - `src/interactions/activateFunctions.ts` — duplicate deleted; canonical
2391
+ imported and re-exported via `export type`
2392
+ - `src/interactions/dexFunctions.ts` — same
2393
+ - `src/interactions/kpayFunctions.ts` — same
2394
+ - `src/interactions/coilFunctions.ts` — same; sibling non-exported
2395
+ `IOuroAccountKeypair` duplicate also deleted (routed to
2396
+ `./ouroFunctions`); imports reordered into a single contiguous block
2397
+ - `src/interactions/addLiquidityFunctions.ts` — line 10's combined import
2398
+ split (F-INT-001 cycle break for `IKadenaKeypair`)
2399
+ - `src/interactions/guardFunctions.ts:13` — value-position import from
2400
+ `./ouroFunctions` switched to type-only from `../signing`
2401
+ - `src/interactions/wrapFunctions.ts:18` — same
2402
+ - `src/interactions/ouroFunctions.ts:812-818` — `@deprecated` JSDoc added;
2403
+ declaration body byte-equivalent (preserves `any` for backwards-compat)
2404
+ - `vitest.config.ts` — added `test.typecheck = { enabled: true,
2405
+ tsconfig: "tsconfig.tests.json", include: ["tests/types.test.ts"] }`
2406
+
2407
+ ### Added
2408
+
2409
+ - `tests/types.test.ts` — type-level regression lock with 5 assignability
2410
+ assertion sites (1 canonical via direct type import + 4
2411
+ `Parameters<typeof fn>[N]` extractions covering both struct-nested and
2412
+ direct-positional shapes)
2413
+ - `tsconfig.tests.json` — narrow tsconfig that includes only
2414
+ `tests/types.test.ts` alongside `src/**/*.ts`, used exclusively by
2415
+ vitest's typecheck pass to make the regression lock fire under
2416
+ `npm test`
2417
+
2418
+ ### Process notes
2419
+
2420
+ This release was produced via the BeeDev workflow (`/bee:init` →
2421
+ `/bee:audit` → `/bee:audit-to-spec` → `/bee:new-spec` → `/bee:plan-all` →
2422
+ `/bee:ship`). The audit pass produced 32 confirmed findings; this
2423
+ release closes one of them (the CRITICAL). Specs for the remaining 9
2424
+ are stored in `.bee/audit-specs/` for future releases. The plan went
2425
+ through 3 rounds of plan-review per phase plus 3 rounds of cross-plan
2426
+ consistency review, surfacing several real planning errors before
2427
+ implementation began (see `.bee/STATE.md` Decisions Log for the full
2428
+ audit trail). Final implementation review caught a public-API
2429
+ regression (deletion of `export interface` without re-export) and a
2430
+ critical regression-lock failure (vitest 4.1.5's typecheck mode does
2431
+ not auto-add test files to its tsc program); both were auto-fixed.
2432
+
2433
+ ## 1.6.1 — 2026-04-27
2434
+
2435
+ **Fix: every internal `interactions/*` helper now honors the active node.**
2436
+
2437
+ `PACT_URL` was a static module-level constant frozen at import time —
2438
+ `https://node2.stoachain.com/.../pact`. Code inside `interactions/*.ts`
2439
+ called `createClient(PACT_URL)` directly, which created Pact clients
2440
+ pinned to node2 forever, completely bypassing the failover machinery
2441
+ in `network/nodeFailover` (which `withFailover` and `pactRead` honor
2442
+ correctly). Symptom: when a consumer flipped the primary to node1
2443
+ via `setNodeConfig("node1")`, batched read/write helpers in core's
2444
+ interactions still hit node2 and timed out / returned stale data.
2445
+
2446
+ 55 occurrences across 11 files swapped from `createClient(PACT_URL)`
2447
+ to `createClient(getPactUrl(KADENA_CHAIN_ID))`. The `getPactUrl`
2448
+ helper (which itself wraps `getActivePactUrl` from nodeFailover) was
2449
+ already exported from `constants/kadena.ts` since v1.5.0 — this
2450
+ release just makes everything inside core actually use it.
2451
+
2452
+ ### Changed
2453
+
2454
+ - `src/interactions/activateFunctions.ts` — 1 site
2455
+ - `src/interactions/addLiquidityFunctions.ts` — 18 sites
2456
+ - `src/interactions/coilFunctions.ts` — 1 site
2457
+ - `src/interactions/dexFunctions.ts` — 6 sites
2458
+ - `src/interactions/guardFunctions.ts` — 1 site
2459
+ - `src/interactions/kadenaFunctions.ts` — 2 sites
2460
+ - `src/interactions/kpayFunctions.ts` — 1 site
2461
+ - `src/interactions/ouroFunctions.ts` — 13 sites
2462
+ - `src/interactions/pensionFunctions.ts` — 2 sites
2463
+ - `src/interactions/urStoaFunctions.ts` — 4 sites
2464
+ - `src/interactions/wrapFunctions.ts` — 6 sites
2465
+
2466
+ Each file's `import` from `../constants` swapped `PACT_URL` for
2467
+ `getPactUrl`. No other behaviour change in any helper.
2468
+
2469
+ ### Unchanged
2470
+
2471
+ - `PACT_URL` still exported from `constants/kadena` for backwards
2472
+ compatibility (consumers may still depend on it). New code should
2473
+ use `getPactUrl(chainId)` instead.
2474
+ - `getActivePactUrl` / `getActiveSpvUrl` / `setNodeConfig` /
2475
+ `getCurrentNodeStatus` / `withFailover` from `network` —
2476
+ unchanged.
2477
+ - All 320 tests pass; no test changes required.
2478
+
2479
+ ### Why this matters for consumers
2480
+
2481
+ Before 1.6.1: a UI calling `setNodeConfig("node1")` got correct
2482
+ failover for any read using `pactRead` (URC_0027 batched account
2483
+ selector, balance fetches, guard fetches), but broken behaviour for
2484
+ every transaction-submitting modal — those built their `Pact.builder`
2485
+ through a `createClient(PACT_URL)` instance still pointing at node2.
2486
+
2487
+ After 1.6.1: the entire core resolves the Pact endpoint through the
2488
+ same dynamic getter, so a single `setNodeConfig` flip routes every
2489
+ internal call (reads + simulates + submits) to the chosen node.
2490
+
2491
+ UI consumers (OuronetUI specifically) should ALSO replace any
2492
+ direct `import { PACT_URL }` + `createClient(PACT_URL)` pattern in
2493
+ their own source — the OuronetUI v0.30.13b bump does this
2494
+ alongside the core upgrade.
2495
+
2496
+ ## 1.6.0 — 2026-04-25
2497
+
2498
+ **Smart Ouronet Account auth-path resolution + Rotate Sovereign builder.**
2499
+
2500
+ Smart accounts (Σ. prefix) authorise mutations via `enforce-one` over
2501
+ three branches: the account's own guard, the current sovereign
2502
+ account's guard, and the account's governor. Any one branch
2503
+ satisfying its predicate authorises the transaction. This release
2504
+ adds the primitives that let downstream consumers (OuronetUI's
2505
+ AuthPathZone, the future HUB, custom tooling) discriminate the four
2506
+ Pact guard shapes (keyset / keyset-ref / capability / user) and
2507
+ produce a ready-to-render summary of which branches the codex can
2508
+ sign for, plus the first CFM builder that targets a Smart account's
2509
+ auth path (`C_RotateSovereign`).
2510
+
2511
+ `CodexSigningStrategy` is **unchanged**: it still takes
2512
+ `guards: IKeyset[]` (AND-required). The UI resolves the OR-of-3 to a
2513
+ single chosen keyset before calling `strategy.execute`. This keeps
2514
+ the strategy small and pushes the auth-path picker UX where it
2515
+ belongs (the consumer).
2516
+
2517
+ ### Added
2518
+
2519
+ - `src/guard/smartAccountAuth.ts` — three new primitives, all pure
2520
+ (no I/O, no `@kadena/client`):
2521
+ - `classifyGuardKind(g: unknown): 'keyset' | 'keyset-ref' | 'capability' | 'user' | 'unknown'`
2522
+ — pure shape discriminator. Mirrors OuronetUI's `<GuardTree>`
2523
+ detection 1:1; both stay in lockstep across releases.
2524
+ - `extractKeysetFromGuard(g: unknown): IKeyset | null` — returns
2525
+ the keyset payload for inline keysets and resolved keyset-refs;
2526
+ null for unresolved refs / capabilities / user-guards.
2527
+ Caller resolves keyset-refs upstream via existing `resolveGuard`.
2528
+ - `analyzeSmartAccountAuthPaths({ accountGuard, sovereignGuard, governor }, codexPubs, manualKeys)`
2529
+ — composes the classifier + extractor + `analyzeGuard` for each
2530
+ of the three branches and returns a `SmartAccountAuthPaths`
2531
+ summary: per-branch kind / keyBased / GuardAnalysis / rawGuard,
2532
+ plus derived `anyKeyBased` and `firstSatisfied` flags.
2533
+ - `src/pact/cfmBuilders.ts` — `buildRotateSovereignPactCode({ patron, account, newSovereign })`.
2534
+ Emits `(ouronet-ns.TS01-C1.DALOS|C_RotateSovereign "<patron>" "<account>" "<new-sovereign>")`.
2535
+ - `src/guard/index.ts` re-exports the new module via the existing
2536
+ `/guard` subpath. No package.json `exports` change required.
2537
+ - Tests: `tests/smart-account-auth.test.ts` (full truth-tables for all
2538
+ three primitives) + `tests/cfm-builders.test.ts` extended with
2539
+ `buildRotateSovereignPactCode` cases.
2540
+
2541
+ ### Public-API surface (semver minor — additive only)
2542
+
2543
+ ```ts
2544
+ // New from "@ouronet/ouronet-core/guard"
2545
+ export type GuardKind = "keyset" | "keyset-ref" | "capability" | "user" | "unknown";
2546
+ export function classifyGuardKind(g: unknown): GuardKind;
2547
+ export function extractKeysetFromGuard(g: unknown): IKeyset | null;
2548
+ export interface SmartAccountAuthBranch {
2549
+ readonly which: "guard" | "sovereign" | "governor";
2550
+ readonly kind: GuardKind;
2551
+ readonly keyBased: boolean;
2552
+ readonly analysis: GuardAnalysis | null;
2553
+ readonly rawGuard: unknown;
2554
+ }
2555
+ export interface SmartAccountAuthPaths {
2556
+ readonly branches: readonly [SmartAccountAuthBranch, SmartAccountAuthBranch, SmartAccountAuthBranch];
2557
+ readonly anyKeyBased: boolean;
2558
+ readonly firstSatisfied: number; // 0 | 1 | 2 | -1
2559
+ }
2560
+ export function analyzeSmartAccountAuthPaths(
2561
+ guards: { accountGuard: unknown; sovereignGuard: unknown; governor: unknown },
2562
+ codexPubs: Set<string>,
2563
+ resolvedManualKeys?: Record<string, string>,
2564
+ ): SmartAccountAuthPaths;
2565
+
2566
+ // New from "@ouronet/ouronet-core/pact"
2567
+ export function buildRotateSovereignPactCode(p: {
2568
+ patron: string;
2569
+ account: string;
2570
+ newSovereign: string;
2571
+ }): string;
2572
+ ```
2573
+
2574
+ No breaking changes — every existing symbol stays at its v1.5.0
2575
+ signature. Consumers can upgrade with a `^1.6.0` range bump and
2576
+ `npm install`.
2577
+
2578
+ ### Unchanged
2579
+
2580
+ - `CodexSigningStrategy.execute` signature.
2581
+ - `analyzeGuard` / `GuardAnalysis` — the new module composes them,
2582
+ doesn't replace them.
2583
+ - `createDefaultRegistry()`, `createOuronetAccount`, encryption,
2584
+ codex codec, gas helpers, all 14 pre-existing CFM builders.
2585
+
2586
+ ## 1.5.0 — 2026-04-24
2587
+
2588
+ **Historical-curve primitives surfaced via the `/dalos` subpath.**
2589
+ Pairs with `@ouronet/dalos-crypto@1.2.0`, which promotes LETO /
2590
+ ARTEMIS / APOLLO from low-level `Ellipse` constants to full
2591
+ `CryptographicPrimitive` singletons with their own address prefixes,
2592
+ Schnorr v2 sign + verify, and registry detection.
2593
+
2594
+ OuronetCore re-exports the new symbols through its `/dalos` subpath so
2595
+ downstream consumers (OuronetUI, AncientHoldings HUB, custom tools)
2596
+ can reach them without adding a direct dependency on dalos-crypto.
2597
+
2598
+ ### Added (re-exports from `@ouronet/dalos-crypto/registry`)
2599
+
2600
+ - `Leto`, `Artemis`, `Apollo` — the three historical-curve primitive
2601
+ singletons. Each implements `CryptographicPrimitive` (5 input paths
2602
+ + Schnorr v2 + detect-by-prefix). **NOT registered in the default
2603
+ registry** — opt-in via `registry.register(Leto)`.
2604
+ - `createGen1Primitive(config)` — factory for building custom
2605
+ Gen-1-family primitives from any `Ellipse` + prefix-pair config.
2606
+ - `AddressPrefixPair` type + `DALOS_PREFIXES` constant — for
2607
+ consumers needing to construct primitives with their own prefix
2608
+ conventions.
2609
+
2610
+ ### Changed
2611
+
2612
+ - `package.json` dependency range on `@ouronet/dalos-crypto` tightened
2613
+ to `^1.2.0` (was `^1.0.0`) — the new symbols require that version
2614
+ floor.
2615
+
2616
+ ### Unchanged (Ouronet behaviour preserved)
2617
+
2618
+ - `createDefaultRegistry()` still returns a registry with only
2619
+ `DalosGenesis`. Ouronet continues to use DALOS Genesis exclusively.
2620
+ - `createOuronetAccount` / Pact builders / signing / codex — no
2621
+ changes.
2622
+ - Full 295/295 test suite still passes.
2623
+
2624
+ ## 1.4.0 — 2026-04-24
2625
+
2626
+ **Smart Ouronet Account fields in batched selector.** Extends the
2627
+ on-chain `URC_0027_AccountSelectorMapper` response type (the Pact-side
2628
+ change already landed on-chain) so the TypeScript consumers can type
2629
+ and display the three new fields that the mapper now returns:
2630
+ `public-key`, `sovereign`, and `governor`.
2631
+
2632
+ ### Added
2633
+
2634
+ - `AccountSelectorData.public-key: string` — on-chain source-of-truth
2635
+ public key (DALOS `{prefixLenBase49}.{xyBase49}` format). Populated
2636
+ for both Standard and Smart accounts; expected to match the
2637
+ codex-stored `publicKey` for any account created through the normal
2638
+ flow. A mismatch indicates either an admin-level chain-side rotation
2639
+ (last-resort correction tool) or a corrupted codex entry.
2640
+ - `AccountSelectorData.sovereign: string | false` — only populated for
2641
+ Smart (Σ.) accounts. Pact returns the Ѻ. address of the sovereign
2642
+ Standard account that controls the Smart account's sovereign
2643
+ authorization path. Standard accounts and unactivated accounts
2644
+ return `false`.
2645
+ - `AccountSelectorData.governor: any | false` — only populated for
2646
+ Smart accounts. The Pact guard used for complex custom authorization
2647
+ (capability / module / user guards, alternative keysets). For Smart
2648
+ accounts where no custom governor has been set, this matches the
2649
+ account's own `ouronet-account-guard`. Standard accounts and
2650
+ unactivated accounts return `false`.
2651
+
2652
+ ### Notes
2653
+
2654
+ No other changes to the interaction surface; `getAccountSelectorData`
2655
+ itself is unchanged (it already returns whatever the Pact mapper
2656
+ sends, just with an extended TS type now). The three new fields flow
2657
+ through the existing T5 read → 80-second sync cycle in OuronetUI.
2658
+
2659
+ Future Smart-account management operations (spawn on-chain,
2660
+ rotate-governor, etc.) will be added in a subsequent release as typed
2661
+ Pact builders.
2662
+
2663
+ ## 1.3.0 — 2026-04-23
2664
+
2665
+ **DALOS Cryptography integration.** OuronetCore now exposes the full DALOS cryptographic stack through a new `./dalos` subpath. Consumers (OuronetUI, AncientHoldings hub, CLI tools) can mint Ouronet accounts locally, sign with Schnorr v2, and verify addresses via a single, stable API without depending on the remote `go.ouronetwork.io/api/generate` service.
2666
+
2667
+ ### Added
2668
+
2669
+ - **Dependency**: `@ouronet/dalos-crypto@^1.0.0` (byte-identical TypeScript port of the Go DALOS reference — all 85 Go test vectors reproduced byte-for-byte, all 20 Schnorr signatures match).
2670
+ - **Subpath**: `@ouronet/ouronet-core/dalos` — re-exports the full `CryptographicPrimitive` + `CryptographicRegistry` surface plus a `createOuronetAccount` convenience helper.
2671
+ - **`src/dalos/index.ts`** — re-exports:
2672
+ - Types: `KeyPair`, `PrivateKeyForms`, `FullKey`, `PrimitiveMetadata`, `CryptographicPrimitive`, `DalosGenesisPrimitive`, `Bitmap`
2673
+ - Values: `DalosGenesis`, `CryptographicRegistry`, `createDefaultRegistry`, `isDalosGenesisPrimitive`, bitmap utilities
2674
+ - **`src/dalos/account.ts`** — `createOuronetAccount(registry, options)`:
2675
+ - Discriminated-union `CreateAccountOptions` covering all 6 input modes (`random`, `bitString`, `integerBase10`, `integerBase49`, `seedWords`, `bitmap`)
2676
+ - Dispatches to the right primitive method
2677
+ - Returns a fully-materialised `FullKey` (keyPair + all 3 private-key forms + both Ѻ./Σ. addresses)
2678
+ - Throws descriptive errors on unregistered primitives or unsupported modes
2679
+ - **`tests/dalos-integration.test.ts`** — 9 integration tests (all 6 modes + registry detect/sign/verify end-to-end + error paths).
2680
+
2681
+ ### Usage example
2682
+
2683
+ ```typescript
2684
+ import {
2685
+ createDefaultRegistry,
2686
+ createOuronetAccount,
2687
+ } from '@ouronet/ouronet-core/dalos';
2688
+
2689
+ const registry = createDefaultRegistry();
2690
+
2691
+ // Mint an account from seed words:
2692
+ const account = createOuronetAccount(registry, {
2693
+ mode: 'seedWords',
2694
+ data: ['hello', 'world', 'dalos', 'genesis'],
2695
+ });
2696
+ console.log(account.standardAddress); // Ѻ.xxxxx...
2697
+
2698
+ // Sign + verify:
2699
+ const primitive = registry.detect(account.standardAddress);
2700
+ const sig = primitive!.sign!(account.keyPair, 'approve tx 123');
2701
+ const valid = primitive!.verify!(sig, 'approve tx 123', account.keyPair.publ);
2702
+ ```
2703
+
2704
+ ### Verified
2705
+
2706
+ - All existing **286 OuronetCore tests still pass** (no regressions).
2707
+ - **9 new integration tests all pass**.
2708
+ - **Total: 295/295 tests pass in ~14 s**.
2709
+ - Build clean (`tsc -p tsconfig.build.json`).
2710
+
2711
+ ### Dependency resolution
2712
+
2713
+ `@ouronet/dalos-crypto@^1.0.0` is resolved from the public npmjs registry
2714
+ (`https://registry.npmjs.org/@ouronet/dalos-crypto/-/dalos-crypto-1.0.0.tgz`).
2715
+ The earlier `file:../DALOS_Crypto/ts` placeholder was cleared in this same
2716
+ version — the published tarball now ships with a registry-ranged dep, so
2717
+ downstream consumers (OuronetUI, AncientHoldings hub) don't need a sibling
2718
+ checkout to install.
2719
+
2720
+ ### Migration notes for consumers
2721
+
2722
+ This release is purely additive — no existing OuronetCore API changed. Consumers that don't import from `./dalos` see zero behavioural difference. The OuronetUI migration to the local-generation path lands separately as Phase 9 of the DALOS TypeScript port.
2723
+
2724
+ ---
2725
+
2726
+ ## 1.2.2 — 2026-04-23
2727
+
2728
+ **Secret name fix.** v1.2.1 still failed ENEEDAUTH because the workflow referenced `secrets.NPM_TOKEN` but the actual GitHub repo secret is named `NPMPUSHER`. Updated the workflow to use the correct secret name. The secret itself (content-wise) is correct — just a name mismatch between what the workflow expected and what was registered. No source changes.
2729
+
2730
+ ## 1.2.1 — 2026-04-23
2731
+
2732
+ **Publish fix.** v1.2.0's publish workflow failed with `ENEEDAUTH` because `setup-node`'s scope-based `.npmrc` generation didn't reliably propagate `NODE_AUTH_TOKEN` to `npm publish`. Rewrote the workflow to write `.npmrc` itself with an explicit `_authToken` line, plus added an `npm whoami` verify step so future auth failures surface earlier. Pure infrastructure fix — source code identical to v1.2.0.
2733
+
2734
+ ## 1.2.0 — 2026-04-22
2735
+
2736
+ **Registry switch: GitHub Packages → npmjs.org.** Pure distribution change, zero source code changes. Everything that made v1.1.0 work still works identically; consumers just pull from a different registry.
2737
+
2738
+ ### Changed
2739
+
2740
+ - `.github/workflows/publish.yml` rewired to publish to `https://registry.npmjs.org` instead of `https://npm.pkg.github.com`. Uses the `NPM_TOKEN` repo secret (granular npmjs token, 90-day expiry, scoped to `@stoachain` read+write with bypass-2FA enabled for CI automation).
2741
+ - `package.json` `publishConfig` → `{ registry: https://registry.npmjs.org, access: public }`. Scoped packages default to "restricted" on npmjs.org; explicit `access: public` makes them installable anonymously.
2742
+
2743
+ ### Why the switch
2744
+
2745
+ GitHub Packages for npm requires any consumer to authenticate with a GitHub PAT even for packages marked "public" at the package level — the StoaChain org doesn't have the legacy "allow anonymous access" toggle, so every install site (Ploi, local dev, team laptops, CI) needed `NPM_TOKEN` configured. npmjs.org is the standard public JS registry — `npm install @ouronet/ouronet-core` just works anywhere with zero auth setup. Switch has no tradeoff: the package is already public in source form on github.com/StoaChain/OuronetCore, so making it installable without friction is pure upside.
2746
+
2747
+ ### For consumers (OuronetUI, future HUB, team laptops)
2748
+
2749
+ - Delete any `.npmrc` entries or `NPM_TOKEN` env vars that pointed at GitHub Packages.
2750
+ - Run `npm install` — everything resolves from the default registry.
2751
+ - No credentials needed. No `@stoachain:registry=...` lines needed.
2752
+
2753
+ ### For contributors publishing new versions
2754
+
2755
+ Same workflow as before: bump version, update changelog, `git tag v$VERSION && git push --tags`. The `publish.yml` workflow handles the rest. The `NPM_TOKEN` secret in the repo needs to be rotated every 90 days (granular npm tokens' max lifetime).
2756
+
2757
+ ## 1.1.0 — 2026-04-22
2758
+
2759
+ **Tier 2 testing pass.** 18 new tests across 2 files (one extension + one new). 286 total pass (was 268). No source changes — tests exercise existing code paths that weren't previously covered. See `OuronetUI/docs/TESTING_STRATEGY.md` §Tier 2.
2760
+
2761
+ ### Added
2762
+
2763
+ - **`tests/strategy.test.ts` extended** — 6 new edge-case tests:
2764
+ - Foreign key synthesis via `resolvedForeignKeys` (ForeignKeySignModal flow)
2765
+ - Tx with unsigned slot for a foreign pub when no resolvedForeignKeys supplied — documents that strategy doesn't police guard satisfaction; chain-level rejection is the user-visible failure mode
2766
+ - `resolver.requestForeignKey` invocation path + error propagation
2767
+ - Impossible case: only codex key is also payment key AND guard key → throw
2768
+ - Resolver throw (HD derivation fail, password cancelled) propagates up execute()
2769
+ - Multi-guard: 2-of-3 patron + 1-of-1 resident → caps correctly picks the one free codex key
2770
+ - Keyset-ref guards flow through without surprise
2771
+ - **`tests/encryption-upgrade.test.ts`** NEW — 12 tests for the V1 → V2 upgrade-on-unlock flow:
2772
+ - Happy path: V1 blob decrypts → re-encrypt with schemaVersion=1 → V2 blob → decrypts back to same plaintext
2773
+ - Idempotent: re-running upgrade on a V2 blob leaves it V2
2774
+ - schemaVersion-null/0 → V1 blob (fail-safe pre-upgrade behaviour)
2775
+ - Mixed-codex state: some V1 + some V2 blobs all decrypt via `smartDecrypt`
2776
+ - Wrong-password rejection for both V1 and V2 (no silent V2-fallback slip)
2777
+ - `isCodexUpgraded` ↔ `smartEncrypt` contract across null/0/1/2/99/garbage inputs
2778
+ - Password-change-during-upgrade: new password decrypts, old password fails
2779
+ - `decryptStringV2` V1-fallback path (belt-and-suspenders)
2780
+ - Full-codex simulation: 5 entries (wallet + account + pure keypair fields), all round-trip through the upgrade pipeline
2781
+
2782
+ ### No changes
2783
+
2784
+ - Source. These are pure test additions.
2785
+
2786
+ ## 1.0.0 — 2026-04-22
2787
+
2788
+ **Extraction complete.** Symbolic bump to 1.0.0 to mark the end of the OuronetUI → OuronetCore migration. Every piece of blockchain logic that used to live in OuronetUI (Pact builders, signing pipeline, encryption, guard analysis, gas calibration, codex codec, seed-type migration) now lives here. OuronetUI is a pure consumer. No API changes from 0.11.0 — strict semver would call this 0.11.1, but the bump signals "this is the public surface we commit to and will semver against going forward."
2789
+
2790
+ ### Documentation
2791
+
2792
+ - `README.md` — rewritten. Status now "extraction complete", not "skeleton only". Submodule table reflects current exports (including the 14 `buildXxxPactCode` builders added in 0.11.0). Cross-links to OuronetUI's TESTING_STRATEGY + CFM_BUILD_GUIDE. Documents the `npm link` local-dev flow and the tag-push publish workflow.
2793
+
2794
+ ### What 1.0.0 commits to
2795
+
2796
+ - Public API: the 10 subpath exports listed in `package.json` (`/constants`, `/network`, `/gas`, `/guard`, `/crypto`, `/signing`, `/codex`, `/reads`, `/pact`, `/interactions`). Removing or renaming anything exported at the top level of any of these = major version bump.
2797
+ - Codex export format: `"version": "1.2"` is stable — existing `OuronetCodex_*.json` files users have on disk must stay importable forever.
2798
+ - Signing strategy contract: `CodexSigningStrategy.execute({ build, guards, paymentKey?, resolvedForeignKeys?, extraSigners? })` is the stable shape.
2799
+
2800
+ ## 0.11.0 — 2026-04-22
2801
+
2802
+ **Tier 1 testing pass.** Extracts the per-modal Pact-code builders into a pure module and adds 75 tests across 4 new/extended test files. All 268 tests pass (was 193). See `OuronetUI/docs/TESTING_STRATEGY.md` for the testing strategy rationale.
2803
+
2804
+ ### Added
2805
+
2806
+ - **`@ouronet/ouronet-core/pact/cfmBuilders`** — 14 pure Pact-code string builders, one per CFM function the ecosystem ships: `buildTransferPactCode`, `buildClearDispoPactCode`, `buildSublimatePactCode`, `buildCompressPactCode`, `buildCoilPactCode`, `buildCurlPactCode`, `buildBrumatePactCode`, `buildConstrictPactCode`, `buildColdRecoveryPactCode`, `buildDirectRecoveryPactCode`, `buildCullPactCode`, `buildAwakePactCode`, `buildSlumberPactCode`, `buildFirestarterPactCode`. Replaces the inline template literals that used to live in OuronetUI's 23 CFM modals. Each builder takes a typed params object and returns the canonical Pact-code string.
2807
+ - **`tests/cfm-builders.test.ts`** — 35 tests. One per builder + argument-order preservation + decimal formatting + edge cases (empty nonce list, single-item list, dayz as integer not decimal, etc). Cross-cutting "every builder produces `(ouronet-ns...)` shape" test for forward-compat.
2808
+ - **`tests/codex-codec.test.ts`** — 31 tests covering `buildCodexExport` / `serializeCodex` / `deserializeCodex` / `migrateSeedType`. Round-trip + version-mismatch rejection + unicode preservation + idempotent seed-type migration.
2809
+ - **`tests/strategy.test.ts`** — 9 tests for `CodexSigningStrategy.execute()` + `sign()`. Uses mock `PactClient` + mock `KeyResolver` with real Ed25519 nacl signing (RFC 8032 test vectors for keypairs). Verifies call-order (simulate → submit), gas calibration flows through, sim-failure halts pipeline, guard keypair dedup, `extraSigners` folded into sign step.
2810
+
2811
+ ### Changed
2812
+
2813
+ - `tests/guard.test.ts` — extended with multi-guard scenarios (patron+resident cooperating) and keyset-ref guard edge cases. The pattern every CFM modal runs internally but that wasn't directly covered before.
2814
+
2815
+ ### Migration notes
2816
+
2817
+ - `@ouronet/ouronet-core/pact` now also exports the 14 `buildXPactCode` functions. Existing imports of `formatDecimalForPact`, `safeCreationTime`, `filterFreePositionData`, `mayComeWithDeimal`, `parseEU`, `formatEU` all still work.
2818
+ - No breaking change — pure additions.
2819
+
2820
+ ## 0.10.0 — 2026-04-22
2821
+
2822
+ **Phase 4 of the OuronetUI → OuronetCore extraction.** Moves encryption primitives + introduces portable Codex types + the backup-JSON codec. Pure additions — no existing export changes.
2823
+
2824
+ ### Added
2825
+
2826
+ - **`@ouronet/ouronet-core/crypto`** — migrated wholesale from OuronetUI's `src/lib/encryptor.ts` + `src/lib/encryptorV2.ts`. Both V1 (PBKDF2-SHA256 10k) and V2 (PBKDF2-SHA512 600k) primitives, plus `smartDecrypt` auto-format-detection + `smartEncrypt` (now pure — takes `schemaVersion: string | null` as an argument instead of reading `localStorage.codex_schema_version`). Works in browser + Node.js.
2827
+ - **`@ouronet/ouronet-core/codex`** — portable Codex shape + codec:
2828
+ - `PlaintextCodex<KS, OA, PK, AB, UI>` — generic in-memory shape. Default type params are `unknown`; consumers (OuronetUI, future HUB) plug in their own wallet/account/keypair types. Fields: kadenaWallets, ouronetWallets, addressBook, pureKeypairs, uiSettings, schemaVersion, lastUpdatedAt, lastUpdatedDevice.
2829
+ - `CodexExportV1_2<KS, OA, AB, UI>` — the `"version": "1.2"` backup-JSON shape OuronetUI has been writing since early 2025. Intentionally preserved byte-for-byte: existing `OuronetCodex_*.json` files stay valid.
2830
+ - `buildCodexExport(codex)` + `serializeCodex(codex)` (stringify with 2-space indent) + `deserializeCodex(json)` (throws on version mismatch — fail-fast before mis-decoding a hypothetical future V2).
2831
+ - `migrateSeedType(rawType)` + `SeedType`/`RawSeedType` types — the historical `legacy → chainweaver`, `new → koala` mapping. Was inlined in OuronetUI's WalletStorage; lives here now so HUB doesn't rediscover it. Idempotent.
2832
+ - 31 new encryption tests moved to `tests/encryption.test.ts` (from OuronetUI's `src/lib/__tests__/encryption.test.ts`). Covers V1/V2 round-trips, wrong-password, envelope shape, smartDecrypt mixed-format, isCodexUpgraded predicate, smartEncrypt schema-version dispatch. **193 tests total pass** (was 162).
2833
+
2834
+ ### Migration notes
2835
+
2836
+ - `smartEncrypt` API changed: now `smartEncrypt(plaintext, password, schemaVersion)` instead of the browser-only `smartEncrypt(plaintext, password)`. OuronetUI keeps a tiny `src/lib/smart-encrypt-browser.ts` wrapper that reads `localStorage.codex_schema_version` and delegates here — no behaviour change for UI consumers.
2837
+ - Existing codex blobs decrypt identically — no on-disk format change.
2838
+
2839
+ ## 0.9.1 — 2026-04-22
2840
+
2841
+ **Phase 3b cleanup.** Deletes 15 now-unused `executeX` helpers from `interactions/wrapFunctions.ts` and re-tightens `tsconfig.json` (`noUnusedLocals` + `noUnusedParameters` back on, were relaxed during the 3a/3b scaffolding).
2842
+
2843
+ ### Removed
2844
+
2845
+ - `executeFirestarter`, `executeSublimate`, `executeCompress`, `executeTransferToken`, `executeCoil`, `executeCurl`, `executeBrumate`, `executeConstrict`, `executeColdRecovery`, `executeDirectRecovery`, `executeCull`, `buildNativeTransferTx`, `executeAwake`, `executeSlumber`, `executeClearDispo` — every last CFM modal in OuronetUI (v0.29.7c) moved to `strategy.execute()`, so these direct-path helpers have no remaining callers. Kept: `executeWrapStoa` + `executeWrapUrStoa` (still used by the two Wrap* modals, which aren't CFM modals).
2846
+
2847
+ ### Changed
2848
+
2849
+ - `tsconfig.json`: `noUnusedLocals: true`, `noUnusedParameters: true` — both were off during 3a/3b to let scaffolding compile with in-flight unused symbols. Back on, with a handful of leftover unused imports (`NATIVE_TOKEN_VAULT`, `IKeyset`, a couple of dev-local variables) cleaned up.
2850
+
2851
+ ### Migration
2852
+
2853
+ Consumers that still imported these helpers will fail to resolve — if you're one of those, switch to `CodexSigningStrategy` via `new CodexSigningStrategy(resolver, client)` + `strategy.execute({...})` (see codexStrategy.ts docstring).
2854
+
2855
+ ## 0.9.0 — 2026-04-22
2856
+
2857
+ **Phase 3b.2 Wave 4 support.** Small SigningStrategy API addition — adds `extraSigners?: IKadenaKeypair[]` to `execute()` so flows with more than two signer roles (like Firestarter, which needs GAS_PAYER + payment-key with `coin.TRANSFER` cap + account guards) can plug in. No breaking change: existing consumers pass nothing and behave exactly as before.
2858
+
2859
+ ### Added
2860
+
2861
+ - `SigningStrategy.execute({ extraSigners? })` — optional array of pre-resolved `IKadenaKeypair`s. The strategy folds them into the sign step alongside the guard keypairs (deduplicated by pubkey). Used by OuronetUI's `FirestarterCFMModal` to supply the payment-key signer whose `coin.TRANSFER` cap the build closure wires explicitly via `addSigner`.
2862
+
2863
+ ## 0.8.0 — 2026-04-22
2864
+
2865
+ **Phase 3b.2 Wave 1 support.** Pure addition — exposes `safeCreationTime()` from `@ouronet/ouronet-core/pact` so CFM modals in OuronetUI can mint `creationTime` values the same way every core `execute*` helper already does (Pact `setMeta`'s creationTime − 30s to sidestep node clock-skew rejections). No behavior changes to existing exports.
2866
+
2867
+ ### Added
2868
+
2869
+ - `safeCreationTime(): number` — shared `Math.floor(Date.now()/1000) - 30` helper. Used by the CFM modals' `strategy.execute({ build })` closures so their `setMeta({creationTime})` matches what the A-F pipeline has always done. Keeps sim + submit consistent across every modal.
2870
+
2871
+ ## 0.7.0 — 2026-04-22
2872
+
2873
+ **Phase 3b.1 of the OuronetUI → OuronetCore extraction.** Ships `CodexSigningStrategy` — the first real `SigningStrategy` implementation. The 23 CFM modals in OuronetUI can now delete their ~43-line `handleExecute` A-F pipeline in favor of a ~30-line `strategy.execute({...})` call. Done one modal at a time with smoke-testing between each; `CompressCFMModal` is the first consumer (see OuronetUI v0.29.6).
2874
+
2875
+ ### Added
2876
+
2877
+ - **`@ouronet/ouronet-core/signing/CodexSigningStrategy`** — implements the full pipeline:
2878
+ 1. Get codex pub set from resolver
2879
+ 2. `analyzeGuard` each guard (with any caller-provided resolvedForeignKeys)
2880
+ 3. Resolve keypairs via `resolver.getKeyPairByPublicKey` (or synthesize inline for resolved-foreign keys)
2881
+ 4. `selectCapsSigningKey` for GAS_PAYER avoiding pure-signer overlap
2882
+ 5. Build via caller closure (given the resolved pubkeys)
2883
+ 6. `client.dirtyRead` to simulate → fail-fast
2884
+ 7. `calculateAutoGasLimit` on measured gas
2885
+ 8. Rebuild with real gas
2886
+ 9. `universalSignTransaction` with deduped keypairs
2887
+ 10. `client.submit` → return `{requestKey, raw}`
2888
+ - `.execute(...)` for the full pipeline; `.sign(...)` as a lower-level primitive for callers that own their simulation flow.
2889
+
2890
+ ### Changed
2891
+
2892
+ - `SigningStrategy.execute`'s `build` closure signature widened: now receives `{gasLimit, capsKeyPub, guardPubs}` instead of just `gasLimit`. Necessary because Pact.builder's `addSigner` calls need the pubkeys at simulation time (cap-requiring modules reject sims with missing capability signers).
2893
+
2894
+ ### Migration semantics
2895
+
2896
+ Resolved-foreign-keys handling: when a guard-signer pubkey is in the caller's `resolvedForeignKeys` map (user pasted a raw priv via `ForeignKeySignModal` or equivalent), the strategy synthesizes `{publicKey: pub, privateKey, seedType: "foreign"}` inline rather than asking the resolver — the resolver never knew about it. Codex keys still go through the resolver which handles password prompts + HD derivation.
2897
+
2898
+ ## 0.6.0 — 2026-04-22
2899
+
2900
+ **Phase 3a of the OuronetUI → OuronetCore extraction.** Pure scaffolding release — introduces the signing abstractions Phase 3b will wire up and collapse the 23 CFM `handleExecute` duplicates against.
2901
+
2902
+ ### Added
2903
+
2904
+ - **`@ouronet/ouronet-core/signing/types`** — three interfaces grounded in the research pass (`docs/EXTRACT_OURONET_CORE_PLAN.md §2.2` in the OuronetUI repo):
2905
+ - **`IKadenaKeypair`** — canonical home for the keypair shape. Same structure ouroFunctions has been exporting since Phase 2b; this version is the authoritative one going forward. Both paths compile because the shape is identical.
2906
+ - **`KeyResolver`** — the three-method contract (`listCodexPubs`, `getKeyPairByPublicKey`, optional `requestForeignKey`) each consumer implements against their own Codex backend. OuronetUI: `ReduxCodexResolver` (Redux + wallet-context). HUB: future `FileCodexResolver` (disk file + env/KMS passphrase). CLI: `readline`. Etc.
2907
+ - **`PactClient`** — minimal `dirtyRead` + `submit` subset of `@kadena/client`'s `createClient` return. Strategies accept one so the URL isn't baked into core (browser needs the CF-worker proxy; server hits Stoa directly).
2908
+ - **`SigningStrategy`** — the `execute(...)` + `sign(...)` pipeline interface. Still unimplemented in this release; `CodexSigningStrategy` lands in Phase 3b.
2909
+
2910
+ ### Changed
2911
+
2912
+ Nothing — this is additive scaffolding. Every existing import path keeps working unchanged; every runtime behavior is identical to v0.5.0.
2913
+
2914
+ ### Tests
2915
+
2916
+ Still 162 — the new interfaces are compile-time only and have no runtime until Phase 3b wires an implementation. On-chain acceptance for the whole signing surface runs at the end of 3b (9-item real-wallet matrix).
2917
+
2918
+ ## 0.5.0 — 2026-04-22
2919
+
2920
+ **Phase 2c of the OuronetUI → OuronetCore extraction.** HD keypair derivation + runtime wallet class move to core; `CodexStorageAdapter` interface defined so browser + server consumers each implement their own storage backend.
2921
+
2922
+ ### Added
2923
+
2924
+ - **`@ouronet/ouronet-core/wallet/KadenaWalletBuilder`** — HD keypair derivation + mnemonic generation/validation. Two paths: `koala` (24-word BIP39 + SLIP-10 Ed25519) and `chainweaver`/`eckowallet` (12-word Kadena mnemonic + BIP32-Ed25519). Plus `encrypt`/`decrypt` wrapping `@kadena/hd-wallet`'s AES-GCM primitive (distinct from core/crypto's Codex-level encryption — this is the inner per-seed wrapper).
2925
+ - **`@ouronet/ouronet-core/wallet/KadenaWallet`** — runtime account class with `address`, `publicKey`, `derivationPath`, and lazy `getBalance()`. Pure data holder + one async chain read.
2926
+ - **`@ouronet/ouronet-core/wallet/SeedType`** — `"koala" | "chainweaver" | "eckowallet"`. Picks the derivation algorithm; NOT a delegation marker (no browser-wallet integration is wired).
2927
+ - **`@ouronet/ouronet-core/wallet/CodexStorageAdapter`** — interface only. Two methods: `load()`, `save(codex)`, `clear()`. Concrete implementations live in each consumer: OuronetUI ships `LocalStorageCodexAdapter` (backed by localStorage + redux-persist); the HUB will ship `EncryptedFileCodexAdapter` (AES-GCM file on disk). Core intentionally provides no default — each runtime brings its own.
2928
+
2929
+ ### Why no default adapter in core
2930
+
2931
+ Different runtimes have fundamentally different idioms: Redux action-dispatch (browser) vs direct-mutation-then-write (server) vs async-backend (future). Trying to force them through one shared state machine gains nothing; the interface is the minimal contract. Phase 4's `PlaintextCodex` type will concrete-type the payload both adapters persist.
2932
+
2933
+ ### Tests
2934
+
2935
+ Still 162 — the wallet code is integration-level (needs real @kadena/hd-wallet WASM + a mnemonic); unit-testing would mostly exercise the library. Phase 3b's on-chain checklist covers HD-derivation end-to-end.
2936
+
2937
+ ## 0.4.1 — 2026-04-22
2938
+
2939
+ **Phase 2b refinement.** Adds a pluggable Pact reader so consumers can wire their own cache-aware implementation, restoring the read behavior OuronetUI had before Phase 2b. Caught via a Smart Swap UI flicker bug: after v0.4.0, every dex read inside `interactions/*` went through `rawCalibratedDirtyRead` — no cache, no dedup — so a widget that fires reads per-keystroke (Smart Swap's token selector) flickered and couldn't finalize a selection.
2940
+
2941
+ ### Added
2942
+
2943
+ - **`@ouronet/ouronet-core/reads`** — new `setPactReader(reader)` + `getPactReader()` + `pactRead(pactCode, options)`. Interactions now call `pactRead` instead of `rawCalibratedDirtyRead` directly; the default is `rawCalibratedDirtyRead` (so HUB / server consumers see no change), but OuronetUI calls `setPactReader(calibratedDirtyRead)` at boot and its cache-aware wrapper takes over.
2944
+
2945
+ ### Changed
2946
+
2947
+ - Every `rawCalibratedDirtyRead(...)` call inside `src/interactions/*` rewritten to `pactRead(...)`. Behavior identical when no reader is configured (default is still raw); behavior cache-aware when a consumer configures one.
2948
+
2949
+ ### Why
2950
+
2951
+ Phase 2b's sed swapped `calibratedDirtyRead` → `rawCalibratedDirtyRead` blanket across all moved interactions. That was too aggressive — the intent was "simulations shouldn't be cached" (one-shot reads before signing), but the same swap also touched routine display reads inside `interactions/*` (`getPoolTotalFee`, `getSwpairs`, `getSWPairGeneralInfo`, etc.). These need cache dedup because UI widgets call them repeatedly as users interact. Pluggable reader keeps both worlds clean: raw by default, cached on request.
2952
+
2953
+ ## 0.4.0 — 2026-04-22
2954
+
2955
+ **Phase 2b of the OuronetUI → OuronetCore extraction.** The largest single phase so far — all Pact builders + error helpers + signing core move into the package. Both consumers (OuronetUI today, HUB in future) now get every on-chain action OuronetUI performs by importing from `@ouronet/ouronet-core/interactions/*`.
2956
+
2957
+ ### Added
2958
+
2959
+ - **`@ouronet/ouronet-core/interactions/*`** — 13 files from OuronetUI's `src/kadena/interactions/`:
2960
+ `activateFunctions`, `addLiquidityFunctions`, `coilFunctions`, `crossChainFunctions`, `dexFunctions` (swap + pool reads), `guardFunctions` (guard rotation), `infoOneFunctions` (INFO_* cost-estimate reads), `kadenaFunctions` (native coin + account reads), `kpayFunctions`, `ouroFunctions` (OURO token family, ignis, virtual-OURO, activation flow), `pensionFunctions` (brumate / hibernate), `urStoaFunctions` (stake / unstake / collect), `wrapFunctions` (Coil / Curl / Compress / Sublimate / Awake / Slumber / Transfer / Firestater). Sub-path-importable as `@ouronet/ouronet-core/interactions/ouroFunctions` etc. — the package now declares a wildcard subpath so every file is its own entry.
2961
+ - **`@ouronet/ouronet-core/errors`** — `TransactionError`, `SigningError` + `createSigningError` / `createSimulationError` / `formatErrorForUser` / `logDetailedError`. Moved wholesale from OuronetUI's `src/lib/transaction-errors.ts`; 100% pure, no browser deps.
2962
+ - **`@ouronet/ouronet-core/signing/universalSign.ts`** — `universalSignTransaction`, `UniversalKeypair`, `fromKeypair`. Phase-3 will collapse with OuronetUI's local copy (which still exists — interactions in core use core's version, the rest of UI uses its own until signing's full refactor lands).
2963
+ - **`@ouronet/ouronet-core/guard`** adds `IKeyset` type (was in OuronetUI's `src/ouro.d.ts`).
2964
+
2965
+ ### Changed
2966
+
2967
+ - `rawCalibratedDirtyRead` gained accepted-and-ignored `tier?: string` + `skipTempWatcher?: boolean` options — source-compatibility shim for the 20+ call sites that previously hit OuronetUI's cache-aware wrapper with these options.
2968
+ - Barrel `src/interactions/index.ts` now re-exports only `ouroFunctions` (the canonical source of shared types). Cross-file collisions on `IKadenaKeypair` / `IOuroAccountKeypair` / etc. surface if consumers import from the root barrel; use sub-path imports (`./interactions/<filename>`) when in doubt.
2969
+
2970
+ ### Internal
2971
+
2972
+ - Relaxed `tsconfig.json`: removed `noUnusedLocals` + `noUnusedParameters` (OuronetUI's cached typecheck was silently tolerating these; re-tighten in a later cleanup phase).
2973
+ - ~12 surgical `as any` casts inside the moved interactions where stricter `@kadena/types` in TS 5.9 rejected access patterns that worked under the OuronetUI cache. All casts are boundary-level (narrowing response bodies, slippage-bounds addData args, BIP32 WASM hashBytes). No behaviour change.
2974
+
2975
+ ### Tests
2976
+
2977
+ Still 162 tests / 5 files on the core side — the moved interaction code has no tests yet (interactions are integration-level; Phase 3b's on-chain acceptance checklist is the real verification). Phase 3 / 4 add more.
2978
+
2979
+ ## 0.3.0 — 2026-04-22
2980
+
2981
+ **Phase 2a of the OuronetUI → OuronetCore extraction.** Adds raw on-chain read + Pact-format helpers.
2982
+
2983
+ ### Added
2984
+
2985
+ - **`@ouronet/ouronet-core/reads`** — `rawCalibratedDirtyRead(pactCode, options?)`. Uncached Pact dirty-read with a read-friendly 10M gas ceiling. Pure, no React lifecycle. OuronetUI layers its PactQueryCache on top; the HUB will call this directly.
2986
+ - **`@ouronet/ouronet-core/pact`** — three helpers moved from OuronetUI's `src/lib/utils.ts`:
2987
+ - `formatDecimalForPact(amount, maxDecimals?)` — canonicalize a decimal string for Pact code literals (adds `.0` to integers, truncates overlong fractional parts, validates shape).
2988
+ - `mayComeWithDeimal(data)` — unwrap Pact's `{ decimal: "…" }` envelope to the underlying string (typo preserved from original to keep name compatibility).
2989
+ - `filterFreePositionData(raw)` — normalise the `[{ "reward-tokens": [0] }]` sentinel the chain returns for "no positions" to an empty array.
2990
+ - **`@ouronet/ouronet-core/signing`** — `toHexString(byteArray)` added alongside `publicKeyFromPrivateKey` + `publicKeyFromExtendedKey`. Used wherever raw bytes cross into strings (derived private keys, signed-tx hashes).
2991
+
2992
+ ### Tests
2993
+
2994
+ +52 tests across `pact-format.test.ts` (29: formatDecimalForPact, mayComeWithDeimal, filterFreePositionData, Pact code template snapshots) and `signing.test.ts` (signing-primitives test moved from OuronetUI, plus 7 new tests for toHexString). Total suite: 162 tests across 5 files (was 110).
2995
+
2996
+ ### `exports` map
2997
+
2998
+ Added `./pact` subpath to `package.json` exports. Existing `./reads` and `./signing` subpaths gain new symbols; consumers don't need to change import style.
2999
+
3000
+ ## 0.2.0 — 2026-04-22
3001
+
3002
+ **Phase 1 of the OuronetUI → OuronetCore extraction.** First real code move.
3003
+
3004
+ ### Added
3005
+
3006
+ - **`@ouronet/ouronet-core/constants`** — full StoaChain / Chainweb / Pact constants:
3007
+ - `KADENA_NETWORK`, `KADENA_CHAIN_ID`, `KADENA_NAMESPACE`, `KADENA_BASE_URL`, `PACT_URL`, `KADENA_CHAINS`, `STOA_CHAINS`, `STOA_CHAIN_COUNT`
3008
+ - Stoa autonomic account addresses: `STOA_AUTONOMIC_OUROBOROS`, `STOA_AUTONOMIC_LIQUIDPOT`, `STOA_AUTONOMIC_OURONETGASSTATION`, legacy aliases `GAS_STATION` + `NATIVE_TOKEN_VAULT`
3009
+ - `MAIN_TOKENS` list
3010
+ - Token IDs: `TOKEN_ID_OURO`, `TOKEN_ID_IGNIS`, `TOKEN_ID_AURYN`, `TOKEN_ID_ELITEAURYN`, `TOKEN_ID_WSTOA`, `TOKEN_ID_SSTOA`, `TOKEN_ID_GSTOA`, `ALL_TOKEN_IDS`
3011
+ - Helper accessors `getPactUrl(chainId)` + `getSpvUrl(chainId)` that route through node-failover
3012
+ - **`@ouronet/ouronet-core/network`** — Stoa node failover:
3013
+ - Primary node2 → fallback node1, with health check + 30s retry loop
3014
+ - `getActiveBaseUrl`, `getActiveHost`, `getActivePactUrl`, `getActiveSpvUrl`
3015
+ - `setNodeConfig` (node2 / node1 / custom), `getNodeConfig`, `getCurrentNodeStatus`, `getNodeGasLimit`, `getActiveGasLimit`, `CHAINWEB_DEFAULT_GAS_LIMIT`
3016
+ - `withFailover(fn)` — wrapper that retries once on fallback for network errors
3017
+ - `initNodeFailover()` — optional startup health check
3018
+ - **`@ouronet/ouronet-core/gas`** — gas + ANU/STOA math:
3019
+ - `ANU_PER_STOA` (10^12), `GAS_LIMIT_MAX` (2M), `GAS_PRICE_MIN_ANU`, TTL constants
3020
+ - `anuToStoa`, `stoaToAnu`, `formatAnuAsStoa`
3021
+ - `getGasLimitStatus` (safe/warning/danger bands) + `GAS_LIMIT_COLORS`
3022
+ - `formatMaxFee`, `calculateAutoGasLimit` (5-bucket buffer with node-cap)
3023
+ - **`@ouronet/ouronet-core/guard`** — full guard analysis surface:
3024
+ - `computeThreshold` for 14 predicates: standard (keys-all/keys-any/keys-2), stoic fixed (keys-1/3/4), M-of-N (2-of-3 through 5-of-9), percentage (51/60/66/75/90pct), tolerance (all-but-one/two)
3025
+ - `predicateLabel`, `analyzeGuard`, `buildCodexPubSet`, `classifyPaymentKey`, `tryDerivePublicKey`, `selectCapsSigningKey` — all pure
3026
+ - **`@ouronet/ouronet-core/signing`** — phase-1 temp copy of pure public-key primitives:
3027
+ - `publicKeyFromPrivateKey` (standard Ed25519 from 64-char seed)
3028
+ - `publicKeyFromExtendedKey` (BIP32-Ed25519 from kL half of extended key)
3029
+ - Full signing surface (universalSignTransaction, KeyResolver, SigningStrategy) lands in Phase 3
3030
+
3031
+ ### Tests
3032
+
3033
+ 110 tests across 3 files (`guard.test.ts` 54, `gas.test.ts` 31, `network.test.ts` 25), all green in Node environment.
3034
+
3035
+ ### Peer dependencies added
3036
+
3037
+ - `@kadena/cryptography-utils ^0.4.0` (public-key derivation)
3038
+ - `@noble/curves ^1.4.0` (BIP32-Ed25519 math)
3039
+
3040
+ ## 0.1.0 — 2026-04-21
3041
+
3042
+ Initial scaffold commit. Empty-barrel skeleton. See
3043
+ [`docs/EXTRACT_OURONET_CORE_PLAN.md`](https://github.com/DemiourgosHoldings/OuronetUI/blob/dev/docs/EXTRACT_OURONET_CORE_PLAN.md) in the OuronetUI repo for the multi-phase plan driving this package's development.