diff --git a/.changeset/famous-worms-pull.md b/.changeset/famous-worms-pull.md new file mode 100644 index 000000000000..eab7b44e41c1 --- /dev/null +++ b/.changeset/famous-worms-pull.md @@ -0,0 +1,9 @@ +--- +"@ledgerhq/cryptoassets": minor +"ledger-live-desktop": minor +"live-mobile": minor +"@ledgerhq/live-common": minor +"@ledgerhq/live-env": minor +--- + +Add ERC20 token support for filecoin diff --git a/apps/ledger-live-desktop/src/renderer/families/filecoin/TransactionConfirmFields.tsx b/apps/ledger-live-desktop/src/renderer/families/filecoin/TransactionConfirmFields.tsx index eea43bbd8124..c97bb00d58b1 100644 --- a/apps/ledger-live-desktop/src/renderer/families/filecoin/TransactionConfirmFields.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/filecoin/TransactionConfirmFields.tsx @@ -26,6 +26,7 @@ const fieldComponents = { "filecoin.gasPremium": FilecoinField, "filecoin.gasLimit": FilecoinField, "filecoin.method": FilecoinField, + "filecoin.recipient": FilecoinField, }; export default { fieldComponents, diff --git a/apps/ledger-live-desktop/static/i18n/en/app.json b/apps/ledger-live-desktop/static/i18n/en/app.json index f0f871d72556..7d713e124547 100644 --- a/apps/ledger-live-desktop/static/i18n/en/app.json +++ b/apps/ledger-live-desktop/static/i18n/en/app.json @@ -5569,6 +5569,13 @@ "title": "Impossible to calculate amount and fees", "description": "Impossible to calculate amount and fees" }, + "InvalidRecipientForTokenTransfer": { + "title": "Invalid recipient for token transfer, supported account types for token transfer: [f0, f4, 0x]" + }, + "FilecoinFeeEstimationFailed": { + "title": "Sorry, fee estimation failed", + "description": "Fee estimation failed on the network. Check inputs, transaction most likely to fail" + }, "NotEnoughBalance": { "title": "Sorry, insufficient funds", "description": "Please make sure the account has enough funds." diff --git a/apps/ledger-live-mobile/src/families/filecoin/TransactionConfirmFields.tsx b/apps/ledger-live-mobile/src/families/filecoin/TransactionConfirmFields.tsx index 998e0bc1f0d1..80332e5ca486 100644 --- a/apps/ledger-live-mobile/src/families/filecoin/TransactionConfirmFields.tsx +++ b/apps/ledger-live-mobile/src/families/filecoin/TransactionConfirmFields.tsx @@ -37,6 +37,7 @@ const fieldComponents = { "filecoin.gasPremium": FilecoinField, "filecoin.gasLimit": FilecoinField, "filecoin.method": FilecoinField, + "filecoin.recipient": FilecoinField, }; export default { fieldComponents, diff --git a/apps/ledger-live-mobile/src/locales/en/common.json b/apps/ledger-live-mobile/src/locales/en/common.json index debc7fd0fb20..8369e9a66983 100644 --- a/apps/ledger-live-mobile/src/locales/en/common.json +++ b/apps/ledger-live-mobile/src/locales/en/common.json @@ -919,6 +919,13 @@ "InvalidMemoICP": { "title": "Memo is required to be a number" }, + "InvalidRecipientForTokenTransfer": { + "title": "Invalid recipient for token transfer, supported account types for token transfer: [f0, f4, 0x]" + }, + "FilecoinFeeEstimationFailed": { + "title": "Sorry, fee estimation failed", + "description": "Fee estimation failed on the network. Check inputs, transaction most likely to fail" + }, "FirmwareNotRecognized": { "title": "Invalid Provider", "description": "You have to change \"My Ledger provider\" setting. To change it, open Ledger Live \"Settings\", select \"Experimental features\", and then select a different provider." diff --git a/libs/ledger-live-common/package.json b/libs/ledger-live-common/package.json index e53247d7b787..e689d07040c6 100644 --- a/libs/ledger-live-common/package.json +++ b/libs/ledger-live-common/package.json @@ -221,10 +221,11 @@ "cosmjs-types": "0.2.1", "date-fns": "^2.23.0", "eip55": "^2.1.1", + "ethers": "5.7.2", "expect": "^27.4.6", "fuse.js": "^6.6.2", "invariant": "^2.2.2", - "iso-filecoin": "^4.0.3", + "iso-filecoin": "^4.1.0", "isomorphic-ws": "^4.0.1", "jotai": "^2.7.0", "json-rpc-2.0": "^0.2.19", diff --git a/libs/ledger-live-common/src/__tests__/currencies.ts b/libs/ledger-live-common/src/__tests__/currencies.ts index 88916cbedcdb..88db05cc04ae 100644 --- a/libs/ledger-live-common/src/__tests__/currencies.ts +++ b/libs/ledger-live-common/src/__tests__/currencies.ts @@ -25,6 +25,9 @@ test("erc20 are all consistent with those on ledgerjs side", () => { } if (token.tokenType === "erc20") { + if (token.parentCurrency.family === "filecoin") { + continue; + } const tokenData = byContractAddressAndChainId( token.contractAddress, token.parentCurrency.ethereumLikeInfo?.chainId || 0, diff --git a/libs/ledger-live-common/src/currencies/__snapshots__/sortByMarketcap.test.ts.snap b/libs/ledger-live-common/src/currencies/__snapshots__/sortByMarketcap.test.ts.snap index 3f656eff656b..2ec1816a5d98 100644 --- a/libs/ledger-live-common/src/currencies/__snapshots__/sortByMarketcap.test.ts.snap +++ b/libs/ledger-live-common/src/currencies/__snapshots__/sortByMarketcap.test.ts.snap @@ -224,6 +224,7 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "ethereum/erc20/lambda", "ethereum/erc20/dent", "ethereum/erc20/odem_token", + "filecoin/erc20/securitized_filecoin_token", "ethereum/erc20/elitium", "ethereum/erc20/anchor", "ethereum/erc20/arcblock_token", @@ -11027,6 +11028,13 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "polygon/erc20/zrx", "polygon/erc20/zurf", "polygon/erc20/zurf_f91e", + "filecoin/erc20/axelar_wrapped_usdc", + "filecoin/erc20/collectif_staked_fil", + "filecoin/erc20/infinity_pool_staked_fil", + "filecoin/erc20/node_fil", + "filecoin/erc20/pfil_token", + "filecoin/erc20/wrapped_fil", + "filecoin/erc20/wrapped_pfil_token", "tron/trc10/1002670", "tron/trc10/1001943", "tron/trc10/1001351", diff --git a/libs/ledger-live-common/src/families/filecoin/__snapshots__/bridge.integration.test.ts.snap b/libs/ledger-live-common/src/families/filecoin/__snapshots__/bridge.integration.test.ts.snap new file mode 100644 index 000000000000..a4b2417d21d1 --- /dev/null +++ b/libs/ledger-live-common/src/families/filecoin/__snapshots__/bridge.integration.test.ts.snap @@ -0,0 +1,242 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`filecoin currency bridge scanAccounts filecoin seed 1 1`] = ` +[ + { + "balance": "1000000000000000", + "currencyId": "filecoin", + "derivationMode": "glifLegacy", + "freshAddress": "t15lauyzdivqj7m3yob3rxmzdsy7uyhfflwyheuri", + "freshAddressPath": "44'/1'/0'/0/0", + "id": "js:2:filecoin:t15lauyzdivqj7m3yob3rxmzdsy7uyhfflwyheuri:glifLegacy", + "index": 0, + "operationsCount": 0, + "pendingOperations": [], + "seedIdentifier": "04ca7b02cafdf36e8b4caaf530a96b949764af71b956b2a3328b7a10940794c860f574a9199be98bde3c261887fec8e5fd94bc5f104908bf5f992f52ef2a89abb0", + "spendableBalance": "1000000000000000", + "subAccounts": [], + "swapHistory": [], + "syncHash": undefined, + "used": true, + }, + { + "balance": "100000000000000000", + "currencyId": "filecoin", + "derivationMode": "glif", + "freshAddress": "f1p74d4mlmeyc4agflhjqsnvoyzyfdai7fmkyso2a", + "freshAddressPath": "44'/461'/0'/0/0", + "id": "js:2:filecoin:f1p74d4mlmeyc4agflhjqsnvoyzyfdai7fmkyso2a:glif", + "index": 0, + "operationsCount": 1, + "pendingOperations": [], + "seedIdentifier": "04ca7b02cafdf36e8b4caaf530a96b949764af71b956b2a3328b7a10940794c860f574a9199be98bde3c261887fec8e5fd94bc5f104908bf5f992f52ef2a89abb0", + "spendableBalance": "100000000000000000", + "subAccounts": [], + "swapHistory": [], + "syncHash": undefined, + "used": true, + }, + { + "balance": "10000000000000000", + "currencyId": "filecoin", + "derivationMode": "glif", + "freshAddress": "f1plmg3kklvmnodfboimcq5w27mnfs2hwjtas6gia", + "freshAddressPath": "44'/461'/0'/0/1", + "id": "js:2:filecoin:f1plmg3kklvmnodfboimcq5w27mnfs2hwjtas6gia:glif", + "index": 1, + "operationsCount": 1, + "pendingOperations": [], + "seedIdentifier": "04ca7b02cafdf36e8b4caaf530a96b949764af71b956b2a3328b7a10940794c860f574a9199be98bde3c261887fec8e5fd94bc5f104908bf5f992f52ef2a89abb0", + "spendableBalance": "10000000000000000", + "subAccounts": [], + "swapHistory": [], + "syncHash": undefined, + "used": true, + }, + { + "balance": "12340000000000000", + "id": "js:2:filecoin:f1plmg3kklvmnodfboimcq5w27mnfs2hwjtas6gia:glif+filecoin%2Ferc20%2Fwrapped~!underscore!~fil", + "operationsCount": 1, + "parentId": "js:2:filecoin:f1plmg3kklvmnodfboimcq5w27mnfs2hwjtas6gia:glif", + "pendingOperations": [], + "spendableBalance": "12340000000000000", + "swapHistory": [], + "tokenId": "filecoin/erc20/wrapped_fil", + "type": "TokenAccountRaw", + }, + { + "balance": "0", + "currencyId": "filecoin", + "derivationMode": "glif", + "freshAddress": "f15nh4cywefp6pavzjifbe2ormma74malwdyy5rfq", + "freshAddressPath": "44'/461'/0'/0/2", + "id": "js:2:filecoin:f15nh4cywefp6pavzjifbe2ormma74malwdyy5rfq:glif", + "index": 2, + "operationsCount": 0, + "pendingOperations": [], + "seedIdentifier": "04ca7b02cafdf36e8b4caaf530a96b949764af71b956b2a3328b7a10940794c860f574a9199be98bde3c261887fec8e5fd94bc5f104908bf5f992f52ef2a89abb0", + "spendableBalance": "0", + "subAccounts": [], + "swapHistory": [], + "syncHash": undefined, + "used": false, + }, + { + "balance": "0", + "currencyId": "filecoin", + "derivationMode": "glif", + "freshAddress": "f1lg3347otxjblwqx6pnqnqoz4cqwasq57yo2bxcy", + "freshAddressPath": "44'/461'/0'/0/3", + "id": "js:2:filecoin:f1lg3347otxjblwqx6pnqnqoz4cqwasq57yo2bxcy:glif", + "index": 3, + "operationsCount": 0, + "pendingOperations": [], + "seedIdentifier": "04ca7b02cafdf36e8b4caaf530a96b949764af71b956b2a3328b7a10940794c860f574a9199be98bde3c261887fec8e5fd94bc5f104908bf5f992f52ef2a89abb0", + "spendableBalance": "0", + "subAccounts": [], + "swapHistory": [], + "syncHash": undefined, + "used": false, + }, + { + "balance": "0", + "currencyId": "filecoin", + "derivationMode": "glif", + "freshAddress": "f1j2uoeuoiusmh2godly4fivd6gomi6asmzkd47li", + "freshAddressPath": "44'/461'/0'/0/4", + "id": "js:2:filecoin:f1j2uoeuoiusmh2godly4fivd6gomi6asmzkd47li:glif", + "index": 4, + "operationsCount": 0, + "pendingOperations": [], + "seedIdentifier": "04ca7b02cafdf36e8b4caaf530a96b949764af71b956b2a3328b7a10940794c860f574a9199be98bde3c261887fec8e5fd94bc5f104908bf5f992f52ef2a89abb0", + "spendableBalance": "0", + "subAccounts": [], + "swapHistory": [], + "syncHash": undefined, + "used": false, + }, + { + "balance": "0", + "currencyId": "filecoin", + "derivationMode": "glif", + "freshAddress": "f1l7h6nvobmqc7adq7plta5kn7xaltx6k6l7paqsq", + "freshAddressPath": "44'/461'/0'/0/5", + "id": "js:2:filecoin:f1l7h6nvobmqc7adq7plta5kn7xaltx6k6l7paqsq:glif", + "index": 5, + "operationsCount": 0, + "pendingOperations": [], + "seedIdentifier": "04ca7b02cafdf36e8b4caaf530a96b949764af71b956b2a3328b7a10940794c860f574a9199be98bde3c261887fec8e5fd94bc5f104908bf5f992f52ef2a89abb0", + "spendableBalance": "0", + "subAccounts": [], + "swapHistory": [], + "syncHash": undefined, + "used": false, + }, + { + "balance": "0", + "currencyId": "filecoin", + "derivationMode": "glif", + "freshAddress": "f1i2drwsilv35pp75dvd52enl3ohqakua2mshf5fy", + "freshAddressPath": "44'/461'/0'/0/6", + "id": "js:2:filecoin:f1i2drwsilv35pp75dvd52enl3ohqakua2mshf5fy:glif", + "index": 6, + "operationsCount": 0, + "pendingOperations": [], + "seedIdentifier": "04ca7b02cafdf36e8b4caaf530a96b949764af71b956b2a3328b7a10940794c860f574a9199be98bde3c261887fec8e5fd94bc5f104908bf5f992f52ef2a89abb0", + "spendableBalance": "0", + "subAccounts": [], + "swapHistory": [], + "syncHash": undefined, + "used": false, + }, + { + "balance": "0", + "currencyId": "filecoin", + "derivationMode": "glif", + "freshAddress": "f1347zfz7jiz3oeufp2lrbcwwt265ejk32n2wqrla", + "freshAddressPath": "44'/461'/0'/0/7", + "id": "js:2:filecoin:f1347zfz7jiz3oeufp2lrbcwwt265ejk32n2wqrla:glif", + "index": 7, + "operationsCount": 0, + "pendingOperations": [], + "seedIdentifier": "04ca7b02cafdf36e8b4caaf530a96b949764af71b956b2a3328b7a10940794c860f574a9199be98bde3c261887fec8e5fd94bc5f104908bf5f992f52ef2a89abb0", + "spendableBalance": "0", + "subAccounts": [], + "swapHistory": [], + "syncHash": undefined, + "used": false, + }, +] +`; + +exports[`filecoin currency bridge scanAccounts filecoin seed 1 2`] = ` +[ + [], + [ + { + "accountId": "js:2:filecoin:f1p74d4mlmeyc4agflhjqsnvoyzyfdai7fmkyso2a:glif", + "blockHash": null, + "blockHeight": 1802367, + "extra": {}, + "fee": "0", + "hasFailed": false, + "hash": "bafy2bzaceaztepnzdzbul7nmkef6blk5x2luvbvangrjal25k6xi5qob36k32", + "id": "js:2:filecoin:f1p74d4mlmeyc4agflhjqsnvoyzyfdai7fmkyso2a:glif-bafy2bzaceaztepnzdzbul7nmkef6blk5x2luvbvangrjal25k6xi5qob36k32-IN", + "recipients": [ + "f1p74d4mlmeyc4agflhjqsnvoyzyfdai7fmkyso2a", + ], + "senders": [ + "f1ov6d42tujoyexkbdh34oik2vhe5unqo2a5ocqoq", + ], + "type": "IN", + "value": "100000000000000000", + }, + ], + [ + { + "accountId": "js:2:filecoin:f1plmg3kklvmnodfboimcq5w27mnfs2hwjtas6gia:glif", + "blockHash": null, + "blockHeight": 3853241, + "extra": {}, + "fee": "0", + "hasFailed": false, + "hash": "bafy2bzacecwvvvxtlbm5s6wpk546r66hjqw4amjioihe2rt5ufoebu3zv6xiw", + "id": "js:2:filecoin:f1plmg3kklvmnodfboimcq5w27mnfs2hwjtas6gia:glif-bafy2bzacecwvvvxtlbm5s6wpk546r66hjqw4amjioihe2rt5ufoebu3zv6xiw-IN", + "recipients": [ + "f1plmg3kklvmnodfboimcq5w27mnfs2hwjtas6gia", + ], + "senders": [ + "f1ovlkmtnqji7wrvdpcys3i22c62obbamgokmg35q", + ], + "type": "IN", + "value": "10000000000000000", + }, + ], + [ + { + "accountId": "js:2:filecoin:f1plmg3kklvmnodfboimcq5w27mnfs2hwjtas6gia:glif+filecoin%2Ferc20%2Fwrapped~!underscore!~fil", + "blockHash": "", + "blockHeight": 3853248, + "extra": {}, + "fee": "0", + "hasFailed": false, + "hash": "bafy2bzaceaw6jxm3bp6rdvtse7k4evsg2zhis3ootn2pe5k3txtxor4efhl7k", + "id": "js:2:filecoin:f1plmg3kklvmnodfboimcq5w27mnfs2hwjtas6gia:glif+filecoin%2Ferc20%2Fwrapped~!underscore!~fil-bafy2bzaceaw6jxm3bp6rdvtse7k4evsg2zhis3ootn2pe5k3txtxor4efhl7k-IN", + "recipients": [ + "f1plmg3kklvmnodfboimcq5w27mnfs2hwjtas6gia", + ], + "senders": [ + "f1pnc33cba2c2a3olqadho7wiohyosbkl3upescei", + ], + "type": "IN", + "value": "12340000000000000", + }, + ], + [], + [], + [], + [], + [], + [], +] +`; diff --git a/libs/ledger-live-common/src/families/filecoin/bridge.integration.test.ts b/libs/ledger-live-common/src/families/filecoin/bridge.integration.test.ts index 2261474cb29a..bff96f2bd250 100644 --- a/libs/ledger-live-common/src/families/filecoin/bridge.integration.test.ts +++ b/libs/ledger-live-common/src/families/filecoin/bridge.integration.test.ts @@ -2,21 +2,114 @@ import "../../__tests__/test-helpers/setup"; import { testBridge } from "../../__tests__/test-helpers/bridge"; import { getAccountBridge } from "../../bridge"; import { decodeAccountId, encodeAccountId, fromAccountRaw } from "../../account"; -import { BigNumber } from "bignumber.js"; -import { AmountRequired, InvalidAddress, NotEnoughBalance } from "@ledgerhq/errors"; +import { InvalidAddress, NotEnoughBalance, AmountRequired } from "@ledgerhq/errors"; import type { DatasetTest, CurrenciesData } from "@ledgerhq/types-live"; import type { Transaction } from "./types"; import { fromTransactionRaw } from "../filecoin/transaction"; +import { InvalidRecipientForTokenTransfer } from "./errors"; +import { getSubAccount } from "./bridge/utils/utils"; +import BigNumber from "bignumber.js"; -const SEED_IDENTIFIER = "f1p74d4mlmeyc4agflhjqsnvoyzyfdai7fmkyso2a"; -const ACCOUNT_1 = "f1p74d4mlmeyc4agflhjqsnvoyzyfdai7fmkyso2a"; +const SEED_IDENTIFIER = "f1plmg3kklvmnodfboimcq5w27mnfs2hwjtas6gia"; +const ACCOUNT_1 = "f1plmg3kklvmnodfboimcq5w27mnfs2hwjtas6gia"; const ACCOUNT_2 = "f410fncojwmrseefktoco6rcnb3zv2eiqfli7muhvqma"; const ACCOUNT_3 = "0x689c9b3232210aa9b84ef444d0ef35d11102ad1f"; const ACCOUNT_4 = "f01840380"; const filecoin: CurrenciesData = { - scanAccounts: [], + scanAccounts: [ + { + name: "filecoin seed 1", + apdus: ` + => 0600000000 + <= 0000180100331000049000 + => 06010000142c000080cd010080000000800000000000000000 + <= 04ca7b02cafdf36e8b4caaf530a96b949764af71b956b2a3328b7a10940794c860f574a9199be98bde3c261887fec8e5fd94bc5f104908bf5f992f52ef2a89abb015017ff83e316c2605c018ab3a6126d5d8ce0a3023e529663170373464346d6c6d657963346167666c686a71736e766f797a796664616937666d6b79736f32619000 + => 0600000000 + <= 0000180100331000049000 + => 06010000142c00008001000080000000800000000000000000 + <= 04d493dab3ff63d6f0673454f8f6d6adec08db19e8c0298c65cfcace76a10757774afdee518b15a8b82452531bcb7860eefce02a89ef9051037646f5d4d6bd74171501eac14c6468ac13f66f0e0ee3766472c7e98394ab297431356c6175797a646976716a376d33796f623372786d7a6473793775796866666c777968657572699000 + => 0600000000 + <= 0000180100331000049000 + => 06010000142c00008001000080000000800000000001000000 + <= 040cbb57791d797268e34340565393f8a014ae754026159973fbe46bdcef3b6d28d155f4923256a2beb9bcff3bd39e5066d1c778306f28a2d4faa3a66dbf70453e150148c9b66fef1fce116652099bec6301ced31e56472974316a6465336d33377064376862637a737362676e36797979627a336a7234767368676370696b32619000 + => 0600000000 + <= 0000180100331000049000 + => 06010000142c00008001000080000000800000000002000000 + <= 04b5d3766bcfe150e887fe08355d8801901957d4f940ac33086b211cbd2879ccdb475765391f6bdc93329670c899e82ec212e9c90912117d002e5f8d4a25d6c6d01501917d45c3017ba05f783d3ff6c88921273a73daac297431736636756c717962706f7166363662356837336d72636a62653435686877766d6f737a6d7a71719000 + => 0600000000 + <= 0000180100331000049000 + => 06010000142c00008001000080000000800000000003000000 + <= 041e039416e7b916778a6a6fbcb7b3bce298fb46f084ba27b3f584f7fac52410140cbc275974f9dcb03dde23d39940833fca8d2998710323102e87e04a15e249a215019948a798f70a807872e490d53b50bb0315d494632974317466656b70676878626b61687134786573646b7477756633616d6b356a66646468346b613776699000 + => 0600000000 + <= 0000180100331000049000 + => 06010000142c00008001000080000000800000000004000000 + <= 04eb83d066b4cb200f1632379f73b1ca6bff0fff17111f7e82a89c8433c6226b34139531f184499b333f31dc9937987a5536953eace7d8ba288aadd42d69d78dc01501d42efce0327edbdf975dcadf472de1d97b5003d1297431327178707a79627370336e35376632357a6c70756f6c7062336635766161367261796f68716a719000 + => 0600000000 + <= 0000180100331000049000 + => 06010000142c00008001000080000000800000000005000000 + <= 0407230f40bade572d4feeec0e3768789860227986e85b01bb4d462c1b517cd2cc5af4ce2097b06fda52176a542ecce4482291b4f6ece48d59fb9b2a1247f2418d1501d981129d32078f65537f1061f28cd00f005fb4832974313367617266686a73613668776b753337636271376664677162346166376e65647272737a6561619000 + => 0600000000 + <= 0000180100331000049000 + => 06010000142c00008001000080000000800000000006000000 + <= 0466a53388d30f8323cb39ca62cd261a63d43b6f1bbf05250ea7bef8077037ae74bfa6a42f5ef9757e53e713a9695c2de82cad06472385292c63337db0cbd80daa150144b33ed6e4940cce710c70146b8cfd18eb97f85929743169737a74357678657371676d3434696d6f616b67786468356464767a7036637a6b61756f377a799000 + => 0600000000 + <= 0000180100331000049000 + => 06010000142c000080cd010080010000800000000000000000 + <= 0463c18e9a3c08ecffab4fa384f977e5d380d2b252dbbd96875df9cc91c44c81e82639fd659a3eeeaed96901696d8c7832b03befafd0af6ab2bee634ea8141e52a15016f62da10a416f416e80ec32866b91f01f7746af82966316e35726e75656665633332626e32616f796d75676e6f693761683378693278796e766c746e71699000 + => 0600000000 + <= 0000180100331000049000 + => 06010000142c000080cd010080020000800000000000000000 + <= 04f838a8b60e8c8ddaa40b164c458b32094be15606c261008fbb4a38d9d7fd63d0b3343f1e0b861b142774fa242234185f79d95e180609bf6c56ad38f36197bf3715010b485688119885d1fa4a27d026c7f30531a62936296631626e65666e636172746363356436736b653769636e723774617579326d6b6a7732796e676275799000 + => 0600000000 + <= 0000180100331000049000 + => 06010000142c000080cd010080030000800000000000000000 + <= 04e3493fab64823760f42c5fca00dc27a3eb9cb8ba0b52c498b940a7ec82bad70f8b68bd2c7d6cc4074eb08d24187d925fab1fa54dbe7c26f544fd53b454ebcacd1501188ac818a6471dc0c43deef4cc4fb3594a85885d2966316463666d7167666769346f34627262353533326d797435746c6666696c6363353236637a7578699000 + => 0600000000 + <= 0000180100331000049000 + => 06010000142c000080cd010080040000800000000000000000 + <= 04851093dcfb51ae7eb3a085003c50a2f6ee635cc7192b713bd5d89301c1ce38d6ee44ec601b0c67218f502dfd7bc8bc96014e1db2f3754552507a5c313ebe0e2c1501ac247d637e126ed294b6761bf3877bda8f04d9002966317671736832793336636a786e666666776f796e3768623333336b68716a7769616b7a63616e32799000 + => 0600000000 + <= 0000180100331000049000 + => 06010000142c000080cd010080050000800000000000000000 + <= 04368d7fe53c84f52574cb0f9b0a78dcddf37d1b1e09e9c8a7bfab2e9bf06ce71a7ab0f453b938ffa093d1c8028b159bad8727b71a6508acbc2d530eb45ff9dab51501ec13e2bf751dd038dceb18db8447699bc67a3d6c29663135716a3666703376647869647278686c64646e796972336a7470646875706c6d6f656f717463699000 + => 0600000000 + <= 0000180100331000049000 + => 06010000142c000080cd010080060000800000000000000000 + <= 0418de25cee71d38de134e9fede2c42c28c5a8b06a4643527fe61ac272e45e562d957b18d85364da0cce8ffb470a30dba6e1d56f68cbaffc25f2332932731227991501a223f50f49d9804314bd96636ae7f5267360aeb2296631756972376b64326a3367616567666635737a7277767a3776657a7a77626c7673776669347974699000 + => 0600000000 + <= 0000180100331000049000 + => 06010000142c000080cd010080000000800000000001000000 + <= 0419d91b4f9c3015637f5aa6b601b337f2770b1dcf93ba896d64627cd2aa67493ba924decd3024643478a84e06486b0a6b10d9f7607b6e92b0545bb3c998d4d75d15017ad86da94bab1ae1942e43050edb5f634b2d1ec9296631706c6d67336b6b6c766d6e6f6466626f696d6371357732376d6e66733268776a746173366769619000 + => 0600000000 + <= 0000180100331000049000 + => 06010000142c000080cd010080000000800000000002000000 + <= 047fc6dc24f9c4bb243023b760637fc604c492da6406824f22592d96a4f435767c975d675b5aef0e160f87d2950860c50e2ab691f197033d234ce7af61fcded4b01501eb4fc162c42bfcf0572941424d3a2c603fc60176296631356e6834637977656670367061767a6a69666265326f726d6d6137346d616c77647979357266719000 + => 0600000000 + <= 0000180100331000049000 + => 06010000142c000080cd010080000000800000000003000000 + <= 04b5a4495a2edc7d6e121715332fcac13a28e1167c702d0a3ddbb5a5ff9027182db0a4c94387ee5c9cdd53fb7474b968a9f1310225c02c375d10189b4fc5e1f24a150159b7be7dd3ba42bb42fe7b60d83b3c142c0943bf2966316c67333334376f74786a626c77717836706e716e716f7a346371776173713537796f32627863799000 + => 0600000000 + <= 0000180100331000049000 + => 06010000142c000080cd010080000000800000000004000000 + <= 04b479cb2a3d8fe60534943f3bfad0e55ba4283d31974581e8d348623aeb4a3e15c8167a003c92dcf52e12ee253126faebc7d445bce8e347bdd26d8a2e53a46b3315014ea8e251c8a4987d19c35e3854547e33988f024c2966316a32756f65756f6975736d6832676f646c79346669766436676f6d693661736d7a6b6434376c699000 + => 0600000000 + <= 0000180100331000049000 + => 06010000142c000080cd010080000000800000000005000000 + <= 04a2dc85c8669642342c476dfd7a85f23309c7c06df209ed6846815786a6740ab35f8b9024b1616780a39dea7a9e7a05b8e808539688f70ad1ad814889546e738815015fcfe6d5c16405f00e1f7ae60ea9bfb8173bf95e2966316c3768366e766f626d71633761647137706c7461356b6e3778616c7478366b366c3770617173719000 + => 0600000000 + <= 0000180100331000049000 + => 06010000142c000080cd010080000000800000000006000000 + <= 045d4460005b5218e70aac7e6d1d8980b24fae39c743fa50b2cfd0dd062b5f88ea27ea5fe614ff059e0a62691bb91774dea71f5b8c0bf57703688a05e1bb0b436a150146871b490baefaf7ffa3a8fba2357b71e005501a296631693264727773696c763335707037356476643532656e6c336f6871616b7561326d7368663566799000 + => 0600000000 + <= 0000180100331000049000 + => 06010000142c000080cd010080000000800000000007000000 + <= 0453fd3f49647650ba84cf7016e370a7f26a2b19684fc90c0e669ea1f7120ef4fb812c6b8e9e8fc9bf9d20b02109064482742c478e275851a181eb1bb24ac0b1ae1501df3f92e7e94676e250afd2e2115ad3d7ba44ab7a2966313334377a667a376a697a336f65756670326c726263777774323635656a6b33326e327771726c619000 + + `, + }, + ], accounts: [ { raw: { @@ -213,6 +306,95 @@ const filecoin: CurrenciesData = { }; }, }, + // Subaccout tests + { + name: "Subaccount erc20 transfer", + transaction: fromTransactionRaw({ + family: "filecoin", + method: 1, + version: 1, + nonce: 100, + gasFeeCap: "1000", + gasLimit: 10, + gasPremium: "10000", + recipient: ACCOUNT_4, + amount: "10", + subAccountId: + "js:2:filecoin:f1plmg3kklvmnodfboimcq5w27mnfs2hwjtas6gia:+filecoin%2Ferc20%2Fwrapped~!underscore!~fil", + }), + expectedStatus: { + errors: {}, + warnings: {}, + }, + }, + { + name: "Subaccount Send to unsupported address", + transaction: fromTransactionRaw({ + family: "filecoin", + method: 1, + version: 1, + nonce: 100, + gasFeeCap: "1000", + gasLimit: 10, + gasPremium: "10000", + recipient: ACCOUNT_1, + amount: "10000", + subAccountId: + "js:2:filecoin:f1plmg3kklvmnodfboimcq5w27mnfs2hwjtas6gia:+filecoin%2Ferc20%2Fwrapped~!underscore!~fil", + }), + expectedStatus: { + errors: { + recipient: new InvalidRecipientForTokenTransfer(), + }, + }, + }, + { + name: "Subaccount Send max", + transaction: fromTransactionRaw({ + family: "filecoin", + method: 1, + version: 1, + nonce: 100, + gasFeeCap: "1000", + gasLimit: 10, + gasPremium: "10000", + recipient: ACCOUNT_2, + amount: "1", + useAllAmount: true, + subAccountId: + "js:2:filecoin:f1plmg3kklvmnodfboimcq5w27mnfs2hwjtas6gia:+filecoin%2Ferc20%2Fwrapped~!underscore!~fil", + }), + expectedStatus: (account, tx) => { + const subAccount = getSubAccount(account, tx); + return { + amount: subAccount?.spendableBalance, + errors: {}, + warnings: {}, + }; + }, + }, + { + name: "Subaccount not enough balance", + transaction: fromTransactionRaw({ + family: "filecoin", + method: 1, + version: 1, + nonce: 100, + gasFeeCap: "1000", + gasLimit: 10, + gasPremium: "10000", + recipient: ACCOUNT_3, + amount: "123400000000000000", + subAccountId: + "js:2:filecoin:f1plmg3kklvmnodfboimcq5w27mnfs2hwjtas6gia:+filecoin%2Ferc20%2Fwrapped~!underscore!~fil", + }), + expectedStatus: { + errors: { + amount: new NotEnoughBalance(), + }, + warnings: {}, + }, + }, ], }, ], diff --git a/libs/ledger-live-common/src/families/filecoin/bridge/utils/addresses.ts b/libs/ledger-live-common/src/families/filecoin/bridge/utils/addresses.ts index a39a28f04d7f..03f9dd8b278e 100644 --- a/libs/ledger-live-common/src/families/filecoin/bridge/utils/addresses.ts +++ b/libs/ledger-live-common/src/families/filecoin/bridge/utils/addresses.ts @@ -1,4 +1,11 @@ -import { IAddress, PROTOCOL_INDICATOR, fromEthAddress, fromString } from "iso-filecoin/address"; +import { + IAddress, + PROTOCOL_INDICATOR, + fromEthAddress, + isEthAddress, + fromString, + toEthAddress, +} from "iso-filecoin/address"; import { log } from "@ledgerhq/logs"; export type ValidateAddressResult = @@ -13,6 +20,11 @@ export type ValidateAddressResult = 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); @@ -22,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) { @@ -32,3 +42,52 @@ export const validateAddress = (input: string): ValidateAddressResult => { return { isValid: false }; }; + +export const isRecipientValidForTokenTransfer = (addr: string): boolean => { + if (addr.length < 2) { + return false; + } + + const valid = validateAddress(addr); + if (!valid.isValid) { + return false; + } + + if (isEthereumConvertableAddr(valid.parsedAddress)) { + return true; + } + + return false; +}; + +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 convertAddressFilToEth = (addr: string): string => { + if (isEthAddress(addr)) { + return addr; + } + + 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(); +}; diff --git a/libs/ledger-live-common/src/families/filecoin/bridge/utils/api.ts b/libs/ledger-live-common/src/families/filecoin/bridge/utils/api.ts index f72658909f19..600c4d6c6911 100644 --- a/libs/ledger-live-common/src/families/filecoin/bridge/utils/api.ts +++ b/libs/ledger-live-common/src/families/filecoin/bridge/utils/api.ts @@ -4,6 +4,7 @@ import { AxiosRequestConfig, AxiosResponse } from "axios"; import network from "@ledgerhq/live-network/network"; import { makeLRUCache } from "@ledgerhq/live-network/cache"; import { getEnv } from "@ledgerhq/live-env"; + import { BalanceResponse, BroadcastTransactionRequest, @@ -13,7 +14,11 @@ import { NetworkStatusResponse, TransactionResponse, TransactionsResponse, + FetchERC20TransactionsResponse, + ERC20Transfer, + ERC20BalanceResponse, } from "./types"; +import { FilecoinFeeEstimationFailed } from "../../errors"; const getFilecoinURL = (path?: string): string => { const baseUrl = getEnv("API_FILECOIN_ENDPOINT"); @@ -30,6 +35,7 @@ const fetch = async (path: string) => { method: "GET", url, }; + const rawResponse = await network(opts); // We force data to this way as network func is not using the correct param type. Changing that func will generate errors in other implementations @@ -65,8 +71,13 @@ export const fetchBalances = async (addr: string): Promise => { export const fetchEstimatedFees = makeLRUCache( async (request: EstimatedFeesRequest): Promise => { - const data = await send(`/fees/estimate`, request); - return data; // TODO Validate if the response fits this interface + try { + const data = await send(`/fees/estimate`, request); + return data; // TODO Validate if the response fits this interface + } catch (e: any) { + log("error", "filecoin fetchEstimatedFees", e); + throw new FilecoinFeeEstimationFailed(); + } }, request => `${request.from}-${request.to}`, { @@ -90,3 +101,25 @@ export const broadcastTx = async ( const response = await send(`/transaction/broadcast`, message); return response; // TODO Validate if the response fits this interface }; + +export const fetchERC20TokenBalance = async ( + ethAddr: string, + contractAddr: string, +): Promise => { + const res = await fetch( + `/contract/${contractAddr}/address/${ethAddr}/balance/erc20`, + ); + + if (res.data.length) { + return res.data[0].balance; + } + + return "0"; +}; + +export const fetchERC20Transactions = async (ethAddr: string): Promise => { + const res = await fetch( + `/addresses/${ethAddr}/transactions/erc20`, + ); + return res.txs.sort((a, b) => b.timestamp - a.timestamp); +}; diff --git a/libs/ledger-live-common/src/families/filecoin/bridge/utils/erc20/ERC20.json b/libs/ledger-live-common/src/families/filecoin/bridge/utils/erc20/ERC20.json new file mode 100644 index 000000000000..1efbcc77e99c --- /dev/null +++ b/libs/ledger-live-common/src/families/filecoin/bridge/utils/erc20/ERC20.json @@ -0,0 +1,12 @@ +[ + { + "inputs": [ + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transfer", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/libs/ledger-live-common/src/families/filecoin/bridge/utils/erc20/tokenAccounts.ts b/libs/ledger-live-common/src/families/filecoin/bridge/utils/erc20/tokenAccounts.ts new file mode 100644 index 000000000000..5b8806c19414 --- /dev/null +++ b/libs/ledger-live-common/src/families/filecoin/bridge/utils/erc20/tokenAccounts.ts @@ -0,0 +1,183 @@ +import cbor from "@zondax/cbor"; +import { Account, Operation, TokenAccount } from "@ledgerhq/types-live"; +import { fetchERC20TokenBalance, fetchERC20Transactions } from "../api"; +import invariant from "invariant"; +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 { convertAddressFilToEth } from "../addresses"; +import { ethers } from "ethers"; +import contractABI from "./ERC20.json"; +import { RecipientRequired } from "@ledgerhq/errors"; +import { Unit } from "@ledgerhq/types-cryptoassets"; +import { valueFromUnit } from "../../../../../currencies/valueFromUnit"; +import { AccountType } from "../../../utils"; + +export const erc20TxnToOperation = ( + tx: ERC20Transfer, + address: string, + accountId: string, + unit: Unit, +): Operation[] => { + try { + 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(); + const isReceiving = address.toLowerCase() === to.toLowerCase(); + + const fee = new BigNumber(0); + + const date = new Date(timestamp * 1000); + const hash = tx_cid ?? tx_hash; + const hasFailed = status !== TxStatus.Ok; + + const ops: Operation[] = []; + if (isSending) { + ops.push({ + id: encodeOperationId(accountId, hash, "OUT"), + hash, + type: "OUT", + value: value, + fee, + blockHeight: height, + blockHash: "", + accountId, + senders: [from], + recipients: [to], + date, + hasFailed, + extra: {}, + }); + } + + if (isReceiving) { + ops.push({ + id: encodeOperationId(accountId, hash, "IN"), + hash, + type: "IN", + value, + fee, + blockHeight: height, + blockHash: "", + accountId, + senders: [from], + recipients: [to], + date, + hasFailed, + extra: {}, + }); + } + + invariant(ops, "filecoin operation is not defined"); + + return ops; + } catch (e) { + log("error", "filecoin error converting erc20 transaction to operation", e); + return []; + } +}; + +export async function buildTokenAccounts( + filAddr: string, + parentAccountId: string, + initialAccount?: Account, +): Promise { + try { + const transfers = await fetchERC20Transactions(filAddr); + const transfersUntangled: { [addr: string]: ERC20Transfer[] } = transfers.reduce( + (prev, curr) => { + curr.contract_address = curr.contract_address.toLowerCase(); + if (prev[curr.contract_address]) { + prev[curr.contract_address] = [...prev[curr.contract_address], curr]; + } else { + prev[curr.contract_address] = [curr]; + } + return prev; + }, + {}, + ); + + const subs: TokenAccount[] = []; + for (const [cAddr, txns] of Object.entries(transfersUntangled)) { + const token = findTokenByAddressInCurrency(cAddr, "filecoin"); + if (!token) { + log("error", `filecoin token not found, addr: ${cAddr}`); + continue; + } + + const balance = await fetchERC20TokenBalance(filAddr, cAddr); + const bnBalance = new BigNumber(balance.toString()); + const tokenAccountId = encodeTokenAccountId(parentAccountId, token); + + const operations = txns + .flatMap(txn => erc20TxnToOperation(txn, filAddr, tokenAccountId, token.units[0])) + .flat() + .sort((a, b) => b.date.getTime() - a.date.getTime()); + + if (operations.length === 0 && bnBalance.isZero()) { + continue; + } + + const maybeExistingSubAccount = + initialAccount && + initialAccount.subAccounts && + initialAccount.subAccounts.find(a => a.id === tokenAccountId); + + const sub: TokenAccount = { + type: AccountType.TokenAccount, + id: tokenAccountId, + parentId: parentAccountId, + token, + balance: bnBalance, + spendableBalance: bnBalance, + operationsCount: txns.length, + operations, + pendingOperations: maybeExistingSubAccount ? maybeExistingSubAccount.pendingOperations : [], + creationDate: operations.length > 0 ? operations[0].date : new Date(), + swapHistory: maybeExistingSubAccount ? maybeExistingSubAccount.swapHistory : [], + balanceHistoryCache: emptyHistoryCache, // calculated in the jsHelpers + }; + + subs.push(sub); + } + + return subs; + } catch (e) { + log("error", "filecoin error building token accounts", e); + return []; + } +} + +export const encodeTxnParams = (abiEncodedParams: string) => { + log("debug", `filecoin/abiEncodedParams: ${abiEncodedParams}`); + if (!abiEncodedParams) { + throw new Error("Cannot encode empty abi encoded params"); + } + + const buffer = Buffer.from(abiEncodedParams.slice(2), "hex"); // buffer/byte array + const dataEncoded = cbor.encode(buffer); + + return dataEncoded.toString("base64"); +}; + +export const abiEncodeTransferParams = (recipient: string, amount: string) => { + const contract = new ethers.utils.Interface(contractABI); + const data = contract.encodeFunctionData("transfer", [recipient, amount]); + return data; +}; + +export const generateTokenTxnParams = (recipient: string, amount: BigNumber) => { + log("debug", "generateTokenTxnParams", { recipient, amount: amount.toString() }); + + if (!recipient) { + throw new RecipientRequired(); + } + + recipient = convertAddressFilToEth(recipient); + + return abiEncodeTransferParams(recipient, amount.toString()); +}; diff --git a/libs/ledger-live-common/src/families/filecoin/bridge/utils/serializer.ts b/libs/ledger-live-common/src/families/filecoin/bridge/utils/serializer.ts index 341dc6738e55..c09909d5ce98 100644 --- a/libs/ledger-live-common/src/families/filecoin/bridge/utils/serializer.ts +++ b/libs/ledger-live-common/src/families/filecoin/bridge/utils/serializer.ts @@ -1,82 +1,63 @@ -import cbor from "@zondax/cbor"; import { Account } from "@ledgerhq/types-live"; -import BigNumber from "bignumber.js"; import { Transaction } from "../../types"; -import { getAddress } from "./utils"; +import { getAddress, getSubAccount } from "./utils"; import { validateAddress } from "./addresses"; +import { Message } from "iso-filecoin/message"; +import { encodeTxnParams } from "./erc20/tokenAccounts"; +import BigNumber from "bignumber.js"; +import { AccountType } from "../../utils"; -const bigNumberToArray = (v: BigNumber) => { - let tmp; - - // Adding byte sign - let signByte = "00"; - if (v.lt(0)) { - signByte = "01"; - } - - if (v.toString() === "") { - // to test with null bigint - return Buffer.from(signByte, "hex"); - } else { - tmp = v.toString(16); - // not sure why it is not padding and buffer does not like it - if (tmp.length % 2 === 1) tmp = "0" + tmp; - } - - return Buffer.concat([Buffer.from(signByte, "hex"), Buffer.from(tmp, "hex")]); -}; +export interface toCBORResponse { + txPayload: Buffer; + recipientToBroadcast: string; + parsedSender: string; + encodedParams: string; + amountToBroadcast: BigNumber; +} -export const toCBOR = ( - account: Account, - tx: Transaction, -): { txPayload: Buffer; parsedRecipient: string; parsedSender: string } => { +export const toCBOR = async (account: Account, tx: Transaction): Promise => { const { address: from } = getAddress(account); - const { method, version, nonce, gasLimit, gasPremium, gasFeeCap, params, amount, recipient } = tx; - const answer: any[] = []; - + const { method, version, nonce, gasLimit, gasPremium, gasFeeCap, amount, recipient } = tx; const recipientValidation = validateAddress(recipient); const fromValidation = validateAddress(from); + const subAccount = getSubAccount(account, tx); + const tokenTransfer = subAccount && subAccount.type === AccountType.TokenAccount; + const params = tokenTransfer ? encodeTxnParams(tx.params ?? "") : undefined; + if (!recipientValidation.isValid || !fromValidation.isValid) throw new Error("recipient and/or from address are not valid"); - // "version" field - answer.push(version); - - // "to" field - answer.push(Buffer.from(recipientValidation.parsedAddress.toBytes())); - - // "from" field - answer.push(Buffer.from(fromValidation.parsedAddress.toBytes())); - - // "nonce" field - answer.push(nonce); - - // "value" - let buf = bigNumberToArray(amount); - answer.push(buf); - - // "gaslimit" - answer.push(gasLimit.toNumber()); - - // "gasfeecap" - buf = bigNumberToArray(gasFeeCap); - answer.push(buf); - - // "gaspremium" - buf = bigNumberToArray(gasPremium); - answer.push(buf); - - // "method" - answer.push(method); + let finalRecipient: string; + if (tokenTransfer) { + const validated = validateAddress(subAccount.token.contractAddress); + if (!validated.isValid) throw new Error("token contract address is not valid"); + finalRecipient = validated.parsedAddress.toString(); + } else { + finalRecipient = recipientValidation.parsedAddress.toString(); + } - if (params) answer.push(params); - else answer.push(Buffer.alloc(0)); + const finalAmount = tokenTransfer ? "0" : amount.toString(); + + const message = new Message({ + to: finalRecipient, + from: fromValidation.parsedAddress.toString(), + gasFeeCap: gasFeeCap.toString(), + gasLimit: gasLimit.toNumber(), + gasPremium: gasPremium.toString(), + method, + nonce, + params, + version: version === 0 ? 0 : undefined, + value: finalAmount, + }); return { - txPayload: cbor.encode(answer), - parsedRecipient: recipientValidation.parsedAddress.toString(), + txPayload: message.serialize(), + recipientToBroadcast: finalRecipient, parsedSender: fromValidation.parsedAddress.toString(), + encodedParams: params ?? "", + amountToBroadcast: new BigNumber(finalAmount), }; }; diff --git a/libs/ledger-live-common/src/families/filecoin/bridge/utils/types.ts b/libs/ledger-live-common/src/families/filecoin/bridge/utils/types.ts index aa62a479dd78..dddb03c58f11 100644 --- a/libs/ledger-live-common/src/families/filecoin/bridge/utils/types.ts +++ b/libs/ledger-live-common/src/families/filecoin/bridge/utils/types.ts @@ -10,6 +10,8 @@ export interface EstimatedFeesRequest { from: string; methodNum?: number; blockIncl?: number; + params?: string; + value?: string; } export interface EstimatedFeesResponse { @@ -75,3 +77,29 @@ interface BlockIdentifier { index: number; hash: string; } + +export interface ConvertFilToEthResponse { + address: string; +} + +export interface FetchERC20TransactionsResponse { + txs: ERC20Transfer[]; +} + +export interface ERC20Transfer { + id: string; + height: number; + type: string; + status: string; + to: string; + from: string; + amount: string; + contract_address: string; + timestamp: number; + tx_hash: string; + tx_cid?: string; +} + +export interface ERC20BalanceResponse { + data: { balance: string; height: number }[]; +} diff --git a/libs/ledger-live-common/src/families/filecoin/bridge/utils/utils.ts b/libs/ledger-live-common/src/families/filecoin/bridge/utils/utils.ts index f11c435f258b..cf1a581978f4 100644 --- a/libs/ledger-live-common/src/families/filecoin/bridge/utils/utils.ts +++ b/libs/ledger-live-common/src/families/filecoin/bridge/utils/utils.ts @@ -8,6 +8,8 @@ import { fetchBalances, fetchBlockHeight, fetchTxs } from "./api"; import { encodeAccountId } from "../../../../account"; import { encodeOperationId } from "../../../../operation"; import flatMap from "lodash/flatMap"; +import { buildTokenAccounts } from "./erc20/tokenAccounts"; +import { Transaction } from "../../types"; type TxsById = { [id: string]: @@ -93,10 +95,11 @@ export const mapTxToOps = const hasFailed = status !== TxStatus.Ok; if (isSending) { + const type = value.eq(0) ? "FEES" : "OUT"; ops.push({ - id: encodeOperationId(accountId, hash, "OUT"), + id: encodeOperationId(accountId, hash, type), hash, - type: "OUT", + type, value: value.plus(feeToUse), fee: feeToUse, blockHeight: tx.height, @@ -111,10 +114,11 @@ export const mapTxToOps = } if (isReceiving) { + const type = value.eq(0) ? "FEES" : "IN"; ops.push({ - id: encodeOperationId(accountId, hash, "IN"), + id: encodeOperationId(accountId, hash, type), hash, - type: "IN", + type, value, fee: feeToUse, blockHeight: tx.height, @@ -143,21 +147,32 @@ export const getTxToBroadcast = ( signature: string, rawData: Record, ): BroadcastTransactionRequest => { - const { senders, recipients, value, fee } = operation; - const { gasLimit, gasFeeCap, gasPremium, method, version, nonce, signatureType } = rawData; + const { + sender, + recipient, + gasLimit, + gasFeeCap, + gasPremium, + method, + version, + nonce, + signatureType, + params, + value, + } = rawData; return { message: { version, method, nonce, - params: "", - to: recipients[0], - from: senders[0], + params: params ?? "", + to: recipient, + from: sender, gaslimit: gasLimit.toNumber(), gaspremium: gasPremium.toString(), gasfeecap: gasFeeCap.toString(), - value: value.minus(fee).toFixed(), + value, }, signature: { type: signatureType, @@ -180,14 +195,27 @@ export const getAccountShape: GetAccountShape = async info => { const blockHeight = await fetchBlockHeight(); const balance = await fetchBalances(address); const rawTxs = await fetchTxs(address); + const tokenAccounts = await buildTokenAccounts(address, accountId, info.initialAccount); - const result = { + const result: Partial = { id: accountId, + subAccounts: tokenAccounts, balance: new BigNumber(balance.total_balance), spendableBalance: new BigNumber(balance.spendable_balance), - operations: flatMap(processTxs(rawTxs), mapTxToOps(accountId, info)), + operations: flatMap(processTxs(rawTxs), mapTxToOps(accountId, info)).sort( + (a, b) => b.date.getTime() - a.date.getTime(), + ), blockHeight: blockHeight.current_block_identifier.index, }; return result; }; + +export const getSubAccount = (account: Account, tx: Transaction) => { + const subAccount = + tx.subAccountId && account.subAccounts + ? account.subAccounts.find(sa => sa.id === tx.subAccountId) + : null; + + return subAccount; +}; diff --git a/libs/ledger-live-common/src/families/filecoin/buildOptimisticOperation.ts b/libs/ledger-live-common/src/families/filecoin/buildOptimisticOperation.ts index 3041da941133..e9ac18fb8470 100644 --- a/libs/ledger-live-common/src/families/filecoin/buildOptimisticOperation.ts +++ b/libs/ledger-live-common/src/families/filecoin/buildOptimisticOperation.ts @@ -1,32 +1,58 @@ import { Account, Operation, OperationType } from "@ledgerhq/types-live"; import { encodeOperationId } from "@ledgerhq/coin-framework/operation"; import { Transaction } from "./types"; -import { toCBOR } from "./bridge/utils/serializer"; +import { toCBORResponse } from "./bridge/utils/serializer"; import { calculateEstimatedFees } from "./utils"; +import { convertAddressEthToFil } from "./bridge/utils/addresses"; +import { getSubAccount } from "./bridge/utils/utils"; -export const buildOptimisticOperation = ( +export const buildOptimisticOperation = async ( account: Account, transaction: Transaction, + serializationRes: toCBORResponse, operationType: OperationType = "OUT", -): Operation => { - const { id: accountId } = account; +): Promise => { + const type = operationType; + const subAccount = getSubAccount(account, transaction); // resolved at broadcast time - const hash = ""; - const { parsedSender, parsedRecipient } = toCBOR(account, transaction); - const fee = calculateEstimatedFees(transaction.gasFeeCap, transaction.gasLimit); + const txHash = ""; + const { gasFeeCap, gasLimit } = transaction; + const { parsedSender, recipientToBroadcast, amountToBroadcast: finalAmount } = serializationRes; + const fee = calculateEstimatedFees(gasFeeCap, gasLimit); - return { - id: encodeOperationId(accountId, hash, operationType), - hash, - type: "OUT", - senders: [parsedSender], - recipients: [parsedRecipient], - accountId, - value: transaction.amount.plus(fee), - fee, - blockHash: null, - blockHeight: null, - date: new Date(), - extra: {}, - }; + let operation: Operation; + if (subAccount) { + const recipientFilAddr = convertAddressEthToFil(transaction.recipient); + operation = { + id: encodeOperationId(subAccount.id, txHash, "OUT"), + hash: txHash, + type, + value: finalAmount, + fee, + blockHeight: null, + blockHash: null, + accountId: subAccount.id, + senders: [parsedSender], + recipients: [recipientFilAddr], + date: new Date(), + extra: {}, + }; + } else { + operation = { + id: encodeOperationId(account.id, txHash, "OUT"), + hash: txHash, + type, + senders: [parsedSender], + recipients: [recipientToBroadcast], + accountId: account.id, + value: finalAmount.plus(fee), + fee, + blockHash: null, + blockHeight: null, + date: new Date(), + extra: {}, + }; + } + + return operation; }; diff --git a/libs/ledger-live-common/src/families/filecoin/cli-transaction.ts b/libs/ledger-live-common/src/families/filecoin/cli-transaction.ts index 709803cd2117..44376a5ad0df 100644 --- a/libs/ledger-live-common/src/families/filecoin/cli-transaction.ts +++ b/libs/ledger-live-common/src/families/filecoin/cli-transaction.ts @@ -1,30 +1,72 @@ import type { Account, AccountLike, AccountLikeArray } from "@ledgerhq/types-live"; import invariant from "invariant"; import flatMap from "lodash/flatMap"; -import type { Transaction } from "../../generated/types"; +import type { Transaction } from "./types"; +import { getAccountCurrency } from "../../account"; +import { AccountType } from "./utils"; -const options = []; +const options = [ + { + name: "token", + alias: "t", + type: String, + desc: "use an token account children of the account", + }, +]; -function inferAccounts(account: Account): AccountLikeArray { +function inferAccounts(account: Account, opts: Record): AccountLikeArray { invariant(account.currency.family === "filecoin", "filecoin family"); - const accounts: Account[] = [account]; - return accounts; + if (!opts.token) { + const accounts: Account[] = [account]; + return accounts; + } + + const token = opts.token; + + const subAccounts = account.subAccounts || []; + + if (token) { + const subAccount = subAccounts.find(t => { + const currency = getAccountCurrency(t); + return ( + token.toLowerCase() === currency.ticker.toLowerCase() || token.toLowerCase() === currency.id + ); + }); + + if (!subAccount) { + throw new Error( + "token account '" + + token + + "' not found. Available: " + + subAccounts.map(t => getAccountCurrency(t).ticker).join(", "), + ); + } + + return [subAccount]; + } + + return []; } function inferTransactions( transactions: Array<{ account: AccountLike; transaction: Transaction; - mainAccount: Account; }>, ): Transaction[] { - return flatMap(transactions, ({ transaction }) => { + return flatMap(transactions, ({ transaction, account }) => { invariant(transaction.family === "filecoin", "filecoin family"); + if (account.type === AccountType.TokenAccount) { + const isDelisted = account.token.delisted === true; + invariant(!isDelisted, "token is delisted"); + } + return { ...transaction, family: "filecoin", + subAccountId: account.type === AccountType.TokenAccount ? account.id : null, } as Transaction; }); } diff --git a/libs/ledger-live-common/src/families/filecoin/deviceTransactionConfig.ts b/libs/ledger-live-common/src/families/filecoin/deviceTransactionConfig.ts index a2128fd51e78..bcb4e3692f26 100644 --- a/libs/ledger-live-common/src/families/filecoin/deviceTransactionConfig.ts +++ b/libs/ledger-live-common/src/families/filecoin/deviceTransactionConfig.ts @@ -1,10 +1,15 @@ import type { DeviceTransactionField } from "../../transaction"; -import type { Account, AccountLike } from "@ledgerhq/types-live"; +import type { Account, AccountLike, TokenAccount } from "@ledgerhq/types-live"; import type { Transaction, TransactionStatus } from "./types"; -import { formatCurrencyUnit, getCryptoCurrencyById } from "../../currencies"; -import { methodToString } from "./utils"; - -const currency = getCryptoCurrencyById("filecoin"); +import { formatCurrencyUnit } from "../../currencies"; +import { + AccountType, + Methods, + expectedToFieldForTokenTransfer, + getAccountUnit, + methodToString, +} from "./utils"; +import { validateAddress } from "./bridge/utils/addresses"; export type ExtraDeviceTransactionField = | { @@ -26,6 +31,11 @@ export type ExtraDeviceTransactionField = type: "filecoin.method"; label: string; value: string; + } + | { + type: "filecoin.recipient"; + label: string; + value: string; }; function getDeviceTransactionConfig(input: { @@ -34,39 +44,73 @@ function getDeviceTransactionConfig(input: { transaction: Transaction; status: TransactionStatus; }): Array { + const tokenTransfer = input.account.type === AccountType.TokenAccount; + const subAccount = tokenTransfer ? (input.account as TokenAccount) : null; + const fields: Array = []; + const unit = input.parentAccount + ? input.parentAccount.currency.units[0] + : getAccountUnit(input.account); + const formatConfig = { + disableRounding: true, + alwaysShowSign: false, + showCode: false, + }; + + if (subAccount) { + fields.push({ + type: "filecoin.recipient", + label: "To", + value: expectedToFieldForTokenTransfer(input.transaction.recipient), + }); + } 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, + }); + } + } + } - fields.push({ - type: "amount", - label: "Value", - }); fields.push({ type: "filecoin.gasLimit", label: "Gas Limit", - value: input.transaction.gasLimit.toFixed(), - }); - fields.push({ - type: "filecoin.gasPremium", - label: "Gas Premium", - value: formatCurrencyUnit(currency.units[0], input.transaction.gasPremium, { - showCode: false, - disableRounding: true, - }), - }); - fields.push({ - type: "filecoin.gasFeeCap", - label: "Gas Fee Cap", - value: formatCurrencyUnit(currency.units[0], input.transaction.gasFeeCap, { - showCode: false, - disableRounding: true, - }), - }); - fields.push({ - type: "filecoin.method", - label: "Method", - value: methodToString(input.transaction.method), + value: formatCurrencyUnit(unit, input.transaction.gasLimit, formatConfig), }); + if (!subAccount) { + fields.push({ + type: "filecoin.gasFeeCap", + label: "Gas Fee Cap", + value: formatCurrencyUnit(unit, input.transaction.gasFeeCap, formatConfig), + }); + + fields.push({ + type: "filecoin.gasPremium", + label: "Gas Premium", + value: formatCurrencyUnit(unit, input.transaction.gasPremium, formatConfig), + }); + fields.push({ + type: "filecoin.method", + label: "Method", + value: methodToString(input.transaction.method), + }); + } + + if (subAccount) { + fields.push({ + type: "filecoin.method", + label: "Method", + value: methodToString(Methods.ERC20Transfer), + }); + } + return fields; } diff --git a/libs/ledger-live-common/src/families/filecoin/errors.ts b/libs/ledger-live-common/src/families/filecoin/errors.ts new file mode 100644 index 000000000000..1317fbc0e976 --- /dev/null +++ b/libs/ledger-live-common/src/families/filecoin/errors.ts @@ -0,0 +1,13 @@ +import { createCustomErrorClass } from "@ledgerhq/errors"; + +/* + * When the recipient is non f0, f4 or eth address during token transfer + */ +export const InvalidRecipientForTokenTransfer = createCustomErrorClass( + "InvalidRecipientForTokenTransfer", +); + +/* + * When the fee estimation endpoint fails + */ +export const FilecoinFeeEstimationFailed = createCustomErrorClass("FilecoinFeeEstimationFailed"); diff --git a/libs/ledger-live-common/src/families/filecoin/estimateMaxSpendable.ts b/libs/ledger-live-common/src/families/filecoin/estimateMaxSpendable.ts index 2c88acf8fcbf..d07a4c1208ef 100644 --- a/libs/ledger-live-common/src/families/filecoin/estimateMaxSpendable.ts +++ b/libs/ledger-live-common/src/families/filecoin/estimateMaxSpendable.ts @@ -1,13 +1,18 @@ -import { AccountBridge } from "@ledgerhq/types-live"; +import { AccountBridge, TokenAccount } from "@ledgerhq/types-live"; import { Transaction } from "./types"; import { getMainAccount } from "../../account"; -import { getAddress } from "./bridge/utils/utils"; -import { Methods, calculateEstimatedFees } from "./utils"; -import { InvalidAddress } from "@ledgerhq/errors"; +import { getAddress, getSubAccount } from "./bridge/utils/utils"; +import { AccountType, Methods, calculateEstimatedFees } from "./utils"; +import { + InvalidAddress, + NotEnoughBalanceInParentAccount, + NotEnoughSpendableBalance, +} from "@ledgerhq/errors"; 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["estimateMaxSpendable"] = async ({ account, @@ -15,16 +20,31 @@ export const estimateMaxSpendable: AccountBridge["estimateMaxSpenda transaction, }) => { // log("debug", "[estimateMaxSpendable] start fn"); + if (transaction && !transaction.subAccountId) { + transaction.subAccountId = account.type === "Account" ? null : account.id; + } + + let tokenAccountTxn: boolean = false; + let subAccount: TokenAccount | undefined | null; + const a = getMainAccount(account, parentAccount); + if (account.type === AccountType.TokenAccount) { + tokenAccountTxn = true; + subAccount = account; + } + if (transaction && transaction.subAccountId && !subAccount) { + tokenAccountTxn = true; + subAccount = getSubAccount(a, transaction) ?? null; + } - const mainAccount = getMainAccount(account, parentAccount); - let { address: sender } = getAddress(mainAccount); + let { address: sender } = getAddress(a); let methodNum = Methods.Transfer; let recipient = transaction?.recipient; const invalidAddressErr = new InvalidAddress(undefined, { - currencyName: mainAccount.currency.name, + currencyName: subAccount ? subAccount.token.name : a.currency.name, }); + const senderValidation = validateAddress(sender); if (!senderValidation.isValid) throw invalidAddressErr; sender = senderValidation.parsedAddress.toString(); @@ -36,33 +56,64 @@ export const estimateMaxSpendable: AccountBridge["estimateMaxSpenda } recipient = recipientValidation.parsedAddress.toString(); - methodNum = isFilEthAddress(recipientValidation.parsedAddress) - ? Methods.InvokeEVM - : Methods.Transfer; + methodNum = + isFilEthAddress(recipientValidation.parsedAddress) || tokenAccountTxn + ? Methods.InvokeEVM + : Methods.Transfer; } - const balances = await fetchBalances(sender); - let balance = new BigNumber(balances.spendable_balance); + let balance = new BigNumber((await fetchBalances(sender)).spendable_balance); if (balance.eq(0)) return balance; - const amount = transaction?.amount; + const validatedContractAddress = validateAddress(subAccount?.token.contractAddress ?? ""); + if (tokenAccountTxn && !validatedContractAddress.isValid) { + throw invalidAddressErr; + } + const contractAddress = + tokenAccountTxn && validatedContractAddress.isValid + ? 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: recipient, + 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); - if (balance.lte(estimatedFees)) return new BigNumber(0); + if (balance.lte(estimatedFees)) { + if (tokenAccountTxn) { + throw new NotEnoughBalanceInParentAccount(undefined, { + currencyName: a.currency.name, + }); + } + throw new NotEnoughSpendableBalance(undefined, { + currencyName: a.currency.name, + }); + } balance = balance.minus(estimatedFees); - if (amount) balance = balance.minus(amount); + if (tokenAccountTxn && subAccount) { + return subAccount.spendableBalance; + } // log("debug", "[estimateMaxSpendable] finish fn"); return balance; diff --git a/libs/ledger-live-common/src/families/filecoin/getTransactionStatus.ts b/libs/ledger-live-common/src/families/filecoin/getTransactionStatus.ts index 9c12371e5900..fd13946e14dd 100644 --- a/libs/ledger-live-common/src/families/filecoin/getTransactionStatus.ts +++ b/libs/ledger-live-common/src/families/filecoin/getTransactionStatus.ts @@ -5,45 +5,47 @@ import { NotEnoughBalance, RecipientRequired, } from "@ledgerhq/errors"; -import { AccountBridge } from "@ledgerhq/types-live"; +import { Account, AccountBridge } from "@ledgerhq/types-live"; import { Transaction, TransactionStatus } from "./types"; -import { validateAddress } from "./bridge/utils/addresses"; -import { getAddress } from "./bridge/utils/utils"; +import { isRecipientValidForTokenTransfer, validateAddress } from "./bridge/utils/addresses"; +import { getAddress, getSubAccount } from "./bridge/utils/utils"; import { calculateEstimatedFees } from "./utils"; +import { InvalidRecipientForTokenTransfer } from "./errors"; +import BigNumber from "bignumber.js"; export const getTransactionStatus: AccountBridge["getTransactionStatus"] = async ( - account, - transaction, -) => { + account: Account, + transaction: Transaction, +): Promise => { // log("debug", "[getTransactionStatus] start fn"); - const errors: TransactionStatus["errors"] = {}; const warnings: TransactionStatus["warnings"] = {}; - const { balance } = account; const { address } = getAddress(account); + const subAccount = getSubAccount(account, transaction); const { recipient, useAllAmount, gasPremium, gasFeeCap, gasLimit } = transaction; let { amount } = transaction; - const invalidAddressErr = new InvalidAddress(undefined, { currencyName: account.currency.name, }); if (!recipient) errors.recipient = new RecipientRequired(); else if (!validateAddress(recipient).isValid) errors.recipient = invalidAddressErr; else if (!validateAddress(address).isValid) errors.sender = invalidAddressErr; - if (gasFeeCap.eq(0) || gasPremium.eq(0) || gasLimit.eq(0)) errors.gas = new FeeNotLoaded(); - // This is the worst case scenario (the tx won't cost more than this value) const estimatedFees = calculateEstimatedFees(gasFeeCap, gasLimit); - - let totalSpent; - if (useAllAmount) { + let totalSpent: BigNumber; + if (useAllAmount && !subAccount) { totalSpent = account.spendableBalance; amount = totalSpent.minus(estimatedFees); if (amount.lte(0) || totalSpent.gt(balance)) { errors.amount = new NotEnoughBalance(); } + } + + if (subAccount) { + totalSpent = estimatedFees; + if (totalSpent.gt(account.spendableBalance)) errors.amount = new NotEnoughBalance(); } else { totalSpent = amount.plus(estimatedFees); if (amount.eq(0)) { @@ -51,6 +53,21 @@ export const getTransactionStatus: AccountBridge["getTransactionSta } else if (totalSpent.gt(account.spendableBalance)) errors.amount = new NotEnoughBalance(); } + if (subAccount) { + const spendable = subAccount.spendableBalance; + if (transaction.amount.gt(spendable)) { + errors.amount = new NotEnoughBalance(); + } + if (useAllAmount) { + amount = spendable; + } + totalSpent = amount; + + if (recipient && !isRecipientValidForTokenTransfer(recipient)) { + errors.recipient = new InvalidRecipientForTokenTransfer(); + } + } + // log("debug", "[getTransactionStatus] finish fn"); return { diff --git a/libs/ledger-live-common/src/families/filecoin/prepareTransaction.ts b/libs/ledger-live-common/src/families/filecoin/prepareTransaction.ts index 226e5fdd442c..49161a738969 100644 --- a/libs/ledger-live-common/src/families/filecoin/prepareTransaction.ts +++ b/libs/ledger-live-common/src/families/filecoin/prepareTransaction.ts @@ -1,12 +1,17 @@ import BigNumber from "bignumber.js"; import { AccountBridge } from "@ledgerhq/types-live"; import { defaultUpdateTransaction } from "@ledgerhq/coin-framework/bridge/jsHelpers"; -import { isFilEthAddress, validateAddress } from "./bridge/utils/addresses"; +import { + isEthereumConvertableAddr, + isFilEthAddress, + validateAddress, +} from "./bridge/utils/addresses"; import { BroadcastBlockIncl } from "./bridge/utils/types"; import { Methods, calculateEstimatedFees } from "./utils"; import { fetchEstimatedFees } from "./bridge/utils/api"; -import { getAddress } from "./bridge/utils/utils"; +import { getAddress, getSubAccount } from "./bridge/utils/utils"; import { Transaction } from "./types"; +import { encodeTxnParams, generateTokenTxnParams } from "./bridge/utils/erc20/tokenAccounts"; export const prepareTransaction: AccountBridge["prepareTransaction"] = async ( account, @@ -14,7 +19,11 @@ export const prepareTransaction: AccountBridge["prepareTransaction" ) => { const { balance } = account; const { address } = getAddress(account); - const { recipient, useAllAmount } = transaction; + const { useAllAmount } = transaction; + const recipient = transaction.recipient.toLowerCase(); + + const subAccount = getSubAccount(account, transaction); + const tokenAccountTxn = !!subAccount; if (recipient && address) { const recipientValidation = validateAddress(recipient); @@ -23,15 +32,37 @@ export const prepareTransaction: AccountBridge["prepareTransaction" if (recipientValidation.isValid && senderValidation.isValid) { const patch: Partial = {}; - const method = isFilEthAddress(recipientValidation.parsedAddress) - ? Methods.InvokeEVM - : Methods.Transfer; + const method = + isFilEthAddress(recipientValidation.parsedAddress) || tokenAccountTxn + ? Methods.InvokeEVM + : Methods.Transfer; + + const validatedContractAddress = validateAddress(subAccount?.token.contractAddress ?? ""); + let finalRecipient = recipientValidation; + let params: string | undefined = undefined; + // used as fallback only for estimation of fees + let fallbackParams: string = ""; + if (tokenAccountTxn && validatedContractAddress.isValid) { + finalRecipient = validatedContractAddress; + // If token transfer, the evm payload is required to estimate fees + if (isEthereumConvertableAddr(recipientValidation.parsedAddress)) { + params = generateTokenTxnParams( + recipient, + transaction.amount.isZero() ? BigNumber(1) : transaction.amount, + ); + } else { + fallbackParams = generateTokenTxnParams(subAccount.token.contractAddress, BigNumber(1)); + } + } + const paramsForEstimation = params ? params : fallbackParams; const result = await fetchEstimatedFees({ - to: recipientValidation.parsedAddress.toString(), + to: finalRecipient.parsedAddress.toString(), from: senderValidation.parsedAddress.toString(), methodNum: method, blockIncl: BroadcastBlockIncl, + params: tokenAccountTxn ? encodeTxnParams(paramsForEstimation) : 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) }); patch.gasFeeCap = new BigNumber(result.gas_fee_cap); @@ -39,9 +70,14 @@ export const prepareTransaction: AccountBridge["prepareTransaction" patch.gasLimit = new BigNumber(result.gas_limit); patch.nonce = result.nonce; patch.method = method; + patch.params = params; const fee = calculateEstimatedFees(patch.gasFeeCap, patch.gasLimit); - if (useAllAmount) patch.amount = balance.minus(fee); + if (useAllAmount) { + patch.amount = subAccount ? subAccount.spendableBalance : balance.minus(fee); + patch.params = + tokenAccountTxn && params ? generateTokenTxnParams(recipient, patch.amount) : undefined; + } return defaultUpdateTransaction(transaction, patch); } diff --git a/libs/ledger-live-common/src/families/filecoin/signOperation.ts b/libs/ledger-live-common/src/families/filecoin/signOperation.ts index 2a2114b7b1e1..f7b97702103a 100644 --- a/libs/ledger-live-common/src/families/filecoin/signOperation.ts +++ b/libs/ledger-live-common/src/families/filecoin/signOperation.ts @@ -2,12 +2,12 @@ import { Observable } from "rxjs"; import { log } from "@ledgerhq/logs"; import Fil from "@zondax/ledger-filecoin"; import { FeeNotLoaded } from "@ledgerhq/errors"; -import { AccountBridge, SignOperationEvent } from "@ledgerhq/types-live"; +import { AccountBridge, Operation, SignOperationEvent } from "@ledgerhq/types-live"; import { buildOptimisticOperation } from "./buildOptimisticOperation"; import { withDevice } from "../../hw/deviceAccess"; import { toCBOR } from "./bridge/utils/serializer"; -import { getAddress } from "./bridge/utils/utils"; -import { getPath, isError } from "./utils"; +import { getAddress, getSubAccount } from "./bridge/utils/utils"; +import { AccountType, getPath, isError } from "./utils"; import { Transaction } from "./types"; import { close } from "../../hw"; @@ -24,6 +24,8 @@ export const signOperation: AccountBridge["signOperation"] = ({ const { method, version, nonce, gasFeeCap, gasLimit, gasPremium } = transaction; const { derivationPath } = getAddress(account); + const subAccount = getSubAccount(account, transaction); + const tokenAccountTxn = subAccount?.type === AccountType.TokenAccount; if (!gasFeeCap.gt(0) || !gasLimit.gt(0)) { log( @@ -41,7 +43,14 @@ export const signOperation: AccountBridge["signOperation"] = ({ }); // Serialize tx - const { txPayload } = toCBOR(account, transaction); + const toCBORResponse = await toCBOR(account, transaction); + const { + txPayload, + parsedSender, + recipientToBroadcast, + encodedParams, + amountToBroadcast: finalAmount, + } = toCBORResponse; log("debug", `[signOperation] serialized CBOR tx: [${txPayload.toString("hex")}]`); @@ -56,10 +65,17 @@ export const signOperation: AccountBridge["signOperation"] = ({ // build signature on the correct format const signature = `${result.signature_compact.toString("base64")}`; - const operation = buildOptimisticOperation(account, transaction); + const operation: Operation = await buildOptimisticOperation( + account, + transaction, + toCBORResponse, + ); // Necessary for broadcast const additionalTxFields = { + sender: parsedSender, + recipient: recipientToBroadcast, + params: encodedParams, gasLimit, gasFeeCap, gasPremium, @@ -67,6 +83,8 @@ export const signOperation: AccountBridge["signOperation"] = ({ version, nonce, signatureType: 1, + tokenTransfer: tokenAccountTxn, + value: finalAmount.toString(), }; o.next({ diff --git a/libs/ledger-live-common/src/families/filecoin/specs.ts b/libs/ledger-live-common/src/families/filecoin/specs.ts index 8826e591bd00..19c315c7540e 100644 --- a/libs/ledger-live-common/src/families/filecoin/specs.ts +++ b/libs/ledger-live-common/src/families/filecoin/specs.ts @@ -2,12 +2,13 @@ import invariant from "invariant"; import { DeviceModelId } from "@ledgerhq/devices"; import BigNumber from "bignumber.js"; -import type { Transaction } from "../../families/filecoin/types"; +import type { Transaction } from "./types"; import { getCryptoCurrencyById } from "../../currencies"; -import { genericTestDestination, pickSiblings } from "../../bot/specs"; +import { pickSiblings } from "../../bot/specs"; import type { AppSpec } from "../../bot/types"; import { generateDeviceActionFlow } from "./speculos-deviceActions"; -import { BotScenario } from "./utils"; +import { AccountType, BotScenario } from "./utils"; +import { botTest, genericTestDestination } from "@ledgerhq/coin-framework/bot/specs"; const F4_RECIPIENT = "f410fncojwmrseefktoco6rcnb3zv2eiqfli7muhvqma"; const ETH_RECIPIENT = "0x689c9b3232210aa9b84ef444d0ef35d11102ad1f"; @@ -20,9 +21,10 @@ const filecoinSpecs: AppSpec = { appQuery: { model: DeviceModelId.nanoSP, appName: "Filecoin", + appVersion: "0.24.3", }, genericDeviceAction: generateDeviceActionFlow(BotScenario.DEFAULT), - testTimeout: 6 * 60 * 1000, + testTimeout: 16 * 60 * 1000, minViableAmount: MIN_SAFE, transactionCheck: ({ maxSpendable }) => { invariant(maxSpendable.gt(MIN_SAFE), "balance is too low"); @@ -113,6 +115,48 @@ const filecoinSpecs: AppSpec = { }; }, }, + { + name: "Send ~50% WFIL", + maxRun: 1, + deviceAction: generateDeviceActionFlow(BotScenario.TOKEN_TRANSFER), + transaction: ({ account, bridge, maxSpendable }) => { + invariant(maxSpendable.gt(0), "Spendable balance is too low"); + const subAccount = account.subAccounts?.find( + a => a.type === AccountType.TokenAccount && a.spendableBalance.gt(0), + ); + invariant( + subAccount && subAccount.type === AccountType.TokenAccount, + "no subAccount with WFIL", + ); + const amount = subAccount.balance.div(1.9 + 0.2 * Math.random()).integerValue(); + return { + transaction: bridge.createTransaction(account), + updates: [ + { + subAccountId: subAccount.id, + }, + { + recipient: F4_RECIPIENT, + }, + { + amount, + }, + ], + }; + }, + test: ({ account, accountBeforeTransaction, transaction, status }) => { + const subAccountId = transaction.subAccountId; + const subAccount = account.subAccounts?.find(sa => sa.id === subAccountId); + const subAccountBeforeTransaction = accountBeforeTransaction.subAccounts?.find( + sa => sa.id === subAccountId, + ); + botTest("subAccount balance moved with the tx status amount", () => + expect(subAccount?.balance.toString()).toBe( + subAccountBeforeTransaction?.balance.minus(status.amount).toString(), + ), + ); + }, + }, ], }; diff --git a/libs/ledger-live-common/src/families/filecoin/speculos-deviceActions.ts b/libs/ledger-live-common/src/families/filecoin/speculos-deviceActions.ts index 42e716320809..f30fa8498dd8 100644 --- a/libs/ledger-live-common/src/families/filecoin/speculos-deviceActions.ts +++ b/libs/ledger-live-common/src/families/filecoin/speculos-deviceActions.ts @@ -1,9 +1,11 @@ import type { DeviceAction } from "../../bot/types"; import type { Transaction } from "./types"; import { deviceActionFlow, formatDeviceAmount, SpeculosButton } from "../../bot/specs"; -import { BotScenario, Methods } from "./utils"; +import { BotScenario, expectedToFieldForTokenTransfer, Methods, methodToString } from "./utils"; import { isFilEthAddress } from "./bridge/utils/addresses"; import { fromEthAddress, fromString, toEthAddress } from "iso-filecoin/address"; +import { getSubAccount } from "./bridge/utils/utils"; +import invariant from "invariant"; export const generateDeviceActionFlow = (scenario: BotScenario): DeviceAction => { const data: Parameters>[0] = { steps: [] }; @@ -34,6 +36,14 @@ export const generateDeviceActionFlow = (scenario: BotScenario): DeviceAction { + return expectedToFieldForTokenTransfer(transaction.recipient); + }, + }); } else { data.steps.push({ title: "To", @@ -53,7 +63,24 @@ export const generateDeviceActionFlow = (scenario: BotScenario): DeviceAction transaction.nonce.toString(), }, - { + ]); + + if (scenario == BotScenario.TOKEN_TRANSFER) { + data.steps.push({ + title: "Value", + button: SpeculosButton.RIGHT, + expectedValue: ({ account, transaction }) => { + const subAccount = getSubAccount(account, transaction); + invariant(subAccount, "subAccount is required for token transfer"); + + return formatDeviceAmount(subAccount.token, transaction.amount, { + hideCode: false, + showAllDigits: true, + }); + }, + }); + } else { + data.steps.push({ title: "Value", button: SpeculosButton.RIGHT, expectedValue: ({ account, status }) => @@ -61,31 +88,46 @@ export const generateDeviceActionFlow = (scenario: BotScenario): DeviceAction transaction.gasLimit.toFixed(), - }, - { - title: "Gas Premium", - button: SpeculosButton.RIGHT, - expectedValue: ({ account, transaction }) => - formatDeviceAmount(account.currency, transaction.gasPremium, { - hideCode: true, - showAllDigits: true, - }), - }, - { - title: "Gas Fee Cap", - button: SpeculosButton.RIGHT, - expectedValue: ({ account, transaction }) => - formatDeviceAmount(account.currency, transaction.gasFeeCap, { - hideCode: true, + expectedValue: ({ transaction, account }) => + formatDeviceAmount(account.currency, transaction.gasLimit, { + hideCode: false, showAllDigits: true, }), - }, - ]); + }); + } else { + data.steps = data.steps.concat([ + { + title: "Gas Limit", + button: SpeculosButton.RIGHT, + expectedValue: ({ transaction }) => transaction.gasLimit.toFixed(), + }, + { + title: "Gas Premium", + button: SpeculosButton.RIGHT, + expectedValue: ({ account, transaction }) => + formatDeviceAmount(account.currency, transaction.gasPremium, { + hideCode: true, + showAllDigits: true, + }), + }, + { + title: "Gas Fee Cap", + button: SpeculosButton.RIGHT, + expectedValue: ({ account, transaction }) => + formatDeviceAmount(account.currency, transaction.gasFeeCap, { + hideCode: true, + showAllDigits: true, + }), + }, + ]); + } if (scenario == BotScenario.ETH_RECIPIENT || scenario == BotScenario.F4_RECIPIENT) { data.steps.push({ @@ -93,6 +135,12 @@ export const generateDeviceActionFlow = (scenario: BotScenario): DeviceAction Methods.InvokeEVM.toString(), }); + } else if (scenario == BotScenario.TOKEN_TRANSFER) { + data.steps.push({ + title: "Method", + button: SpeculosButton.RIGHT, + expectedValue: () => methodToString(Methods.ERC20Transfer), + }); } else { data.steps.push({ title: "Method", diff --git a/libs/ledger-live-common/src/families/filecoin/utils.ts b/libs/ledger-live-common/src/families/filecoin/utils.ts index 98958ae62a34..c9c0e771e1cc 100644 --- a/libs/ledger-live-common/src/families/filecoin/utils.ts +++ b/libs/ledger-live-common/src/families/filecoin/utils.ts @@ -1,14 +1,23 @@ +import { AccountLike } from "@ledgerhq/types-live"; import { BigNumber } from "bignumber.js"; +import { getEquivalentAddress } from "./bridge/utils/addresses"; export enum Methods { Transfer = 0, + ERC20Transfer = 1, InvokeEVM = 3844450837, } +export enum AccountType { + Account = "Account", + TokenAccount = "TokenAccount", +} + export enum BotScenario { DEFAULT = "default", ETH_RECIPIENT = "eth-recipient", F4_RECIPIENT = "f4-recipient", + TOKEN_TRANSFER = "token-transfer", } const validHexRegExp = new RegExp(/^(0x)?[a-fA-F0-9]+$/); @@ -34,6 +43,8 @@ export const methodToString = (method: number): string => { return "Transfer"; case Methods.InvokeEVM: return "InvokeEVM (3844450837)"; + case Methods.ERC20Transfer: + return "ERC20 Transfer"; default: return "Unknown"; } @@ -48,3 +59,22 @@ export const getBufferFromString = (message: string): Buffer => export const calculateEstimatedFees = (gasFeeCap: BigNumber, gasLimit: BigNumber): BigNumber => gasFeeCap.multipliedBy(gasLimit); + +export function getAccountUnit(account: AccountLike) { + if (account.type === AccountType.TokenAccount) { + return account.token.units[0]; + } + + return account.currency.units[0]; +} + +export const expectedToFieldForTokenTransfer = (recipient: string) => { + let value = recipient; + const equivalent = getEquivalentAddress(value); + + if (equivalent && value != equivalent) { + value += ` / ${equivalent}`; + } + + return value; +}; diff --git a/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/filecoin/filecoin-erc20.test.ts b/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/filecoin/filecoin-erc20.test.ts new file mode 100644 index 000000000000..b7e71af0d1b6 --- /dev/null +++ b/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/filecoin/filecoin-erc20.test.ts @@ -0,0 +1,72 @@ +import axios from "axios"; +import { importFilecoinERC20Tokens } from "."; +import fs from "fs"; + +const filecoinERC20 = [ + [ + "filecoin", + "axelar_wrapped_usdc", + "AXLUSDC", + 6, + "Axelar Wrapped USDC", + "3044022045ea1314b020f8e217f4cf5aa6bda23c29cbe41fbde96b04ac9b4c8d1fe2a6e802207bbd5d6029e8c57af71bf0121c714ad95216e8b24346bc6e46eb0c2dca1bb50f", + "0xEB466342C4d449BC9f53A865D5Cb90586f405215", + false, + false, + null, + ], +]; + +const mockedAxios = jest.spyOn(axios, "get"); + +describe("import filecoin ERC20 tokens", () => { + beforeEach(() => { + mockedAxios.mockImplementation(() => + Promise.resolve({ data: filecoinERC20, headers: { etag: "etagHash" } }), + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should output the file in the correct format", async () => { + const expectedFile = `export type FilecoinERC20Token = [ + string, // parent currency id + string, // token + string, // ticker + number, // precision + string, // name + string, // ledgerSignature + string, // contract eth address + // string, // contract fil address + boolean, // disabled counter values + boolean, // delisted + (string | null)?, // countervalue_ticker (legacy) +]; + +import tokens from "./filecoin-erc20.json"; + +export { default as hash } from "./filecoin-erc20-hash.json"; + +export default tokens as FilecoinERC20Token[]; +`; + + const mockedFs = (fs.writeFileSync = jest.fn()); + + await importFilecoinERC20Tokens("."); + + expect(mockedAxios).toHaveBeenCalledWith(expect.stringMatching(/.*\/evm\/314\/erc20.json/)); + expect(mockedFs).toHaveBeenNthCalledWith( + 1, + "filecoin-erc20.json", + JSON.stringify(filecoinERC20), + ); + expect(mockedFs).toHaveBeenNthCalledWith( + 2, + "filecoin-erc20-hash.json", + JSON.stringify("etagHash"), + ); + expect(mockedFs).toHaveBeenNthCalledWith(3, "filecoin-erc20.ts", expectedFile); + }); +}); diff --git a/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/filecoin/index.ts b/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/filecoin/index.ts new file mode 100644 index 000000000000..26fceba01142 --- /dev/null +++ b/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/filecoin/index.ts @@ -0,0 +1,57 @@ +import fs from "fs"; +import path from "path"; +import { fetchTokens } from "../../fetch"; + +type FilecoinERC20Token = [ + string, // parent currency id + string, // token + string, // ticker + number, // precision + string, // name + string, // ledgerSignature + string, // contract eth address + // string, // contract fil address + boolean, // disabled counter values + boolean, // delisted + (string | null)?, // countervalue_ticker (legacy) +]; + +export const importFilecoinERC20Tokens = async (outputDir: string) => { + try { + console.log("importing filecoin erc20 tokens..."); + const [erc20tokens, hash] = await fetchTokens("evm/314/erc20.json"); + const filePath = path.join(outputDir, "filecoin-erc20"); + fs.writeFileSync(`${filePath}.json`, JSON.stringify(erc20tokens)); + if (hash) { + fs.writeFileSync(`${filePath}-hash.json`, JSON.stringify(hash)); + } + + fs.writeFileSync( + `${filePath}.ts`, + `export type FilecoinERC20Token = [ + string, // parent currency id + string, // token + string, // ticker + number, // precision + string, // name + string, // ledgerSignature + string, // contract eth address + // string, // contract fil address + boolean, // disabled counter values + boolean, // delisted + (string | null)?, // countervalue_ticker (legacy) +]; + +import tokens from "./filecoin-erc20.json"; + +${hash ? `export { default as hash } from "./filecoin-erc20-hash.json";` : ""} + +export default tokens as FilecoinERC20Token[]; +`, + ); + + console.log("importing filecoin erc20 tokens sucess"); + } catch (err) { + console.error(err); + } +}; diff --git a/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/index.ts b/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/index.ts index bc73fea16154..b7a4bc6baf7d 100644 --- a/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/index.ts +++ b/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/index.ts @@ -12,6 +12,7 @@ import { importSPLTokens } from "./importers/spl"; import { importStellarTokens } from "./importers/stellar"; import { importTRC10Tokens } from "./importers/trc10"; import { importTRC20Tokens } from "./importers/trc20"; +import { importFilecoinERC20Tokens } from "./importers/filecoin"; import { importBEP20Exchange } from "./exchange/bep20"; import { importERC20Exchange } from "./exchange/erc20"; @@ -38,6 +39,7 @@ const importTokens = async () => { importStellarTokens(outputFolder), importTRC10Tokens(outputFolder), importTRC20Tokens(outputFolder), + importFilecoinERC20Tokens(outputFolder), ]; await Promise.allSettled(promises); diff --git a/libs/ledgerjs/packages/cryptoassets/src/data/filecoin-erc20-hash.json b/libs/ledgerjs/packages/cryptoassets/src/data/filecoin-erc20-hash.json new file mode 100644 index 000000000000..7b36c1d40573 --- /dev/null +++ b/libs/ledgerjs/packages/cryptoassets/src/data/filecoin-erc20-hash.json @@ -0,0 +1 @@ +"\"62598ed2fa56dd6082fabfd402a5756f\"" \ No newline at end of file diff --git a/libs/ledgerjs/packages/cryptoassets/src/data/filecoin-erc20.json b/libs/ledgerjs/packages/cryptoassets/src/data/filecoin-erc20.json new file mode 100644 index 000000000000..e5c8d3e62023 --- /dev/null +++ b/libs/ledgerjs/packages/cryptoassets/src/data/filecoin-erc20.json @@ -0,0 +1 @@ +[["filecoin","axelar_wrapped_usdc","AXLUSDC",6,"Axelar Wrapped USDC","3044022045ea1314b020f8e217f4cf5aa6bda23c29cbe41fbde96b04ac9b4c8d1fe2a6e802207bbd5d6029e8c57af71bf0121c714ad95216e8b24346bc6e46eb0c2dca1bb50f","0xEB466342C4d449BC9f53A865D5Cb90586f405215",false,false,null],["filecoin","collectif_staked_fil","CLFIL",18,"Collectif Staked FIL","3044022029bbb2de272b6240140722bebe3996e402d216f73a64f8edee51e1be10953d860220289c41e8d59a6a5e665b3983d0b55c089bd0dd54392d4730390886fdf926abb7","0xd0437765D1Dc0e2fA14E97d290F135eFdf1a8a9A",false,false,null],["filecoin","infinity_pool_staked_fil","IFIL",18,"Infinity Pool Staked FIL","304402205759ac41f39c72de14f7764113659638e0485b002862054e0556219f4c96663302207c19d300bf88e5fc8fef9716981bf5afef04f803e9e4981f0644bd687d4fc82b","0x690908f7fa93afC040CFbD9fE1dDd2C2668Aa0e0",false,false,null],["filecoin","node_fil","NFIL",18,"Node FIL","30450221008714936bd8fb284d2841a3e0882611a9933c1857dd5ec1c5b396095d535d144e02205deb3ea25bc592b6b677de8cdc82c19d9ddd30c98408da7835c23ec67dc100e9","0x84B038DB0fCde4fae528108603C7376695dc217F",false,false,null],["filecoin","pfil_token","PFIL",18,"PFIL Token","3045022100e0e68bc479e2091e00cc551dc3e95ff7d74c3bbb02461822bd9c3b212786c26e022035cb30950a59ea1742b5b363671bb71de4afc822763e41cd6c0f778911a96439","0xAaa93ac72bECfbBc9149f293466bbdAa4b5Ef68C",false,false,null],["filecoin","securitized_filecoin_token","SFT",18,"Securitized Filecoin Token","304402206a592371b894592751d359c7116a41324093f4079664076c72cdb7225bde856c02201a58901b1a5266b42a6a607900770647f04722248c7fb86a24ff048c015a4bc0","0xC5eA96Dd365983cfEc90E72b6A2daC9562f458Ba",false,false,null],["filecoin","wrapped_fil","WFIL",18,"Wrapped FIL","3045022100db2e9495c8ad5c58d92f2c2f36d351f2d4ea6d7a5a4ed9988c11f0ebe8429d89022020ce70afe96ac64a8b18b7727b36c653ac9765e284a3a77d39fa85a78a6f7fed","0x60E1773636CF5E4A227d9AC24F20fEca034ee25A",false,false,null],["filecoin","wrapped_pfil_token","WPFIL",18,"Wrapped PFIL Token","304402202f1f912694c6d2c1be898d7698897e305681f1a7f86a46cd7319e675779fcc3502201d05d18d2b3737e56a2caff6cf23ccf4a7016f32af51e90e1693c5ec0e1392b8","0x57E3BB9F790185Cfe70Cc2C15Ed5d6B84dCf4aDb",false,false,null]] \ No newline at end of file diff --git a/libs/ledgerjs/packages/cryptoassets/src/data/filecoin-erc20.ts b/libs/ledgerjs/packages/cryptoassets/src/data/filecoin-erc20.ts new file mode 100644 index 000000000000..3f25b356ab26 --- /dev/null +++ b/libs/ledgerjs/packages/cryptoassets/src/data/filecoin-erc20.ts @@ -0,0 +1,19 @@ +export type FilecoinERC20Token = [ + string, // parent currency id + string, // token + string, // ticker + number, // precision + string, // name + string, // ledgerSignature + string, // contract eth address + // string, // contract fil address + boolean, // disabled counter values + boolean, // delisted + (string | null)?, // countervalue_ticker (legacy) +]; + +import tokens from "./filecoin-erc20.json"; + +export { default as hash } from "./filecoin-erc20-hash.json"; + +export default tokens as FilecoinERC20Token[]; diff --git a/libs/ledgerjs/packages/cryptoassets/src/tokens.ts b/libs/ledgerjs/packages/cryptoassets/src/tokens.ts index 4ed95df062ac..db83f2ed8a6b 100644 --- a/libs/ledgerjs/packages/cryptoassets/src/tokens.ts +++ b/libs/ledgerjs/packages/cryptoassets/src/tokens.ts @@ -7,6 +7,7 @@ import casperTokens, { CasperToken } from "./data/casper"; import erc20tokens, { ERC20Token } from "./data/erc20"; import esdttokens, { ElrondESDTToken } from "./data/esdt"; import polygonTokens, { PolygonERC20Token } from "./data/polygon-erc20"; +import filecoinTokens, { FilecoinERC20Token } from "./data/filecoin-erc20"; import stellarTokens, { StellarToken } from "./data/stellar"; import trc10tokens, { TRC10Token } from "./data/trc10"; import trc20tokens, { TRC20Token } from "./data/trc20"; @@ -25,6 +26,7 @@ const tokenListHashes = new Set(); addTokens(erc20tokens.map(convertERC20)); addTokens(polygonTokens.map(convertERC20)); +addTokens(filecoinTokens.map(convertERC20)); addTokens(trc10tokens.map(convertTRONTokens("trc10"))); addTokens(trc20tokens.map(convertTRONTokens("trc20"))); addTokens(bep20tokens.map(convertBEP20)); @@ -241,7 +243,7 @@ export function convertERC20([ contractAddress, disableCountervalue, delisted, -]: ERC20Token | PolygonERC20Token): TokenCurrency | undefined { +]: ERC20Token | PolygonERC20Token | FilecoinERC20Token): TokenCurrency | undefined { const parentCurrency = findCryptoCurrencyById(parentCurrencyId); if (!parentCurrency) { diff --git a/libs/ui/packages/crypto-icons/src/svg/CLFIL.svg b/libs/ui/packages/crypto-icons/src/svg/CLFIL.svg new file mode 100644 index 000000000000..da5db7c744f6 --- /dev/null +++ b/libs/ui/packages/crypto-icons/src/svg/CLFIL.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/libs/ui/packages/crypto-icons/src/svg/IFIL.svg b/libs/ui/packages/crypto-icons/src/svg/IFIL.svg new file mode 100644 index 000000000000..996a08c34102 --- /dev/null +++ b/libs/ui/packages/crypto-icons/src/svg/IFIL.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/libs/ui/packages/crypto-icons/src/svg/PFIL.svg b/libs/ui/packages/crypto-icons/src/svg/PFIL.svg new file mode 100644 index 000000000000..6d11f5dd0498 --- /dev/null +++ b/libs/ui/packages/crypto-icons/src/svg/PFIL.svg @@ -0,0 +1,3 @@ + + + diff --git a/libs/ui/packages/crypto-icons/src/svg/WFIL.svg b/libs/ui/packages/crypto-icons/src/svg/WFIL.svg new file mode 100644 index 000000000000..5b90a94e9659 --- /dev/null +++ b/libs/ui/packages/crypto-icons/src/svg/WFIL.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/ui/packages/crypto-icons/src/svg/WPFIL.svg b/libs/ui/packages/crypto-icons/src/svg/WPFIL.svg new file mode 100644 index 000000000000..bf66cecbbd03 --- /dev/null +++ b/libs/ui/packages/crypto-icons/src/svg/WPFIL.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d770f3b4a5dc..e5b6fd7aab20 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3480,6 +3480,9 @@ importers: eip55: specifier: ^2.1.1 version: 2.1.1 + ethers: + specifier: 5.7.2 + version: 5.7.2 expect: specifier: ^27.4.6 version: 27.5.1 @@ -3490,8 +3493,8 @@ importers: specifier: ^2.2.2 version: 2.2.4 iso-filecoin: - specifier: ^4.0.3 - version: 4.0.3 + specifier: ^4.1.0 + version: 4.1.0 isomorphic-ws: specifier: ^4.0.1 version: 4.0.1(ws@7.5.10) @@ -9915,8 +9918,8 @@ packages: '@iov/utils@2.0.2': resolution: {integrity: sha512-4D8MEvTcFc/DVy5q25vHxRItmgJyeX85dixMH+MxdKr+yy71h3sYk+sVBEIn70uqGP7VqAJkGOPNFs08/XYELw==} - '@ipld/dag-cbor@9.2.0': - resolution: {integrity: sha512-N14oMy0q4gM6OuZkIpisKe0JBSjf1Jb39VI+7jMLiWX9124u1Z3Fdj/Tag1NA0cVxxqWDh0CqsjcVfOKtelPDA==} + '@ipld/dag-cbor@9.2.1': + resolution: {integrity: sha512-nyY48yE7r3dnJVlxrdaimrbloh4RokQaNRdI//btfTkcTEZbpmSrbYcBQ4VKTf8ZxXAOUJy4VsRpkJo+y9RTnA==} engines: {node: '>=16.0.0', npm: '>=7.0.0'} '@isaacs/cliui@8.0.2': @@ -21066,21 +21069,21 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - iso-base@2.0.1: - resolution: {integrity: sha512-ip0nUW9oZP+LG6mslSgdJPt9NWZOq1qIOKpoUIdm8sq/87VwY8WZXw9ptCkhdJclY+r4feFbZuu9P/OFDqCvkQ==} + iso-base@4.0.0: + resolution: {integrity: sha512-Vf+6r7XlP+LQD0HSR0ZBLSj/mwuP+59ElbgMKT+iFSnkBW2RzRohQScgKfPCYbaGZAjEdbi40eYq28E/RAnrVA==} - iso-filecoin@4.0.3: - resolution: {integrity: sha512-JVEHvEoDs+Gz4uEvMo9wociX5LgUBxe84rXMoRtBwwXR2kjb1CeWbEiY8BeVzKOAiucy9vmFeYMdSqbxbvO9zA==} + iso-filecoin@4.1.0: + resolution: {integrity: sha512-jUtgWHC2I0j+0WtjhoRc321Mdo54/ntKbLZ1Ad1Ptq55N6RoHL2rJHp0RvPmjtCHlKc++DfMJnO4nWLwFVFr2g==} - iso-kv@3.0.2: - resolution: {integrity: sha512-DL4TNf1SRVskOKRsEk2QjMHsLUWh1H+iV4LPc9dMDfi1wcb/HlkGl+9ETphk3FN5ToL6l6CUKdeHGYyOPdmMHg==} + iso-kv@3.0.3: + resolution: {integrity: sha512-NVW7TJGiTbDlfBclLJeGqGLvZhgMu02tPId6ghLEygCVgitp3946TM6HJordcSbyJbD0wRzByilRYK/GY8dKlw==} iso-url@0.4.7: resolution: {integrity: sha512-27fFRDnPAMnHGLq36bWTpKET+eiXct3ENlCcdcMdk+mjXrb2kw3mhBUg1B7ewAC0kVzlOPhADzQgz1SE6Tglog==} engines: {node: '>=10'} - iso-web@1.0.5: - resolution: {integrity: sha512-ZRZ5BgGAvwau+WtNLZXp1byawaMPFUFnTwVHLXKFXIKxz0CD9hkEkSe505kxcNEui0TqVEQBOwg4806au1djGg==} + iso-web@1.0.6: + resolution: {integrity: sha512-aPG6UoEoUSecDAArSilUcmAlt4sW0eROQhDgvSC0yTWzh/RrhEwM3VIgrv2cTV0oXZiMpzmpLb4bNWT3ea7CzQ==} isobject@3.0.1: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} @@ -29591,6 +29594,9 @@ packages: zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -34779,7 +34785,7 @@ snapshots: '@iov/utils@2.0.2': {} - '@ipld/dag-cbor@9.2.0': + '@ipld/dag-cbor@9.2.1': dependencies: cborg: 4.1.4 multiformats: 13.1.0 @@ -45901,7 +45907,7 @@ snapshots: conf@12.0.0: dependencies: - ajv: 8.12.0 + ajv: 8.16.0 ajv-formats: 2.1.1 atomically: 2.0.2 debounce-fn: 5.1.2 @@ -51303,24 +51309,24 @@ snapshots: isexe@2.0.0: {} - iso-base@2.0.1: + iso-base@4.0.0: dependencies: base-x: 4.0.0 bigint-mod-arith: 3.3.1 - iso-filecoin@4.0.3: + iso-filecoin@4.1.0: dependencies: - '@ipld/dag-cbor': 9.2.0 + '@ipld/dag-cbor': 9.2.1 '@noble/curves': 1.4.0 - '@noble/hashes': 1.3.3 + '@noble/hashes': 1.4.0 '@scure/bip32': 1.4.0 '@scure/bip39': 1.3.0 bignumber.js: 9.1.2 - iso-base: 2.0.1 - iso-web: 1.0.5 - zod: 3.22.4 + iso-base: 4.0.0 + iso-web: 1.0.6 + zod: 3.23.8 - iso-kv@3.0.2: + iso-kv@3.0.3: dependencies: conf: 12.0.0 idb-keyval: 6.2.1 @@ -51328,10 +51334,10 @@ snapshots: iso-url@0.4.7: {} - iso-web@1.0.5: + iso-web@1.0.6: dependencies: delay: 6.0.0 - iso-kv: 3.0.2 + iso-kv: 3.0.3 p-retry: 6.2.0 isobject@3.0.1: {} @@ -64645,6 +64651,8 @@ snapshots: zod@3.22.4: {} + zod@3.23.8: {} + zwitch@2.0.4: {} zx@7.2.3: