wative-core 1.2.2 → 2.2.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/LICENSE +55 -0
  3. package/README.md +577 -0
  4. package/dist/artifacts/evm/index.cjs +1 -0
  5. package/dist/artifacts/evm/index.d.cts +6 -0
  6. package/dist/artifacts/evm/index.d.ts +6 -0
  7. package/dist/artifacts/evm/index.js +1 -0
  8. package/dist/artifacts/svm/index.cjs +1 -0
  9. package/dist/artifacts/svm/index.d.cts +9 -0
  10. package/dist/artifacts/svm/index.d.ts +9 -0
  11. package/dist/artifacts/svm/index.js +1 -0
  12. package/dist/container-provider-C59yqd5Z.d.ts +48 -0
  13. package/dist/container-provider-i614LDE1.d.cts +48 -0
  14. package/dist/evm-BLml33LX.d.cts +34 -0
  15. package/dist/evm-DTzqGlL6.d.ts +34 -0
  16. package/dist/file-sink-B73MuigK.d.cts +44 -0
  17. package/dist/file-sink-LS4hyXXi.d.ts +44 -0
  18. package/dist/index.cjs +1 -0
  19. package/dist/index.d.cts +194 -0
  20. package/dist/index.d.ts +194 -0
  21. package/dist/index.js +1 -0
  22. package/dist/index.node.cjs +1 -0
  23. package/dist/index.node.d.cts +6 -0
  24. package/dist/index.node.d.ts +6 -0
  25. package/dist/index.node.js +1 -0
  26. package/dist/node/index.cjs +1 -0
  27. package/dist/node/index.d.cts +7 -0
  28. package/dist/node/index.d.ts +7 -0
  29. package/dist/node/index.js +1 -0
  30. package/dist/svm-CCzqNAWM.d.cts +59 -0
  31. package/dist/svm-DieFbh9k.d.ts +59 -0
  32. package/dist/workspace-CamTHxso.d.cts +737 -0
  33. package/dist/workspace-CamTHxso.d.ts +737 -0
  34. package/examples/01-quick-start.test.cjs +37 -0
  35. package/examples/02-hd-account.test.cjs +71 -0
  36. package/examples/03-pk-account.test.cjs +59 -0
  37. package/examples/04-network-management.test.cjs +94 -0
  38. package/examples/05-asset-management.test.cjs +117 -0
  39. package/examples/06-address-signing.test.cjs +55 -0
  40. package/examples/07-custom-provider.test.cjs +164 -0
  41. package/examples/08-persistence.test.cjs +105 -0
  42. package/examples/09-workspace-filter.test.cjs +84 -0
  43. package/examples/10-default-network.test.cjs +70 -0
  44. package/examples/11-subpath-imports.test.cjs +24 -0
  45. package/examples/12-logger.test.cjs +44 -0
  46. package/examples/13-workspace-config.test.cjs +43 -0
  47. package/examples/14-esm-runtime.test.mjs +81 -0
  48. package/examples/15-package-resolution.test.cjs +76 -0
  49. package/examples/16-known-answer-vectors.test.cjs +141 -0
  50. package/examples/17-locked-state.test.cjs +162 -0
  51. package/examples/README.md +72 -0
  52. package/package.json +85 -39
  53. package/lib/account.d.ts +0 -69
  54. package/lib/index.d.ts +0 -6
  55. package/lib/index.esm.js +0 -1
  56. package/lib/index.umd.js +0 -1
  57. package/lib/mnemonic.d.ts +0 -3
  58. package/lib/utils.d.ts +0 -21
  59. package/src/account.ts +0 -749
  60. package/src/index.ts +0 -11
  61. package/src/mnemonic.ts +0 -2084
  62. package/src/utils.ts +0 -132
