diff --git a/src/background.js b/src/background.js index 4a369233a..7c2d26766 100644 --- a/src/background.js +++ b/src/background.js @@ -16,7 +16,7 @@ store.subscribe(({ type, payload }, state) => { store.dispatch('updateBalances', { network: state.activeNetwork, walletId: state.activeWalletId }) store.dispatch('updateFiatRates') store.dispatch('updateMarketData', { network: state.activeNetwork }) - store.dispatch('checkPendingSwaps', { walletId: state.activeWalletId }) + store.dispatch('checkPendingActions', { walletId: state.activeWalletId }) if (!balanceInterval) { balanceInterval = setInterval(() => { diff --git a/src/broker/notification.js b/src/broker/notification.js index 5761e5714..7307001a4 100644 --- a/src/broker/notification.js +++ b/src/broker/notification.js @@ -1,20 +1,46 @@ import { prettyBalance } from '../utils/coinFormatter' +import { getAssetIcon } from '../utils/asset' const SWAP_STATUS_MAP = { INITIATION_REPORTED () { - return 'Swap initiated' + return { + message: 'Swap initiated' + } }, CONFIRM_COUNTER_PARTY_INITIATION (item) { - return `Counterparty sent ${prettyBalance(item.toAmount, item.to)} ${item.to} to escrow` + return { + message: `Counterparty sent ${prettyBalance(item.toAmount, item.to)} ${item.to} to escrow` + } }, READY_TO_CLAIM () { - return 'Claiming funds' + return { + message: 'Claiming funds' + } }, SUCCESS (item) { - return `Swap completed, ${prettyBalance(item.toAmount, item.to)} ${item.to} ready to use` + return { + message: `Swap completed, ${prettyBalance(item.toAmount, item.to)} ${item.to} ready to use` + } }, REFUNDED (item) { - return `Swap refunded, ${prettyBalance(item.fromAmount, item.from)} ${item.from} returned` + return { + message: `Swap refunded, ${prettyBalance(item.fromAmount, item.from)} ${item.from} returned` + } + } +} + +const SEND_STATUS_MAP = { + WAITING_FOR_CONFIRMATIONS (item) { + return { + title: `New ${item.from} Transaction`, + message: `Sending ${prettyBalance(item.amount, item.from)} ${item.from} to ${item.toAddress}` + } + }, + SUCCESS (item) { + return { + title: `${item.from} Transaction Confirmed`, + message: `Sent ${prettyBalance(item.amount, item.from)} ${item.from} to ${item.toAddress}` + } } } @@ -24,12 +50,27 @@ export const createNotification = config => browser.notifications.create({ ...config }) -export const createSwapNotification = item => { - const fn = SWAP_STATUS_MAP[item.status] - if (!fn) return +const createSwapNotification = item => { + if (!(item.status in SWAP_STATUS_MAP)) return + const notification = SWAP_STATUS_MAP[item.status](item) return createNotification({ title: `${item.from} -> ${item.to}`, - message: fn(item) + ...notification + }) +} + +const createSendNotification = item => { + if (!(item.status in SEND_STATUS_MAP)) return + const notification = SEND_STATUS_MAP[item.status](item) + + return createNotification({ + iconUrl: getAssetIcon(item.from), + ...notification }) } + +export const createHistoryNotification = item => { + if (item.type === 'SEND') return createSendNotification(item) + else if (item.type === 'SWAP') return createSwapNotification(item) +} diff --git a/src/store/actions/checkPendingSwaps.js b/src/store/actions/checkPendingActions.js similarity index 50% rename from src/store/actions/checkPendingSwaps.js rename to src/store/actions/checkPendingActions.js index a706e9408..ad1eccc5e 100644 --- a/src/store/actions/checkPendingSwaps.js +++ b/src/store/actions/checkPendingActions.js @@ -5,16 +5,15 @@ const COMPLETED_STATES = [ 'REFUNDED' ] // TODO: Pull this out so it's being used everywhere else (Transaction icons etc.) -export const checkPendingSwaps = async ({ state, dispatch }, { walletId }) => { +export const checkPendingActions = async ({ state, dispatch }, { walletId }) => { Networks.forEach(network => { const history = state.history[network]?.[walletId] if (!history) return - history.forEach(order => { - if (order.type !== 'SWAP') return - if (order.error) return + history.forEach(item => { + if (item.error) return - if (!COMPLETED_STATES.includes(order.status)) { - dispatch('performNextAction', { network, walletId, id: order.id }) + if (!COMPLETED_STATES.includes(item.status)) { + dispatch('performNextAction', { network, walletId, id: item.id }) } }) }) diff --git a/src/store/actions/index.js b/src/store/actions/index.js index 0830152c0..e80c61157 100644 --- a/src/store/actions/index.js +++ b/src/store/actions/index.js @@ -2,7 +2,7 @@ import { changeActiveWalletId } from './changeActiveWalletId' import { changeActiveNetwork } from './changeActiveNetwork' import { changePassword } from './changePassword' import { checkIfQuoteExpired } from './checkIfQuoteExpired' -import { checkPendingSwaps } from './checkPendingSwaps' +import { checkPendingActions } from './checkPendingActions' import { clientExec } from './clientExec' import { getLockForAsset } from './getLockForAsset' import { getUnusedAddresses } from './getUnusedAddresses' @@ -35,7 +35,7 @@ export { changeActiveNetwork, changePassword, checkIfQuoteExpired, - checkPendingSwaps, + checkPendingActions, clientExec, getLockForAsset, getUnusedAddresses, diff --git a/src/store/actions/newSwap.js b/src/store/actions/newSwap.js index ec610dca4..2d1fc7c48 100644 --- a/src/store/actions/newSwap.js +++ b/src/store/actions/newSwap.js @@ -20,4 +20,6 @@ export const newSwap = async ({ dispatch, commit }, { network, walletId, agent, commit('NEW_ORDER', { network, walletId, order }) dispatch('performNextAction', { network, walletId, id: order.id }) + + return order } diff --git a/src/store/actions/performNextAction/index.js b/src/store/actions/performNextAction/index.js new file mode 100644 index 000000000..482cc9645 --- /dev/null +++ b/src/store/actions/performNextAction/index.js @@ -0,0 +1,36 @@ +import { performNextSwapAction } from './swap' +import { performNextTransactionAction } from './send' +import { createHistoryNotification } from '../../../broker/notification' + +export const performNextAction = async (store, { network, walletId, id }) => { + const { dispatch, commit, getters } = store + const item = getters.historyItemById(network, walletId, id) + if (!item) return + if (!item.status) return + + let updates + if (item.type === 'SWAP') { + updates = await performNextSwapAction(store, { network, walletId, order: item }) + } + if (item.type === 'SEND') { + updates = await performNextTransactionAction(store, { network, walletId, transaction: item }) + } + + if (updates) { + commit('UPDATE_HISTORY', { + network, + walletId, + id, + updates + }) + + createHistoryNotification({ + ...item, + ...updates + }) + + if (!updates.error) { + dispatch('performNextAction', { network, walletId, id }) + } + } +} diff --git a/src/store/actions/performNextAction/send.js b/src/store/actions/performNextAction/send.js new file mode 100644 index 000000000..33bd8219e --- /dev/null +++ b/src/store/actions/performNextAction/send.js @@ -0,0 +1,27 @@ + +import { withInterval } from './utils' + +async function waitForConfirmations ({ getters, dispatch }, { transaction, network, walletId }) { + const client = getters.client(network, walletId, transaction.from) + + const tx = await client.chain.getTransactionByHash(transaction.txHash) + + if (tx && tx.confirmations > 0) { + dispatch('updateBalances', { network, walletId, assets: [transaction.from] }) + + return { + endTime: Date.now(), + status: 'SUCCESS' + } + } +} + +export const performNextTransactionAction = async (store, { network, walletId, transaction }) => { + let updates + + if (transaction.status === 'WAITING_FOR_CONFIRMATIONS') { + updates = await withInterval(async () => waitForConfirmations(store, { transaction, network, walletId })) + } + + return updates +} diff --git a/src/store/actions/performNextAction.js b/src/store/actions/performNextAction/swap.js similarity index 83% rename from src/store/actions/performNextAction.js rename to src/store/actions/performNextAction/swap.js index 7842efc51..e8de255c9 100644 --- a/src/store/actions/performNextAction.js +++ b/src/store/actions/performNextAction/swap.js @@ -1,52 +1,6 @@ -import { random } from 'lodash-es' import { sha256 } from '@liquality/crypto' -import { updateOrder, unlockAsset, wait } from '../utils' -import { createSwapNotification } from '../../broker/notification' - -async function withLock ({ dispatch }, { order, network, walletId, asset }, func) { - const lock = await dispatch('getLockForAsset', { order, network, walletId, asset }) - try { - return await func() - } catch (e) { - return { error: e.toString() } - } finally { - unlockAsset(lock) - } -} - -async function withInterval (func) { - const updates = await func() - if (updates) { return updates } - return new Promise((resolve, reject) => { - const interval = setInterval(async () => { - const updates = await func() - if (updates) { - clearInterval(interval) - resolve(updates) - } - }, random(15000, 30000)) - }) -} - -async function hasChainTimePassed ({ getters }, { network, walletId, asset, timestamp }) { - const client = getters.client(network, walletId, asset) - const maxTries = 3 - let tries = 0 - while (tries < maxTries) { - try { - const blockNumber = await client.chain.getBlockHeight() - const latestBlock = await client.chain.getBlockByNumber(blockNumber) - return latestBlock.timestamp > timestamp - } catch (e) { - tries++ - if (tries >= maxTries) throw e - else { - console.warn(e) - await wait(2000) - } - } - } -} +import { withLock, withInterval, hasChainTimePassed } from './utils' +import { updateOrder } from '../../utils' async function canRefund ({ getters }, { network, walletId, order }) { return hasChainTimePassed({ getters }, { network, walletId, asset: order.from, timestamp: order.swapExpiration }) @@ -278,12 +232,7 @@ async function sendTo ({ getters, dispatch }, { order, network, walletId }) { } } -export const performNextAction = async (store, { network, walletId, id }) => { - const { dispatch, commit, getters } = store - const order = getters.historyItemById(network, walletId, id) - if (!order) return - if (!order.status) return - +export const performNextSwapAction = async (store, { network, walletId, order }) => { let updates switch (order.status) { @@ -340,21 +289,5 @@ export const performNextAction = async (store, { network, walletId, id }) => { break } - if (updates) { - commit('UPDATE_HISTORY', { - network, - walletId, - id, - updates - }) - - createSwapNotification({ - ...order, - ...updates - }) - - if (!updates.error) { - dispatch('performNextAction', { network, walletId, id }) - } - } + return updates } diff --git a/src/store/actions/performNextAction/utils.js b/src/store/actions/performNextAction/utils.js new file mode 100644 index 000000000..1b0ec8f20 --- /dev/null +++ b/src/store/actions/performNextAction/utils.js @@ -0,0 +1,48 @@ + +import { random } from 'lodash-es' +import { unlockAsset, wait } from '../../utils' + +export async function withLock ({ dispatch }, { order, network, walletId, asset }, func) { + const lock = await dispatch('getLockForAsset', { order, network, walletId, asset }) + try { + return await func() + } catch (e) { + return { error: e.toString() } + } finally { + unlockAsset(lock) + } +} + +export async function withInterval (func) { + const updates = await func() + if (updates) { return updates } + return new Promise((resolve, reject) => { + const interval = setInterval(async () => { + const updates = await func() + if (updates) { + clearInterval(interval) + resolve(updates) + } + }, random(15000, 30000)) + }) +} + +export async function hasChainTimePassed ({ getters }, { network, walletId, asset, timestamp }) { + const client = getters.client(network, walletId, asset) + const maxTries = 3 + let tries = 0 + while (tries < maxTries) { + try { + const blockNumber = await client.chain.getBlockHeight() + const latestBlock = await client.chain.getBlockByNumber(blockNumber) + return latestBlock.timestamp > timestamp + } catch (e) { + tries++ + if (tries >= maxTries) throw e + else { + console.warn(e) + await wait(2000) + } + } + } +} diff --git a/src/store/actions/sendTransaction.js b/src/store/actions/sendTransaction.js index 0f469b490..b69d9c10e 100644 --- a/src/store/actions/sendTransaction.js +++ b/src/store/actions/sendTransaction.js @@ -1,41 +1,31 @@ -import { createNotification } from '../../broker/notification' -import { prettyBalance } from '../../utils/coinFormatter' -import { getAssetIcon } from '../../utils/asset' +import { v4 as uuidv4 } from 'uuid' +import { createHistoryNotification } from '../../broker/notification' -export const sendTransaction = async ({ commit, getters }, { network, walletId, asset, to, amount, data, fee }) => { +export const sendTransaction = async ({ dispatch, commit, getters }, { network, walletId, asset, to, amount, data, fee }) => { const client = getters.client(network, walletId, asset) - const tx = await client.chain.sendTransaction(to, amount, data, fee) const transaction = { + id: uuidv4(), type: 'SEND', network, walletId, - to: asset, from: asset, toAddress: to, - amount, fee, tx, txHash: tx.hash, - startTime: Date.now(), - status: 'SUCCESS' + status: 'WAITING_FOR_CONFIRMATIONS' } - commit('NEW_TRASACTION', { - network, - walletId, - transaction - }) + commit('NEW_TRASACTION', { network, walletId, transaction }) + + dispatch('performNextAction', { network, walletId, id: transaction.id }) - createNotification({ - title: `New ${asset} Transaction`, - message: `Sent ${prettyBalance(amount, asset)} ${asset} to ${to}`, - iconUrl: getAssetIcon(asset) - }) + createHistoryNotification(transaction) return tx } diff --git a/src/utils/order.js b/src/utils/history.js similarity index 54% rename from src/utils/order.js rename to src/utils/history.js index 1048895a2..7c40d3ad8 100644 --- a/src/utils/order.js +++ b/src/utils/history.js @@ -1,4 +1,4 @@ -export const ORDER_STATUS_STEP_MAP = { +export const SWAP_STATUS_STEP_MAP = { QUOTE: 0, SECRET_READY: 0, INITIATED: 0, @@ -15,7 +15,7 @@ export const ORDER_STATUS_STEP_MAP = { READY_TO_SEND: 3 } -export const ORDER_STATUS_LABEL_MAP = { +export const SWAP_STATUS_LABEL_MAP = { QUOTE: 'Locking {from}', SECRET_READY: 'Locking {from}', INITIATED: 'Locking {from}', @@ -32,6 +32,30 @@ export const ORDER_STATUS_LABEL_MAP = { READY_TO_SEND: 'Sending' } -export function getOrderStatusLabel (item) { - return ORDER_STATUS_LABEL_MAP[item.status].replace('{from}', item.from).replace('{to}', item.to) +export const SEND_STATUS_STEP_MAP = { + WAITING_FOR_CONFIRMATIONS: 0, + SUCCESS: 1 +} + +export const SEND_STATUS_LABEL_MAP = { + WAITING_FOR_CONFIRMATIONS: 'Pending', + SUCCESS: 'Completed' +} + +export function getStatusLabel (item) { + if (item.type === 'SEND') { + return SEND_STATUS_LABEL_MAP[item.status] + } + if (item.type === 'SWAP') { + return SWAP_STATUS_LABEL_MAP[item.status].replace('{from}', item.from).replace('{to}', item.to) + } +} + +export function getStep (item) { + if (item.type === 'SEND') { + return SEND_STATUS_STEP_MAP[item.status] + } + if (item.type === 'SWAP') { + return SWAP_STATUS_STEP_MAP[item.status] + } } diff --git a/src/views/Account.vue b/src/views/Account.vue index c9a9404c7..19dfbc861 100644 --- a/src/views/Account.vue +++ b/src/views/Account.vue @@ -68,7 +68,7 @@ import Transaction from '@/components/Transaction' import { prettyBalance, prettyFiatBalance } from '@/utils/coinFormatter' import { shortenAddress } from '@/utils/address' import { getAssetIcon } from '@/utils/asset' -import { ORDER_STATUS_STEP_MAP, getOrderStatusLabel } from '@/utils/order' +import { getStep, getStatusLabel } from '@/utils/history' export default { components: { @@ -131,17 +131,16 @@ export default { this.updateBalances({ network: this.activeNetwork, walletId: this.activeWalletId }) }, getTransactionStatus (item) { - return item.type === 'SWAP' ? getOrderStatusLabel(item) : undefined + return getStatusLabel(item) }, getTransactionStep (item) { - return item.type === 'SWAP' ? ORDER_STATUS_STEP_MAP[item.status] + 1 : undefined + return getStep(item) + 1 }, getTransactionNumSteps (item) { - if (item.type !== 'SWAP') { - return undefined - } - - return item.sendTo ? 5 : 4 + return { + SEND: 2, + SWAP: item.sendTo ? 5 : 4 + }[item.type] }, getTransactionTitle (item) { if (item.type === 'SWAP') { diff --git a/src/views/TransactionDetails.vue b/src/views/TransactionDetails.vue index 6463e07fc..c3144f9a7 100644 --- a/src/views/TransactionDetails.vue +++ b/src/views/TransactionDetails.vue @@ -232,7 +232,7 @@ import moment from '@/utils/moment' import cryptoassets from '@liquality/cryptoassets' import { prettyBalance } from '@/utils/coinFormatter' -import { ORDER_STATUS_STEP_MAP, getOrderStatusLabel } from '@/utils/order' +import { getStep, getStatusLabel } from '@/utils/history' import { getChainFromAsset, getExplorerLink } from '@/utils/asset' import NavBar from '@/components/NavBar.vue' @@ -285,7 +285,7 @@ export default { .find((item) => item.id === this.id) }, status () { - return getOrderStatusLabel(this.item) + return getStatusLabel(this.item) }, reverseRate () { return BN(1).div(this.item.rate).dp(8) @@ -390,8 +390,8 @@ export default { ] for (let i = 0; i < steps.length; i++) { - const completed = ORDER_STATUS_STEP_MAP[this.item.status] > i - const pending = ORDER_STATUS_STEP_MAP[this.item.status] === i + const completed = getStep(this.item) > i + const pending = getStep(this.item) === i const step = await steps[i](completed, pending) timeline.push(step) }