UNPKG

@ledgerhq/live-common

Version:
277 lines • 14.3 kB
import { getAbandonSeedAddress } from "@ledgerhq/cryptoassets"; import { TransportStatusError, WrongDeviceForAccountPayout, WrongDeviceForAccountRefund, } from "@ledgerhq/errors"; import Exchange, { decodePayloadProtobuf, } from "@ledgerhq/hw-app-exchange"; import network from "@ledgerhq/live-network/network"; import { log } from "@ledgerhq/logs"; import { BigNumber } from "bignumber.js"; import invariant from "invariant"; import { firstValueFrom, from, Observable } from "rxjs"; import { secp256k1 } from "@noble/curves/secp256k1"; import { getCurrencyExchangeConfig } from "../"; import { getAccountCurrency, getMainAccount } from "../../account"; import { getAccountBridge } from "../../bridge"; import { getEnv } from "@ledgerhq/live-env"; import { SwapGenericAPIError, SwapRateExpiredError, TransactionRefusedOnDevice, } from "../../errors"; import { withDevice } from "../../hw/deviceAccess"; import { delay } from "../../promise"; import { getSwapAPIBaseURL, getSwapUserIP } from "./"; import { mockInitSwap } from "./mock"; import { convertToAppExchangePartnerKey, getSwapProvider } from "../providers"; import { getDefaultAccountName } from "@ledgerhq/live-wallet/accountName"; const withDevicePromise = (deviceId, fn) => firstValueFrom(withDevice(deviceId)(transport => from(fn(transport)))); // init a swap with the Exchange app // throw if TransactionStatus have errors // you get at the end a final Transaction to be done (it's not yet signed, nor broadcasted!) and a swapId const initSwap = (input) => { let swapId; let { transaction } = input; const { exchange, exchangeRate, deviceId } = input; if (getEnv("MOCK")) return mockInitSwap(exchange, exchangeRate, transaction); return new Observable(o => { let unsubscribed = false; const confirmSwap = async () => { let ignoreTransportError; let magnitudeAwareRate; log("swap", `attempt to connect to ${deviceId}`); await withDevicePromise(deviceId, async (transport) => { const ratesFlag = exchangeRate.tradeMethod === "fixed" ? 0 /* RateTypes.Fixed */ : 1 /* RateTypes.Floating */; const swap = new Exchange(transport, 0 /* ExchangeTypes.Swap */, ratesFlag); // NB this id is crucial to prevent replay attacks, if it changes // we need to start the flow again. const deviceTransactionId = await swap.startNewTransaction(); if (unsubscribed) return; const { provider, rateId } = exchangeRate; const { fromParentAccount, fromAccount, toParentAccount, toAccount } = exchange; const { amount } = transaction; const refundCurrency = getAccountCurrency(fromAccount); const unitFrom = getAccountCurrency(exchange.fromAccount).units[0]; const payoutCurrency = getAccountCurrency(toAccount); const refundAccount = getMainAccount(fromAccount, fromParentAccount); const payoutAccount = getMainAccount(toAccount, toParentAccount); const apiAmount = new BigNumber(amount).div(new BigNumber(10).pow(unitFrom.magnitude)); // Request a swap, this locks the rates for fixed trade method only. // NB Added the try/catch because of the API stability issues. let res; const swapProviderConfig = await getSwapProvider(provider); const headers = { EquipmentId: getEnv("USER_ID"), ...(getSwapUserIP() !== undefined ? getSwapUserIP() : {}), }; const data = { provider, amountFrom: apiAmount.toString(), amountFromInSmallestDenomination: amount.toNumber(), from: refundCurrency.id, to: payoutCurrency.id, address: payoutAccount.freshAddress, refundAddress: refundAccount.freshAddress, deviceTransactionId, ...(rateId && ratesFlag === 0 /* RateTypes.Fixed */ ? { rateId, } : {}), // NB float rates dont need rate ids. }; try { res = await network({ method: "POST", url: `${getSwapAPIBaseURL()}/swap`, headers, data, }); if (unsubscribed || !res || !res.data) return; } catch (e) { if (e.msg.messageKey == "WRONG_OR_EXPIRED_RATE_ID") { o.next({ type: "init-swap-error", error: new SwapRateExpiredError(), swapId, }); } o.next({ type: "init-swap-error", error: new SwapGenericAPIError(), swapId, }); o.complete(); return; } const swapResult = res.data; swapId = swapResult.swapId; const accountBridge = getAccountBridge(refundAccount); transaction = accountBridge.updateTransaction(transaction, { recipient: swapResult.payinAddress, }); if (refundCurrency.id === "ripple") { transaction = accountBridge.updateTransaction(transaction, { tag: new BigNumber(swapResult.payinExtraId).toNumber(), }); invariant(transaction.tag, "Refusing to swap xrp without a destination tag"); } else if (refundCurrency.id === "stellar") { transaction = accountBridge.updateTransaction(transaction, { memoValue: swapResult.payinExtraId, memoType: "MEMO_TEXT", }); invariant(transaction.memoValue, "Refusing to swap xlm without a destination memo"); } // Triplecheck we're not working with an abandonseed recipient anymore invariant(transaction.recipient !== getAbandonSeedAddress(refundCurrency.type === "TokenCurrency" ? refundCurrency.parentCurrency.id : refundCurrency.id), "Recipient address should never be the abandonseed address"); transaction = await accountBridge.prepareTransaction(refundAccount, transaction); if (unsubscribed) return; const { errors, estimatedFees } = await accountBridge.getTransactionStatus(refundAccount, transaction); if (unsubscribed) return; const errorsKeys = Object.keys(errors); if (errorsKeys.length > 0) { throw errors[errorsKeys[0]]; // throw the first error } if (swapProviderConfig.useInExchangeApp === false) { throw new Error(`Unsupported provider type ${swapProviderConfig.type}`); } // Prepare swap app to receive the tx to forward. await swap.setPartnerKey(convertToAppExchangePartnerKey(swapProviderConfig)); if (unsubscribed) return; await swap.checkPartner(swapProviderConfig.signature); if (unsubscribed) return; await swap.processTransaction(Buffer.from(swapResult.binaryPayload, "hex"), estimatedFees); if (unsubscribed) return; const goodSign = (() => { const sig = secp256k1.Signature.fromCompact(Buffer.from(swapResult.signature, "hex")); return Buffer.from(sig.toDERRawBytes()); })(); await swap.checkTransactionSignature(goodSign); if (unsubscribed) return; const mainPayoutCurrency = getAccountCurrency(payoutAccount); invariant(mainPayoutCurrency.type === "CryptoCurrency", "This should be a cryptocurrency"); // FIXME: invariant not triggering typescriptp type guard if (mainPayoutCurrency.type !== "CryptoCurrency") { throw new Error("This should be a cryptocurrency"); } const mainPayoutBridge = getAccountBridge(payoutAccount); const payoutAddressParameters = mainPayoutBridge.getSerializedAddressParameters(payoutAccount, mainPayoutCurrency.id); if (unsubscribed) return; if (!payoutAddressParameters) { throw new Error(`Family not supported: ${mainPayoutCurrency.family}`); } const { config: payoutAddressConfig, signature: payoutAddressConfigSignature } = await getCurrencyExchangeConfig(payoutCurrency); try { await swap.validatePayoutOrAsset(payoutAddressConfig, payoutAddressConfigSignature, payoutAddressParameters); } catch (e) { if (e instanceof TransportStatusError && e.statusCode === 0x6a83) { throw new WrongDeviceForAccountPayout(undefined, { accountName: getDefaultAccountName(payoutAccount), }); } throw e; } if (unsubscribed) return; const mainRefundCurrency = getAccountCurrency(refundAccount); invariant(mainRefundCurrency.type === "CryptoCurrency", "This should be a cryptocurrency"); // FIXME: invariant not triggering typescriptp type guard if (mainRefundCurrency.type !== "CryptoCurrency") { throw new Error("This should be a cryptocurrency"); } const refundAddressParameters = accountBridge.getSerializedAddressParameters(refundAccount, mainRefundCurrency.id); if (unsubscribed) return; if (!refundAddressParameters) { throw new Error(`Family not supported: ${mainRefundCurrency.family}`); } const { config: refundAddressConfig, signature: refundAddressConfigSignature } = await getCurrencyExchangeConfig(refundCurrency); if (unsubscribed) return; // NB Floating rates may change the original amountTo so we can pass an override // to properly render the amount on the device confirmation steps. Although changelly // made the calculation inside the binary payload, we still have to deal with it here // to not break their other clients. let amountExpectedTo; if (swapResult.binaryPayload) { const decodePayload = await decodePayloadProtobuf(swapResult.binaryPayload); amountExpectedTo = new BigNumber(decodePayload.amountToWallet.toString()); magnitudeAwareRate = transaction.amount && amountExpectedTo.dividedBy(transaction.amount); } let amountExpectedFrom; if (swapResult.binaryPayload) { const decodePayload = await decodePayloadProtobuf(swapResult.binaryPayload); amountExpectedFrom = new BigNumber(decodePayload.amountToProvider.toString()); if (data.amountFromInSmallestDenomination !== amountExpectedFrom.toNumber()) throw new Error("AmountFrom received from partner's payload mismatch user input"); } o.next({ type: "init-swap-requested", amountExpectedTo, estimatedFees, }); try { await swap.checkRefundAddress(refundAddressConfig, refundAddressConfigSignature, refundAddressParameters); } catch (e) { if (e instanceof TransportStatusError && e.statusCode === 0x6a83) { throw new WrongDeviceForAccountRefund(undefined, { accountName: getDefaultAccountName(refundAccount), }); } throw e; } if (unsubscribed) return; ignoreTransportError = true; await swap.signCoinTransaction(); }).catch(e => { if (ignoreTransportError) return; if (e instanceof TransportStatusError && e.statusCode === 0x6a84) { throw new TransactionRefusedOnDevice("", { step: "SIGN_COIN_TRANSACTION" }); } throw e; }); if (!swapId) return; log("swap", "awaiting device disconnection"); await delay(3000); if (unsubscribed) return; o.next({ type: "init-swap-result", initSwapResult: { transaction, swapId, magnitudeAwareRate, }, }); }; confirmSwap().then(() => { o.complete(); unsubscribed = true; }, e => { o.next({ type: "init-swap-error", error: e, swapId, }); o.complete(); unsubscribed = true; }); return () => { unsubscribed = true; }; }); }; export default initSwap; //# sourceMappingURL=initSwap.js.map