123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539 |
- /* Import modules. */
- import { defineStore } from 'pinia'
- import {
- decodeAddress,
- encodeAddress,
- } from '@nexajs/address'
- import {
- ripemd160,
- sha256,
- } from '@nexajs/crypto'
- import { mnemonicToEntropy } from '@nexajs/hdnode'
- import { broadcast } from '@nexajs/provider'
- import {
- buildCoins,
- getCoins,
- sendCoins,
- } from '@nexajs/purse'
- import { getAddressUnspent } from '@nexajs/rostrum'
- import {
- encodeDataPush,
- OP,
- } from '@nexajs/script'
- import {
- binToHex,
- hexToBin,
- utf8ToBin,
- } from '@nexajs/utils'
- import {
- Wallet,
- WalletStatus,
- } from '@nexajs/wallet'
- import { useSystemStore } from '@/stores/system'
- import _setEntropy from './wallet/setEntropy.ts'
- /* Initialize constants. */
- const CASHOUT_FEE = BigInt(1000) // FIXME We MUST calculate this (same as change).
- const TIMETOCASHOUT_HEX = '6c6c6c6c5479009c63557a7cad6d6d67547a519d537ab275030051147b7e00cd8800cc55ea537a94a2697568'
- const UPDATE_BALANCE_INTERVAL = 5000
- /* Initialize globals. */
- let balanceHandler
- /**
- * Wallet Store
- */
- export const useWalletStore = defineStore('wallet', {
- state: () => ({
- /**
- * Assets
- *
- * Will hold ALL assets that the wallet manages.
- */
- _assets: null,
- /**
- * Cashout Address
- *
- * Use to automatically cashout a wallet (after a specified timeout).
- */
- _cashoutAddr: null,
- /**
- * Coins
- *
- * Manage unspent coins.
- */
- _coins: null,
- /**
- * Entropy
- * (DEPRECATED -- MUST REMAIN SUPPORTED INDEFINITELY)
- *
- * Initialize entropy (used for HD wallet).
- *
- * NOTE: This is a cryptographically-secure "random"
- * 32-byte (256-bit) value.
- */
- _entropy: null,
- /**
- * Wallet
- *
- * Currently active wallet object.
- */
- _wallet: null,
- }),
- getters: {
- /* Return (abbreviated) wallet status. */
- // abbr(_state) {
- // if (!_state._wallet) {
- // return null
- // }
- // return _state._wallet.abbr
- // },
- abbr(_state) {
- if (!this.address) {
- return null
- }
- return this.address.slice(5, 17) + '...' + this.address.slice(-12)
- },
- /* Return wallet status. */
- // address(_state) {
- // if (!_state._wallet) {
- // return null
- // }
- // return _state._wallet.address
- // },
- address(_state) {
- /* Initialize locals. */
- let contractAddress
- let prefix
- /* Set prefix. */
- prefix = 'nexa'
- /* Encode the public key hash into a P2PKH nexa address. */
- contractAddress = encodeAddress(
- prefix,
- 'TEMPLATE',
- this.scriptPubkey,
- )
- // console.info('(CONTRACT) ADDRESS', contractAddress)
- return contractAddress
- },
- // asset(_state) {
- // if (!this.assets || !this.wallet) {
- // return null
- // }
- // return this.assets[this.wallet.assetid]
- // },
- asset(_state) {
- if (!this.assets || !this.wallet) {
- return null
- }
- /* Initialize locals. */
- let amount
- let decimal_places
- let fiat
- let satoshis
- let USD
- /* Set decimal places. */
- decimal_places = 2
- /* Calculate satoshis. */
- satoshis = this._assets.reduce(
- (totalSats, coin) => (totalSats + BigInt(coin.value)), BigInt(0)
- )
- /* Calculate amount. */
- amount = satoshis
- /* Initialize System. */
- const System = useSystemStore()
- /* Initialize USD value. */
- USD = 0
- /* Validate system ticker. */
- if (System.ticker) {
- /* Set quote. */
- const quote = System.ticker.quote
- console.log('QUOTE', quote)
- /* Validate quote. */
- if (quote) {
- /* Set (USD) price. */
- USD = (quote?.USD?.price * parseFloat(Number(satoshis) / 100))
- }
- }
- /* Set fiat. */
- fiat = { USD }
- /* Build asset. */
- const asset = {
- amount,
- satoshis,
- decimal_places,
- fiat,
- }
- /* Return asset. */
- return asset
- },
- assets(_state) {
- if (_state._assets) {
- return _state._assets
- }
- if (!_state._wallet) {
- return null
- }
- return _state._wallet.assets
- },
- /* Return cashout address. */
- cashoutAddr(_state) {
- if (!_state._cashoutAddr) {
- return null
- }
- return _state._cashoutAddr
- },
- /* Return unspent coins. */
- coins(_state) {
- if (!_state._coins) {
- return []
- }
- return _state._coins
- },
- /* Return wallet status. */
- isLoading(_state) {
- if (!_state._wallet) {
- return true
- }
- return _state._wallet.isLoading
- },
- /* Return wallet status. */
- isReady(_state) {
- if (!_state._wallet) {
- return false
- }
- if (_state._wallet._entropy) {
- return true
- }
- return _state._wallet.isReady
- },
- /* Return public key. */
- publicKey(_state) {
- if (!_state._wallet?.publicKey) {
- return null
- }
- return _state._wallet?.publicKey
- },
- scriptPubkey(_state) {
- /* Initialize locals. */
- let constraintData
- let constraintHash
- let decoded
- let gratitude
- let lockingScript
- let prefix
- let publicKey
- let recoveryPkh
- let scriptHash
- let scriptPubkey
- let timeout
- /* Set prefix. */
- prefix = 'nexa'
- /* Set locking script. */
- lockingScript = hexToBin(TIMETOCASHOUT_HEX)
- // console.info('CONTRACT TEMPLATE', binToHex(lockingScript))
- scriptHash = ripemd160(sha256(lockingScript))
- // console.log('TEMPLATE HASH', binToHex(scriptHash))
- /* Set public key. */
- publicKey = this.publicKey
- // console.log('PUBLIC KEY', publicKey)
- /* Hash the public key hash according to the P2PKH/P2PKT scheme. */
- constraintData = encodeDataPush(publicKey)
- /* Set the constraint hash. */
- constraintHash = ripemd160(sha256(constraintData))
- /* Decode (cashout) address. */
- decoded = decodeAddress(this._cashoutAddr)
- // console.log('DECODED (cashout addr)', decoded)
- recoveryPkh = decoded.hash.slice(3) // NOTE: Strip out public key hash.
- // console.log('RECOVERY PKH', binToHex(recoveryPkh))
- gratitude = hexToBin('2710') // 10,000 (100.00 NEXA)
- // gratitude = hexToBin('03e8') // 1,000 (10.00 NEXA)
- gratitude.reverse()
- gratitude = encodeDataPush(gratitude)
- // timeout = hexToBin('4013c7') // (5,063) approx 30 days
- // timeout = hexToBin('4000a9') // (169) approx 24 hours
- // timeout = hexToBin('400007') // (7) approx 1 hour
- timeout = hexToBin('400001') // (1) approx 8 1/2 minutes
- timeout.reverse()
- timeout = encodeDataPush(timeout)
- /* Build script public key. */
- scriptPubkey = new Uint8Array([
- OP.ZERO, // groupid or empty stack item
- ...encodeDataPush(scriptHash), // script hash
- ...encodeDataPush(constraintHash), // arguments hash
- ...encodeDataPush(recoveryPkh), // A recovery address (specified by the Owner) used to cashout the balance, after a timeout period.
- ...gratitude, // The rate of exchange, charged by the Provider. (measured in <satoshis> per <asset>)
- ...timeout, // The rate of exchange, charged by the Provider. (measured in <satoshis> per <asset>)
- ])
- // console.info('\nSCRIPT PUBLIC KEY', binToHex(scriptPubkey))
- /* Return script public key. */
- return scriptPubkey
- },
- /* Return NEXA.js wallet instance. */
- wallet(_state) {
- return _state._wallet
- },
- WalletStatus() {
- return WalletStatus
- },
- wif(_state) {
- return _state._wallet.wif
- }
- },
- actions: {
- /**
- * Initialize
- *
- * Setup the wallet store.
- * 1. Retrieve the saved entropy.
- * 2. Initialize a Wallet instance.
- * 3. Load assets.
- */
- async init() {
- console.info('Initializing wallet...')
- if (this._entropy === null) {
- this._wallet = 'NEW' // FIXME TEMP NEW WALLET FLAG
- // throw new Error('Missing wallet entropy.')
- return console.error('Missing wallet entropy.')
- }
- /* Request a wallet instance (by mnemonic). */
- this._wallet = await Wallet.init(this._entropy, true)
- console.info('(Initialized) wallet', this.wallet)
- /* Update balance. */
- balanceHandler = setInterval(this.updateBalance, UPDATE_BALANCE_INTERVAL)
- this.updateBalance()
- },
- /**
- * Create Wallet
- *
- * Create a fresh wallet.
- *
- * @param _entropy A 32-byte (hex-encoded) random value.
- */
- createWallet(_entropy) {
- /* Validate entropy. */
- // NOTE: Expect HEX value to be 32 or 64 characters.
- if (_entropy?.length !== 32 && _entropy?.length !== 64) {
- console.error(_entropy, 'is NOT valid entropy.')
- /* Clear (invalid) entropy. */
- _entropy = null
- }
- /* Set entropy. */
- _setEntropy.bind(this)(_entropy)
- /* Initialize wallet. */
- this.init()
- },
- async cashout(_receiver) {
- /* Calculate remaining satoshis. */
- // FIXME This needs to be ZERO balance.
- const satoshis = (this.asset.satoshis - CASHOUT_FEE)
- /* Transfer assets to receiver. */
- this.transfer(_receiver, satoshis)
- },
- async transfer(_receiver, _satoshis) {
- // console.log('ASSET', this.asset)
- // console.log('ASSETS', this.assets)
- // console.log('COINS-1', this.coins)
- // console.log('PUBLIC KEY', binToHex(this._wallet.publicKey))
- /* Initialize locals. */
- let coins
- let lockingScript
- let receivers
- let response
- let scriptPubkey
- let unlockingScript
- /* Set script public key. */
- scriptPubkey = encodeDataPush(this.publicKey)
- /* Set locking script. */
- lockingScript = hexToBin(TIMETOCASHOUT_HEX)
- // console.info('\nCONTRACT TEMPLATE', binToHex(lockingScript))
- /* Set unlocking script. */
- // NOTE: Index of (executed) contract method.
- unlockingScript = new Uint8Array([
- ...encodeDataPush(scriptPubkey),
- ...utf8ToBin('{{SIGNATURE}}'), // placeholder for signature
- OP.ZERO, // contract function index
- ])
- /* Handle coin locks. */
- coins = this.coins.map(_coin => {
- return {
- ..._coin,
- locking: lockingScript,
- unlocking: unlockingScript,
- }
- })
- // console.log('COINS-2', coins)
- /* Initialize receivers. */
- receivers = []
- /* Add value output. */
- receivers.push({
- address: _receiver,
- satoshis: _satoshis,
- })
- /* Add change output. */
- receivers.push({
- address: _receiver,
- })
- // console.log('RECEIVERS', receivers)
- /* Send UTXO request. */
- response = await buildCoins(coins, receivers)
- .catch(err => console.error(err))
- // console.log('BUILD TRANSACTION', response.raw)
- response = await broadcast(response.raw)
- .catch(err => console.error(err))
- console.log('SEND TRANSACTION', response)
- },
- async updateBalance() {
- /* Initialize locals. */
- let assets
- let unspent
- /* Request (unspent) coins. */
- this._coins = await getCoins(this.wif, this.scriptPubkey)
- .catch(err => console.error(err))
- /* Request (unspent) coins. */
- unspent = await getAddressUnspent(this.address)
- .catch(err => console.error(err))
- // console.log('UNSPENT', unspent)
- /* Filter coins ONLY. */
- assets = unspent.filter(_unspent => {
- return _unspent.has_token === false
- })
- /* Set assets. */
- this._assets = assets
- },
- setEntropy(_entropy) {
- this._entropy = _entropy
- },
- setCashoutAddr(_cashoutAddr) {
- this._cashoutAddr = _cashoutAddr
- },
- setMnemonic(_mnemonic) {
- let entropy
- let error
- try {
- /* Derive entropy. */
- entropy = mnemonicToEntropy(_mnemonic)
- } catch (err) {
- /* Set error message. */
- error = err.message
- }
- /* Validate error. */
- if (error) {
- return error
- }
- /* Set entropy. */
- this._entropy = entropy
- /* Create wallet. */
- this.createWallet(entropy)
- /* Return entropy. */
- return this.wallet
- },
- cleanup() {
- console.info('Cleaning up wallet...')
- /* Validate balance handler. */
- if (balanceHandler) {
- /* Stop balance interval. */
- clearInterval(balanceHandler)
- }
- },
- destroy() {
- /* Reset wallet. */
- this._entropy = null
- this._wallet = null
- console.info('Wallet destroyed successfully!')
- },
- },
- })
|