UNPKG

@ledgerhq/live-common

Version:
229 lines (198 loc) • 7.9 kB
import { secp256k1 } from "@noble/curves/secp256k1"; import { firstValueFrom, from, Observable } from "rxjs"; import { TransportStatusError, WrongDeviceForAccount } from "@ledgerhq/errors"; import { delay } from "../../../promise"; import { createExchange, ExchangeTypes, isExchangeTypeNg, PayloadSignatureComputedFormat, } from "@ledgerhq/hw-app-exchange"; import { getAccountCurrency, getMainAccount } from "../../../account"; import { getAccountBridge } from "../../../bridge"; import { TransactionRefusedOnDevice } from "../../../errors"; import { withDevice } from "../../../hw/deviceAccess"; import { getCurrencyExchangeConfig } from "../.."; import { convertToAppExchangePartnerKey, getProviderConfig } from "../../providers"; import type { CompleteExchangeInputFund, CompleteExchangeInputSell, CompleteExchangeRequestEvent, } from "../types"; import { CompleteExchangeError, CompleteExchangeStep, convertTransportError } from "../../error"; const withDevicePromise = (deviceId, fn) => firstValueFrom(withDevice(deviceId)(transport => from(fn(transport)))); const completeExchange = ( input: CompleteExchangeInputFund | CompleteExchangeInputSell, ): Observable<CompleteExchangeRequestEvent> => { let { transaction } = input; // TODO build a tx from the data const { deviceId, exchange, provider, binaryPayload, signature, exchangeType, rateType, // TODO Pass fixed/float for UI switch ? } = input; const { fromAccount, fromParentAccount } = exchange; return new Observable(o => { let unsubscribed = false; let ignoreTransportError = false; let currentStep: CompleteExchangeStep = "INIT"; const confirmExchange = async () => { await withDevicePromise(deviceId, async transport => { const providerNameAndSignature = await getProviderConfig(exchangeType, provider); if (!providerNameAndSignature) throw new CompleteExchangeError( "INIT", "ProviderConfigError", "Could not get provider infos", ); const exchange = createExchange( transport, exchangeType, rateType, providerNameAndSignature.version, ); const mainAccount = getMainAccount(fromAccount, fromParentAccount); const accountBridge = getAccountBridge(mainAccount); const mainPayoutCurrency = getAccountCurrency(mainAccount); const payoutCurrency = getAccountCurrency(fromAccount); if (mainPayoutCurrency.type !== "CryptoCurrency") throw new CompleteExchangeError( "INIT", "InvalidCurrencyType", `This should be a cryptocurrency, got ${mainPayoutCurrency.type}`, ); transaction = await accountBridge.prepareTransaction(mainAccount, transaction); if (unsubscribed) return; const { errors, estimatedFees } = await accountBridge.getTransactionStatus( mainAccount, transaction, ); if (unsubscribed) return; const errorsKeys = Object.keys(errors); if (errorsKeys.length > 0) { const firstKey = errorsKeys[0]; const validationError = errors[firstKey]; throw new CompleteExchangeError( currentStep, firstKey, validationError.message || validationError.name || `Transaction validation failed: ${firstKey}`, ); } currentStep = "SET_PARTNER_KEY"; await exchange.setPartnerKey(convertToAppExchangePartnerKey(providerNameAndSignature)); if (unsubscribed) return; currentStep = "CHECK_PARTNER"; await exchange.checkPartner(providerNameAndSignature.signature!); if (unsubscribed) return; currentStep = "PROCESS_TRANSACTION"; const { payload, format }: { payload: Buffer; format: PayloadSignatureComputedFormat } = isExchangeTypeNg(exchange.transactionType) ? { payload: Buffer.from("." + binaryPayload), format: "jws" } : { payload: Buffer.from(binaryPayload, "hex"), format: "raw" }; await exchange.processTransaction(payload, estimatedFees, format); if (unsubscribed) return; const goodSign = convertSignature(signature, exchange.transactionType); currentStep = "CHECK_TRANSACTION_SIGNATURE"; await exchange.checkTransactionSignature(goodSign); if (unsubscribed) return; const payoutAddressParameters = accountBridge.getSerializedAddressParameters(mainAccount); if (unsubscribed) return; if (!payoutAddressParameters) { throw new CompleteExchangeError( currentStep, "UnsupportedFamily", `Family not supported: ${mainPayoutCurrency.family}`, ); } const { config: payoutAddressConfig, signature: payoutAddressConfigSignature } = await getCurrencyExchangeConfig(payoutCurrency); try { o.next({ type: "complete-exchange-requested", estimatedFees: estimatedFees.toString(), }); currentStep = "CHECK_PAYOUT_ADDRESS"; await exchange.validatePayoutOrAsset( payoutAddressConfig, payoutAddressConfigSignature, payoutAddressParameters, ); } catch (e) { if (e instanceof TransportStatusError && e.statusCode === 0x6a83) { throw new WrongDeviceForAccount(); } throw convertTransportError(currentStep, e); } if (unsubscribed) return; ignoreTransportError = true; currentStep = "SIGN_COIN_TRANSACTION"; await exchange.signCoinTransaction(); }).catch(e => { if (ignoreTransportError) return; if ( e instanceof TransportStatusError && (e.statusCode === 0x6a84 || e.statusCode === 0x5501) ) { throw new TransactionRefusedOnDevice(); } // Preserve known error types checked by instanceof downstream if (e instanceof CompleteExchangeError) throw e; if (e instanceof WrongDeviceForAccount || e instanceof TransactionRefusedOnDevice) throw e; // Wrap any remaining unknown errors with the current step context throw new CompleteExchangeError( currentStep, e?.name, e?.message || "Unknown exchange error", ); }); await delay(3000); o.next({ type: "complete-exchange-result", completeExchangeResult: transaction, }); if (unsubscribed) return; }; confirmExchange().then( () => { o.complete(); unsubscribed = true; }, e => { o.next({ type: "complete-exchange-error", error: e, }); o.complete(); unsubscribed = true; }, ); return () => { unsubscribed = true; }; }); }; /** * For the Fund and Swap flow, the signature sent to the nano needs to * be in DER format, which is not the case for Sell flow. Hence the * ternary. * cf. https://github.com/LedgerHQ/app-exchange/blob/e67848f136dc7227521791b91f608f7cd32e7da7/src/check_tx_signature.c#L14-L32 * @param {Buffer} bufferSignature * @param {ExchangeTypes} exchangeType * @return {Buffer} The correct format Buffer for AppExchange call. */ function convertSignature(signature: string, exchangeType: ExchangeTypes): Buffer { if (isExchangeTypeNg(exchangeType)) { const base64Signature = signature.replace(/-/g, "+").replace(/_/g, "/"); return Buffer.from(base64Signature, "base64"); } if (exchangeType === ExchangeTypes.Sell) return Buffer.from(signature, "hex"); const sig = secp256k1.Signature.fromCompact(Buffer.from(signature, "hex")); return Buffer.from(sig.toDERRawBytes()); } export default completeExchange;