Skip to content

Commit

Permalink
Merge pull request #7509 from Zondax/support/filecoin-evm
Browse files Browse the repository at this point in the history
Support/filecoin evm
  • Loading branch information
hzheng-ledger authored Aug 5, 2024
2 parents 5fa6f45 + 28545a7 commit 1f40d14
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 118 deletions.
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

0 comments on commit 1f40d14

Please sign in to comment.