@wopr-network/crypto-plugins 1.0.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 (211) hide show
  1. package/.github/workflows/ci.yml +33 -0
  2. package/.github/workflows/publish.yml +12 -0
  3. package/biome.json +23 -0
  4. package/dist/__tests__/bitcoin-encoder.test.d.ts +2 -0
  5. package/dist/__tests__/bitcoin-encoder.test.d.ts.map +1 -0
  6. package/dist/__tests__/bitcoin-encoder.test.js +97 -0
  7. package/dist/__tests__/bitcoin-encoder.test.js.map +1 -0
  8. package/dist/__tests__/dogecoin-encoder.test.d.ts +2 -0
  9. package/dist/__tests__/dogecoin-encoder.test.d.ts.map +1 -0
  10. package/dist/__tests__/dogecoin-encoder.test.js +57 -0
  11. package/dist/__tests__/dogecoin-encoder.test.js.map +1 -0
  12. package/dist/__tests__/litecoin-encoder.test.d.ts +2 -0
  13. package/dist/__tests__/litecoin-encoder.test.d.ts.map +1 -0
  14. package/dist/__tests__/litecoin-encoder.test.js +44 -0
  15. package/dist/__tests__/litecoin-encoder.test.js.map +1 -0
  16. package/dist/__tests__/registry.test.d.ts +2 -0
  17. package/dist/__tests__/registry.test.d.ts.map +1 -0
  18. package/dist/__tests__/registry.test.js +75 -0
  19. package/dist/__tests__/registry.test.js.map +1 -0
  20. package/dist/__tests__/rpc.test.d.ts +2 -0
  21. package/dist/__tests__/rpc.test.d.ts.map +1 -0
  22. package/dist/__tests__/rpc.test.js +31 -0
  23. package/dist/__tests__/rpc.test.js.map +1 -0
  24. package/dist/__tests__/solana-encoder.test.d.ts +2 -0
  25. package/dist/__tests__/solana-encoder.test.d.ts.map +1 -0
  26. package/dist/__tests__/solana-encoder.test.js +85 -0
  27. package/dist/__tests__/solana-encoder.test.js.map +1 -0
  28. package/dist/__tests__/solana-watcher.test.d.ts +2 -0
  29. package/dist/__tests__/solana-watcher.test.d.ts.map +1 -0
  30. package/dist/__tests__/solana-watcher.test.js +281 -0
  31. package/dist/__tests__/solana-watcher.test.js.map +1 -0
  32. package/dist/__tests__/sweep-key-parity.test.d.ts +2 -0
  33. package/dist/__tests__/sweep-key-parity.test.d.ts.map +1 -0
  34. package/dist/__tests__/sweep-key-parity.test.js +236 -0
  35. package/dist/__tests__/sweep-key-parity.test.js.map +1 -0
  36. package/dist/__tests__/tron-encoder.test.d.ts +2 -0
  37. package/dist/__tests__/tron-encoder.test.d.ts.map +1 -0
  38. package/dist/__tests__/tron-encoder.test.js +93 -0
  39. package/dist/__tests__/tron-encoder.test.js.map +1 -0
  40. package/dist/__tests__/utxo-watcher.test.d.ts +2 -0
  41. package/dist/__tests__/utxo-watcher.test.d.ts.map +1 -0
  42. package/dist/__tests__/utxo-watcher.test.js +218 -0
  43. package/dist/__tests__/utxo-watcher.test.js.map +1 -0
  44. package/dist/bitcoin/encoder.d.ts +15 -0
  45. package/dist/bitcoin/encoder.d.ts.map +1 -0
  46. package/dist/bitcoin/encoder.js +286 -0
  47. package/dist/bitcoin/encoder.js.map +1 -0
  48. package/dist/bitcoin/index.d.ts +4 -0
  49. package/dist/bitcoin/index.d.ts.map +1 -0
  50. package/dist/bitcoin/index.js +20 -0
  51. package/dist/bitcoin/index.js.map +1 -0
  52. package/dist/dogecoin/encoder.d.ts +19 -0
  53. package/dist/dogecoin/encoder.d.ts.map +1 -0
  54. package/dist/dogecoin/encoder.js +145 -0
  55. package/dist/dogecoin/encoder.js.map +1 -0
  56. package/dist/dogecoin/index.d.ts +4 -0
  57. package/dist/dogecoin/index.d.ts.map +1 -0
  58. package/dist/dogecoin/index.js +20 -0
  59. package/dist/dogecoin/index.js.map +1 -0
  60. package/dist/evm/encoder.d.ts +7 -0
  61. package/dist/evm/encoder.d.ts.map +1 -0
  62. package/dist/evm/encoder.js +43 -0
  63. package/dist/evm/encoder.js.map +1 -0
  64. package/dist/evm/eth-watcher.d.ts +38 -0
  65. package/dist/evm/eth-watcher.d.ts.map +1 -0
  66. package/dist/evm/eth-watcher.js +138 -0
  67. package/dist/evm/eth-watcher.js.map +1 -0
  68. package/dist/evm/index.d.ts +16 -0
  69. package/dist/evm/index.d.ts.map +1 -0
  70. package/dist/evm/index.js +34 -0
  71. package/dist/evm/index.js.map +1 -0
  72. package/dist/evm/types.d.ts +43 -0
  73. package/dist/evm/types.d.ts.map +1 -0
  74. package/dist/evm/types.js +101 -0
  75. package/dist/evm/types.js.map +1 -0
  76. package/dist/evm/watcher.d.ts +42 -0
  77. package/dist/evm/watcher.d.ts.map +1 -0
  78. package/dist/evm/watcher.js +162 -0
  79. package/dist/evm/watcher.js.map +1 -0
  80. package/dist/index.d.ts +7 -0
  81. package/dist/index.d.ts.map +1 -0
  82. package/dist/index.js +7 -0
  83. package/dist/index.js.map +1 -0
  84. package/dist/litecoin/encoder.d.ts +8 -0
  85. package/dist/litecoin/encoder.d.ts.map +1 -0
  86. package/dist/litecoin/encoder.js +16 -0
  87. package/dist/litecoin/encoder.js.map +1 -0
  88. package/dist/litecoin/index.d.ts +4 -0
  89. package/dist/litecoin/index.d.ts.map +1 -0
  90. package/dist/litecoin/index.js +20 -0
  91. package/dist/litecoin/index.js.map +1 -0
  92. package/dist/shared/test-helpers/index.d.ts +9 -0
  93. package/dist/shared/test-helpers/index.d.ts.map +1 -0
  94. package/dist/shared/test-helpers/index.js +30 -0
  95. package/dist/shared/test-helpers/index.js.map +1 -0
  96. package/dist/shared/utxo/index.d.ts +5 -0
  97. package/dist/shared/utxo/index.d.ts.map +1 -0
  98. package/dist/shared/utxo/index.js +3 -0
  99. package/dist/shared/utxo/index.js.map +1 -0
  100. package/dist/shared/utxo/rpc.d.ts +24 -0
  101. package/dist/shared/utxo/rpc.d.ts.map +1 -0
  102. package/dist/shared/utxo/rpc.js +75 -0
  103. package/dist/shared/utxo/rpc.js.map +1 -0
  104. package/dist/shared/utxo/types.d.ts +40 -0
  105. package/dist/shared/utxo/types.d.ts.map +1 -0
  106. package/dist/shared/utxo/types.js +2 -0
  107. package/dist/shared/utxo/types.js.map +1 -0
  108. package/dist/shared/utxo/watcher.d.ts +55 -0
  109. package/dist/shared/utxo/watcher.d.ts.map +1 -0
  110. package/dist/shared/utxo/watcher.js +150 -0
  111. package/dist/shared/utxo/watcher.js.map +1 -0
  112. package/dist/solana/encoder.d.ts +13 -0
  113. package/dist/solana/encoder.d.ts.map +1 -0
  114. package/dist/solana/encoder.js +62 -0
  115. package/dist/solana/encoder.js.map +1 -0
  116. package/dist/solana/index.d.ts +17 -0
  117. package/dist/solana/index.d.ts.map +1 -0
  118. package/dist/solana/index.js +32 -0
  119. package/dist/solana/index.js.map +1 -0
  120. package/dist/solana/sweeper.d.ts +47 -0
  121. package/dist/solana/sweeper.d.ts.map +1 -0
  122. package/dist/solana/sweeper.js +151 -0
  123. package/dist/solana/sweeper.js.map +1 -0
  124. package/dist/solana/types.d.ts +49 -0
  125. package/dist/solana/types.d.ts.map +1 -0
  126. package/dist/solana/types.js +2 -0
  127. package/dist/solana/types.js.map +1 -0
  128. package/dist/solana/watcher.d.ts +59 -0
  129. package/dist/solana/watcher.d.ts.map +1 -0
  130. package/dist/solana/watcher.js +251 -0
  131. package/dist/solana/watcher.js.map +1 -0
  132. package/dist/sweep/evm-sweeper.d.ts +31 -0
  133. package/dist/sweep/evm-sweeper.d.ts.map +1 -0
  134. package/dist/sweep/evm-sweeper.js +229 -0
  135. package/dist/sweep/evm-sweeper.js.map +1 -0
  136. package/dist/sweep/index.d.ts +22 -0
  137. package/dist/sweep/index.d.ts.map +1 -0
  138. package/dist/sweep/index.js +290 -0
  139. package/dist/sweep/index.js.map +1 -0
  140. package/dist/sweep/tron-sweeper.d.ts +40 -0
  141. package/dist/sweep/tron-sweeper.d.ts.map +1 -0
  142. package/dist/sweep/tron-sweeper.js +363 -0
  143. package/dist/sweep/tron-sweeper.js.map +1 -0
  144. package/dist/sweep/utxo-sweeper.d.ts +14 -0
  145. package/dist/sweep/utxo-sweeper.d.ts.map +1 -0
  146. package/dist/sweep/utxo-sweeper.js +13 -0
  147. package/dist/sweep/utxo-sweeper.js.map +1 -0
  148. package/dist/tron/address-convert.d.ts +15 -0
  149. package/dist/tron/address-convert.d.ts.map +1 -0
  150. package/dist/tron/address-convert.js +95 -0
  151. package/dist/tron/address-convert.js.map +1 -0
  152. package/dist/tron/encoder.d.ts +20 -0
  153. package/dist/tron/encoder.d.ts.map +1 -0
  154. package/dist/tron/encoder.js +67 -0
  155. package/dist/tron/encoder.js.map +1 -0
  156. package/dist/tron/index.d.ts +6 -0
  157. package/dist/tron/index.d.ts.map +1 -0
  158. package/dist/tron/index.js +20 -0
  159. package/dist/tron/index.js.map +1 -0
  160. package/dist/tron/sha256.d.ts +6 -0
  161. package/dist/tron/sha256.d.ts.map +1 -0
  162. package/dist/tron/sha256.js +90 -0
  163. package/dist/tron/sha256.js.map +1 -0
  164. package/dist/tron/watcher.d.ts +42 -0
  165. package/dist/tron/watcher.d.ts.map +1 -0
  166. package/dist/tron/watcher.js +168 -0
  167. package/dist/tron/watcher.js.map +1 -0
  168. package/package.json +47 -0
  169. package/src/__tests__/bitcoin-encoder.test.ts +115 -0
  170. package/src/__tests__/dogecoin-encoder.test.ts +66 -0
  171. package/src/__tests__/litecoin-encoder.test.ts +51 -0
  172. package/src/__tests__/registry.test.ts +91 -0
  173. package/src/__tests__/rpc.test.ts +36 -0
  174. package/src/__tests__/solana-encoder.test.ts +103 -0
  175. package/src/__tests__/solana-watcher.test.ts +316 -0
  176. package/src/__tests__/sweep-key-parity.test.ts +302 -0
  177. package/src/__tests__/tron-encoder.test.ts +108 -0
  178. package/src/__tests__/utxo-watcher.test.ts +252 -0
  179. package/src/bitcoin/encoder.ts +320 -0
  180. package/src/bitcoin/index.ts +23 -0
  181. package/src/dogecoin/encoder.ts +161 -0
  182. package/src/dogecoin/index.ts +23 -0
  183. package/src/evm/encoder.ts +49 -0
  184. package/src/evm/eth-watcher.ts +168 -0
  185. package/src/evm/index.ts +46 -0
  186. package/src/evm/types.ts +146 -0
  187. package/src/evm/watcher.ts +189 -0
  188. package/src/index.ts +21 -0
  189. package/src/litecoin/encoder.ts +18 -0
  190. package/src/litecoin/index.ts +23 -0
  191. package/src/shared/test-helpers/index.ts +36 -0
  192. package/src/shared/utxo/index.ts +12 -0
  193. package/src/shared/utxo/rpc.ts +80 -0
  194. package/src/shared/utxo/types.ts +43 -0
  195. package/src/shared/utxo/watcher.ts +195 -0
  196. package/src/solana/encoder.ts +72 -0
  197. package/src/solana/index.ts +36 -0
  198. package/src/solana/sweeper.ts +196 -0
  199. package/src/solana/types.ts +52 -0
  200. package/src/solana/watcher.ts +282 -0
  201. package/src/sweep/evm-sweeper.ts +296 -0
  202. package/src/sweep/index.ts +353 -0
  203. package/src/sweep/tron-sweeper.ts +467 -0
  204. package/src/sweep/utxo-sweeper.ts +23 -0
  205. package/src/tron/address-convert.ts +91 -0
  206. package/src/tron/encoder.ts +74 -0
  207. package/src/tron/index.ts +23 -0
  208. package/src/tron/sha256.ts +100 -0
  209. package/src/tron/watcher.ts +208 -0
  210. package/tsconfig.json +17 -0
  211. package/vitest.config.ts +8 -0
