@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
189 lines (167 loc) • 5.32 kB
text/typescript
import BigNumber from "bignumber.js";
import {
CardanoAccount,
Token,
Transaction as CardanoTransaction,
TransactionStatus,
} from "@ledgerhq/coin-cardano/types";
import { utils as TyphonUtils } from "@stricahq/typhonjs";
import type { AccountBridge, CurrencyBridge, Account } from "@ledgerhq/types-live";
import { decodeTokenAssetId, decodeTokenCurrencyId } from "@ledgerhq/coin-cardano/buildSubAccounts";
import {
AmountRequired,
FeeNotLoaded,
InvalidAddress,
NotEnoughBalance,
RecipientRequired,
} from "@ledgerhq/errors";
import { CardanoMinAmountError, CardanoNotEnoughFunds } from "@ledgerhq/coin-cardano/errors";
import { buildTransaction } from "@ledgerhq/coin-cardano/buildTransaction";
import { CARDANO_MAX_SUPPLY } from "@ledgerhq/coin-cardano/constants";
import {
getSerializedAddressParameters,
updateTransaction,
} from "@ledgerhq/ledger-wallet-framework/bridge/jsHelpers";
import {
scanAccounts,
signOperation,
signRawOperation,
broadcast,
sync,
makeAccountBridgeReceive,
} from "../../../bridge/mockHelpers";
import { validateAddress } from "../../../bridge/validateAddress";
const receive = makeAccountBridgeReceive();
const createTransaction = (): CardanoTransaction => {
return {
family: "cardano",
mode: "send",
amount: new BigNumber(100),
recipient: "",
poolId: "",
};
};
const estimateMaxSpendable = ({ account }) => {
return account.balance;
};
const isValidAddress = (address: string) => {
return address.length > 0;
};
const getTransactionStatus = async (
account: CardanoAccount,
transaction: CardanoTransaction,
): Promise<TransactionStatus> => {
const errors = { fees: new Error(), recipient: new Error(), amount: new Error() };
const warnings = {};
const estimatedFees = transaction.fees || new BigNumber(0);
const tokenAccount =
transaction.subAccountId && account.subAccounts
? account.subAccounts.find(a => {
return a.id === transaction.subAccountId;
})
: undefined;
const mockAccount = tokenAccount || account;
let amount = transaction.useAllAmount
? await estimateMaxSpendable({ account: mockAccount })
: transaction.amount;
let totalSpent = transaction.amount.plus(estimatedFees);
const useAllAmount = Boolean(transaction.useAllAmount);
let tokensToSend: Array<Token> = [];
const isTokenTx = !!transaction.subAccountId;
if (isTokenTx) {
// Token transaction
if (!tokenAccount || tokenAccount.type !== "TokenAccount") {
throw new Error("TokenAccount not found");
}
const { assetId } = decodeTokenCurrencyId(tokenAccount.token.id);
const { policyId, assetName } = decodeTokenAssetId(assetId);
amount = transaction.useAllAmount ? tokenAccount.balance : transaction.amount;
totalSpent = amount;
tokensToSend = [
{
policyId,
assetName,
amount,
},
];
} else {
amount = transaction.useAllAmount ? await estimateMaxSpendable({ account }) : amount;
totalSpent = amount.plus(estimatedFees);
}
let minTransactionAmount = new BigNumber(0);
if (!transaction.fees) {
errors.fees = new FeeNotLoaded();
}
if (!transaction.recipient) {
errors.recipient = new RecipientRequired();
} else if (!isValidAddress(transaction.recipient)) {
errors.recipient = new InvalidAddress("", {
currencyName: account.currency.name,
});
} else {
// minTransactionAmount can only be calculated with valid recipient
const recipient = TyphonUtils.getAddressFromString(transaction.recipient);
minTransactionAmount = TyphonUtils.calculateMinUtxoAmountBabbage(
{
address: recipient,
amount: new BigNumber(CARDANO_MAX_SUPPLY),
tokens: tokensToSend,
},
new BigNumber(account.cardanoResources.protocolParams.utxoCostPerByte),
);
}
if (!amount.gt(0)) {
errors.amount = useAllAmount ? new CardanoNotEnoughFunds() : new AmountRequired();
} else if (!isTokenTx && amount.lt(minTransactionAmount)) {
errors.amount = new CardanoMinAmountError("", {
amount: minTransactionAmount.div(1e6).toString(),
});
} else if (tokenAccount ? totalSpent.gt(tokenAccount.balance) : totalSpent.gt(account.balance)) {
errors.amount = new NotEnoughBalance();
} else {
try {
await buildTransaction(account, transaction);
} catch (e: any) {
if (
e.message.toLowerCase() === "not enough ada" ||
e.message.toLowerCase() === "not enough tokens"
) {
errors.amount = new CardanoNotEnoughFunds();
}
}
}
return Promise.resolve({
errors,
warnings,
estimatedFees,
amount,
totalSpent,
});
};
const prepareTransaction = async (account: Account, transaction: CardanoTransaction) => {
transaction.fees = new BigNumber(100);
return transaction;
};
const accountBridge: AccountBridge<CardanoTransaction> = {
createTransaction,
updateTransaction,
getTransactionStatus,
estimateMaxSpendable,
prepareTransaction,
sync,
receive,
signOperation,
signRawOperation,
broadcast,
getSerializedAddressParameters,
validateAddress,
};
const currencyBridge: CurrencyBridge = {
preload: () => Promise.resolve({}),
hydrate: () => {},
scanAccounts,
};
export default {
accountBridge,
currencyBridge,
};