wative-core 2.3.9 → 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 +197 -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-D_557LVN.d.ts → container-provider--npoeR2N.d.ts} +1 -1
- package/dist/{container-provider-CkKGyHis.d.cts → container-provider-Ca_5z1uQ.d.cts} +1 -1
- package/dist/{evm-Xw4zBLUH.d.ts → evm-EHpaW_sK.d.ts} +1 -1
- package/dist/{evm-gsdoVj16.d.cts → evm-hSJJOVG3.d.cts} +1 -1
- package/dist/{file-sink-C_SlT9IC.d.cts → file-sink-7ZAutH45.d.cts} +2 -2
- package/dist/{file-sink-D33jvque.d.ts → file-sink-Bk5zYHw-.d.ts} +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +12 -8
- package/dist/index.d.ts +12 -8
- package/dist/index.js +1 -1
- package/dist/index.node.cjs +1 -1
- package/dist/index.node.d.cts +6 -6
- package/dist/index.node.d.ts +6 -6
- 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-CUuqpl1h.d.ts → svm-B5q8YaV9.d.ts} +1 -1
- package/dist/{svm-CTCJsYri.d.cts → svm-CXFwmeMo.d.cts} +1 -1
- package/dist/{workspace-Du_jy51u.d.cts → workspace-kq3QyAgm.d.cts} +51 -10
- package/dist/{workspace-Du_jy51u.d.ts → workspace-kq3QyAgm.d.ts} +51 -10
- 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,203 @@
|
|
|
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
|
+
|
|
54
|
+
## [2.4.0] — 2026-08-11
|
|
55
|
+
|
|
56
|
+
Six breaking items, each one an operation that reported success without having
|
|
57
|
+
done what it said. A broadcast whose reply never arrived was reported as one
|
|
58
|
+
that never went out — on both chains. Moving an account between workspaces saved
|
|
59
|
+
its later changes to the wrong place; changing a password could leave the old one
|
|
60
|
+
working; erasing a browser workspace could leave everything in it; and restoring
|
|
61
|
+
a backup over an existing workspace kept records the backup did not contain. The
|
|
62
|
+
rest either clears secrets from memory sooner, or accepts input that was being
|
|
63
|
+
refused for no reason.
|
|
64
|
+
|
|
65
|
+
### Security
|
|
66
|
+
- **BREAKING. A broadcast whose outcome is unknown no longer reports as one that
|
|
67
|
+
was refused — on both chains.** When the connection to an endpoint died during
|
|
68
|
+
a broadcast, or the endpoint answered with an error status or something that
|
|
69
|
+
was not a reply, the send reported `failed`: the one status meaning "that never
|
|
70
|
+
went out, send it again". Those bytes may well have reached the network. On
|
|
71
|
+
EVM a rebuilt transaction takes a fresh position from a node whose count the
|
|
72
|
+
relayed one already advanced; on Solana there is no position to collide at all,
|
|
73
|
+
so a rebuild takes a fresh blockhash, becomes a different transaction, and
|
|
74
|
+
nothing dedupes the two. Either way both can land.
|
|
75
|
+
|
|
76
|
+
Such a send now reports `timeout`, meaning the outcome is unknown, and the
|
|
77
|
+
identifier of the transaction that was signed is reported so it can be looked
|
|
78
|
+
up before anything else is sent.
|
|
79
|
+
|
|
80
|
+
An endpoint that answers with a reason it refused still reports `failed`,
|
|
81
|
+
because nothing was sent and sending again is safe. That is the ordinary way a
|
|
82
|
+
send fails — an underpriced or badly-positioned transaction on EVM, an expired
|
|
83
|
+
blockhash or a failed simulation on Solana — and for an expired blockhash
|
|
84
|
+
rebuilding is exactly the right response.
|
|
85
|
+
- **BREAKING. An endpoint that refuses is reported differently from one that
|
|
86
|
+
never answered.** Every way a request could go wrong reported the same thing,
|
|
87
|
+
including the one case where the endpoint read the request and declined it.
|
|
88
|
+
Those are opposite facts for a broadcast, and a caller needs to tell them
|
|
89
|
+
apart to know whether sending again is safe. A refusal now carries its own
|
|
90
|
+
error code and the reason the endpoint gave; a lost connection, an error
|
|
91
|
+
status, or an unreadable reply still report the endpoint as unreachable. The
|
|
92
|
+
two chains previously disagreed about which code a refusal carried; they now
|
|
93
|
+
agree.
|
|
94
|
+
- **A value an endpoint sent is cleaned before it appears in an error.** Two
|
|
95
|
+
errors raised while reading a reply placed that reply into the message exactly
|
|
96
|
+
as received, so control characters chosen by the endpoint were reproduced into
|
|
97
|
+
whatever log the caller writes, and an overlong value was repeated in full.
|
|
98
|
+
The value is still shown, since for a number the rejected text is the
|
|
99
|
+
diagnosis, but it is cleaned and shortened.
|
|
100
|
+
|
|
101
|
+
### Security
|
|
102
|
+
- **BREAKING. Changing an account's password now reports failure rather than a
|
|
103
|
+
success that did not happen.** If the account was locked between the moment
|
|
104
|
+
the change was requested and the moment it was carried out, the recovery
|
|
105
|
+
phrase was quietly left under the old password while everything else about the
|
|
106
|
+
change was saved. The account then reported that its password had been
|
|
107
|
+
changed, after which the new password failed and the old one still worked —
|
|
108
|
+
the exact reverse of the promise, in the situation you change a password for.
|
|
109
|
+
An account managed by its workspace was additionally recorded as having a
|
|
110
|
+
password of its own, which made it unopenable the next time it was loaded.
|
|
111
|
+
Two changes started at the same time could also both report success, with the
|
|
112
|
+
second overwriting the first using a password the first had already retired.
|
|
113
|
+
Both cases now report failure and leave the account exactly as it was.
|
|
114
|
+
- **BREAKING. Erasing a browser workspace now reports whether it happened.**
|
|
115
|
+
Erasing one while the same application still had it open reported success and
|
|
116
|
+
left every stored record in the browser. Because nothing released the
|
|
117
|
+
workspace, that was the ordinary outcome rather than a rare one. The workspace
|
|
118
|
+
is now closed first so the erase can finish. An erase that cannot be confirmed
|
|
119
|
+
is reported as unconfirmed rather than as failed — it may still complete on its
|
|
120
|
+
own, and erasing again is always safe. Erasing where the browser offers no
|
|
121
|
+
storage at all now reports that, instead of returning quietly.
|
|
122
|
+
- **Secrets are cleared from memory once they are no longer needed.** Several
|
|
123
|
+
intermediate values produced while deriving a key were left behind after use,
|
|
124
|
+
and a derived key dropped from the internal cache was discarded without being
|
|
125
|
+
cleared, putting it beyond the reach of the cleanup that runs when a workspace
|
|
126
|
+
locks. Unlocking an account whose recovery phrase could not be read also left
|
|
127
|
+
that phrase in memory although the account reported itself locked. Keys and
|
|
128
|
+
addresses are unchanged: derivation was checked byte for byte against the
|
|
129
|
+
previous release and against the published Argon2 test vectors.
|
|
130
|
+
- **An endpoint that is not text is refused rather than saved unchecked.**
|
|
131
|
+
`rpcUrl` was the only network field with no type check, so a value that was not
|
|
132
|
+
text skipped every endpoint check — including the one that refuses addresses on
|
|
133
|
+
your local network — and was stored anyway. Leaving the field out reported an
|
|
134
|
+
error from outside this library rather than its own, including while loading a
|
|
135
|
+
stored network. An empty endpoint still means no endpoint configured.
|
|
136
|
+
- **A rejected name or colour is quoted before it is reported.** Both were placed
|
|
137
|
+
into the error message exactly as supplied, so control characters in them were
|
|
138
|
+
reproduced into whatever log you write. A name is not always your own — it can
|
|
139
|
+
come from a filename on disk, or from a backup being restored.
|
|
140
|
+
|
|
141
|
+
### Fixed
|
|
142
|
+
- **BREAKING. An account moved to another workspace now belongs to the workspace
|
|
143
|
+
holding it.** Moving one wrote a single record into the receiving workspace and
|
|
144
|
+
then kept sending every later change back to the workspace it came from.
|
|
145
|
+
Reopening the receiving workspace showed the account without those changes, and
|
|
146
|
+
closing the original one made the account unusable even though the receiving
|
|
147
|
+
workspace was open and still listed it. For an imported private key, discarding
|
|
148
|
+
the original workspace lost the key. The account now saves to the workspace
|
|
149
|
+
holding it, unlocks with that workspace's password, and is closed by it.
|
|
150
|
+
Locking the receiving workspace while the move is still going on now closes the
|
|
151
|
+
account rather than leaving it open and unreachable. Re-deleting an account
|
|
152
|
+
that was moved in reports that it is already gone, rather than reporting it as
|
|
153
|
+
belonging elsewhere.
|
|
154
|
+
- **BREAKING. Restoring a backup over an existing workspace now replaces it.**
|
|
155
|
+
Records the backup did not contain were left in place beside the restored ones,
|
|
156
|
+
still under the previous password. They could not be opened, could not be seen,
|
|
157
|
+
kept their names reserved, and made removing a network fail from then on. Only
|
|
158
|
+
what the backup carries is kept now, and anything it omits is removed. A backup
|
|
159
|
+
that cannot be restored in full is refused before anything changes, and a
|
|
160
|
+
backup carrying no configuration is refused outright rather than leaving a
|
|
161
|
+
workspace no password can open. The workspace is closed at the end, since the
|
|
162
|
+
password that opened it belonged to the workspace that was just replaced.
|
|
163
|
+
- **A wallet can be labelled in your own script.** Tags and token symbols
|
|
164
|
+
accepted the letters of every script but refused the marks that most of them
|
|
165
|
+
require, so Devanagari, Bengali, Tamil, Thai, Arabic and Hebrew could not be
|
|
166
|
+
used at all. They now are. A label that would display as blank, or as nothing
|
|
167
|
+
at all, is still refused. Token symbols containing `+` are also accepted, which
|
|
168
|
+
several real tokens need.
|
|
169
|
+
- **The two spellings of one password are one password.** The same password can
|
|
170
|
+
arrive in two forms depending on the keyboard, clipboard or operating system it
|
|
171
|
+
came from, and both have always unlocked a wallet. Several places compared the
|
|
172
|
+
forms directly instead, so a correct password could be reported as wrong when
|
|
173
|
+
re-entered on an already-open workspace or account, when confirming a record's
|
|
174
|
+
password, and when creating an account that shares the workspace password.
|
|
175
|
+
Checking a new password against previous ones now recognises the same password
|
|
176
|
+
in a different form as a reuse.
|
|
177
|
+
- **A deleted account's name can be used again.** Every account name seen when a
|
|
178
|
+
workspace opened was reserved for as long as it stayed open, and deleting the
|
|
179
|
+
account did not release it. Creating an account under that name again quietly
|
|
180
|
+
produced a numbered variant, and adding one back was refused with a message
|
|
181
|
+
asking you to restore a file that had just been deleted.
|
|
182
|
+
- **A token address pasted with a leading space is accepted.** It was refused
|
|
183
|
+
here while the same address was accepted everywhere else in the library.
|
|
184
|
+
- **A hex value carrying two prefixes is refused rather than decoded.**
|
|
185
|
+
`0x0x41` produced two bytes with a zero in front instead of an error.
|
|
186
|
+
- **Moving an account needs the networks it uses to exist in the receiving
|
|
187
|
+
workspace.** An account refers to its networks by name, so moving one into a
|
|
188
|
+
workspace that does not have a network it uses wrote a record that could not
|
|
189
|
+
be read back. The move reported success, and after reopening the account was
|
|
190
|
+
simply absent — not listed, not reported as held back, and not even holding
|
|
191
|
+
its name, while a record containing its recovery phrase and every key stayed
|
|
192
|
+
in the workspace, unreachable and impossible to remove. The move is now
|
|
193
|
+
refused before anything is written, naming the missing network.
|
|
194
|
+
- **An account cannot be left holding no keys.** Both ways of removing wallets
|
|
195
|
+
checked that at least one wallet remained, which does not mean at least one
|
|
196
|
+
key remains: a wallet can legitimately be added without addresses, so removing
|
|
197
|
+
the wallet carrying an account's only keys left an account with none. It still
|
|
198
|
+
reported itself as an account, and reopening it produced a shell with nothing
|
|
199
|
+
in it. Removing wallets that hold no keys is unaffected, as is removing an
|
|
200
|
+
account outright.
|
|
201
|
+
|
|
5
202
|
## [2.3.9] — 2026-08-10
|
|
6
203
|
|
|
7
204
|
Three breaking items. Two are on paths that were producing results no
|
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";
|