wative-core 2.5.1 → 2.5.3

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 (35) hide show
  1. package/CHANGELOG.md +61 -55
  2. package/README.md +27 -3
  3. package/dist/artifacts/evm/index.cjs +1 -1
  4. package/dist/artifacts/evm/index.d.cts +2 -2
  5. package/dist/artifacts/evm/index.d.ts +2 -2
  6. package/dist/artifacts/evm/index.js +1 -1
  7. package/dist/artifacts/svm/index.cjs +1 -1
  8. package/dist/artifacts/svm/index.d.cts +3 -3
  9. package/dist/artifacts/svm/index.d.ts +3 -3
  10. package/dist/artifacts/svm/index.js +1 -1
  11. package/dist/{container-provider-CBBJbpUn.d.ts → container-provider-D5bLhd__.d.ts} +4 -2
  12. package/dist/{container-provider-COvXmCLM.d.cts → container-provider-DObyv4EW.d.cts} +4 -2
  13. package/dist/{evm-V5Fn53uW.d.cts → evm-CJqPjHHu.d.cts} +1 -1
  14. package/dist/{evm-Dk4ceFz0.d.ts → evm-Yd0NCJAL.d.ts} +1 -1
  15. package/dist/{file-sink-BjbtAHUL.d.cts → file-sink-D8l-OwW9.d.cts} +2 -2
  16. package/dist/{file-sink-Bf9mpegF.d.ts → file-sink-GrtMBpmU.d.ts} +2 -2
  17. package/dist/index.cjs +1 -1
  18. package/dist/index.d.cts +67 -7
  19. package/dist/index.d.ts +67 -7
  20. package/dist/index.js +1 -1
  21. package/dist/index.node.cjs +1 -1
  22. package/dist/index.node.d.cts +6 -6
  23. package/dist/index.node.d.ts +6 -6
  24. package/dist/index.node.js +1 -1
  25. package/dist/node/index.cjs +1 -1
  26. package/dist/node/index.d.cts +3 -3
  27. package/dist/node/index.d.ts +3 -3
  28. package/dist/node/index.js +1 -1
  29. package/dist/{svm-CjNIJZ4l.d.cts → svm-DgxTJqrR.d.ts} +3 -2
  30. package/dist/{svm-D8zsWS64.d.ts → svm-qBfrN98x.d.cts} +3 -2
  31. package/dist/{workspace-DqUdScht.d.cts → workspace-CZO-ANYP.d.cts} +265 -16
  32. package/dist/{workspace-DqUdScht.d.ts → workspace-CZO-ANYP.d.ts} +265 -16
  33. package/examples/14-esm-runtime.test.mjs +0 -3
  34. package/examples/17-locked-state.test.cjs +1 -0
  35. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to `wative-core` are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
4
 
5
+ ## [2.5.3] — 2026-09-19
6
+
7
+ Adds foreign-transaction signing, sign-in, a keyless chain client, and Solana versioned transactions.
8
+
9
+ ### Added
10
+ - Sign a transaction you did not build with `address.signRawTransaction(...)` (behind a confirm step, with explicit opt-in for the riskiest kinds), preview one with `address.estimate(...)`, sign in to a site with `address.signIn(...)`, and sign several at once with `address.signAll(...)`.
11
+ - `ChainClient` reads balances, simulates, and broadcasts on a network with no account or key; `address.checkDestination(...)` reports whether a recipient is safe, and transfers now refuse an irreversible-loss recipient unless you override.
12
+ - Solana transactions support address lookup tables and versioned (v0) transactions; `send(...)` takes per-send options — confirmation level, timeout, and endpoint route.
13
+
14
+ ### Changed
15
+ - `Contract` and `Program` are no longer added to a shared registry on construction; `Contract.load(name)` / `Program.load(name)` return the built-ins only.
16
+
17
+ ## [2.5.2] — 2026-09-18
18
+
19
+ Adds two methods for signing bytes you built yourself.
20
+
21
+ ### Added
22
+ - `address.signDigest(digest)` signs a 32-byte digest on an EVM address exactly as given, with no hashing and no prefix. It returns `r`, `s`, the recovery id (0 or 1), and a 65-byte `signature` with `v = 27 + recovery`. Build `v` from `recovery` for a typed or EIP-155 transaction.
23
+ - `address.signBytes(message)` signs arbitrary bytes on an SVM address exactly as given, for example a serialized Solana transaction message. It returns the 64-byte signature as base58 and as hex.
24
+ - Both take a `Uint8Array` or a `0x`-prefixed hex string of whole bytes, need the account unlocked, and sign whatever they are given, so pass only data you assembled yourself.
25
+
5
26
  ## [2.5.1] — 2026-09-12
6
27
 
7
28
  Renames the transfer helper and improves fee handling.
