@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
232 lines (197 loc) • 6.29 kB
text/typescript
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<Transaction> = {
createTransaction,
updateTransaction,
prepareTransaction,
getTransactionStatus,
sync,
receive,
signOperation,
signRawOperation,
broadcast,
estimateMaxSpendable,
getSerializedAddressParameters,
validateAddress,
};
export default {
currencyBridge,
accountBridge,
};