UNPKG

@ledgerhq/live-common

Version:
305 lines • 16 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const cryptoassets_1 = require("@ledgerhq/cryptoassets"); const errors_1 = require("@ledgerhq/errors"); const hw_app_exchange_1 = __importStar(require("@ledgerhq/hw-app-exchange")); const network_1 = __importDefault(require("@ledgerhq/live-network/network")); const logs_1 = require("@ledgerhq/logs"); const bignumber_js_1 = require("bignumber.js"); const invariant_1 = __importDefault(require("invariant")); const rxjs_1 = require("rxjs"); const secp256k1_1 = require("@noble/curves/secp256k1"); const __1 = require("../"); const account_1 = require("../../account"); const bridge_1 = require("../../bridge"); const live_env_1 = require("@ledgerhq/live-env"); const errors_2 = require("../../errors"); const deviceAccess_1 = require("../../hw/deviceAccess"); const promise_1 = require("../../promise"); const _1 = require("./"); const mock_1 = require("./mock"); const providers_1 = require("../providers"); const accountName_1 = require("@ledgerhq/live-wallet/accountName"); const withDevicePromise = (deviceId, fn) => (0, rxjs_1.firstValueFrom)((0, deviceAccess_1.withDevice)(deviceId)(transport => (0, rxjs_1.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 ((0, live_env_1.getEnv)("MOCK")) return (0, mock_1.mockInitSwap)(exchange, exchangeRate, transaction); return new rxjs_1.Observable(o => { let unsubscribed = false; const confirmSwap = async () => { let ignoreTransportError; let magnitudeAwareRate; (0, logs_1.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 hw_app_exchange_1.default(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 = (0, account_1.getAccountCurrency)(fromAccount); const unitFrom = (0, account_1.getAccountCurrency)(exchange.fromAccount).units[0]; const payoutCurrency = (0, account_1.getAccountCurrency)(toAccount); const refundAccount = (0, account_1.getMainAccount)(fromAccount, fromParentAccount); const payoutAccount = (0, account_1.getMainAccount)(toAccount, toParentAccount); const apiAmount = new bignumber_js_1.BigNumber(amount).div(new bignumber_js_1.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 (0, providers_1.getSwapProvider)(provider); const headers = { EquipmentId: (0, live_env_1.getEnv)("USER_ID"), ...((0, _1.getSwapUserIP)() !== undefined ? (0, _1.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 (0, network_1.default)({ method: "POST", url: `${(0, _1.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 errors_2.SwapRateExpiredError(), swapId, }); } o.next({ type: "init-swap-error", error: new errors_2.SwapGenericAPIError(), swapId, }); o.complete(); return; } const swapResult = res.data; swapId = swapResult.swapId; const accountBridge = (0, bridge_1.getAccountBridge)(refundAccount); transaction = accountBridge.updateTransaction(transaction, { recipient: swapResult.payinAddress, }); if (refundCurrency.id === "ripple") { transaction = accountBridge.updateTransaction(transaction, { tag: new bignumber_js_1.BigNumber(swapResult.payinExtraId).toNumber(), }); (0, invariant_1.default)(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", }); (0, invariant_1.default)(transaction.memoValue, "Refusing to swap xlm without a destination memo"); } // Triplecheck we're not working with an abandonseed recipient anymore (0, invariant_1.default)(transaction.recipient !== (0, cryptoassets_1.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((0, providers_1.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_1.secp256k1.Signature.fromCompact(Buffer.from(swapResult.signature, "hex")); return Buffer.from(sig.toDERRawBytes()); })(); await swap.checkTransactionSignature(goodSign); if (unsubscribed) return; const mainPayoutCurrency = (0, account_1.getAccountCurrency)(payoutAccount); (0, invariant_1.default)(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 = (0, bridge_1.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 (0, __1.getCurrencyExchangeConfig)(payoutCurrency); try { await swap.validatePayoutOrAsset(payoutAddressConfig, payoutAddressConfigSignature, payoutAddressParameters); } catch (e) { if (e instanceof errors_1.TransportStatusError && e.statusCode === 0x6a83) { throw new errors_1.WrongDeviceForAccountPayout(undefined, { accountName: (0, accountName_1.getDefaultAccountName)(payoutAccount), }); } throw e; } if (unsubscribed) return; const mainRefundCurrency = (0, account_1.getAccountCurrency)(refundAccount); (0, invariant_1.default)(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 (0, __1.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 (0, hw_app_exchange_1.decodePayloadProtobuf)(swapResult.binaryPayload); amountExpectedTo = new bignumber_js_1.BigNumber(decodePayload.amountToWallet.toString()); magnitudeAwareRate = transaction.amount && amountExpectedTo.dividedBy(transaction.amount); } let amountExpectedFrom; if (swapResult.binaryPayload) { const decodePayload = await (0, hw_app_exchange_1.decodePayloadProtobuf)(swapResult.binaryPayload); amountExpectedFrom = new bignumber_js_1.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 errors_1.TransportStatusError && e.statusCode === 0x6a83) { throw new errors_1.WrongDeviceForAccountRefund(undefined, { accountName: (0, accountName_1.getDefaultAccountName)(refundAccount), }); } throw e; } if (unsubscribed) return; ignoreTransportError = true; await swap.signCoinTransaction(); }).catch(e => { if (ignoreTransportError) return; if (e instanceof errors_1.TransportStatusError && e.statusCode === 0x6a84) { throw new errors_2.TransactionRefusedOnDevice("", { step: "SIGN_COIN_TRANSACTION" }); } throw e; }); if (!swapId) return; (0, logs_1.log)("swap", "awaiting device disconnection"); await (0, promise_1.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; }; }); }; exports.default = initSwap; //# sourceMappingURL=initSwap.js.map