@@ -0,0 +1,37 @@
1
+ // 01 — Quick start. Open a workspace, create an HD account, derive wallets,
2
+ // lock down. The shortest end-to-end demo.
3
+
4
+ const test = require("node:test");
5
+ const assert = require("node:assert");
6
+ const { mkdtempSync } = require("node:fs");
7
+ const { tmpdir } = require("node:os");
8
+ const { join } = require("node:path");
9
+
10
+ const { Workspace } = require("wative-core");
11
+
12
+ test("quick start — open, create HD account, derive wallets, lock", async () => {
13
+ const root = mkdtempSync(join(tmpdir(), "wative-quickstart-"));
14
+ const password = "your-workspace-password";
15
+
16
+ const ws = await Workspace.open({ path: root, password });
17
+ assert.strictEqual(ws.locked, false);
18
+
19
+ const account = await ws.accounts.create(
20
+ "Trading Desk",
21
+ password,
22
+ "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
23
+ );
24
+ assert.strictEqual(account.wallets.length, 1);
25
+
26
+ await account.deriveWallets(4);
27
+ assert.strictEqual(account.wallets.length, 5);
28
+
29
+ const wallet = account.wallets[0];
30
+ const evm = wallet.addresses.find((a) => a.vm === "evm");
31
+ const svm = wallet.addresses.find((a) => a.vm === "svm");
32
+ assert.ok(evm && evm.publicKey.startsWith("0x"));
33
+ assert.ok(svm && svm.publicKey.length >= 32);
34
+
35
+ await ws.lock();
36
+ assert.strictEqual(ws.locked, true);
37
+ });
@@ -0,0 +1,71 @@
1
+ // 02 — HD account flows. derive / slice wallets, dump mnemonic, reset password,
2
+ // reopen a locked workspace.
3
+
4
+ const test = require("node:test");
5
+ const assert = require("node:assert");
6
+ const { mkdtempSync } = require("node:fs");
7
+ const { tmpdir } = require("node:os");
8
+ const { join } = require("node:path");
9
+
10
+ const { Workspace } = require("wative-core");
11
+
12
+ const MNEMONIC =
13
+ "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
14
+
15
+ test("HD account — derive, slice, dump mnemonic", async () => {
16
+ const root = mkdtempSync(join(tmpdir(), "wative-hd-"));
17
+ const ws = await Workspace.open({ path: root, password: "wsp-pwd" });
18
+
19
+ const acc = await ws.accounts.create("Desk", "wsp-pwd", MNEMONIC);
20
+ await acc.deriveWallets(10);
21
+ assert.strictEqual(acc.wallets.length, 11);
22
+
23
+ await acc.sliceWallets(6);
24
+ assert.strictEqual(acc.wallets.length, 6);
25
+
26
+ assert.strictEqual(acc.dumpMnemonic(), MNEMONIC);
27
+
28
+ await ws.lock();
29
+ });
30
+
31
+ test("HD account — own password + resetPassword", async () => {
32
+ const root = mkdtempSync(join(tmpdir(), "wative-hd-pwd-"));
33
+ const ws = await Workspace.open({ path: root, password: "wsp-pwd" });
34
+
35
+ const sub = await ws.accounts.create("SubAccount", "sub-pwd", MNEMONIC, undefined, {
36
+ hasOwnPassword: true,
37
+ });
38
+ assert.strictEqual(sub.locked, false);
39
+
40
+ sub.lock();
41
+ assert.strictEqual(sub.locked, true);
42
+
43
+ await assert.rejects(sub.tryUnlock("wrong"), (err) => err.code === "BAD_PASSWORD");
44
+ await sub.tryUnlock("sub-pwd");
45
+
46
+ await sub.resetPassword("sub-pwd", "new-sub-pwd");
47
+ sub.lock();
48
+ await assert.rejects(sub.tryUnlock("sub-pwd"), (err) => err.code === "BAD_PASSWORD");
49
+ await sub.tryUnlock("new-sub-pwd");
50
+
51
+ await ws.lock();
52
+ });
53
+
54
+ test("HD account — round-trip across lock + reopen", async () => {
55
+ const root = mkdtempSync(join(tmpdir(), "wative-hd-reopen-"));
56
+ let ws = await Workspace.open({ path: root, password: "wsp-pwd" });
57
+
58
+ const acc = await ws.accounts.create("Persistent", "wsp-pwd", MNEMONIC);
59
+ await acc.deriveWallets(2);
60
+ const firstAddr = acc.wallets[0].addresses[0].publicKey;
61
+
62
+ await ws.lock();
63
+
64
+ ws = await Workspace.open({ path: root, password: "wsp-pwd" });
65
+ const reopened = ws.accounts.bySlug(acc.slug);
66
+ assert.ok(reopened);
67
+ assert.strictEqual(reopened.wallets.length, 3);
68
+ assert.strictEqual(reopened.wallets[0].addresses[0].publicKey, firstAddr);
69
+
70
+ await ws.lock();
71
+ });
@@ -0,0 +1,59 @@
1
+ // 03 — PK account flows. Heterogeneous private keys, one wallet per import.
2
+
3
+ const test = require("node:test");
4
+ const assert = require("node:assert");
5
+ const { mkdtempSync } = require("node:fs");
6
+ const { tmpdir } = require("node:os");
7
+ const { join } = require("node:path");
8
+
9
+ const { Workspace } = require("wative-core");
10
+
11
+ const EVM_PK = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d";
12
+ const SVM_PK = "xtGWcHvQsr5ue8zG2F5fm31bW8vyn597Y92gUWaZZg3S1Z6FeJMATL8KU3xJMGbLfALnokcct9y5wYtCRrwNXZW";
13
+
14
+ test("PK account — create from EVM key + import a Solana key", async () => {
15
+ const root = mkdtempSync(join(tmpdir(), "wative-pk-"));
16
+ const ws = await Workspace.open({ path: root, password: "wsp-pwd" });
17
+
18
+ const acc = await ws.accounts.create(
19
+ "External Hot Wallet",
20
+ "wsp-pwd",
21
+ EVM_PK,
22
+ undefined,
23
+ { kind: "PK" },
24
+ );
25
+ assert.strictEqual(acc.organizationType, "PK");
26
+ assert.strictEqual(acc.wallets.length, 1);
27
+ assert.strictEqual(acc.wallets[0].addresses[0].vm, "evm");
28
+
29
+ await acc.importPrivateKey(SVM_PK, "svm");
30
+ assert.strictEqual(acc.wallets.length, 2);
31
+ assert.strictEqual(acc.wallets[1].addresses[0].vm, "svm");
32
+
33
+ await ws.lock();
34
+ });
35
+
36
+ test("PK account — duplicate import is rejected", async () => {
37
+ const root = mkdtempSync(join(tmpdir(), "wative-pk-dup-"));
38
+ const ws = await Workspace.open({ path: root, password: "wsp-pwd" });
39
+
40
+ const acc = await ws.accounts.create("HotWallet", "wsp-pwd", EVM_PK, undefined, { kind: "PK" });
41
+ await assert.rejects(acc.importPrivateKey(EVM_PK, "evm"), (err) => err.code === "PARAMETER_ERROR");
42
+
43
+ await ws.lock();
44
+ });
45
+
46
+ test("PK account — drop a wallet (account keeps at least one)", async () => {
47
+ const root = mkdtempSync(join(tmpdir(), "wative-pk-drop-"));
48
+ const ws = await Workspace.open({ path: root, password: "wsp-pwd" });
49
+
50
+ const acc = await ws.accounts.create("HotWallet", "wsp-pwd", EVM_PK, undefined, { kind: "PK" });
51
+ await acc.importPrivateKey(SVM_PK, "svm");
52
+
53
+ await acc.wallets.drop(acc.wallets[1]);
54
+ assert.strictEqual(acc.wallets.length, 1);
55
+
56
+ await assert.rejects(acc.wallets.drop(acc.wallets[0]), (err) => err.code === "UNSUPPORTED_OP");
57
+
58
+ await ws.lock();
59
+ });
@@ -0,0 +1,94 @@
1
+ // 04 — Network management. Pre-loaded networks, add custom networks,
2
+ // override an existing network's RPC URL, drop user networks.
3
+
4
+ const test = require("node:test");
5
+ const assert = require("node:assert");
6
+ const { mkdtempSync } = require("node:fs");
7
+ const { tmpdir } = require("node:os");
8
+ const { join } = require("node:path");
9
+
10
+ const { Workspace, Network } = require("wative-core");
11
+
12
+ test("network — 10 pre-loaded networks ship at workspace open", async () => {
13
+ const root = mkdtempSync(join(tmpdir(), "wative-net-builtin-"));
14
+ const ws = await Workspace.open({ path: root, password: "wsp-pwd" });
15
+
16
+ const slugs = ws.networks.map((n) => n.slug).sort();
17
+ assert.deepStrictEqual(slugs, [
18
+ "arbitrum", "arbitrum-sepolia", "base", "bnbchain", "ethereum", "optimism",
19
+ "sepolia", "solana", "solana-devnet", "solana-testnet",
20
+ ]);
21
+
22
+ assert.strictEqual(Network.ArbitrumSepolia.chainId, 421614);
23
+ assert.strictEqual(Network.Ethereum.chainId, 1);
24
+ assert.strictEqual(Network.Solana.vm, "svm");
25
+ assert.strictEqual(ws.networks.bySlug("ethereum").chainId, 1);
26
+ assert.strictEqual(ws.networks.bySlug("arbitrum-sepolia").vm, "evm");
27
+ assert.strictEqual(ws.networks.byChainId(8453).slug, "base");
28
+
29
+ await ws.lock();
30
+ });
31
+
32
+ test("network — add a brand-new user network", async () => {
33
+ const root = mkdtempSync(join(tmpdir(), "wative-net-add-"));
34
+ const ws = await Workspace.open({ path: root, password: "wsp-pwd" });
35
+
36
+ const monad = new Network({
37
+ slug: "monad-testnet",
38
+ name: "Monad Testnet",
39
+ chainId: 10143,
40
+ rpcUrl: "https://testnet-rpc.monad.xyz",
41
+ nativeCurrency: { name: "Monad", symbol: "MON", decimals: 18 },
42
+ vm: "evm",
43
+ });
44
+ await ws.networks.add(monad);
45
+
46
+ assert.strictEqual(ws.networks.length, 11);
47
+ assert.strictEqual(ws.networks.bySlug("monad-testnet").chainId, 10143);
48
+ await assert.rejects(ws.networks.add(monad), (err) => err.code === "PARAMETER_ERROR");
49
+
50
+ await ws.lock();
51
+ });
52
+
53
+ test("network — override a pre-loaded RPC URL via update()", async () => {
54
+ const root = mkdtempSync(join(tmpdir(), "wative-net-override-"));
55
+ const ws = await Workspace.open({ path: root, password: "wsp-pwd" });
56
+
57
+ const eth = ws.networks.bySlug("ethereum");
58
+ const customEth = new Network({
59
+ ...eth,
60
+ rpcUrl: "https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY",
61
+ });
62
+ await ws.networks.update(customEth);
63
+
64
+ assert.strictEqual(
65
+ ws.networks.bySlug("ethereum").rpcUrl,
66
+ "https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY",
67
+ );
68
+ assert.strictEqual(ws.networks.length, 10);
69
+
70
+ await ws.lock();
71
+ });
72
+
73
+ test("network — drop user network; pre-loaded networks reject drop", async () => {
74
+ const root = mkdtempSync(join(tmpdir(), "wative-net-drop-"));
75
+ const ws = await Workspace.open({ path: root, password: "wsp-pwd" });
76
+
77
+ const custom = new Network({
78
+ slug: "my-net",
79
+ name: "My Net",
80
+ chainId: 99999,
81
+ rpcUrl: "https://rpc.example",
82
+ nativeCurrency: { name: "Foo", symbol: "FOO", decimals: 18 },
83
+ vm: "evm",
84
+ });
85
+ await ws.networks.add(custom);
86
+
87
+ await ws.networks.drop(custom);
88
+ assert.strictEqual(ws.networks.bySlug("my-net"), null);
89
+
90
+ const eth = ws.networks.bySlug("ethereum");
91
+ await assert.rejects(ws.networks.drop(eth), (err) => err.code === "UNSUPPORTED_OP");
92
+
93
+ await ws.lock();
94
+ });
@@ -0,0 +1,117 @@
1
+ // 05 — Asset management. Pre-loaded tokens, add custom tokens,
2
+ // drop user tokens, search via filter.
3
+
4
+ const test = require("node:test");
5
+ const assert = require("node:assert");
6
+ const { mkdtempSync } = require("node:fs");
7
+ const { tmpdir } = require("node:os");
8
+ const { join } = require("node:path");
9
+
10
+ const { Workspace, Network, Asset } = require("wative-core");
11
+
12
+ test("asset — pre-loaded tokens per network", async () => {
13
+ const root = mkdtempSync(join(tmpdir(), "wative-asset-builtin-"));
14
+ const ws = await Workspace.open({ path: root, password: "wsp-pwd" });
15
+
16
+ const ethAssets = await ws.assets(Network.Ethereum);
17
+ assert.deepStrictEqual(ethAssets.map((a) => a.symbol).sort(), ["ETH", "USDC", "USDT", "WETH"]);
18
+
19
+ const solAssets = await ws.assets(Network.Solana);
20
+ assert.deepStrictEqual(solAssets.map((a) => a.symbol).sort(), ["SOL", "USDC", "USDT", "WSOL"]);
21
+
22
+ const sepoliaAssets = await ws.assets("sepolia");
23
+ assert.strictEqual(sepoliaAssets.length, 1);
24
+
25
+ const arbSepoliaAssets = await ws.assets("arbitrum-sepolia");
26
+ const arbSepoliaUsdc = arbSepoliaAssets.find((a) => a.symbol === "USDC");
27
+ assert.ok(arbSepoliaUsdc);
28
+ assert.strictEqual(arbSepoliaUsdc.decimals, 6);
29
+ assert.strictEqual(arbSepoliaUsdc.contractAddress, "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d");
30
+ assert.strictEqual(arbSepoliaUsdc.native, false);
31
+
32
+ await ws.lock();
33
+ });
34
+
35
+ test("asset — add a custom user token", async () => {
36
+ const root = mkdtempSync(join(tmpdir(), "wative-asset-add-"));
37
+ const ws = await Workspace.open({ path: root, password: "wsp-pwd" });
38
+
39
+ const pepe = new Asset({
40
+ id: 200,
41
+ symbol: "PEPE",
42
+ name: "Pepe",
43
+ decimals: 18,
44
+ contractAddress: "0x6982508145454Ce325dDbE47a25d4ec3d2311933",
45
+ network: Network.Ethereum,
46
+ });
47
+ await ws.addAsset(pepe);
48
+
49
+ const ethAssets = await ws.assets(Network.Ethereum);
50
+ assert.strictEqual(ethAssets.length, 5);
51
+ assert.ok(ethAssets.some((a) => a.id === 200 && a.symbol === "PEPE"));
52
+
53
+ await ws.lock();
54
+ });
55
+
56
+ test("asset — id and address collisions are rejected", async () => {
57
+ const root = mkdtempSync(join(tmpdir(), "wative-asset-collide-"));
58
+ const ws = await Workspace.open({ path: root, password: "wsp-pwd" });
59
+
60
+ const dupId = new Asset({
61
+ id: 1,
62
+ symbol: "FAKE",
63
+ name: "Fake",
64
+ decimals: 18,
65
+ contractAddress: "0x1234567890123456789012345678901234567890",
66
+ network: Network.Ethereum,
67
+ });
68
+ await assert.rejects(ws.addAsset(dupId), (err) => err.code === "PARAMETER_ERROR");
69
+
70
+ const dupAddr = new Asset({
71
+ id: 201,
72
+ symbol: "USDC2",
73
+ name: "Other USDC",
74
+ decimals: 6,
75
+ contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
76
+ network: Network.Ethereum,
77
+ });
78
+ await assert.rejects(ws.addAsset(dupAddr), (err) => err.code === "PARAMETER_ERROR");
79
+
80
+ await ws.lock();
81
+ });
82
+
83
+ test("asset — drop user token; pre-loaded tokens reject drop", async () => {
84
+ const root = mkdtempSync(join(tmpdir(), "wative-asset-drop-"));
85
+ const ws = await Workspace.open({ path: root, password: "wsp-pwd" });
86
+
87
+ const my = new Asset({
88
+ id: 300,
89
+ symbol: "MINE",
90
+ name: "Mine",
91
+ decimals: 18,
92
+ contractAddress: "0x1111111111111111111111111111111111111111",
93
+ network: Network.Ethereum,
94
+ });
95
+ await ws.addAsset(my);
96
+ await ws.dropAsset(300);
97
+
98
+ const ethAssets = await ws.assets(Network.Ethereum);
99
+ assert.ok(!ethAssets.some((a) => a.id === 300));
100
+
101
+ await assert.rejects(ws.dropAsset(1), (err) => err.code === "UNSUPPORTED_OP");
102
+
103
+ await ws.lock();
104
+ });
105
+
106
+ test("asset — workspace.filter('Asset', q) finds by symbol or contract address", async () => {
107
+ const root = mkdtempSync(join(tmpdir(), "wative-asset-filter-"));
108
+ const ws = await Workspace.open({ path: root, password: "wsp-pwd" });
109
+
110
+ const bySymbol = await ws.filter("USDC", "Asset");
111
+ assert.ok(bySymbol && bySymbol.symbol === "USDC");
112
+
113
+ const byAddr = await ws.filter("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "Asset");
114
+ assert.ok(byAddr && byAddr.symbol === "USDC");
115
+
116
+ await ws.lock();
117
+ });
@@ -0,0 +1,55 @@
1
+ // 06 — Address signing. Sign personal messages and build transactions
2
+ // fully offline.
3
+
4
+ const test = require("node:test");
5
+ const assert = require("node:assert");
6
+ const { mkdtempSync } = require("node:fs");
7
+ const { tmpdir } = require("node:os");
8
+ const { join } = require("node:path");
9
+
10
+ const { Workspace, Network } = require("wative-core");
11
+
12
+ const MNEMONIC =
13
+ "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
14
+
15
+ test("address — signMessage on EVM and SVM", async () => {
16
+ const root = mkdtempSync(join(tmpdir(), "wative-sign-msg-"));
17
+ const ws = await Workspace.open({ path: root, password: "wsp-pwd" });
18
+
19
+ const acc = await ws.accounts.create("Desk", "wsp-pwd", MNEMONIC);
20
+ const evm = acc.wallets[0].addresses.find((a) => a.vm === "evm");
21
+ const svm = acc.wallets[0].addresses.find((a) => a.vm === "svm");
22
+
23
+ const evmSig = evm.signMessage("hello");
24
+ const svmSig = svm.signMessage("hello");
25
+
26
+ assert.ok(typeof evmSig === "string" && evmSig.startsWith("0x"));
27
+ assert.ok(typeof svmSig === "string" && svmSig.length > 0);
28
+ assert.notStrictEqual(evmSig, svmSig);
29
+
30
+ await ws.lock();
31
+ });
32
+
33
+ test("address — buildTransaction convenience helper (offline)", async () => {
34
+ const root = mkdtempSync(join(tmpdir(), "wative-build-tx-"));
35
+ const ws = await Workspace.open({ path: root, password: "wsp-pwd" });
36
+
37
+ const acc = await ws.accounts.create("Desk", "wsp-pwd", MNEMONIC);
38
+ const evm = acc.wallets[0].addresses.find((a) => a.vm === "evm");
39
+
40
+ const tx = await evm.buildTransaction({
41
+ to: "0x1234567890123456789012345678901234567890",
42
+ value: 1_000_000_000_000_000n,
43
+ chainId: 1,
44
+ nonce: 0,
45
+ gasLimit: 21000n,
46
+ maxFeePerGas: 50_000_000_000n,
47
+ maxPriorityFeePerGas: 1_000_000_000n,
48
+ type: 2,
49
+ network: Network.Ethereum,
50
+ });
51
+
52
+ assert.strictEqual(tx.from.toLowerCase(), evm.publicKey.toLowerCase());
53
+
54
+ await ws.lock();
55
+ });
@@ -0,0 +1,164 @@
1
+ // 07 — Custom storage backend. Extend the abstract `Provider` class to plug
2
+ // `Workspace.open()` into any storage system. This example uses an in-memory
3
+ // key-value store with a trivial XOR cipher; real custom Providers should use
4
+ // an authenticated cipher (AES-GCM, ChaCha20-Poly1305).
5
+
6
+ const test = require("node:test");
7
+ const assert = require("node:assert");
8
+
9
+ const { Workspace, Provider, Record, WativeError } = require("wative-core");
10
+
11
+ /**
12
+ * In-memory `Provider` implementation. Stores records in a Map under
13
+ * filesystem-style paths, encrypted with a single-byte-header XOR scheme.
14
+ */
15
+ class InMemoryProvider extends Provider {
16
+ #store = new Map();
17
+ #dirs = new Set();
18
+ #password = null;
19
+
20
+ constructor(label) {
21
+ super(label);
22
+ this.#dirs.add("/");
23
+ this.#dirs.add("/accounts");
24
+ }
25
+
26
+ _exist(path) { return this.#store.has(path) || this.#dirs.has(path); }
27
+ _read(path) {
28
+ const v = this.#store.get(path);
29
+ if (!v) throw new WativeError("RECORD_NOT_FOUND", `no entry at ${path}`);
30
+ return v;
31
+ }
32
+ _write(path, data) { this.#store.set(path, data); }
33
+ _remove(path) { this.#store.delete(path); }
34
+ _ensureDir(path) { this.#dirs.add(path); }
35
+ _listItems(path) {
36
+ const prefix = path.endsWith("/") ? path : path + "/";
37
+ const out = new Set();
38
+ for (const k of this.#store.keys()) {
39
+ if (k.startsWith(prefix)) {
40
+ const head = k.slice(prefix.length).split("/")[0];
41
+ if (head) out.add(head);
42
+ }
43
+ }
44
+ return Array.from(out);
45
+ }
46
+
47
+ async unlockContainer(password) {
48
+ if (typeof password !== "string" || password.length === 0) return false;
49
+ const path = this.#pathFor("CONFIG", "config");
50
+ if (this.#store.has(path)) {
51
+ try { this.#decrypt(this.#store.get(path), password); }
52
+ catch { return false; }
53
+ }
54
+ this.#password = password;
55
+ return true;
56
+ }
57
+ async lockContainer() { this.#password = null; }
58
+ isContainerUnlocked() { return this.#password !== null; }
59
+
60
+ async loadRecord(type, slug) {
61
+ if (this.#password === null) throw new WativeError("PROVIDER_IO", "container locked");
62
+ const eff = slug ?? this.#defaultSlug(type);
63
+ const path = this.#pathFor(type, eff);
64
+ if (!this.#store.has(path)) throw new WativeError("RECORD_NOT_FOUND", path);
65
+ const encrypted = this.#store.get(path);
66
+ const value = JSON.parse(new TextDecoder().decode(this.#decrypt(encrypted, this.#password)));
67
+ return new Record(eff, path, {
68
+ provider: this,
69
+ recordType: type,
70
+ encrypted,
71
+ value,
72
+ password: this.#password,
73
+ decrypt: (ct, pwd) => this.#decrypt(ct, pwd),
74
+ persist: async (val, pwd) => {
75
+ this.#store.set(path, this.#encrypt(JSON.stringify(val), pwd));
76
+ },
77
+ });
78
+ }
79
+ async loadRecords(type) {
80
+ if (type !== "ACCOUNTS") {
81
+ const r = await this.loadRecord(type).catch((e) => {
82
+ if (e instanceof WativeError && e.code === "RECORD_NOT_FOUND") return null;
83
+ throw e;
84
+ });
85
+ return r ? [r] : [];
86
+ }
87
+ const items = this._listItems("/accounts").filter((n) => n.endsWith(".kv"));
88
+ return Promise.all(items.map((n) => this.loadRecord("ACCOUNTS", n.slice(0, -3))));
89
+ }
90
+ async writeRecord(type, slug, value) {
91
+ if (this.#password === null) throw new WativeError("PROVIDER_IO", "container locked");
92
+ this.#store.set(this.#pathFor(type, slug), this.#encrypt(JSON.stringify(value), this.#password));
93
+ }
94
+ async dropRecord(type, slug) { this.#store.delete(this.#pathFor(type, slug)); }
95
+ async close() { this.#password = null; }
96
+
97
+ #defaultSlug(type) {
98
+ return ({ CONFIG: "config", NETWORK: "network", ASSET: "asset", LOG: "log", ACCOUNTS: "" })[type] || "";
99
+ }
100
+ #pathFor(type, slug) {
101
+ if (type === "ACCOUNTS") return `/accounts/${slug}.kv`;
102
+ return `/${this.#defaultSlug(type)}`;
103
+ }
104
+ #encrypt(plaintext, password) {
105
+ const data = new TextEncoder().encode(plaintext);
106
+ const stream = this.#streamFor(password, data.length);
107
+ const out = new Uint8Array(data.length + 1);
108
+ out[0] = 0x42;
109
+ for (let i = 0; i < data.length; i++) out[i + 1] = data[i] ^ stream[i];
110
+ return out;
111
+ }
112
+ #decrypt(blob, password) {
113
+ if (blob.length < 1 || blob[0] !== 0x42) throw new WativeError("DECRYPT_FAILED", "bad header");
114
+ const ct = blob.subarray(1);
115
+ const stream = this.#streamFor(password, ct.length);
116
+ const out = new Uint8Array(ct.length);
117
+ for (let i = 0; i < ct.length; i++) out[i] = ct[i] ^ stream[i];
118
+ try { JSON.parse(new TextDecoder().decode(out)); }
119
+ catch (e) { throw new WativeError("BAD_PASSWORD", "non-JSON after decrypt", { cause: e }); }
120
+ return out;
121
+ }
122
+ #streamFor(password, length) {
123
+ const seed = new TextEncoder().encode("salt|" + password);
124
+ const out = new Uint8Array(length);
125
+ for (let i = 0; i < length; i++) out[i] = seed[i % seed.length] ^ ((i * 7) & 0xff);
126
+ return out;
127
+ }
128
+ }
129
+
130
+ test("custom provider — full lifecycle against an in-memory backend", async () => {
131
+ const provider = new InMemoryProvider("memory://desk-1");
132
+
133
+ let ws = await Workspace.open({ provider, password: "wsp-pwd" });
134
+ assert.strictEqual(ws.locked, false);
135
+
136
+ const slugs = ws.networks.map((n) => n.slug).sort();
137
+ assert.strictEqual(slugs.length, 10);
138
+ assert.ok(slugs.includes("ethereum"));
139
+ assert.ok(slugs.includes("arbitrum-sepolia"));
140
+
141
+ const acc = await ws.accounts.create(
142
+ "Trader",
143
+ "wsp-pwd",
144
+ "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
145
+ );
146
+ await acc.deriveWallets(2);
147
+ assert.strictEqual(acc.wallets.length, 3);
148
+
149
+ await ws.lock();
150
+ ws = await Workspace.open({ provider, password: "wsp-pwd" });
151
+ const reopened = ws.accounts.bySlug(acc.slug);
152
+ assert.ok(reopened);
153
+ assert.strictEqual(reopened.wallets.length, 3);
154
+
155
+ await ws.lock();
156
+ });
157
+
158
+ test("custom provider — bad password is rejected", async () => {
159
+ const provider = new InMemoryProvider("memory://desk-2");
160
+ const ws = await Workspace.open({ provider, password: "right-pwd" });
161
+ await ws.lock();
162
+
163
+ await assert.rejects(Workspace.open({ provider, password: "wrong-pwd" }), (err) => err.code === "BAD_PASSWORD");
164
+ });