diff --git a/src/shared/config.js b/src/shared/config.js index 1a54682526..78a23ed13b 100644 --- a/src/shared/config.js +++ b/src/shared/config.js @@ -34,6 +34,8 @@ export const DEFAULT_BALANCES_THRESHOLD = 100; export const BUNDLE_OUTPUTS_THRESHOLD = 50; +export const MAX_REQUEST_TIMEOUT = 60 * 1000 * 2; + export const DEFAULT_NODE_REQUEST_TIMEOUT = 6000 * 2; export const GET_NODE_INFO_REQUEST_TIMEOUT = 2500; export const GET_BALANCES_REQUEST_TIMEOUT = 6000; diff --git a/src/shared/libs/errors.js b/src/shared/libs/errors.js index 9e71952fcb..3096b62705 100644 --- a/src/shared/libs/errors.js +++ b/src/shared/libs/errors.js @@ -57,4 +57,5 @@ export default { LEDGER_CANCELLED: 'Ledger transaction cancelled', LEDGER_DENIED: 'Ledger transaction denied by user', LEDGER_INVALID_INDEX: 'Incorrect Ledger device or changed mnemonic', + REQUEST_TIMED_OUT: 'Request timed out', }; diff --git a/src/shared/libs/iota/extendedApi.js b/src/shared/libs/iota/extendedApi.js index 75237cbc17..6e3855b15f 100644 --- a/src/shared/libs/iota/extendedApi.js +++ b/src/shared/libs/iota/extendedApi.js @@ -27,7 +27,7 @@ import { isBundle, isBundleTraversable, } from './transfers'; -import { EMPTY_HASH_TRYTES } from './utils'; +import { EMPTY_HASH_TRYTES, withRequestTimeoutsHandler } from './utils'; /** * Returns timeouts for specific quorum requests @@ -505,36 +505,41 @@ const attachToTangleAsync = (provider, seedStore) => ( const shouldOffloadPow = get(seedStore, 'offloadPow') === true; if (shouldOffloadPow) { - return new Promise((resolve, reject) => { - getIotaInstance(provider, getApiTimeout('attachToTangle')).api.attachToTangle( - trunkTransaction, - branchTransaction, - minWeightMagnitude, - // Make sure trytes are sorted properly - sortTransactionTrytesArray(trytes), - (err, attachedTrytes) => { - if (err) { - reject(err); - } else { - constructBundleFromAttachedTrytes(attachedTrytes, seedStore) - .then((transactionObjects) => { - if ( - isBundle(transactionObjects) && - isBundleTraversable(transactionObjects, trunkTransaction, branchTransaction) - ) { - resolve({ - transactionObjects, - trytes: attachedTrytes, - }); - } else { - reject(new Error(Errors.INVALID_BUNDLE_CONSTRUCTED_WITH_REMOTE_POW)); - } - }) - .catch(reject); - } - }, - ); - }); + const request = (requestTimeout) => + new Promise((resolve, reject) => { + getIotaInstance(provider, requestTimeout).api.attachToTangle( + trunkTransaction, + branchTransaction, + minWeightMagnitude, + // Make sure trytes are sorted properly + sortTransactionTrytesArray(trytes), + (err, attachedTrytes) => { + if (err) { + reject(err); + } else { + constructBundleFromAttachedTrytes(attachedTrytes, seedStore) + .then((transactionObjects) => { + if ( + isBundle(transactionObjects) && + isBundleTraversable(transactionObjects, trunkTransaction, branchTransaction) + ) { + resolve({ + transactionObjects, + trytes: attachedTrytes, + }); + } else { + reject(new Error(Errors.INVALID_BUNDLE_CONSTRUCTED_WITH_REMOTE_POW)); + } + }) + .catch(reject); + } + }, + ); + }); + + const defaultRequestTimeout = getApiTimeout('attachToTangle'); + + return withRequestTimeoutsHandler(defaultRequestTimeout)(request); } return seedStore diff --git a/src/shared/libs/iota/utils.js b/src/shared/libs/iota/utils.js index 3efe9d4d2a..bc2f6fabe0 100644 --- a/src/shared/libs/iota/utils.js +++ b/src/shared/libs/iota/utils.js @@ -10,7 +10,7 @@ import URL from 'url-parse'; import { BigNumber } from 'bignumber.js'; import { iota } from './index'; import { isNodeHealthy } from './extendedApi'; -import { NODELIST_URL } from '../../config'; +import { NODELIST_URL, MAX_REQUEST_TIMEOUT } from '../../config'; import Errors from '../errors'; export const MAX_SEED_LENGTH = 81; @@ -442,3 +442,38 @@ export const throwIfNodeNotHealthy = (provider) => { return isSynced; }); }; + +/** + * Handles timeouts for network requests made to IRI nodes + * Catches "request timeout" exceptions and retries network request with increased timeout + * See (https://github.com/iotaledger/iota.js/blob/master/lib/utils/makeRequest.js#L115) + * + * @method withRequestTimeoutsHandler + * + * @param {number} timeout + * + * @returns {function} + */ +export const withRequestTimeoutsHandler = (timeout) => { + let attempt = 1; + + const getNextTimeout = () => attempt * timeout; + + const handleTimeout = (promiseFunc) => { + return promiseFunc(getNextTimeout()).catch((error) => { + attempt += 1; + + if ( + (includes(error.message, Errors.REQUEST_TIMED_OUT) || + includes(error.message, Errors.REQUEST_TIMED_OUT.toLowerCase())) && + getNextTimeout() < MAX_REQUEST_TIMEOUT + ) { + return handleTimeout(promiseFunc); + } + + throw error; + }); + }; + + return handleTimeout; +};