@@ -743,67 +764,54 @@ Unlocking is much faster. Drop-in from 2.2.x; wallets from 1.x need a manual ste
743
764
 
744
765
  ## [2.2.1] — 2026-07-29
745
766
 
746
- Documentation and packaging only. `dist/` is byte-identical to 2.2.0 — no source changed, so the published build was deliberately left exactly as built and verified for that release.
767
+ Documentation and packaging only. The code is identical to 2.2.0.
747
768
 
748
769
  ### Changed
749
- - **The runnable examples ship as `examples/` instead of `tests/`.** They are the only human-readable code in the published package, and `tests/` reads as internal scaffolding nobody is invited to open. The `files` allowlist follows. Consumers who referenced `wative-core/tests/...` by path must update; nothing importable from the package entry changed.
750
- - **README** gains npm / CI / node / license badges, an ASCII diagram of the containment model (`Workspace > Account > Wallet > Address`), and a section distinguishing the two account kinds. The HD/PK difference is not in the nesting but in what a `Wallet` *means* — a derivation slot holding both an EVM and a Solana key under HD, versus a single imported key on one chain under PK. That was previously described in prose only, and it is the thing new users get wrong first.
770
+ - **The runnable examples ship as `examples/` instead of `tests/`.** If you referenced `wative-core/tests/...` by path, update it; nothing importable from the package entry changed.
771
+ - **README** gains npm / CI / node / license badges, a diagram of how `Workspace > Account > Wallet > Address` nest, and a section on the two account kinds. Under HD a `Wallet` is a derivation slot holding both an EVM and a Solana key; under PK it is a single imported key on one chain.
751
772
 
752
773
  ### Fixed
753
- - **`11-subpath-imports` example** imported the chain artifacts by relative path into `dist/` rather than by subpath, despite its own header documenting the subpath form. It resolved inside the repo and nowhere else, so it passed in-repo while being broken for every real installation. Caught by the new published-package CI job before release.
754
-
755
- ### Internal
756
- - `.github/workflows/ci.yml`: runs the examples against this build on Node 22.12 and 24, and separately installs `wative-core` from the registry into a clean directory and runs the same files against it — exercising the real tarball's `files` list, `exports` map and dependency resolution. Runs weekly as well, since a published package sits on a moving dependency graph.
757
- - The private development repo is now guarded against reaching npm (`private`, a `prepublishOnly` refusal, and a `publishConfig` registry pointing nowhere). It shares a package name with the public repo, so a stray publish there would have succeeded and replaced the package.
774
+ - **The `11-subpath-imports` example** now imports the chain artifacts through their subpaths, as its header documents. It previously failed in a real installation.
758
775
 
759
776
  ## [2.2.0] — 2026-07-28
760
777
 
761
- > **The browser entry is here.** The main entry now resolves for a browser with **zero** unresolvable Node builtins, and ships an IndexedDB storage backend. **No public API was removed** — existing Node imports keep working unchanged, so this remains a minor.
778
+ > **The browser entry is here.** The main entry now bundles for a browser with **zero** unresolvable Node builtins, and ships an IndexedDB storage backend. **No public API was removed** — existing Node imports keep working unchanged, so this remains a minor.
762
779
 
763
780
  ### Added
764
- - **`IdbProvider` — IndexedDB storage backend.** Created via the async factory `IdbProvider.create(name, opts)`, which negotiates durable storage before returning. Exposes `durability` (`"persistent" | "best-effort"`) and a `quota` snapshot.
765
- - **`ContainerProvider`** — the storage-agnostic base extracted from `HybridProvider`. A custom backend now implements only six primitives (`_exist`, `_listItems`, `_read`, `_write`, `_remove`, `_ensureDir`) and inherits record framing, the encrypted envelope and its identity AAD, the key layout and the container session. Previously `Provider` declared the record API abstract, so a custom provider had to reimplement encryption — which is why the shipped custom-provider example carried a hand-rolled XOR cipher and a warning to use a real one.
766
- - **`exportContainer()` / `importContainer()`** on `ContainerProvider`. Both require an unlocked workspace and throw `WORKSPACE_LOCKED` otherwise — a precondition that was not stated when they shipped. Sealed records move verbatim — never decrypted, so no key is materialized and nothing is re-keyed. Because every backend shares one framing, a container round-trips IndexedDB ⇄ filesystem and opens with the same password. Import refuses a non-empty target without `{ overwrite: true }` and rejects entries outside the container layout.
781
+ - **`IdbProvider` — IndexedDB storage backend.** Created via the async factory `IdbProvider.create(name, opts)`, which requests durable storage before returning. Exposes `durability` (`"persistent" | "best-effort"`) and a `quota` snapshot.
782
+ - **`ContainerProvider`** — a base class for custom storage backends. A backend implements six methods (`_exist`, `_listItems`, `_read`, `_write`, `_remove`, `_ensureDir`) and inherits encryption and the workspace layout, so it no longer has to implement encryption itself.
783
+ - **`exportContainer()` / `importContainer()`** on `ContainerProvider`. Both require an unlocked workspace and throw `WORKSPACE_LOCKED` otherwise. Records move still encrypted and are never decrypted on the way. A container exported from one backend imports into another (IndexedDB ⇄ filesystem) and opens with the same password. Import refuses a non-empty target without `{ overwrite: true }` and rejects entries that do not belong to a container.
767
784
  - **`wative-core/node` subpath** — `HybridProvider`, `FileSink` and the default-workspace-path resolver.
768
785
  - **`STORAGE_NOT_DURABLE`** error code.
769
786
 
770
787
  ### Changed
771
- - **The root entry is condition-split.** A browser bundler resolves the universal core; Node resolves an entry that still re-exports `HybridProvider` and `FileSink`, so `import { Workspace, HybridProvider } from "wative-core"` compiles and runs exactly as before. Those two re-exports are **deprecated** in favour of `wative-core/node` and will be removed in a future major. Condition order is significant — `browser` is declared before `node`, since resolvers take the first match.
772
- - `tsconfig` `lib` gains `DOM`; the browser is now a real target.
788
+ - **The root entry has separate browser and Node builds.** A browser bundler picks the browser build; Node picks one that still re-exports `HybridProvider` and `FileSink`, so `import { Workspace, HybridProvider } from "wative-core"` compiles and runs exactly as before. Those two re-exports are **deprecated** in favour of `wative-core/node` and will be removed in a future major.
773
789
 
774
790
  ### Browser notes
775
791
  - Creating a **new** workspace in evictable storage is **refused** with `STORAGE_NOT_DURABLE` unless `{ acknowledgeEvictionRisk: true }` is passed. Opening an existing workspace is never blocked. Browser storage is evictable and this library holds private keys: Chrome and Firefox treat IndexedDB as best-effort unless `navigator.storage.persist()` is granted, Safari caps script-writable storage at roughly 7 days without user interaction, and private-mode windows may have no IndexedDB at all. Request persistence from a user gesture, and use `exportContainer()` for a durable backup.
776
792
  - A **`Buffer` polyfill is still required** — `@solana/web3.js` and `@coral-xyz/anchor` read the global. Standard for any Solana dapp.
777
793
 
778
- ### Internal
779
- - `scripts/browser-e2e.mjs` (`pnpm test:browser`) bundles the real dist through the `browser` condition, loads it into headless Chromium and drives a full lifecycle against the browser's own IndexedDB, asserting the same published BIP-44 vectors the Node suite pins. Resolving is not the same as running — a build can resolve cleanly and still fail at runtime, as a 2.1.0 regression showed. Wired into `prepublishOnly`.
780
- - The published bundle is exercised under each platform to prove the export map lands where intended; asserting its shape is not the same as asserting a resolver's behaviour.
781
-
782
794
  ## [2.1.0] — 2026-07-28
783
795
 
784
- > **Browser groundwork.** This release removes every Node-builtin dependency from the crypto and EVM layers. `wative-core/artifacts/evm` and `wative-core/artifacts/svm` now bundle for the browser with **zero** unresolvable Node builtins; the main entry is down to four (`fs`, `fs/promises`, `os`, `path`), all belonging to the filesystem provider. **No public API changed** — this is a drop-in upgrade from 2.0.7.
796
+ > **Browser groundwork.** `wative-core/artifacts/evm` and `wative-core/artifacts/svm` now bundle for the browser with **zero** unresolvable Node builtins; the main entry is down to four (`fs`, `fs/promises`, `os`, `path`), all used by the filesystem storage backend. **No public API changed** — this is a drop-in upgrade from 2.0.7.
785
797
 
786
798
  ### License
787
799
  - **Relicensed to a Modified MIT License** (previously BUSL-1.1). The library is now free for any use, including commercial. The sole added condition: a product or service generating more than 50,000 USD in monthly revenue must prominently display "Wative" on its user interface. Measured per product, not per company. `package.json` declares `"license": "SEE LICENSE IN LICENSE"`, the npm convention for a non-standard licence.
788
800
 
789
801
  ### Changed
