From 809defc6efb36f411364a89e883f4f98f51e6db3 Mon Sep 17 00:00:00 2001 From: bludnic Date: Fri, 30 Aug 2019 17:58:35 +0300 Subject: [PATCH 01/35] refactor(intervals): move intervals to Vuex * build(package): remove rxjs dependency --- package.json | 1 - src/App.vue | 3 +-- src/lib/AppInterval.js | 28 ---------------------------- src/store/index.js | 22 ++++++++++++++++++++++ src/store/modules/chat/index.js | 22 ++++++++++++++++++++++ src/store/plugins/indexedDb.js | 5 ++--- src/views/Login.vue | 5 ++--- src/views/Options.vue | 3 +-- yarn.lock | 7 ------- 9 files changed, 50 insertions(+), 46 deletions(-) delete mode 100644 src/lib/AppInterval.js diff --git a/package.json b/package.json index 8b78a9d9e..ff47eb09a 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "qrcode": "^1.3.2", "qs": "^6.6.0", "register-service-worker": "^1.0.0", - "rxjs": "^6.3.3", "semver": "^5.5.1", "simple-audio": "^1.0.1", "sodium-browserify-tweetnacl": "^0.2.3", diff --git a/src/App.vue b/src/App.vue index a56f1f046..f07d55202 100644 --- a/src/App.vue +++ b/src/App.vue @@ -10,7 +10,6 @@ import dayjs from 'dayjs' import Notifications from '@/lib/notifications' -import AppInterval from '@/lib/AppInterval' export default { created () { @@ -22,7 +21,7 @@ export default { }, beforeDestroy () { this.notifications.stop() - AppInterval.unsubscribe() + this.$store.dispatch('stopInterval') }, computed: { layout () { diff --git a/src/lib/AppInterval.js b/src/lib/AppInterval.js deleted file mode 100644 index ecb68edc1..000000000 --- a/src/lib/AppInterval.js +++ /dev/null @@ -1,28 +0,0 @@ -import { interval, from, of } from 'rxjs' -import { mergeMap, catchError } from 'rxjs/operators' - -import store from '@/store' - -export default { - messageInterval: interval(3000) - .pipe(mergeMap(() => - from( - store.dispatch('chat/getNewMessages') - ).pipe(catchError(err => of(err.message))) - )), - accountInterval: interval(20000), - messageSubscription: null, - accountSubscription: null, - - subscribe () { - this.messageSubscription = this.messageInterval.subscribe(() => {}) - this.accountSubscription = this.accountInterval.subscribe(() => { - store.dispatch('updateBalance') - }) - }, - - unsubscribe () { - this.messageSubscription && this.messageSubscription.unsubscribe() - this.accountSubscription && this.accountSubscription.unsubscribe() - } -} diff --git a/src/store/index.js b/src/store/index.js index 158e03612..ca20a3944 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -34,6 +34,8 @@ import notification from './modules/notification' Vue.use(Vuex) +export let interval + const store = { state: () => ({ address: '', @@ -151,6 +153,26 @@ const store = { flushCryptoAddresses() } }) + }, + + startInterval: { + root: true, + handler ({ dispatch }) { + function repeat () { + dispatch('updateBalance') + .catch(err => console.error(err)) + .then(() => (interval = setTimeout(repeat, 20000))) + } + + repeat() + } + }, + + stopInterval: { + root: true, + handler () { + clearTimeout(interval) + } } }, plugins: [nodesPlugin, sessionStoragePlugin, localStoragePlugin, indexedDbPlugin, navigatorOnline], diff --git a/src/store/modules/chat/index.js b/src/store/modules/chat/index.js index 1e64ae997..a3d24509d 100644 --- a/src/store/modules/chat/index.js +++ b/src/store/modules/chat/index.js @@ -12,6 +12,8 @@ import { import { isNumeric } from '@/lib/numericHelpers' import { EPOCH, Cryptos, TransactionStatus as TS } from '@/lib/constants' +export let interval + /** * type State { * chats: { @@ -653,6 +655,26 @@ const actions = { return transactionObject.id }, + startInterval: { + root: true, + handler ({ dispatch }) { + function repeat () { + dispatch('getNewMessages') + .catch(err => console.error(err)) + .then(() => (interval = setTimeout(repeat, 3000))) + } + + repeat() + } + }, + + stopInterval: { + root: true, + handler () { + clearTimeout(interval) + } + }, + /** Resets module state **/ reset: { root: true, diff --git a/src/store/plugins/indexedDb.js b/src/store/plugins/indexedDb.js index e6deed495..1c178419b 100644 --- a/src/store/plugins/indexedDb.js +++ b/src/store/plugins/indexedDb.js @@ -5,7 +5,6 @@ import { Base64 } from 'js-base64' import router from '@/router' import { Modules, Chats, Security, clearDb } from '@/lib/idb' import { restoreState, modules } from '@/lib/idb/state' -import AppInterval from '@/lib/AppInterval' const chatModuleMutations = ['setHeight', 'setFulfilled'] const multipleChatMutations = ['markAllAsRead', 'createEmptyChat', 'createAdamantChats'] @@ -107,7 +106,7 @@ export default store => { } }) .then(() => { - AppInterval.subscribe() + store.dispatch('startInterval') }) .catch(() => { console.error('Can not decode IDB with current password. Fallback to Login via Passphrase.') @@ -132,7 +131,7 @@ export default store => { store.dispatch('unlock') store.commit('chat/createAdamantChats') store.dispatch('chat/loadChats') - .then(() => AppInterval.subscribe()) + .then(() => store.dispatch('startInterval')) store.dispatch('afterLogin', Base64.decode(store.state.passphrase)) } diff --git a/src/views/Login.vue b/src/views/Login.vue index 59802c014..36d8a991c 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -103,7 +103,6 @@ import QrCodeScanIcon from '@/components/icons/common/QrCodeScan' import FileIcon from '@/components/icons/common/File' import LoginPasswordForm from '@/components/LoginPasswordForm' import Logo from '@/components/icons/common/Logo' -import AppInterval from '@/lib/AppInterval' export default { computed: { @@ -137,9 +136,9 @@ export default { if (!this.$store.state.chat.isFulfilled) { this.$store.commit('chat/createAdamantChats') this.$store.dispatch('chat/loadChats') - .then(() => AppInterval.subscribe()) + .then(() => this.$store.dispatch('startInterval')) } else { - AppInterval.subscribe() + this.$store.dispatch('startInterval') } }, onLoginError (key) { diff --git a/src/views/Options.vue b/src/views/Options.vue index cd42d4f3b..3318bffed 100644 --- a/src/views/Options.vue +++ b/src/views/Options.vue @@ -205,7 +205,6 @@ import LanguageSwitcher from '@/components/LanguageSwitcher' import AppToolbarCentered from '@/components/AppToolbarCentered' import PasswordSetDialog from '@/components/PasswordSetDialog' import { clearDb, db as isIDBSupported } from '@/lib/idb' -import AppInterval from '@/lib/AppInterval' import scrollPosition from '@/mixins/scrollPosition' export default { @@ -318,7 +317,7 @@ export default { } }, logout () { - AppInterval.unsubscribe() + this.$store.dispatch('stopInterval') this.$store.dispatch('logout') if (this.isLoginViaPassword) { diff --git a/yarn.lock b/yarn.lock index dde3b457c..7b1b41404 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9934,13 +9934,6 @@ rxjs@^5.0.0-beta.11: dependencies: symbol-observable "1.0.1" -rxjs@^6.3.3: - version "6.3.3" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.3.tgz#3c6a7fa420e844a81390fb1158a9ec614f4bad55" - integrity sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw== - dependencies: - tslib "^1.9.0" - safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" From f5c72bf862b84b232ea8cc4c7d1d84a2faaeaa01 Mon Sep 17 00:00:00 2001 From: bludnic Date: Wed, 4 Sep 2019 16:42:23 +0300 Subject: [PATCH 02/35] feat: add socket support --- package.json | 1 + src/i18n/en.json | 7 +- src/i18n/ru.json | 6 +- src/lib/adamant-api-client.js | 12 ++- src/lib/adamant-api.js | 2 +- src/lib/chatHelpers.js | 2 +- src/lib/sockets.js | 53 +++++++++ src/store/index.js | 3 +- src/store/modules/chat/index.js | 10 +- src/store/modules/options/index.js | 3 +- src/store/plugins/localStorage.js | 3 +- src/store/plugins/socketsPlugin.js | 41 +++++++ src/views/Nodes.vue | 29 +++++ yarn.lock | 166 ++++++++++++++++++++++++++++- 14 files changed, 323 insertions(+), 15 deletions(-) create mode 100644 src/lib/sockets.js create mode 100644 src/store/plugins/socketsPlugin.js diff --git a/package.json b/package.json index ff47eb09a..63c381158 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "register-service-worker": "^1.0.0", "semver": "^5.5.1", "simple-audio": "^1.0.1", + "socket.io-client": "^2.2.0", "sodium-browserify-tweetnacl": "^0.2.3", "throttle-promise": "^1.0.4", "underscore": "^1.9.1", diff --git a/src/i18n/en.json b/src/i18n/en.json index f3f6408fe..8ca11ec23 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -34,7 +34,7 @@ "start_chat": "Start a chat", "title": "Chats", "too_long": "The message is too long", - "transaction_statuses": { + "transaction_statuses": { "delivered": "Confirmed in token’s blockchain", "invalid": "Incorrect information. Check transaction in token's blockchain carefully", "pending": "Not confirmed yet", @@ -117,7 +117,10 @@ "nodeLabelDescription": "Support decentralization and enhance privacy level—run your own ADAMANT node. To add your nodes to the list, you need to deploy web application on separate domain. This limitation refers to Content Security Policy (CSP).", "offline": "Offline", "ping": "Ping", - "unsupported": "Unsupported" + "socket": "Socket", + "unsupported": "Unsupported", + "use_socket_connection": "Use socket connection by default", + "use_socket_connection_tooltip": "" }, "notifications": { "tabMessage": { diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 8f377433a..d689f3a74 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -118,7 +118,11 @@ "ms": "мс", "nodeLabelDescription": "Поддержите децентрализацию и повысьте уровень приватности — установите свой узел (ноду) АДАМАНТа. Чтобы добавить свои узлы в список для подключения, вам нужно развернуть свое веб-приложение на отдельном домене. Это ограничение связано с политикой безопасности Content Security Policy (CSP).", "offline": "Недоступна", - "ping": "Пинг" + "ping": "Пинг", + "socket": "Сокеты", + "unsupported": "Не поддерживается", + "use_socket_connection": "Использовать сокеты по умолчанию", + "use_socket_connection_tooltip": "" }, "notifications": { "tabMessage": { diff --git a/src/lib/adamant-api-client.js b/src/lib/adamant-api-client.js index d0067e889..7810156a5 100644 --- a/src/lib/adamant-api-client.js +++ b/src/lib/adamant-api-client.js @@ -59,6 +59,7 @@ class ApiNode { this._timeDelta = 0 this._version = '' this._height = 0 + this._socketSupport = false this._client = axios.create({ baseURL: this._baseUrl @@ -113,6 +114,10 @@ class ApiNode { return this._height } + get socketSupport () { + return this._socketSupport + } + /** * Performs an API request. * @@ -168,6 +173,7 @@ class ApiNode { this._height = status.height this._ping = status.ping this._online = true + this._socketSupport = status.socketSupport }) .catch(() => { this._online = false @@ -187,7 +193,8 @@ class ApiNode { return { version: res.version.version, height: Number(res.network.height), - ping: Date.now() - time + ping: Date.now() - time, + socketSupport: res.wsClient && res.wsClient.enabled } } @@ -239,7 +246,8 @@ class ApiClient { version: node.version, active: node.active, outOfSync: node.outOfSync, - hasMinApiVersion: node.version >= this._minApiVersion + hasMinApiVersion: node.version >= this._minApiVersion, + socketSupport: node.socketSupport }) this._onInit = null diff --git a/src/lib/adamant-api.js b/src/lib/adamant-api.js index ec65cf396..95bce4a16 100644 --- a/src/lib/adamant-api.js +++ b/src/lib/adamant-api.js @@ -423,7 +423,7 @@ export function getChats (from = 0, offset = 0, orderBy = 'desc') { * @param {Buffer} key sender public key * @returns {{senderId: string, asset: object, message: any, isI18n: boolean}} */ -function decodeChat (transaction, key) { +export function decodeChat (transaction, key) { const chat = transaction.asset.chat const message = utils.decodeMessage(chat.message, key, myKeypair.privateKey, chat.own_message) diff --git a/src/lib/chatHelpers.js b/src/lib/chatHelpers.js index 4bfaccbf0..b9cb10d1e 100644 --- a/src/lib/chatHelpers.js +++ b/src/lib/chatHelpers.js @@ -185,7 +185,7 @@ export function transformMessage (abstract) { transaction.recipientId = abstract.recipientId transaction.admTimestamp = abstract.timestamp transaction.timestamp = getRealTimestamp(abstract.timestamp) - transaction.status = abstract.status || TS.DELIVERED + transaction.status = abstract.status || abstract.height ? TS.DELIVERED : TS.PENDING transaction.i18n = !!abstract.i18n transaction.amount = abstract.amount ? abstract.amount : 0 transaction.message = '' diff --git a/src/lib/sockets.js b/src/lib/sockets.js new file mode 100644 index 000000000..e04347e6b --- /dev/null +++ b/src/lib/sockets.js @@ -0,0 +1,53 @@ +import io from 'socket.io-client' +import random from 'lodash/random' + +import config from '@/config' + +export class SocketClient { + /** + * @param {string[]} nodes Array of nodes URLs + */ + constructor (nodes) { + this.nodes = nodes + } + + /** + * Get random socket address. + * @returns {string} + */ + get socketAddress () { + return this.nodes[random(this.nodes.length - 1)] + } + + on (event, fn) { + this.connection.on(event, fn) + } + + connect (adamantAddress) { + this.connection = io(this.socketAddress, { reconnection: false, timeout: 5000 }) + + this.connection.on('connect', () => { + this.connection.emit('msg', adamantAddress + ' connected!') + this.connection.emit('address', adamantAddress) + }) + + this.connection.on('disconnect', reason => { + if (reason === 'ping timeout' || reason === 'io server disconnect') { + this.connect(adamantAddress) + } + }) + + this.connection.on('connect_error', (err) => { + console.warn('connect_error', err) + setTimeout(() => this.connect(adamantAddress), 5000) + }) + } + + disconnect () { + this.connection && this.connection.close() + } +} + +const nodes = config.server.adm.map(node => node.url.replace(/^https?:\/\/(.*)$/, 'wss://$1')) + +export default new SocketClient(nodes) diff --git a/src/store/index.js b/src/store/index.js index ca20a3944..10bcb05b4 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -16,6 +16,7 @@ import sessionStoragePlugin from './plugins/sessionStorage' import localStoragePlugin from './plugins/localStorage' import indexedDbPlugin from './plugins/indexedDb' import navigatorOnline from './plugins/navigatorOnline' +import socketsPlugin from './plugins/socketsPlugin' import ethModule from './modules/eth' import erc20Module from './modules/erc20' import partnersModule from './modules/partners' @@ -175,7 +176,7 @@ const store = { } } }, - plugins: [nodesPlugin, sessionStoragePlugin, localStoragePlugin, indexedDbPlugin, navigatorOnline], + plugins: [nodesPlugin, sessionStoragePlugin, localStoragePlugin, indexedDbPlugin, navigatorOnline, socketsPlugin], modules: { eth: ethModule, // Ethereum-related data bnb: erc20Module(Cryptos.BNB, '0xB8c77482e45F1F44dE1745F52C74426C631bDD52', 18), diff --git a/src/store/modules/chat/index.js b/src/store/modules/chat/index.js index a3d24509d..4690b3d69 100644 --- a/src/store/modules/chat/index.js +++ b/src/store/modules/chat/index.js @@ -291,10 +291,11 @@ const mutations = { const chat = state.chats[partnerId] // Shouldn't duplicate local messages added directly - // when dispatch('getNewMessages'). Just update `status`. + // when dispatch('getNewMessages'). Just update `status, height`. const localMessage = chat.messages.find(localMessage => localMessage.id === message.id) if (localMessage) { // is message in state localMessage.status = message.status + localMessage.height = message.height return } @@ -657,11 +658,14 @@ const actions = { startInterval: { root: true, - handler ({ dispatch }) { + handler ({ dispatch, rootState }) { function repeat () { dispatch('getNewMessages') .catch(err => console.error(err)) - .then(() => (interval = setTimeout(repeat, 3000))) + .then(() => { + const timeout = rootState.options.useSocketConnection ? 60000 : 3000 + interval = setTimeout(repeat, timeout) + }) } repeat() diff --git a/src/store/modules/options/index.js b/src/store/modules/options/index.js index 20c8ccf89..5b447b83d 100644 --- a/src/store/modules/options/index.js +++ b/src/store/modules/options/index.js @@ -8,7 +8,8 @@ const state = () => ({ allowPushNotifications: false, darkTheme: false, formatMessages: true, - currentWallet: Cryptos.ADM // current Wallet Tab on Account view (this is not an option) + currentWallet: Cryptos.ADM, // current Wallet Tab on Account view (this is not an option) + useSocketConnection: true }) const getters = { diff --git a/src/store/plugins/localStorage.js b/src/store/plugins/localStorage.js index af11130b9..fda6355e0 100644 --- a/src/store/plugins/localStorage.js +++ b/src/store/plugins/localStorage.js @@ -14,7 +14,8 @@ const vuexPersistence = new VuexPersistence({ allowTabNotifications: state.options.allowTabNotifications, allowPushNotifications: state.options.allowPushNotifications, darkTheme: state.options.darkTheme, - formatMessages: state.options.formatMessages + formatMessages: state.options.formatMessages, + useSocketConnection: state.options.useSocketConnection } } } diff --git a/src/store/plugins/socketsPlugin.js b/src/store/plugins/socketsPlugin.js new file mode 100644 index 000000000..00d358d1a --- /dev/null +++ b/src/store/plugins/socketsPlugin.js @@ -0,0 +1,41 @@ +import socketClient from '@/lib/sockets' +import { decodeChat, getPublicKey } from '@/lib/adamant-api' + +function openSocketConnection (adamantAddress, store) { + if (!store.state.options.useSocketConnection) return + if (socketClient.connected) return + + socketClient.connect(adamantAddress) + socketClient.on('newTrans', transaction => { + const promise = (transaction.recipientId === store.state.address) + ? Promise.resolve(transaction.senderPublicKey) + : getPublicKey(transaction.recipientId) + + promise.then(publicKey => { + const decoded = decodeChat(transaction, publicKey) + store.dispatch('chat/pushMessages', [decoded]) + }) + }) +} + +export default store => { + // open socket connection when chats are loaded + store.watch(() => store.state.chat.isFulfilled, value => { + if (value) openSocketConnection(store.state.address, store) + }) + + // when logout or update `useSocketConnection` option + store.subscribe((mutation, state) => { + if (mutation.type === 'reset') { + socketClient.disconnect() + } + + if (mutation.type === 'options/updateOption' && mutation.payload.key === 'useSocketConnection') { + if (mutation.payload.value) { + openSocketConnection(state.address, store) + } else { + socketClient.disconnect() + } + } + }) +} diff --git a/src/views/Nodes.vue b/src/views/Nodes.vue index 0d9198989..27959382e 100644 --- a/src/views/Nodes.vue +++ b/src/views/Nodes.vue @@ -57,6 +57,11 @@ size="small" >mdi-checkbox-blank-circle + + + {{ props.item.socketSupport ? 'mdi-check' : 'mdi-close' }} + + @@ -68,6 +73,14 @@ />
{{ $t('nodes.fastest_tooltip') }}
+ +
{{ $t('nodes.use_socket_connection_tooltip') }}
+
'nodes-view', + useSocketConnection: { + get () { + return this.$store.state.options.useSocketConnection + }, + set (value) { + this.$store.commit('options/updateOption', { + key: 'useSocketConnection', + value + }) + } + }, preferFastestNodeOption: { get () { return this.$store.state.nodes.useFastest @@ -123,6 +147,11 @@ export default { text: 'nodes.ping', value: 'ping', align: 'left' + }, + { + text: 'nodes.socket', + value: 'socket', + align: 'left' } ], timer: null diff --git a/yarn.lock b/yarn.lock index 7b1b41404..433bc3d91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1440,6 +1440,11 @@ address@^1.0.3: resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9" integrity sha512-z55ocwKBRLryBs394Sm3ushTtBeg6VAeuku7utSoSnsJKvKcnXFIyC6vh27n3rXyxSgkJBBCAvyOn7gSUcTYjg== +after@0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" + integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= + ajv-errors@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" @@ -1739,6 +1744,11 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= +arraybuffer.slice@~0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" + integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog== + arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -2113,6 +2123,11 @@ babylon@^6.18.0: resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== +backo2@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" + integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -2125,6 +2140,11 @@ base-x@^3.0.2: dependencies: safe-buffer "^5.0.1" +base64-arraybuffer@0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" + integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= + base64-js@^1.0.2, base64-js@^1.2.3: version "1.3.0" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" @@ -2160,6 +2180,13 @@ bech32@^1.1.2: resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.3.tgz#bd47a8986bbb3eec34a56a097a84b8d3e9a2dfcd" integrity sha512-yuVFUvrNcoJi0sv5phmqc6P+Fl1HjRDRNOOkHY2X/3LBy2bIGNSFx4fZ95HMaXHupuS7cZR15AsvtmCIF4UEyg== +better-assert@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" + integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI= + dependencies: + callsite "1.0.0" + bezier-easing@^2.0.3: version "2.1.0" resolved "https://registry.yarnpkg.com/bezier-easing/-/bezier-easing-2.1.0.tgz#c04dfe8b926d6ecaca1813d69ff179b7c2025d86" @@ -2280,6 +2307,11 @@ bl@^1.0.0: readable-stream "^2.3.5" safe-buffer "^5.1.1" +blob@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" + integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig== + bluebird-lst@^1.0.6, bluebird-lst@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/bluebird-lst/-/bluebird-lst-1.0.7.tgz#f0babade9ef1dce3989b603f3796ff3b16b90d50" @@ -2704,6 +2736,11 @@ caller-path@^2.0.0: dependencies: caller-callsite "^2.0.0" +callsite@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" + integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA= + callsites@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" @@ -3129,11 +3166,21 @@ compare-version@^0.1.2: resolved "https://registry.yarnpkg.com/compare-version/-/compare-version-0.1.2.tgz#0162ec2d9351f5ddd59a9202cba935366a725080" integrity sha1-AWLsLZNR9d3VmpICy6k1NmpyUIA= -component-emitter@^1.2.1: +component-bind@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" + integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E= + +component-emitter@1.2.1, component-emitter@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= +component-inherit@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" + integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM= + compress-commons@^1.2.0: version "1.2.2" resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-1.2.2.tgz#524a9f10903f3a813389b0225d27c48bb751890f" @@ -3780,7 +3827,7 @@ debug@2.6.9, debug@^2.1.2, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6. dependencies: ms "2.0.0" -debug@3.1.0, debug@=3.1.0: +debug@3.1.0, debug@=3.1.0, debug@~3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== @@ -4328,6 +4375,34 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" +engine.io-client@~3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.3.2.tgz#04e068798d75beda14375a264bb3d742d7bc33aa" + integrity sha512-y0CPINnhMvPuwtqXfsGuWE8BB66+B6wTtCofQDRecMQPYX3MYUZXFNKDhdrSe3EVjgOu4V3rxdeqN/Tr91IgbQ== + dependencies: + component-emitter "1.2.1" + component-inherit "0.0.3" + debug "~3.1.0" + engine.io-parser "~2.1.1" + has-cors "1.1.0" + indexof "0.0.1" + parseqs "0.0.5" + parseuri "0.0.5" + ws "~6.1.0" + xmlhttprequest-ssl "~1.5.4" + yeast "0.1.2" + +engine.io-parser@~2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.1.3.tgz#757ab970fbf2dfb32c7b74b033216d5739ef79a6" + integrity sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA== + dependencies: + after "0.8.2" + arraybuffer.slice "~0.0.7" + base64-arraybuffer "0.1.5" + blob "0.0.5" + has-binary2 "~1.0.2" + enhanced-resolve@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" @@ -5592,6 +5667,18 @@ has-ansi@^2.0.0: dependencies: ansi-regex "^2.0.0" +has-binary2@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d" + integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw== + dependencies: + isarray "2.0.1" + +has-cors@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" + integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk= + has-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" @@ -6450,6 +6537,11 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= +isarray@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" + integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4= + isarray@^2.0.1: version "2.0.4" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.4.tgz#38e7bcbb0f3ba1b7933c86ba1894ddfc3781bbb7" @@ -8237,6 +8329,11 @@ object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= +object-component@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" + integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE= + object-copy@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" @@ -8591,6 +8688,20 @@ parse5@4.0.0: resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== +parseqs@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" + integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0= + dependencies: + better-assert "~1.0.0" + +parseuri@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" + integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo= + dependencies: + better-assert "~1.0.0" + parseurl@~1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" @@ -10265,6 +10376,35 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +socket.io-client@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.2.0.tgz#84e73ee3c43d5020ccc1a258faeeb9aec2723af7" + integrity sha512-56ZrkTDbdTLmBIyfFYesgOxsjcLnwAKoN4CiPyTVkMQj3zTUh0QAx3GbvIvLpFEOvQWu92yyWICxB0u7wkVbYA== + dependencies: + backo2 "1.0.2" + base64-arraybuffer "0.1.5" + component-bind "1.0.0" + component-emitter "1.2.1" + debug "~3.1.0" + engine.io-client "~3.3.1" + has-binary2 "~1.0.2" + has-cors "1.1.0" + indexof "0.0.1" + object-component "0.0.3" + parseqs "0.0.5" + parseuri "0.0.5" + socket.io-parser "~3.3.0" + to-array "0.1.4" + +socket.io-parser@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.0.tgz#2b52a96a509fdf31440ba40fed6094c7d4f1262f" + integrity sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng== + dependencies: + component-emitter "1.2.1" + debug "~3.1.0" + isarray "2.0.1" + sockjs-client@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.3.0.tgz#12fc9d6cb663da5739d3dc5fb6e8687da95cb177" @@ -11033,6 +11173,11 @@ tmpl@1.0.x: resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= +to-array@0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" + integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA= + to-arraybuffer@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" @@ -12250,6 +12395,13 @@ ws@^6.0.0: dependencies: async-limiter "~1.0.0" +ws@~6.1.0: + version "6.1.4" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9" + integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA== + dependencies: + async-limiter "~1.0.0" + xdg-basedir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" @@ -12275,6 +12427,11 @@ xmldom@0.1.x: resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk= +xmlhttprequest-ssl@~1.5.4: + version "1.5.5" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" + integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4= + xmlhttprequest@*: version "1.8.0" resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" @@ -12438,6 +12595,11 @@ yauzl@2.8.0: buffer-crc32 "~0.2.3" fd-slicer "~1.0.1" +yeast@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" + integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= + yorkie@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/yorkie/-/yorkie-2.0.0.tgz#92411912d435214e12c51c2ae1093e54b6bb83d9" From da39d254ab1c1adfb13e880a9858596b5222e55d Mon Sep 17 00:00:00 2001 From: bludnic Date: Wed, 4 Sep 2019 16:44:41 +0300 Subject: [PATCH 03/35] fix(AChatMessage): display status for incoming messages Because sockets returns unconfirmed transactions. --- src/components/AChat/AChatMessage.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AChat/AChatMessage.vue b/src/components/AChat/AChatMessage.vue index 6fa179fb0..057ed0a17 100644 --- a/src/components/AChat/AChatMessage.vue +++ b/src/components/AChat/AChatMessage.vue @@ -22,7 +22,7 @@
{{ time }}
-
+
Date: Wed, 4 Sep 2019 18:24:38 +0300 Subject: [PATCH 04/35] fix(Chat Vuex): mark unconfirmed transactions as new (socket) --- src/store/modules/chat/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/store/modules/chat/index.js b/src/store/modules/chat/index.js index 4690b3d69..9795a692b 100644 --- a/src/store/modules/chat/index.js +++ b/src/store/modules/chat/index.js @@ -315,8 +315,10 @@ const mutations = { // Exception only when `height = 0`, this means that the // user cleared `localStorage` or logged in first time. if ( - message.height > state.lastMessageHeight && - state.lastMessageHeight > 0 && + ( + message.height === undefined || // unconfirmed transaction (socket) + (message.height > state.lastMessageHeight && state.lastMessageHeight > 0) + ) && userId !== message.senderId // do not notify yourself when send message from other device ) { chat.numOfNewMessages += 1 From 5b5f06aa63d0a54788a17f61e36870fa7a41e7b1 Mon Sep 17 00:00:00 2001 From: bludnic Date: Wed, 4 Sep 2019 19:26:02 +0300 Subject: [PATCH 05/35] fix(socketsPlugin): do not decode transactions with type 0 --- src/store/plugins/socketsPlugin.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/store/plugins/socketsPlugin.js b/src/store/plugins/socketsPlugin.js index 00d358d1a..718e1bb85 100644 --- a/src/store/plugins/socketsPlugin.js +++ b/src/store/plugins/socketsPlugin.js @@ -12,7 +12,9 @@ function openSocketConnection (adamantAddress, store) { : getPublicKey(transaction.recipientId) promise.then(publicKey => { - const decoded = decodeChat(transaction, publicKey) + const decoded = transaction.type === 0 + ? transaction + : decodeChat(transaction, publicKey) store.dispatch('chat/pushMessages', [decoded]) }) }) From 0e285258ed34b788ba5c664f1d6601744bd2830c Mon Sep 17 00:00:00 2001 From: bludnic Date: Thu, 5 Sep 2019 14:25:32 +0300 Subject: [PATCH 06/35] refactor(Chat Vuex): add constants for getNewMessages interval --- src/store/modules/chat/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/store/modules/chat/index.js b/src/store/modules/chat/index.js index 9795a692b..5c7dcff6f 100644 --- a/src/store/modules/chat/index.js +++ b/src/store/modules/chat/index.js @@ -14,6 +14,9 @@ import { EPOCH, Cryptos, TransactionStatus as TS } from '@/lib/constants' export let interval +const SOCKET_ENABLED_TIMEOUT = 3000 +const SOCKET_DISABLED_TIMEOUT = 3000 + /** * type State { * chats: { @@ -665,7 +668,7 @@ const actions = { dispatch('getNewMessages') .catch(err => console.error(err)) .then(() => { - const timeout = rootState.options.useSocketConnection ? 60000 : 3000 + const timeout = rootState.options.useSocketConnection ? SOCKET_ENABLED_TIMEOUT : SOCKET_DISABLED_TIMEOUT interval = setTimeout(repeat, timeout) }) } From 42fe9d73e1d0c5b0e2d00ba0c2afd262efe3d224 Mon Sep 17 00:00:00 2001 From: bludnic Date: Thu, 5 Sep 2019 14:33:51 +0300 Subject: [PATCH 07/35] fix(socketsPlugin): filter transactions with type 0 and type 8 --- src/store/plugins/socketsPlugin.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/store/plugins/socketsPlugin.js b/src/store/plugins/socketsPlugin.js index 718e1bb85..e99adfb38 100644 --- a/src/store/plugins/socketsPlugin.js +++ b/src/store/plugins/socketsPlugin.js @@ -7,6 +7,8 @@ function openSocketConnection (adamantAddress, store) { socketClient.connect(adamantAddress) socketClient.on('newTrans', transaction => { + if (transaction.type !== 0 && transaction.type !== 8) return + const promise = (transaction.recipientId === store.state.address) ? Promise.resolve(transaction.senderPublicKey) : getPublicKey(transaction.recipientId) From 37328cf244c5eed6765df8010483cada191bec12 Mon Sep 17 00:00:00 2001 From: bludnic Date: Tue, 10 Sep 2019 14:46:19 +0300 Subject: [PATCH 08/35] feat(AChatMessage): add blockchain status --- src/assets/stylus/components/_chat.styl | 8 ++++++++ src/components/AChat/AChatMessage.vue | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/assets/stylus/components/_chat.styl b/src/assets/stylus/components/_chat.styl index 4e5f7fbe2..f692f509b 100644 --- a/src/assets/stylus/components/_chat.styl +++ b/src/assets/stylus/components/_chat.styl @@ -166,6 +166,10 @@ $chat-avatar-size := 40px i line-height: 1.2 + &__blockchain-status + font-size: 12px + line-height: 1 + &__direction font-size: 14px font-weight: 300 @@ -225,6 +229,8 @@ $chat-avatar-size := 40px color: $grey.darken-4 &__timestamp color: $adm-colors.muted + &__blockchain-status + color: $adm-colors.muted &__amount color: $adm-colors.regular &__direction @@ -281,6 +287,8 @@ $chat-avatar-size := 40px color: $shades.white &__timestamp color: $grey.base + &__blockchain-status + color: $grey.base &__message-text a color: $blue.lighten-2 diff --git a/src/components/AChat/AChatMessage.vue b/src/components/AChat/AChatMessage.vue index 057ed0a17..d1edd0523 100644 --- a/src/components/AChat/AChatMessage.vue +++ b/src/components/AChat/AChatMessage.vue @@ -21,8 +21,9 @@
+
{{ time }}
-
+
['delivered', 'pending', 'rejected'].includes(v) }, userId: { From e3fc6288f1aef45794e7784eeaf8149f46fd02f7 Mon Sep 17 00:00:00 2001 From: bludnic Date: Mon, 23 Sep 2019 18:20:38 +0300 Subject: [PATCH 09/35] refactor(sockets): refactor SocketClient and SocketPlugin * Implement EventEmitter * Revise connection every 5 sec * Handle node status change, toggle node, use fastest node * Handle useSocketConnection option --- src/lib/sockets.js | 205 ++++++++++++++++++++++++++--- src/store/plugins/socketsPlugin.js | 47 ++++--- 2 files changed, 215 insertions(+), 37 deletions(-) diff --git a/src/lib/sockets.js b/src/lib/sockets.js index e04347e6b..35654afd9 100644 --- a/src/lib/sockets.js +++ b/src/lib/sockets.js @@ -1,53 +1,222 @@ import io from 'socket.io-client' import random from 'lodash/random' -import config from '@/config' +/** + * interface Events { + * [key: string]: Function[]; + * } + */ +export class EventEmitter { + /** + * @param {Events} events + */ + constructor (events) { + this.events = events || {} + } -export class SocketClient { /** - * @param {string[]} nodes Array of nodes URLs + * @param {string} event + * @param {Function} cb */ - constructor (nodes) { - this.nodes = nodes + subscribe (event, cb) { + (this.events[event] || (this.events[event] = [])).push(cb) + + return { + unsubscribe: () => + this.events[event] && this.events[event].splice(this.events[event].indexOf(cb) >>> 0, 1) + } + } + + /** + * @param {string} event + * @param {any[]} args + */ + emit (event, ...args) { + (this.events[event] || []).forEach(fn => fn(...args)) } +} + +export class SocketClient extends EventEmitter { + adamantAddress = '' + nodes = {} + + /** + * The current node to which we are connected + * @type {string} + */ + currentSocketAddress = '' + + /** + * Set true when chat messages are loaded + * @type {boolean} + */ + isSocketReady = false + + /** + * Sync with store.state.options.useSocketConnection + * @type {boolean} + */ + isSocketEnabled = false + + /** + * Sync with store.state.nodes.useFastest + * @type {boolean} + */ + useFastest = false + + interval = null + + /** + * @type {number} + */ + REVISE_CONNECTION_TIMEOUT = 5000 /** * Get random socket address. * @returns {string} */ get socketAddress () { - return this.nodes[random(this.nodes.length - 1)] + const node = this.useFastest + ? this.fastestNode + : this.randomNode + + return node.url.replace(/^https?:\/\/(.*)$/, '$1') + } + + get fastestNode () { + return this.nodes.reduce((fastest, current) => { + if (!current.online || !current.active || current.outOfSync) { + return fastest + } + return (!fastest || fastest.ping > current.ping) ? current : fastest + }) + } + + get randomNode () { + const activeNodes = this.nodes.filter(n => n.online && n.active && !n.outOfSync && n.socketSupport) + + return activeNodes[random(activeNodes.length - 1)] } - on (event, fn) { - this.connection.on(event, fn) + get hasActiveNodes () { + return Object.values(this.nodes) + .some(n => n.online && n.active && !n.outOfSync && n.socketSupport) } - connect (adamantAddress) { - this.connection = io(this.socketAddress, { reconnection: false, timeout: 5000 }) + get isOnline () { + return this.connection && this.connection.connected + } + + get isCurrentNodeActive () { + return this.nodes.some( + node => node.url.match(new RegExp(this.currentSocketAddress)) && node.active + ) + } + + /** + * Update nodes statuses. + * @param nodes + */ + setNodes (nodes) { + this.nodes = nodes + } + + setAdamantAddress (address) { + this.adamantAddress = address + } + + setSocketReady (value) { + this.isSocketReady = value + } + + setSocketEnabled (value) { + this.isSocketEnabled = value + + if (!value) this.disconnect() + } + + /** + * @param {boolean} value + */ + setUseFastest (value) { + this.useFastest = value + } + + /** + * Subscribe to socket events. + */ + subscribeToEvents () { + if (this.connection) { + this.connection.on('newTrans', transaction => { + if (transaction.type === 0 || transaction.type === 8) this.emit('newMessage', transaction) + }) + } + } + + /** + * @param address ADAMANT address + */ + init (address) { + this.setAdamantAddress(address) + this.setSocketReady(true) + this.interval = setInterval(() => this.reviseConnection(), this.REVISE_CONNECTION_TIMEOUT) + } + + destroy () { + clearInterval(this.interval) + this.setSocketReady(false) + this.disconnect() + } + + connect (socketAddress) { + this.connection = io(`wss://${socketAddress}`, { reconnection: false, timeout: 5000 }) this.connection.on('connect', () => { - this.connection.emit('msg', adamantAddress + ' connected!') - this.connection.emit('address', adamantAddress) + this.currentSocketAddress = socketAddress + this.connection.emit('msg', this.adamantAddress + ' connected!') + this.connection.emit('address', this.adamantAddress) }) this.connection.on('disconnect', reason => { if (reason === 'ping timeout' || reason === 'io server disconnect') { - this.connect(adamantAddress) + console.warn('[Socket] Disconnected. Reason:', reason) } }) this.connection.on('connect_error', (err) => { - console.warn('connect_error', err) - setTimeout(() => this.connect(adamantAddress), 5000) + console.warn('[Socket] connect_error', err) }) } disconnect () { this.connection && this.connection.close() } -} -const nodes = config.server.adm.map(node => node.url.replace(/^https?:\/\/(.*)$/, 'wss://$1')) + reviseConnection () { + if (!this.isSocketReady) return + if (!this.isSocketEnabled) return + if (!this.hasActiveNodes) { + this.disconnect() + console.warn('[Socket]: No active nodes') + return + } + + const socketAddress = this.socketAddress + + if ( + ( + this.isOnline && + this.useFastest && + this.currentSocketAddress !== socketAddress + ) || + !this.isOnline || + !this.isCurrentNodeActive + ) { + this.disconnect() + this.connect(socketAddress) + this.subscribeToEvents() + } + } +} -export default new SocketClient(nodes) +export default new SocketClient() diff --git a/src/store/plugins/socketsPlugin.js b/src/store/plugins/socketsPlugin.js index e99adfb38..70ef95c66 100644 --- a/src/store/plugins/socketsPlugin.js +++ b/src/store/plugins/socketsPlugin.js @@ -1,14 +1,8 @@ import socketClient from '@/lib/sockets' import { decodeChat, getPublicKey } from '@/lib/adamant-api' -function openSocketConnection (adamantAddress, store) { - if (!store.state.options.useSocketConnection) return - if (socketClient.connected) return - - socketClient.connect(adamantAddress) - socketClient.on('newTrans', transaction => { - if (transaction.type !== 0 && transaction.type !== 8) return - +function subscribe (store) { + socketClient.subscribe('newMessage', transaction => { const promise = (transaction.recipientId === store.state.address) ? Promise.resolve(transaction.senderPublicKey) : getPublicKey(transaction.recipientId) @@ -23,23 +17,38 @@ function openSocketConnection (adamantAddress, store) { } export default store => { + subscribe(store) + + socketClient.setSocketEnabled(store.state.options.useSocketConnection) + // open socket connection when chats are loaded - store.watch(() => store.state.chat.isFulfilled, value => { - if (value) openSocketConnection(store.state.address, store) + store.watch(() => store.state.chat.isFulfilled, isFulfilled => { + if (isFulfilled) socketClient.init(store.state.address) }) // when logout or update `useSocketConnection` option - store.subscribe((mutation, state) => { - if (mutation.type === 'reset') { - socketClient.disconnect() + store.subscribe(mutation => { + if (mutation.type === 'reset') socketClient.destroy() + + if ( + mutation.type === 'options/updateOption' && + mutation.payload.key === 'useSocketConnection' + ) { + socketClient.setSocketEnabled(mutation.payload.value) + } + }) + + // when statusUpdate/enable/disable/useFastest node + store.subscribe(mutation => { + if ( + mutation.type === 'nodes/status' || + mutation.type === 'nodes/toggle' + ) { + socketClient.setNodes(store.getters['nodes/list']) } - if (mutation.type === 'options/updateOption' && mutation.payload.key === 'useSocketConnection') { - if (mutation.payload.value) { - openSocketConnection(state.address, store) - } else { - socketClient.disconnect() - } + if (mutation.type === 'nodes/useFastest') { + socketClient.setUseFastest(mutation.payload) } }) } From 963104752fa8de650213950bcb7a7f75fc4ac5ea Mon Sep 17 00:00:00 2001 From: bludnic Date: Mon, 23 Sep 2019 18:23:43 +0300 Subject: [PATCH 10/35] refactor(_chat.styl): add margin-right to __blockchain-status --- src/assets/stylus/components/_chat.styl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/assets/stylus/components/_chat.styl b/src/assets/stylus/components/_chat.styl index f692f509b..727ca4ee2 100644 --- a/src/assets/stylus/components/_chat.styl +++ b/src/assets/stylus/components/_chat.styl @@ -169,6 +169,7 @@ $chat-avatar-size := 40px &__blockchain-status font-size: 12px line-height: 1 + margin-right: 5px &__direction font-size: 14px From 9456534be7661a160e62cf8c5e509eaedb617aa4 Mon Sep 17 00:00:00 2001 From: bludnic Date: Tue, 24 Sep 2019 17:23:00 +0300 Subject: [PATCH 11/35] feat: add additional status CONFIRMED MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit So now: - CONFIRMED = Confirmed in token’s blockchain - DELIVERED = Delivered to Node * test(Chat Module): fix tests * feat(i18n): update locales --- src/components/AChat/AChatMessage.vue | 8 ++++---- src/components/AChat/AChatTransaction.vue | 5 +++-- src/components/ChatPreview.vue | 2 +- src/i18n/de.json | 10 ++++++---- src/i18n/en.json | 3 ++- src/i18n/ru.json | 3 ++- src/lib/chatHelpers.js | 2 +- src/lib/constants.js | 1 + src/mixins/transaction.js | 2 +- src/store/__tests__/modules/chat.test.js | 4 ++-- src/store/modules/chat/index.js | 6 +++--- 11 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/components/AChat/AChatMessage.vue b/src/components/AChat/AChatMessage.vue index d1edd0523..81bb0236a 100644 --- a/src/components/AChat/AChatMessage.vue +++ b/src/components/AChat/AChatMessage.vue @@ -21,7 +21,7 @@
-
+
{{ time }}
['delivered', 'pending', 'rejected'].includes(v) + default: 'confirmed', + validator: v => ['confirmed', 'delivered', 'pending', 'rejected'].includes(v) }, userId: { type: String, diff --git a/src/components/AChat/AChatTransaction.vue b/src/components/AChat/AChatTransaction.vue index 60d69fc9e..43672a9c5 100644 --- a/src/components/AChat/AChatTransaction.vue +++ b/src/components/AChat/AChatTransaction.vue @@ -51,7 +51,7 @@ export default { }, computed: { statusIcon () { - if (this.status === 'delivered') { + if (this.status === 'confirmed') { return 'mdi-check' } else if (this.status === 'pending') { return 'mdi-clock-outline' @@ -115,6 +115,7 @@ export default { sent: 'Sent', received: 'Received', statuses: { + confirmed: '', delivered: '', pending: '', rejected: '', @@ -130,7 +131,7 @@ export default { status: { type: String, default: 'confirmed', - validator: v => ['delivered', 'pending', 'rejected', 'invalid', 'unknown'].includes(v) + validator: v => ['confirmed', 'delivered', 'pending', 'rejected', 'invalid', 'unknown'].includes(v) }, isClickable: { type: Boolean, diff --git a/src/components/ChatPreview.vue b/src/components/ChatPreview.vue index 95e249a47..8c061fac2 100644 --- a/src/components/ChatPreview.vue +++ b/src/components/ChatPreview.vue @@ -128,7 +128,7 @@ export default { return this.getTransactionStatus(this.transaction) }, statusIcon () { - if (this.status === 'delivered') { + if (this.status === 'confirmed' || this.status === 'delivered') { return 'mdi-check' } else if (this.status === 'pending') { return 'mdi-clock-outline' diff --git a/src/i18n/de.json b/src/i18n/de.json index 31cbf84a0..e1c4bd48f 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -41,10 +41,12 @@ "title": "Chats", "too_long": "Nachricht zu lang", "transaction_statuses": { - "delivered": "Transaction is confirmed on token’s network", - "invalid": "Information about transaction, fetched from rich message in ADAMANT newtwork, differs from information, fetched from token’s network or this type of cryptocurrency is not supported", - "pending": "Transaction is not confirmed yet", - "rejected": "Transaction is cancelled or not accepted on token’s network" + "confirmed": "Confirmed in token’s blockchain", + "delivered": "Delivered to Node", + "invalid": "Incorrect information. Check transaction in token's blockchain carefully", + "pending": "Not confirmed yet", + "rejected": "The transaction is cancelled or not accepted", + "unknown": "This cryptocurrency is not supported yet" }, "welcome_message": "ADAMANT ist ein Messenger, der vollständig auf der Blockchain funktioniert. Er ist unabhängig von Staaten, Konzernen und sogar Entwicklern. Das wird dank der dezentralen Netzinfrastruktur ermöglicht, deren Quellcode offen ist und die von Nutzern unterstützt wird. Deswegen kostet jede Transaktion wie Versand von Nachrichten oder Speichern von Kontakten eine Netzwerkgebühr von 0.001 ADM.\n\nDie Blockchain hilft, herausragende Sicherheit und Privatsphäre zu erlangen, die für keinen gewöhnlichen P2P- oder zentralisierten Messenger verfügbar ist. Außerdem stellt die Blockchain neue Möglichkeiten dar. Sie können Kryptowährungen in der Wallet aufbewahren sowie direkt im Chat versenden, ohne dabei die Kontrolle über Ihre privaten Schlüssel zu verlieren, oder auch ADAMANT als eine 2FA-Lösung verwenden.\n\nIn ADAMANT kann kein Konto kontrolliert, geblockt, deaktiviert, eingeschränkt oder zensiert werden. Alle Nutzer tragen die volle Verantwortung für die Inhalte, Nachrichten, Medien, Absichten und Ziele bei der Nutzung des Messenger.\n\nBedenken Sie, dass Sicherheit und Anonymität auch von Ihnen selbst abhängt. Folgen Sie keinen Links in den Chats, andernfalls kann Ihre IP-Adresse für Dritte sichtbar werden. Verwenden Sie einen PIN-Code auf Ihrem Gerät. Mehr Informationen zu Sicherheit und Anonymität lesen Sie hier: https://adamant.im/de-staysecured/.\n\nVergewissern Sie sich, dass sie die Passphrase von Ihrem Konto gespeichert haben – loggen Sie sich aus und wieder ein. Schreiben Sie die Passphrase auf einem Stück Papier. Nur Sie tragen die Verantwortung für die sichere Aufbewahrung Ihrer Passphrase. Diese kann nämlich nicht wiederhergestellt werden. Gelangt Sie in die Hände Dritter, verlieren Sie Ihr Geld und Ihr Chatverlauf wird gelesen. Behandeln Sie Ihre Passphrase so, als würden Tokens in Ihrer Wallet 1.000.000.000 Dollar kosten.\n\nUm mit einer Konversation zu beginnen, erhalten Sie zuerst kostenlose Tokens im Account-Tab. Erstellen Sie einen neuen Chat und geben Sie die ADM-Adresse Ihres Gesprächspartners. Zeigen Sie Ihre ADM-Adresse persönlich und senden Sie sie nicht über andere Messenger.", "welcome_message_title": "Willkommen in ADAMANT", diff --git a/src/i18n/en.json b/src/i18n/en.json index 95a6ba397..8b6c6dcea 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -37,7 +37,8 @@ "title": "Chats", "too_long": "The message is too long", "transaction_statuses": { - "delivered": "Confirmed in token’s blockchain", + "confirmed": "Confirmed in token’s blockchain", + "delivered": "Delivered to Node", "invalid": "Incorrect information. Check transaction in token's blockchain carefully", "pending": "Not confirmed yet", "rejected": "The transaction is cancelled or not accepted", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 60a74ceac..1e9b1e597 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -37,7 +37,8 @@ "title": "Чаты", "too_long": "Сообщение слишком длинное", "transaction_statuses": { - "delivered": "Подтверждено в блокчейне криптовалюты", + "confirmed": "Подтверждено в блокчейне криптовалюты", + "delivered": "Доставлено на Ноду", "invalid": "Противоречивая информация. Внимательно проверьте перевод в блокчейне криптовалюты", "pending": "В ожидании", "rejected": "Перевод не принят или отменен", diff --git a/src/lib/chatHelpers.js b/src/lib/chatHelpers.js index b9cb10d1e..56aa25f30 100644 --- a/src/lib/chatHelpers.js +++ b/src/lib/chatHelpers.js @@ -185,7 +185,7 @@ export function transformMessage (abstract) { transaction.recipientId = abstract.recipientId transaction.admTimestamp = abstract.timestamp transaction.timestamp = getRealTimestamp(abstract.timestamp) - transaction.status = abstract.status || abstract.height ? TS.DELIVERED : TS.PENDING + transaction.status = abstract.status || abstract.height ? TS.CONFIRMED : TS.DELIVERED transaction.i18n = !!abstract.i18n transaction.amount = abstract.amount ? abstract.amount : 0 transaction.message = '' diff --git a/src/lib/constants.js b/src/lib/constants.js index d5c5c0d89..4aec3b0a8 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -128,6 +128,7 @@ export const UserPasswordHashSettings = { } export const TransactionStatus = { + CONFIRMED: 'confirmed', DELIVERED: 'delivered', PENDING: 'pending', REJECTED: 'rejected', diff --git a/src/mixins/transaction.js b/src/mixins/transaction.js index 31004b9ce..e0ebbc72a 100644 --- a/src/mixins/transaction.js +++ b/src/mixins/transaction.js @@ -109,7 +109,7 @@ export default { recipientCryptoAddress, senderCryptoAddress })) { - status = TS.DELIVERED + status = TS.CONFIRMED } else { status = TS.INVALID } diff --git a/src/store/__tests__/modules/chat.test.js b/src/store/__tests__/modules/chat.test.js index 2b1238981..f3b438a8c 100644 --- a/src/store/__tests__/modules/chat.test.js +++ b/src/store/__tests__/modules/chat.test.js @@ -1154,7 +1154,7 @@ describe('Store: chat.js', () => { ['updateMessage', { id: messageObject.id, realId: transactionId, - status: TS.PENDING, + status: TS.DELIVERED, partnerId: recipientId }] ]) @@ -1205,7 +1205,7 @@ describe('Store: chat.js', () => { ['updateMessage', { id: messageId, realId: transactionId, - status: TS.PENDING, + status: TS.DELIVERED, partnerId: recipientId }] ]) diff --git a/src/store/modules/chat/index.js b/src/store/modules/chat/index.js index 5c7dcff6f..a8b98ea82 100644 --- a/src/store/modules/chat/index.js +++ b/src/store/modules/chat/index.js @@ -389,7 +389,7 @@ const mutations = { senderId: 'chats.welcome_message_title', type: 'message', i18n: true, - status: TS.DELIVERED + status: TS.CONFIRMED } ] @@ -548,7 +548,7 @@ const actions = { commit('updateMessage', { id: messageObject.id, realId: res.transactionId, - status: TS.PENDING, // not confirmed yet, wait to be stored in the blockchain (optional line) + status: TS.DELIVERED, // not confirmed yet, wait to be stored in the blockchain (optional line) partnerId: recipientId }) @@ -593,7 +593,7 @@ const actions = { commit('updateMessage', { id: messageId, realId: res.transactionId, - status: TS.PENDING, + status: TS.DELIVERED, partnerId: recipientId }) From f0c58fd023701a7c7f0a38ea70fc9fa72a66e3b6 Mon Sep 17 00:00:00 2001 From: bludnic Date: Tue, 24 Sep 2019 19:24:20 +0300 Subject: [PATCH 12/35] fix(AChatTransaction): display PENDING icon when status is DELIVERED --- src/components/AChat/AChatTransaction.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AChat/AChatTransaction.vue b/src/components/AChat/AChatTransaction.vue index 43672a9c5..cce37a59d 100644 --- a/src/components/AChat/AChatTransaction.vue +++ b/src/components/AChat/AChatTransaction.vue @@ -53,7 +53,7 @@ export default { statusIcon () { if (this.status === 'confirmed') { return 'mdi-check' - } else if (this.status === 'pending') { + } else if (this.status === 'pending' || this.status === 'delivered') { return 'mdi-clock-outline' } else if (this.status === 'rejected') { return 'mdi-close-circle-outline' From 2cc97fce232d240f25e2e6eef13508cb4f003b1d Mon Sep 17 00:00:00 2001 From: MaaKut Date: Mon, 9 Dec 2019 22:29:39 +0300 Subject: [PATCH 13/35] BTC support (#363) --- .travis.yml | 2 +- package.json | 2 +- 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 | 11 +-- src/i18n/ru.json | 11 +-- src/lib/bitcoin/bitcoin-api.js | 83 +++++++++++++++++++ src/lib/bitcoin/btc-base-api.js | 25 ++---- 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/store/index.js | 2 + src/store/modules/adm/adm-getters.js | 4 +- .../modules/btc-base/btc-base-actions.js | 48 ++++++++--- src/store/modules/btc/btc-actions.js | 65 +++++++++++++++ src/store/modules/btc/btc-getters.js | 33 ++++++++ src/store/modules/btc/btc-mutations.js | 18 ++++ src/store/modules/btc/btc-state.js | 9 ++ src/store/modules/btc/index.js | 12 +++ src/store/modules/dash/dash-getters.js | 4 +- src/store/modules/doge/doge-getters.js | 4 +- src/store/modules/erc20/erc20-getters.js | 4 +- src/store/modules/eth/getters.js | 4 +- yarn.lock | 8 +- 33 files changed, 395 insertions(+), 130 deletions(-) create mode 100644 src/components/icons/BtcFill.vue create mode 100644 src/lib/bitcoin/bitcoin-api.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 dcd9752d2..9cc639a51 100644 --- a/package.json +++ b/package.json @@ -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..b46eaf142 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", @@ -241,6 +233,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..2f20e10f5 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": "АДАМАНТ", @@ -242,6 +234,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..d6f1a70ff 100644 --- a/src/lib/bitcoin/btc-base-api.js +++ b/src/lib/bitcoin/btc-base-api.js @@ -41,24 +41,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 +94,7 @@ export default class BtcBaseApi { * @abstract * @returns {Promise>} */ - _getUnspents () { + getUnspents () { return Promise.resolve([]) } @@ -111,16 +103,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 +126,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) { 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/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-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..039324374 100644 --- a/src/store/modules/btc-base/btc-base-actions.js +++ b/src/store/modules/btc-base/btc-base-actions.js @@ -2,13 +2,37 @@ 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 MAX_ATTEMPTS = 15 const NEW_TRANSACTION_TIMEOUT = 120 const OLD_TRANSACTION_TIMEOUT = 5 -export default options => { +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} maxFetchAttempts max number of attempts to fetch the transaction details + * @property {number} newTxFetchTimeout seconds to wait between subsequent attempts to get new transaction details + * @property {number} oldTxFetchTimeout seconds to wait between subsequent attempts to get old 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, + maxFetchAttempts = MAX_ATTEMPTS, + newTxFetchTimeout = NEW_TRANSACTION_TIMEOUT, + oldTxFetchTimeout = OLD_TRANSACTION_TIMEOUT + } = options /** @type {BtcBaseApi} */ let api = null @@ -63,13 +87,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,7 +125,7 @@ export default options => { senderId: context.state.address, recipientId: address, amount, - fee: api.getFee(amount), + fee: api.getFee(amount) || fee, status: 'PENDING', timestamp: Date.now() }]) @@ -145,18 +169,18 @@ export default options => { ) .then(replay => { const attempt = payload.attempt || 0 - if (replay && attempt < MAX_ATTEMPTS) { + if (replay && attempt < maxFetchAttempts) { const newPayload = { ...payload, attempt: attempt + 1, force: true } - const timeout = payload.isNew ? NEW_TRANSACTION_TIMEOUT : OLD_TRANSACTION_TIMEOUT + const timeout = payload.isNew ? newTxFetchTimeout : oldTxFetchTimeout setTimeout(() => context.dispatch('getTransaction', newPayload), timeout * 1000) } - if (replay && attempt >= MAX_ATTEMPTS) { + if (replay && attempt >= maxFetchAttempts) { context.commit('transactions', [{ hash: payload.hash, status: 'ERROR' }]) } }) @@ -174,6 +198,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..c68c9afda --- /dev/null +++ b/src/store/modules/btc/btc-actions.js @@ -0,0 +1,65 @@ +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 + api.getHeight().then(height => context.commit('height', height)) + } +}) + +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, + maxFetchAttempts: 180, + oldTxFetchTimeout: 10 + }) +} 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..fdf8740f4 --- /dev/null +++ b/src/store/modules/btc/btc-state.js @@ -0,0 +1,9 @@ +import baseState from '../btc-base/btc-base-state' +import { Cryptos } from '../../../lib/constants' + +export default () => ({ + crypto: Cryptos.BTC, + ...baseState(), + utxo: [], + feeRate: 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-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-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-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/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/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" From 3abb4666325b8c950f3bd6a2d5f071aa9bf5c067 Mon Sep 17 00:00:00 2001 From: Dmitrij Papulovskij Date: Tue, 10 Dec 2019 12:36:52 +0500 Subject: [PATCH 14/35] 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. --- src/middlewares/isLogged.js | 3 +- src/router/navigationGuard.js | 60 +++++++++++++++++++++++++++++++++++ src/views/Login.vue | 47 ++------------------------- 3 files changed, 64 insertions(+), 46 deletions(-) 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/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/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: { From 269b8cfe2da22ee8ee30ccd49d71115e89a3269f Mon Sep 17 00:00:00 2001 From: MaaKut Date: Sun, 15 Dec 2019 23:47:40 +0300 Subject: [PATCH 15/35] Feature/tx statuses (#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 --- src/i18n/en.json | 1 + src/i18n/ru.json | 1 + src/lib/bitcoin/btc-base-api.js | 17 +++- src/lib/transactionsFetching.js | 25 +++++ src/mixins/transaction.js | 12 +-- src/store/modules/adm/adm-actions.js | 9 ++ .../modules/btc-base/btc-base-actions.js | 94 +++++++++++-------- src/store/modules/btc/btc-actions.js | 29 +++++- src/store/modules/btc/btc-state.js | 3 +- src/store/modules/dash/dash-actions.js | 3 +- src/store/modules/doge/doge-actions.js | 3 +- src/store/modules/erc20/erc20-actions.js | 2 +- .../modules/eth-base/eth-base-actions.js | 88 +++++++++-------- src/views/transactions/Transaction.vue | 2 +- 14 files changed, 193 insertions(+), 96 deletions(-) create mode 100644 src/lib/transactionsFetching.js diff --git a/src/i18n/en.json b/src/i18n/en.json index b46eaf142..0344a5ee4 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -197,6 +197,7 @@ "statuses": { "error": "Error", "pending": "Pending", + "registered": "Pending", "success": "Success" }, "transactions": "Transactions", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 2f20e10f5..ed1c03b6c 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -198,6 +198,7 @@ "statuses": { "error": "Ошибка", "pending": "Ожидание", + "registered": "Ожидание", "success": "Успешно" }, "transactions": "Транзакции", diff --git a/src/lib/bitcoin/btc-base-api.js b/src/lib/bitcoin/btc-base-api.js index d6f1a70ff..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] @@ -140,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] } @@ -193,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/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/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/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/btc-base/btc-base-actions.js b/src/store/modules/btc-base/btc-base-actions.js index 039324374..e1544f664 100644 --- a/src/store/modules/btc-base/btc-base-actions.js +++ b/src/store/modules/btc-base/btc-base-actions.js @@ -1,10 +1,7 @@ import BigNumber from '@/lib/bignumber' import BtcBaseApi from '../../../lib/bitcoin/btc-base-api' import { storeCryptoAddress } from '../../../lib/store-crypto-address' - -const MAX_ATTEMPTS = 15 -const NEW_TRANSACTION_TIMEOUT = 120 -const OLD_TRANSACTION_TIMEOUT = 5 +import * as tf from '../../../lib/transactionsFetching' const DEFAULT_CUSTOM_ACTIONS = () => ({ }) @@ -14,9 +11,7 @@ const DEFAULT_CUSTOM_ACTIONS = () => ({ }) * @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} maxFetchAttempts max number of attempts to fetch the transaction details - * @property {number} newTxFetchTimeout seconds to wait between subsequent attempts to get new transaction details - * @property {number} oldTxFetchTimeout seconds to wait between subsequent attempts to get old transaction details + * @property {number} fetchRetryTimeout interval (ms) between attempts to fetch the registered transaction details */ /** @@ -29,9 +24,7 @@ function createActions (options) { getNewTransactions, getOldTransactions, customActions = DEFAULT_CUSTOM_ACTIONS, - maxFetchAttempts = MAX_ATTEMPTS, - newTxFetchTimeout = NEW_TRANSACTION_TIMEOUT, - oldTxFetchTimeout = OLD_TRANSACTION_TIMEOUT + fetchRetryTimeout } = options /** @type {BtcBaseApi} */ @@ -130,7 +123,7 @@ function createActions (options) { timestamp: Date.now() }]) - context.dispatch('getTransaction', { hash, isNew: true, force: true }) + context.dispatch('getTransaction', { hash, force: true }) return hash } @@ -142,14 +135,14 @@ function createActions (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, @@ -159,31 +152,58 @@ function createActions (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 < maxFetchAttempts) { - const newPayload = { - ...payload, - attempt: attempt + 1, - force: true - } - - const timeout = payload.isNew ? newTxFetchTimeout : oldTxFetchTimeout - 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 >= maxFetchAttempts) { - 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) { diff --git a/src/store/modules/btc/btc-actions.js b/src/store/modules/btc/btc-actions.js index c68c9afda..9f06f0692 100644 --- a/src/store/modules/btc/btc-actions.js +++ b/src/store/modules/btc/btc-actions.js @@ -17,7 +17,31 @@ const customActions = getApi => ({ api.getFeeRate().then(rate => context.commit('feeRate', rate)) // Last block height - api.getHeight().then(height => context.commit('height', 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) + } } }) @@ -59,7 +83,6 @@ export default { getOldTransactions, getNewTransactions, customActions, - maxFetchAttempts: 180, - oldTxFetchTimeout: 10 + fetchRetryTimeout: 60 * 1000 }) } diff --git a/src/store/modules/btc/btc-state.js b/src/store/modules/btc/btc-state.js index fdf8740f4..6499a2311 100644 --- a/src/store/modules/btc/btc-state.js +++ b/src/store/modules/btc/btc-state.js @@ -5,5 +5,6 @@ export default () => ({ crypto: Cryptos.BTC, ...baseState(), utxo: [], - feeRate: 0 + feeRate: 0, + height: 0 }) 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/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/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/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/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 }) } }, From 7b6a14d037bc1794fc1527ab09b667c67dca631b Mon Sep 17 00:00:00 2001 From: Alexander Kiselev Date: Sun, 15 Dec 2019 23:54:51 +0300 Subject: [PATCH 16/35] Version 2.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9cc639a51..67b1debdf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "adamant-im", - "version": "2.0.1", + "version": "2.3.0", "private": true, "scripts": { "serve": "vue-cli-service serve", From 5b2789200c835d4792894dd40703e49726c95ab8 Mon Sep 17 00:00:00 2001 From: adamant-al <33592982+adamant-al@users.noreply.github.com> Date: Fri, 20 Dec 2019 13:01:17 +0300 Subject: [PATCH 17/35] Resfinex Token (RES) support --- src/lib/constants.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/lib/constants.js b/src/lib/constants.js index 56b1fad2f..6e22d5ecf 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -22,14 +22,16 @@ export const Cryptos = { DOGE: 'DOGE', DASH: 'DASH', BNB: 'BNB', - USDS: 'USDS' + USDS: 'USDS', + RES: 'RES' } export const CryptosNames = { [Cryptos.ADM]: 'ADAMANT', [Cryptos.BNB]: 'Binance Coin', [Cryptos.ETH]: 'Ethereum', - [Cryptos.BZ]: 'Bit-Z', + [Cryptos.BZ]: 'Bit-Z Token', + [Cryptos.RES]: 'Resfinex Token', [Cryptos.DOGE]: 'DOGE', [Cryptos.DASH]: 'DASH', [Cryptos.KCS]: 'KuCoin Shares', @@ -41,7 +43,8 @@ export const ERC20 = Object.freeze([ Cryptos.BNB, Cryptos.BZ, Cryptos.KCS, - Cryptos.USDS + Cryptos.USDS, + Cryptos.RES ]) export const BTC_BASED = Object.freeze([ @@ -61,6 +64,7 @@ export const CryptoAmountPrecision = { BNB: 6, DOGE: 8, BZ: 6, + RES: 5, DASH: 5, KCS: 6, USDS: 6, @@ -74,6 +78,7 @@ export const CryptoNaturalUnits = { DOGE: 8, BZ: 18, DASH: 8, + RES: 5, KCS: 6, USDS: 6, BTC: 8 From 934b30f161bfbf8bc8a93f560ec34203f5091469 Mon Sep 17 00:00:00 2001 From: Aleksei Lebedev Date: Fri, 20 Dec 2019 13:13:04 +0300 Subject: [PATCH 18/35] Save state for res token --- src/lib/idb/state.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/idb/state.js b/src/lib/idb/state.js index 0539db4cf..ad2805b49 100644 --- a/src/lib/idb/state.js +++ b/src/lib/idb/state.js @@ -6,7 +6,7 @@ import Security from './stores/Security' import { Cryptos } from '@/lib/constants' /** Modules that will be stored in IDB **/ -const modules = ['adm', 'eth', 'doge', 'bnb', 'bz', 'dash', 'kcs', 'usds', 'partners', 'delegates'] +const modules = ['adm', 'eth', 'doge', 'bnb', 'bz', 'dash', 'kcs', 'usds', 'res', 'partners', 'delegates'] /** * Clone modules from state. From f5b8b7bd5958025927066623423d50271511286d Mon Sep 17 00:00:00 2001 From: Aleksei Lebedev Date: Fri, 20 Dec 2019 13:41:32 +0300 Subject: [PATCH 19/35] Add Resfinex icon --- src/components/icons/CryptoIcon.vue | 2 ++ src/components/icons/ResFill.vue | 5 +++++ src/store/index.js | 1 + 3 files changed, 8 insertions(+) create mode 100644 src/components/icons/ResFill.vue diff --git a/src/components/icons/CryptoIcon.vue b/src/components/icons/CryptoIcon.vue index 800a6c7c4..1b50c68dc 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 ResFillIcon from './ResFill' import BtcFillIcon from './BtcFill' import UnknownCryptoFillIcon from './UnknownCryptoFill' @@ -39,6 +40,7 @@ export default { KcsFillIcon, LskFillIcon, UsdsFillIcon, + ResFillIcon, BtcFillIcon, UnknownCryptoFillIcon }, diff --git a/src/components/icons/ResFill.vue b/src/components/icons/ResFill.vue new file mode 100644 index 000000000..779f77ff1 --- /dev/null +++ b/src/components/icons/ResFill.vue @@ -0,0 +1,5 @@ + diff --git a/src/store/index.js b/src/store/index.js index 29986f8ec..b28859aa5 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -161,6 +161,7 @@ const store = { bz: erc20Module(Cryptos.BZ, '0x4375e7ad8a01b8ec3ed041399f62d9cd120e0063', 18), kcs: erc20Module(Cryptos.KCS, '0x039b5649a59967e3e936d7471f9c3700100ee1ab', 6), usds: erc20Module(Cryptos.USDS, '0xa4bdb11dc0a2bec88d24a3aa1e6bb17201112ebe', 6), + res: erc20Module(Cryptos.RES, '0x0a9f693fce6f00a51a8e0db4351b5a8078b4242e', 5), adm: admModule, // ADM transfers doge: dogeModule, dash: dashModule, From 5c9b7e4c3f836312821d4449fdea55c607cfb8a0 Mon Sep 17 00:00:00 2001 From: Aleksei Lebedev Date: Fri, 20 Dec 2019 13:59:03 +0300 Subject: [PATCH 20/35] Upd wallet icons --- src/components/icons/BnzFill.vue | 2 +- src/components/icons/DogeFill.vue | 4 ++-- src/lib/constants.js | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/icons/BnzFill.vue b/src/components/icons/BnzFill.vue index 3ad85e95b..931cc6563 100644 --- a/src/components/icons/BnzFill.vue +++ b/src/components/icons/BnzFill.vue @@ -1,5 +1,5 @@ diff --git a/src/components/icons/DogeFill.vue b/src/components/icons/DogeFill.vue index 9e305bf56..c0cf51ae4 100644 --- a/src/components/icons/DogeFill.vue +++ b/src/components/icons/DogeFill.vue @@ -1,10 +1,10 @@ diff --git a/src/lib/constants.js b/src/lib/constants.js index 6e22d5ecf..2128c4bb2 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -17,13 +17,13 @@ export const Cryptos = { ADM: 'ADM', BTC: 'BTC', ETH: 'ETH', - BZ: 'BZ', - KCS: 'KCS', DOGE: 'DOGE', DASH: 'DASH', - BNB: 'BNB', USDS: 'USDS', - RES: 'RES' + RES: 'RES', + BZ: 'BZ', + KCS: 'KCS', + BNB: 'BNB' } export const CryptosNames = { From 3308afdb0c9b8b486783f1f97541ccf802948810 Mon Sep 17 00:00:00 2001 From: Aleksei Lebedev Date: Fri, 20 Dec 2019 16:46:30 +0300 Subject: [PATCH 21/35] Tune Send tokens menu style --- src/components/Chat/ChatMenu.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Chat/ChatMenu.vue b/src/components/Chat/ChatMenu.vue index 4d7c0e531..c75fac2fa 100644 --- a/src/components/Chat/ChatMenu.vue +++ b/src/components/Chat/ChatMenu.vue @@ -1,6 +1,6 @@