@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
273 lines • 10.6 kB
JavaScript
import { BigNumber } from "bignumber.js";
import { useEffect, useReducer, useCallback, useRef } from "react";
import { log } from "@ledgerhq/logs";
import { getAccountBridge } from ".";
import { getMainAccount } from "../account";
import { delay } from "../promise";
import { LiveConfig } from "@ledgerhq/live-config/LiveConfig";
const initial = {
account: null,
parentAccount: null,
transaction: null,
status: {
errors: {},
warnings: {},
estimatedFees: new BigNumber(0),
amount: new BigNumber(0),
totalSpent: new BigNumber(0),
},
statusOnTransaction: null,
errorAccount: null,
errorStatus: null,
syncing: false,
synced: false,
};
export const shouldSyncBeforeTx = (currency) => {
const currencyConfig = LiveConfig.getValueByKey(`config_currency_${currency.id}`);
const sharedConfig = LiveConfig.getValueByKey("config_currency");
if (currencyConfig && "syncBeforeTx" in currencyConfig) {
return currencyConfig.syncBeforeTx === true;
}
else {
return sharedConfig && "syncBeforeTx" in sharedConfig && sharedConfig.syncBeforeTx === true;
}
};
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 = getMainAccount(account, parentAccount);
const bridge = 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,] = useReducer(reducer, undefined, makeInit(optionalInit));
const setAccount = useCallback((account, parentAccount) => dispatch({
type: "setAccount",
account,
parentAccount,
}), [dispatch]);
const setTransaction = useCallback(transaction => dispatch({
type: "setTransaction",
transaction,
}), [dispatch]);
const updateTransaction = useCallback(updater => dispatch({
type: "updateTransaction",
updater,
}), [dispatch]);
const mainAccount = account ? getMainAccount(account, parentAccount) : null;
const errorDelay = useRef(INITIAL_ERROR_RETRY_DELAY);
const statusIsPending = useRef(false); // Stores if status already being processed
const shouldSync = mainAccount && shouldSyncBeforeTx(mainAccount.currency);
useEffect(() => {
if (mainAccount === null || synced || syncing)
return;
if (!shouldSync)
return; // skip sync if not required by currency config
dispatch({ type: "onStartSync" });
const bridge = 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
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 ? delay(DEBOUNCE_STATUS_DELAY) : null;
statusIsPending.current = true; // consider pending until status is resolved (error or success)
Promise.resolve(debounce)
.then(() => 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,
});
log("useBridgeTransaction", "prepareTransaction failed " + String(e));
// After X seconds of hanging in this error case, we try again
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;
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 / ...
export function setGlobalOnBridgeError(f) {
globalOnBridgeError = f;
}
export function getGlobalOnBridgeError() {
return globalOnBridgeError;
}
export default useBridgeTransaction;
//# sourceMappingURL=useBridgeTransaction.js.map