@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
229 lines (198 loc) • 7.9 kB
text/typescript
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;