Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support/filecoin evm #7509

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ import {
IAddress,
PROTOCOL_INDICATOR,
fromEthAddress,
isEthAddress,
fromString,
toEthAddress,
} from "iso-filecoin/address";
import { log } from "@ledgerhq/logs";
import BigNumber from "bignumber.js";
import { fetchEthAddrForF1Fil } from "./api";

export type ValidateAddressResult =
| {
Expand All @@ -18,26 +17,14 @@ export type ValidateAddressResult =
isValid: false;
};

export const convertF4ToEthAddress = (addr: string) => {
const parsed = fromString(addr);

return toEthAddress(parsed);
};

export const convertF0toEthAddress = (addr: string): string => {
if (addr.slice(0, 2) != "f0") {
throw new Error("Address does not start with f0");
}

const id = new BigNumber(addr.slice(2));
const idHex = id.toString(16).padStart(16, "0");

return `0xff${"".padStart(22, "0")}${idHex}`;
};

export const isFilEthAddress = (addr: IAddress) =>
addr.protocol === PROTOCOL_INDICATOR.DELEGATED && addr.namespace === 10;

export const isIdAddress = (addr: IAddress) => addr.protocol === PROTOCOL_INDICATOR.ID;

export const isEthereumConvertableAddr = (addr: IAddress) =>
isIdAddress(addr) || isFilEthAddress(addr);

