Shuffler.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790
  1. <template>
  2. <main class="tab-pane">
  3. <div class="row">
  4. <div class="col-xs-12 col-sm-7">
  5. <div class="row">
  6. <div class="form-group col-sm-7">
  7. <select class="form-control ">
  8. <option value="BCH"> Bitcoin Cash (BCH) </option>
  9. <option value="USDT"> Tether (USDt) </option>
  10. </select>
  11. </div>
  12. <div class="form-group col-sm-5">
  13. <input
  14. type="text"
  15. class="form-control"
  16. placeholder="Your wallet balance"
  17. :value="displayBalance"
  18. disabled
  19. />
  20. </div>
  21. </div>
  22. <div
  23. class="coin-rows"
  24. v-for="coin of getCoins"
  25. :key="coin.txid+coin.vout"
  26. >
  27. <!-- <div class="col-sm-12">
  28. <strong>
  29. <small>
  30. <a :href="'https://explorer.bitcoin.com/bch/address/' + coin.cashAddress" target="_blank">
  31. {{coin.cashAddress}}
  32. </a>
  33. </small>
  34. </strong>
  35. </div> -->
  36. <div class="row">
  37. <div class="col-xs-8 text-center">
  38. <a :href="'https://explorer.bitcoin.com/bch/tx/' + coin.txid" target="_blank">
  39. <small>{{coin.txid.slice(0, 10)}} ... {{coin.txid.slice(-10)}}</small>
  40. </a>
  41. </div>
  42. <div class="col-xs-4 text-center">
  43. <small>{{formattedValue(coin)}}</small>
  44. </div>
  45. </div>
  46. <div class="utxo-buttons text-center">
  47. <input
  48. type="button"
  49. class="btn btn-sm btn-primary utxo-button"
  50. value="details"
  51. @click="openExplorer(coin.details)"
  52. />
  53. <input
  54. type="button"
  55. class="btn btn-sm btn-success utxo-button"
  56. value="shuffle"
  57. @click="startShuffle(coin.details)"
  58. />
  59. <input
  60. type="button"
  61. class="btn btn-sm btn-danger utxo-button"
  62. value="fusion"
  63. @click="startFusion(coin.details)"
  64. />
  65. <input
  66. type="button"
  67. class="btn btn-sm btn-primary utxo-button"
  68. value="send"
  69. @click="send(coin.details)"
  70. />
  71. </div>
  72. </div>
  73. <input
  74. v-if="coinsTable.length"
  75. type="text"
  76. class="form-control mt-2"
  77. placeholder="Enter a destination address"
  78. v-model="output.address"
  79. />
  80. </div> <!-- end of left column -->
  81. <div class="address-win col-xs-12 col-sm-5 text-center">
  82. <div class="qr-code text-center" v-html="qr" />
  83. <h5 class="text-center text-info">{{displayAddress}}</h5>
  84. <div v-if="showMnemonic" class="mnemonic" @click="toggleMnemonic">
  85. {{getMnemonic}}
  86. </div>
  87. <div class="flex address-buttons">
  88. <input
  89. type="button"
  90. class="btn btn-error btn-sm address-button"
  91. :value="showMnemonic ? 'hide mnemonic' : 'show mnemonic'"
  92. @click="toggleMnemonic"
  93. />
  94. <input
  95. type="button"
  96. class="btn btn-warning btn-sm address-button"
  97. value="re-sync coins"
  98. @click="updateCoins"
  99. />
  100. </div>
  101. </div> <!-- end of right column -->
  102. </div>
  103. </main>
  104. </template>
  105. <script>
  106. /* Import modules. */
  107. // import Nexa from 'nexajs'
  108. let Nexa
  109. import moment from 'moment'
  110. import numeral from 'numeral'
  111. import QRCode from 'qrcode'
  112. import Swal from 'sweetalert2'
  113. import { v4 as uuidv4 } from 'uuid'
  114. export default {
  115. data: () => {
  116. return {
  117. usd: null,
  118. balance: null,
  119. wallet: null,
  120. output: {
  121. address: null,
  122. satoshis: null,
  123. notes: null,
  124. },
  125. showMnemonic: null,
  126. roomName: null,
  127. }
  128. },
  129. computed: {
  130. // ...mapGetters('system', [
  131. // 'getRequests',
  132. // ]),
  133. // ...mapGetters('utils', [
  134. // 'getFormattedValue',
  135. // ]),
  136. // ...mapGetters('wallet', [
  137. // 'getAddress',
  138. // 'getBalance',
  139. // 'getCoins',
  140. // 'getMasterSeed',
  141. // 'getMnemonic',
  142. // ]),
  143. displayAddress() {
  144. const address = this.getAddress('deposit')
  145. return address.slice(12, 20) + ' ... ' + address.slice(-12)
  146. },
  147. /**
  148. * Display Balance
  149. */
  150. displayBalance() {
  151. if (!this.balance) {
  152. return 'loading...'
  153. }
  154. /* Set display. */
  155. const display = `${this.balance.value} ${this.balance.unit} | ${this.balance.fiat}`
  156. /* Return display. */
  157. return display
  158. },
  159. /**
  160. * Coins Table
  161. */
  162. coinsTable() {
  163. /* Set table data. */
  164. const tableData = []
  165. /* Validate coins. */
  166. if (this.getCoins) {
  167. /* Initialize coins. */
  168. const coins = this.getCoins
  169. // console.log('COINS TABLE (coins):', coins)
  170. Object.keys(coins).forEach(async coinId => {
  171. /* Initialize coin. */
  172. const coin = coins[coinId]
  173. // console.log('COINS (coin):', coin)
  174. /* Set id. */
  175. const id = `${coin.txid}:${coin.vout}`
  176. /* Set label. */
  177. const label = `${coin.txid.slice(0, 8)} ... ${coin.txid.slice(-8)} : ${coin.vout}`
  178. /* Initialize flags. */
  179. const flags = {}
  180. /* Set status. */
  181. // TODO: Will probably develop a rating scale??
  182. let status = null
  183. switch(coin.status) {
  184. case 'active':
  185. // status = '<icon class="fa fa-check"></icon>'
  186. status = coin.status
  187. flags.spendable = true
  188. break
  189. case 'locked':
  190. // status = '<icon class="fa fa-lock text-danger"></icon>'
  191. status = coin.status
  192. flags.locked = true
  193. break
  194. default:
  195. // status = ''
  196. status = ''
  197. }
  198. /* Set satoshis. */
  199. const satoshis = coin.satoshis
  200. /* Build coin data. */
  201. const coinData = {
  202. id,
  203. label,
  204. flags,
  205. status,
  206. satoshis,
  207. details: coin, // FIXME: Write this to outbox for multi-coin sending.
  208. }
  209. // TODO: Allow display of spent coins.
  210. if (status !== '') {
  211. tableData.push(coinData)
  212. }
  213. })
  214. }
  215. // console.log('TABLE DATA:', tableData)
  216. return tableData
  217. },
  218. qr() {
  219. if (!this.getAddress('deposit')) {
  220. return null
  221. }
  222. /* Initialize (string) value. */
  223. let strValue = ''
  224. /* Initialize scanner parameters. */
  225. const params = {
  226. type: 'svg',
  227. width: 250,
  228. height: 250,
  229. color: {
  230. dark: '#000',
  231. light: '#fff'
  232. }
  233. }
  234. QRCode.toString(this.getAddress('deposit'), params, (err, value) => {
  235. if (err) {
  236. return console.error('QR Code ERROR:', err)
  237. }
  238. /* Set (string) value. */
  239. strValue = value
  240. })
  241. /* Return (string) value. */
  242. return strValue
  243. },
  244. /**
  245. * Magic Hash
  246. */
  247. // magicHash () {
  248. // /* Initialize first prefix. */
  249. // const prefix1 = BufferWriter.varintBufNum(MAGIC_BYTES.length)
  250. //
  251. // /* Set buffer message. */
  252. // const messageBuffer = Buffer.from(this.message, this.messageEncoding)
  253. //
  254. // /* Initialize second prefix. */
  255. // const prefix2 = BufferWriter.varintBufNum(messageBuffer.length)
  256. //
  257. // /* Set (complete) buffer. */
  258. // const buf = Buffer
  259. // .concat([prefix1, MAGIC_BYTES, prefix2, messageBuffer])
  260. //
  261. // /* Set buffer hash. */
  262. // const hash = sha256sha256(buf)
  263. //
  264. // /* Return hash. */
  265. // return hash
  266. // }
  267. },
  268. methods: {
  269. // ...mapActions('system', [
  270. // 'updateRequests',
  271. // ]),
  272. // ...mapActions('utils', [
  273. // 'toast',
  274. // ]),
  275. // ...mapActions('wallet', [
  276. // 'updateCoins',
  277. // ]),
  278. /**
  279. * Coin Value Display
  280. */
  281. coinValueDisplay(_coin) {
  282. return numeral(_coin.satoshis).format('0,0')
  283. },
  284. /**
  285. * Coin (USD) Value Display
  286. */
  287. coinUsdValueDisplay(_coin) {
  288. if (this.usd) {
  289. // console.log('CALC', (_coin.satoshis / 100000000.0) * this.usd)
  290. return numeral((_coin.satoshis / 100000000.0) * this.usd).format('$0,0.00')
  291. } else {
  292. return '$0.00'
  293. }
  294. },
  295. formattedValue(_coin) {
  296. return numeral(_coin.satoshis / 100).format('0,0[.]00') + ' bits'
  297. },
  298. /**
  299. * Send Message
  300. *
  301. * Broadcast the connection information for this IPFS node.
  302. */
  303. async sendMessage(_message) {
  304. try {
  305. /* Set message buffer. */
  306. const msgBuf = Buffer.from(JSON.stringify(_message))
  307. // Publish the message to the pubsub channel.
  308. await ipfs.pubsub.publish(this.roomName, msgBuf)
  309. console.log(`Published message to ${this.roomName}\n`)
  310. } catch (err) {
  311. console.error('Error in sendMessage()')
  312. throw err
  313. }
  314. },
  315. async send(_coin) {
  316. // console.log('SENDING COIN', _coin)
  317. if (!this.output.address) {
  318. return this.toast(['Oops!', 'Invalid destination address, please try again', 'error'])
  319. }
  320. /* Build receivers. */
  321. const receivers = [
  322. {
  323. address: this.output.address,
  324. satoshis: _coin.satoshis,
  325. }
  326. ]
  327. /* Set auto fee (flag). */
  328. const autoFee = true
  329. const results = await Nexa.Transaction
  330. .sendCoin(_coin, receivers, autoFee)
  331. .catch(err => {
  332. console.error(err) // eslint-disable-line no-console
  333. /* Report error. */
  334. this.report(err)
  335. })
  336. // console.log('OUTBOX SEND COIN (results):', results)
  337. if (results) {
  338. /* Update outbox. */
  339. // this.updateOutbox(null)
  340. /* Clear output address. */
  341. this.output.address = null
  342. /* Set message. */
  343. const message = `Your coins have been sent successfully!`
  344. /* Display notification. */
  345. this.toast(['Done!', message, 'success'])
  346. /* Wait a bit then update coins. */
  347. // FIXME: How long should we wait?
  348. // Probably better to update coins w/out on-chain query?
  349. setTimeout(() => {
  350. /* Update coins. */
  351. // FIXME: Why is this blocking the entire initial UI setup??
  352. this.updateCoins()
  353. }, 2000)
  354. } else {
  355. /* Set message. */
  356. const message = `Something went wrong and your coin(s) were NOT sent`
  357. /* Display notification. */
  358. this.toast(['Oops!', message, 'error'])
  359. }
  360. },
  361. /**
  362. * Toggle Mnemonic
  363. */
  364. toggleMnemonic() {
  365. this.showMnemonic = !this.showMnemonic
  366. },
  367. /**
  368. * Set Clipboard
  369. */
  370. copyAddress() {
  371. /* Set clipboard. */
  372. this.setClipboard(this.getAddress('deposit'))
  373. /* Set message. */
  374. const message = `Deposit address copied to your clipboard.`
  375. /* Display notification. */
  376. this.toast(['Done!', message, 'info'])
  377. },
  378. previewNotice() {
  379. Swal.fire({
  380. title: 'Campaign Preview',
  381. text: 'Thanks for checking out this early look of Hush Your Money. Our team has been working around the clock to deliver this portal to you asap. Please consider supporting our development work by donating to our Flipstarter.',
  382. icon: 'warning',
  383. confirmButtonColor: '#3085d6',
  384. confirmButtonText: 'Open Campaign',
  385. showCancelButton: true,
  386. cancelButtonColor: '#d33',
  387. cancelButtonText: 'Close',
  388. }).then((result) => {
  389. if (result.value) {
  390. window.open('https://hushyourmoney.com')
  391. } else if (result.isDismissed) {
  392. // if (result.dismiss === 'cancel') { // backdrop | cancel | esc
  393. Swal.fire({
  394. title: 'Thanks for visiting!',
  395. text: `Please check back soon for updates!`,
  396. icon: 'info',
  397. showConfirmButton: false,
  398. timer: 5000,
  399. timerProgressBar: true,
  400. })
  401. // }
  402. }
  403. })
  404. },
  405. openExplorer(_details) {
  406. // window.open(`https://explorer.bitcoin.com/bch/tx/${_details.txid}`)
  407. if (this.wallet) {
  408. console.log('WALLET', this.wallet)
  409. const debug = this.wallet.debug()
  410. console.log('WALLET DEBUG', debug, _details)
  411. } else {
  412. console.log('WALLET IS NOT INITITALIZED')
  413. }
  414. },
  415. startFusion() {
  416. // this.previewNotice()
  417. if (this.wallet) {
  418. const create = this.wallet.create(this.getMasterSeed)
  419. console.log('WALLET CREATE', create)
  420. } else {
  421. console.log('WALLET IS NOT INITITALIZED')
  422. }
  423. },
  424. /**
  425. * Start Shuffle
  426. */
  427. async startShuffle(_coin) {
  428. /* Set coin signature. */
  429. // NOTE: This is a signature of the `requestid` by the private key
  430. // of the player's submitted UTXO.
  431. const verificationKey = _coin
  432. /* Set outpoint. */
  433. // NOTE: UTXO + tx position.
  434. const outpoint = 'outpoint'
  435. /* Set destination. */
  436. const destination = 'destination'
  437. /* Set signature. */
  438. // NOTE: Used to verify coin owner.
  439. // const sig = 'sig'
  440. /* Set players. */
  441. const players = []
  442. /* Set ourselves. */
  443. const me = {
  444. verificationKey,
  445. outpoint,
  446. destination,
  447. // sig,
  448. isValid: null, // NOTE: This is just a placeholder and must be verified by each player
  449. createdAt: moment().valueOf(),
  450. updatedAt: moment().valueOf(),
  451. }
  452. /**
  453. I AM the last player
  454. 1. take all the encryption signatures from the other players
  455. 2. wrap our desitnation address in an onion (p1 .. p?) of `isValid` outpoints
  456. 3. send that package to (p?)
  457. that player will then unwrap our layer, make a copy and replace our destination with theirs
  458. (p1) will then unwrap (p2) layer and add their destination to the bunch
  459. I'm NOT the last player
  460. 1. wait until i see the player after me add their sigs.
  461. */
  462. /* Add ourselves to the players. */
  463. // FIXME: Sort by verification key.
  464. players.push(me)
  465. /* Set request id. */
  466. const requestid = uuidv4()
  467. /* Initialize transaction manager. */
  468. const txManager = {
  469. covertAddrs: {
  470. players: [],
  471. nextPlayer: null,
  472. },
  473. changeAddrs: [], // NOTE: These addresses are "toxic" and NOT safe for re-use.
  474. sigs: [], // NOTE: Signatures for each of the "source" UTXOs.
  475. }
  476. /* Calculate players hash. */
  477. // FIXME: We need to concatenate all player's `verificationKey`, then sha256.
  478. const playersHash = 'sha256-hash-goes-here'
  479. /* Build (request) package. */
  480. const pkg = {
  481. requestid,
  482. txManager,
  483. players,
  484. playersHash,
  485. createdAt: moment().valueOf(),
  486. updatedAt: null,
  487. completedAt: null,
  488. }
  489. console.log('REQUEST PACKAGE', pkg)
  490. // FIXME: FOR DEV ONLY
  491. this.updateRequests(pkg)
  492. this.sendMessage(pkg)
  493. // const message = 'START THE SHUFFLE!'
  494. // /* Set message buffer. */
  495. // const msgBuf = Buffer.from(JSON.stringify(message))
  496. //
  497. // try {
  498. // // Publish the message to the pubsub channel.
  499. // await this.getIpfs.pubsub.publish(this.roomName, msgBuf)
  500. //
  501. // console.log(`Published message to ${this.roomName}\n`)
  502. // } catch (err) {
  503. // console.error('Error in startShuffle()')
  504. // throw err
  505. // }
  506. },
  507. /**
  508. * Stop Shuffle
  509. */
  510. stopShuffle(_coin) {
  511. console.log('STOPPING SHUFFLE FOR:', _coin)
  512. },
  513. /**
  514. * Sign
  515. *
  516. * Will sign a message with a given bitcoin private key.
  517. */
  518. // sign(privateKey) {
  519. // $.checkArgument(privateKey instanceof PrivateKey,
  520. // 'First argument should be an instance of PrivateKey')
  521. //
  522. // /* Initialize hash. */
  523. // const hash = this.magicHash
  524. //
  525. // /* Initialize ECDSA. */
  526. // const ecdsa = new ECDSA()
  527. //
  528. // /* Set hash buffer. */
  529. // ecdsa.hashbuf = hash
  530. //
  531. // /* Set private key. */
  532. // ecdsa.privkey = privateKey
  533. //
  534. // /* Set public key. */
  535. // ecdsa.pubkey = privateKey.toPublicKey()
  536. //
  537. // /* Sign. */
  538. // ecdsa.signRandomK()
  539. //
  540. // /* Calculate. */
  541. // ecdsa.calci()
  542. //
  543. // /* Return signature. */
  544. // return ecdsa.sig.toCompact().toString('base64')
  545. // }
  546. /**
  547. * Verify
  548. *
  549. * Will return a boolean of the signature is valid for a given
  550. * bitcoin address. If it isn't the specific reason is accessible via
  551. * the "error" member.
  552. */
  553. // verify(bitcoinAddress, signatureString) {
  554. // $.checkArgument(bitcoinAddress)
  555. //
  556. // $.checkArgument(signatureString && _.isString(signatureString))
  557. //
  558. // if (_.isString(bitcoinAddress)) {
  559. // bitcoinAddress = Address.fromString(bitcoinAddress)
  560. // }
  561. //
  562. // /* Set signature. */
  563. // const signature = Signature
  564. // .fromCompact(Buffer.from(signatureString, 'base64'))
  565. //
  566. // /* Initialize ECDSA. */
  567. // const ecdsa = new ECDSA()
  568. //
  569. // /* Set hash buffer. */
  570. // ecdsa.hashbuf = this.magicHash
  571. //
  572. // /* Set signature. */
  573. // ecdsa.sig = signature
  574. //
  575. // /* Set public key. */
  576. // const publicKey = ecdsa.toPublicKey()
  577. //
  578. // /* Set signature address. */
  579. // const signatureAddress = Address
  580. // .fromPublicKey(publicKey, bitcoinAddress.network)
  581. //
  582. // /* Validate addresses. */
  583. // if (bitcoinAddress.toString() !== signatureAddress.toString()) {
  584. // this.error = 'The signature did not match the message digest'
  585. //
  586. // return false
  587. // }
  588. //
  589. // /* Set verification. */
  590. // const verified = ECDSA.verify(this.magicHash, signature, publicKey)
  591. //
  592. // /* Validate verification. */
  593. // if (!verified) {
  594. // this.error = 'The signature was invalid'
  595. // }
  596. //
  597. // /* Return verification. */
  598. // return verified
  599. // }
  600. },
  601. created: async function () {
  602. // console.log('BALANCE DISPLAY', this.displayBalance)
  603. // console.log('DISPLAY ADDRESS', this.displayAddress)
  604. /* Initialize mnemonic flag. */
  605. this.showMnemonic = false
  606. // Pubsub channel that nodes will use to coordinate.
  607. this.roomName = 'af84de592984f9403c9539c1049a01369e6302f08043b79db783bd34ad344190' // #lobby:nitoblender.com
  608. /* Request BCH/USD market price. */
  609. this.usd = await Nexa.Markets.getTicker('BCH', 'USD')
  610. console.log('USD', this.usd)
  611. this.wallet = new Nexa.Wallet()
  612. console.log('WALLET', this.wallet)
  613. },
  614. mounted: async function () {
  615. // const balance = this.getBalance
  616. const balance = await this
  617. .getBalance('USD')
  618. .catch(err => {
  619. console.error(err) // eslint-disable-line no-console
  620. })
  621. console.log('SHUFFLER GET BALANCE', balance)
  622. const coins = this.getCoins
  623. console.log('SHUFFLER COINS', coins)
  624. /* Set balance. */
  625. this.balance = balance
  626. /* Retrieve requests. */
  627. const requests = this.getRequests
  628. console.log('REQUESTS', requests)
  629. },
  630. }
  631. </script>
  632. <style>
  633. .utxo-buttons {
  634. margin-top: 5px;
  635. }
  636. .utxo-button {
  637. margin: 0 5px;
  638. }
  639. .coin-rows {
  640. margin: 0 5px;
  641. padding: 10px;
  642. border-bottom: 1pt solid rgba(180, 180, 180, 0.5);
  643. }
  644. .my-available-coins small {
  645. font-size: 0.7em;
  646. font-weight: 500;
  647. }
  648. </style>
  649. <style scoped>
  650. .address-win h5 {
  651. margin-top: -20px;
  652. }
  653. .address-buttons {
  654. justify-content: space-around;
  655. }
  656. .mnemonic {
  657. cursor: pointer;
  658. padding: 20px;
  659. color: rgba(30, 30, 30, 0.8);
  660. }
  661. .actions a {
  662. display: inline-block;
  663. }
  664. </style>