wallet.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. /* Import modules. */
  2. import { defineStore } from 'pinia'
  3. import {
  4. decodeAddress,
  5. encodeAddress,
  6. } from '@nexajs/address'
  7. import {
  8. ripemd160,
  9. sha256,
  10. } from '@nexajs/crypto'
  11. import { mnemonicToEntropy } from '@nexajs/hdnode'
  12. import { broadcast } from '@nexajs/provider'
  13. import {
  14. buildCoins,
  15. getCoins,
  16. sendCoins,
  17. } from '@nexajs/purse'
  18. import { getAddressUnspent } from '@nexajs/rostrum'
  19. import {
  20. encodeDataPush,
  21. OP,
  22. } from '@nexajs/script'
  23. import {
  24. binToHex,
  25. hexToBin,
  26. utf8ToBin,
  27. } from '@nexajs/utils'
  28. import {
  29. Wallet,
  30. WalletStatus,
  31. } from '@nexajs/wallet'
  32. import { useSystemStore } from '@/stores/system'
  33. import _setEntropy from './wallet/setEntropy.ts'
  34. /* Initialize constants. */
  35. const CASHOUT_FEE = BigInt(1000) // FIXME We MUST calculate this (same as change).
  36. const TIMETOCASHOUT_HEX = '6c6c6c6c5479009c63557a7cad6d6d67547a519d537ab275030051147b7e00cd8800cc55ea537a94a2697568'
  37. const UPDATE_BALANCE_INTERVAL = 5000
  38. /* Initialize globals. */
  39. let balanceHandler
  40. /**
  41. * Wallet Store
  42. */
  43. export const useWalletStore = defineStore('wallet', {
  44. state: () => ({
  45. /**
  46. * Assets
  47. *
  48. * Will hold ALL assets that the wallet manages.
  49. */
  50. _assets: null,
  51. /**
  52. * Cashout Address
  53. *
  54. * Use to automatically cashout a wallet (after a specified timeout).
  55. */
  56. _cashoutAddr: null,
  57. /**
  58. * Coins
  59. *
  60. * Manage unspent coins.
  61. */
  62. _coins: null,
  63. /**
  64. * Entropy
  65. * (DEPRECATED -- MUST REMAIN SUPPORTED INDEFINITELY)
  66. *
  67. * Initialize entropy (used for HD wallet).
  68. *
  69. * NOTE: This is a cryptographically-secure "random"
  70. * 32-byte (256-bit) value.
  71. */
  72. _entropy: null,
  73. /**
  74. * Wallet
  75. *
  76. * Currently active wallet object.
  77. */
  78. _wallet: null,
  79. }),
  80. getters: {
  81. /* Return (abbreviated) wallet status. */
  82. // abbr(_state) {
  83. // if (!_state._wallet) {
  84. // return null
  85. // }
  86. // return _state._wallet.abbr
  87. // },
  88. abbr(_state) {
  89. if (!this.address) {
  90. return null
  91. }
  92. return this.address.slice(5, 17) + '...' + this.address.slice(-12)
  93. },
  94. /* Return wallet status. */
  95. // address(_state) {
  96. // if (!_state._wallet) {
  97. // return null
  98. // }
  99. // return _state._wallet.address
  100. // },
  101. address(_state) {
  102. /* Initialize locals. */
  103. let contractAddress
  104. let prefix
  105. /* Set prefix. */
  106. prefix = 'nexa'
  107. /* Encode the public key hash into a P2PKH nexa address. */
  108. contractAddress = encodeAddress(
  109. prefix,
  110. 'TEMPLATE',
  111. this.scriptPubkey,
  112. )
  113. // console.info('(CONTRACT) ADDRESS', contractAddress)
  114. return contractAddress
  115. },
  116. // asset(_state) {
  117. // if (!this.assets || !this.wallet) {
  118. // return null
  119. // }
  120. // return this.assets[this.wallet.assetid]
  121. // },
  122. asset(_state) {
  123. if (!this.assets || !this.wallet) {
  124. return null
  125. }
  126. /* Initialize locals. */
  127. let amount
  128. let decimal_places
  129. let fiat
  130. let satoshis
  131. let USD
  132. /* Set decimal places. */
  133. decimal_places = 2
  134. /* Calculate satoshis. */
  135. satoshis = this._assets.reduce(
  136. (totalSats, coin) => (totalSats + BigInt(coin.value)), BigInt(0)
  137. )
  138. /* Calculate amount. */
  139. amount = satoshis
  140. /* Initialize System. */
  141. const System = useSystemStore()
  142. /* Initialize USD value. */
  143. USD = 0
  144. /* Validate system ticker. */
  145. if (System.ticker) {
  146. /* Set quote. */
  147. const quote = System.ticker.quote
  148. console.log('QUOTE', quote)
  149. /* Validate quote. */
  150. if (quote) {
  151. /* Set (USD) price. */
  152. USD = (quote?.USD?.price * parseFloat(Number(satoshis) / 100))
  153. }
  154. }
  155. /* Set fiat. */
  156. fiat = { USD }
  157. /* Build asset. */
  158. const asset = {
  159. amount,
  160. satoshis,
  161. decimal_places,
  162. fiat,
  163. }
  164. /* Return asset. */
  165. return asset
  166. },
  167. assets(_state) {
  168. if (_state._assets) {
  169. return _state._assets
  170. }
  171. if (!_state._wallet) {
  172. return null
  173. }
  174. return _state._wallet.assets
  175. },
  176. /* Return cashout address. */
  177. cashoutAddr(_state) {
  178. if (!_state._cashoutAddr) {
  179. return null
  180. }
  181. return _state._cashoutAddr
  182. },
  183. /* Return unspent coins. */
  184. coins(_state) {
  185. if (!_state._coins) {
  186. return []
  187. }
  188. return _state._coins
  189. },
  190. /* Return wallet status. */
  191. isLoading(_state) {
  192. if (!_state._wallet) {
  193. return true
  194. }
  195. return _state._wallet.isLoading
  196. },
  197. /* Return wallet status. */
  198. isReady(_state) {
  199. if (!_state._wallet) {
  200. return false
  201. }
  202. if (_state._wallet._entropy) {
  203. return true
  204. }
  205. return _state._wallet.isReady
  206. },
  207. /* Return public key. */
  208. publicKey(_state) {
  209. if (!_state._wallet?.publicKey) {
  210. return null
  211. }
  212. return _state._wallet?.publicKey
  213. },
  214. scriptPubkey(_state) {
  215. /* Initialize locals. */
  216. let constraintData
  217. let constraintHash
  218. let decoded
  219. let gratitude
  220. let lockingScript
  221. let prefix
  222. let publicKey
  223. let recoveryPkh
  224. let scriptHash
  225. let scriptPubkey
  226. let timeout
  227. /* Set prefix. */
  228. prefix = 'nexa'
  229. /* Set locking script. */
  230. lockingScript = hexToBin(TIMETOCASHOUT_HEX)
  231. // console.info('CONTRACT TEMPLATE', binToHex(lockingScript))
  232. scriptHash = ripemd160(sha256(lockingScript))
  233. // console.log('TEMPLATE HASH', binToHex(scriptHash))
  234. /* Set public key. */
  235. publicKey = this.publicKey
  236. // console.log('PUBLIC KEY', publicKey)
  237. /* Hash the public key hash according to the P2PKH/P2PKT scheme. */
  238. constraintData = encodeDataPush(publicKey)
  239. /* Set the constraint hash. */
  240. constraintHash = ripemd160(sha256(constraintData))
  241. /* Decode (cashout) address. */
  242. decoded = decodeAddress(this._cashoutAddr)
  243. // console.log('DECODED (cashout addr)', decoded)
  244. recoveryPkh = decoded.hash.slice(3) // NOTE: Strip out public key hash.
  245. // console.log('RECOVERY PKH', binToHex(recoveryPkh))
  246. gratitude = hexToBin('2710') // 10,000 (100.00 NEXA)
  247. // gratitude = hexToBin('03e8') // 1,000 (10.00 NEXA)
  248. gratitude.reverse()
  249. gratitude = encodeDataPush(gratitude)
  250. // timeout = hexToBin('4013c7') // (5,063) approx 30 days
  251. // timeout = hexToBin('4000a9') // (169) approx 24 hours
  252. // timeout = hexToBin('400007') // (7) approx 1 hour
  253. timeout = hexToBin('400001') // (1) approx 8 1/2 minutes
  254. timeout.reverse()
  255. timeout = encodeDataPush(timeout)
  256. /* Build script public key. */
  257. scriptPubkey = new Uint8Array([
  258. OP.ZERO, // groupid or empty stack item
  259. ...encodeDataPush(scriptHash), // script hash
  260. ...encodeDataPush(constraintHash), // arguments hash
  261. ...encodeDataPush(recoveryPkh), // A recovery address (specified by the Owner) used to cashout the balance, after a timeout period.
  262. ...gratitude, // The rate of exchange, charged by the Provider. (measured in <satoshis> per <asset>)
  263. ...timeout, // The rate of exchange, charged by the Provider. (measured in <satoshis> per <asset>)
  264. ])
  265. // console.info('\nSCRIPT PUBLIC KEY', binToHex(scriptPubkey))
  266. /* Return script public key. */
  267. return scriptPubkey
  268. },
  269. /* Return NEXA.js wallet instance. */
  270. wallet(_state) {
  271. return _state._wallet
  272. },
  273. WalletStatus() {
  274. return WalletStatus
  275. },
  276. wif(_state) {
  277. return _state._wallet.wif
  278. }
  279. },
  280. actions: {
  281. /**
  282. * Initialize
  283. *
  284. * Setup the wallet store.
  285. * 1. Retrieve the saved entropy.
  286. * 2. Initialize a Wallet instance.
  287. * 3. Load assets.
  288. */
  289. async init() {
  290. console.info('Initializing wallet...')
  291. if (this._entropy === null) {
  292. this._wallet = 'NEW' // FIXME TEMP NEW WALLET FLAG
  293. // throw new Error('Missing wallet entropy.')
  294. return console.error('Missing wallet entropy.')
  295. }
  296. /* Request a wallet instance (by mnemonic). */
  297. this._wallet = await Wallet.init(this._entropy, true)
  298. console.info('(Initialized) wallet', this.wallet)
  299. /* Update balance. */
  300. balanceHandler = setInterval(this.updateBalance, UPDATE_BALANCE_INTERVAL)
  301. this.updateBalance()
  302. },
  303. /**
  304. * Create Wallet
  305. *
  306. * Create a fresh wallet.
  307. *
  308. * @param _entropy A 32-byte (hex-encoded) random value.
  309. */
  310. createWallet(_entropy) {
  311. /* Validate entropy. */
  312. // NOTE: Expect HEX value to be 32 or 64 characters.
  313. if (_entropy?.length !== 32 && _entropy?.length !== 64) {
  314. console.error(_entropy, 'is NOT valid entropy.')
  315. /* Clear (invalid) entropy. */
  316. _entropy = null
  317. }
  318. /* Set entropy. */
  319. _setEntropy.bind(this)(_entropy)
  320. /* Initialize wallet. */
  321. this.init()
  322. },
  323. async cashout(_receiver) {
  324. /* Calculate remaining satoshis. */
  325. // FIXME This needs to be ZERO balance.
  326. const satoshis = (this.asset.satoshis - CASHOUT_FEE)
  327. /* Transfer assets to receiver. */
  328. this.transfer(_receiver, satoshis)
  329. },
  330. async transfer(_receiver, _satoshis) {
  331. // console.log('ASSET', this.asset)
  332. // console.log('ASSETS', this.assets)
  333. // console.log('COINS-1', this.coins)
  334. // console.log('PUBLIC KEY', binToHex(this._wallet.publicKey))
  335. /* Initialize locals. */
  336. let coins
  337. let lockingScript
  338. let receivers
  339. let response
  340. let scriptPubkey
  341. let unlockingScript
  342. /* Set script public key. */
  343. scriptPubkey = encodeDataPush(this.publicKey)
  344. /* Set locking script. */
  345. lockingScript = hexToBin(TIMETOCASHOUT_HEX)
  346. // console.info('\nCONTRACT TEMPLATE', binToHex(lockingScript))
  347. /* Set unlocking script. */
  348. // NOTE: Index of (executed) contract method.
  349. unlockingScript = new Uint8Array([
  350. ...encodeDataPush(scriptPubkey),
  351. ...utf8ToBin('{{SIGNATURE}}'), // placeholder for signature
  352. OP.ZERO, // contract function index
  353. ])
  354. /* Handle coin locks. */
  355. coins = this.coins.map(_coin => {
  356. return {
  357. ..._coin,
  358. locking: lockingScript,
  359. unlocking: unlockingScript,
  360. }
  361. })
  362. // console.log('COINS-2', coins)
  363. /* Initialize receivers. */
  364. receivers = []
  365. /* Add value output. */
  366. receivers.push({
  367. address: _receiver,
  368. satoshis: _satoshis,
  369. })
  370. /* Add change output. */
  371. receivers.push({
  372. address: _receiver,
  373. })
  374. // console.log('RECEIVERS', receivers)
  375. /* Send UTXO request. */
  376. response = await buildCoins(coins, receivers)
  377. .catch(err => console.error(err))
  378. // console.log('BUILD TRANSACTION', response.raw)
  379. response = await broadcast(response.raw)
  380. .catch(err => console.error(err))
  381. console.log('SEND TRANSACTION', response)
  382. },
  383. async updateBalance() {
  384. /* Initialize locals. */
  385. let assets
  386. let unspent
  387. /* Request (unspent) coins. */
  388. this._coins = await getCoins(this.wif, this.scriptPubkey)
  389. .catch(err => console.error(err))
  390. /* Request (unspent) coins. */
  391. unspent = await getAddressUnspent(this.address)
  392. .catch(err => console.error(err))
  393. // console.log('UNSPENT', unspent)
  394. /* Filter coins ONLY. */
  395. assets = unspent.filter(_unspent => {
  396. return _unspent.has_token === false
  397. })
  398. /* Set assets. */
  399. this._assets = assets
  400. },
  401. setEntropy(_entropy) {
  402. this._entropy = _entropy
  403. },
  404. setCashoutAddr(_cashoutAddr) {
  405. this._cashoutAddr = _cashoutAddr
  406. },
  407. setMnemonic(_mnemonic) {
  408. let entropy
  409. let error
  410. try {
  411. /* Derive entropy. */
  412. entropy = mnemonicToEntropy(_mnemonic)
  413. } catch (err) {
  414. /* Set error message. */
  415. error = err.message
  416. }
  417. /* Validate error. */
  418. if (error) {
  419. return error
  420. }
  421. /* Set entropy. */
  422. this._entropy = entropy
  423. /* Create wallet. */
  424. this.createWallet(entropy)
  425. /* Return entropy. */
  426. return this.wallet
  427. },
  428. cleanup() {
  429. console.info('Cleaning up wallet...')
  430. /* Validate balance handler. */
  431. if (balanceHandler) {
  432. /* Stop balance interval. */
  433. clearInterval(balanceHandler)
  434. }
  435. },
  436. destroy() {
  437. /* Reset wallet. */
  438. this._entropy = null
  439. this._wallet = null
  440. console.info('Wallet destroyed successfully!')
  441. },
  442. },
  443. })