790
- - **`web3` 1.7.3 and `ethereumjs-wallet` 1.0.2 replaced by `ethers` 6.** Both were used only for offline work — a provider-less `new Web3()` for the ABI codec and transaction/message signing, and `ethereumjs-wallet` solely for BIP-44 derivation. All RPC already went through `fetch`. web3's transitive tree (`web3-providers-http`/`ws`, `xhr2-cookies`, `readable-stream`, `cipher-base`, `ethereumjs-util`, `@ethereumjs/common`) pulled in `http`, `https`, `stream`, `events`, `url`, `assert` and `os`, which alone made the package impossible to bundle for a browser. web3 1.x is also end-of-life.
791
- - **Crypto core moved from `node:crypto` to `@noble`.** AES-256-GCM now runs on `@noble/ciphers`; PBKDF2, HMAC and CSPRNG bytes on `@noble/hashes` (Argon2id already used `@noble/hashes`). Every primitive was verified byte-identical against `node:crypto` before the switch. **`Cipher.encrypt`/`decrypt` remain synchronous** — that is why `@noble` was chosen over WebCrypto, whose async-only `SubtleCrypto` would have forced the whole `Cipher` contract async and made this a breaking change.
792
- - **`node:net.isIP` replaced by a pure-JS classifier**, differential-tested against `node:net.isIP` over ~600 valid, malformed and adversarial inputs.
793
- - **Wire format, key derivation and on-disk layout are unchanged.** A workspace written by 2.0.7 opens under 2.1.0 with byte-identical results, and the production KDF parameters (PBKDF2 600 000 iterations; Argon2id t=3, m=64 MiB, p=1) are pinned by test.
802
+ - **`web3` 1.7.3 and `ethereumjs-wallet` 1.0.2 replaced by `ethers` 6.** Their dependencies pulled in Node builtins that kept the package from bundling for a browser, and web3 1.x is end-of-life.
803
+ - **Encryption and hashing no longer depend on `node:crypto`**, so they run in the browser. Results are byte-identical, and `Cipher.encrypt`/`decrypt` stay synchronous.
804
+ - **IP-address validation no longer depends on `node:net`.**
805
+ - **Workspaces are unchanged.** A workspace written by 2.0.7 opens under 2.1.0, and derived addresses and signatures are identical.
794
806
 
795
807
  ### Fixed
796
- - **EIP-712 with an empty `bytes` field no longer crashes.** web3's `keccak256("0x")` returned `null` and broke the signer; empty bytes now hash to the canonical empty-input digest, which is what EIP-712 specifies.
797
- - **Intrinsic-gas validation preserved.** web3 rejected an under-funded `gasLimit` before signing and ethers does not, so the check is reimplemented — a transaction that can never execute is no longer signed and broadcast, burning the nonce.
808
+ - **EIP-712 with an empty `bytes` field no longer crashes**; it now signs as EIP-712 specifies.
809
+ - **A `gasLimit` too low for the transaction is still refused before signing**, so a transaction that can never execute is not broadcast.
798
810
 
799
811
  ### Notes for browser consumers
800
812
  - `wative-core/artifacts/evm` and `wative-core/artifacts/svm` bundle cleanly today (the EVM artifact is ~128 KB min+gzip).
801
813
  - A **`Buffer` polyfill is still required**, because `@solana/web3.js` and `@coral-xyz/anchor` depend on the global. This is standard for any Solana dapp.
802
- - The main entry still requires the filesystem builtins listed above. Splitting the Node-only surface (`HybridProvider`, `FileSink`) behind a `wative-core/node` subpath, and adding an IndexedDB provider, is planned for 2.2.0.
803
-
804
- ### Internal
805
- - `scripts/check-browser-bundle.mjs` bundles every published entry for `platform=browser` and ratchets the set of unresolvable Node builtins — failing both on a regression and on a stale baseline, so browser compatibility can only improve. Wired into `prepublishOnly`.
806
- - Known-answer vectors (`tests/kat/`) pin exact bytes for HD derivation, EIP-191/712 signing, ed25519, the ABI codec, all three EVM transaction types, both KDFs and the envelope wire format; a committed v2.0.7 workspace fixture is opened on every run.
814
+ - The main entry still requires the filesystem builtins listed above. A `wative-core/node` subpath for the Node-only parts (`HybridProvider`, `FileSink`) and an IndexedDB storage backend are planned for 2.2.0.
807
815
 
808
816
  ## [2.0.7] — 2026-07-21
809
817
 
@@ -811,44 +819,43 @@ Documentation and packaging only. `dist/` is byte-identical to 2.2.0 — no sour
811
819
 
812
820
  ### Added
813
821
  - **`Workspace.open()` options object.** `open()` now accepts `{ provider?, path?, password? }` alongside the existing positional form, so the default location no longer needs an `undefined` placeholder: `await Workspace.open({ password })`. Passing both `provider` and `path` is rejected.