export const validateAddress = (input: string): ValidateAddressResult => {
try {
const parsedAddress = fromString(input);
Expand All @@ -47,8 +34,6 @@ export const validateAddress = (input: string): ValidateAddressResult => {
}

try {
// allow non 0x starting eth addresses as well
if (!input.startsWith("0x")) input = "0x" + input;
const parsedAddress = fromEthAddress(input, "mainnet");
return { isValid: true, parsedAddress };
} catch (error) {
Expand All @@ -68,48 +53,41 @@ export const isRecipientValidForTokenTransfer = (addr: string): boolean => {
return false;
}

const accountType = addr.substring(0, 2);
if (["f0", "0x", "f4"].includes(accountType)) {
if (isEthereumConvertableAddr(valid.parsedAddress)) {
return true;
}

return false;
};

export const convertAddressFilToEthSync = (addr: string): string => {
const recipientAddressProtocol = addr.slice(0, 2);

switch (recipientAddressProtocol) {
case "f0":
return convertF0toEthAddress(addr);
case "f4":
return convertF4ToEthAddress(addr);
case "0x":
return addr;
default:
throw new Error("supported address protocols are f0, f4");
export const getEquivalentAddress = (addr: string): string => {
if (isEthAddress(addr)) {
return fromEthAddress(addr, "mainnet").toString();
} else {
const parsed = fromString(addr);
if (isEthereumConvertableAddr(parsed)) {
return toEthAddress(parsed);
}
return "";
}
};

export const convertAddressFilToEthAsync = async (addr: string): Promise<string> => {
if (addr.length > 0 && addr.slice(0, 2) === "t1") {
addr = `f${addr.slice(1)}`;
export const convertAddressFilToEth = (addr: string): string => {
if (isEthAddress(addr)) {
return addr;
}
const recipientAddressProtocol = addr.slice(0, 2);

switch (recipientAddressProtocol) {
case "f1": {
const res = await fetchEthAddrForF1Fil(addr);
if (!res) {
throw new Error("recipient account id not available on the network");
}
return res;
}
case "f0":
case "f4":
case "0x":
return convertAddressFilToEthSync(addr);
default:
throw new Error("supported address protocols are f0, f1, f4, 0x");

const parsed = fromString(addr);
if (isEthereumConvertableAddr(parsed)) {
return toEthAddress(parsed);
}
throw new Error("address is not convertible to ethereum address");
};

export const convertAddressEthToFil = (addr: string): string => {
if (!isEthAddress(addr)) {
return addr;
}

return fromEthAddress(addr, "mainnet").toString();
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
FetchERC20TransactionsResponse,
ERC20Transfer,
ERC20BalanceResponse,
ConvertFilToEthResponse,
} from "./types";

const getFilecoinURL = (path?: string): string => {
Expand Down Expand Up @@ -118,11 +117,3 @@ export const fetchERC20Transactions = async (ethAddr: string): Promise<ERC20Tran
);
return res.txs.sort((a, b) => b.timestamp - a.timestamp);
};

export const fetchEthAddrForF1Fil = async (filAddr: string): Promise<string> => {
const response = await fetch<ConvertFilToEthResponse>(`/convert/address/${filAddr}/eth`);
if (!response) {
return "";
}
return response.address;
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import cbor from "@zondax/cbor";
import { Account, Operation, TokenAccount } from "@ledgerhq/types-live";
import { fetchERC20TokenBalance, fetchERC20Transactions } from "../api";
import invariant from "invariant";
import { ERC20Transfer } from "../types";
import { ERC20Transfer, TxStatus } from "../types";
import { emptyHistoryCache, encodeTokenAccountId } from "../../../../../account";
import { findTokenByAddressInCurrency } from "@ledgerhq/cryptoassets/tokens";
import { log } from "@ledgerhq/logs";
import BigNumber from "bignumber.js";
import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
import { convertAddressFilToEthAsync } from "../addresses";
import { convertAddressFilToEth } from "../addresses";
import { ethers } from "ethers";
import contractABI from "./ERC20.json";
import { RecipientRequired } from "@ledgerhq/errors";
Expand All @@ -23,7 +23,7 @@ export const erc20TxnToOperation = (
unit: Unit,
): Operation[] => {
try {
const { to, from, timestamp, tx_hash, tx_cid, amount, height } = tx;
const { to, from, timestamp, tx_hash, tx_cid, amount, height, status } = tx;
const value = valueFromUnit(new BigNumber(amount), unit);

const isSending = address.toLowerCase() === from.toLowerCase();
Expand All @@ -33,6 +33,7 @@ export const erc20TxnToOperation = (

const date = new Date(timestamp * 1000);
const hash = tx_cid ?? tx_hash;
const hasFailed = status !== TxStatus.Ok;

const ops: Operation[] = [];
if (isSending) {
Expand All @@ -48,6 +49,7 @@ export const erc20TxnToOperation = (
senders: [from],
recipients: [to],
date,
hasFailed,
extra: {},
});
}
Expand All @@ -65,6 +67,7 @@ export const erc20TxnToOperation = (
senders: [from],
recipients: [to],
date,
hasFailed,
extra: {},
});
}
Expand All @@ -84,8 +87,7 @@ export async function buildTokenAccounts(
initialAccount?: Account,
): Promise<TokenAccount[]> {
try {
const nativeEthAddr = await convertAddressFilToEthAsync(filAddr);
const transfers = await fetchERC20Transactions(nativeEthAddr);
const transfers = await fetchERC20Transactions(filAddr);
const transfersUntangled: { [addr: string]: ERC20Transfer[] } = transfers.reduce(
(prev, curr) => {
curr.contract_address = curr.contract_address.toLowerCase();
Expand All @@ -107,12 +109,12 @@ export async function buildTokenAccounts(
continue;
}

const balance = await fetchERC20TokenBalance(nativeEthAddr, cAddr);
const balance = await fetchERC20TokenBalance(filAddr, cAddr);
const bnBalance = new BigNumber(balance.toString());
const tokenAccountId = encodeTokenAccountId(parentAccountId, token);

const operations = txns
.flatMap(txn => erc20TxnToOperation(txn, nativeEthAddr, tokenAccountId, token.units[0]))
.flatMap(txn => erc20TxnToOperation(txn, filAddr, tokenAccountId, token.units[0]))
.flat();

if (operations.length === 0 && bnBalance.isZero()) {
Expand Down Expand Up @@ -167,14 +169,14 @@ export const abiEncodeTransferParams = (recipient: string, amount: string) => {
return data;
};

export const generateTokenTxnParams = async (recipient: string, amount: BigNumber) => {
export const generateTokenTxnParams = (recipient: string, amount: BigNumber) => {
log("debug", "generateTokenTxnParams", { recipient, amount: amount.toString() });

if (!recipient) {
throw new RecipientRequired();
}

recipient = await convertAddressFilToEthAsync(recipient);
recipient = convertAddressFilToEth(recipient);

return abiEncodeTransferParams(recipient, amount.toString());
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export interface EstimatedFeesRequest {
from: string;
methodNum?: number;
blockIncl?: number;
params?: string;
value?: string;
}

export interface EstimatedFeesResponse {
Expand Down Expand Up @@ -88,6 +90,7 @@ export interface ERC20Transfer {
id: string;
height: number;
type: string;
status: string;
to: string;
from: string;
amount: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
import { Transaction } from "./types";
import { toCBORResponse } from "./bridge/utils/serializer";
import { calculateEstimatedFees } from "./utils";
import { convertAddressFilToEthAsync } from "./bridge/utils/addresses";
import { convertAddressEthToFil } from "./bridge/utils/addresses";
import { getSubAccount } from "./bridge/utils/utils";

export const buildOptimisticOperation = async (
Expand All @@ -22,8 +22,7 @@ export const buildOptimisticOperation = async (

let operation: Operation;
if (subAccount) {
const senderEthAddr = await convertAddressFilToEthAsync(parsedSender);
const recipientEthAddr = await convertAddressFilToEthAsync(transaction.recipient);
const recipientFilAddr = convertAddressEthToFil(transaction.recipient);
operation = {
id: encodeOperationId(subAccount.id, txHash, "OUT"),
hash: txHash,
Expand All @@ -33,8 +32,8 @@ export const buildOptimisticOperation = async (
blockHeight: null,
blockHash: null,
accountId: subAccount.id,
senders: [senderEthAddr],
recipients: [recipientEthAddr],
senders: [parsedSender],
recipients: [recipientFilAddr],
date: new Date(),
extra: {},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,24 +58,23 @@ function getDeviceTransactionConfig(input: {
};

if (subAccount) {
const value = expectedToFieldForTokenTransfer(input.transaction.recipient);
fields.push({
type: "filecoin.recipient",
label: "To",
value,
value: expectedToFieldForTokenTransfer(input.transaction.recipient),
});
}

const recipient = input.transaction.recipient;
if (recipient.length >= 4 && recipient.substring(0, 4) === "0xff") {
const validated = validateAddress(recipient);
if (validated.isValid) {
const value = validated.parsedAddress.toString();
fields.push({
type: "filecoin.recipient",
label: "To",
value,
});
} else {
const recipient = input.transaction.recipient;
if (recipient.length >= 4 && recipient.substring(0, 4) === "0xff") {
const validated = validateAddress(recipient);
if (validated.isValid) {
const value = validated.parsedAddress.toString();
fields.push({
type: "filecoin.recipient",
label: "To",
value,
});
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { isFilEthAddress, validateAddress } from "./bridge/utils/addresses";
import { fetchBalances, fetchEstimatedFees } from "./bridge/utils/api";
import BigNumber from "bignumber.js";
import { BroadcastBlockIncl } from "./bridge/utils/types";
import { encodeTxnParams, generateTokenTxnParams } from "./bridge/utils/erc20/tokenAccounts";

export const estimateMaxSpendable: AccountBridge<Transaction>["estimateMaxSpendable"] = async ({
account,
Expand Down Expand Up @@ -39,6 +40,7 @@ export const estimateMaxSpendable: AccountBridge<Transaction>["estimateMaxSpenda
const invalidAddressErr = new InvalidAddress(undefined, {
currencyName: subAccount ? subAccount.token.name : a.currency.name,
});

const senderValidation = validateAddress(sender);
if (!senderValidation.isValid) throw invalidAddressErr;
sender = senderValidation.parsedAddress.toString();
Expand All @@ -63,17 +65,30 @@ export const estimateMaxSpendable: AccountBridge<Transaction>["estimateMaxSpenda
const amount = transaction?.amount;

const validatedContractAddress = validateAddress(subAccount?.token.contractAddress ?? "");
const finalRecipient =
tokenAccountTxn && validatedContractAddress.isValid
? validatedContractAddress.parsedAddress.toString()
: recipient;
if (!validatedContractAddress.isValid) {
throw invalidAddressErr;
}
const contractAddress = validatedContractAddress.parsedAddress.toString();
const finalRecipient = tokenAccountTxn ? contractAddress : recipient;

// If token transfer, the evm payload is required to estimate fees
const params =
tokenAccountTxn && transaction && subAccount
? generateTokenTxnParams(
contractAddress,
transaction.amount.isZero() ? BigNumber(1) : transaction.amount,
)
: undefined;

const result = await fetchEstimatedFees({
to: finalRecipient,
from: sender,
methodNum,
blockIncl: BroadcastBlockIncl,
params: params ? encodeTxnParams(params) : undefined, // If token transfer, the eth call params are required to estimate fees
value: tokenAccountTxn ? "0" : undefined, // If token transfer, the value should be 0 (avoid any native token transfer on fee estimation)
});

const gasFeeCap = new BigNumber(result.gas_fee_cap);
const gasLimit = new BigNumber(result.gas_limit);
const estimatedFees = calculateEstimatedFees(gasFeeCap, gasLimit);
Expand Down
Loading
Loading