UNPKG

@ledgerhq/live-common

Version:
279 lines • 11.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.shouldSyncBeforeTx = void 0; exports.setGlobalOnBridgeError = setGlobalOnBridgeError; exports.getGlobalOnBridgeError = getGlobalOnBridgeError; const bignumber_js_1 = require("bignumber.js"); const react_1 = require("react"); const logs_1 = require("@ledgerhq/logs"); const _1 = require("."); const account_1 = require("../account"); const promise_1 = require("../promise"); const LiveConfig_1 = require("@ledgerhq/live-config/LiveConfig"); const initial = { account: null, parentAccount: null, transaction: null, status: { errors: {}, warnings: {}, estimatedFees: new bignumber_js_1.BigNumber(0), amount: new bignumber_js_1.BigNumber(0), totalSpent: new bignumber_js_1.BigNumber(0), }, statusOnTransaction: null, errorAccount: null, errorStatus: null, syncing: false, synced: false, }; const shouldSyncBeforeTx = (currency) => { const currencyConfig = LiveConfig_1.LiveConfig.getValueByKey(`config_currency_${currency.id}`); const sharedConfig = LiveConfig_1.LiveConfig.getValueByKey("config_currency"); if (currencyConfig && "syncBeforeTx" in currencyConfig) { return currencyConfig.syncBeforeTx === true; } else { return sharedConfig && "syncBeforeTx" in sharedConfig && sharedConfig.syncBeforeTx === true; } }; exports.shouldSyncBeforeTx = shouldSyncBeforeTx; const makeInit = (optionalInit) => () => { let s = initial; if (optionalInit) { const patch = optionalInit(); const { account, parentAccount, transaction } = patch; if (account) { s = reducer(s, { type: "setAccount", account, parentAccount, }); } if (transaction) { s = reducer(s, { type: "setTransaction", transaction, }); } } return s; }; const reducer = (state, action) => { switch (action.type) { case "setAccount": { const { account, parentAccount } = action; try { const mainAccount = (0, account_1.getMainAccount)(account, parentAccount); const bridge = (0, _1.getAccountBridge)(account, parentAccount); const subAccountId = account.type !== "Account" && account.id; let t = bridge.createTransaction(mainAccount); if ( // @ts-expect-error transaction.mode is not available on all union types. type guard is required state.transaction?.mode && // @ts-expect-error transaction.mode is not available on all union types. type guard is required state.transaction.mode !== t.mode) { t = bridge.updateTransaction(t, { // @ts-expect-error transaction.mode is not available on all union types. type guard is required mode: state.transaction.mode, }); } if (subAccountId) { t = { ...t, subAccountId }; } return { ...initial, account, parentAccount, transaction: t, syncing: false, synced: false, }; } catch (e) { return { ...initial, account, parentAccount, errorAccount: e, syncing: false, synced: false, }; } } case "setTransaction": if (state.transaction === action.transaction) return state; return { ...state, transaction: action.transaction }; case "updateTransaction": { if (!state.transaction) return state; const transaction = action.updater(state.transaction); if (state.transaction === transaction) return state; return { ...state, transaction }; } case "onStatus": return { ...state, errorStatus: null, transaction: action.transaction, status: action.status, statusOnTransaction: action.transaction, }; case "onStatusError": if (action.error === state.errorStatus) return state; return { ...state, errorStatus: action.error }; case "onStartSync": return { ...state, syncing: true, synced: false }; case "onSync": return { ...state, syncing: false, synced: true }; default: return state; } }; const INITIAL_ERROR_RETRY_DELAY = 1000; const ERROR_RETRY_DELAY_MULTIPLIER = 1.5; const DEBOUNCE_STATUS_DELAY = 300; const useBridgeTransaction = (optionalInit) => { const [{ account, parentAccount, transaction, status, statusOnTransaction, syncing, synced, errorAccount, errorStatus, }, dispatch,] = (0, react_1.useReducer)(reducer, undefined, makeInit(optionalInit)); const setAccount = (0, react_1.useCallback)((account, parentAccount) => dispatch({ type: "setAccount", account, parentAccount, }), [dispatch]); const setTransaction = (0, react_1.useCallback)(transaction => dispatch({ type: "setTransaction", transaction, }), [dispatch]); const updateTransaction = (0, react_1.useCallback)(updater => dispatch({ type: "updateTransaction", updater, }), [dispatch]); const mainAccount = account ? (0, account_1.getMainAccount)(account, parentAccount) : null; const errorDelay = (0, react_1.useRef)(INITIAL_ERROR_RETRY_DELAY); const statusIsPending = (0, react_1.useRef)(false); // Stores if status already being processed const shouldSync = mainAccount && (0, exports.shouldSyncBeforeTx)(mainAccount.currency); (0, react_1.useEffect)(() => { if (mainAccount === null || synced || syncing) return; if (!shouldSync) return; // skip sync if not required by currency config dispatch({ type: "onStartSync" }); const bridge = (0, _1.getAccountBridge)(mainAccount, null); const sub = bridge.sync(mainAccount, { paginationConfig: {} }).subscribe({ error: (_) => { // we do not block the user in case of error for now but it should be the case dispatch({ type: "onSync" }); }, complete: () => { dispatch({ type: "onSync" }); }, }); return () => { sub.unsubscribe(); }; }, [mainAccount, synced, syncing, shouldSync]); const bridgePending = transaction !== statusOnTransaction; // when transaction changes, prepare the transaction (0, react_1.useEffect)(() => { let ignore = false; let errorTimeout; // If bridge is not pending, transaction change is due to // the last onStatus dispatch (prepareTransaction changed original transaction) and must be ignored if (!bridgePending && !synced) return; if (mainAccount && transaction) { // We don't debounce first status refresh, but any subsequent to avoid multiple calls // First call is immediate const debounce = statusIsPending.current ? (0, promise_1.delay)(DEBOUNCE_STATUS_DELAY) : null; statusIsPending.current = true; // consider pending until status is resolved (error or success) Promise.resolve(debounce) .then(() => (0, _1.getAccountBridge)(mainAccount, null)) .then(async (bridge) => { if (ignore) return; const preparedTransaction = await bridge.prepareTransaction(mainAccount, transaction); if (ignore) return; const status = await bridge.getTransactionStatus(mainAccount, preparedTransaction); if (ignore) return; return { preparedTransaction, status, }; }) .then(result => { if (ignore || !result) return; const { preparedTransaction, status } = result; errorDelay.current = INITIAL_ERROR_RETRY_DELAY; // reset delay statusIsPending.current = false; // status is now synced with transaction dispatch({ type: "onStatus", status, transaction: preparedTransaction, }); }, e => { if (ignore) return; statusIsPending.current = false; dispatch({ type: "onStatusError", error: e, }); (0, logs_1.log)("useBridgeTransaction", "prepareTransaction failed " + String(e)); // After X seconds of hanging in this error case, we try again (0, logs_1.log)("useBridgeTransaction", "retrying prepareTransaction..."); errorTimeout = setTimeout(() => { // $FlowFixMe (mobile) errorDelay.current *= ERROR_RETRY_DELAY_MULTIPLIER; // increase delay // $FlowFixMe const transactionCopy = { ...transaction, }; dispatch({ type: "setTransaction", transaction: transactionCopy, }); // $FlowFixMe (mobile) }, errorDelay.current); }); } return () => { ignore = true; if (errorTimeout) { clearTimeout(errorTimeout); errorTimeout = null; } }; }, [transaction, mainAccount, bridgePending, dispatch, synced]); const bridgeError = errorAccount || errorStatus; (0, react_1.useEffect)(() => { if (bridgeError && globalOnBridgeError) { globalOnBridgeError(bridgeError); } }, [bridgeError]); return { transaction, setTransaction, updateTransaction, status, account, parentAccount, setAccount, bridgeError, bridgePending: bridgePending && (shouldSync ? !synced : true), }; }; let globalOnBridgeError = null; // allows to globally set a bridge error catch function in order to log it / report to sentry / ... function setGlobalOnBridgeError(f) { globalOnBridgeError = f; } function getGlobalOnBridgeError() { return globalOnBridgeError; } exports.default = useBridgeTransaction; //# sourceMappingURL=useBridgeTransaction.js.map