814
- - **Automatic create-vs-open with folder validation.** `open()` decides for itself: an existing workspace is opened, an empty or absent location is created, and a **non-empty location that is not a wative workspace is refused** — so a mistyped path can no longer write a workspace into unrelated files. Backed by a new overridable `Provider.inspectContainer()`; `HybridProvider` implements the filesystem check and the base class defaults to permissive, leaving custom providers unaffected.
822
+ - **Automatic create-vs-open with folder validation.** `open()` decides for itself: an existing workspace is opened, an empty or absent location is created, and a **non-empty location that is not a wative workspace is refused** — so a mistyped path can no longer write a workspace into unrelated files. Custom providers can override `Provider.inspectContainer()`; by default it accepts any location, so existing custom providers are unaffected.
815
823
 
816
824
  ### Changed
817
825
  - **`create` parameter is deprecated and ignored.** The third positional argument to `open()` still type-checks so existing calls compile, but create-vs-open is now automatic. The former `"Workspace is not initialized. Pass create=true"` error is gone.
818
- - **Dependencies.** `@solana/web3.js` 1.91.8 → **1.98.4**. The previous pin resolved `rpc-websockets@^7.11.0` to 7.11.2, which no longer ships the `dist/lib/client` entry that 1.91.8 deep-requires — leaving the published package unloadable under plain Node. 1.98.4 requires the package root instead. Its dependency chain reaches an ESM-only `uuid`, which is why the minimum Node version moves to **22.12** (the first release where `require()` of an ES module is supported).
826
+ - **Dependencies.** `@solana/web3.js` 1.91.8 → **1.98.4**. With 1.91.8 the published package could no longer be loaded under plain Node after a change in one of its dependencies. 1.98.4 needs Node **22.12** or later, which is why the minimum Node version moves.
819
827
 
820
828
  ### Fixed
821
829
  - **Signing.** `signTypedData` rejects a malformed `domain.chainId` with `PARAMETER_ERROR` instead of leaking a raw `SyntaxError`.
822
- - **Transactions.** EIP-1559 auto-fill clamps a defaulted priority fee to a user-supplied sub-gwei `maxFeePerGas` (previously produced an invalid tip-over-cap pair); receipt status is compared numerically so a non-canonical `0x01` is no longer misread as a revert; an auto-estimated `gasLimit` gets a 1.2x safety buffer.
830
+ - **Transactions.** EIP-1559 auto-fill clamps a defaulted priority fee to a user-supplied sub-gwei `maxFeePerGas` (previously produced an invalid tip-over-cap pair); a receipt that reports its status as `0x01` is no longer misread as a revert; an auto-estimated `gasLimit` gets a 1.2x safety buffer.
823
831
  - **Transaction tracker.** `abort()` now wakes pending `whenMined`/`whenConfirmed`/`whenFinalized` awaiters instead of hanging them forever; a failed send rejects those awaiters with the real cause rather than a misleading `TX_TIMEOUT`; `whenConfirmed(n)` with `n > 1` fails loudly instead of silently resolving at first inclusion.
824
832
  - **Solana.** Anchor instruction accounts now take their signer/writable flags from the IDL instead of being forced to writable non-signer; `memo` is encoded as a real Memo-program instruction; `addressLookupTables` are rejected rather than silently dropped.
825
- - **Workspace.** A network can no longer be dropped while a user-asset still references it, and the cross-queue race that could orphan an asset or an account's default network is closed.
826
- - **Accounts & wallets.** A forced `kind: "HD"` now validates the mnemonic checksum at create time; wallet tag read-modify-write runs inside the account mutation queue; invisible Hangul filler characters are rejected in validated text; `formatUnits` no longer emits a misleading `"-0"`.
833
+ - **Workspace.** A network can no longer be dropped while a user-asset still references it, and concurrent changes can no longer leave an asset or an account's default network pointing at a network that is gone.
834
+ - **Accounts & wallets.** A forced `kind: "HD"` now validates the mnemonic checksum at create time; concurrent tag changes on one wallet no longer overwrite each other; invisible Hangul filler characters are rejected in validated text; `formatUnits` no longer emits a misleading `"-0"`.
827
835
 
828
836
  ### Packaging
829
- - `exports` carries per-condition `types`, so CJS TypeScript consumers resolve the `.d.cts` declarations.
830
- - The `files` allowlist no longer ships the internal `tests/` directory, which had been leaking TypeScript sources into the tarball.
837
+ - CommonJS TypeScript projects now resolve the `.d.cts` declarations.
838
+ - The package no longer includes a `tests/` directory.
831
839
 
832
840
  ## [2.0.5] — 2026-05-04
833
841
 
834
842
  ### Added
835
843
  - **`EvmTransaction.toRawTx()` and `SvmTransaction.toRawTx()`.** Hand-off accessors that return the lowest-level transaction representation each chain ecosystem already understands, so callers can route a wative-core transaction through any external signer / RPC client without re-deriving the inputs.
