BlockchainWalletService.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. import * as bip39 from 'bip39'
  2. import { BIP32Factory } from 'bip32'
  3. import * as bitcoin from 'bitcoinjs-lib'
  4. import { ethers } from 'ethers'
  5. import { TronWeb } from 'tronweb'
  6. import * as ecc from 'tiny-secp256k1'
  7. import Logger from '@ioc:Adonis/Core/Logger'
  8. import axios from 'axios'
  9. interface WalletBalance {
  10. address: string
  11. balance: string
  12. }
  13. interface WalletAddresses {
  14. btc: WalletBalance
  15. eth: WalletBalance
  16. tron: WalletBalance
  17. }
  18. const CONFIG = {
  19. ETH_API_KEY: process.env.ETH_API_KEY || 'JSYCSZEYYRI1G6CTYW8J6RPXEZGTMBFFMM',
  20. BITCOIN_API_URL: 'https://blockchain.info/rawaddr',
  21. ETHERSCAN_API_URL: 'https://api.etherscan.io/v2/api',
  22. TRON_API_URL: 'https://apilist.tronscan.org/api/account',
  23. TRON_FULL_HOST: 'https://api.trongrid.io',
  24. DECIMAL_PLACES: 8,
  25. BIP_PATHS: {
  26. BTC: (accountIndex: number) => `m/44'/0'/${accountIndex}'/0/0`,
  27. ETH: (accountIndex: number) => `m/44'/60'/${accountIndex}'/0/0`,
  28. TRON: (accountIndex: number) => `m/44'/195'/${accountIndex}'/0/0`
  29. }
  30. }
  31. const bip32 = BIP32Factory(ecc)
  32. class BlockchainWalletService {
  33. /**
  34. * 获取所有支持的区块链地址和余额
  35. * @param mnemonic 助记词字符串
  36. * @param accountIndex 账户索引,默认为0
  37. * @returns 包含所有地址和余额的对象
  38. */
  39. public async getAllAddresses(
  40. mnemonic: string,
  41. accountIndex: number = 0
  42. ): Promise<WalletAddresses> {
  43. // 设置默认返回值
  44. const defaultResult: WalletAddresses = {
  45. btc: { address: '-', balance: this.formatBalance(0) },
  46. eth: { address: '-', balance: this.formatBalance(0) },
  47. tron: { address: '-', balance: this.formatBalance(0) }
  48. }
  49. try {
  50. // 验证助记词
  51. this.validateMnemonic(mnemonic)
  52. // 获取种子
  53. const seed = this.getSeedFromMnemonic(mnemonic)
  54. // 并行生成所有地址
  55. const [btcAddress, ethAddress, tronAddress] = await Promise.all([
  56. Promise.resolve(this.generateBTCAddress(seed, accountIndex)),
  57. Promise.resolve(this.generateETHAddress(seed, accountIndex)),
  58. Promise.resolve(this.generateTRONAddress(seed, accountIndex))
  59. ])
  60. // 并行获取所有余额
  61. const [btcBalance, ethBalance, tronBalance] = await Promise.all([
  62. this.getBitcoinBalance(btcAddress),
  63. this.getEthereumBalance(ethAddress),
  64. this.getTronBalance(tronAddress)
  65. ])
  66. return {
  67. btc: { address: btcAddress, balance: btcBalance },
  68. eth: { address: ethAddress, balance: ethBalance },
  69. tron: { address: tronAddress, balance: tronBalance }
  70. }
  71. } catch (error) {
  72. Logger.error(`Failed to get addresses: ${(error as Error).message}`)
  73. return defaultResult
  74. }
  75. }
  76. /**
  77. * 格式化余额为统一的8位小数格式
  78. * @param balance 数值余额
  79. * @returns 格式化后的字符串余额,固定8位小数
  80. */
  81. private formatBalance(balance: number): string {
  82. return balance.toFixed(CONFIG.DECIMAL_PLACES)
  83. }
  84. /**
  85. * 验证助记词是否有效
  86. * @param mnemonic 助记词字符串
  87. * @throws {Error} 如果助记词无效
  88. */
  89. private validateMnemonic(mnemonic: string): void {
  90. if (!bip39.validateMnemonic(mnemonic)) {
  91. throw new Error('Invalid mnemonic')
  92. }
  93. }
  94. /**
  95. * 获取助记词的种子
  96. * @param mnemonic 助记词字符串
  97. * @returns 种子Buffer
  98. */
  99. private getSeedFromMnemonic(mnemonic: string): Buffer {
  100. this.validateMnemonic(mnemonic)
  101. return bip39.mnemonicToSeedSync(mnemonic)
  102. }
  103. /**
  104. * 根据给定的路径派生子节点
  105. * @param seed 助记词生成的种子
  106. * @param path 路径
  107. * @returns 派生的子节点
  108. * @throws {Error} 如果派生过程失败
  109. */
  110. private deriveChildNodeFromSeed(seed: Buffer, path: string) {
  111. try {
  112. const root = bip32.fromSeed(seed)
  113. return root.derivePath(path)
  114. } catch (error) {
  115. Logger.error(`Failed to derive child node: ${(error as Error).message}`)
  116. throw new Error(`Key derivation failed: ${(error as Error).message}`)
  117. }
  118. }
  119. /**
  120. * 生成比特币(BTC)地址
  121. * @param seed 助记词生成的种子
  122. * @param accountIndex 账户索引,默认为0
  123. * @param network 网络类型,默认为主网
  124. * @returns BTC地址
  125. * @throws {Error} 如果地址生成失败
  126. */
  127. private generateBTCAddress(
  128. seed: Buffer,
  129. accountIndex: number = 0,
  130. network = bitcoin.networks.bitcoin
  131. ): string {
  132. const path = CONFIG.BIP_PATHS.BTC(accountIndex)
  133. const child = this.deriveChildNodeFromSeed(seed, path)
  134. if (!child.publicKey) {
  135. throw new Error('Failed to generate BTC public key')
  136. }
  137. const { address } = bitcoin.payments.p2pkh({
  138. pubkey: Buffer.from(child.publicKey),
  139. network
  140. })
  141. if (!address) {
  142. throw new Error('Failed to generate BTC address')
  143. }
  144. return address
  145. }
  146. /**
  147. * 生成以太坊(ETH)地址
  148. * @param seed 助记词生成的种子
  149. * @param accountIndex 账户索引,默认为0
  150. * @returns ETH地址
  151. * @throws {Error} 如果地址生成失败
  152. */
  153. private generateETHAddress(seed: Buffer, accountIndex: number = 0): string {
  154. const path = CONFIG.BIP_PATHS.ETH(accountIndex)
  155. const child = this.deriveChildNodeFromSeed(seed, path)
  156. if (!child.privateKey) {
  157. throw new Error('Failed to generate ETH private key')
  158. }
  159. const privateKeyHex = Buffer.from(child.privateKey).toString('hex')
  160. const wallet = new ethers.Wallet(`0x${privateKeyHex}`)
  161. return wallet.address
  162. }
  163. /**
  164. * 生成波场(TRON)地址
  165. * @param seed 助记词生成的种子
  166. * @param accountIndex 账户索引,默认为0
  167. * @returns TRON地址
  168. * @throws {Error} 如果地址生成失败
  169. */
  170. private generateTRONAddress(seed: Buffer, accountIndex: number = 0): string {
  171. const path = CONFIG.BIP_PATHS.TRON(accountIndex)
  172. const child = this.deriveChildNodeFromSeed(seed, path)
  173. if (!child.privateKey) {
  174. throw new Error('Failed to generate TRON private key')
  175. }
  176. const privateKeyHex = Buffer.from(child.privateKey).toString('hex')
  177. const tronWeb = new TronWeb({ fullHost: CONFIG.TRON_FULL_HOST })
  178. const account = tronWeb.address.fromPrivateKey(privateKeyHex)
  179. if (typeof account !== 'string') {
  180. throw new Error('Failed to generate TRON address')
  181. }
  182. return account
  183. }
  184. /**
  185. * 生成比特币(BTC)地址 - 公共接口
  186. * @param mnemonic 助记词字符串
  187. * @param accountIndex 账户索引,默认为0
  188. * @param network 网络类型,默认为主网
  189. * @returns BTC地址
  190. */
  191. public getBTCAddress(
  192. mnemonic: string,
  193. accountIndex: number = 0,
  194. network = bitcoin.networks.bitcoin
  195. ): string {
  196. const seed = this.getSeedFromMnemonic(mnemonic)
  197. return this.generateBTCAddress(seed, accountIndex, network)
  198. }
  199. /**
  200. * 生成以太坊(ETH)地址 - 公共接口
  201. * @param mnemonic 助记词字符串
  202. * @param accountIndex 账户索引,默认为0
  203. * @returns ETH地址
  204. */
  205. public getETHAddress(mnemonic: string, accountIndex: number = 0): string {
  206. const seed = this.getSeedFromMnemonic(mnemonic)
  207. return this.generateETHAddress(seed, accountIndex)
  208. }
  209. /**
  210. * 生成波场(TRON)地址 - 公共接口
  211. * @param mnemonic 助记词字符串
  212. * @param accountIndex 账户索引,默认为0
  213. * @returns TRON地址
  214. */
  215. public getTRONAddress(mnemonic: string, accountIndex: number = 0): string {
  216. const seed = this.getSeedFromMnemonic(mnemonic)
  217. return this.generateTRONAddress(seed, accountIndex)
  218. }
  219. /**
  220. * 获取比特币余额
  221. * @param address 比特币地址
  222. * @returns 比特币余额(8位小数格式字符串)
  223. */
  224. public async getBitcoinBalance(address: string): Promise<string> {
  225. try {
  226. const response = await axios.get(`${CONFIG.BITCOIN_API_URL}/${address}`)
  227. const balance = response.data.final_balance / 1e8
  228. return this.formatBalance(balance)
  229. } catch (error) {
  230. Logger.error(`Failed to get Bitcoin balance: ${(error as Error).message}`)
  231. return this.formatBalance(0)
  232. }
  233. }
  234. /**
  235. * 获取以太坊余额
  236. * @param address 以太坊地址
  237. * @returns 以太坊余额(8位小数格式字符串)
  238. */
  239. public async getEthereumBalance(address: string): Promise<string> {
  240. try {
  241. const response = await axios.get(CONFIG.ETHERSCAN_API_URL, {
  242. params: {
  243. chainid: 1,
  244. module: 'account',
  245. action: 'balance',
  246. address: address,
  247. tag: 'latest',
  248. apikey: CONFIG.ETH_API_KEY
  249. }
  250. })
  251. if (response.data.status !== '1') {
  252. Logger.warn(`Etherscan API error: ${response.data.message}`)
  253. return this.formatBalance(0)
  254. }
  255. const balance = Number(response.data.result) / 1e18
  256. return this.formatBalance(balance)
  257. } catch (error) {
  258. Logger.error(`Failed to get Ethereum balance: ${(error as Error).message}`)
  259. return this.formatBalance(0)
  260. }
  261. }
  262. /**
  263. * 获取波场USDT余额
  264. * @param address 波场地址
  265. * @returns 波场USDT余额(8位小数格式字符串)
  266. */
  267. public async getTronBalance(address: string): Promise<string> {
  268. try {
  269. const response = await axios.get(`${CONFIG.TRON_API_URL}?address=${address}`)
  270. const data = response.data
  271. // 查找USDT TRC20代币余额
  272. let usdtBalance = 0
  273. if (data.trc20token_balances && Array.isArray(data.trc20token_balances)) {
  274. const usdtToken = data.trc20token_balances.find(
  275. (token: any) => token.tokenAbbr === 'USDT'
  276. )
  277. if (usdtToken) {
  278. const decimals = Number(usdtToken.tokenDecimal) || 6
  279. usdtBalance = Number(usdtToken.balance) / Math.pow(10, decimals)
  280. }
  281. }
  282. return this.formatBalance(usdtBalance)
  283. } catch (error) {
  284. Logger.error(`Failed to get TRON balance: ${(error as Error).message}`)
  285. return this.formatBalance(0)
  286. }
  287. }
  288. }
  289. export default new BlockchainWalletService()