@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,91 @@
1
+ import type { IChainPlugin } from "@wopr-network/platform-core/crypto-plugin";
2
+ import { describe, expect, it } from "vitest";
3
+ import { bitcoinPlugin, dogecoinPlugin, evmPlugin, litecoinPlugin, solanaPlugin, tronPlugin } from "../index.js";
4
+
5
+ const allPlugins: IChainPlugin[] = [evmPlugin, bitcoinPlugin, litecoinPlugin, dogecoinPlugin, tronPlugin, solanaPlugin];
6
+
7
+ /** Plugin IDs that have real createWatcher implementations. */
8
+ const implementedIds = new Set(["evm", "bitcoin", "litecoin", "dogecoin", "tron", "solana"]);
9
+
10
+ /** Plugin IDs that have real createSweeper implementations (don't throw on construction). */
11
+ const sweeperImplementedIds = new Set(["solana"]);
12
+
13
+ /** Plugins that still have stub watcher/sweeper implementations. */
14
+ const stubPlugins = allPlugins.filter((p) => !implementedIds.has(p.pluginId));
15
+
16
+ /** Plugins with real createWatcher implementations. */
17
+ const implementedPlugins = allPlugins.filter((p) => implementedIds.has(p.pluginId));
18
+
19
+ describe("Plugin registry", () => {
20
+ it("all plugins have unique pluginIds", () => {
21
+ const ids = allPlugins.map((p) => p.pluginId);
22
+ expect(new Set(ids).size).toBe(ids.length);
23
+ });
24
+
25
+ it("all plugins implement IChainPlugin shape", () => {
26
+ for (const plugin of allPlugins) {
27
+ expect(plugin.pluginId).toBeTypeOf("string");
28
+ expect(plugin.supportedCurve).toMatch(/^(secp256k1|ed25519)$/);
29
+ expect(plugin.encoders).toBeTypeOf("object");
30
+ expect(plugin.createWatcher).toBeTypeOf("function");
31
+ expect(plugin.createSweeper).toBeTypeOf("function");
32
+ expect(plugin.version).toBeTypeOf("number");
33
+ }
34
+ });
35
+
36
+ it("secp256k1 plugins: evm, bitcoin, litecoin, dogecoin, tron", () => {
37
+ const secp = allPlugins.filter((p) => p.supportedCurve === "secp256k1");
38
+ expect(secp.map((p) => p.pluginId).sort()).toEqual(["bitcoin", "dogecoin", "evm", "litecoin", "tron"]);
39
+ });
40
+
41
+ it("ed25519 plugins: solana", () => {
42
+ const ed = allPlugins.filter((p) => p.supportedCurve === "ed25519");
43
+ expect(ed.map((p) => p.pluginId)).toEqual(["solana"]);
44
+ });
45
+
46
+ it("stub createWatcher throws Not implemented", () => {
47
+ for (const plugin of stubPlugins) {
48
+ expect(() => plugin.createWatcher({} as never)).toThrow("Not implemented");
49
+ }
50
+ });
51
+
52
+ it("implemented createWatcher does not throw", () => {
53
+ for (const plugin of implementedPlugins) {
54
+ expect(() => plugin.createWatcher({} as never)).not.toThrow();
55
+ }
56
+ });
57
+
58
+ it("stub createSweeper throws Not implemented", () => {
59
+ const sweeperStubs = allPlugins.filter((p) => !sweeperImplementedIds.has(p.pluginId));
60
+ for (const plugin of sweeperStubs) {
61
+ expect(() => plugin.createSweeper({} as never)).toThrow("Not implemented");
62
+ }
63
+ });
64
+
65
+ it("implemented createSweeper does not throw", () => {
66
+ const sweeperImpl = allPlugins.filter((p) => sweeperImplementedIds.has(p.pluginId));
67
+ for (const plugin of sweeperImpl) {
68
+ expect(() => plugin.createSweeper({} as never)).not.toThrow();
69
+ }
70
+ });
71
+
72
+ it("evm plugin has evm encoder", () => {
73
+ expect(evmPlugin.encoders).toHaveProperty("evm");
74
+ expect(evmPlugin.encoders.evm.encodingType()).toBe("evm");
75
+ });
76
+
77
+ it("solana plugin has base58-solana encoder", () => {
78
+ expect(solanaPlugin.encoders).toHaveProperty("base58-solana");
79
+ expect(solanaPlugin.encoders["base58-solana"].encodingType()).toBe("base58-solana");
80
+ });
81
+
82
+ it("can build a registry map from plugins", () => {
83
+ const registry = new Map<string, IChainPlugin>();
84
+ for (const plugin of allPlugins) {
85
+ registry.set(plugin.pluginId, plugin);
86
+ }
87
+ expect(registry.size).toBe(6);
88
+ expect(registry.get("evm")).toBe(evmPlugin);
89
+ expect(registry.get("solana")).toBe(solanaPlugin);
90
+ });
91
+ });
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { parseRpcUrl } from "../shared/utxo/rpc.js";
4
+
5
+ describe("parseRpcUrl", () => {
6
+ it("extracts credentials from URL with embedded auth", () => {
7
+ const result = parseRpcUrl("http://myuser:mypass@localhost:8332");
8
+ expect(result.rpcUser).toBe("myuser");
9
+ expect(result.rpcPassword).toBe("mypass");
10
+ expect(result.rpcUrl).toBe("http://localhost:8332");
11
+ });
12
+
13
+ it("handles URL-encoded credentials", () => {
14
+ const result = parseRpcUrl("http://user%40name:p%40ss@host:8332");
15
+ expect(result.rpcUser).toBe("user@name");
16
+ expect(result.rpcPassword).toBe("p@ss");
17
+ });
18
+
19
+ it("returns empty credentials for URL without auth", () => {
20
+ const result = parseRpcUrl("http://localhost:8332");
21
+ expect(result.rpcUser).toBe("");
22
+ expect(result.rpcPassword).toBe("");
23
+ expect(result.rpcUrl).toBe("http://localhost:8332");
24
+ });
25
+
26
+ it("strips trailing slash from parsed URL", () => {
27
+ const result = parseRpcUrl("http://user:pass@localhost:8332/");
28
+ expect(result.rpcUrl).toBe("http://localhost:8332");
29
+ });
30
+
31
+ it("handles non-URL strings gracefully", () => {
32
+ const result = parseRpcUrl("not-a-url");
33
+ expect(result.rpcUrl).toBe("not-a-url");
34
+ expect(result.rpcUser).toBe("");
35
+ });
36
+ });
@@ -0,0 +1,103 @@
1
+ import { ed25519 } from "@noble/curves/ed25519.js";
2
+ import { describe, expect, it } from "vitest";
3
+ import { base58Encode, SolanaAddressEncoder } from "../solana/encoder.js";
4
+
5
+ describe("SolanaAddressEncoder", () => {
6
+ const encoder = new SolanaAddressEncoder();
7
+
8
+ it("encodingType returns base58-solana", () => {
9
+ expect(encoder.encodingType()).toBe("base58-solana");
10
+ });
11
+
12
+ it("encodes a known 32-byte pubkey to correct Base58 address", () => {
13
+ // All-ones pubkey (32 bytes of 0x01)
14
+ const pubkey = new Uint8Array(32).fill(1);
15
+ const address = encoder.encode(pubkey, {});
16
+ // Base58 encoding of 32 bytes of 0x01
17
+ expect(typeof address).toBe("string");
18
+ expect(address.length).toBeGreaterThanOrEqual(32);
19
+ expect(address.length).toBeLessThanOrEqual(44);
20
+ });
21
+
22
+ it("produces deterministic addresses", () => {
23
+ const pubkey = new Uint8Array(32);
24
+ pubkey[31] = 42;
25
+ const a1 = encoder.encode(pubkey, {});
26
+ const a2 = encoder.encode(pubkey, {});
27
+ expect(a1).toBe(a2);
28
+ });
29
+
30
+ it("rejects non-32-byte input", () => {
31
+ expect(() => encoder.encode(new Uint8Array(20), {})).toThrow("32-byte");
32
+ expect(() => encoder.encode(new Uint8Array(33), {})).toThrow("32-byte");
33
+ expect(() => encoder.encode(new Uint8Array(0), {})).toThrow("32-byte");
34
+ });
35
+
36
+ it("encodes all-zero pubkey with leading 1s", () => {
37
+ const pubkey = new Uint8Array(32); // all zeros
38
+ const address = encoder.encode(pubkey, {});
39
+ // Base58 encodes leading zero bytes as '1'
40
+ expect(address.startsWith("1")).toBe(true);
41
+ });
42
+
43
+ it("derives correct address from Ed25519 seed 0x00...01", () => {
44
+ // Create a 32-byte seed with last byte = 1
45
+ const seed = new Uint8Array(32);
46
+ seed[31] = 1;
47
+
48
+ // Derive Ed25519 public key from private key (seed)
49
+ const publicKey = ed25519.getPublicKey(seed);
50
+ expect(publicKey.length).toBe(32);
51
+
52
+ // Encode as Solana address
53
+ const address = encoder.encode(publicKey, {});
54
+
55
+ // Verify it's a valid Base58 string
56
+ expect(address).toMatch(/^[1-9A-HJ-NP-Za-km-z]+$/);
57
+ expect(address.length).toBeGreaterThanOrEqual(32);
58
+ expect(address.length).toBeLessThanOrEqual(44);
59
+
60
+ // The address should be the Base58 of the raw pubkey — verify round-trip
61
+ const reencoded = base58Encode(publicKey);
62
+ expect(address).toBe(reencoded);
63
+ });
64
+
65
+ it("derives correct address for well-known Ed25519 test vector", () => {
66
+ // Seed = 32 bytes of 0x00...01
67
+ const seed = new Uint8Array(32);
68
+ seed[31] = 1;
69
+
70
+ const publicKey = ed25519.getPublicKey(seed);
71
+
72
+ // The Ed25519 public key for private key [0..0, 1] is a known value.
73
+ // The Solana address is just the Base58 of this 32-byte key.
74
+ const address = encoder.encode(publicKey, {});
75
+
76
+ // Ed25519 pubkey for seed 0x00..01 = 4cb5abf6ad79fbf5abbccafcc269d85cd2651ed4b885b5869f241aedf0a5ba29
77
+ // (this is the compressed Y coordinate of the point)
78
+ const expectedPubkeyHex = "4cb5abf6ad79fbf5abbccafcc269d85cd2651ed4b885b5869f241aedf0a5ba29";
79
+ const expectedPubkey = new Uint8Array(32);
80
+ for (let i = 0; i < 32; i++) {
81
+ expectedPubkey[i] = Number.parseInt(expectedPubkeyHex.slice(i * 2, i * 2 + 2), 16);
82
+ }
83
+ expect(publicKey).toEqual(expectedPubkey);
84
+
85
+ // Now verify the Base58 encoding of this pubkey
86
+ const expectedAddress = base58Encode(expectedPubkey);
87
+ expect(address).toBe(expectedAddress);
88
+ });
89
+
90
+ it("base58Encode handles various byte patterns", () => {
91
+ // Single byte
92
+ const single = new Uint8Array([0xff]);
93
+ const encoded = base58Encode(single);
94
+ expect(encoded).toBe("5Q");
95
+
96
+ // Empty-ish: all zeros should give all '1's
97
+ const zeros = new Uint8Array(3);
98
+ expect(base58Encode(zeros)).toBe("111");
99
+
100
+ // Known value: [1] should give "2"
101
+ expect(base58Encode(new Uint8Array([1]))).toBe("2");
102
+ });
103
+ });
@@ -0,0 +1,316 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { SolanaTransaction } from "../solana/types.js";
3
+ import { SolanaWatcher } from "../solana/watcher.js";
4
+
5
+ /** Create a minimal mock WatcherOpts. */
6
+ function createMockOpts(rpcResponses: Map<string, unknown>) {
7
+ // Override global fetch to return mocked RPC responses
8
+ const mockFetch = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => {
9
+ const body = JSON.parse((init?.body as string) ?? "{}") as { method: string; id: number };
10
+ const result = rpcResponses.get(body.method);
11
+ return new Response(JSON.stringify({ jsonrpc: "2.0", id: body.id, result }), {
12
+ status: 200,
13
+ headers: { "Content-Type": "application/json" },
14
+ });
15
+ });
16
+
17
+ // biome-ignore lint/suspicious/noExplicitAny: test mock override
18
+ (globalThis as any).fetch = mockFetch;
19
+
20
+ return {
21
+ rpcUrl: "http://localhost:8899",
22
+ rpcHeaders: {},
23
+ oracle: { getPrice: vi.fn().mockResolvedValue({ priceMicros: 150_000_000 }) },
24
+ cursorStore: {
25
+ get: vi.fn().mockResolvedValue(null),
26
+ save: vi.fn().mockResolvedValue(undefined),
27
+ getConfirmationCount: vi.fn().mockResolvedValue(null),
28
+ saveConfirmationCount: vi.fn().mockResolvedValue(undefined),
29
+ },
30
+ token: "SOL",
31
+ chain: "solana",
32
+ decimals: 9,
33
+ confirmations: 1,
34
+ _mockFetch: mockFetch,
35
+ };
36
+ }
37
+
38
+ describe("SolanaWatcher", () => {
39
+ it("initializes with cursor from store", async () => {
40
+ const rpcResponses = new Map();
41
+ const opts = createMockOpts(rpcResponses);
42
+ opts.cursorStore.get = vi.fn().mockResolvedValue(500);
43
+
44
+ const watcher = new SolanaWatcher(opts);
45
+ await watcher.init();
46
+ expect(watcher.getCursor()).toBe(500);
47
+ });
48
+
49
+ it("returns empty when no watched addresses", async () => {
50
+ const rpcResponses = new Map();
51
+ const opts = createMockOpts(rpcResponses);
52
+
53
+ const watcher = new SolanaWatcher(opts);
54
+ await watcher.init();
55
+ const events = await watcher.poll();
56
+ expect(events).toEqual([]);
57
+ });
58
+
59
+ it("returns empty when stopped", async () => {
60
+ const rpcResponses = new Map();
61
+ const opts = createMockOpts(rpcResponses);
62
+
63
+ const watcher = new SolanaWatcher(opts);
64
+ await watcher.init();
65
+ watcher.setWatchedAddresses(["SomeAddress111111111111111111111111111111111"]);
66
+ watcher.stop();
67
+ const events = await watcher.poll();
68
+ expect(events).toEqual([]);
69
+ });
70
+
71
+ it("detects native SOL transfer to watched address", async () => {
72
+ const watchedAddr = "ReceiverAddr1111111111111111111111111111111";
73
+ const senderAddr = "SenderAddr11111111111111111111111111111111";
74
+
75
+ const mockTx: SolanaTransaction = {
76
+ slot: 100,
77
+ blockTime: 1700000000,
78
+ meta: {
79
+ err: null,
80
+ fee: 5000,
81
+ preBalances: [2_000_000_000, 500_000_000],
82
+ postBalances: [1_000_000_000, 1_500_000_000],
83
+ },
84
+ transaction: {
85
+ message: {
86
+ accountKeys: [senderAddr, watchedAddr],
87
+ instructions: [{ programIdIndex: 0, accounts: [0, 1], data: "" }],
88
+ },
89
+ signatures: ["sig123abc"],
90
+ },
91
+ };
92
+
93
+ const rpcResponses = new Map<string, unknown>();
94
+ rpcResponses.set("getSignaturesForAddress", [
95
+ {
96
+ signature: "sig123abc",
97
+ slot: 100,
98
+ err: null,
99
+ memo: null,
100
+ blockTime: 1700000000,
101
+ confirmationStatus: "finalized",
102
+ },
103
+ ]);
104
+ rpcResponses.set("getTransaction", mockTx);
105
+
106
+ const opts = createMockOpts(rpcResponses);
107
+ const watcher = new SolanaWatcher(opts);
108
+ await watcher.init();
109
+ watcher.setWatchedAddresses([watchedAddr]);
110
+
111
+ const events = await watcher.poll();
112
+ expect(events).toHaveLength(1);
113
+ expect(events[0].txHash).toBe("sig123abc");
114
+ expect(events[0].to).toBe(watchedAddr);
115
+ expect(events[0].from).toBe(senderAddr);
116
+ expect(events[0].rawAmount).toBe("1000000000"); // 1 SOL increase
117
+ expect(events[0].chain).toBe("solana");
118
+ expect(events[0].token).toBe("SOL");
119
+ expect(events[0].blockNumber).toBe(100);
120
+ });
121
+
122
+ it("advances cursor for finalized transactions", async () => {
123
+ const watchedAddr = "ReceiverAddr1111111111111111111111111111111";
124
+
125
+ const mockTx: SolanaTransaction = {
126
+ slot: 200,
127
+ blockTime: 1700000000,
128
+ meta: {
129
+ err: null,
130
+ fee: 5000,
131
+ preBalances: [2_000_000_000, 0],
132
+ postBalances: [1_000_000_000, 1_000_000_000],
133
+ },
134
+ transaction: {
135
+ message: {
136
+ accountKeys: ["Sender1111111111111111111111111111111111111", watchedAddr],
137
+ instructions: [],
138
+ },
139
+ signatures: ["sig456def"],
140
+ },
141
+ };
142
+
143
+ const rpcResponses = new Map<string, unknown>();
144
+ rpcResponses.set("getSignaturesForAddress", [
145
+ {
146
+ signature: "sig456def",
147
+ slot: 200,
148
+ err: null,
149
+ memo: null,
150
+ blockTime: 1700000000,
151
+ confirmationStatus: "finalized",
152
+ },
153
+ ]);
154
+ rpcResponses.set("getTransaction", mockTx);
155
+
156
+ const opts = createMockOpts(rpcResponses);
157
+ const watcher = new SolanaWatcher(opts);
158
+ await watcher.init();
159
+ watcher.setWatchedAddresses([watchedAddr]);
160
+
161
+ await watcher.poll();
162
+ expect(watcher.getCursor()).toBe(200);
163
+ expect(opts.cursorStore.save).toHaveBeenCalledWith("solana:solana:SOL", 200);
164
+ });
165
+
166
+ it("skips errored transactions", async () => {
167
+ const watchedAddr = "ReceiverAddr1111111111111111111111111111111";
168
+
169
+ const rpcResponses = new Map<string, unknown>();
170
+ rpcResponses.set("getSignaturesForAddress", [
171
+ {
172
+ signature: "errored-sig",
173
+ slot: 300,
174
+ err: { InstructionError: [0, "Custom"] },
175
+ memo: null,
176
+ blockTime: 1700000000,
177
+ confirmationStatus: "finalized",
178
+ },
179
+ ]);
180
+
181
+ const opts = createMockOpts(rpcResponses);
182
+ const watcher = new SolanaWatcher(opts);
183
+ await watcher.init();
184
+ watcher.setWatchedAddresses([watchedAddr]);
185
+
186
+ const events = await watcher.poll();
187
+ expect(events).toEqual([]);
188
+ });
189
+
190
+ it("skips already-emitted confirmations", async () => {
191
+ const watchedAddr = "ReceiverAddr1111111111111111111111111111111";
192
+
193
+ const mockTx: SolanaTransaction = {
194
+ slot: 400,
195
+ blockTime: 1700000000,
196
+ meta: {
197
+ err: null,
198
+ fee: 5000,
199
+ preBalances: [2_000_000_000, 0],
200
+ postBalances: [1_000_000_000, 1_000_000_000],
201
+ },
202
+ transaction: {
203
+ message: {
204
+ accountKeys: ["Sender1111111111111111111111111111111111111", watchedAddr],
205
+ instructions: [],
206
+ },
207
+ signatures: ["sig-dup"],
208
+ },
209
+ };
210
+
211
+ const rpcResponses = new Map<string, unknown>();
212
+ rpcResponses.set("getSignaturesForAddress", [
213
+ {
214
+ signature: "sig-dup",
215
+ slot: 400,
216
+ err: null,
217
+ memo: null,
218
+ blockTime: 1700000000,
219
+ confirmationStatus: "finalized",
220
+ },
221
+ ]);
222
+ rpcResponses.set("getTransaction", mockTx);
223
+
224
+ const opts = createMockOpts(rpcResponses);
225
+ // Already emitted at confirmation count 1
226
+ opts.cursorStore.getConfirmationCount = vi.fn().mockResolvedValue(1);
227
+
228
+ const watcher = new SolanaWatcher(opts);
229
+ await watcher.init();
230
+ watcher.setWatchedAddresses([watchedAddr]);
231
+
232
+ const events = await watcher.poll();
233
+ expect(events).toEqual([]);
234
+ });
235
+
236
+ it("detects SPL token transfer to watched address", async () => {
237
+ const watchedAddr = "ReceiverAddr1111111111111111111111111111111";
238
+ const senderAddr = "SenderAddr11111111111111111111111111111111";
239
+ const usdcMint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
240
+
241
+ const mockTx: SolanaTransaction = {
242
+ slot: 500,
243
+ blockTime: 1700000000,
244
+ meta: {
245
+ err: null,
246
+ fee: 5000,
247
+ preBalances: [2_000_000_000, 1_000_000],
248
+ postBalances: [1_995_000_000, 1_000_000],
249
+ preTokenBalances: [
250
+ {
251
+ accountIndex: 0,
252
+ mint: usdcMint,
253
+ uiTokenAmount: { amount: "10000000", decimals: 6, uiAmountString: "10.0" },
254
+ owner: senderAddr,
255
+ },
256
+ {
257
+ accountIndex: 1,
258
+ mint: usdcMint,
259
+ uiTokenAmount: { amount: "0", decimals: 6, uiAmountString: "0.0" },
260
+ owner: watchedAddr,
261
+ },
262
+ ],
263
+ postTokenBalances: [
264
+ {
265
+ accountIndex: 0,
266
+ mint: usdcMint,
267
+ uiTokenAmount: { amount: "5000000", decimals: 6, uiAmountString: "5.0" },
268
+ owner: senderAddr,
269
+ },
270
+ {
271
+ accountIndex: 1,
272
+ mint: usdcMint,
273
+ uiTokenAmount: { amount: "5000000", decimals: 6, uiAmountString: "5.0" },
274
+ owner: watchedAddr,
275
+ },
276
+ ],
277
+ },
278
+ transaction: {
279
+ message: {
280
+ accountKeys: [senderAddr, watchedAddr],
281
+ instructions: [],
282
+ },
283
+ signatures: ["spl-sig-789"],
284
+ },
285
+ };
286
+
287
+ const rpcResponses = new Map<string, unknown>();
288
+ rpcResponses.set("getSignaturesForAddress", [
289
+ {
290
+ signature: "spl-sig-789",
291
+ slot: 500,
292
+ err: null,
293
+ memo: null,
294
+ blockTime: 1700000000,
295
+ confirmationStatus: "finalized",
296
+ },
297
+ ]);
298
+ rpcResponses.set("getTransaction", mockTx);
299
+
300
+ const opts = createMockOpts(rpcResponses);
301
+ // Configure as SPL token watcher
302
+ const splOpts = { ...opts, token: "USDC", contractAddress: usdcMint };
303
+
304
+ const watcher = new SolanaWatcher(splOpts);
305
+ await watcher.init();
306
+ watcher.setWatchedAddresses([watchedAddr]);
307
+
308
+ const events = await watcher.poll();
309
+ expect(events).toHaveLength(1);
310
+ expect(events[0].txHash).toBe("spl-sig-789");
311
+ expect(events[0].to).toBe(watchedAddr);
312
+ expect(events[0].from).toBe(senderAddr);
313
+ expect(events[0].rawAmount).toBe("5000000"); // 5 USDC
314
+ expect(events[0].token).toBe("USDC");
315
+ });
316
+ });