836
844
  - **`EvmTransaction.toRawTx(): EvmRawTx`** — synchronous. Returns a plain `{ from, to, value, data, type, chainId, nonce?, gasLimit?, gasPrice?, maxFeePerGas?, maxPriorityFeePerGas?, accessList? }` object matching the `web3.js` / `ethers` / `viem` `TransactionRequest` shape. Unset autofill fields are simply omitted, leaving the consumer's provider to fill them in. Does not sign or hit the network.
837
- - **`SvmTransaction.toRawTx(): Promise<unknown>`** — async. Returns the underlying `@solana/web3.js` `Transaction` instance with `instructions`, `feePayer`, and `recentBlockhash` populated. The build is cached, so repeated calls return the same instance and a subsequent `.sign()` reuses it.
838
- - **Build-requirement relaxation on the SVM side.** `SvmTransaction` no longer requires a bound `Address` purely to construct the underlying transaction object — if `recentBlockhash` is supplied at construction, `toRawTx()` builds entirely structurally. An Address is still required when blockhash needs to be fetched over RPC.
845
+ - **`SvmTransaction.toRawTx(): Promise<unknown>`** — async. Returns the underlying `@solana/web3.js` `Transaction` instance with `instructions`, `feePayer`, and `recentBlockhash` populated. Repeated calls return the same instance, and a subsequent `.sign()` reuses it.
846
+ - **`SvmTransaction` no longer needs an `Address` just to build.** If `recentBlockhash` is supplied at construction, `toRawTx()` builds without one. An Address is still required when the blockhash has to be fetched over RPC.
839
847
  - **`EvmRawTx`** type exported from the package entry.
840
848
 
841
849
  ### Changed