@@ -0,0 +1,302 @@
1
+ import { ed25519 } from "@noble/curves/ed25519.js";
2
+ import { hmac } from "@noble/hashes/hmac.js";
3
+ import { sha512 } from "@noble/hashes/sha2.js";
4
+ import { HDKey } from "@scure/bip32";
5
+ import * as bip39 from "@scure/bip39";
6
+ import { privateKeyToAccount } from "viem/accounts";
7
+ import { describe, expect, it } from "vitest";
8
+
9
+ import { bech32Encoder } from "../bitcoin/encoder.js";
10
+ import { p2pkhEncoder } from "../dogecoin/encoder.js";
11
+ import { EvmAddressEncoder } from "../evm/encoder.js";
12
+ import { bech32Encoder as ltcBech32Encoder } from "../litecoin/encoder.js";
13
+ import { SolanaAddressEncoder } from "../solana/encoder.js";
14
+ import { keccakB58Encoder } from "../tron/encoder.js";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Well-known BIP-39 test mnemonic (public, never use in production)
18
+ // ---------------------------------------------------------------------------
19
+
20
+ const TEST_MNEMONIC = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
21
+ const TEST_SEED = bip39.mnemonicToSeedSync(TEST_MNEMONIC);
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ function toHex(bytes: Uint8Array): string {
28
+ return Array.from(bytes as unknown as number[], (b: number) => b.toString(16).padStart(2, "0")).join("");
29
+ }
30
+
31
+ function privKeyHex(key: Uint8Array): `0x${string}` {
32
+ return `0x${toHex(key)}` as `0x${string}`;
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // secp256k1 chain configs
37
+ // ---------------------------------------------------------------------------
38
+
39
+ interface Secp256k1ChainConfig {
40
+ name: string;
41
+ coinType: number;
42
+ encoder: { encode(pk: Uint8Array, params: Record<string, unknown>): string };
43
+ params: Record<string, unknown>;
44
+ addrRegex: RegExp;
45
+ }
46
+
47
+ const SECP256K1_CHAINS: Secp256k1ChainConfig[] = [
48
+ {
49
+ name: "Bitcoin",
50
+ coinType: 0,
51
+ encoder: bech32Encoder,
52
+ params: { hrp: "bc" },
53
+ addrRegex: /^bc1q/,
54
+ },
55
+ {
56
+ name: "Litecoin",
57
+ coinType: 2,
58
+ encoder: ltcBech32Encoder,
59
+ params: { hrp: "ltc" },
60
+ addrRegex: /^ltc1q/,
61
+ },
62
+ {
63
+ name: "Dogecoin",
64
+ coinType: 3,
65
+ encoder: p2pkhEncoder,
66
+ params: { version: "0x1e" },
67
+ addrRegex: /^D/,
68
+ },
69
+ {
70
+ name: "EVM",
71
+ coinType: 60,
72
+ encoder: new EvmAddressEncoder(),
73
+ params: {},
74
+ addrRegex: /^0x[0-9a-fA-F]{40}$/,
75
+ },
76
+ {
77
+ name: "Tron",
78
+ coinType: 195,
79
+ encoder: keccakB58Encoder,
80
+ params: { version: "0x41" },
81
+ addrRegex: /^T/,
82
+ },
83
+ ];
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // SLIP-0010 Ed25519 derivation (manual, since @scure/bip32 is secp256k1 only)
87
+ // ---------------------------------------------------------------------------
88
+
89
+ interface Slip0010Key {
90
+ privateKey: Uint8Array;
91
+ chainCode: Uint8Array;
92
+ }
93
+
94
+ function slip0010MasterKey(seed: Uint8Array): Slip0010Key {
95
+ const I = hmac(sha512, new TextEncoder().encode("ed25519 seed"), seed);
96
+ return {
97
+ privateKey: I.slice(0, 32),
98
+ chainCode: I.slice(32),
99
+ };
100
+ }
101
+
102
+ function slip0010DeriveChild(parent: Slip0010Key, index: number): Slip0010Key {
103
+ // SLIP-0010 Ed25519: only hardened derivation (index >= 0x80000000)
104
+ const hardenedIndex = (index | 0x80000000) >>> 0;
105
+ const data = new Uint8Array(1 + 32 + 4);
106
+ data[0] = 0x00;
107
+ data.set(parent.privateKey, 1);
108
+ const view = new DataView(data.buffer);
109
+ view.setUint32(33, hardenedIndex, false); // big-endian
110
+ const I = hmac(sha512, parent.chainCode, data);
111
+ return {
112
+ privateKey: I.slice(0, 32),
113
+ chainCode: I.slice(32),
114
+ };
115
+ }
116
+
117
+ function slip0010DerivePath(seed: Uint8Array, path: number[]): Slip0010Key {
118
+ let key = slip0010MasterKey(seed);
119
+ for (const index of path) {
120
+ key = slip0010DeriveChild(key, index);
121
+ }
122
+ return key;
123
+ }
124
+
125
+ // ===========================================================================
126
+ // Tests
127
+ // ===========================================================================
128
+
129
+ describe("sweep key parity -- mnemonic private key controls derived address", () => {
130
+ // -----------------------------------------------------------------------
131
+ // secp256k1 chains: pubkey from xpub must equal pubkey from privkey
132
+ // -----------------------------------------------------------------------
133
+
134
+ describe("secp256k1 chains", () => {
135
+ for (const chain of SECP256K1_CHAINS) {
136
+ const path = `m/44'/${chain.coinType}'/0'`;
137
+
138
+ it(`${chain.name}: privkey pubkey matches xpub pubkey at indices 0-4`, () => {
139
+ const masterHD = HDKey.fromMasterSeed(TEST_SEED);
140
+ const accountKey = masterHD.derive(path);
141
+ const xpub = accountKey.publicExtendedKey;
142
+
143
+ for (let i = 0; i < 5; i++) {
144
+ // From full key (has private key): derive external chain (0), index i
145
+ const fullChild = accountKey.deriveChild(0).deriveChild(i);
146
+ // From xpub only: derive external chain (0), index i
147
+ const xpubChild = HDKey.fromExtendedKey(xpub).deriveChild(0).deriveChild(i);
148
+
149
+ const fullPk = fullChild.publicKey as Uint8Array;
150
+ const xpubPk = xpubChild.publicKey as Uint8Array;
151
+
152
+ expect(fullPk).toBeDefined();
153
+ expect(xpubPk).toBeDefined();
154
+ expect(toHex(fullPk)).toBe(toHex(xpubPk));
155
+ }
156
+ });
157
+
158
+ it(`${chain.name}: encoder produces valid address from derived pubkey`, () => {
159
+ const masterHD = HDKey.fromMasterSeed(TEST_SEED);
160
+ const accountKey = masterHD.derive(path);
161
+ const xpub = accountKey.publicExtendedKey;
162
+
163
+ for (let i = 0; i < 5; i++) {
164
+ const xpubChild = HDKey.fromExtendedKey(xpub).deriveChild(0).deriveChild(i);
165
+ const pubkey = xpubChild.publicKey as Uint8Array;
166
+ const address = chain.encoder.encode(pubkey, chain.params);
167
+
168
+ expect(address).toMatch(chain.addrRegex);
169
+ }
170
+ });
171
+
172
+ it(`${chain.name}: different indices produce different addresses`, () => {
173
+ const masterHD = HDKey.fromMasterSeed(TEST_SEED);
174
+ const accountKey = masterHD.derive(path);
175
+ const xpub = accountKey.publicExtendedKey;
176
+
177
+ const addresses = new Set<string>();
178
+ for (let i = 0; i < 5; i++) {
179
+ const xpubChild = HDKey.fromExtendedKey(xpub).deriveChild(0).deriveChild(i);
180
+ const pubkey = xpubChild.publicKey as Uint8Array;
181
+ const address = chain.encoder.encode(pubkey, chain.params);
182
+ addresses.add(address);
183
+ }
184
+ expect(addresses.size).toBe(5);
185
+ });
186
+
187
+ it(`${chain.name}: derivation is deterministic`, () => {
188
+ const masterHD = HDKey.fromMasterSeed(TEST_SEED);
189
+ const accountKey = masterHD.derive(path);
190
+ const xpub = accountKey.publicExtendedKey;
191
+
192
+ const xpubChild = HDKey.fromExtendedKey(xpub).deriveChild(0).deriveChild(3);
193
+ const pubkey = xpubChild.publicKey as Uint8Array;
194
+ const addr1 = chain.encoder.encode(pubkey, chain.params);
195
+ const addr2 = chain.encoder.encode(pubkey, chain.params);
196
+ expect(addr1).toBe(addr2);
197
+ });
198
+ }
199
+ });
200
+
201
+ // -----------------------------------------------------------------------
202
+ // EVM signing proof: viem privateKeyToAccount matches encoder output
203
+ // -----------------------------------------------------------------------
204
+
205
+ describe("EVM signing proof", () => {
206
+ it("viem privateKeyToAccount matches encoder output at indices 0-9", () => {
207
+ const evmPath = "m/44'/60'/0'";
208
+ const masterHD = HDKey.fromMasterSeed(TEST_SEED);
209
+ const accountKey = masterHD.derive(evmPath);
210
+ const encoder = new EvmAddressEncoder();
211
+
212
+ for (let i = 0; i < 10; i++) {
213
+ const child = accountKey.deriveChild(0).deriveChild(i);
214
+ const privKey = child.privateKey as Uint8Array;
215
+ const pubKey = child.publicKey as Uint8Array;
216
+
217
+ // Address from encoder (pubkey -> keccak -> EIP-55)
218
+ const encoderAddr = encoder.encode(pubKey, {});
219
+
220
+ // Address from viem (privkey -> account -> address)
221
+ const viemAccount = privateKeyToAccount(privKeyHex(privKey));
222
+
223
+ expect(viemAccount.address.toLowerCase()).toBe(encoderAddr.toLowerCase());
224
+ }
225
+ });
226
+
227
+ it("viem treasury key matches encoder output", () => {
228
+ const evmPath = "m/44'/60'/0'";
229
+ const masterHD = HDKey.fromMasterSeed(TEST_SEED);
230
+ const accountKey = masterHD.derive(evmPath);
231
+ const encoder = new EvmAddressEncoder();
232
+
233
+ // Treasury: chain index 1, address index 0
234
+ const child = accountKey.deriveChild(1).deriveChild(0);
235
+ const privKey = child.privateKey as Uint8Array;
236
+ const pubKey = child.publicKey as Uint8Array;
237
+
238
+ const encoderAddr = encoder.encode(pubKey, {});
239
+ const viemAccount = privateKeyToAccount(privKeyHex(privKey));
240
+
241
+ expect(viemAccount.address.toLowerCase()).toBe(encoderAddr.toLowerCase());
242
+ });
243
+ });
244
+
245
+ // -----------------------------------------------------------------------
246
+ // Solana Ed25519 (SLIP-0010 derivation)
247
+ // -----------------------------------------------------------------------
248
+
249
+ describe("Solana Ed25519", () => {
250
+ const solanaEncoder = new SolanaAddressEncoder();
251
+
252
+ it("SLIP-0010 derivation produces valid Solana address", () => {
253
+ // m/44'/501'/0'/0' -- all hardened for SLIP-0010 Ed25519
254
+ const derived = slip0010DerivePath(TEST_SEED, [44, 501, 0, 0]);
255
+ const publicKey = ed25519.getPublicKey(derived.privateKey);
256
+
257
+ expect(publicKey.length).toBe(32);
258
+
259
+ const address = solanaEncoder.encode(publicKey, {});
260
+
261
+ // Valid Base58 string, typical Solana address length 32-44 chars
262
+ expect(address).toMatch(/^[1-9A-HJ-NP-Za-km-z]+$/);
263
+ expect(address.length).toBeGreaterThanOrEqual(32);
264
+ expect(address.length).toBeLessThanOrEqual(44);
265
+ });
266
+
267
+ it("same seed always produces same address", () => {
268
+ const derived1 = slip0010DerivePath(TEST_SEED, [44, 501, 0, 0]);
269
+ const pub1 = ed25519.getPublicKey(derived1.privateKey);
270
+ const addr1 = solanaEncoder.encode(pub1, {});
271
+
272
+ const derived2 = slip0010DerivePath(TEST_SEED, [44, 501, 0, 0]);
273
+ const pub2 = ed25519.getPublicKey(derived2.privateKey);
274
+ const addr2 = solanaEncoder.encode(pub2, {});
275
+
276
+ expect(addr1).toBe(addr2);
277
+ });
278
+
279
+ it("different account indices produce different addresses", () => {
280
+ const addresses = new Set<string>();
281
+ for (let i = 0; i < 5; i++) {
282
+ // m/44'/501'/i'/0'
283
+ const derived = slip0010DerivePath(TEST_SEED, [44, 501, i, 0]);
284
+ const publicKey = ed25519.getPublicKey(derived.privateKey);
285
+ const address = solanaEncoder.encode(publicKey, {});
286
+ addresses.add(address);
287
+ }
288
+ expect(addresses.size).toBe(5);
289
+ });
290
+
291
+ it("privkey and pubkey are consistent via Ed25519", () => {
292
+ const derived = slip0010DerivePath(TEST_SEED, [44, 501, 0, 0]);
293
+ const publicKey = ed25519.getPublicKey(derived.privateKey);
294
+
295
+ // Sign something and verify to prove the key pair is valid
296
+ const message = new TextEncoder().encode("test message");
297
+ const signature = ed25519.sign(message, derived.privateKey);
298
+ const valid = ed25519.verify(signature, message, publicKey);
299
+ expect(valid).toBe(true);
300
+ });
301
+ });
302
+ });
@@ -0,0 +1,108 @@
1
+ import { HDKey } from "@scure/bip32";
2
+ import { describe, expect, it } from "vitest";
3
+ import { hexToTron, isTronAddress, tronToHex } from "../tron/address-convert.js";
4
+ import { encodeKeccakB58Address, keccakB58Encoder } from "../tron/encoder.js";
5
+
6
+ const TEST_XPUB =
7
+ "xpub6BnqJwdqnXEZdkynN5CsrYZr3MULY933SdLrswFfKPDFandTXPQDWY225FveTPUJXS8D91Ddp7FEfaGrvVxuMBGQsyoBYRLu6VMB3Ni2H2Z";
8
+
9
+ function derivePublicKey(xpub: string, index: number): Uint8Array {
10
+ const master = HDKey.fromExtendedKey(xpub);
11
+ const child = master.deriveChild(0).deriveChild(index);
12
+ if (!child.publicKey) throw new Error("Failed to derive public key");
13
+ return child.publicKey;
14
+ }
15
+
16
+ describe("Tron keccak-b58check encoder", () => {
17
+ it("produces T... addresses with default version byte 0x41", () => {
18
+ const pubkey = derivePublicKey(TEST_XPUB, 0);
19
+ const address = keccakB58Encoder.encode(pubkey, {});
20
+ expect(address).toMatch(/^T[1-9A-HJ-NP-Za-km-z]+$/);
21
+ });
22
+
23
+ it("index 0 produces TDTkBJWfXqfCPhNAgHxmgPNHigJEg4ghww", () => {
24
+ const pubkey = derivePublicKey(TEST_XPUB, 0);
25
+ const address = keccakB58Encoder.encode(pubkey, {});
26
+ expect(address).toBe("TDTkBJWfXqfCPhNAgHxmgPNHigJEg4ghww");
27
+ });
28
+
29
+ it("produces different addresses for different indices", () => {
30
+ const addr0 = keccakB58Encoder.encode(derivePublicKey(TEST_XPUB, 0), {});
31
+ const addr1 = keccakB58Encoder.encode(derivePublicKey(TEST_XPUB, 1), {});
32
+ expect(addr0).not.toBe(addr1);
33
+ });
34
+
35
+ it("produces consistent results for the same key", () => {
36
+ const pubkey = derivePublicKey(TEST_XPUB, 0);
37
+ const addr1 = keccakB58Encoder.encode(pubkey, {});
38
+ const addr2 = keccakB58Encoder.encode(pubkey, {});
39
+ expect(addr1).toBe(addr2);
40
+ });
41
+
42
+ it("respects custom version param", () => {
43
+ const pubkey = derivePublicKey(TEST_XPUB, 0);
44
+ // Different version byte produces different address
45
+ const addr41 = keccakB58Encoder.encode(pubkey, {});
46
+ const addr00 = keccakB58Encoder.encode(pubkey, { version: "0" });
47
+ expect(addr41).not.toBe(addr00);
48
+ });
49
+
50
+ it("encodeKeccakB58Address function works directly", () => {
51
+ const pubkey = derivePublicKey(TEST_XPUB, 0);
52
+ const address = encodeKeccakB58Address(pubkey, 0x41);
53
+ expect(address).toMatch(/^T/);
54
+ });
55
+
56
+ it("reports encoding type as keccak-b58check", () => {
57
+ expect(keccakB58Encoder.encodingType()).toBe("keccak-b58check");
58
+ });
59
+
60
+ it("addresses only contain valid Base58 characters", () => {
61
+ const pubkey = derivePublicKey(TEST_XPUB, 0);
62
+ const address = keccakB58Encoder.encode(pubkey, {});
63
+ expect(address).not.toMatch(/[0OIl]/);
64
+ });
65
+ });
66
+
67
+ describe("Tron address conversion", () => {
68
+ it("tronToHex converts T... to 0x hex", () => {
69
+ const pubkey = derivePublicKey(TEST_XPUB, 0);
70
+ const tronAddr = keccakB58Encoder.encode(pubkey, {});
71
+ const hex = tronToHex(tronAddr);
72
+ expect(hex).toMatch(/^0x[0-9a-f]{40}$/);
73
+ });
74
+
75
+ it("hexToTron converts 0x hex back to T...", () => {
76
+ const pubkey = derivePublicKey(TEST_XPUB, 0);
77
+ const tronAddr = keccakB58Encoder.encode(pubkey, {});
78
+ const hex = tronToHex(tronAddr);
79
+ const roundTrip = hexToTron(hex);
80
+ expect(roundTrip).toBe(tronAddr);
81
+ });
82
+
83
+ it("tronToHex throws on non-T address", () => {
84
+ expect(() => tronToHex("D7abc123")).toThrow("Not a Tron address");
85
+ });
86
+
87
+ it("hexToTron throws on wrong-length hex", () => {
88
+ expect(() => hexToTron("0x1234")).toThrow("Invalid hex address length");
89
+ });
90
+
91
+ it("isTronAddress detects T... addresses", () => {
92
+ const pubkey = derivePublicKey(TEST_XPUB, 0);
93
+ const tronAddr = keccakB58Encoder.encode(pubkey, {});
94
+ expect(isTronAddress(tronAddr)).toBe(true);
95
+ expect(isTronAddress("0x1234567890abcdef1234567890abcdef12345678")).toBe(false);
96
+ expect(isTronAddress("D7abcdef")).toBe(false);
97
+ });
98
+
99
+ it("round-trip through multiple addresses", () => {
100
+ for (let i = 0; i < 5; i++) {
101
+ const pubkey = derivePublicKey(TEST_XPUB, i);
102
+ const tronAddr = keccakB58Encoder.encode(pubkey, {});
103
+ const hex = tronToHex(tronAddr);
104
+ const back = hexToTron(hex);
105
+ expect(back).toBe(tronAddr);
106
+ }
107
+ });
108
+ });
@@ -0,0 +1,252 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { createMockCursorStore, createMockOracle } from "../shared/test-helpers/index.js";
3
+ import type { RpcCall } from "../shared/utxo/types.js";
4
+ import { UtxoWatcher } from "../shared/utxo/watcher.js";
5
+
6
+ function createTestWatcher(rpc: RpcCall, opts?: { confirmations?: number; token?: string; chain?: string }) {
7
+ const cursorStore = createMockCursorStore();
8
+ const oracle = createMockOracle();
9
+ const watcher = new UtxoWatcher({
10
+ rpc,
11
+ token: opts?.token ?? "BTC",
12
+ chain: opts?.chain ?? "bitcoin",
13
+ decimals: 8,
14
+ confirmations: opts?.confirmations ?? 3,
15
+ oracle,
16
+ cursorStore,
17
+ });
18
+ return { watcher, cursorStore, oracle };
19
+ }
20
+
21
+ describe("UtxoWatcher", () => {
22
+ it("returns empty array when no addresses are watched", async () => {
23
+ const rpc = vi.fn();
24
+ const { watcher } = createTestWatcher(rpc);
25
+ const events = await watcher.poll();
26
+ expect(events).toEqual([]);
27
+ expect(rpc).not.toHaveBeenCalled();
28
+ });
29
+
30
+ it("detects a payment with confirmations", async () => {
31
+ const rpc = vi.fn<RpcCall>().mockImplementation(async (method, _params) => {
32
+ if (method === "listreceivedbyaddress") {
33
+ return [{ address: "bc1qtest", amount: 0.001, confirmations: 3, txids: ["tx1"] }];
34
+ }
35
+ if (method === "gettransaction") {
36
+ return {
37
+ details: [{ address: "bc1qtest", amount: 0.001, category: "receive" }],
38
+ confirmations: 3,
39
+ };
40
+ }
41
+ return null;
42
+ });
43
+
44
+ const { watcher } = createTestWatcher(rpc, { confirmations: 3 });
45
+ watcher.setWatchedAddresses(["bc1qtest"]);
46
+
47
+ const events = await watcher.poll();
48
+ expect(events).toHaveLength(1);
49
+ expect(events[0]!.chain).toBe("bitcoin");
50
+ expect(events[0]!.token).toBe("BTC");
51
+ expect(events[0]!.to).toBe("bc1qtest");
52
+ expect(events[0]!.txHash).toBe("tx1");
53
+ expect(events[0]!.confirmations).toBe(3);
54
+ expect(events[0]!.confirmationsRequired).toBe(3);
55
+ // 0.001 BTC = 100,000 sats. priceMicros=100_000_000_000 (=$100,000).
56
+ // nativeToCents(100000, 100000000000, 8) = (100000 * 100000000000) / (10000 * 10^8) = 10000000000000000 / 1000000000000 = 10000
57
+ expect(events[0]!.rawAmount).toBe("100000");
58
+ expect(events[0]!.amountUsdCents).toBe(10000); // $100.00
59
+ });
60
+
61
+ it("skips addresses not in the watch set", async () => {
62
+ const rpc = vi.fn<RpcCall>().mockImplementation(async (method) => {
63
+ if (method === "listreceivedbyaddress") {
64
+ return [{ address: "bc1qother", amount: 1.0, confirmations: 6, txids: ["tx2"] }];
65
+ }
66
+ return null;
67
+ });
68
+
69
+ const { watcher } = createTestWatcher(rpc);
70
+ watcher.setWatchedAddresses(["bc1qmine"]);
71
+
72
+ const events = await watcher.poll();
73
+ expect(events).toEqual([]);
74
+ });
75
+
76
+ it("skips txs that have not gained new confirmations", async () => {
77
+ const rpc = vi.fn<RpcCall>().mockImplementation(async (method) => {
78
+ if (method === "listreceivedbyaddress") {
79
+ return [{ address: "bc1qtest", amount: 0.5, confirmations: 2, txids: ["tx3"] }];
80
+ }
81
+ if (method === "gettransaction") {
82
+ return {
83
+ details: [{ address: "bc1qtest", amount: 0.5, category: "receive" }],
84
+ confirmations: 2,
85
+ };
86
+ }
87
+ return null;
88
+ });
89
+
90
+ const { watcher } = createTestWatcher(rpc, { confirmations: 6 });
91
+ watcher.setWatchedAddresses(["bc1qtest"]);
92
+
93
+ // First poll: should return event
94
+ const first = await watcher.poll();
95
+ expect(first).toHaveLength(1);
96
+
97
+ // Second poll with same confirmations: should skip
98
+ const second = await watcher.poll();
99
+ expect(second).toEqual([]);
100
+ });
101
+
102
+ it("emits new event when confirmations increase", async () => {
103
+ let confirmations = 1;
104
+ const rpc = vi.fn<RpcCall>().mockImplementation(async (method) => {
105
+ if (method === "listreceivedbyaddress") {
106
+ return [{ address: "bc1qtest", amount: 0.1, confirmations, txids: ["tx4"] }];
107
+ }
108
+ if (method === "gettransaction") {
109
+ return {
110
+ details: [{ address: "bc1qtest", amount: 0.1, category: "receive" }],
111
+ confirmations,
112
+ };
113
+ }
114
+ return null;
115
+ });
116
+
117
+ const { watcher } = createTestWatcher(rpc, { confirmations: 3 });
118
+ watcher.setWatchedAddresses(["bc1qtest"]);
119
+
120
+ const first = await watcher.poll();
121
+ expect(first).toHaveLength(1);
122
+ expect(first[0]!.confirmations).toBe(1);
123
+
124
+ confirmations = 2;
125
+ const second = await watcher.poll();
126
+ expect(second).toHaveLength(1);
127
+ expect(second[0]!.confirmations).toBe(2);
128
+
129
+ confirmations = 3;
130
+ const third = await watcher.poll();
131
+ expect(third).toHaveLength(1);
132
+ expect(third[0]!.confirmations).toBe(3);
133
+ });
134
+
135
+ it("does not emit events after stop()", async () => {
136
+ const rpc = vi.fn<RpcCall>().mockImplementation(async (method) => {
137
+ if (method === "listreceivedbyaddress") {
138
+ return [{ address: "bc1qtest", amount: 1.0, confirmations: 6, txids: ["tx5"] }];
139
+ }
140
+ return null;
141
+ });
142
+
143
+ const { watcher } = createTestWatcher(rpc);
144
+ watcher.setWatchedAddresses(["bc1qtest"]);
145
+ watcher.stop();
146
+
147
+ const events = await watcher.poll();
148
+ expect(events).toEqual([]);
149
+ expect(rpc).not.toHaveBeenCalled();
150
+ });
151
+
152
+ it("importAddress adds to watch set and calls RPC", async () => {
153
+ const rpc = vi.fn<RpcCall>().mockImplementation(async (method) => {
154
+ if (method === "getdescriptorinfo") {
155
+ return { descriptor: "addr(bc1qnew)#checksum" };
156
+ }
157
+ if (method === "importdescriptors") {
158
+ return [{ success: true }];
159
+ }
160
+ if (method === "listreceivedbyaddress") {
161
+ return [{ address: "bc1qnew", amount: 0.01, confirmations: 1, txids: ["tx6"] }];
162
+ }
163
+ if (method === "gettransaction") {
164
+ return {
165
+ details: [{ address: "bc1qnew", amount: 0.01, category: "receive" }],
166
+ confirmations: 1,
167
+ };
168
+ }
169
+ return null;
170
+ });
171
+
172
+ const { watcher } = createTestWatcher(rpc);
173
+ await watcher.importAddress("bc1qnew");
174
+
175
+ const events = await watcher.poll();
176
+ expect(events).toHaveLength(1);
177
+ expect(events[0]!.to).toBe("bc1qnew");
178
+ });
179
+
180
+ it("importAddress falls back to legacy importaddress on error", async () => {
181
+ const rpc = vi.fn<RpcCall>().mockImplementation(async (method) => {
182
+ if (method === "getdescriptorinfo") {
183
+ throw new Error("Method not found");
184
+ }
185
+ if (method === "importaddress") {
186
+ return null;
187
+ }
188
+ if (method === "listreceivedbyaddress") {
189
+ return [];
190
+ }
191
+ return null;
192
+ });
193
+
194
+ const { watcher } = createTestWatcher(rpc);
195
+ await watcher.importAddress("bc1qlegacy");
196
+
197
+ expect(rpc).toHaveBeenCalledWith("importaddress", ["bc1qlegacy", "", false]);
198
+ });
199
+
200
+ it("works with different chain configs (LTC, DOGE)", async () => {
201
+ const rpc = vi.fn<RpcCall>().mockImplementation(async (method) => {
202
+ if (method === "listreceivedbyaddress") {
203
+ return [{ address: "ltc1qtest", amount: 0.5, confirmations: 6, txids: ["ltctx1"] }];
204
+ }
205
+ if (method === "gettransaction") {
206
+ return {
207
+ details: [{ address: "ltc1qtest", amount: 0.5, category: "receive" }],
208
+ confirmations: 6,
209
+ };
210
+ }
211
+ return null;
212
+ });
213
+
214
+ const { watcher } = createTestWatcher(rpc, { token: "LTC", chain: "litecoin", confirmations: 6 });
215
+ watcher.setWatchedAddresses(["ltc1qtest"]);
216
+
217
+ const events = await watcher.poll();
218
+ expect(events).toHaveLength(1);
219
+ expect(events[0]!.chain).toBe("litecoin");
220
+ expect(events[0]!.token).toBe("LTC");
221
+ });
222
+
223
+ it("skips entries without a receive category detail", async () => {
224
+ const rpc = vi.fn<RpcCall>().mockImplementation(async (method) => {
225
+ if (method === "listreceivedbyaddress") {
226
+ return [{ address: "bc1qtest", amount: 1.0, confirmations: 6, txids: ["tx7"] }];
227
+ }
228
+ if (method === "gettransaction") {
229
+ return {
230
+ details: [{ address: "bc1qtest", amount: 1.0, category: "send" }],
231
+ confirmations: 6,
232
+ };
233
+ }
234
+ return null;
235
+ });
236
+
237
+ const { watcher } = createTestWatcher(rpc);
238
+ watcher.setWatchedAddresses(["bc1qtest"]);
239
+
240
+ const events = await watcher.poll();
241
+ expect(events).toEqual([]);
242
+ });
243
+
244
+ it("init loads cursor from store", async () => {
245
+ const rpc = vi.fn();
246
+ const { watcher, cursorStore } = createTestWatcher(rpc);
247
+ cursorStore._cursors.set("bitcoin:BTC", 42);
248
+
249
+ await watcher.init();
250
+ expect(watcher.getCursor()).toBe(42);
251
+ });
252
+ });