1
0

wallet.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. /* Import modules. */
  2. import { defineStore } from 'pinia'
  3. import moment from 'moment'
  4. import BCHJS from '@psf/bch-js'
  5. import { sha256 } from '@nexajs/crypto'
  6. import {
  7. mnemonicToEntropy,
  8. mnemonicToSeed,
  9. } from '@nexajs/hdnode'
  10. import { Wallet } from '@nexajs/wallet'
  11. import _broadcast from './wallet/broadcast.ts'
  12. import _completeFusion from './wallet/completeFusion.ts'
  13. import _getWifForAddress from './wallet/getWifForAddress.ts'
  14. import _setEntropy from './wallet/setEntropy.ts'
  15. import _setupKeychain from './wallet/setupKeychain.ts'
  16. import _setupHushKeychain from './wallet/setupHushKeychain.ts'
  17. import _signFusion from './wallet/signFusion.ts'
  18. import _startFusion from './wallet/startFusion.ts'
  19. /* Initialize constants. */
  20. const HUSH_PROTOCOL_ID = 0x48555348
  21. // REST API servers.
  22. const BCHN_MAINNET = 'https://bchn.fullstack.cash/v5/'
  23. // Instantiate bch-js based on the network.
  24. const bchjs = new BCHJS({
  25. restURL: BCHN_MAINNET,
  26. })
  27. /* Set constants. */
  28. // const UPDATE_UTXOS_INTERVAL = 8000 // Allows for ~7 (REST) requests per minute.
  29. const UPDATE_UTXOS_INTERVAL = 15000 // Allows for ~8 (Double REST) requests per minute.
  30. /**
  31. * Wallet Store
  32. */
  33. export const useWalletStore = defineStore('wallet', {
  34. state: () => ({
  35. /**
  36. * Entropy
  37. * (DEPRECATED -- MUST REMAIN SUPPORTED INDEFINITELY)
  38. *
  39. * Initialize entropy (used for HD wallet).
  40. *
  41. * NOTE: This is a cryptographically-secure "random"
  42. * 32-byte (256-bit) value.
  43. */
  44. _entropy: null,
  45. /**
  46. * Keychain
  47. *
  48. * Manages a collection of BIP-32 wallets.
  49. *
  50. * [
  51. * {
  52. * id : '5be2e5c3-9d27-4b0f-bb3c-8b2ef6fdaafd',
  53. * type : 'studio',
  54. * title : `My Studio Wallet`,
  55. * entropy : 0x0000000000000000000000000000000000000000000000000000000000000000,
  56. * createdAt : 0123456789,
  57. * updatedAt : 1234567890,
  58. * },
  59. * {
  60. * id : 'f2457985-4b92-4025-be8d-5f11a5fc4077',
  61. * type : 'ledger',
  62. * title : `My Ledger Wallet`,
  63. * createdAt : 0123456789,
  64. * updatedAt : 1234567890,
  65. * },
  66. * ]
  67. */
  68. _keychain: null,
  69. /**
  70. * Unspent Transaction Outputs (UTXOs)
  71. *
  72. * A dedicated handler for ALL network UTXOs.
  73. */
  74. _utxos: null,
  75. /**
  76. * Wallet
  77. *
  78. * Currently active wallet object.
  79. */
  80. _wallet: null,
  81. }),
  82. getters: {
  83. /* Return NexaJS wallet instance. */
  84. wallet(_state) {
  85. return _state._wallet
  86. },
  87. /* Return wallet status. */
  88. isReady(_state) {
  89. return _state.wallet?.isReady
  90. },
  91. /* Return wallet status. */
  92. address(_state) {
  93. return _state.wallet?.address
  94. },
  95. /* Return wallet status. */
  96. assets(_state) {
  97. return _state.wallet?.assets
  98. },
  99. /* Return wallet status. */
  100. balances(_state) {
  101. // FIXME: Update library to expose data OR
  102. // refactor to `markets`.
  103. return _state.wallet?._balances
  104. },
  105. /* Return keychain. */
  106. keychain(_state) {
  107. return _state._keychain
  108. },
  109. /* Return mnemonic. */
  110. mnemonic(_state) {
  111. if (!_state._wallet) {
  112. return null
  113. }
  114. return _state._wallet._mnemonic
  115. },
  116. /* Return UTXOs. */
  117. utxos(_state) {
  118. if (!_state._wallet) {
  119. return null
  120. }
  121. return _state._utxos
  122. },
  123. /* Return UTXOs. */
  124. fusionInputs(_state) {
  125. if (!_state._wallet) {
  126. return null
  127. }
  128. // const collection = _state._utxos
  129. console.log('STATE (utxos)', _state._utxos)
  130. /* Initialize inputs (collection). */
  131. let inputs = {}
  132. /* Verify Main chain. */
  133. if (_state._utxos[0]) {
  134. inputs = { ...inputs, ..._state._utxos[0] }
  135. }
  136. /* Verify Hush chain. */
  137. if (_state._utxos[HUSH_PROTOCOL_ID]) {
  138. inputs = { ...inputs, ..._state._utxos[HUSH_PROTOCOL_ID] }
  139. }
  140. console.log('FUSION (inputs)', inputs)
  141. /* Return inputs. */
  142. return inputs
  143. },
  144. fusionAddrs() {
  145. return this._keychain[HUSH_PROTOCOL_ID]
  146. },
  147. /* Return wif. */
  148. wif(_state) {
  149. if (!_state._wallet) {
  150. return null
  151. }
  152. return _state._wallet.wif
  153. },
  154. },
  155. actions: {
  156. /**
  157. * Initialize
  158. *
  159. * Setup the wallet store.
  160. * 1. Retrieve the saved entropy.
  161. * 2. Initialize a Wallet instance.
  162. * 3. Load assets.
  163. */
  164. async init() {
  165. console.info('Initializing wallet...')
  166. if (this._entropy === null) {
  167. this._wallet = 'NEW' // FIXME TEMP NEW WALLET FLAG
  168. // throw new Error('Missing wallet entropy.')
  169. return console.error('Missing wallet entropy.')
  170. }
  171. /* Validate keychain. */
  172. if (this._keychain === null) {
  173. this._keychain = {
  174. 0: {}, // NEXA chain
  175. 0x48555348: {}, // HUSH chain (1_213_551_432)
  176. }
  177. console.log('Keychain initialized successfully!', this._keychain)
  178. /* Set keychain. */
  179. _setupKeychain.bind(this)()
  180. /* Set Hush keychian. */
  181. _setupHushKeychain.bind(this)()
  182. }
  183. // FOR DEV PURPOSES ONLY
  184. _setupKeychain.bind(this)()
  185. _setupHushKeychain.bind(this)()
  186. /* Request a wallet instance (by mnemonic). */
  187. this._wallet = await Wallet.init(this._entropy, true)
  188. console.info('(Initialized) wallet', this.wallet)
  189. // this._assets = { ...this.wallet.assets } // cloned assets
  190. /* Set (default) asset. */
  191. this.wallet.setAsset('0')
  192. /* Handle balance updates. */
  193. this.wallet.on('balances', async (_assets) => {
  194. // console.log('Wallet Balances (onChanges):', _assets)
  195. /* Close asset locally. */
  196. // FIXME Read ASSETS directly from library (getter).
  197. this._assets = { ..._assets }
  198. })
  199. // FIXME ADDED FOR BITCOIN CASH SUPPORT
  200. // setInterval(this.updateUtxos, UPDATE_UTXOS_INTERVAL)
  201. /* Update ALL chains. */
  202. this.updateUtxos(true)
  203. },
  204. /**
  205. * Create Wallet
  206. *
  207. * Create a fresh wallet.
  208. *
  209. * @param _entropy A 32-byte (hex-encoded) random value.
  210. */
  211. createWallet(_entropy) {
  212. /* Validate entropy. */
  213. // NOTE: Expect HEX value to be 32 or 64 characters.
  214. if (_entropy?.length !== 32 && _entropy?.length !== 64) {
  215. console.error(_entropy, 'is NOT valid entropy.')
  216. /* Clear (invalid) entropy. */
  217. _entropy = null
  218. }
  219. /* Set entropy. */
  220. _setEntropy.bind(this)(_entropy)
  221. /* Initialize wallet. */
  222. this.init()
  223. },
  224. /**
  225. * Get Bitcoin Cash Address
  226. *
  227. * Will return the "child" address of a master node,
  228. * based on the account index, change flag and address index.
  229. */
  230. getBchAddress(
  231. _accountIdx = 0,
  232. _isChange = 0, // NOTE: 0 = false, 1 = true
  233. _addressIdx = 0,
  234. ) {
  235. /* Set root seed. */
  236. const rootSeed = mnemonicToSeed(this.mnemonic)
  237. // console.log('rootSeed', rootSeed)
  238. /* Set HD master node. */
  239. const masterHdnode = bchjs.HDNode.fromSeed(Buffer.from(rootSeed, 'hex') )
  240. // console.log('masterHdnode', masterHdnode);
  241. /* Set child node. */
  242. const childNode = masterHdnode
  243. .derivePath(`m/44'/145'/${_accountIdx}'/${_isChange}/${_addressIdx}`)
  244. // console.log('childNode', childNode)
  245. /* Set Bitcoin Cash address. */
  246. const cashAddress = bchjs.HDNode.toCashAddress(childNode)
  247. // console.log('cashAddress', cashAddress)
  248. return cashAddress
  249. },
  250. async updateUtxos(_allChains = false) {
  251. /* Initialize locals. */
  252. let coins
  253. let data
  254. let hushAddresses
  255. let usedAddresses
  256. let utxos
  257. let walletAddresses
  258. /* Validate UTXO handler. */
  259. if (this._utxos === null) {
  260. /* Initialize UTXO handler. */
  261. this._utxos = {}
  262. }
  263. coins = this._keychain[HUSH_PROTOCOL_ID]
  264. // console.log('WAITING FOR COINS', coins)
  265. hushAddresses = Object.keys(coins).map(_coinid => {
  266. const coin = coins[_coinid]
  267. return coin.address
  268. })
  269. // console.log('HUSH ADDRESSES', hushAddresses)
  270. /* Request UTXO data. */
  271. data = await $fetch('/api/electrum', {
  272. method: 'POST',
  273. body: JSON.stringify({
  274. method: 'blockchain.scripthash.listunspent',
  275. params: hushAddresses.slice(0, 20)
  276. }),
  277. })
  278. .catch(err => console.error(err))
  279. console.log('HUSH UTXOS', data)
  280. // FIXME Update the deltas ONLY!
  281. this._utxos[HUSH_PROTOCOL_ID] = {}
  282. /* Handle unspent outputs. */
  283. data.forEach(_unspent => {
  284. _unspent.utxos.forEach(_utxo => {
  285. // console.log('ADDING HUSH UTXO...', _utxo)
  286. /* Generate outpoint (hash). */
  287. const outpoint = sha256(_utxo.tx_hash + ':' + _utxo.tx_pos)
  288. /* Add to UTXOs. */
  289. this._utxos[HUSH_PROTOCOL_ID][outpoint] = {
  290. address: _unspent.address,
  291. ..._utxo,
  292. wif: _getWifForAddress.bind(this)(_unspent.address),
  293. }
  294. })
  295. })
  296. /* Request history data. */
  297. data = await $fetch('/api/electrum', {
  298. method: 'POST',
  299. body: JSON.stringify({
  300. method: 'blockchain.scripthash.get_history',
  301. params: hushAddresses.slice(0, 20)
  302. }),
  303. })
  304. .catch(err => console.error(err))
  305. console.log('HUSH TX HISTORY', data)
  306. usedAddresses = data?.filter(_tx => {
  307. if (_tx.txs.length > 0) {
  308. return _tx.address
  309. }
  310. })
  311. usedAddresses = usedAddresses.map(_tx => {
  312. return _tx.address
  313. })
  314. // console.log('USED ADDRESSES', usedAddresses)
  315. Object.keys(coins).forEach(_coinid => {
  316. /* Set coin. */
  317. const coin = coins[_coinid]
  318. /* Verify address exists. */
  319. if (usedAddresses.includes(coin.address)) {
  320. /* Set (used) flag. */
  321. coin.isUsed = true
  322. // console.log('COIN (is used)', coin)
  323. }
  324. })
  325. /* Validate chain handler flag. */
  326. if (_allChains) {
  327. let bchAddress1
  328. let bchAddress2
  329. let bchAddress3
  330. bchAddress1 = this.getBchAddress(0, 0, 0)
  331. // console.log('BCH ADDRESS-1', bchAddress1)
  332. bchAddress2 = this.getBchAddress(0, 0, 1)
  333. // console.log('BCH ADDRESS-2', bchAddress2)
  334. bchAddress3 = this.getBchAddress(0, 0, 2)
  335. // console.log('BCH ADDRESS-3', bchAddress3)
  336. data = await $fetch('/api/electrum', {
  337. method: 'POST',
  338. body: JSON.stringify({
  339. method: 'blockchain.scripthash.listunspent',
  340. params: [
  341. bchAddress1,
  342. bchAddress2,
  343. bchAddress3,
  344. ],
  345. })
  346. })
  347. console.log('MAIN WALLET DATA', data)
  348. // if (!this._utxos[0]) {
  349. this._utxos[0] = {}
  350. // }
  351. /* Handle unspent outputs. */
  352. data.forEach(_unspent => {
  353. _unspent.utxos.forEach(_utxo => {
  354. console.log('ADDING UTXO...', _utxo)
  355. /* Generate outpoint (hash). */
  356. const outpoint = sha256(_utxo.tx_hash + ':' + _utxo.tx_pos)
  357. /* Add to UTXOs. */
  358. this._utxos[0][outpoint] = {
  359. address: _unspent.address,
  360. ..._utxo,
  361. wif: _getWifForAddress.bind(this)(_unspent.address),
  362. }
  363. })
  364. })
  365. // FIXME Update the delta ONLY!
  366. // this._utxos[0] = data?.utxos
  367. }
  368. return true
  369. },
  370. /**
  371. * Get Fusion Address
  372. *
  373. * Will retrieve the next available fusion address and lock it.
  374. */
  375. getFusionAddress() {
  376. if (!this.fusionAddrs) {
  377. return ''
  378. }
  379. const addressIdx = Object.keys(this.fusionAddrs).find(_addressIdx => {
  380. const fusionAddress = this.fusionAddrs[_addressIdx]
  381. // console.log('fusionAddress', fusionAddress)
  382. return fusionAddress.isUsed === false && fusionAddress.isLocked === false
  383. })
  384. // console.log('ADDRESS IDX', addressIdx)
  385. if (typeof addressIdx !== 'undefined') {
  386. /* Set locked flag. */
  387. this._keychain[HUSH_PROTOCOL_ID][addressIdx].isLocked = true
  388. this._keychain[HUSH_PROTOCOL_ID][addressIdx].updatedAt = moment().unix()
  389. /* Return address. */
  390. return this.fusionAddrs[addressIdx].address
  391. } else {
  392. return ''
  393. }
  394. },
  395. /**
  396. * Get WIF for Address
  397. *
  398. * TBD..
  399. */
  400. getWifForAddress(_address) {
  401. return _getWifForAddress.bind(this)(_address)
  402. },
  403. async startFusion() {
  404. /* Start fusions. */
  405. return _startFusion.bind(this)()
  406. },
  407. async signFusion() {
  408. /* Start fusions. */
  409. return _signFusion.bind(this)()
  410. },
  411. async completeFusion() {
  412. /* Start fusions. */
  413. return _completeFusion.bind(this)()
  414. },
  415. async transfer(_receiver, _satoshis) {
  416. /* Validate transaction type. */
  417. if (this.asset.group === '0') {
  418. /* Send coins. */
  419. return await this.wallet.send(_receiver, _satoshis)
  420. } else {
  421. /* Send tokens. */
  422. return await this.wallet.send(this.asset.token_id_hex, _receiver, _satoshis)
  423. }
  424. },
  425. broadcast(_network, _rawTx) {
  426. /* Broadcast to raw (hex) transaction to mainnet. */
  427. return _broadcast.bind(this)(_network, _rawTx)
  428. },
  429. setEntropy(_entropy) {
  430. this._entropy = _entropy
  431. },
  432. setMnemonic(_mnemonic) {
  433. let entropy
  434. let error
  435. try {
  436. /* Derive entropy. */
  437. entropy = mnemonicToEntropy(_mnemonic)
  438. } catch (err) {
  439. /* Set error message. */
  440. error = err.message
  441. }
  442. /* Validate error. */
  443. if (error) {
  444. return error
  445. }
  446. /* Set entropy. */
  447. this._entropy = entropy
  448. /* Create wallet. */
  449. this.createWallet(entropy)
  450. /* Return entropy. */
  451. return this.wallet
  452. },
  453. destroy() {
  454. /* Reset wallet. */
  455. this._entropy = null
  456. this._wallet = null
  457. console.info('Wallet destroyed successfully!')
  458. },
  459. },
  460. })