842
- - **Cross-OS IO hardening pass.** The `HybridProvider` file backend now applies the Windows `\\?\` long-path prefix when a resolved path crosses 240 chars, canonicalizes the workspace root via `fs.realpath` at unlock, and refuses to traverse symlinked ancestors at write time. Atomic-write temp files use a 64-bit random suffix (was 48-bit) and retry on `EEXIST`; orphan `*.tmp.<rand>` files older than 60 s are swept on first unlock. Directory `fsync` is gated to POSIX. The logger `chmod`s pre-existing log files to `0o600` after open so they don't inherit permissive bits from a prior run under a different `umask`.
843
- - **`tweetnacl` is now loaded behind a lazy singleton.** Matches the existing `web3.js` / `bs58` lazy-load pattern; first call loads, subsequent calls reuse.
844
- - **Per-process KDF cache HMAC secret is lazy-initialized.** No `crypto.randomBytes` call at module load time, removing one pre-condition for browser/Edge runtimes.
850
+ - **More robust file storage across operating systems.** `HybridProvider` handles long Windows paths, refuses to write through a symlinked parent directory, and cleans up temporary files left behind by an interrupted write. Log files that already exist are made owner-only (`0600`) when opened.
851
+ - **Importing the package no longer generates random bytes at load time**, which some edge runtimes forbid.
845
852
 
846
853
  ### Security
847
- - **Passwords are normalized to NFC at the KDF boundary.** Clipboard managers and OS input methods occasionally hand back equivalent strings in different Unicode normalization forms; without normalization, a workspace sealed on one host would refuse to unlock on another. ASCII passwords are unaffected.
848
- - **`TextDecoder` calls now use `{ fatal: true }`** in every record-payload, recovery-envelope, and decoded-bytes path. Malformed UTF-8 surfaces as `DECRYPT_FAILED` instead of silently substituting `U+FFFD`.
849
- - **Account slug reserved-name lookup is case-insensitive.** Belt-and-suspenders over the slug regex (which already enforces lowercase output) — Windows resolves `CON`/`con`/`Con` to the same device, so reserved-name matching now follows the same rule explicitly.
854
+ - **Passwords are normalized to NFC.** Clipboard managers and OS input methods occasionally hand back equivalent strings in different Unicode normalization forms; without normalization, a workspace created on one host would refuse to unlock on another. ASCII passwords are unaffected.
855
+ - **Malformed text in stored data fails with `DECRYPT_FAILED`** instead of being silently replaced with `U+FFFD`.
856
+ - **Reserved account names are matched case-insensitively.** Windows resolves `CON`/`con`/`Con` to the same device, so all three are reserved.
850
857
  - **Workspace path resolver throws clearly when no home is available.** Containers and jails without `HOME` / `USERPROFILE` previously fell through to a malformed path; now `Workspace.open()` surfaces a `PARAMETER_ERROR` directing the caller to set `WATIVE_WORKSPACE_PATH` or pass an explicit path.
851
- - **`ENAMETOOLONG` on the on-disk path now returns `PROVIDER_IO`** with an actionable "relocate workspace closer to root" message, distinguishing OS-limit overflow from invalid input (slug length is already capped by the validator).
858
+ - **A workspace path too long for the OS now returns `PROVIDER_IO`** with an actionable "relocate workspace closer to root" message.
852
859
 
853
860
  ## [2.0.3] — 2026-05-04
854
861
 
@@ -856,16 +863,15 @@ Documentation and packaging only. `dist/` is byte-identical to 2.2.0 — no sour
856
863
  - **Default workspace path renamed** from `<cwd>/.wative` to `<cwd>/.wative2`. Existing on-disk workspaces under `.wative/` are not migrated automatically — pass the old path explicitly to `Workspace.open()` if you want to keep using one, or rename the directory.
857
864
  - **`Workspace.open()` no-arg path now resolves through a 3-tier strategy:**
858
865
  1. `WATIVE_WORKSPACE_PATH` env var (if set & non-empty) — wins unconditionally. `~` is expanded.
859
- 2. `<process.cwd()>/.wative2` — used when the env var is unset and the directory already exists on disk. Symlinks at this path are refused (fall through to home) to defend against attacker-controlled redirection on shared/multi-user systems.
866
+ 2. `<process.cwd()>/.wative2` — used when the env var is unset and the directory already exists on disk. A symlink at this path is refused and resolution falls through to the home directory.
860
867
  3. `<os.homedir()>/.wative2` — last-resort fallback. Where `create=true` lands on a fresh machine when no env var is set and there's no project-local `.wative2/`.
861
868
 
862
869
  Calls that pass an explicit path or `Provider` instance are unaffected — those short-circuit before any resolution.
863
870
 
864
871
  ### Security
865
- - **Envelope v2 — Argon2id + AAD-bound on-disk records.** New seals use Argon2id (RFC 9106; t=3, m=64 MiB, p=1) and bind every record's GCM tag to its on-disk identity. HybridProvider records bind to `wative:v2:record:<recordType>:<slug>`; per-account mnemonic ciphertexts bind to `wative:v2:account:<slug>:mnemonic`; per-address private-key ciphertexts bind to `wative:v2:account:<slug>:wallet:<id>:<vm>:pk`; recovery envelopes bind to `wative:v2:recovery:account:<slug>`. Closes the blob-swap class of attack where an attacker with workspace-dir write access (but not the password) could substitute one record's ciphertext for another's. Legacy v1 records (PBKDF2-SHA256, no AAD) continue to read; new writes are always v2.
866
- - **Slug-collision suffix now CSPRNG.** Was `Math.random()`; now `crypto.randomInt`. Prevents predictable filenames in `accounts/<slug>-<suffix>.db`.
867
- - **The derived seed is zeroed on lock.** `Account.lock()` now overwrites the BIP-39 seed bytes before releasing them. JS string immutability still prevents perfect zeroization of mnemonic + plaintext private keys (documented on `Workspace.lock()`).
868
- - **`useTestKdfIterations` Jest auto-detect removed.** The `typeof globalThis.expect !== "undefined"` opt-in arm fired in any downstream consumer using Jest, not just our own test process. Now requires explicit `WATIVE_ALLOW_TEST_KDF=1` (or `NODE_ENV=test`). Internal-only function; not on the package surface.
872
+ - **Passwords are stretched with Argon2id, and stored records can no longer be swapped.** Each stored record is now bound to where it lives, so someone who can write to the workspace folder but does not know the password can no longer substitute one record's contents for another's. Workspaces written by 2.0.1 still open; everything written from this release on uses the new format.
873
+ - **The suffix added to a duplicate account name is now unpredictable.**
874
+ - **`Account.lock()` now wipes the derived seed from memory.** Mnemonics and private keys held as JavaScript strings cannot be wiped the same way (see `Workspace.lock()`).
869
875
 
870
876
  ## [2.0.1] — 2026-04-29
871
877
 
@@ -885,12 +891,12 @@ First release of the v2 line.
885
891
 
886
892
  ### Security
887
893
  - Built-in networks and tokens cannot be dropped from the workspace (they would orphan accounts that reference them by default).
888
- - Concurrent `addAsset` / `dropAsset` and network-collection mutations are serialized — no duplicate ids or slugs from interleaved calls.
889
- - Hydrate-time guards reject corrupted records (duplicate asset ids or network slugs) at workspace open.
894
+ - Concurrent `addAsset` / `dropAsset` and network changes can no longer produce duplicate asset ids or network slugs.
895
+ - A workspace whose stored data holds duplicate asset ids or network slugs fails to open with an error instead of loading.
890
896
 
891
897
  ### Notes
892
- - Requires Node.js 18.18+. The crypto and address-encoding paths are pure-JS via `@noble/hashes`, `@noble/curves`, and `bs58` — no native gyp build is needed for installation. Some chain helpers (`web3@1.7.x`, `@solana/web3.js`) and a few transitive deps (`ethereumjs-wallet`, `tweetnacl`) currently target Node and are loaded lazily on first use; full Deno / Bun / browser support is partial pending those upgrades.
893
- - Source maps are not published. The npm package contains only compiled JavaScript and TypeScript declaration files. TypeScript source lives in a separate private repository.
898
+ - Requires Node.js 18.18+. Installation needs no native build step. Deno, Bun and browser support is partial.
899
+ - Source maps are not published. The npm package contains only compiled JavaScript and TypeScript declaration files.
894
900
 
895
901
  [2.0.7]: https://github.com/braady/wative-core/releases/tag/v2.0.7
896
902
  [2.0.3]: https://github.com/braady/wative-core/releases/tag/v2.0.3
package/README.md CHANGED
@@ -477,7 +477,7 @@ const tx = evmAddr.buildTransaction({
477
477
  value: 1_000_000_000_000_000n,
478
478
  chainId: 1,
479
479
  });
480
- await evmAddr.signTransaction(tx);
480
+ await tx.sign(); // resolves once the signature exists
481
481
  const tracker = await evmAddr.sendTransaction(tx);
482
482
 
483
483
  tracker.on("change", (state) => console.log("state →", state.status));
@@ -491,6 +491,8 @@ const sim = await evmAddr.simulateTransaction(tx);
491
491
  console.log(sim.gasUsed);
492
492
  ```
