/* 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 per ) ...timeout, // The rate of exchange, charged by the Provider. (measured in per ) ]) // 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!') }, }, })