import invariant from "invariant"; import { BigNumber } from "bignumber.js"; import { AmountRequired, NotEnoughBalance, FeeNotLoaded, RecipientRequired, InvalidAddress, InvalidAddressBecauseDestinationIsAlsoSource, NotEnoughBalanceBecauseDestinationNotCreated, NotEnoughSpendableBalance, } from "@ledgerhq/errors"; import type { Account, AccountBridge, CurrencyBridge } from "@ledgerhq/types-live"; import { getSerializedAddressParameters } from "@ledgerhq/ledger-wallet-framework/bridge/jsHelpers"; import type { Transaction } from "../types"; import { StellarSourceHasMultiSign, StellarWrongMemoFormat } from "@ledgerhq/coin-stellar/errors"; import { getMainAccount } from "../../../account"; import { formatCurrencyUnit } from "../../../currencies"; import { scanAccounts, signOperation, signRawOperation, broadcast, sync, isInvalidRecipient, makeAccountBridgeReceive, } from "../../../bridge/mockHelpers"; import { validateAddress } from "../../../bridge/validateAddress"; const receive = makeAccountBridgeReceive(); const notCreatedStellarMockAddress = "GAW46JE3SHIAYLNNNQCAZFQ437WB5ZH7LDRDWR5LVDWHCTHCKYB6RCCH"; const multisignStellarMockAddress = "GCDDN6T2LJN3T7SPWJQV6BCCL5KNY5GBN7X4CMSZLDEXDHXAH32TOAHS"; const notCreatedAddresses: string[] = []; const multiSignAddresses: string[] = []; export function addNotCreatedStellarMockAddresses(addr: string) { notCreatedAddresses.push(addr); } export function addMultisignStellarMockAddresses(addr: string) { multiSignAddresses.push(addr); } addNotCreatedStellarMockAddresses(notCreatedStellarMockAddress); addMultisignStellarMockAddresses(multisignStellarMockAddress); const createTransaction = (): Transaction => ({ family: "stellar", amount: new BigNumber(0), baseReserve: null, networkInfo: null, fees: null, recipient: "", memoValue: null, memoType: null, useAllAmount: false, mode: "send", assetReference: "", assetOwner: "", }); const updateTransaction = (t, patch) => { return { ...t, ...patch }; }; const isMemoValid = (memoType: string, memoValue: string): boolean => { switch (memoType) { case "MEMO_TEXT": if (memoValue.length > 28) { return false; } break; case "MEMO_ID": if (new BigNumber(memoValue.toString()).isNaN()) { return false; } break; case "MEMO_HASH": case "MEMO_RETURN": if (!memoValue.length || memoValue.length !== 64) { return false; } break; } return true; }; const getTransactionStatus = async (a: Account, t: Transaction) => { const errors: { recipient?: Error; fees?: Error; amount?: Error; transaction?: Error; } = {}; const warnings = {}; const useAllAmount = !!t.useAllAmount; if (a.freshAddress === t.recipient) { errors.recipient = new InvalidAddressBecauseDestinationIsAlsoSource(); } else { if (!t.recipient) { errors.recipient = new RecipientRequired(""); } else if (isInvalidRecipient(t.recipient)) { errors.recipient = new InvalidAddress(""); } } if (multiSignAddresses.includes(a.freshAddress)) { errors.recipient = new StellarSourceHasMultiSign("", { currencyName: a.currency.name, }); } if (!t.fees || !t.baseReserve) { errors.fees = new FeeNotLoaded(); } const estimatedFees = !t.fees ? new BigNumber(0) : t.fees; const baseReserve = !t.baseReserve ? new BigNumber(0) : t.baseReserve; let amount = !useAllAmount ? t.amount : a.balance.minus(baseReserve).minus(estimatedFees); let totalSpent = !useAllAmount ? amount.plus(estimatedFees) : a.balance.minus(baseReserve); if (totalSpent.gt(a.balance.minus(baseReserve))) { errors.amount = new NotEnoughSpendableBalance("", { minimumAmount: formatCurrencyUnit(a.currency.units[0], baseReserve, { disableRounding: true, useGrouping: false, showCode: true, }), }); } if (!errors.amount && amount.plus(estimatedFees).plus(baseReserve).gt(a.balance)) { errors.amount = new NotEnoughBalance(); } if (!errors.recipient && !errors.amount && (amount.lt(0) || totalSpent.gt(a.balance))) { errors.amount = new NotEnoughBalance(); totalSpent = new BigNumber(0); amount = new BigNumber(0); } if (!errors.amount && amount.eq(0)) { errors.amount = new AmountRequired(); } // if amount < 1.0 you can't if (!errors.amount && notCreatedAddresses.includes(t.recipient) && amount.lt(10000000)) { errors.amount = new NotEnoughBalanceBecauseDestinationNotCreated("", { minimalAmount: "1 XLM", }); } if (t.memoType && t.memoValue && !isMemoValid(t.memoType, t.memoValue)) { errors.transaction = new StellarWrongMemoFormat(); } return Promise.resolve({ errors, warnings, estimatedFees, amount, totalSpent, }); }; const prepareTransaction = async (a, t) => { const networkInfo = t.networkInfo || { family: "stellar", fees: new BigNumber("100"), baseReserve: new BigNumber("100000"), }; invariant(networkInfo.family === "stellar", "stellar networkInfo expected"); const fees = t.fees || networkInfo.fees; const baseReserve = t.baseReserve || networkInfo.baseReserve; if (t.networkInfo !== networkInfo || t.fees !== fees || t.baseReserve !== baseReserve) { return { ...t, networkInfo, fees, baseReserve }; } return t; }; const estimateMaxSpendable = async ({ account, parentAccount, transaction }) => { const mainAccount = getMainAccount(account, parentAccount); const t = await prepareTransaction(mainAccount, { ...createTransaction(), recipient: notCreatedAddresses[0], // not used address ...transaction, useAllAmount: true, }); const s = await getTransactionStatus(mainAccount, t); return s.amount; }; const preload = async () => ({}); const hydrate = () => {}; const currencyBridge: CurrencyBridge = { preload, hydrate, scanAccounts, }; const accountBridge: AccountBridge = { createTransaction, updateTransaction, prepareTransaction, getTransactionStatus, sync, receive, signOperation, signRawOperation, broadcast, estimateMaxSpendable, getSerializedAddressParameters, validateAddress, }; export default { currencyBridge, accountBridge, };