493
493
 
494
+ `address.signTransaction(tx)` returns straight away and does not wait for the signature. To use the signed bytes (`tx.rawTransaction`, `tx.hash`), `await tx.sign()` first — it resolves once signing is done and is safe to call more than once.
495
+
494
496
  `buildTransferPayload()` is the one-call path for moving value — it works out the right payload for the asset (native value, ERC-20 calldata, or SPL instructions). You then pass that payload to `buildTransaction()` and send it like any other transaction:
495
497
 
496
498
  ```ts
@@ -503,11 +505,32 @@ const bySymbol = evmAddr.buildTransferPayload({ to: recipient, asset: { symbol:
503
505
 
504
506
  // build the transaction from the payload, then sign/send it
505
507
  const tx = evmAddr.buildTransaction({ ...nativePayload, chainId: 1 });
506
- await evmAddr.signTransaction(tx);
508
+ await tx.sign();
507
509
  ```
508
510
 
509
511
  Amounts are in the token's smallest unit. `buildTransaction` fills in nonce/gas/fees for you when you omit them, or uses the values you pass. An SPL transfer to a recipient without a token account prepends the account-creation step for you.
510
512
 
513
+ #### Signing bytes you built yourself
514
+
515
+ If you build and broadcast transactions yourself and only need a signature, two methods sign exactly the bytes you give them. Neither hashes its input, adds a prefix, or touches the network.
516
+
517
+ ```ts
518
+ // EVM: sign a 32-byte digest you computed (for example the keccak of an encoded transaction)
519
+ const { r, s, recovery, signature } = evmAddr.signDigest("0x" + "ab".repeat(32));
520
+ // r, s: 32-byte hex (s is low); recovery: 0 or 1; signature: 65 bytes, v = 27 + recovery
521
+
522
+ // SVM: sign arbitrary bytes, for example a serialized Solana transaction message
523
+ const { signature: sig58, signatureHex } = svmAddr.signBytes(messageBytes);
524
+ ```
525
+
526
+ - Both take a `Uint8Array` or a `0x`-prefixed hex string of whole bytes. Base64 and hex without the prefix are refused; decode base64 yourself first.
527
+ - `signDigest` requires exactly 32 bytes.
528
+ - `signBytes` accepts any length, including binary data that `signMessageEncoded(..., "ed25519")` cannot carry, because that method takes text.
529
+ - The 65-byte `signature` uses the message-signing convention for `v`. For a typed transaction use `recovery` as the y-parity, and for a legacy EIP-155 transaction use `35 + 2 * chainId + recovery`.
530
+ - `signDigest` works only on an EVM address and `signBytes` only on an SVM address. Each needs the account unlocked.
531
+
532
+ > ⚠️ These sign anything you hand them, with none of the checks `signTypedData` applies. Only pass digests and bytes you assembled yourself.
533
+
511
534
  ### Network — pre-loaded networks + your own
512
535
 
513
536
  ```ts
@@ -581,7 +604,8 @@ const evmTx = new EvmTransaction({
581
604
  maxFeePerGas: 50_000_000_000n,
582
605
  maxPriorityFeePerGas: 1_000_000_000n,
583
606
  });
584
- await evmAddr.signTransaction(evmTx);
607
+ evmAddr.signTransaction(evmTx); // checks `from` and attaches the transaction to this address
608
+ await evmTx.sign(); // resolves once the signature exists
585
609
  const evmTracker = await evmAddr.sendTransaction(evmTx);
586
610
 
587
611
  const receipt = await evmTracker.whenConfirmed();