diff --git a/package.json b/package.json index 3c6c21ae7..7c9bc7eec 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "@binance-chain/javascript-sdk": "^2.14.4", "@ethereumjs/common": "^2.4.0", "@ethereumjs/tx": "^3.3.0", - "@fioprotocol/fiosdk": "^1.4.0", + "@fioprotocol/fiosdk": "^1.5.0", "@hashgraph/sdk": "^1.1.9", "@walletconnect/client": "1.6.6", "biggystring": "^3.0.0", @@ -115,7 +115,7 @@ "babel-eslint": "^10.1.0", "babel-loader": "^8.1.0", "chai": "^4.2.0", - "edge-core-js": "^0.19.1", + "edge-core-js": "^0.19.4", "eslint": "^7.14.0", "eslint-config-standard-kit": "0.15.1", "eslint-plugin-flowtype": "^5.2.0", diff --git a/src/fio/fioConst.js b/src/fio/fioConst.js index 169bd21fa..be1449928 100644 --- a/src/fio/fioConst.js +++ b/src/fio/fioConst.js @@ -10,41 +10,115 @@ export const HISTORY_NODE_ACTIONS = { } export const HISTORY_NODE_OFFSET = 20 -export const BROADCAST_ACTIONS = { - recordObtData: true, - requestFunds: true, - registerFioAddress: true, - registerFioDomain: true, - renewFioDomain: true, - transferTokens: true, - addPublicAddresses: true, - transferFioAddress: true, - transferFioDomain: true, - addBundledTransactions: true -} - -export const ACTIONS_TO_END_POINT_KEYS = { - requestFunds: 'newFundsRequest', - registerFioAddress: 'registerFioAddress', - registerFioDomain: 'registerFioDomain', - renewFioDomain: 'renewFioDomain', - addPublicAddresses: 'addPubAddress', +export const ACTIONS = { + transferTokens: 'transferTokens', + addPublicAddress: 'addPublicAddress', + addPublicAddresses: 'addPublicAddresses', setFioDomainPublic: 'setFioDomainPublic', rejectFundsRequest: 'rejectFundsRequest', + requestFunds: 'requestFunds', recordObtData: 'recordObtData', - transferTokens: 'transferTokens', - pushTransaction: 'pushTransaction', + registerFioAddress: 'registerFioAddress', + registerFioDomain: 'registerFioDomain', + renewFioDomain: 'renewFioDomain', transferFioAddress: 'transferFioAddress', transferFioDomain: 'transferFioDomain', + pushTransaction: 'pushTransaction', + addBundledTransactions: 'addBundledTransactions', + stakeFioTokens: 'stakeFioTokens', + unStakeFioTokens: 'unStakeFioTokens' +} + +export const BROADCAST_ACTIONS = { + [ACTIONS.recordObtData]: true, + [ACTIONS.requestFunds]: true, + [ACTIONS.registerFioAddress]: true, + [ACTIONS.registerFioDomain]: true, + [ACTIONS.renewFioDomain]: true, + [ACTIONS.transferTokens]: true, + [ACTIONS.addPublicAddresses]: true, + [ACTIONS.transferFioAddress]: true, + [ACTIONS.transferFioDomain]: true, + [ACTIONS.addBundledTransactions]: true, + [ACTIONS.stakeFioTokens]: true, + [ACTIONS.unStakeFioTokens]: true +} + +export const ACTIONS_TO_END_POINT_KEYS = { + [ACTIONS.requestFunds]: 'newFundsRequest', + [ACTIONS.registerFioAddress]: 'registerFioAddress', + [ACTIONS.registerFioDomain]: 'registerFioDomain', + [ACTIONS.renewFioDomain]: 'renewFioDomain', + [ACTIONS.addPublicAddresses]: 'addPubAddress', + [ACTIONS.setFioDomainPublic]: 'setFioDomainPublic', + [ACTIONS.rejectFundsRequest]: 'rejectFundsRequest', + [ACTIONS.recordObtData]: 'recordObtData', + [ACTIONS.transferTokens]: 'transferTokens', + [ACTIONS.pushTransaction]: 'pushTransaction', + [ACTIONS.transferFioAddress]: 'transferFioAddress', + [ACTIONS.transferFioDomain]: 'transferFioDomain', + [ACTIONS.stakeFioTokens]: 'pushTransaction', + [ACTIONS.unStakeFioTokens]: 'pushTransaction', addBundledTransactions: 'addBundledTransactions' } +export const ACTIONS_TO_FEE_END_POINT_KEYS = { + [ACTIONS.requestFunds]: 'newFundsRequest', + [ACTIONS.registerFioAddress]: 'registerFioAddress', + [ACTIONS.registerFioDomain]: 'registerFioDomain', + [ACTIONS.renewFioDomain]: 'renewFioDomain', + [ACTIONS.addPublicAddresses]: 'addPubAddress', + [ACTIONS.setFioDomainPublic]: 'setFioDomainPublic', + [ACTIONS.rejectFundsRequest]: 'rejectFundsRequest', + [ACTIONS.recordObtData]: 'recordObtData', + [ACTIONS.transferTokens]: 'transferTokens', + [ACTIONS.pushTransaction]: 'pushTransaction', + [ACTIONS.transferFioAddress]: 'transferFioAddress', + [ACTIONS.transferFioDomain]: 'transferFioDomain', + [ACTIONS.addBundledTransactions]: 'addBundledTransactions', + [ACTIONS.stakeFioTokens]: 'stakeFioTokens', + [ACTIONS.unStakeFioTokens]: 'unStakeFioTokens' +} + export const FIO_REQUESTS_TYPES = { PENDING: 'PENDING', SENT: 'SENT' } +export const FEE_ACTION_MAP = { + [ACTIONS.addPublicAddress]: { + action: 'getFeeForAddPublicAddress', + propName: 'fioAddress' + }, + [ACTIONS.addPublicAddresses]: { + action: 'getFeeForAddPublicAddress', + propName: 'fioAddress' + }, + [ACTIONS.rejectFundsRequest]: { + action: 'getFeeForRejectFundsRequest', + propName: 'payerFioAddress' + }, + [ACTIONS.requestFunds]: { + action: 'getFeeForNewFundsRequest', + propName: 'payeeFioAddress' + }, + [ACTIONS.recordObtData]: { + action: 'getFeeForRecordObtData', + propName: 'payerFioAddress' + }, + [ACTIONS.stakeFioTokens]: { + propName: 'fioAddress' + }, + [ACTIONS.unStakeFioTokens]: { + propName: 'fioAddress' + } +} + export const DEFAULT_BUNDLED_TXS_AMOUNT = 100 +export const DEFAULT_APR = 450 +export const STAKING_REWARD_MEMO = 'Paying Staking Rewards' +export const STAKING_LOCK_PERIOD = 1000 * 60 * 60 * 24 * 7 // 7 days +export const DAY_INTERVAL = 1000 * 60 * 60 * 24 export type FioRequest = { fio_request_id: string, @@ -69,3 +143,24 @@ export type FioDomain = { expiration: string, isPublic: boolean } + +export type TxOtherParams = { + account: string, + name: string, + authorization: Array<{ actor: string, permission: string }>, + data?: { + amount?: number, + max_fee?: number, + tpid?: string, + actor?: string + } & any, + action?: { + name: string, + params: any + }, + meta: { + isTransferProcessed?: boolean, + isFeeProcessed?: boolean + }, + ui?: any +} diff --git a/src/fio/fioEngine.js b/src/fio/fioEngine.js index 550229e55..91f6c968c 100644 --- a/src/fio/fioEngine.js +++ b/src/fio/fioEngine.js @@ -11,6 +11,7 @@ import { type EdgeFetchFunction, type EdgeFreshAddress, type EdgeSpendInfo, + type EdgeStakingStatus, type EdgeTransaction, type EdgeWalletInfo, InsufficientFundsError @@ -31,19 +32,27 @@ import { type FioAddress, type FioDomain, type FioRequest, + type TxOtherParams, + ACTIONS, ACTIONS_TO_END_POINT_KEYS, + ACTIONS_TO_FEE_END_POINT_KEYS, BROADCAST_ACTIONS, + DAY_INTERVAL, DEFAULT_BUNDLED_TXS_AMOUNT, + FEE_ACTION_MAP, FIO_REQUESTS_TYPES, HISTORY_NODE_ACTIONS, - HISTORY_NODE_OFFSET -} from './fioConst.js' + HISTORY_NODE_OFFSET, + STAKING_LOCK_PERIOD, + STAKING_REWARD_MEMO +} from './fioConst' import { fioApiErrorCodes, FioError } from './fioError' import { FioPlugin } from './fioPlugin.js' import { type FioHistoryNodeAction, type GetFioName, asFioHistoryNodeAction, + asGetFioBalanceResponse, asGetFioName, asHistoryResponse } from './fioSchema.js' @@ -52,28 +61,6 @@ const ADDRESS_POLL_MILLISECONDS = 10000 const BLOCKCHAIN_POLL_MILLISECONDS = 15000 const TRANSACTION_POLL_MILLISECONDS = 10000 const REQUEST_POLL_MILLISECONDS = 10000 -const FEE_ACTION_MAP = { - addPublicAddress: { - action: 'getFeeForAddPublicAddress', - propName: 'fioAddress' - }, - addPublicAddresses: { - action: 'getFeeForAddPublicAddress', - propName: 'fioAddress' - }, - rejectFundsRequest: { - action: 'getFeeForRejectFundsRequest', - propName: 'payerFioAddress' - }, - requestFunds: { - action: 'getFeeForNewFundsRequest', - propName: 'payeeFioAddress' - }, - recordObtData: { - action: 'getFeeForRecordObtData', - propName: 'payerFioAddress' - } -} type RecentFioFee = { publicAddress: string, @@ -103,7 +90,10 @@ export class FioEngine extends CurrencyEngine { PENDING: FioRequest[], SENT: FioRequest[] }, - fioRequestsToApprove: { [requestId: string]: any } + fioRequestsToApprove: { [requestId: string]: any }, + srps: number, + stakingRoe: string, + stakingStatus: EdgeStakingStatus } localDataDirty() { @@ -238,6 +228,7 @@ export class FioEngine extends CurrencyEngine { endPoint: EndPoint.registerFioDomain }) params.max_fee = fee + // todo: why we use pushTransaction here? const res = await this.multicastServers('pushTransaction', { action: 'regdomain', account: '', @@ -294,7 +285,7 @@ export class FioEngine extends CurrencyEngine { fioAddress: string = '' ): Promise => { const { fee } = await this.multicastServers('getFee', { - endPoint: EndPoint[actionName], + endPoint: EndPoint[ACTIONS_TO_FEE_END_POINT_KEYS[actionName]], fioAddress }) return fee @@ -425,6 +416,38 @@ export class FioEngine extends CurrencyEngine { return super.getBalance(options) } + doInitialBalanceCallback() { + super.doInitialBalanceCallback() + + const balanceCurrencyCodes = + this.currencyInfo.defaultSettings.balanceCurrencyCodes + for (const currencyCodeKey in balanceCurrencyCodes) { + try { + this.currencyEngineCallbacks.onBalanceChanged( + balanceCurrencyCodes[currencyCodeKey], + this.walletLocalData.totalBalances[ + balanceCurrencyCodes[currencyCodeKey] + ] ?? '0' + ) + } catch (e) { + this.log.error( + 'doInitialBalanceCallback Error for currencyCode', + balanceCurrencyCodes[currencyCodeKey], + e + ) + } + } + + try { + this.currencyEngineCallbacks.onStakingStatusChanged({ + stakedAmounts: [], + ...this.otherData.stakingStatus + }) + } catch (e) { + this.error(`doInitialBalanceCallback onStakingStatusChanged`, e) + } + } + updateBalance(tk: string, balance: string) { if (typeof this.walletLocalData.totalBalances[tk] === 'undefined') { this.walletLocalData.totalBalances[tk] = '0' @@ -439,37 +462,146 @@ export class FioEngine extends CurrencyEngine { this.updateOnAddressesChecked() } - processTransaction(action: FioHistoryNodeAction, actor: string): number { + checkUnStakeTx(otherParams: TxOtherParams): boolean { + return ( + otherParams.name === 'unstakefio' || + (otherParams.data != null && + otherParams.data.memo === STAKING_REWARD_MEMO) + ) + } + + updateStakingStatus( + nativeAmount: string, + blockTime: string, + txId: string + ): void { + // Might not be necessary, but better to be safe than sorry + if ( + this.otherData.stakingStatus == null || + this.otherData.stakingStatus.stakedAmounts == null + ) { + this.otherData.stakingStatus = { + stakedAmounts: [] + } + } + + const stakedAmountIndex = + this.otherData.stakingStatus.stakedAmounts.findIndex( + ({ otherParams }) => { + if (otherParams == null || otherParams.date == null) return false + + return ( + new Date(otherParams.date).toDateString() === + new Date(blockTime).toDateString() + ) + } + ) + + if (stakedAmountIndex < 0) { + const blockTimeBeginingOfGmtDay = + Math.floor(new Date(blockTime).getTime() / DAY_INTERVAL) * DAY_INTERVAL + const unlockDate = new Date( + blockTimeBeginingOfGmtDay + STAKING_LOCK_PERIOD + ) + this.otherData.stakingStatus.stakedAmounts.push({ + nativeAmount, + unlockDate, + otherParams: { + date: new Date(blockTime), + txs: [{ txId, nativeAmount, blockTime }] + } + }) + } else { + const stakedAmount = { + ...this.otherData.stakingStatus.stakedAmounts[stakedAmountIndex], + nativeAmount: '0' + } + const addedTxIndex = stakedAmount.otherParams.txs.findIndex( + ({ txId: itemTxId }) => itemTxId === txId + ) + + if (addedTxIndex < 0) { + stakedAmount.otherParams.txs.push({ + txId, + nativeAmount, + blockTime + }) + } else { + stakedAmount.otherParams.txs[addedTxIndex] = { + txId, + nativeAmount, + blockTime + } + } + + for (const tx of stakedAmount.otherParams.txs) { + stakedAmount.nativeAmount = bns.add( + stakedAmount.nativeAmount, + tx.nativeAmount + ) + } + + this.otherData.stakingStatus.stakedAmounts[stakedAmountIndex] = + stakedAmount + } + + this.localDataDirty() + try { + this.currencyEngineCallbacks.onStakingStatusChanged({ + ...this.otherData.stakingStatus + }) + } catch (e) { + this.error('onStakingStatusChanged error') + } + } + + async getStakingStatus(): Promise { + return { ...this.otherData.stakingStatus } + } + + processTransaction( + action: FioHistoryNodeAction, + actor: string, + currencyCode: string = this.currencyInfo.currencyCode + ): number { const { - act: { name: trxName, data } + act: { name: trxName, data, account, authorization } } = action.action_trace + const lockedTokenCode = + this.currencyInfo.defaultSettings.balanceCurrencyCodes.locked let nativeAmount let actorSender let networkFee = '0' - let otherParams: { - isTransferProcessed?: boolean, - isFeeProcessed?: boolean - } = {} - const currencyCode = this.currencyInfo.currencyCode + let otherParams: TxOtherParams = { + account, + name: trxName, + authorization, + data, + meta: {} + } const ourReceiveAddresses = [] if (action.block_num <= this.walletLocalData.otherData.highestTxHeight) { return action.block_num } - if (trxName !== 'trnsfiopubky' && trxName !== 'transfer') { - return action.block_num - } // Transfer funds transaction - if (trxName === 'trnsfiopubky' && data.amount != null) { - nativeAmount = data.amount.toString() - actorSender = data.actor - if (data.payee_public_key === this.walletInfo.keys.publicKey) { - ourReceiveAddresses.push(this.walletInfo.keys.publicKey) - if (actorSender === actor) { - nativeAmount = '0' + if (trxName !== 'transfer') { + nativeAmount = '0' + if (trxName === 'trnsfiopubky' && data.amount != null) { + nativeAmount = data.amount.toString() + actorSender = data.actor + if (data.payee_public_key === this.walletInfo.keys.publicKey) { + ourReceiveAddresses.push(this.walletInfo.keys.publicKey) + if (actorSender === actor) { + nativeAmount = '0' + } + } else { + nativeAmount = `-${nativeAmount}` } - } else { - nativeAmount = `-${nativeAmount}` + } + + if (currencyCode === lockedTokenCode) { + nativeAmount = data.amount != null ? data.amount.toString() : '0' } const index = this.findTransaction( @@ -479,23 +611,50 @@ export class FioEngine extends CurrencyEngine { // Check if fee transaction have already added if (index > -1) { const existingTrx = this.transactionList[currencyCode][index] - otherParams = { ...existingTrx.otherParams } - if (bns.gte(nativeAmount, '0')) { - return action.block_num + otherParams = { + ...existingTrx.otherParams, + ...otherParams, + data: { + ...(existingTrx.otherParams != null && + existingTrx.otherParams.data != null + ? existingTrx.otherParams.data + : {}), + ...otherParams.data + }, + meta: { + ...(existingTrx.otherParams != null && + existingTrx.otherParams.meta != null + ? existingTrx.otherParams.meta + : {}), + ...otherParams.meta + } } - if (otherParams.isTransferProcessed) { + + if (otherParams.meta.isTransferProcessed) { return action.block_num } - if (otherParams.isFeeProcessed) { - nativeAmount = bns.sub(nativeAmount, existingTrx.networkFee) - networkFee = existingTrx.networkFee + if (otherParams.meta.isFeeProcessed) { + if (trxName === 'trnsfiopubky') { + nativeAmount = bns.sub(nativeAmount, existingTrx.networkFee) + networkFee = existingTrx.networkFee + } else { + nativeAmount = existingTrx.nativeAmount + networkFee = '0' + + if (currencyCode === lockedTokenCode) { + nativeAmount = bns.add( + nativeAmount, + data.amount != null ? data.amount.toString() : '0' + ) + } + } } else { this.error( 'processTransaction error - existing spend transaction should have isTransferProcessed or isFeeProcessed set' ) } } - otherParams.isTransferProcessed = true + otherParams.meta.isTransferProcessed = true const edgeTransaction: EdgeTransaction = { txid: action.action_trace.trx_id, @@ -516,14 +675,19 @@ export class FioEngine extends CurrencyEngine { if (trxName === 'transfer' && data.quantity != null) { const [amount] = data.quantity.split(' ') const exchangeAmount = amount.toString() - const denom = getDenomInfo(this.currencyInfo, currencyCode) + let denom = getDenomInfo(this.currencyInfo, currencyCode) if (!denom) { - this.error(`Received unsupported currencyCode: ${currencyCode}`) - return 0 + denom = getDenomInfo(this.currencyInfo, this.currencyInfo.currencyCode) + if (!denom) { + this.error(`Received unsupported currencyCode: ${currencyCode}`) + return 0 + } } + const fioAmount = bns.mul(exchangeAmount, denom.multiplier) if (data.to === actor) { nativeAmount = `${fioAmount}` + networkFee = `-${fioAmount}` } else { nativeAmount = `-${fioAmount}` networkFee = fioAmount @@ -536,15 +700,43 @@ export class FioEngine extends CurrencyEngine { // Check if transfer transaction have already added if (index > -1) { const existingTrx = this.transactionList[currencyCode][index] - otherParams = { ...existingTrx.otherParams } - if (bns.gte(existingTrx.nativeAmount, '0')) { - return action.block_num + otherParams = { + ...otherParams, + ...existingTrx.otherParams, + data: { + ...otherParams.data, + ...(existingTrx.otherParams != null && + existingTrx.otherParams.data != null + ? existingTrx.otherParams.data + : {}) + }, + meta: { + ...otherParams.meta, + ...(existingTrx.otherParams != null && + existingTrx.otherParams.meta != null + ? existingTrx.otherParams.meta + : {}) + } } - if (otherParams.isFeeProcessed) { + if (otherParams.meta.isFeeProcessed) { return action.block_num } - if (otherParams.isTransferProcessed) { - nativeAmount = bns.sub(existingTrx.nativeAmount, networkFee) + if (otherParams.meta.isTransferProcessed) { + if (data.to !== actor) { + nativeAmount = bns.sub(existingTrx.nativeAmount, networkFee) + } else { + networkFee = '0' + + if (currencyCode === lockedTokenCode) { + nativeAmount = bns.add( + nativeAmount, + existingTrx.otherParams != null && + existingTrx.otherParams.data != null + ? existingTrx.otherParams.data.amount.toString() + : '0' + ) + } + } } else { this.error( 'processTransaction error - existing spend transaction should have isTransferProcessed or isFeeProcessed set' @@ -552,7 +744,7 @@ export class FioEngine extends CurrencyEngine { } } - otherParams.isFeeProcessed = true + otherParams.meta.isFeeProcessed = true const edgeTransaction: EdgeTransaction = { txid: action.action_trace.trx_id, date: this.getUTCDate(action.block_time) / 1000, @@ -567,6 +759,18 @@ export class FioEngine extends CurrencyEngine { this.addTransaction(currencyCode, edgeTransaction) } + if (this.checkUnStakeTx(otherParams)) { + if (currencyCode === this.currencyInfo.currencyCode) + this.processTransaction(action, actor, lockedTokenCode) + + if (currencyCode === lockedTokenCode) + this.updateStakingStatus( + nativeAmount || '0', + action.block_time, + action.action_trace.trx_id + ) + } + return action.block_num } @@ -726,6 +930,13 @@ export class FioEngine extends CurrencyEngine { switch (actionName) { case 'getChainInfo': res = await fioSdk.transactions.getChainInfo() + break + case 'getFioBalance': + res = await fioSdk.genericAction(actionName, params) + asGetFioBalanceResponse(res) + if (res.balance != null && res.balance < 0) + throw new Error('Invalid balance') + break default: res = await fioSdk.genericAction(actionName, params) @@ -899,22 +1110,37 @@ export class FioEngine extends CurrencyEngine { // Check all account balance and other relevant info async checkAccountInnerLoop() { const currencyCode = this.currencyInfo.currencyCode - let nativeAmount = '0' + const balanceCurrencyCodes = + this.currencyInfo.defaultSettings.balanceCurrencyCodes + + // Initialize balance if ( typeof this.walletLocalData.totalBalances[currencyCode] === 'undefined' ) { - this.walletLocalData.totalBalances[currencyCode] = '0' + this.updateBalance(currencyCode, '0') } // Balance try { - const { balance } = await this.multicastServers('getFioBalance') - nativeAmount = balance + '' + const balances: { + staked: string, + locked: string + } = {} + const { balance, available, staked, srps, roe } = + await this.multicastServers('getFioBalance') + const nativeAmount = String(balance) + balances.staked = String(staked) + balances.locked = bns.sub(nativeAmount, String(available)) + + this.otherData.srps = srps + this.otherData.stakingRoe = roe + + this.updateBalance(currencyCode, nativeAmount) + this.updateBalance(balanceCurrencyCodes.staked, balances.staked) + this.updateBalance(balanceCurrencyCodes.locked, balances.locked) } catch (e) { - this.log('checkAccountInnerLoop error: ', e) - nativeAmount = '0' + this.log('checkAccountInnerLoop getFioBalance error: ', e) } - this.updateBalance(currencyCode, nativeAmount) // Fio Addresses try { @@ -1146,6 +1372,9 @@ export class FioEngine extends CurrencyEngine { [FIO_REQUESTS_TYPES.PENDING]: [] } this.otherData.fioRequestsToApprove = {} + this.otherData.stakingStatus = { + stakedAmounts: [] + } } // **************************************************************************** @@ -1173,110 +1402,107 @@ export class FioEngine extends CurrencyEngine { const { edgeSpendInfo, nativeBalance, currencyCode } = super.makeSpend( edgeSpendInfoIn ) + const lockedBalance = + this.walletLocalData.totalBalances[ + this.currencyInfo.defaultSettings.balanceCurrencyCodes.locked + ] || '0' + const availableBalance = bns.sub(nativeBalance, lockedBalance) - const { otherParams } = edgeSpendInfo - let fee - if (otherParams?.fioAction) { - let feeFioAddress = '' - if (FEE_ACTION_MAP[otherParams.fioAction] && otherParams.fioParams) { - feeFioAddress = - otherParams.fioParams[FEE_ACTION_MAP[otherParams.fioAction].propName] + // Set common vars + const publicAddress = edgeSpendInfo.spendTargets[0].publicAddress + const quantity = edgeSpendInfo.spendTargets[0].nativeAmount + const { otherParams = {} }: { otherParams: TxOtherParams } = edgeSpendInfo + + // Set default action if not specified + if (!otherParams.action) { + otherParams.action = { + name: ACTIONS.transferTokens, + params: { + payeeFioPublicKey: publicAddress, + amount: quantity, + maxFee: 0 + } } - const feeResponse = await this.multicastServers('getFee', { - endPoint: EndPoint[otherParams.fioAction], - fioAddress: feeFioAddress - }) - fee = feeResponse.fee + } + otherParams.meta = { isTransferProcessed: true } + + const { name, params }: { name: string, params: any } = otherParams.action + + // Only query FIO fee if the public address is different from last makeSpend() + let fee + if ( + name === ACTIONS.transferTokens && + publicAddress === this.recentFioFee.publicAddress // todo: ask why such condition + ) { + fee = this.recentFioFee.fee } else { - // Only query FIO fee if the public address is different from last makeSpend() - if ( - edgeSpendInfo.spendTargets[0].publicAddress === - this.recentFioFee.publicAddress - ) { - fee = this.recentFioFee.fee - } else { - const feeResponse = await this.multicastServers('getFee', { - endPoint: EndPoint.transferTokens - }) - fee = feeResponse.fee + let feeFioAddress = '' + if (FEE_ACTION_MAP[name] != null && params) { + feeFioAddress = params[FEE_ACTION_MAP[name].propName] } + fee = await this.otherMethods.getFee(name, feeFioAddress) } + params.maxFee = fee - const publicAddress = edgeSpendInfo.spendTargets[0].publicAddress - const quantity = edgeSpendInfo.spendTargets[0].nativeAmount - if (bns.gt(bns.add(quantity, `${fee}`), nativeBalance)) { + // Set recent fee for transferTokens action + if (name === ACTIONS.transferTokens) { + this.recentFioFee = { publicAddress, fee } + } + + // We don't need to check the available balance for an unstake action (because that's handled separately below). + if ( + name !== ACTIONS.unStakeFioTokens && + bns.gt(bns.add(quantity, `${fee}`), availableBalance) + ) { throw new InsufficientFundsError() } - if (otherParams?.fioAction) { - if ( - ['transferFioAddress', 'transferFioDomain'].indexOf( - otherParams.fioAction - ) > -1 - ) { - otherParams.fioParams.newOwnerKey = publicAddress - } - const edgeTransaction: EdgeTransaction = { - txid: '', - date: 0, - currencyCode: this.currencyInfo.currencyCode, - blockHeight: 0, - nativeAmount: `-${fee}`, - networkFee: `${fee}`, - parentNetworkFee: '0', - signedTx: '', - ourReceiveAddresses: [], - otherParams: { - transactionJson: otherParams - }, - metadata: { - notes: '' - } - } + if ( + [ACTIONS.transferFioAddress, ACTIONS.transferFioDomain].indexOf(name) > -1 + ) { + params.newOwnerKey = publicAddress // todo: move this to the gui + } - return edgeTransaction - } else { - const memo = '' - const actor = '' - const transactionJson = { - actions: [ - { - account: 'fio.token', - name: 'trnsfiopubky', - authorization: [ - { - actor: actor, - permission: 'active' - } - ], - data: { - from: this.walletInfo.keys.publicKey, - to: publicAddress, - quantity, - memo - } - } + if (name === ACTIONS.stakeFioTokens) { + params.amount = quantity + } + + if (name === ACTIONS.unStakeFioTokens) { + const stakedBalance = + this.walletLocalData.totalBalances[ + this.currencyInfo.defaultSettings.balanceCurrencyCodes.staked ] + if (bns.gt(quantity, stakedBalance)) { + throw new InsufficientFundsError() } - const edgeTransaction: EdgeTransaction = { - txid: '', // txid - date: 0, // date - currencyCode, // currencyCode - blockHeight: 0, // blockHeight - nativeAmount: bns.sub(`-${quantity}`, `${fee}`), // nativeAmount - networkFee: `${fee}`, // networkFee - ourReceiveAddresses: [], // ourReceiveAddresses - signedTx: '0', // signedTx - otherParams: { - transactionJson - } + params.amount = quantity + const accrued = bns.mul( + bns.mul(bns.div(quantity, stakedBalance, 18), `${this.otherData.srps}`), + this.otherData.stakingRoe + ) + const estReward = bns.max(bns.sub(accrued, quantity), '0') + otherParams.ui = { + accrued, + estReward } + } - this.recentFioFee = { publicAddress, fee } - - return edgeTransaction + const edgeTransaction: EdgeTransaction = { + txid: '', + date: 0, + currencyCode, + blockHeight: 0, + nativeAmount: bns.sub(`-${quantity}`, `${fee}`), + networkFee: `${fee}`, + ourReceiveAddresses: [], + signedTx: '', + otherParams: { + ...otherParams + } } + + return edgeTransaction } async signTx(edgeTransaction: EdgeTransaction): Promise { @@ -1288,32 +1514,18 @@ export class FioEngine extends CurrencyEngine { edgeTransaction: EdgeTransaction ): Promise { let trx - if ( - edgeTransaction.otherParams && - edgeTransaction.otherParams.transactionJson && - edgeTransaction.otherParams.transactionJson.fioAction - ) { + const { otherParams } = edgeTransaction + if (otherParams != null && otherParams.action && otherParams.action.name) { trx = await this.otherMethods.fioAction( - edgeTransaction.otherParams.transactionJson.fioAction, - edgeTransaction.otherParams.transactionJson.fioParams + otherParams.action.name, + otherParams.action.params ) edgeTransaction.metadata = { notes: trx.transaction_id } - } else if (edgeTransaction.spendTargets) { - // do transfer - const publicAddress = edgeTransaction.spendTargets[0].publicAddress - const amount = bns.abs( - bns.add(edgeTransaction.nativeAmount, edgeTransaction.networkFee) - ) - trx = await this.multicastServers('transferTokens', { - payeeFioPublicKey: publicAddress, - amount, - maxFee: edgeTransaction.networkFee - }) } else { throw new Error( - 'transactionJson not set. FIO transferTokens requires publicAddress' + 'Action is not set, "action" prop of otherParams is required for FIO actions' ) } diff --git a/src/fio/fioInfo.js b/src/fio/fioInfo.js index 1db4383db..fcb7d086c 100644 --- a/src/fio/fioInfo.js +++ b/src/fio/fioInfo.js @@ -14,13 +14,10 @@ const defaultSettings: any = { 'https://fio.acherontrading.com/v1/', 'https://fio.eos.barcelona/v1/', 'https://api.fio.eosdetroit.io/v1/', - 'https://fio.zenblocks.io/v1/', 'https://api.fio.alohaeos.com/v1/', 'https://fio.greymass.com/v1/', - 'https://fio.eosusa.news/v1/', 'https://fio.eosargentina.io/v1/', 'https://fio.cryptolions.io/v1/', - 'https://fio-mainnet.eosblocksmith.io/v1/', 'https://api.fio.currencyhub.io/v1/', 'https://fio.eoscannon.io/v1/', 'https://fio.eosdublin.io/v1/', @@ -29,12 +26,12 @@ const defaultSettings: any = { historyNodeUrls: [ 'https://fio.greymass.com/v1/', 'https://fio.greymass.com/v1/', - 'https://fio.eosphere.io/v1/', - 'https://fio.eossweden.org/v1/' + 'https://fio.eosphere.io/v1/' ], fioRegApiUrl: 'https://reg.fioprotocol.io/public-api/', fioDomainRegUrl: 'https://reg.fioprotocol.io/domain/', fioAddressRegUrl: 'https://reg.fioprotocol.io/address/', + fioStakingApyUrl: 'https://fioprotocol.io/staking', defaultRef: 'edge', fallbackRef: 'edge', freeAddressRef: 'edgefree', @@ -48,7 +45,12 @@ const defaultSettings: any = { FIO_ADDRESS_IS_NOT_LINKED: 'FIO_ADDRESS_IS_NOT_LINKED', SERVER_ERROR: 'SERVER_ERROR' }, - fioRequestsTypes: FIO_REQUESTS_TYPES + fioRequestsTypes: FIO_REQUESTS_TYPES, + balanceCurrencyCodes: { + // TODO: Remove these currencyCodes in favor of adding a dedicated locked balances field to the API + staked: 'FIO:STAKED', + locked: 'FIO:LOCKED' + } } export const currencyInfo: EdgeCurrencyInfo = { diff --git a/src/fio/fioPlugin.js b/src/fio/fioPlugin.js index b12257020..aea0f8751 100644 --- a/src/fio/fioPlugin.js +++ b/src/fio/fioPlugin.js @@ -23,7 +23,11 @@ import { safeErrorMessage, shuffleArray } from '../common/utils' -import { FIO_REG_API_ENDPOINTS, FIO_REQUESTS_TYPES } from './fioConst.js' +import { + DEFAULT_APR, + FIO_REG_API_ENDPOINTS, + FIO_REQUESTS_TYPES +} from './fioConst' import { FioEngine } from './fioEngine' import { fioApiErrorCodes, FioError, fioRegApiErrorCodes } from './fioError.js' import { currencyInfo } from './fioInfo.js' @@ -232,6 +236,11 @@ export function makeFioPlugin(opts: EdgeCorePluginOptions): EdgeCurrencyPlugin { [FIO_REQUESTS_TYPES.PENDING]: [] } } + if (currencyEngine.otherData.stakingStatus == null) { + currencyEngine.otherData.stakingStatus = { + stakedAmounts: [] + } + } const out: EdgeCurrencyEngine = currencyEngine return out @@ -485,6 +494,44 @@ export function makeFioPlugin(opts: EdgeCorePluginOptions): EdgeCurrencyPlugin { currencyInfo.defaultSettings.errorCodes.SERVER_ERROR ) } + }, + async getStakeEstReturn(): Promise { + try { + const result = await fetchCors( + `${currencyInfo.defaultSettings.fioStakingApyUrl}`, + { + method: 'GET' + } + ) + const json: { + staked_token_pool: number, + outstanding_srps: number, + rewards_token_pool: number, + combined_token_pool: number, + staking_rewards_reserves_minted: number, + roe: number, + activated: boolean, + historical_apr: { + '1day': number | null, + '7day': number | null, + '30day': number | null + } + } = await result.json() + if (!result.ok) { + throw new Error(currencyInfo.defaultSettings.errorCodes.SERVER_ERROR) + } + const apr = json.historical_apr['7day'] + return (apr != null && apr > DEFAULT_APR) || apr == null + ? DEFAULT_APR + : apr + } catch (e) { + if (e.labelCode) throw e + throw new FioError( + e.message, + 500, + currencyInfo.defaultSettings.errorCodes.SERVER_ERROR + ) + } } } diff --git a/src/fio/fioSchema.js b/src/fio/fioSchema.js index 0db08da3f..604398e30 100644 --- a/src/fio/fioSchema.js +++ b/src/fio/fioSchema.js @@ -29,6 +29,12 @@ export const asFioHistoryNodeAction = asObject({ act: asObject({ account: asString, name: asString, + authorization: asArray( + asObject({ + actor: asString, + permission: asString + }) + ), data: asObject({ payee_public_key: asOptional(asString), amount: asOptional(asNumber), @@ -53,4 +59,12 @@ export const asHistoryResponse = asObject({ actions: asArray(asFioHistoryNodeAction) }) +export const asGetFioBalanceResponse = asObject({ + balance: asNumber, + available: asNumber, + staked: asNumber, + srps: asNumber, + roe: asString +}) + export type FioHistoryNodeAction = $Call diff --git a/test/engine/engine.test.js b/test/engine/engine.test.js index 030b76c48..41cae8e17 100644 --- a/test/engine/engine.test.js +++ b/test/engine/engine.test.js @@ -62,6 +62,7 @@ for (const fixture of fixtures) { // console.log('onBlockHeightChange:', height) emitter.emit('onBlockHeightChange', height) }, + onStakingStatusChanged() {}, onTransactionsChanged(transactionList) { // console.log('onTransactionsChanged:', transactionList) emitter.emit('onTransactionsChanged', transactionList) @@ -225,6 +226,7 @@ const callbacks: EdgeCurrencyEngineCallbacks = { // console.log('onBlockHeightChange:', height) emitter.emit('onBlockHeightChange', height) }, + onStakingStatusChanged() {}, onTransactionsChanged(transactionList) { // console.log('onTransactionsChanged:', transactionList) emitter.emit('onTransactionsChanged', transactionList) diff --git a/test/eos/activation.test.js b/test/eos/activation.test.js index 629baa28f..3c0a3283a 100644 --- a/test/eos/activation.test.js +++ b/test/eos/activation.test.js @@ -50,6 +50,7 @@ describe(`EOS activation`, function () { // console.log('onBlockHeightChange:', height) emitter.emit('onBlockHeightChange', height) }, + onStakingStatusChanged() {}, onTransactionsChanged(transactionList) { // console.log('onTransactionsChanged:', transactionList) emitter.emit('onTransactionsChanged', transactionList) diff --git a/test/tezos/engine.test.js b/test/tezos/engine.test.js index b0e53a258..4935ae8b2 100644 --- a/test/tezos/engine.test.js +++ b/test/tezos/engine.test.js @@ -46,6 +46,7 @@ describe(`Tezos engine`, function () { onBlockHeightChanged(height) { emitter.emit('onBlockHeightChange', height) }, + onStakingStatusChanged() {}, onTransactionsChanged(transactionList) { emitter.emit('onTransactionsChanged', transactionList) }, diff --git a/yarn.lock b/yarn.lock index 114b5bbbf..5d5b92f94 100644 --- a/yarn.lock +++ b/yarn.lock @@ -993,10 +993,10 @@ randombytes "^2.1.0" text-encoding "0.7.0" -"@fioprotocol/fiosdk@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@fioprotocol/fiosdk/-/fiosdk-1.4.0.tgz#c98c79ce2c8db9a8edff64d5485e0dc88fa5984b" - integrity sha512-/41cTJb2pRD1E8mXp8gtek60c6tyv0RzbKclUctfHJ8FHBcWwbqt8+kJtASg6pLvgMj80HyhebWPL2HfM+PqVw== +"@fioprotocol/fiosdk@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@fioprotocol/fiosdk/-/fiosdk-1.5.0.tgz#2bc2c857101de778de7f8be0e9bad6f958184e11" + integrity sha512-lWKj/VIWv1R57WEOJaTDg0nb0XTcxuQrFrUXqRs82cekoSe8GVQ7MfgWl0hbRghtO895DFBSxrlqfT5jZiP5/w== dependencies: "@fioprotocol/fiojs" "1.0.1" "@types/text-encoding" "0.0.35" @@ -3035,10 +3035,10 @@ ed25519@0.0.4: bindings "^1.2.1" nan "^2.0.9" -edge-core-js@^0.19.1: - version "0.19.1" - resolved "https://registry.yarnpkg.com/edge-core-js/-/edge-core-js-0.19.1.tgz#1eff97206853bf07256b44216f3a1eea08474017" - integrity sha512-p5rMM5ndvTNmqLlGDrukm9sVEvjYusM9478YYmcvvNGpRlvRjPB5wcMzrWMgnXi4Ma7cvK/DmaBIMTk6QgFDBw== +edge-core-js@^0.19.4: + version "0.19.4" + resolved "https://registry.yarnpkg.com/edge-core-js/-/edge-core-js-0.19.4.tgz#550164ca94ba5270d6b3ad130aff08ffb52f3acc" + integrity sha512-PgB4uELyL0r6USF2bqipU1IrC7lQKWNh0XHW/dFz+a4zSSSz+wa3dST6hOh2G0sMHgCrKiXBddfF37xZbO0UyQ== dependencies: aes-js "^3.1.0" base-x "^1.0.4"