From a380da70929f2b4dcb4fa84dfa5434ff47e3fef2 Mon Sep 17 00:00:00 2001 From: adamant-al <33592982+adamant-al@users.noreply.github.com> Date: Mon, 4 Nov 2019 12:39:39 +0300 Subject: [PATCH 1/2] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9e32e325d..2397a943a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "adamant-im", - "version": "2.1.0", + "version": "2.1.0a", "private": true, "scripts": { "serve": "vue-cli-service serve", From 6310cec76ab5ef7f2b1b8ddaa6c36eec4b1782a5 Mon Sep 17 00:00:00 2001 From: MaaKut Date: Mon, 16 Dec 2019 00:06:52 +0300 Subject: [PATCH 2/2] Upgrade to 2.3.0 (#367) BTC support (#363) Handle share link for authorized user (#358) * refactor(Login): move to `navigationGuard` ADAMANTPWA-443 - Move navigate by URI logic to separate reusable method. - Move `navigateByContact` to `navigationGuard`. * feat(isLogged.js): redirect to Chats ADAMANTPWA-443 - Redirect to Chats if current URI not containing contact information. Updated transactions statuses processing logic (#366) * Changed transactions statuses processing * Fixed status tracking for the ETH transfers * Added custom BTC confirmations updating logic * Server error handling * Minor fixes * Missed BTC height store declaration * Removed ETH balance retrieval failure reporting * Version 2.3.0 --- .travis.yml | 2 +- package.json | 4 +- src/components/SendFundsForm.vue | 46 +++++-- src/components/icons/BtcFill.vue | 7 + src/components/icons/CryptoIcon.vue | 2 + .../transactions/BtcTransaction.vue | 14 +- src/config/development.json | 3 + src/config/production.json | 3 + src/config/test.json | 3 + src/config/tor.json | 3 + src/i18n/de.json | 35 +---- src/i18n/en.json | 12 +- src/i18n/ru.json | 12 +- src/lib/bitcoin/bitcoin-api.js | 83 ++++++++++++ src/lib/bitcoin/btc-base-api.js | 42 +++--- src/lib/bitcoin/dash-api.js | 3 +- src/lib/bitcoin/doge-api.js | 3 +- src/lib/bitcoin/networks.js | 16 +-- src/lib/constants.js | 36 ++++- src/lib/getExplorerUrl.js | 2 + src/lib/transactionsFetching.js | 25 ++++ src/middlewares/isLogged.js | 3 +- src/mixins/transaction.js | 12 +- src/router/navigationGuard.js | 60 +++++++++ src/store/index.js | 2 + src/store/modules/adm/adm-actions.js | 9 ++ src/store/modules/adm/adm-getters.js | 4 +- .../modules/btc-base/btc-base-actions.js | 124 ++++++++++++------ src/store/modules/btc/btc-actions.js | 88 +++++++++++++ src/store/modules/btc/btc-getters.js | 33 +++++ src/store/modules/btc/btc-mutations.js | 18 +++ src/store/modules/btc/btc-state.js | 10 ++ src/store/modules/btc/index.js | 12 ++ src/store/modules/dash/dash-actions.js | 3 +- src/store/modules/dash/dash-getters.js | 4 +- src/store/modules/doge/doge-actions.js | 3 +- src/store/modules/doge/doge-getters.js | 4 +- src/store/modules/erc20/erc20-actions.js | 2 +- src/store/modules/erc20/erc20-getters.js | 4 +- .../modules/eth-base/eth-base-actions.js | 88 +++++++------ src/store/modules/eth/getters.js | 4 +- src/views/Login.vue | 47 +------ src/views/transactions/Transaction.vue | 2 +- yarn.lock | 8 +- 44 files changed, 640 insertions(+), 260 deletions(-) create mode 100644 src/components/icons/BtcFill.vue create mode 100644 src/lib/bitcoin/bitcoin-api.js create mode 100644 src/lib/transactionsFetching.js create mode 100644 src/store/modules/btc/btc-actions.js create mode 100644 src/store/modules/btc/btc-getters.js create mode 100644 src/store/modules/btc/btc-mutations.js create mode 100644 src/store/modules/btc/btc-state.js create mode 100644 src/store/modules/btc/index.js diff --git a/.travis.yml b/.travis.yml index d49e046e3..e02742793 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: - - "lts/*" + - 10 os: - linux cache: diff --git a/package.json b/package.json index 2397a943a..67b1debdf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "adamant-im", - "version": "2.1.0a", + "version": "2.3.0", "private": true, "scripts": { "serve": "vue-cli-service serve", @@ -25,7 +25,7 @@ "bip39": "^3.0.1", "bitcoinjs-lib": "^4.0.2", "bytebuffer": "^5.0.1", - "coininfo": "^4.3.0", + "coininfo": "^5.1.0", "core-js": "^3.0.1", "dayjs": "^1.8.11", "deepmerge": "^2.1.1", diff --git a/src/components/SendFundsForm.vue b/src/components/SendFundsForm.vue index 863e56c6f..9aceaa427 100644 --- a/src/components/SendFundsForm.vue +++ b/src/components/SendFundsForm.vue @@ -101,6 +101,13 @@ maxlength="100" /> + +
{ - return validator.call(this, propertyValue) - }) + .map(validator => validator.call(this, propertyValue)) .filter(v => v !== true) // returns only errors }) .slice(0, 1) // get first error @@ -220,7 +225,7 @@ export default { * @returns {number} */ transferFee () { - return this.$store.getters[`${this.currency.toLowerCase()}/fee`] + return this.calculateTransferFee(this.amount) }, /** @@ -289,8 +294,14 @@ export default { if (this.balance < this.transferFee) return 0 - return BigNumber(this.balance) - .minus(this.transferFee) + let amount = BigNumber(this.balance) + // For BTC we keep 1000 satoshis (0.00001 BTC) untouched as there are problems when we try to drain the wallet + if (this.currency === Cryptos.BTC) { + amount = amount.minus(0.00001) + } + + return amount + .minus(this.calculateTransferFee(this.balance)) .toNumber() }, /** @@ -332,7 +343,8 @@ export default { amount: BigNumber(this.amount).toFixed(), crypto: this.currency, name: this.recipientName, - address: this.cryptoAddress + address: this.cryptoAddress, + fee: this.transferFee }) }, validationRules () { @@ -344,12 +356,16 @@ export default { amount: [ v => v > 0 || this.$t('transfer.error_incorrect_amount'), v => this.finalAmount <= this.balance || this.$t('transfer.error_not_enough'), + v => this.validateMinAmount(v, this.currency) || this.$t('transfer.error_dust_amount'), v => this.validateNaturalUnits(v, this.currency) || this.$t('transfer.error_dust_amount'), v => isErc20(this.currency) ? this.ethBalance >= this.transferFee || this.$t('transfer.error_not_enough_eth_fee') : true ] } + }, + allowIncreaseFee () { + return this.currency === Cryptos.BTC } }, watch: { @@ -391,7 +407,8 @@ export default { showQrcodeScanner: false, showSpinner: false, dialog: false, - fetchAddress: null // fn throttle + fetchAddress: null, // fn throttle + increaseFee: false }), methods: { confirm () { @@ -540,7 +557,8 @@ export default { amount: this.amount, admAddress: this.address, address: this.cryptoAddress, - comments: this.comment + comments: this.comment, + fee: this.transferFee }) } }, @@ -586,12 +604,20 @@ export default { }) } }, + validateMinAmount (amount, currency) { + const min = getMinAmount(currency) + return amount > min + }, validateNaturalUnits (amount, currency) { const units = CryptoNaturalUnits[currency] const [ , right = '' ] = BigNumber(amount).toFixed().split('.') return right.length <= units + }, + calculateTransferFee (amount) { + const coef = this.increaseFee ? 2 : 1 + return coef * this.$store.getters[`${this.currency.toLowerCase()}/fee`](amount) } }, filters: { diff --git a/src/components/icons/BtcFill.vue b/src/components/icons/BtcFill.vue new file mode 100644 index 000000000..73252a017 --- /dev/null +++ b/src/components/icons/BtcFill.vue @@ -0,0 +1,7 @@ + diff --git a/src/components/icons/CryptoIcon.vue b/src/components/icons/CryptoIcon.vue index 358b59f85..800a6c7c4 100644 --- a/src/components/icons/CryptoIcon.vue +++ b/src/components/icons/CryptoIcon.vue @@ -15,6 +15,7 @@ import DashFillIcon from './DashFill' import KcsFillIcon from './KcsFill' import LskFillIcon from './LskFill' import UsdsFillIcon from './UsdsFill' +import BtcFillIcon from './BtcFill' import UnknownCryptoFillIcon from './UnknownCryptoFill' import { Cryptos } from '@/lib/constants' @@ -38,6 +39,7 @@ export default { KcsFillIcon, LskFillIcon, UsdsFillIcon, + BtcFillIcon, UnknownCryptoFillIcon }, props: { diff --git a/src/components/transactions/BtcTransaction.vue b/src/components/transactions/BtcTransaction.vue index 5914e8b03..d95d76386 100644 --- a/src/components/transactions/BtcTransaction.vue +++ b/src/components/transactions/BtcTransaction.vue @@ -65,7 +65,19 @@ export default { return getExplorerUrl(this.crypto, this.id) }, confirmations () { - return this.transaction.confirmations || 0 + const { height, confirmations } = this.transaction + + let result = confirmations + if (height) { + // Calculate confirmations count based on the tx block height and the last block height. + // That's for BTC only as it does not return the confirmations for the transaction. + const c = this.$store.getters[`${this.cryptoKey}/height`] - height + if (isFinite(c) && c > result) { + result = c + } + } + + return result } }, methods: { diff --git a/src/config/development.json b/src/config/development.json index dd5b1be5a..1ae806db1 100644 --- a/src/config/development.json +++ b/src/config/development.json @@ -23,6 +23,9 @@ ], "dash": [ { "url": "https://dashnode1.adamant.im" } + ], + "btc": [ + { "url": "https://btcnode2.adamant.im" } ] }, "env": "development" diff --git a/src/config/production.json b/src/config/production.json index 56ce52c90..492bc5709 100644 --- a/src/config/production.json +++ b/src/config/production.json @@ -23,6 +23,9 @@ ], "dash": [ { "url": "https://dashnode1.adamant.im" } + ], + "btc": [ + { "url": "https://btcnode2.adamant.im" } ] }, "env": "production" diff --git a/src/config/test.json b/src/config/test.json index acacec0a7..d6e413bd9 100644 --- a/src/config/test.json +++ b/src/config/test.json @@ -17,6 +17,9 @@ ], "dash": [ { "url": "https://dashnode1.adamant.im" } + ], + "btc": [ + { "url": "https://btcnode2.adamant.im" } ] }, "env": "test" diff --git a/src/config/tor.json b/src/config/tor.json index d414c6c5b..fe8dbf2db 100644 --- a/src/config/tor.json +++ b/src/config/tor.json @@ -23,6 +23,9 @@ ], "dash": [ { "url": "http://ldod53womnlwjmd4noq5yuq26xx3mmbrr4uogkfymuakhofjcuqeygad.onion" } + ], + "btc": [ + { "url": "http://mgplh7en3d6ywsec5h6q3tc6mirsleb5cmlyn6nmp25qrb6gy35nypad.onion" } ] }, "env": "production" diff --git a/src/i18n/de.json b/src/i18n/de.json index 31cbf84a0..0908e188b 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -65,39 +65,7 @@ "send_btn": "Send funds", "share_uri": "Share {crypto} address", "show_qr_code": "Show QR code", - "wallet": "wallet", - "your_address_ADM": "ADAMANT adresse", - "your_address_BNB": "Binance Coin adresse", - "your_address_BZ": "Bit-Z adresse", - "your_address_DASH": "Dash adresse", - "your_address_DOGE": "Dogecoin adresse", - "your_address_ETH": "Ethereum adresse", - "your_address_KCS": "KuCoin Shares adresse", - "your_address_USDS": "StableUSD adresse", - "your_address_tooltip_ADM": "Das ist deine eindeutige Identifizierung auf ADAMANT. Klick hier um diese in die Zwischenablage zu kopieren.", - "your_address_tooltip_BNB": "Das ist deine eindeutige Identifizierung auf Binance Coin. Klick hier um diese in die Zwischenablage zu kopieren.", - "your_address_tooltip_BZ": "Das ist deine eindeutige Identifizierung auf Bit-Z. Klick hier um diese in die Zwischenablage zu kopieren.", - "your_address_tooltip_DASH": "Das ist deine eindeutige Identifizierung auf Dash. Klick hier um diese in die Zwischenablage zu kopieren.", - "your_address_tooltip_DOGE": "Das ist deine eindeutige Identifizierung auf Dogecoin. Klick hier um diese in die Zwischenablage zu kopieren.", - "your_address_tooltip_ETH": "Das ist deine eindeutige Identifizierung auf Ethereum. Klick hier um diese in die Zwischenablage zu kopieren.", - "your_address_tooltip_KCS": "Das ist deine eindeutige Identifizierung auf KuCoin Shares. Klick hier um diese in die Zwischenablage zu kopieren.", - "your_address_tooltip_USDS": "Das ist deine eindeutige Identifizierung auf StableUSD. Klick hier um diese in die Zwischenablage zu kopieren.", - "your_balance_ADM": "ADAMANT Balance", - "your_balance_BNB": "Binance Coin Balance", - "your_balance_BZ": "Bit-Z Balance", - "your_balance_DASH": "Dash Balance", - "your_balance_DOGE": "Dogecoin Balance", - "your_balance_ETH": "Ethereum Balance", - "your_balance_KCS": "KuCoin Shares Balance", - "your_balance_USDS": "StableUSD Balance", - "your_balance_tooltip_ADM": "Hier siehst du deinen ADAMANT Kontostand. Klicke hier um alle Ein- und ausgehenden Transaktionen zu sehen.", - "your_balance_tooltip_BNB": "Hier siehst du deinen ADAMANT BNB Kontostand.", - "your_balance_tooltip_BZ": "Hier siehst du deinen ADAMANT BZ Kontostand.", - "your_balance_tooltip_DASH": "Hier siehst du deinen ADAMANT DASH Kontostand.", - "your_balance_tooltip_DOGE": "Hier siehst du deinen ADAMANT DOGE Kontostand.", - "your_balance_tooltip_ETH": "Hier siehst du deinen ADAMANT ETH Kontostand.", - "your_balance_tooltip_KCS": "Hier siehst du deinen ADAMANT KCS Kontostand.", - "your_balance_tooltip_USDS": "Hier siehst du deinen ADAMANT USDS Kontostand." + "wallet": "wallet" }, "login": { "brand_title": "ADAMANT", @@ -243,6 +211,7 @@ "error_transfer_send": "Error while sending transaction", "error_unknown": "Unknown error", "final_amount_label": "Betrag inklusive Transaktionsgebühr", + "increase_fee": "Increase fee", "invalid_qr_code": "QR code does not contain an address or unrecognizable", "max_transfer": "Max to transfer", "no_address_text": "This user does not have a public {crypto} wallet yet. To get it, he should login into messenger when his balance is more than 0.001 ADM.", diff --git a/src/i18n/en.json b/src/i18n/en.json index dc3cceeed..0344a5ee4 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -73,15 +73,7 @@ "send_crypto": "Send {crypto}", "share_uri": "Share {crypto} address", "show_qr_code": "Show QR code", - "wallet": "wallet", - "your_address_ADM": "ADAMANT address", - "your_address_BNB": "Binance Coin address", - "your_address_BZ": "Bit-Z address", - "your_address_DASH": "Dash address", - "your_address_DOGE": "DOGE address", - "your_address_ETH": "Ethereum address", - "your_address_KCS": "KuCoin Shares address", - "your_address_USDS": "StableUSD address" + "wallet": "wallet" }, "login": { "brand_title": "ADAMANT", @@ -205,6 +197,7 @@ "statuses": { "error": "Error", "pending": "Pending", + "registered": "Pending", "success": "Success" }, "transactions": "Transactions", @@ -241,6 +234,7 @@ "error_transfer_send": "Error while completing a transaction", "error_unknown": "Unknown error. Weak connection?", "final_amount_label": "Amount including a transfer fee", + "increase_fee": "Increase fee", "invalid_qr_code": "QR code does not contain an address or unrecognizable", "no_address_text": "This user does not have a public {crypto} wallet yet. They must log in the Messenger with more than 0.001 ADM on their balance", "no_address_title": "Recipient has no {crypto} wallet yet", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index e394cef13..ed1c03b6c 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -75,15 +75,7 @@ "send_crypto": "Отправить {crypto}", "share_uri": "Поделиться ссылкой {crypto}", "show_qr_code": "Показать QR-код", - "wallet": "кошелек", - "your_address_ADM": "ADAMANT address", - "your_address_BNB": "Кошелек Binance Coin", - "your_address_BZ": "Кошелек Bit-Z", - "your_address_DASH": "Кошелек Dash", - "your_address_DOGE": "Кошелек Dogecoin", - "your_address_ETH": "Кошелек Ethereum", - "your_address_KCS": "Кошелек KuCoin Shares", - "your_address_USDS": "Кошелек StableUSD" + "wallet": "кошелек" }, "login": { "brand_title": "АДАМАНТ", @@ -206,6 +198,7 @@ "statuses": { "error": "Ошибка", "pending": "Ожидание", + "registered": "Ожидание", "success": "Успешно" }, "transactions": "Транзакции", @@ -242,6 +235,7 @@ "error_transfre_send": "Ошибка при отправке транзакции", "error_unknown": "Неизвестная ошибка. Плохое подключение?", "final_amount_label": "Количество, включая комиссию за перевод", + "increase_fee": "Увеличить комиссию", "invalid_qr_code": "QR-код не содержит адрес или не удается распознать", "no_address_text": "У этого пользователя еще нет публичного {crypto}-кошелька. Он должен зайти в Мессенджер с балансом более 0.001 ADM", "no_address_title": "У получателя нет {crypto}-кошелька", diff --git a/src/lib/bitcoin/bitcoin-api.js b/src/lib/bitcoin/bitcoin-api.js new file mode 100644 index 000000000..2a956c722 --- /dev/null +++ b/src/lib/bitcoin/bitcoin-api.js @@ -0,0 +1,83 @@ +import BtcBaseApi from './btc-base-api' +import { Cryptos } from '../constants' + +export default class BitcoinApi extends BtcBaseApi { + constructor (passphrase) { + super(Cryptos.BTC, passphrase) + } + + /** + * @override + */ + getBalance () { + return this._get(`/address/${this.address}`).then( + data => (data.chain_stats.funded_txo_sum - data.chain_stats.spent_txo_sum) / this.multiplier + ) + } + + /** @override */ + getFee () { + return 0 + } + + /** Returns last block height */ + getHeight () { + return this._get('/blocks/tip/height').then(data => Number(data) || 0) + } + + /** @override */ + sendTransaction (txHex) { + return this._getClient().post('/tx', txHex).then(response => response.data) + } + + /** @override */ + getTransaction (txid) { + return this._get(`/tx/${txid}`).then(x => this._mapTransaction(x)) + } + + /** @override */ + getTransactions ({ toTx = '' }) { + let url = `/address/${this.address}/txs` + if (toTx) { + url += `/chain/${toTx}` + } + return this._get(url).then(transactions => transactions.map(x => this._mapTransaction(x))) + } + + /** @override */ + getUnspents () { + return this._get(`/address/${this.address}/utxo`).then(outputs => + outputs.map(x => ({ txid: x.txid, amount: x.value, vout: x.vout })) + ) + } + + getFeeRate () { + return this._get('/fee-estimates').then(estimates => estimates['2']) + } + + /** @override */ + _mapTransaction (tx) { + const mapped = super._mapTransaction({ + ...tx, + vin: tx.vin.map(x => ({ ...x, addr: x.prevout.scriptpubkey_address })), + vout: tx.vout.map(x => ({ + ...x, + scriptPubKey: { addresses: [x.scriptpubkey_address] } + })), + fees: tx.fee, + time: tx.status.block_time, + confirmations: tx.status.confirmed ? 1 : 0 + }) + + mapped.amount /= this.multiplier + mapped.fee /= this.multiplier + mapped.height = tx.status.block_height + + return mapped + } + + /** Executes a GET request to the API */ + _get (url, params) { + return this._getClient().get(url, params).then(response => response.data) + } +} diff --git a/src/lib/bitcoin/btc-base-api.js b/src/lib/bitcoin/btc-base-api.js index ee661d614..78e8e96c5 100644 --- a/src/lib/bitcoin/btc-base-api.js +++ b/src/lib/bitcoin/btc-base-api.js @@ -13,6 +13,17 @@ const getUnique = values => { return Object.keys(map) } +const createClient = url => { + const client = axios.create({ baseURL: url }) + client.interceptors.response.use(null, error => { + if (error.response && Number(error.response.status) >= 500) { + console.error('Request failed', error) + } + return Promise.reject(error) + }) + return client +} + export default class BtcBaseApi { constructor (crypto, passphrase) { const network = this._network = networks[crypto] @@ -41,24 +52,16 @@ export default class BtcBaseApi { return Promise.resolve(0) } - /** - * Returns transaction fee - * @abstract - * @returns {number} - */ - getFee () { - return 0 - } - /** * Creates a transfer transaction hex and ID * @param {string} address receiver address * @param {number} amount amount to transfer (coins, not satoshis) + * @param {number} fee transaction fee (coins, not satoshis) * @returns {Promise<{hex: string, txid: string}>} */ - createTransaction (address = '', amount = 0) { - return this._getUnspents().then(unspents => { - const hex = this._buildTransaction(address, amount, unspents) + createTransaction (address = '', amount = 0, fee) { + return this.getUnspents().then(unspents => { + const hex = this._buildTransaction(address, amount, unspents, fee) let txid = bitcoin.crypto.sha256(Buffer.from(hex, 'hex')) txid = bitcoin.crypto.sha256(Buffer.from(txid)) @@ -102,7 +105,7 @@ export default class BtcBaseApi { * @abstract * @returns {Promise>} */ - _getUnspents () { + getUnspents () { return Promise.resolve([]) } @@ -111,16 +114,17 @@ export default class BtcBaseApi { * @param {string} address target address * @param {number} amount amount to send * @param {Array<{txid: string, amount: number, vout: number}>} unspents unspent transaction to use as inputs + * @param {number} fee transaction fee in primary units (BTC, DOGE, DASH, etc) * @returns {string} */ - _buildTransaction (address, amount, unspents) { + _buildTransaction (address, amount, unspents, fee) { amount = new BigNumber(amount).times(this.multiplier).toNumber() amount = Math.floor(amount) const txb = new bitcoin.TransactionBuilder(this._network) txb.setVersion(1) - const target = amount + new BigNumber(this.getFee()).times(this.multiplier).toNumber() + const target = amount + new BigNumber(fee).times(this.multiplier).toNumber() let transferAmount = 0 let inputs = 0 @@ -133,7 +137,7 @@ export default class BtcBaseApi { } }) - txb.addOutput(address, amount) + txb.addOutput(bitcoin.address.toOutputScript(address, this._network), amount) txb.addOutput(this._address, transferAmount - target) for (let i = 0; i < inputs; ++i) { @@ -147,9 +151,7 @@ export default class BtcBaseApi { _getClient () { const url = getEnpointUrl(this._crypto) if (!this._clients[url]) { - this._clients[url] = axios.create({ - baseURL: url - }) + this._clients[url] = createClient(url) } return this._clients[url] } @@ -200,7 +202,7 @@ export default class BtcBaseApi { id: tx.txid, hash: tx.txid, fee, - status: confirmations > 0 ? 'SUCCESS' : 'PENDING', + status: confirmations > 0 ? 'SUCCESS' : 'REGISTERED', timestamp, direction, senders, diff --git a/src/lib/bitcoin/dash-api.js b/src/lib/bitcoin/dash-api.js index eab2df9ba..5553e1c2a 100644 --- a/src/lib/bitcoin/dash-api.js +++ b/src/lib/bitcoin/dash-api.js @@ -31,7 +31,6 @@ export default class DashApi extends BtcBaseApi { .then(result => Number(result.balance) / this.multiplier) } - /** @override */ getFee () { return TX_FEE } @@ -68,7 +67,7 @@ export default class DashApi extends BtcBaseApi { } /** @override */ - _getUnspents () { + getUnspents () { return this._invoke('getaddressutxos', [this.address]).then(result => { if (!Array.isArray(result)) return [] diff --git a/src/lib/bitcoin/doge-api.js b/src/lib/bitcoin/doge-api.js index c129867fe..34b464e5d 100644 --- a/src/lib/bitcoin/doge-api.js +++ b/src/lib/bitcoin/doge-api.js @@ -27,7 +27,6 @@ export default class DogeApi extends BtcBaseApi { .then(balance => Number(balance) / this.multiplier) } - /** @override */ getFee () { return TX_FEE } @@ -54,7 +53,7 @@ export default class DogeApi extends BtcBaseApi { } /** @override */ - _getUnspents () { + getUnspents () { return this._get(`/addr/${this.address}/utxo?noCache=1`) .then(unspents => unspents.map(tx => ({ ...tx, diff --git a/src/lib/bitcoin/networks.js b/src/lib/bitcoin/networks.js index 53fa69817..520718b52 100644 --- a/src/lib/bitcoin/networks.js +++ b/src/lib/bitcoin/networks.js @@ -1,18 +1,8 @@ import coininfo from 'coininfo' import { Cryptos } from '../constants' -const getNetwork = fmt => ({ - messagePrefix: '\x19' + fmt.name + ' Signed Message:\n', - bip32: { - public: fmt.bip32.public, - private: fmt.bip32.private - }, - pubKeyHash: fmt.pubKeyHash, - scriptHash: fmt.scriptHash, - wif: fmt.wif -}) - export default Object.freeze({ - [Cryptos.DOGE]: getNetwork(coininfo.dogecoin.main.toBitcoinJS()), - [Cryptos.DASH]: getNetwork(coininfo.dash.main.toBitcoinJS()) + [Cryptos.DOGE]: coininfo.dogecoin.main.toBitcoinJS(), + [Cryptos.DASH]: coininfo.dash.main.toBitcoinJS(), + [Cryptos.BTC]: coininfo.bitcoin.main.toBitcoinJS() }) diff --git a/src/lib/constants.js b/src/lib/constants.js index d5c5c0d89..56b1fad2f 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -15,6 +15,7 @@ export const Transactions = { export const Cryptos = { ADM: 'ADM', + BTC: 'BTC', ETH: 'ETH', BZ: 'BZ', KCS: 'KCS', @@ -32,7 +33,8 @@ export const CryptosNames = { [Cryptos.DOGE]: 'DOGE', [Cryptos.DASH]: 'DASH', [Cryptos.KCS]: 'KuCoin Shares', - [Cryptos.USDS]: 'StableUSD' + [Cryptos.USDS]: 'StableUSD', + [Cryptos.BTC]: 'Bitcoin' } export const ERC20 = Object.freeze([ @@ -44,7 +46,8 @@ export const ERC20 = Object.freeze([ export const BTC_BASED = Object.freeze([ Cryptos.DOGE, - Cryptos.DASH + Cryptos.DASH, + Cryptos.BTC ]) export const isErc20 = crypto => ERC20.includes(crypto) @@ -60,7 +63,8 @@ export const CryptoAmountPrecision = { BZ: 6, DASH: 5, KCS: 6, - USDS: 6 + USDS: 6, + BTC: 8 } export const CryptoNaturalUnits = { @@ -71,7 +75,8 @@ export const CryptoNaturalUnits = { BZ: 18, DASH: 8, KCS: 6, - USDS: 6 + USDS: 6, + BTC: 8 } /** Fees for the misc ADM operations */ @@ -134,3 +139,26 @@ export const TransactionStatus = { INVALID: 'invalid', UNKNOWN: 'unknown' } + +/** + * Minimal transferrable amounts for the known cryptos + */ +export const MinAmounts = Object.freeze({ + BTC: 546e-8 // 546 satoshis +}) + +/** + * Returns minimal amount that can be transferred for the specified crypto + * @param {string} crypto crypto + * @returns {number} + */ +export function getMinAmount (crypto) { + let amount = MinAmounts[crypto] + + if (!amount) { + const precision = CryptoAmountPrecision[crypto] + amount = precision ? Math.pow(10, -precision) : 0 + } + + return amount +} diff --git a/src/lib/getExplorerUrl.js b/src/lib/getExplorerUrl.js index 0b4be75ac..cbfb84d86 100644 --- a/src/lib/getExplorerUrl.js +++ b/src/lib/getExplorerUrl.js @@ -17,6 +17,8 @@ export default function getExplorerUrl (crypto, transactionId) { return 'https://dogechain.info/tx/' + transactionId case Cryptos.DASH: return 'https://explorer.dash.org/tx/' + transactionId + case Cryptos.BTC: + return 'https://btc.com/' + transactionId } return '' diff --git a/src/lib/transactionsFetching.js b/src/lib/transactionsFetching.js new file mode 100644 index 000000000..31fee8da5 --- /dev/null +++ b/src/lib/transactionsFetching.js @@ -0,0 +1,25 @@ +/** Max number of attempts to fetch a pending transaction */ +export const PENDING_ATTEMPTS = 15 + +/** Interval (ms) between attempts to fetch a new pending transaction */ +export const NEW_PENDING_TIMEOUT = 60 * 1000 + +/** Interval (ms) between attempts to fetch an old pending transaction */ +export const OLD_PENDING_TIMEOUT = 5 * 1000 + +/** Max age (ms) of a transaction that can be considered as new */ +export const NEW_TRANSACTION_AGE = 30 * 60 * 1000 + +/** + * Returns `true` if the transaction with the specified timestamp can be considered as new. + * @param {number} timestamp the timestamp to test + * @returns {boolean} + */ +export const isNew = timestamp => (Date.now() - timestamp) < NEW_TRANSACTION_AGE + +/** + * Returns a retry interval (ms) for a pending transaction details re-fecthing. + * @param {number} timestamp transaction timestamp + * @returns {number} + */ +export const getPendingTxRetryTimeout = timestamp => (isNew(timestamp) ? NEW_PENDING_TIMEOUT : OLD_PENDING_TIMEOUT) diff --git a/src/middlewares/isLogged.js b/src/middlewares/isLogged.js index 5aab93d97..557e7c22e 100644 --- a/src/middlewares/isLogged.js +++ b/src/middlewares/isLogged.js @@ -1,8 +1,9 @@ +import { navigateByURI } from '@/router/navigationGuard' import store from '@/store' export default (to, from, next) => { if (to.name === 'Login' && store.getters.isLogged) { - next({ name: 'Chats' }) + navigateByURI(next) } else { next() } diff --git a/src/mixins/transaction.js b/src/mixins/transaction.js index 31004b9ce..1ba797978 100644 --- a/src/mixins/transaction.js +++ b/src/mixins/transaction.js @@ -27,15 +27,9 @@ export default { * @param {string} hash Transaction hash * @param {number} timestamp ADAMANT special message timestamp */ - fetchTransaction (type, hash, timestamp) { + fetchTransaction (type, hash) { const cryptoModule = type.toLowerCase() - const NEW_TRANSACTION_DELTA = 900 // 15 min - const isNew = (Date.now() - timestamp) / 1000 < NEW_TRANSACTION_DELTA - - return this.$store.dispatch(`${cryptoModule}/getTransaction`, { - hash, - isNew - }) + return this.$store.dispatch(`${cryptoModule}/getTransaction`, { hash }) }, /** @@ -114,7 +108,7 @@ export default { status = TS.INVALID } } else { - status = status === 'PENDING' + status = (status === 'PENDING' || status === 'REGISTERED') ? TS.PENDING : TS.REJECTED } diff --git a/src/router/navigationGuard.js b/src/router/navigationGuard.js index ec9999ad2..56682360c 100644 --- a/src/router/navigationGuard.js +++ b/src/router/navigationGuard.js @@ -1,7 +1,33 @@ import { Cryptos } from '@/lib/constants' +import { parseURI } from '@/lib/uri' import validateAddress from '@/lib/validateAddress' +import i18n from '@/i18n' +import router from '@/router' import store from '@/store' +/** + * Navigate by contact info from URI or redirect to chats + * + * @param {function} next Resolves the hook (redirect to Chats) + */ +export function navigateByURI (next) { + const contact = parseURI() + + if (contact.address) { + _navigateByContact( + contact.params.message, + contact.address, + contact.params.label + ) + } else { + if (next) { + next({ name: 'Chats' }) + } else { + router.push({ name: 'Chats' }) + } + } +} + export default { chats: (to, from, next) => { const chat = store.state.chat.chats[to.params.partnerId] @@ -23,3 +49,37 @@ export default { } else next('/home') } } + +/** + * Navigate to view depending on a partner address validity + * + * @param {string} messageText Text of message to place in textarea + * @param {string} partnerId Partner address to open chat with + * @param {string} partnerName Predefined partner name from label + */ +function _navigateByContact (messageText, partnerId, partnerName) { + if (validateAddress(Cryptos.ADM, partnerId)) { + store.dispatch('chat/createChat', { partnerId, partnerName }) + .then(key => router.push({ + name: 'Chat', + params: { messageText, partnerId } + })) + .catch(x => { + router.push({ + name: 'Chats', + params: { partnerId, showNewContact: true } + }) + store.dispatch('snackbar/show', { + message: x.message + }) + }) + } else { + router.push({ + name: 'Chats', + params: { partnerId, showNewContact: false } + }) + store.dispatch('snackbar/show', { + message: i18n.$t('chats.incorrect_address', { crypto: Cryptos.ADM }) + }) + } +} diff --git a/src/store/index.js b/src/store/index.js index 158e03612..29986f8ec 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -22,6 +22,7 @@ import partnersModule from './modules/partners' import admModule from './modules/adm' import dogeModule from './modules/doge' import dashModule from './modules/dash' +import bitcoinModule from './modules/btc' import nodesModule from './modules/nodes' import delegatesModule from './modules/delegates' import nodesPlugin from './modules/nodes/nodes-plugin' @@ -163,6 +164,7 @@ const store = { adm: admModule, // ADM transfers doge: dogeModule, dash: dashModule, + btc: bitcoinModule, partners: partnersModule, // Partners: display names, crypto addresses and so on delegates: delegatesModule, // Voting for delegates screen nodes: nodesModule, // ADAMANT nodes diff --git a/src/store/modules/adm/adm-actions.js b/src/store/modules/adm/adm-actions.js index b2de7294f..4a7004941 100644 --- a/src/store/modules/adm/adm-actions.js +++ b/src/store/modules/adm/adm-actions.js @@ -88,6 +88,15 @@ export default { ) }, + /** + * Updates the transaction details + * @param {{ dispatch: function }} param0 Vuex context + * @param {{hash: string}} payload action payload + */ + updateTransaction ({ dispatch }, payload) { + return dispatch('getTransaction', payload) + }, + /** * Sends the specified amount of ADM to the specified ADM address * @param {any} context Vuex action context diff --git a/src/store/modules/adm/adm-getters.js b/src/store/modules/adm/adm-getters.js index 736b7bb9a..0f8ea239e 100644 --- a/src/store/modules/adm/adm-getters.js +++ b/src/store/modules/adm/adm-getters.js @@ -23,7 +23,5 @@ export default { */ partnerTransactions: state => partner => Object.values(state.transactions).filter(tx => tx.partner === partner), - fee () { - return Fees.ADM_TRANSFER - } + fee: state => amount => Fees.ADM_TRANSFER } diff --git a/src/store/modules/btc-base/btc-base-actions.js b/src/store/modules/btc-base/btc-base-actions.js index 9dcdb6075..e1544f664 100644 --- a/src/store/modules/btc-base/btc-base-actions.js +++ b/src/store/modules/btc-base/btc-base-actions.js @@ -1,14 +1,31 @@ import BigNumber from '@/lib/bignumber' import BtcBaseApi from '../../../lib/bitcoin/btc-base-api' import { storeCryptoAddress } from '../../../lib/store-crypto-address' - -const MAX_ATTEMPTS = 5 -const NEW_TRANSACTION_TIMEOUT = 120 -const OLD_TRANSACTION_TIMEOUT = 5 - -export default options => { +import * as tf from '../../../lib/transactionsFetching' + +const DEFAULT_CUSTOM_ACTIONS = () => ({ }) + +/** + * @typedef {Object} Options + * @property {function} apiCtor class to use for interaction with the crypto API + * @property {function(BtcBaseApi, object): Promise} getNewTransactions function to get the new transactions list (second arg is a Vuex context) + * @property {function(BtcBaseApi, object): Promise} getOldTransactions function to get the old transactions list (second arg is a Vuex context) + * @property {function(function(): BtcBaseApi): object} customActions function to create custom actions for the current crypto (optional) + * @property {number} fetchRetryTimeout interval (ms) between attempts to fetch the registered transaction details + */ + +/** + * Creates actions for the BTC-based crypto + * @param {Options} options config options + */ +function createActions (options) { const Api = options.apiCtor || BtcBaseApi - const { getNewTransactions, getOldTransactions } = options + const { + getNewTransactions, + getOldTransactions, + customActions = DEFAULT_CUSTOM_ACTIONS, + fetchRetryTimeout + } = options /** @type {BtcBaseApi} */ let api = null @@ -63,13 +80,13 @@ export default options => { api.getBalance().then(balance => context.commit('status', { balance })) }, - sendTokens (context, { amount, admAddress, address, comments }) { + sendTokens (context, { amount, admAddress, address, comments, fee }) { if (!api) return address = address.trim() const crypto = context.state.crypto - return api.createTransaction(address, amount) + return api.createTransaction(address, amount, fee) .then(tx => { if (!admAddress) return tx.hex @@ -101,12 +118,12 @@ export default options => { senderId: context.state.address, recipientId: address, amount, - fee: api.getFee(amount), + fee: api.getFee(amount) || fee, status: 'PENDING', timestamp: Date.now() }]) - context.dispatch('getTransaction', { hash, isNew: true, force: true }) + context.dispatch('getTransaction', { hash, force: true }) return hash } @@ -118,14 +135,14 @@ export default options => { * @param {object} context Vuex action context * @param {{hash: string, force: boolean, timestamp: number, amount: number}} payload hash and timestamp of the transaction to fetch */ - getTransaction (context, payload) { + async getTransaction (context, payload) { if (!api) return if (!payload.hash) return const existing = context.state.transactions[payload.hash] if (existing && !payload.force) return - // Set a stub so far + // Set a stub so far, if the transaction is not in the store yet if (!existing || existing.status === 'ERROR') { context.commit('transactions', [{ hash: payload.hash, @@ -135,31 +152,58 @@ export default options => { }]) } - api.getTransaction(payload.hash) - .then( - tx => { - if (tx) context.commit('transactions', [tx]) - return (!tx && payload.isNew) || (tx && tx.status !== 'SUCCESS') - }, - () => true - ) - .then(replay => { - const attempt = payload.attempt || 0 - if (replay && attempt < MAX_ATTEMPTS) { - const newPayload = { - ...payload, - attempt: attempt + 1, - force: true - } - - const timeout = payload.isNew ? NEW_TRANSACTION_TIMEOUT : OLD_TRANSACTION_TIMEOUT - setTimeout(() => context.dispatch('getTransaction', newPayload), timeout * 1000) - } + let tx = null + try { + tx = await api.getTransaction(payload.hash) + } catch (e) { } + + let retry = false + let retryTimeout = 0 + const attempt = payload.attempt || 0 + + if (tx) { + context.commit('transactions', [tx]) + // The transaction has been confirmed, we're done here + if (tx.status === 'SUCCESS') return + + // If it's not confirmed but is already registered, keep on trying to fetch its details + retryTimeout = fetchRetryTimeout + retry = true + } else if (existing && existing.status === 'REGISTERED') { + // We've failed to fetch the details for some reason, but the transaction is known to be + // accepted by the network - keep on fetching + retryTimeout = fetchRetryTimeout + retry = true + } else { + // The network does not yet know this transaction. We'll make several attempts to retrieve it. + retry = attempt < tf.PENDING_ATTEMPTS + retryTimeout = tf.getPendingTxRetryTimeout(payload.timestamp || (existing && existing.timestamp)) + } - if (replay && attempt >= MAX_ATTEMPTS) { - context.commit('transactions', [{ hash: payload.hash, status: 'ERROR' }]) - } - }) + if (!retry) { + // If we're here, we have abandoned any hope to get the transaction details. + context.commit('transactions', [{ hash: payload.hash, status: 'ERROR' }]) + } else { + // Try to get the details one more time + const newPayload = { + ...payload, + attempt: attempt + 1, + force: true + } + setTimeout(() => context.dispatch('getTransaction', newPayload), retryTimeout) + } + }, + + /** + * Updates the transaction details + * @param {{ dispatch: function }} param0 Vuex context + * @param {{hash: string}} payload action payload + */ + updateTransaction ({ dispatch }, payload) { + return dispatch('getTransaction', { + hash: payload.hash, + force: true + }) }, getNewTransactions (context) { @@ -174,6 +218,10 @@ export default options => { return getOldTransactions(api, context) } return Promise.resolve() - } + }, + + ...customActions(() => api) } } + +export default createActions diff --git a/src/store/modules/btc/btc-actions.js b/src/store/modules/btc/btc-actions.js new file mode 100644 index 000000000..9f06f0692 --- /dev/null +++ b/src/store/modules/btc/btc-actions.js @@ -0,0 +1,88 @@ +import baseActions from '../btc-base/btc-base-actions' +import BtcApi from '../../../lib/bitcoin/bitcoin-api' + +const TX_CHUNK_SIZE = 25 + +const customActions = getApi => ({ + updateStatus (context) { + const api = getApi() + + if (!api) return + api.getBalance().then(balance => context.commit('status', { balance })) + + // The unspent transactions are needed to estimate the fee + api.getUnspents().then(utxo => context.commit('utxo', utxo)) + + // The estimated fee rate is also needed + api.getFeeRate().then(rate => context.commit('feeRate', rate)) + + // Last block height + context.dispatch('updateHeight') + }, + + updateHeight ({ commit }) { + const api = getApi() + if (!api) return + api.getHeight().then(height => commit('height', height)) + }, + + /** + * Updates the transaction details + * @param {{ dispatch: function, getters: object }} param0 Vuex context + * @param {{hash: string}} payload action payload + */ + updateTransaction ({ dispatch, getters }, payload) { + const tx = getters['transaction'](payload.hash) + + if (tx && (tx.status === 'SUCCESS' || tx.status === 'ERROR')) { + // If transaction is in one of the final statuses (either succeded or failed), + // just update the current height to recalculate its confirmations counter. + return dispatch('updateHeight') + } else { + // Otherwise fetch the transaction details + return dispatch('getTransaction', payload) + } + } +}) + +const retrieveNewTransactions = async (api, context, latestTxId, toTx) => { + const transactions = await api.getTransactions({ toTx }) + context.commit('transactions', transactions) + + if (latestTxId && !transactions.some(x => x.txid === latestTxId)) { + const oldest = transactions[transactions.length - 1] + await getNewTransactions(api, context, latestTxId, oldest && oldest.txid) + } +} + +const getNewTransactions = async (api, context) => { + // Determine the most recent transaction ID + const latestTransaction = context.getters['sortedTransactions'][0] + const latestId = latestTransaction && latestTransaction.txid + // Now fetch the transactions until we meet that latestId among the + // retrieved results + await retrieveNewTransactions(api, context, latestId) +} + +const getOldTransactions = async (api, context) => { + const transactions = context.getters['sortedTransactions'] + const oldestTx = transactions[transactions.length - 1] + const toTx = oldestTx && oldestTx.txid + + const chunk = await api.getTransactions({ toTx }) + context.commit('transactions', chunk) + + if (chunk.length < TX_CHUNK_SIZE) { + context.commit('bottom') + } +} + +export default { + ...baseActions({ + apiCtor: BtcApi, + getOldTransactions, + getNewTransactions, + customActions, + fetchRetryTimeout: 60 * 1000 + }) +} diff --git a/src/store/modules/btc/btc-getters.js b/src/store/modules/btc/btc-getters.js new file mode 100644 index 000000000..337344da2 --- /dev/null +++ b/src/store/modules/btc/btc-getters.js @@ -0,0 +1,33 @@ +import baseGetters from '../btc-base/btc-base-getters' +import BigNumber from '../../../lib/bignumber' +import { CryptoAmountPrecision } from '../../../lib/constants' + +const MULTIPLIER = 1e8 + +export default { + ...baseGetters, + + fee: state => amount => { + if (!state.utxo || !state.utxo.length || !state.feeRate) return 0 + + const target = BigNumber(amount).times(MULTIPLIER).toNumber() + + const calculation = state.utxo.reduce((res, item) => { + if ((res.fee + target) > res.total) { + res.total += item.amount + res.count += 1 + + // Estimated tx size is: ins * 180 + outs * 34 + 10 (https://news.bitcoin.com/how-to-calculate-bitcoin-transaction-fees-when-youre-in-a-hurry/) + // We assume that there're always 2 outputs: transfer target and the remains + res.fee = (res.count * 181 + 78) * state.feeRate + } + return res + }, { total: 0, count: 0, fee: 0 }) + + return BigNumber(calculation.fee).div(MULTIPLIER).decimalPlaces(CryptoAmountPrecision.BTC, 6) + }, + + height (state) { + return state.height + } +} diff --git a/src/store/modules/btc/btc-mutations.js b/src/store/modules/btc/btc-mutations.js new file mode 100644 index 000000000..61e218aef --- /dev/null +++ b/src/store/modules/btc/btc-mutations.js @@ -0,0 +1,18 @@ +import baseMutations from '../btc-base/btc-base-mutations' +import state from './btc-state' + +export default { + ...baseMutations(state), + + utxo (state, utxo = []) { + state.utxo = utxo + }, + + feeRate (state, feeRate = 0) { + state.feeRate = feeRate + }, + + height (state, height) { + state.height = height + } +} diff --git a/src/store/modules/btc/btc-state.js b/src/store/modules/btc/btc-state.js new file mode 100644 index 000000000..6499a2311 --- /dev/null +++ b/src/store/modules/btc/btc-state.js @@ -0,0 +1,10 @@ +import baseState from '../btc-base/btc-base-state' +import { Cryptos } from '../../../lib/constants' + +export default () => ({ + crypto: Cryptos.BTC, + ...baseState(), + utxo: [], + feeRate: 0, + height: 0 +}) diff --git a/src/store/modules/btc/index.js b/src/store/modules/btc/index.js new file mode 100644 index 000000000..ffde37626 --- /dev/null +++ b/src/store/modules/btc/index.js @@ -0,0 +1,12 @@ +import actions from './btc-actions' +import getters from './btc-getters' +import mutations from './btc-mutations' +import state from './btc-state' + +export default { + namespaced: true, + state, + actions, + getters, + mutations +} diff --git a/src/store/modules/dash/dash-actions.js b/src/store/modules/dash/dash-actions.js index de0653ada..2eeb4a230 100644 --- a/src/store/modules/dash/dash-actions.js +++ b/src/store/modules/dash/dash-actions.js @@ -20,6 +20,7 @@ export default { ...baseActions({ apiCtor: DashApi, getOldTransactions: getTransactions, - getNewTransactions: getTransactions + getNewTransactions: getTransactions, + fetchRetryTimeout: 30 * 1000 }) } diff --git a/src/store/modules/dash/dash-getters.js b/src/store/modules/dash/dash-getters.js index 1242e741e..889c113a7 100644 --- a/src/store/modules/dash/dash-getters.js +++ b/src/store/modules/dash/dash-getters.js @@ -4,7 +4,5 @@ import { TX_FEE } from '../../../lib/bitcoin/dash-api' export default { ...baseGetters, - fee () { - return TX_FEE - } + fee: state => amount => TX_FEE } diff --git a/src/store/modules/doge/doge-actions.js b/src/store/modules/doge/doge-actions.js index 12e370bf8..b94bb461a 100644 --- a/src/store/modules/doge/doge-actions.js +++ b/src/store/modules/doge/doge-actions.js @@ -21,6 +21,7 @@ export default { ...baseActions({ apiCtor: DogeApi, getOldTransactions, - getNewTransactions + getNewTransactions, + fetchRetryTimeout: 30 * 1000 }) } diff --git a/src/store/modules/doge/doge-getters.js b/src/store/modules/doge/doge-getters.js index a1db8aa20..1e3a96794 100644 --- a/src/store/modules/doge/doge-getters.js +++ b/src/store/modules/doge/doge-getters.js @@ -4,7 +4,5 @@ import baseGetters from '../btc-base/btc-base-getters' export default { ...baseGetters, - fee () { - return TX_FEE - } + fee: state => amount => TX_FEE } diff --git a/src/store/modules/erc20/erc20-actions.js b/src/store/modules/erc20/erc20-actions.js index f94697423..8d0834716 100644 --- a/src/store/modules/erc20/erc20-actions.js +++ b/src/store/modules/erc20/erc20-actions.js @@ -64,7 +64,7 @@ const createSpecificActions = (api, queue) => ({ balance => context.commit('balance', Number( ethUtils.toFraction(balance.toString(10), context.state.decimals) )), - error => console.warn(`${context.state.crypto} balance failed: `, error) + () => { } // Not this time ) .then(() => { const delay = Math.max(0, STATUS_INTERVAL - Date.now() + lastStatusUpdate) diff --git a/src/store/modules/erc20/erc20-getters.js b/src/store/modules/erc20/erc20-getters.js index e89703282..62cbec7a3 100644 --- a/src/store/modules/erc20/erc20-getters.js +++ b/src/store/modules/erc20/erc20-getters.js @@ -7,9 +7,7 @@ export default { return rootGetters['eth/gasPrice'] }, - fee (state, getters) { - return calculateFee(ERC20_TRANSFER_GAS, getters.gasPrice) - }, + fee: (state, getters) => amount => calculateFee(ERC20_TRANSFER_GAS, getters.gasPrice), ...baseGetters } diff --git a/src/store/modules/eth-base/eth-base-actions.js b/src/store/modules/eth-base/eth-base-actions.js index 047c736d2..94ee48df3 100644 --- a/src/store/modules/eth-base/eth-base-actions.js +++ b/src/store/modules/eth-base/eth-base-actions.js @@ -5,12 +5,10 @@ import { toBuffer } from 'ethereumjs-util' import getEndpointUrl from '../../../lib/getEndpointUrl' import * as utils from '../../../lib/eth-utils' import { getTransactions } from '../../../lib/eth-index' +import * as tf from '../../../lib/transactionsFetching' -/** Max number of attempts to retrieve the transaction details */ -const MAX_ATTEMPTS = 5 -const NEW_TRANSACTION_TIMEOUT = 60 -const OLD_TRANSACTION_TIMEOUT = 5 - +/** Interval between attempts to fetch the registered tx details */ +const RETRY_TIMEOUT = 20 * 1000 const CHUNK_SIZE = 25 export default function createActions (config) { @@ -146,17 +144,6 @@ export default function createActions (config) { timestamp: block.timestamp * 1000 }]) } - if (!block && payload.attempt === MAX_ATTEMPTS) { - // Give up, if transaction could not be found after so many attempts - context.commit('transactions', [{ hash: transaction.hash, status: 'ERROR' }]) - } else if (err || !block) { - // In case of an error or a pending transaction fetch its receipt once again later - // Increment attempt counter, if no transaction was found so far - const newPayload = { ...payload, attempt: 1 + (payload.attempt || 0) } - - const timeout = payload.isNew ? NEW_TRANSACTION_TIMEOUT : OLD_TRANSACTION_TIMEOUT - setTimeout(() => context.dispatch('getBlock', newPayload), timeout * 1000) - } }) queue.enqueue('block:' + payload.blockNumber, supplier) @@ -189,18 +176,23 @@ export default function createActions (config) { if (transaction) { context.commit('transactions', [{ ...transaction, - status: 'PENDING' + status: 'REGISTERED' }]) // Fetch receipt details: status and actual gas consumption const { attempt, ...receiptPayload } = payload context.dispatch('getTransactionReceipt', receiptPayload) + + // Now we know that the transaction has been registered by the ETH network. + // Nothing else to do here, let's proceed to checking its status (see getTransactionReceipt) + return } } - if (!tx && payload.attempt === MAX_ATTEMPTS) { + + if (payload.attempt >= tf.PENDING_ATTEMPTS) { // Give up, if transaction could not be found after so many attempts context.commit('transactions', [{ hash: payload.hash, status: 'ERROR' }]) - } else if (err || !tx) { + } else { // In case of an error or a pending transaction fetch its details once again later // Increment attempt counter, if no transaction was found so far const newPayload = tx ? payload : { @@ -209,8 +201,8 @@ export default function createActions (config) { force: true } - const timeout = payload.isNew ? NEW_TRANSACTION_TIMEOUT : OLD_TRANSACTION_TIMEOUT - setTimeout(() => context.dispatch('getTransaction', newPayload), timeout * 1000) + const timeout = tf.getPendingTxRetryTimeout(payload.timestamp || (existing && existing.timestamp)) + setTimeout(() => context.dispatch('getTransaction', newPayload), timeout) } }) @@ -229,36 +221,56 @@ export default function createActions (config) { const gasPrice = transaction.gasPrice const supplier = () => api.eth.getTransactionReceipt.request(payload.hash, (err, tx) => { + let replay = true + if (!err && tx) { - context.commit('transactions', [{ + const update = { hash: payload.hash, - fee: utils.calculateFee(tx.gasUsed, gasPrice), - status: Number(tx.status) ? 'SUCCESS' : 'ERROR', - blockNumber: tx.blockNumber - }]) + fee: utils.calculateFee(tx.gasUsed, gasPrice) + } - context.dispatch('getBlock', { - ...payload, - attempt: 0, - blockNumber: tx.blockNumber - }) + if (Number(tx.status) === 0) { + // Status "0x0" means that the transaction has been rejected + update.status = 'ERROR' + } else if (tx.blockNumber) { + // If blockNumber is not null, the transaction is confirmed + update.status = 'SUCCESS' + update.blockNumber = tx.blockNumber + } + + context.commit('transactions', [update]) + + if (tx.blockNumber) { + context.dispatch('getBlock', { + ...payload, + blockNumber: tx.blockNumber + }) + } + + // Re-fetch tx details if it's status is still unknown + replay = !update.status } - if (!tx && payload.attempt === MAX_ATTEMPTS) { - // Give up, if transaction could not be found after so many attempts - context.commit('transactions', [{ hash: tx.hash, status: 'ERROR' }]) - } else if (err || !tx || !tx.status) { + + if (replay) { // In case of an error or a pending transaction fetch its receipt once again later // Increment attempt counter, if no transaction was found so far const newPayload = { ...payload, attempt: 1 + (payload.attempt || 0) } - - const timeout = payload.isNew ? NEW_TRANSACTION_TIMEOUT : OLD_TRANSACTION_TIMEOUT - setTimeout(() => context.dispatch('getTransactionReceipt', newPayload), timeout * 1000) + setTimeout(() => context.dispatch('getTransactionReceipt', newPayload), RETRY_TIMEOUT) } }) queue.enqueue('transactionReceipt:' + payload.hash, supplier) }, + /** + * Updates the transaction details + * @param {{ dispatch: function }} param0 Vuex context + * @param {{hash: string}} payload action payload + */ + updateTransaction ({ dispatch }, payload) { + return dispatch('getTransaction', payload) + }, + getNewTransactions (context, payload) { const { address, maxHeight, contractAddress, decimals } = context.state diff --git a/src/store/modules/eth/getters.js b/src/store/modules/eth/getters.js index aad43c15e..1a8007d22 100644 --- a/src/store/modules/eth/getters.js +++ b/src/store/modules/eth/getters.js @@ -7,9 +7,7 @@ export default { return state.gasPrice || DEFAULT_GAS_PRICE }, - fee (state) { - return state.fee - }, + fee: state => amount => state.fee, privateKey: state => state.privateKey, diff --git a/src/views/Login.vue b/src/views/Login.vue index 85e807441..0f2c6ce7b 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -104,9 +104,7 @@ import FileIcon from '@/components/icons/common/File' import LoginPasswordForm from '@/components/LoginPasswordForm' import Logo from '@/components/icons/common/Logo' import AppInterval from '@/lib/AppInterval' -import { Cryptos } from '@/lib/constants' -import { parseURI } from '@/lib/uri' -import validateAddress from '@/lib/validateAddress' +import { navigateByURI } from '@/router/navigationGuard' export default { computed: { @@ -135,21 +133,13 @@ export default { console.warn(err) }, onLogin () { - const contact = parseURI() - if (!this.$store.state.chat.isFulfilled) { this.$store.commit('chat/createAdamantChats') this.$store.dispatch('chat/loadChats') .then(() => AppInterval.subscribe()) } else AppInterval.subscribe() - if (contact.address) { - this.navigateByContact( - contact.params.message, - contact.address, - contact.params.label - ) - } else this.$router.push('/chats') + navigateByURI() }, onLoginError (key) { this.$store.dispatch('snackbar/show', { @@ -165,39 +155,6 @@ export default { onScanQrcode (passphrase) { this.passphrase = passphrase this.$nextTick(() => this.$refs.loginForm.submit()) - }, - - /** - * Navigate to view depending on a partner address validity - * @param {string} messageText Text of message to place in textarea - * @param {string} partnerId Partner address to open chat with - * @param {string} partnerName Predefined partner name from label - */ - navigateByContact (messageText, partnerId, partnerName) { - if (validateAddress(Cryptos.ADM, partnerId)) { - this.$store.dispatch('chat/createChat', { partnerId, partnerName }) - .then(key => this.$router.push({ - name: 'Chat', - params: { messageText, partnerId } - })) - .catch(x => { - this.$router.push({ - name: 'Chats', - params: { partnerId, showNewContact: true } - }) - this.$store.dispatch('snackbar/show', { - message: x.message - }) - }) - } else { - this.$router.push({ - name: 'Chats', - params: { partnerId, showNewContact: false } - }) - this.$store.dispatch('snackbar/show', { - message: this.$t('chats.incorrect_address', { crypto: Cryptos.ADM }) - }) - } } }, components: { diff --git a/src/views/transactions/Transaction.vue b/src/views/transactions/Transaction.vue index 51a1c3b24..f9b38a727 100644 --- a/src/views/transactions/Transaction.vue +++ b/src/views/transactions/Transaction.vue @@ -38,7 +38,7 @@ export default { }, methods: { update () { - const action = this.crypto.toLowerCase() + '/getTransaction' + const action = this.crypto.toLowerCase() + '/updateTransaction' this.$store.dispatch(action, { hash: this.txId }) } }, diff --git a/yarn.lock b/yarn.lock index 7b6d2bfd0..071451398 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3096,10 +3096,10 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= -coininfo@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/coininfo/-/coininfo-4.3.0.tgz#5795c842c3563510c9ddbf3ec5607f4152a80562" - integrity sha512-y05vuWLvtIHkCdREhSbopaSzghsjukfJHkokcFGDkGOxL/wqEFokNHqDQS9CSag0tL/Oo7gMeGyUmeD3+MoyUA== +coininfo@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/coininfo/-/coininfo-5.1.0.tgz#512b850d867e49afe55f15322e891f34ab6b49bd" + integrity sha512-q1Bv+yYSca68VpXGvaMwO0xzEJl9Zxxk0g2YWfX8EUHld8t7e3xEnaRa473IqNVWBq9CAwiXzkYR9UCCtUZVrA== dependencies: safe-buffer "^5.1.1"