wative-core 2.4.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/README.md +127 -91
- package/dist/artifacts/evm/index.cjs +1 -1
- package/dist/artifacts/evm/index.d.cts +2 -2
- package/dist/artifacts/evm/index.d.ts +2 -2
- package/dist/artifacts/evm/index.js +1 -1
- package/dist/artifacts/svm/index.cjs +1 -1
- package/dist/artifacts/svm/index.d.cts +3 -3
- package/dist/artifacts/svm/index.d.ts +3 -3
- package/dist/artifacts/svm/index.js +1 -1
- package/dist/{container-provider-D2QluIe_.d.ts → container-provider--npoeR2N.d.ts} +1 -1
- package/dist/{container-provider-DAUXclw_.d.cts → container-provider-Ca_5z1uQ.d.cts} +1 -1
- package/dist/{evm-GxBuNjq7.d.ts → evm-EHpaW_sK.d.ts} +1 -1
- package/dist/{evm-qXdZWugm.d.cts → evm-hSJJOVG3.d.cts} +1 -1
- package/dist/{file-sink-DSOZbsJM.d.cts → file-sink-7ZAutH45.d.cts} +2 -2
- package/dist/{file-sink-B0EjCBvG.d.ts → file-sink-Bk5zYHw-.d.ts} +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +1 -1
- package/dist/index.node.cjs +1 -1
- package/dist/index.node.d.cts +5 -5
- package/dist/index.node.d.ts +5 -5
- package/dist/index.node.js +1 -1
- package/dist/node/index.cjs +1 -1
- package/dist/node/index.d.cts +3 -3
- package/dist/node/index.d.ts +3 -3
- package/dist/node/index.js +1 -1
- package/dist/{svm-Ca4Xyw0Y.d.ts → svm-B5q8YaV9.d.ts} +1 -1
- package/dist/{svm-B_c0L-rJ.d.cts → svm-CXFwmeMo.d.cts} +1 -1
- package/dist/{workspace-DdaY7sWa.d.cts → workspace-kq3QyAgm.d.cts} +46 -9
- package/dist/{workspace-DdaY7sWa.d.ts → workspace-kq3QyAgm.d.ts} +46 -9
- package/examples/15-package-resolution.test.cjs +4 -4
- package/examples/19-simple-provider.test.cjs +134 -0
- package/examples/README.md +10 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,55 @@
|
|
|
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.4.1] — 2026-08-11
|
|
6
|
+
|
|
7
|
+
One breaking item, and it is the kind worth reading before upgrading: a
|
|
8
|
+
transaction no longer guesses which chain it is for. Alongside it, a browser and
|
|
9
|
+
a Node process now each get a working storage backend without being told which
|
|
10
|
+
one to use, and writing your own backend is documented with a runnable example.
|
|
11
|
+
|
|
12
|
+
### Security
|
|
13
|
+
- **BREAKING. `EvmTransaction` requires `chainId` and no longer defaults to
|
|
14
|
+
Ethereum mainnet.** Building a transaction without naming a chain used to
|
|
15
|
+
produce chain 1. That is not a harmless default: a signed transaction is a
|
|
16
|
+
bearer instrument, so the result was a genuine, broadcastable Ethereum
|
|
17
|
+
mainnet signature that the caller never asked for. The same applied to anyone
|
|
18
|
+
who reached for a `network` field — transactions have never had one, the key
|
|
19
|
+
was discarded, and chain 1 came back. Both are now refused with
|
|
20
|
+
`PARAMETER_ERROR`, and the message names the `network` trap directly.
|
|
21
|
+
|
|
22
|
+
TypeScript callers using the object form were already required to pass
|
|
23
|
+
`chainId` and are unaffected. This changes behaviour for JavaScript callers,
|
|
24
|
+
and for the positional form, where `opts` and its `chainId` are now required.
|
|
25
|
+
|
|
26
|
+
Nothing changes for `address.buildTransaction()`, which continues to take the
|
|
27
|
+
chain from the address's own network — that remains the shortest correct way
|
|
28
|
+
to build a transaction, and `chainId` there is still optional.
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
// before — signed for Ethereum, silently
|
|
32
|
+
new EvmTransaction({ from, to, value });
|
|
33
|
+
|
|
34
|
+
// after — name the chain
|
|
35
|
+
new EvmTransaction({ from, to, value, chainId: 8453 });
|
|
36
|
+
// or let the address decide
|
|
37
|
+
address.buildTransaction({ to, value });
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
- **A default storage backend in both environments.** `Workspace.open(password)`
|
|
42
|
+
now works in a browser without importing a provider first, using IndexedDB.
|
|
43
|
+
Node continues to resolve its filesystem backend from `wative-core/node`.
|
|
44
|
+
- **A worked example of a custom storage backend**, at
|
|
45
|
+
`examples/19-simple-provider.test.cjs`: six methods that move bytes, running a
|
|
46
|
+
whole workspace on a `Map`. The published type declarations now ship the
|
|
47
|
+
documentation for the types you implement against.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
- **Node's default backend is now `HybridProviderV3`**, falling back to
|
|
51
|
+
`HybridProvider` where the optional native dependency has no prebuilt binary.
|
|
52
|
+
Both read each other's containers, so the fallback costs no data.
|
|
53
|
+
|
|
5
54
|
## [2.4.0] — 2026-08-11
|
|
6
55
|
|
|
7
56
|
Six breaking items, each one an operation that reported success without having
|
package/README.md
CHANGED
|
@@ -225,13 +225,13 @@ await otherProvider.importContainer(backup); // same password opens it
|
|
|
225
225
|
|
|
226
226
|
## Key derivation
|
|
227
227
|
|
|
228
|
-
Passwords are stretched with Argon2id (RFC 9106)
|
|
229
|
-
|
|
230
|
-
|
|
228
|
+
Passwords are stretched with Argon2id (RFC 9106) before they ever become a key.
|
|
229
|
+
That is deliberately expensive, which is why unlocking takes a moment and why
|
|
230
|
+
the sections below are about speed rather than correctness — every option
|
|
231
|
+
produces the same keys.
|
|
231
232
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
**25x** faster than the pure-JS implementation it replaced.
|
|
233
|
+
Nothing to install: it runs on a bundled WebAssembly build by default, with no
|
|
234
|
+
native binary and no build step.
|
|
235
235
|
|
|
236
236
|
### Browser: grant `'wasm-unsafe-eval'`
|
|
237
237
|
|
|
@@ -239,58 +239,10 @@ step, nothing to install. Measured over a full workspace lifecycle it is about
|
|
|
239
239
|
Content-Security-Policy: script-src 'self' 'wasm-unsafe-eval'
|
|
240
240
|
```
|
|
241
241
|
|
|
242
|
-
Without it
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
goes from under a second to tens of seconds. The fallback is announced once on
|
|
247
|
-
the console and reported permanently by `argon2BackendInfo()`.
|
|
248
|
-
|
|
249
|
-
## Upgrading to 2.3.0
|
|
250
|
-
|
|
251
|
-
**v1 containers can no longer be opened.** Support for the v1 envelope
|
|
252
|
-
(PBKDF2-HMAC-SHA256, written by wative-core 1.x) was removed and there is no
|
|
253
|
-
migration path in this release. Containers written by any 2.x version are v2 and
|
|
254
|
-
open unchanged.
|
|
255
|
-
|
|
256
|
-
If you hold a v1 container, recover it with **2.2.x or earlier** before
|
|
257
|
-
upgrading. `exportContainer()` will not do it — it copies sealed bytes without
|
|
258
|
-
decrypting, so the export is as unreadable as the original. Use the accessors
|
|
259
|
-
that return plaintext:
|
|
260
|
-
|
|
261
|
-
```ts
|
|
262
|
-
// under wative-core 2.2.x — NOT 2.3.0, which can no longer open a v1 container
|
|
263
|
-
import { Workspace, HybridProvider } from "wative-core";
|
|
264
|
-
|
|
265
|
-
const ws = await Workspace.open(new HybridProvider("~/wallets"), password);
|
|
266
|
-
|
|
267
|
-
for (const account of ws.accounts) {
|
|
268
|
-
await account.tryUnlock(password);
|
|
269
|
-
|
|
270
|
-
// HD accounts: the mnemonic regenerates every derived key, so it is the only
|
|
271
|
-
// thing you need.
|
|
272
|
-
if (account.organizationType === "HD") {
|
|
273
|
-
console.log(account.slug, "mnemonic:", account.dumpMnemonic());
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// PK accounts: there is no mnemonic, so every imported key must be kept.
|
|
278
|
-
// `dumpPrivateKey` lives on the WALLET and takes the VM of the address you
|
|
279
|
-
// want — a wallet holds at most one address per VM.
|
|
280
|
-
for (const wallet of account.wallets) {
|
|
281
|
-
for (const address of wallet.addresses) {
|
|
282
|
-
console.log(account.slug, wallet.id, address.vm, wallet.dumpPrivateKey(address.vm));
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
Keep that material somewhere safe, upgrade, then re-import it with
|
|
289
|
-
`accounts.create(...)` and `importPrivateKey(...)`.
|
|
290
|
-
|
|
291
|
-
Three public members were removed with v1: `Provider.passwordTanren`,
|
|
292
|
-
`ContainerProvider.passwordTanren` and `HybridProvider.kdfIterations`. All three
|
|
293
|
-
existed only to expose the PBKDF2 derivation.
|
|
242
|
+
Optional. Without it nothing breaks and nothing throws — key derivation falls
|
|
243
|
+
back to a slower implementation that produces identical results, so unlocking a
|
|
244
|
+
wallet with a dozen addresses takes tens of seconds instead of about one. Grant
|
|
245
|
+
it if that matters to you. `argon2BackendInfo()` reports which one ran.
|
|
294
246
|
|
|
295
247
|
### One derivation per container: `HybridProviderV3` (Node only)
|
|
296
248
|
|
|
@@ -300,34 +252,24 @@ import { Workspace, HybridProviderV3 } from "wative-core";
|
|
|
300
252
|
const ws = await Workspace.open(new HybridProviderV3("~/wallets"), password);
|
|
301
253
|
```
|
|
302
254
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
count.
|
|
255
|
+
`HybridProvider` derives a key per stored secret, so unlocking an account costs
|
|
256
|
+
one derivation per address and gets slower as the account grows.
|
|
257
|
+
`HybridProviderV3` derives once per workspace instead — unlocking a 50-address
|
|
258
|
+
account goes from about 7 seconds to under 200 ms, and stays there however many
|
|
259
|
+
addresses you add.
|
|
309
260
|
|
|
310
|
-
It is
|
|
311
|
-
has no browser build.
|
|
312
|
-
|
|
261
|
+
It is the Node default where `@node-rs/argon2` is installed, and Node-only: that
|
|
262
|
+
package has no browser build. Construct it yourself only to pick a location, or
|
|
263
|
+
to be sure you get it rather than the fallback.
|
|
313
264
|
|
|
314
|
-
|
|
315
|
-
have.** Pointing `HybridProviderV3` at a container built by 2.x keeps every
|
|
316
|
-
existing secret on its own salt, so it keeps costing one derivation each — the
|
|
317
|
-
container still opens, it just does not get faster. Rewriting an account's
|
|
318
|
-
secrets with `account.resetPassword(old, new)` converts them. A container
|
|
319
|
-
reaches one derivation once all of its records have been written as v3, which
|
|
320
|
-
happens on its own only for containers created under v3.
|
|
265
|
+
Two things to plan for:
|
|
321
266
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
library versions cannot read v3**: wative-core 2.2.x and earlier know only v1 and
|
|
329
|
-
v2 and will fail with `Unknown envelope version: 3`. Adopting v3 is a one-way
|
|
330
|
-
door with respect to the library version.
|
|
267
|
+
- **An existing workspace does not get faster on its own.** Secrets written
|
|
268
|
+
before the switch keep their own keys, so they keep costing one derivation
|
|
269
|
+
each. `account.resetPassword(old, new)` rewrites them.
|
|
270
|
+
- ⚠️ **Older versions of this library cannot open a V3 workspace**, and there is
|
|
271
|
+
no way back. Once a workspace has been opened with V3, keep using a version
|
|
272
|
+
that supports it.
|
|
331
273
|
|
|
332
274
|
### Checking which implementation ran
|
|
333
275
|
|
|
@@ -352,7 +294,7 @@ An `examples/` folder ships with the package — runnable JavaScript files demon
|
|
|
352
294
|
Copy them out of `node_modules` first, then run the whole set:
|
|
353
295
|
|
|
354
296
|
```bash
|
|
355
|
-
cp -R node_modules/wative-core/
|
|
297
|
+
cp -R node_modules/wative-core/examples wative-examples
|
|
356
298
|
node --test wative-examples/
|
|
357
299
|
```
|
|
358
300
|
|
|
@@ -362,7 +304,7 @@ The copy is required: Node's test runner skips anything under `node_modules`, so
|
|
|
362
304
|
node node_modules/wative-core/examples/01-quick-start.test.cjs
|
|
363
305
|
```
|
|
364
306
|
|
|
365
|
-
Files cover: quick start, HD vs PK accounts, network management, asset management, address signing, custom storage backends, persistence, workspace search, default-network selection, subpath imports, the workspace logger, workspace config, the ESM entry, and
|
|
307
|
+
Files cover: quick start, HD vs PK accounts, network management, asset management, address signing, custom storage backends, persistence, workspace search, default-network selection, subpath imports, the workspace logger, workspace config, the ESM entry, package resolution, known-answer vectors, locked-state behaviour, and one-key-per-workspace derivation. See [examples/README.md](./examples/README.md) for the full index — it is the list that stays current.
|
|
366
308
|
|
|
367
309
|
## What you can do
|
|
368
310
|
|
|
@@ -518,11 +460,10 @@ const svmAddr = hd.wallets[0].addresses.find((a) => a.vm === "svm")!;
|
|
|
518
460
|
|
|
519
461
|
const sig = evmAddr.signMessage("hello hedgue");
|
|
520
462
|
|
|
521
|
-
|
|
522
|
-
const tx = await evmAddr.buildTransaction({
|
|
463
|
+
const tx = evmAddr.buildTransaction({
|
|
523
464
|
to: "0x1234567890123456789012345678901234567890",
|
|
524
465
|
value: 1_000_000_000_000_000n,
|
|
525
|
-
|
|
466
|
+
chainId: 1,
|
|
526
467
|
});
|
|
527
468
|
await evmAddr.signTransaction(tx);
|
|
528
469
|
const tracker = await evmAddr.sendTransaction(tx);
|
|
@@ -606,7 +547,11 @@ const evmTx = new EvmTransaction({
|
|
|
606
547
|
from: evmAddr.publicKey as EvmAddress,
|
|
607
548
|
to: "0x1234567890123456789012345678901234567890" as EvmAddress,
|
|
608
549
|
value: 10_000_000n,
|
|
609
|
-
|
|
550
|
+
// The chain is set HERE, by id. `chainId` is required and has no default,
|
|
551
|
+
// and there is no `network` field on a transaction — both are refused
|
|
552
|
+
// rather than guessed, because a signed transaction for the wrong chain is
|
|
553
|
+
// still a valid transaction on that chain.
|
|
554
|
+
chainId: 8453 as ChainId, // Base
|
|
610
555
|
gasLimit: 21000n,
|
|
611
556
|
maxFeePerGas: 50_000_000_000n,
|
|
612
557
|
maxPriorityFeePerGas: 1_000_000_000n,
|
|
@@ -617,7 +562,7 @@ const evmTracker = await evmAddr.sendTransaction(evmTx);
|
|
|
617
562
|
const receipt = await evmTracker.whenConfirmed(3);
|
|
618
563
|
```
|
|
619
564
|
|
|
620
|
-
For Solana transactions, build instructions with `@solana/web3.js` (or your preferred Solana toolkit) and pass them to `new SvmTransaction({ from,
|
|
565
|
+
For Solana transactions, build instructions with `@solana/web3.js` (or your preferred Solana toolkit) and pass them to `new SvmTransaction({ from, recipient, amount, instructions })`. The signing, sending, and tracker lifecycle then mirror the EVM flow.
|
|
621
566
|
|
|
622
567
|
#### Hand off to an external signer / RPC client
|
|
623
568
|
|
|
@@ -658,9 +603,100 @@ const address = await ws.filter("0xab12345678…cdef", "Address");
|
|
|
658
603
|
const asset = await ws.filter("USDC", "Asset");
|
|
659
604
|
```
|
|
660
605
|
|
|
606
|
+
## Which storage backend you get
|
|
607
|
+
|
|
608
|
+
`Workspace.open()` picks one for you unless you pass a provider:
|
|
609
|
+
|
|
610
|
+
| environment | default | notes |
|
|
611
|
+
|---|---|---|
|
|
612
|
+
| Node, with `import "wative-core/node"` | `HybridProviderV3`, falling back to `HybridProvider` | encrypted files on disk |
|
|
613
|
+
| Browser | `IdbProvider` | IndexedDB, in the current origin |
|
|
614
|
+
| Node, without the node import | *none* — throws, and the message tells you to add the import | |
|
|
615
|
+
|
|
616
|
+
The optional first argument means a **path** in Node and a **database name** in the browser, so the same call reads sensibly in both:
|
|
617
|
+
|
|
618
|
+
```ts
|
|
619
|
+
await Workspace.open("my-wallet", password); // path in Node, database name in a browser
|
|
620
|
+
await Workspace.open({ password }); // omit it for the conventional location / "wative"
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
Two things to know about the Node default:
|
|
624
|
+
|
|
625
|
+
- It uses `HybridProviderV3` when `@node-rs/argon2` is installed, and `HybridProvider` when it is not. V3 derives one key per workspace instead of one per stored secret, so unlocking an account with many addresses is a single derivation rather than one each. `@node-rs/argon2` is an optional dependency because a few platforms have no prebuilt binary.
|
|
626
|
+
- Either provider opens a workspace the other wrote, so falling back costs speed, not access. ⚠️ Older versions of this library cannot open a V3 workspace, and there is no way back — check `argon2BackendInfo()` if you need to know which you got.
|
|
627
|
+
|
|
628
|
+
Pass a provider explicitly whenever you need to configure it — a non-default location, a named database, or acknowledging evictable browser storage:
|
|
629
|
+
|
|
630
|
+
```ts
|
|
631
|
+
import { Workspace, IdbProvider } from "wative-core";
|
|
632
|
+
|
|
633
|
+
const provider = await IdbProvider.create("my-wallet", { acknowledgeEvictionRisk: true });
|
|
634
|
+
const ws = await Workspace.open({ provider, password });
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
## Records
|
|
638
|
+
|
|
639
|
+
Everything a workspace persists is a `Record` — an account, a network, an asset, the config. You mostly work with the domain objects (`Account`, `Network`, …) and never touch a Record directly; you need this when you write a custom backend, or when you read records straight off a provider.
|
|
640
|
+
|
|
641
|
+
A Record arrives **locked** and holds ciphertext until you unlock it:
|
|
642
|
+
|
|
643
|
+
```ts
|
|
644
|
+
const record = await provider.loadRecord("ACCOUNTS", "alice");
|
|
645
|
+
|
|
646
|
+
record.locked; // true
|
|
647
|
+
record.value; // throws RECORD_LOCKED
|
|
648
|
+
|
|
649
|
+
record.unlock(password); // decrypts in memory
|
|
650
|
+
record.value; // your data
|
|
651
|
+
|
|
652
|
+
record.value.displayName = "Alice Desk";
|
|
653
|
+
await record.save(); // re-encrypts and writes back
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
| member | behaviour |
|
|
657
|
+
|---|---|
|
|
658
|
+
| `locked` | `true` until `unlock()` succeeds |
|
|
659
|
+
| `value` | throws `RECORD_LOCKED` while locked |
|
|
660
|
+
| `unlock(password)` | `BAD_PASSWORD` if wrong, `DECRYPT_FAILED` if the record cannot be read at all |
|
|
661
|
+
| `save()` | throws if locked, or if the workspace that issued it has since been locked |
|
|
662
|
+
|
|
663
|
+
Two things to plan for:
|
|
664
|
+
|
|
665
|
+
- **A handle does not outlive its session.** A Record kept from an earlier unlock stops working once the workspace is locked, and cannot write over a record that has since been replaced or deleted. Reload it rather than holding it.
|
|
666
|
+
- **Records are backend-independent.** `exportContainer()` in a browser imports into a desktop workspace and back — the storage you chose does not change what a workspace is.
|
|
667
|
+
|
|
661
668
|
## Custom storage backends
|
|
662
669
|
|
|
663
|
-
|
|
670
|
+
To store a workspace somewhere else — Redis, S3, SQLite, OPFS, SFTP — extend **`ContainerProvider`** and implement six methods that move bytes. The session, the record API and all the encryption come from the base class, so this is the whole backend:
|
|
671
|
+
|
|
672
|
+
```ts
|
|
673
|
+
import { Workspace, ContainerProvider, WativeError } from "wative-core";
|
|
674
|
+
|
|
675
|
+
class MyProvider extends ContainerProvider {
|
|
676
|
+
#store = new Map<string, Uint8Array>();
|
|
677
|
+
|
|
678
|
+
protected _exist(path: string) { return this.#store.has(path); }
|
|
679
|
+
protected _read(path: string) {
|
|
680
|
+
const v = this.#store.get(path);
|
|
681
|
+
if (!v) throw new WativeError("RECORD_NOT_FOUND", path);
|
|
682
|
+
return v;
|
|
683
|
+
}
|
|
684
|
+
protected _write(path: string, data: Uint8Array) { this.#store.set(path, data); }
|
|
685
|
+
protected _remove(path: string) { this.#store.delete(path); }
|
|
686
|
+
protected _ensureDir(_path: string) {}
|
|
687
|
+
protected _listItems(prefix: string) { /* names directly under prefix */ return []; }
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const ws = await Workspace.open({ provider: new MyProvider("mem://desk"), password });
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
Each method may be sync or async — return a value or a promise — so a synchronous store and a network one share the same contract.
|
|
694
|
+
|
|
695
|
+
Your code never encrypts anything: the base class hands you sealed bytes and opaque paths. A runnable version, including a check that no readable secret reaches the store, is in [19-simple-provider.test.cjs](./examples/19-simple-provider.test.cjs).
|
|
696
|
+
|
|
697
|
+
### Taking over the record layer as well
|
|
698
|
+
|
|
699
|
+
Extend `Provider` instead when the storage system has its own encryption or its own idea of a record. You then implement the six primitives *and* the container session and record API — see [07-custom-provider.test.cjs](./examples/07-custom-provider.test.cjs) for a complete one:
|
|
664
700
|
|
|
665
701
|
```ts
|
|
666
702
|
import { Workspace, Provider, Record, type RecordInit, type RecordType } from "wative-core";
|