@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,467 @@
1
+ /**
2
+ * Tron sweep strategy -- consolidates TRX + TRC-20s from deposit addresses to treasury.
3
+ *
4
+ * Uses Tron HTTP API (not EVM JSON-RPC).
5
+ *
6
+ * 3-phase sweep:
7
+ * 1. Sweep TRX first -- deposit addresses self-fund bandwidth, treasury receives TRX
8
+ * 2. Fund energy -- treasury sends TRX to TRC-20 deposit addresses
9
+ * 3. Sweep TRC-20s -- deposit addresses send all tokens to treasury
10
+ */
11
+
12
+ import { secp256k1 } from "@noble/curves/secp256k1.js";
13
+ import { keccak_256 } from "@noble/hashes/sha3.js";
14
+ import type { DepositInfo, ISweepStrategy, KeyPair, SweepResult } from "@wopr-network/platform-core/crypto-plugin";
15
+ import { sha256 } from "../tron/sha256.js";
16
+
17
+ // TRX has 6 decimals (1 TRX = 1,000,000 SUN)
18
+ const SUN_PER_TRX = 1_000_000n;
19
+ // Min TRX to keep for a simple TRX transfer (~1.1 TRX in SUN)
20
+ const TRX_TRANSFER_COST = 1_100_000n;
21
+ // Energy needed for TRC-20 transfer (~15 TRX in SUN, conservative)
22
+ const TRC20_ENERGY_COST = 15_000_000n;
23
+
24
+ // secp256k1.ProjectivePoint for point decompression
25
+ const ProjectivePoint = (
26
+ secp256k1 as unknown as {
27
+ ProjectivePoint: {
28
+ fromHex(hex: string): { toRawBytes(compressed: boolean): Uint8Array };
29
+ };
30
+ }
31
+ ).ProjectivePoint;
32
+
33
+ export interface TronToken {
34
+ name: string;
35
+ contractAddress: string; // T... base58 address
36
+ decimals: number;
37
+ }
38
+
39
+ export interface TronSweeperOpts {
40
+ rpcUrl: string;
41
+ apiKey?: string;
42
+ tokens: TronToken[];
43
+ }
44
+
45
+ // --- Base58 ---
46
+
47
+ const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
48
+
49
+ function base58encode(data: Uint8Array): string {
50
+ let num = 0n;
51
+ for (const byte of data) num = num * 256n + BigInt(byte);
52
+ let encoded = "";
53
+ while (num > 0n) {
54
+ encoded = BASE58_ALPHABET[Number(num % 58n)] + encoded;
55
+ num = num / 58n;
56
+ }
57
+ for (const byte of data) {
58
+ if (byte !== 0) break;
59
+ encoded = `1${encoded}`;
60
+ }
61
+ return encoded;
62
+ }
63
+
64
+ function base58decode(str: string): Uint8Array {
65
+ let num = 0n;
66
+ for (const ch of str) {
67
+ const idx = BASE58_ALPHABET.indexOf(ch);
68
+ if (idx < 0) throw new Error(`Invalid Base58 char: ${ch}`);
69
+ num = num * 58n + BigInt(idx);
70
+ }
71
+ const hex = num.toString(16).padStart(50, "0"); // 25 bytes = 50 hex chars
72
+ const pairs = hex.match(/.{2}/g) ?? [];
73
+ const bytes = new Uint8Array(pairs.map((h) => Number.parseInt(h, 16)));
74
+ let leadingZeros = 0;
75
+ for (const ch of str) {
76
+ if (ch !== "1") break;
77
+ leadingZeros++;
78
+ }
79
+ const result = new Uint8Array(leadingZeros + bytes.length);
80
+ result.set(bytes, leadingZeros);
81
+ return result;
82
+ }
83
+
84
+ // --- Hex helpers ---
85
+
86
+ function toHex(data: Uint8Array): string {
87
+ return Array.from(data, (b) => b.toString(16).padStart(2, "0")).join("");
88
+ }
89
+
90
+ function fromHex(hex: string): Uint8Array {
91
+ const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
92
+ const pairs = clean.match(/.{2}/g) ?? [];
93
+ return new Uint8Array(pairs.map((h) => Number.parseInt(h, 16)));
94
+ }
95
+
96
+ // --- Address helpers ---
97
+
98
+ function pubkeyToTronAddress(pubkey: Uint8Array): string {
99
+ const uncompressed: Uint8Array = ProjectivePoint.fromHex(toHex(pubkey)).toRawBytes(false);
100
+ const hash = keccak_256(uncompressed.slice(1));
101
+ const addressBytes = hash.slice(-20);
102
+ const payload = new Uint8Array(21);
103
+ payload[0] = 0x41;
104
+ payload.set(addressBytes, 1);
105
+ const checksum = sha256(sha256(payload));
106
+ const full = new Uint8Array(25);
107
+ full.set(payload);
108
+ full.set(checksum.slice(0, 4), 21);
109
+ return base58encode(full);
110
+ }
111
+
112
+ function tronAddressToHex(tAddr: string): string {
113
+ const decoded = base58decode(tAddr);
114
+ return toHex(decoded.slice(0, 21));
115
+ }
116
+
117
+ function formatTrx(sun: bigint): string {
118
+ const whole = sun / SUN_PER_TRX;
119
+ const frac = sun % SUN_PER_TRX;
120
+ if (frac === 0n) return `${whole}`;
121
+ return `${whole}.${frac.toString().padStart(6, "0").replace(/0+$/, "")}`;
122
+ }
123
+
124
+ function formatTokenAmount(amount: bigint, decimals: number): string {
125
+ return (Number(amount) / 10 ** decimals).toString();
126
+ }
127
+
128
+ // --- Tron RPC ---
129
+
130
+ export class TronSweeper implements ISweepStrategy {
131
+ private readonly rpcUrl: string;
132
+ private readonly apiKey?: string;
133
+ private readonly tokens: TronToken[];
134
+ private readonly tokenHexMap: Map<string, string>;
135
+
136
+ constructor(opts: TronSweeperOpts) {
137
+ this.rpcUrl = opts.rpcUrl;
138
+ this.apiKey = opts.apiKey;
139
+ this.tokens = opts.tokens;
140
+ this.tokenHexMap = new Map(opts.tokens.map((t) => [t.name, tronAddressToHex(t.contractAddress)]));
141
+ }
142
+
143
+ private headers(): Record<string, string> {
144
+ const h: Record<string, string> = {
145
+ "Content-Type": "application/json",
146
+ };
147
+ if (this.apiKey) h["TRON-PRO-API-KEY"] = this.apiKey;
148
+ return h;
149
+ }
150
+
151
+ private async post(path: string, body: Record<string, unknown>): Promise<unknown> {
152
+ const res = await fetch(`${this.rpcUrl}${path}`, {
153
+ method: "POST",
154
+ headers: this.headers(),
155
+ body: JSON.stringify(body),
156
+ });
157
+ if (!res.ok) throw new Error(`Tron RPC ${path} returned ${res.status}: ${await res.text()}`);
158
+ return res.json();
159
+ }
160
+
161
+ private async getTrxBalance(addressHex: string): Promise<bigint> {
162
+ const result = (await this.post("/wallet/getaccount", {
163
+ address: addressHex,
164
+ visible: false,
165
+ })) as { balance?: number };
166
+ return BigInt(result.balance ?? 0);
167
+ }
168
+
169
+ private async getTrc20Balance(ownerHex: string, contractHex: string): Promise<bigint> {
170
+ const ownerBytes = ownerHex.startsWith("41") ? ownerHex.slice(2) : ownerHex;
171
+ const parameter = ownerBytes.padStart(64, "0");
172
+
173
+ const result = (await this.post("/wallet/triggerconstantcontract", {
174
+ owner_address: ownerHex,
175
+ contract_address: contractHex,
176
+ function_selector: "balanceOf(address)",
177
+ parameter,
178
+ visible: false,
179
+ })) as { constant_result?: string[] };
180
+
181
+ if (!result.constant_result?.[0]) return 0n;
182
+ return BigInt(`0x${result.constant_result[0]}`);
183
+ }
184
+
185
+ private async createTrxTransfer(
186
+ fromHexAddr: string,
187
+ toHexAddr: string,
188
+ amountSun: bigint,
189
+ ): Promise<{ raw_data: unknown; raw_data_hex: string; txID: string }> {
190
+ const result = (await this.post("/wallet/createtransaction", {
191
+ owner_address: fromHexAddr,
192
+ to_address: toHexAddr,
193
+ amount: Number(amountSun),
194
+ visible: false,
195
+ })) as { raw_data: unknown; raw_data_hex: string; txID: string };
196
+ if (!result.txID) throw new Error(`Failed to create TRX transfer: ${JSON.stringify(result)}`);
197
+ return result;
198
+ }
199
+
200
+ private async createTrc20Transfer(
201
+ fromHexAddr: string,
202
+ contractHex: string,
203
+ toHexAddr: string,
204
+ amount: bigint,
205
+ ): Promise<{
206
+ transaction: { raw_data: unknown; raw_data_hex: string; txID: string };
207
+ }> {
208
+ const toBytes = toHexAddr.startsWith("41") ? toHexAddr.slice(2) : toHexAddr;
209
+ const amountHex = amount.toString(16).padStart(64, "0");
210
+ const parameter = toBytes.padStart(64, "0") + amountHex;
211
+
212
+ const result = (await this.post("/wallet/triggersmartcontract", {
213
+ owner_address: fromHexAddr,
214
+ contract_address: contractHex,
215
+ function_selector: "transfer(address,uint256)",
216
+ parameter,
217
+ fee_limit: 100_000_000,
218
+ visible: false,
219
+ })) as {
220
+ result?: { result: boolean };
221
+ transaction: {
222
+ raw_data: unknown;
223
+ raw_data_hex: string;
224
+ txID: string;
225
+ };
226
+ };
227
+ if (!result.result?.result) throw new Error(`Failed to create TRC-20 transfer: ${JSON.stringify(result)}`);
228
+ return result as {
229
+ transaction: {
230
+ raw_data: unknown;
231
+ raw_data_hex: string;
232
+ txID: string;
233
+ };
234
+ };
235
+ }
236
+
237
+ private signTransaction(
238
+ tx: { raw_data: unknown; raw_data_hex: string; txID: string },
239
+ privateKey: Uint8Array,
240
+ ): {
241
+ raw_data: unknown;
242
+ raw_data_hex: string;
243
+ txID: string;
244
+ signature: string[];
245
+ } {
246
+ const txHash = fromHex(tx.txID);
247
+ // secp256k1.sign returns RecoveredSignature with toCompactRawBytes() and recovery
248
+ const sig = secp256k1.sign(txHash, privateKey, { lowS: true }) as unknown as {
249
+ toCompactRawBytes(): Uint8Array;
250
+ recovery: number;
251
+ };
252
+ // Tron signature = r (32) + s (32) + recovery (1)
253
+ const sigBytes = new Uint8Array(65);
254
+ sigBytes.set(sig.toCompactRawBytes(), 0);
255
+ sigBytes[64] = sig.recovery;
256
+ return { ...tx, signature: [toHex(sigBytes)] };
257
+ }
258
+
259
+ private async broadcastTransaction(signedTx: unknown): Promise<string> {
260
+ const result = (await this.post("/wallet/broadcasttransaction", signedTx as Record<string, unknown>)) as {
261
+ result?: boolean;
262
+ txid?: string;
263
+ message?: string;
264
+ };
265
+ if (!result.result) throw new Error(`Broadcast failed: ${result.message ?? JSON.stringify(result)}`);
266
+ return result.txid ?? (signedTx as { txID: string }).txID;
267
+ }
268
+
269
+ private keyToTronHex(key: KeyPair): string {
270
+ return tronAddressToHex(key.address.startsWith("T") ? key.address : pubkeyToTronAddress(key.publicKey));
271
+ }
272
+
273
+ async scan(keys: KeyPair[], _treasury: string): Promise<DepositInfo[]> {
274
+ const deposits: DepositInfo[] = [];
275
+
276
+ for (let i = 0; i < keys.length; i++) {
277
+ const key = keys[i];
278
+ if (!key) continue;
279
+ const addrHex = this.keyToTronHex(key);
280
+ const trxBalance = await this.getTrxBalance(addrHex);
281
+
282
+ const tokenBalances: DepositInfo["tokenBalances"] = [];
283
+ for (const token of this.tokens) {
284
+ const contractHex = this.tokenHexMap.get(token.name);
285
+ if (!contractHex) continue;
286
+ try {
287
+ const balance = await this.getTrc20Balance(addrHex, contractHex);
288
+ if (balance > 0n) {
289
+ tokenBalances.push({
290
+ token: token.name,
291
+ balance,
292
+ decimals: token.decimals,
293
+ });
294
+ }
295
+ } catch {
296
+ // Contract call failed
297
+ }
298
+ }
299
+
300
+ if (trxBalance > 0n || tokenBalances.length > 0) {
301
+ deposits.push({
302
+ index: key.index,
303
+ address: key.address,
304
+ nativeBalance: trxBalance,
305
+ tokenBalances,
306
+ });
307
+ }
308
+
309
+ // Rate limit protection
310
+ if (i % 10 === 9) await sleep(200);
311
+ }
312
+
313
+ return deposits;
314
+ }
315
+
316
+ async sweep(keys: KeyPair[], treasury: string, dryRun: boolean): Promise<SweepResult[]> {
317
+ const treasuryHex = tronAddressToHex(treasury);
318
+ const deposits = await this.scan(keys, treasury);
319
+
320
+ if (deposits.length === 0) {
321
+ console.log(" No Tron deposits with balances.");
322
+ return [];
323
+ }
324
+
325
+ const trxDeposits = deposits.filter((d) => d.nativeBalance > TRX_TRANSFER_COST);
326
+ const tokenDeposits = deposits.filter((d) => d.tokenBalances.length > 0);
327
+ const totalTrx = trxDeposits.reduce((sum, d) => sum + d.nativeBalance, 0n);
328
+
329
+ // Print scan summary
330
+ console.log(` Found ${trxDeposits.length} TRX deposits (${formatTrx(totalTrx)} TRX)`);
331
+ for (const token of this.tokens) {
332
+ const total = tokenDeposits.reduce(
333
+ (sum, d) => sum + (d.tokenBalances.find((t) => t.token === token.name)?.balance ?? 0n),
334
+ 0n,
335
+ );
336
+ if (total > 0n) {
337
+ console.log(` ${formatTokenAmount(total, token.decimals)} ${token.name}`);
338
+ }
339
+ }
340
+
341
+ if (dryRun) return [];
342
+
343
+ const results: SweepResult[] = [];
344
+ const keyMap = new Map(keys.map((k) => [k.index, k]));
345
+
346
+ // Phase 1: Sweep TRX (self-funded)
347
+ if (trxDeposits.length > 0) {
348
+ console.log(" Phase 1: Sweeping TRX to treasury");
349
+ for (const dep of trxDeposits) {
350
+ const key = keyMap.get(dep.index);
351
+ if (!key) continue;
352
+
353
+ const sweepAmount = dep.nativeBalance - TRX_TRANSFER_COST;
354
+ if (sweepAmount <= 0n) {
355
+ console.log(` [${dep.index}] Balance too low to cover fees, skipping`);
356
+ continue;
357
+ }
358
+
359
+ try {
360
+ const depHex = this.keyToTronHex(key);
361
+ const tx = await this.createTrxTransfer(depHex, treasuryHex, sweepAmount);
362
+ const signed = this.signTransaction(tx, key.privateKey);
363
+ const txId = await this.broadcastTransaction(signed);
364
+ console.log(` [${dep.index}] Swept ${formatTrx(sweepAmount)} TRX: ${txId}`);
365
+ await sleep(3000);
366
+ results.push({
367
+ index: dep.index,
368
+ address: dep.address,
369
+ token: "TRX",
370
+ amount: formatTrx(sweepAmount),
371
+ txHash: txId,
372
+ });
373
+ } catch (err) {
374
+ console.error(` [${dep.index}] Failed: ${err}`);
375
+ }
376
+ }
377
+ }
378
+
379
+ // Phase 2: Fund energy for TRC-20 sweeps
380
+ if (tokenDeposits.length > 0) {
381
+ const treasuryTrx = await this.getTrxBalance(treasuryHex);
382
+ const totalEnergyNeeded =
383
+ TRC20_ENERGY_COST * BigInt(tokenDeposits.reduce((n, d) => n + d.tokenBalances.length, 0));
384
+
385
+ console.log(" Phase 2: Funding energy for TRC-20 sweeps");
386
+ console.log(` Treasury TRX: ${formatTrx(treasuryTrx)}, energy cost: ${formatTrx(totalEnergyNeeded)}`);
387
+
388
+ if (treasuryTrx < totalEnergyNeeded) {
389
+ console.error(
390
+ ` Insufficient treasury TRX. Need ${formatTrx(totalEnergyNeeded)}, have ${formatTrx(treasuryTrx)}.`,
391
+ );
392
+ return results;
393
+ }
394
+
395
+ // Find treasury key
396
+ const treasuryKey = keys.find((k) => k.address === treasury);
397
+ if (!treasuryKey) {
398
+ console.error(" Cannot fund energy: treasury private key not available");
399
+ return results;
400
+ }
401
+
402
+ for (const dep of tokenDeposits) {
403
+ const depHex = tronAddressToHex(dep.address);
404
+ const depTrx = await this.getTrxBalance(depHex);
405
+ const needed = TRC20_ENERGY_COST * BigInt(dep.tokenBalances.length);
406
+ if (depTrx >= needed) {
407
+ console.log(` [${dep.index}] Already has energy TRX, skipping`);
408
+ continue;
409
+ }
410
+
411
+ try {
412
+ const fundAmount = needed - depTrx;
413
+ const tx = await this.createTrxTransfer(treasuryHex, depHex, fundAmount);
414
+ const signed = this.signTransaction(tx, treasuryKey.privateKey);
415
+ const txId = await this.broadcastTransaction(signed);
416
+ console.log(` [${dep.index}] Funded ${formatTrx(fundAmount)} TRX: ${txId}`);
417
+ await sleep(3000);
418
+ } catch (err) {
419
+ console.error(` [${dep.index}] Fund failed: ${err}`);
420
+ }
421
+ }
422
+
423
+ // Phase 3: Sweep TRC-20s
424
+ console.log(" Phase 3: Sweeping TRC-20s to treasury");
425
+ for (const dep of tokenDeposits) {
426
+ const key = keyMap.get(dep.index);
427
+ if (!key) continue;
428
+ const depHex = this.keyToTronHex(key);
429
+
430
+ for (const tokenBal of dep.tokenBalances) {
431
+ const contractHex = this.tokenHexMap.get(tokenBal.token);
432
+ if (!contractHex) continue;
433
+
434
+ try {
435
+ const { transaction: tx } = await this.createTrc20Transfer(
436
+ depHex,
437
+ contractHex,
438
+ treasuryHex,
439
+ tokenBal.balance,
440
+ );
441
+ const signed = this.signTransaction(tx, key.privateKey);
442
+ const txId = await this.broadcastTransaction(signed);
443
+ console.log(
444
+ ` [${dep.index}] Swept ${formatTokenAmount(tokenBal.balance, tokenBal.decimals)} ${tokenBal.token}: ${txId}`,
445
+ );
446
+ await sleep(3000);
447
+ results.push({
448
+ index: dep.index,
449
+ address: dep.address,
450
+ token: tokenBal.token,
451
+ amount: formatTokenAmount(tokenBal.balance, tokenBal.decimals),
452
+ txHash: txId,
453
+ });
454
+ } catch (err) {
455
+ console.error(` [${dep.index}] ${tokenBal.token} sweep failed: ${err}`);
456
+ }
457
+ }
458
+ }
459
+ }
460
+
461
+ return results;
462
+ }
463
+ }
464
+
465
+ function sleep(ms: number): Promise<void> {
466
+ return new Promise((r) => setTimeout(r, ms));
467
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * UTXO sweep stub -- BTC/LTC/DOGE sweeps are manual via wallet software.
3
+ *
4
+ * UTXO chains require coin selection, fee estimation, and input signing
5
+ * that is best handled by dedicated wallet software (Electrum, Sparrow, etc.).
6
+ */
7
+ import type { DepositInfo, ISweepStrategy, KeyPair, SweepResult } from "@wopr-network/platform-core/crypto-plugin";
8
+
9
+ export class UtxoSweeper implements ISweepStrategy {
10
+ private readonly chain: string;
11
+
12
+ constructor(chain: string) {
13
+ this.chain = chain;
14
+ }
15
+
16
+ async scan(_keys: KeyPair[], _treasury: string): Promise<DepositInfo[]> {
17
+ throw new Error(`UTXO sweep not implemented for ${this.chain} -- use wallet software (Electrum/Sparrow)`);
18
+ }
19
+
20
+ async sweep(_keys: KeyPair[], _treasury: string, _dryRun: boolean): Promise<SweepResult[]> {
21
+ throw new Error(`UTXO sweep not implemented for ${this.chain} -- use wallet software (Electrum/Sparrow)`);
22
+ }
23
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Tron address conversion -- T... Base58Check <-> 0x hex.
3
+ *
4
+ * Tron addresses are 21 bytes: 0x41 prefix + 20-byte address.
5
+ * The JSON-RPC layer strips the 0x41 and returns standard 0x-prefixed hex.
6
+ * We need to convert between the two at the watcher boundary.
7
+ */
8
+ import { sha256 } from "./sha256.js";
9
+
10
+ const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
11
+
12
+ function base58decode(s: string): Uint8Array {
13
+ let leadingZeros = 0;
14
+ for (const ch of s) {
15
+ if (ch !== "1") break;
16
+ leadingZeros++;
17
+ }
18
+ let num = 0n;
19
+ for (const ch of s) {
20
+ const idx = BASE58_ALPHABET.indexOf(ch);
21
+ if (idx < 0) throw new Error(`Invalid base58 character: ${ch}`);
22
+ num = num * 58n + BigInt(idx);
23
+ }
24
+ const hex = num.toString(16).padStart(2, "0");
25
+ // Ensure even length for byte parsing
26
+ const paddedHex = hex.length % 2 ? `0${hex}` : hex;
27
+ const dataBytes = new Uint8Array(paddedHex.length / 2);
28
+ for (let i = 0; i < dataBytes.length; i++) dataBytes[i] = Number.parseInt(paddedHex.slice(i * 2, i * 2 + 2), 16);
29
+ const result = new Uint8Array(leadingZeros + dataBytes.length);
30
+ result.set(dataBytes, leadingZeros);
31
+ return result;
32
+ }
33
+
34
+ function base58encode(data: Uint8Array): string {
35
+ let num = 0n;
36
+ for (const byte of data) num = num * 256n + BigInt(byte);
37
+ let encoded = "";
38
+ while (num > 0n) {
39
+ encoded = BASE58_ALPHABET[Number(num % 58n)] + encoded;
40
+ num = num / 58n;
41
+ }
42
+ for (const byte of data) {
43
+ if (byte !== 0) break;
44
+ encoded = `1${encoded}`;
45
+ }
46
+ return encoded;
47
+ }
48
+
49
+ /**
50
+ * Convert a Tron T... address to 0x hex (20 bytes, no 0x41 prefix).
51
+ * For feeding addresses to the EVM watcher JSON-RPC filters.
52
+ */
53
+ export function tronToHex(tronAddr: string): string {
54
+ if (!tronAddr.startsWith("T")) throw new Error(`Not a Tron address: ${tronAddr}`);
55
+ const decoded = base58decode(tronAddr);
56
+ // decoded: [0x41, ...20 bytes address..., ...4 bytes checksum]
57
+ const payload = decoded.slice(0, 21);
58
+ const checksum = sha256(sha256(payload)).slice(0, 4);
59
+ for (let i = 0; i < 4; i++) {
60
+ if (decoded[21 + i] !== checksum[i]) throw new Error(`Invalid checksum for Tron address: ${tronAddr}`);
61
+ }
62
+ // Strip 0x41 prefix, return 20-byte hex with 0x prefix
63
+ const addrBytes = payload.slice(1);
64
+ return `0x${Array.from(addrBytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
65
+ }
66
+
67
+ /**
68
+ * Convert a 0x hex address (20 bytes) back to Tron T... Base58Check.
69
+ * For converting watcher event addresses back to DB format.
70
+ */
71
+ export function hexToTron(hexAddr: string): string {
72
+ const hex = hexAddr.startsWith("0x") ? hexAddr.slice(2) : hexAddr;
73
+ if (hex.length !== 40) throw new Error(`Invalid hex address length: ${hex.length}`);
74
+ // Build payload: 0x41 + 20 bytes
75
+ const payload = new Uint8Array(21);
76
+ payload[0] = 0x41;
77
+ for (let i = 0; i < 20; i++) payload[i + 1] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
78
+ // Compute checksum
79
+ const checksum = sha256(sha256(payload)).slice(0, 4);
80
+ const full = new Uint8Array(25);
81
+ full.set(payload);
82
+ full.set(checksum, 21);
83
+ return base58encode(full);
84
+ }
85
+
86
+ /**
87
+ * Check if an address is a Tron T... address.
88
+ */
89
+ export function isTronAddress(addr: string): boolean {
90
+ return addr.startsWith("T") && addr.length >= 33 && addr.length <= 35;
91
+ }
@@ -0,0 +1,74 @@
1
+ import { secp256k1 } from "@noble/curves/secp256k1.js";
2
+ import { keccak_256 } from "@noble/hashes/sha3.js";
3
+ import type { EncodingParams, IAddressEncoder } from "@wopr-network/platform-core/crypto-plugin";
4
+
5
+ import { sha256 } from "./sha256.js";
6
+
7
+ // secp256k1.Point exists at runtime but the ECDSA TS type doesn't expose it.
8
+ const Point = (secp256k1 as unknown as { Point: { fromHex(h: string): { toBytes(c: boolean): Uint8Array } } }).Point;
9
+
10
+ // ---------- Base58 encoding ----------
11
+
12
+ const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
13
+
14
+ function base58encode(data: Uint8Array): string {
15
+ let num = 0n;
16
+ for (const byte of data) num = num * 256n + BigInt(byte);
17
+ let encoded = "";
18
+ while (num > 0n) {
19
+ encoded = BASE58_ALPHABET[Number(num % 58n)] + encoded;
20
+ num = num / 58n;
21
+ }
22
+ for (const byte of data) {
23
+ if (byte !== 0) break;
24
+ encoded = `1${encoded}`;
25
+ }
26
+ return encoded;
27
+ }
28
+
29
+ function toHex(bytes: Uint8Array): string {
30
+ return Array.from(bytes as unknown as number[], (b: number) => b.toString(16).padStart(2, "0")).join("");
31
+ }
32
+
33
+ // ---------- Keccak-B58Check encoding ----------
34
+
35
+ /**
36
+ * Encode a compressed public key as a Tron (keccak-b58check) address.
37
+ *
38
+ * Steps:
39
+ * 1. Decompress SEC1 compressed pubkey to 65-byte uncompressed
40
+ * 2. keccak256(uncompressed[1:]) — skip the 0x04 prefix
41
+ * 3. Take last 20 bytes as address
42
+ * 4. Prepend version byte (0x41 for Tron mainnet)
43
+ * 5. Append double-SHA-256 checksum (first 4 bytes)
44
+ * 6. Base58 encode
45
+ */
46
+ export function encodeKeccakB58Address(publicKey: Uint8Array, versionByte: number): string {
47
+ const hexKey = toHex(publicKey);
48
+ const uncompressed = Point.fromHex(hexKey).toBytes(false);
49
+ const hash = keccak_256(uncompressed.slice(1));
50
+ const addressBytes = hash.slice(-20);
51
+ const payload = new Uint8Array(21);
52
+ payload[0] = versionByte;
53
+ payload.set(addressBytes, 1);
54
+ const checksum = sha256(sha256(payload));
55
+ const full = new Uint8Array(25);
56
+ full.set(payload);
57
+ full.set(checksum.slice(0, 4), 21);
58
+ return base58encode(full);
59
+ }
60
+
61
+ /**
62
+ * Tron keccak-b58check address encoder.
63
+ * Implements IAddressEncoder for the tron plugin.
64
+ * Default version byte is 0x41 (65) for mainnet. Override with params.version.
65
+ */
66
+ export const keccakB58Encoder: IAddressEncoder = {
67
+ encode(publicKey: Uint8Array, params: EncodingParams): string {
68
+ const versionByte = params.version ? Number(params.version) : 0x41;
69
+ return encodeKeccakB58Address(publicKey, versionByte);
70
+ },
71
+ encodingType(): string {
72
+ return "keccak-b58check";
73
+ },
74
+ };
@@ -0,0 +1,23 @@
1
+ import type { IChainPlugin, WatcherOpts } from "@wopr-network/platform-core/crypto-plugin";
2
+
3
+ import { keccakB58Encoder } from "./encoder.js";
4
+ import { TronEvmWatcher } from "./watcher.js";
5
+
6
+ export { hexToTron, isTronAddress, tronToHex } from "./address-convert.js";
7
+ export { encodeKeccakB58Address, keccakB58Encoder } from "./encoder.js";
8
+ export { TronEvmWatcher } from "./watcher.js";
9
+
10
+ export const tronPlugin: IChainPlugin = {
11
+ pluginId: "tron",
12
+ supportedCurve: "secp256k1",
13
+ encoders: {
14
+ "keccak-b58check": keccakB58Encoder,
15
+ },
16
+ createWatcher(opts: WatcherOpts) {
17
+ return new TronEvmWatcher(opts);
18
+ },
19
+ createSweeper() {
20
+ throw new Error("Not implemented");
21
+ },
22
+ version: 1,
23
+ };