@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
305 lines • 16 kB
JavaScript
"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