wative-core 2.4.0 → 2.4.2
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 +88 -0
- package/README.md +163 -112
- 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-B8M0ylrK.d.ts} +1 -1
- package/dist/{container-provider-DAUXclw_.d.cts → container-provider-YxyMpkhG.d.cts} +1 -1
- package/dist/{evm-GxBuNjq7.d.ts → evm-Bo3Dy492.d.ts} +1 -1
- package/dist/{evm-qXdZWugm.d.cts → evm-DA6Sh6jv.d.cts} +1 -1
- package/dist/{file-sink-B0EjCBvG.d.ts → file-sink-Be5iLbuT.d.ts} +2 -2
- package/dist/{file-sink-DSOZbsJM.d.cts → file-sink-KCCpKHRj.d.cts} +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-B51NevS8.d.ts} +1 -1
- package/dist/{svm-B_c0L-rJ.d.cts → svm-DswX6XRv.d.cts} +1 -1
- package/dist/{workspace-DdaY7sWa.d.cts → workspace-CBxKR80D.d.cts} +48 -10
- package/dist/{workspace-DdaY7sWa.d.ts → workspace-CBxKR80D.d.ts} +48 -10
- package/examples/03-pk-account.test.cjs +65 -0
- package/examples/15-package-resolution.test.cjs +4 -4
- package/examples/19-simple-provider.test.cjs +134 -0
- package/examples/README.md +11 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,94 @@
|
|
|
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.2] — 2026-08-11
|
|
6
|
+
|
|
7
|
+
Importing a private key gets easier and does more: you no longer have to say
|
|
8
|
+
which chain a key is for, more of the formats you actually hold are accepted,
|
|
9
|
+
and one import now gives you an address on both chains.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **BREAKING. A PK wallet now holds two addresses instead of one.** Importing a
|
|
13
|
+
key gives you an address on the chain that key is for, plus one on the other
|
|
14
|
+
chain seeded by the same 32 secret bytes. The chain you imported for stays at
|
|
15
|
+
`addresses[0]`, so code reading that position is unaffected — but
|
|
16
|
+
`addresses.length` is now 2, and totals derived from it will differ.
|
|
17
|
+
|
|
18
|
+
⚠️ **One secret controls both addresses.** There is only one key to import,
|
|
19
|
+
so both addresses come from it, and whoever learns that key holds both
|
|
20
|
+
chains. This is not how an HD account behaves: there a slot's two addresses
|
|
21
|
+
come from two derivation paths and are independent keys. Use an HD account,
|
|
22
|
+
or import a separate key per chain, if you need that separation.
|
|
23
|
+
|
|
24
|
+
Wallets already stored keep the single address they were written with. Only
|
|
25
|
+
new imports are paired.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- **`importPrivateKey(pk)` no longer needs a `vm`.** The accepted formats do not
|
|
29
|
+
overlap — 64 hex characters for EVM, 80–96 base58 for Solana — so the chain is
|
|
30
|
+
read off the key. Passing `vm` explicitly still works and still wins.
|
|
31
|
+
- **`solana-keygen` JSON byte arrays are accepted.** The contents of
|
|
32
|
+
`~/.config/solana/id.json` can be handed to `importPrivateKey` as-is, instead
|
|
33
|
+
of being converted to base58 by hand first.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- **An out-of-range EVM private key is now `INVALID_PRIVATE_KEY`.** 32 bytes of
|
|
37
|
+
hex is not necessarily a key: zero, and anything at or above the secp256k1
|
|
38
|
+
group order, previously reached the curve library and surfaced as a bare
|
|
39
|
+
`Error` with no `code`, thrown from inside a queued mutation. It is refused at
|
|
40
|
+
the boundary now, like every other malformed key.
|
|
41
|
+
- Corrected the README's account diagrams, which described a PK wallet as
|
|
42
|
+
carrying exactly one address as though it were a structural rule.
|
|
43
|
+
|
|
44
|
+
## [2.4.1] — 2026-08-11
|
|
45
|
+
|
|
46
|
+
One breaking item, and it is the kind worth reading before upgrading: a
|
|
47
|
+
transaction no longer guesses which chain it is for. Alongside it, a browser and
|
|
48
|
+
a Node process now each get a working storage backend without being told which
|
|
49
|
+
one to use, and writing your own backend is documented with a runnable example.
|
|
50
|
+
|
|
51
|
+
### Security
|
|
52
|
+
- **BREAKING. `EvmTransaction` requires `chainId` and no longer defaults to
|
|
53
|
+
Ethereum mainnet.** Building a transaction without naming a chain used to
|
|
54
|
+
produce chain 1. That is not a harmless default: a signed transaction is a
|
|
55
|
+
bearer instrument, so the result was a genuine, broadcastable Ethereum
|
|
56
|
+
mainnet signature that the caller never asked for. The same applied to anyone
|
|
57
|
+
who reached for a `network` field — transactions have never had one, the key
|
|
58
|
+
was discarded, and chain 1 came back. Both are now refused with
|
|
59
|
+
`PARAMETER_ERROR`, and the message names the `network` trap directly.
|
|
60
|
+
|
|
61
|
+
TypeScript callers using the object form were already required to pass
|
|
62
|
+
`chainId` and are unaffected. This changes behaviour for JavaScript callers,
|
|
63
|
+
and for the positional form, where `opts` and its `chainId` are now required.
|
|
64
|
+
|
|
65
|
+
Nothing changes for `address.buildTransaction()`, which continues to take the
|
|
66
|
+
chain from the address's own network — that remains the shortest correct way
|
|
67
|
+
to build a transaction, and `chainId` there is still optional.
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
// before — signed for Ethereum, silently
|
|
71
|
+
new EvmTransaction({ from, to, value });
|
|
72
|
+
|
|
73
|
+
// after — name the chain
|
|
74
|
+
new EvmTransaction({ from, to, value, chainId: 8453 });
|
|
75
|
+
// or let the address decide
|
|
76
|
+
address.buildTransaction({ to, value });
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Added
|
|
80
|
+
- **A default storage backend in both environments.** `Workspace.open(password)`
|
|
81
|
+
now works in a browser without importing a provider first, using IndexedDB.
|
|
82
|
+
Node continues to resolve its filesystem backend from `wative-core/node`.
|
|
83
|
+
- **A worked example of a custom storage backend**, at
|
|
84
|
+
`examples/19-simple-provider.test.cjs`: six methods that move bytes, running a
|
|
85
|
+
whole workspace on a `Map`. The published type declarations now ship the
|
|
86
|
+
documentation for the types you implement against.
|
|
87
|
+
|
|
88
|
+
### Changed
|
|
89
|
+
- **Node's default backend is now `HybridProviderV3`**, falling back to
|
|
90
|
+
`HybridProvider` where the optional native dependency has no prebuilt binary.
|
|
91
|
+
Both read each other's containers, so the fallback costs no data.
|
|
92
|
+
|
|
5
93
|
## [2.4.0] — 2026-08-11
|
|
6
94
|
|
|
7
95
|
Six breaking items, each one an operation that reported success without having
|
package/README.md
CHANGED
|
@@ -44,28 +44,64 @@ Account "Trading Desk" (HD) one mnemonic
|
|
|
44
44
|
└── … account.deriveWallets(n)
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
**PK** — imported keys.
|
|
48
|
-
|
|
47
|
+
**PK** — imported keys. Each import allocates its own `Wallet`, and that wallet
|
|
48
|
+
gets an address on **both** chains: the key signs on its own curve, and the same
|
|
49
|
+
32 secret bytes seed a keypair on the other one. The chain you imported for
|
|
50
|
+
comes first.
|
|
49
51
|
|
|
50
52
|
```
|
|
51
53
|
Account "Cold Storage" (PK) no mnemonic
|
|
52
54
|
├── Wallet 0 the key passed to accounts.create()
|
|
53
|
-
│
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
│ ├── Address vm: "evm" 0x90F8bf6A… the imported key
|
|
56
|
+
│ └── Address vm: "svm" HaWmh8svNQ… same secret, other curve
|
|
57
|
+
├── Wallet 1 account.importPrivateKey(evmKey)
|
|
58
|
+
│ ├── Address vm: "evm" 0xFFcf8FDE…
|
|
59
|
+
│ └── Address vm: "svm" Fgdy5QRxtP…
|
|
60
|
+
└── Wallet 2 account.importPrivateKey(svmKey)
|
|
61
|
+
├── Address vm: "svm" 3QVq8D876h… the imported key
|
|
62
|
+
└── Address vm: "evm" 0x1a740984… same secret, other curve
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A PK account can hold as many keys as you like, on any mix of chains — but it
|
|
66
|
+
refuses the same identity twice, on either chain.
|
|
67
|
+
|
|
68
|
+
> ⚠️ **One secret controls both addresses in a PK wallet.** There is only one
|
|
69
|
+
> key to import, so both addresses come from it. Whoever learns that key holds
|
|
70
|
+
> both chains.
|
|
71
|
+
>
|
|
72
|
+
> This is not how an HD account works. There, a slot's two addresses come from
|
|
73
|
+
> two different derivation paths — the mnemonic is shared, the private keys are
|
|
74
|
+
> independent. If you want independent keys per chain, use an HD account, or
|
|
75
|
+
> import a separate key for each and read `addresses[0]` of each wallet.
|
|
76
|
+
|
|
77
|
+
> **`wallet.addresses` is a list.** It holds one address per `(vm, network)`
|
|
78
|
+
> pair and is free to carry several. Read it rather than assuming a position:
|
|
79
|
+
>
|
|
80
|
+
> ```ts
|
|
81
|
+
> const evm = wallet.addresses.find((a) => a.vm === "evm");
|
|
82
|
+
> ```
|
|
83
|
+
|
|
84
|
+
**Which chain a key is for is read off the key** — `vm` is optional:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
await acc.importPrivateKey(evmKey); // 64 hex chars -> evm
|
|
88
|
+
await acc.importPrivateKey(solanaKey); // base58 -> svm
|
|
89
|
+
await acc.importPrivateKey(readFileSync("id.json")); // solana-keygen -> svm
|
|
90
|
+
await acc.importPrivateKey(key, "evm"); // or say so explicitly
|
|
58
91
|
```
|
|
59
92
|
|
|
60
|
-
|
|
61
|
-
|
|
93
|
+
The formats do not overlap, so this is a decision rather than a guess. A value
|
|
94
|
+
that is neither is refused with `INVALID_PRIVATE_KEY` — including 32 bytes of
|
|
95
|
+
hex that is not a valid secp256k1 scalar, and a bare 32-byte base58 value,
|
|
96
|
+
which is indistinguishable from a Solana *public* key.
|
|
62
97
|
|
|
63
98
|
| | HD | PK |
|
|
64
99
|
| ---------------------- | ----------------------------- | ------------------------------- |
|
|
65
100
|
| Created from | a BIP-39 mnemonic | one private key |
|
|
66
101
|
| Grows with | `deriveWallets(n)` | `importPrivateKey(pk, vm)` |
|
|
67
102
|
| A `Wallet` is | a derivation slot | one imported key |
|
|
68
|
-
| Addresses per `Wallet`
|
|
103
|
+
| Addresses per `Wallet` | **2** — one evm, one svm | **2** — imported chain first |
|
|
104
|
+
| Those two addresses | independent keys, two paths | **one key, both curves** |
|
|
69
105
|
| `dumpMnemonic()` | ✔ | throws `UNSUPPORTED_OP` |
|
|
70
106
|
| `sliceWallets(n)` | ✔ | throws `UNSUPPORTED_OP` |
|
|
71
107
|
| `importPrivateKey()` | throws `UNSUPPORTED_OP` | ✔ |
|
|
@@ -150,7 +186,6 @@ await ws.lock();
|
|
|
150
186
|
| Storage | encrypted files on disk | IndexedDB |
|
|
151
187
|
| Where it lives | `Workspace.open({ path })`, or default | `IdbProvider.create(name)` |
|
|
152
188
|
| Extra setup | none | `Buffer` polyfill |
|
|
153
|
-
| Keys can vanish| no | yes — storage is evictable |
|
|
154
189
|
|
|
155
190
|
`HybridProvider`, `HybridProviderV3` and `FileSink` are re-exported from the
|
|
156
191
|
package root in Node.
|
|
@@ -225,72 +260,13 @@ await otherProvider.importContainer(backup); // same password opens it
|
|
|
225
260
|
|
|
226
261
|
## Key derivation
|
|
227
262
|
|
|
228
|
-
Passwords are stretched with Argon2id (RFC 9106)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
By default it runs on a vendored WebAssembly build: no native binary, no build
|
|
233
|
-
step, nothing to install. Measured over a full workspace lifecycle it is about
|
|
234
|
-
**25x** faster than the pure-JS implementation it replaced.
|
|
235
|
-
|
|
236
|
-
### Browser: grant `'wasm-unsafe-eval'`
|
|
237
|
-
|
|
238
|
-
```
|
|
239
|
-
Content-Security-Policy: script-src 'self' 'wasm-unsafe-eval'
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
Without it the browser blocks WebAssembly compilation entirely — synchronously
|
|
243
|
-
and asynchronously, main thread and worker alike. **Nothing throws.** Key
|
|
244
|
-
derivation quietly falls back to a pure-JS implementation that produces
|
|
245
|
-
identical bytes roughly 17x slower, so unlocking a wallet with a dozen addresses
|
|
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);
|
|
263
|
+
Passwords are stretched with Argon2id (RFC 9106) before they ever become a key.
|
|
264
|
+
That is deliberately expensive, which is why unlocking takes a moment and why
|
|
265
|
+
the sections below are about speed rather than correctness — every option
|
|
266
|
+
produces the same keys.
|
|
269
267
|
|
|
270
|
-
|
|
271
|
-
|
|
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.
|
|
268
|
+
Nothing to install: it runs on a bundled WebAssembly build by default, with no
|
|
269
|
+
native binary and no build step.
|
|
294
270
|
|
|
295
271
|
### One derivation per container: `HybridProviderV3` (Node only)
|
|
296
272
|
|
|
@@ -300,34 +276,15 @@ import { Workspace, HybridProviderV3 } from "wative-core";
|
|
|
300
276
|
const ws = await Workspace.open(new HybridProviderV3("~/wallets"), password);
|
|
301
277
|
```
|
|
302
278
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
count.
|
|
309
|
-
|
|
310
|
-
It is opt-in, and Node-only: it derives through native `@node-rs/argon2`, which
|
|
311
|
-
has no browser build. In a browser use `HybridProvider`, which needs no binary
|
|
312
|
-
and no install and is already about 25x faster than what preceded it.
|
|
313
|
-
|
|
314
|
-
⚠️ **The speedup applies to records written as v3, not to ones you already
|
|
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.
|
|
279
|
+
`HybridProvider` derives a key per stored secret, so unlocking an account costs
|
|
280
|
+
one derivation per address and gets slower as the account grows.
|
|
281
|
+
`HybridProviderV3` derives once per workspace instead — unlocking a 50-address
|
|
282
|
+
account goes from about 7 seconds to under 200 ms, and stays there however many
|
|
283
|
+
addresses you add.
|
|
321
284
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
Reads do not depend on the choice — every record carries its own version byte,
|
|
327
|
-
so either 2.3.0 provider opens either format, in both directions. ⚠️ But **older
|
|
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.
|
|
285
|
+
It is the Node default where `@node-rs/argon2` is installed, and Node-only: that
|
|
286
|
+
package has no browser build. Construct it yourself only to pick a location, or
|
|
287
|
+
to be sure you get it rather than the fallback.
|
|
331
288
|
|
|
332
289
|
### Checking which implementation ran
|
|
333
290
|
|
|
@@ -352,7 +309,7 @@ An `examples/` folder ships with the package — runnable JavaScript files demon
|
|
|
352
309
|
Copy them out of `node_modules` first, then run the whole set:
|
|
353
310
|
|
|
354
311
|
```bash
|
|
355
|
-
cp -R node_modules/wative-core/
|
|
312
|
+
cp -R node_modules/wative-core/examples wative-examples
|
|
356
313
|
node --test wative-examples/
|
|
357
314
|
```
|
|
358
315
|
|
|
@@ -362,7 +319,7 @@ The copy is required: Node's test runner skips anything under `node_modules`, so
|
|
|
362
319
|
node node_modules/wative-core/examples/01-quick-start.test.cjs
|
|
363
320
|
```
|
|
364
321
|
|
|
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
|
|
322
|
+
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
323
|
|
|
367
324
|
## What you can do
|
|
368
325
|
|
|
@@ -370,7 +327,7 @@ The library is organized around seven things you'll work with:
|
|
|
370
327
|
|
|
371
328
|
- **`Workspace`** — your top-level container. Holds the password, your accounts, your network and asset list, and a built-in logger.
|
|
372
329
|
- **`Account`** — either an HD account (one BIP-39 mnemonic, can derive many wallets) or a PK account (raw private keys you import one at a time). Each account can have its own password or share the workspace password.
|
|
373
|
-
- **`Wallet`** — a unit inside an account
|
|
330
|
+
- **`Wallet`** — a unit inside an account, holding one address per `(vm, network)`. Both kinds give you an EVM and a Solana address: an HD slot derives one per chain from the mnemonic, an import derives its sibling from the imported key. Treat `wallet.addresses` as the list it is.
|
|
374
331
|
- **`Address`** — an on-chain identity. Sign messages, build transactions, send them.
|
|
375
332
|
- **`Network`** — chain metadata. 10 networks ship pre-loaded.
|
|
376
333
|
- **`Asset`** — token metadata. 25 tokens ship pre-loaded.
|
|
@@ -518,11 +475,10 @@ const svmAddr = hd.wallets[0].addresses.find((a) => a.vm === "svm")!;
|
|
|
518
475
|
|
|
519
476
|
const sig = evmAddr.signMessage("hello hedgue");
|
|
520
477
|
|
|
521
|
-
|
|
522
|
-
const tx = await evmAddr.buildTransaction({
|
|
478
|
+
const tx = evmAddr.buildTransaction({
|
|
523
479
|
to: "0x1234567890123456789012345678901234567890",
|
|
524
480
|
value: 1_000_000_000_000_000n,
|
|
525
|
-
|
|
481
|
+
chainId: 1,
|
|
526
482
|
});
|
|
527
483
|
await evmAddr.signTransaction(tx);
|
|
528
484
|
const tracker = await evmAddr.sendTransaction(tx);
|
|
@@ -606,7 +562,11 @@ const evmTx = new EvmTransaction({
|
|
|
606
562
|
from: evmAddr.publicKey as EvmAddress,
|
|
607
563
|
to: "0x1234567890123456789012345678901234567890" as EvmAddress,
|
|
608
564
|
value: 10_000_000n,
|
|
609
|
-
|
|
565
|
+
// The chain is set HERE, by id. `chainId` is required and has no default,
|
|
566
|
+
// and there is no `network` field on a transaction — both are refused
|
|
567
|
+
// rather than guessed, because a signed transaction for the wrong chain is
|
|
568
|
+
// still a valid transaction on that chain.
|
|
569
|
+
chainId: 8453 as ChainId, // Base
|
|
610
570
|
gasLimit: 21000n,
|
|
611
571
|
maxFeePerGas: 50_000_000_000n,
|
|
612
572
|
maxPriorityFeePerGas: 1_000_000_000n,
|
|
@@ -617,7 +577,7 @@ const evmTracker = await evmAddr.sendTransaction(evmTx);
|
|
|
617
577
|
const receipt = await evmTracker.whenConfirmed(3);
|
|
618
578
|
```
|
|
619
579
|
|
|
620
|
-
For Solana transactions, build instructions with `@solana/web3.js` (or your preferred Solana toolkit) and pass them to `new SvmTransaction({ from,
|
|
580
|
+
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
581
|
|
|
622
582
|
#### Hand off to an external signer / RPC client
|
|
623
583
|
|
|
@@ -658,9 +618,100 @@ const address = await ws.filter("0xab12345678…cdef", "Address");
|
|
|
658
618
|
const asset = await ws.filter("USDC", "Asset");
|
|
659
619
|
```
|
|
660
620
|
|
|
621
|
+
## Which storage backend you get
|
|
622
|
+
|
|
623
|
+
`Workspace.open()` picks one for you unless you pass a provider:
|
|
624
|
+
|
|
625
|
+
| environment | default | notes |
|
|
626
|
+
|---|---|---|
|
|
627
|
+
| Node, with `import "wative-core/node"` | `HybridProviderV3`, falling back to `HybridProvider` | encrypted files on disk |
|
|
628
|
+
| Browser | `IdbProvider` | IndexedDB, in the current origin |
|
|
629
|
+
| Node, without the node import | *none* — throws, and the message tells you to add the import | |
|
|
630
|
+
|
|
631
|
+
The optional first argument means a **path** in Node and a **database name** in the browser, so the same call reads sensibly in both:
|
|
632
|
+
|
|
633
|
+
```ts
|
|
634
|
+
await Workspace.open("my-wallet", password); // path in Node, database name in a browser
|
|
635
|
+
await Workspace.open({ password }); // omit it for the conventional location / "wative"
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
Two things to know about the Node default:
|
|
639
|
+
|
|
640
|
+
- 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.
|
|
641
|
+
- 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.
|
|
642
|
+
|
|
643
|
+
Pass a provider explicitly whenever you need to configure it — a non-default location, a named database, or acknowledging evictable browser storage:
|
|
644
|
+
|
|
645
|
+
```ts
|
|
646
|
+
import { Workspace, IdbProvider } from "wative-core";
|
|
647
|
+
|
|
648
|
+
const provider = await IdbProvider.create("my-wallet", { acknowledgeEvictionRisk: true });
|
|
649
|
+
const ws = await Workspace.open({ provider, password });
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
## Records
|
|
653
|
+
|
|
654
|
+
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.
|
|
655
|
+
|
|
656
|
+
A Record arrives **locked** and holds ciphertext until you unlock it:
|
|
657
|
+
|
|
658
|
+
```ts
|
|
659
|
+
const record = await provider.loadRecord("ACCOUNTS", "alice");
|
|
660
|
+
|
|
661
|
+
record.locked; // true
|
|
662
|
+
record.value; // throws RECORD_LOCKED
|
|
663
|
+
|
|
664
|
+
record.unlock(password); // decrypts in memory
|
|
665
|
+
record.value; // your data
|
|
666
|
+
|
|
667
|
+
record.value.displayName = "Alice Desk";
|
|
668
|
+
await record.save(); // re-encrypts and writes back
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
| member | behaviour |
|
|
672
|
+
|---|---|
|
|
673
|
+
| `locked` | `true` until `unlock()` succeeds |
|
|
674
|
+
| `value` | throws `RECORD_LOCKED` while locked |
|
|
675
|
+
| `unlock(password)` | `BAD_PASSWORD` if wrong, `DECRYPT_FAILED` if the record cannot be read at all |
|
|
676
|
+
| `save()` | throws if locked, or if the workspace that issued it has since been locked |
|
|
677
|
+
|
|
678
|
+
Two things to plan for:
|
|
679
|
+
|
|
680
|
+
- **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.
|
|
681
|
+
- **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.
|
|
682
|
+
|
|
661
683
|
## Custom storage backends
|
|
662
684
|
|
|
663
|
-
|
|
685
|
+
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:
|
|
686
|
+
|
|
687
|
+
```ts
|
|
688
|
+
import { Workspace, ContainerProvider, WativeError } from "wative-core";
|
|
689
|
+
|
|
690
|
+
class MyProvider extends ContainerProvider {
|
|
691
|
+
#store = new Map<string, Uint8Array>();
|
|
692
|
+
|
|
693
|
+
protected _exist(path: string) { return this.#store.has(path); }
|
|
694
|
+
protected _read(path: string) {
|
|
695
|
+
const v = this.#store.get(path);
|
|
696
|
+
if (!v) throw new WativeError("RECORD_NOT_FOUND", path);
|
|
697
|
+
return v;
|
|
698
|
+
}
|
|
699
|
+
protected _write(path: string, data: Uint8Array) { this.#store.set(path, data); }
|
|
700
|
+
protected _remove(path: string) { this.#store.delete(path); }
|
|
701
|
+
protected _ensureDir(_path: string) {}
|
|
702
|
+
protected _listItems(prefix: string) { /* names directly under prefix */ return []; }
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const ws = await Workspace.open({ provider: new MyProvider("mem://desk"), password });
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
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.
|
|
709
|
+
|
|
710
|
+
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).
|
|
711
|
+
|
|
712
|
+
### Taking over the record layer as well
|
|
713
|
+
|
|
714
|
+
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
715
|
|
|
665
716
|
```ts
|
|
666
717
|
import { Workspace, Provider, Record, type RecordInit, type RecordType } from "wative-core";
|