@kazeblockchain/kazejs
Version:
Javascript libraries for Kaze wallet
254 lines (237 loc) • 8.52 kB
JavaScript
import AssetBalance from './components/AssetBalance'
import { Transaction } from '../transactions'
import { ASSETS } from '../consts'
import { Fixed8 } from '../utils'
import { Query } from '../rpc'
/**
* @class Balance
* @classdesc object describing the coins found within an Account. Look up various balances through its symbol. For example, Kaze or STREAM.
* @param {object} bal - Balance object as a JSON.
* @param {string} bal.net - 'MainNet' or 'TestNet'
* @param {string} bal.address - The address of the Account
* @param {string[]} bal.assetSymbols - The symbols of the assets available in this Balance
* @param {object} bal.assets - The collection of assets in this Balance
* @param {string[]} bal.tokenSymbols - The symbols of the tokens available in this Balance
* @param {object} bal.tokens - The collection of tokens in this Balance
*/
class Balance {
constructor (bal = {}) {
/** The address for this Balance */
this.address = bal.address || ''
/** The network for this Balance */
this.net = bal.net || 'NoNet'
/** The symbols of assets found in this Balance. Use this symbol to find the corresponding key in the assets object. */
this.assetSymbols = bal.assetSymbols ? bal.assetSymbols : []
/** The object containing the balances for each asset keyed by its symbol. */
this.assets = {}
if (bal.assets) {
Object.keys(bal.assets).map((key) => {
if (typeof bal.assets[key] === 'object') {
this.addAsset(key, bal.assets[key])
}
})
}
/** The symbols of the NEP5 tokens in this Balance. Use this symbol to find the corresponding key in the tokens object. */
this.tokenSymbols = bal.tokenSymbols ? bal.tokenSymbols : []
/** The token balances in this Balance for each token keyed by its symbol. */
this.tokens = bal.tokens ? bal.tokens : {}
}
get [Symbol.toStringTag] () {
return 'Balance'
}
/**
* Imports a string
* @param {string} jsonString
* @return {Balance}
*/
static import (jsonString) {
const balanceJson = JSON.parse(jsonString)
return new Balance(balanceJson)
}
/**
* Adds a new asset to this Balance.
* @param {string} sym - The symbol to refer by. This function will force it to upper-case.
* @param {AssetBalance} [assetBalance] - The assetBalance if initialized. Default is a zero balance object.
* @return this
*/
addAsset (sym, assetBalance = AssetBalance()) {
sym = sym.toUpperCase()
this.assetSymbols.push(sym)
const cleanedAssetBalance = AssetBalance(assetBalance)
this.assets[sym] = cleanedAssetBalance
return this
}
/**
* Adds a new NEP-5 Token to this Balance.
* @param {string} sym - The NEP-5 Token Symbol to refer by.
* @param {number|Fixed8} tokenBalance - The amount of tokens this account holds.
* @return this
*/
addToken (sym, tokenBalance = 0) {
sym = sym.toUpperCase()
this.tokenSymbols.push(sym)
this.tokens[sym] = new Fixed8(tokenBalance)
return this
}
/**
* Applies a Transaction to a Balance, removing spent coins and adding new coins. This currently applies only to Assets.
* @param {Transaction|string} tx - Transaction that has been sent and accepted by Node.
* @param {boolean} confirmed - If confirmed, new coins will be added to unspent. Else, new coins will be added to unconfirmed property first.
* @return {Balance} this
*/
applyTx (tx, confirmed = false) {
tx = tx instanceof Transaction ? tx : Transaction.deserialize(tx)
const symbols = this.assetSymbols
// Spend coins
for (const input of tx.inputs) {
const findFunc = (el) => el.txid === input.prevHash && el.index === input.prevIndex
for (const sym of symbols) {
let assetBalance = this.assets[sym]
let ind = assetBalance.unspent.findIndex(findFunc)
if (ind >= 0) {
let spentCoin = assetBalance.unspent.splice(ind, 1)
assetBalance.spent = assetBalance.spent.concat(spentCoin)
break
}
}
}
// Add new coins
const hash = tx.hash
for (let i = 0; i < tx.outputs.length; i++) {
const output = tx.outputs[i]
const sym = ASSETS[output.assetId]
let assetBalance = this.assets[sym]
if (!assetBalance) this.addAsset(sym)
const coin = { index: i, txid: hash, value: output.value }
if (confirmed) {
let unconfirmedIndex = assetBalance.unconfirmed.findIndex((el) => el.txid === coin.txid && el.index === coin.index)
if (unconfirmedIndex >= 0) {
assetBalance.unconfirmed.splice(unconfirmedIndex, 1)
}
assetBalance.balance = assetBalance.balance.add(output.value)
if (!assetBalance.unspent) assetBalance.unspent = []
assetBalance.unspent.push(coin)
} else {
if (!assetBalance.unconfirmed) assetBalance.unconfirmed = []
assetBalance.unconfirmed.push(coin)
}
this.assets[sym] = assetBalance
}
return this
}
/**
* Informs the Balance that the next block is confirmed, thus moving all unconfirmed transaction to unspent.
* @return {Balance}
*/
confirm () {
for (const sym of this.assetSymbols) {
let assetBalance = this.assets[sym]
assetBalance.unspent = assetBalance.unspent.concat(assetBalance.unconfirmed)
assetBalance.unconfirmed = []
}
return this
}
/**
* Export this class as a plain JS object
* @return {object}
*/
export () {
return {
net: this.net,
address: this.address,
assetSymbols: this.assetSymbols,
assets: exportAssets(this.assets),
tokenSymbols: this.tokenSymbols,
tokens: this.tokens
}
}
/**
* Verifies the coins in balance are unspent. This is an expensive call.
* @param {string} url - KAze Node to check against.
* @return {Promise<Balance>} Returns this
*/
verifyAssets (url) {
const promises = []
const symbols = this.assetSymbols
symbols.map((key) => {
const assetBalance = this.assets[key]
promises.push(verifyAssetBalance(url, assetBalance))
})
return Promise.all(promises)
.then((newBalances) => {
symbols.map((sym, i) => {
this.assets[sym] = newBalances[i]
})
return this
})
}
}
/**
* Verifies an AssetBalance
* @param {string} url
* @param {AssetBalance} assetBalance
* @return {Promise<AssetBalance>} Returns a new AssetBalance
*/
const verifyAssetBalance = (url, assetBalance) => {
let newAssetBalance = { balance: new Fixed8(0), spent: [], unspent: [], unconfirmed: [] }
return verifyCoins(url, assetBalance.unspent)
.then((values) => {
values.map((v, i) => {
let coin = assetBalance.unspent[i]
if (v) {
if (v.value.cmp(coin.value) !== 0) coin.value = v.value
newAssetBalance.unspent.push(coin)
newAssetBalance.balance = newAssetBalance.balance.add(coin.value)
} else {
newAssetBalance.spent.push(coin)
}
})
return newAssetBalance
})
}
/**
* Verifies a list of Coins
* @param {string} url
* @param {Coin[]} coinArr
* @return {Promise<Coin[]>}
*/
const verifyCoins = (url, coinArr) => {
const promises = []
for (const coin of coinArr) {
const promise = Query.getTxOut(coin.txid, coin.index)
.execute(url)
.then(({ result }) => {
if (!result) return null
return {
txid: coin.txid,
index: result.n,
assetId: result.asset,
value: new Fixed8(result.value)
}
})
promises.push(promise)
}
return Promise.all(promises)
}
export default Balance
const exportAssets = (assets) => {
const exported = {}
Object.keys(assets).map(key => {
const assetBalance = assets[key]
const exportedAssetBalance = {
balance: assetBalance.balance.toNumber(),
spent: assetBalance.spent.map(c => exportCoin(c)),
unspent: assetBalance.unspent.map(c => exportCoin(c)),
unconfirmed: assetBalance.unconfirmed.map(c => exportCoin(c))
}
exported[key] = exportedAssetBalance
})
return exported
}
const exportCoin = (coin) => {
return {
index: coin.index,
txid: coin.txid,
value: coin.value.toNumber()
}
}