Skip to content
This repository has been archived by the owner on Jul 15, 2022. It is now read-only.

LL 7340 & LL-6671 - Sync ERC721 & ERC1155 NFT #1405

Merged
merged 24 commits into from
Nov 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8f871f3
Add NFTs related types (NFT & NFTMetadata)
lambertkevin Aug 24, 2021
f83b0f9
Add NFT related serializers/deserializers
lambertkevin Aug 24, 2021
21df5ec
Update Eth API and sync to handle NFTs
lambertkevin Aug 24, 2021
4f72472
Mocking transactions & metadata calls to sync ERC721
lambertkevin Aug 24, 2021
dab624f
Add erc1155 events to eth tx type + change of mock
lambertkevin Sep 22, 2021
0a97a2b
Merge nfts on sync and make nfts optional with env
lambertkevin Sep 30, 2021
f08497b
Force full resync of eth accounts if nfts undef
lambertkevin Sep 30, 2021
fce0cb8
Update reconciliation for nfts
lambertkevin Sep 30, 2021
a77f55e
Add nftHelper with nftsByCollections
lambertkevin Oct 1, 2021
9e0b0db
Add nftsFromOperations to NFT helpers
lambertkevin Oct 4, 2021
25f0751
Add useNfts hook to overload nfts with metadata
lambertkevin Oct 1, 2021
adaffa9
Add nftsOps to groupOperations
lambertkevin Oct 4, 2021
04dcd60
Add graphql/dataloader to nft metadata helpers
lambertkevin Oct 5, 2021
1843176
Add refreshNft callback to useNfts' hook return
lambertkevin Oct 6, 2021
a9c6219
Add common dep to useNfts hook to refresh globally
lambertkevin Oct 12, 2021
26161e9
Rename nftHelpers
lambertkevin Oct 12, 2021
258c3b8
Add NFTMetadataProvider
lambertkevin Oct 12, 2021
cd41b7f
Remove useNfts hook
lambertkevin Oct 21, 2021
5b36f9a
Replace Metada service mock with staging API
lambertkevin Oct 21, 2021
4af6c9d
Add versionning to NFTs
lambertkevin Oct 26, 2021
f1bcbaf
added expiration logic
IAmMorrow Oct 26, 2021
a17f022
NFT: connect nftExplorerBaseURL
gre Nov 8, 2021
363127f
Merge branch 'master' into LL-7340/sync-erc1155-nft
gre Nov 8, 2021
70026db
update ledgerjs
gre Nov 8, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@
"dependencies": {
"@crypto-com/chain-jslib": "0.0.19",
"@ledgerhq/compressjs": "1.3.2",
"@ledgerhq/cryptoassets": "6.13.0",
"@ledgerhq/cryptoassets": "6.14.0",
"@ledgerhq/devices": "6.11.2",
"@ledgerhq/errors": "6.10.0",
"@ledgerhq/hw-app-algorand": "6.11.2",
"@ledgerhq/hw-app-btc": "6.12.1",
"@ledgerhq/hw-app-cosmos": "6.11.2",
"@ledgerhq/hw-app-eth": "6.13.0",
"@ledgerhq/hw-app-eth": "6.14.0",
"@ledgerhq/hw-app-polkadot": "6.11.2",
"@ledgerhq/hw-app-str": "6.11.2",
"@ledgerhq/hw-app-tezos": "6.11.2",
Expand Down
4 changes: 2 additions & 2 deletions src/account/groupOperations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { AccountLikeArray, AccountLike, Operation } from "../types";
import { flattenAccounts } from "./helpers";
import { flattenOperationWithInternals } from "../operation";
import { flattenOperationWithInternalsAndNfts } from "../operation";
export type DailyOperationsSection = {
day: Date;
data: Operation[];
Expand Down Expand Up @@ -105,7 +105,7 @@ export function groupAccountsOperationsByDay(
indexes[bestOpInfo.accountI]++;
}

const ops = flattenOperationWithInternals(bestOp);
const ops = flattenOperationWithInternalsAndNfts(bestOp);
return {
ops,
date: bestOp.date,
Expand Down
52 changes: 52 additions & 0 deletions src/account/serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import type {
OperationRaw,
SubAccount,
SubAccountRaw,
NFT,
NFTRaw,
} from "../types";
import type { TronResources, TronResourcesRaw } from "../families/tron/types";
import {
Expand Down Expand Up @@ -101,6 +103,7 @@ export const toOperationRaw = (
fee,
subOperations,
internalOperations,
nftOperations,
extra,
id,
hash,
Expand All @@ -112,6 +115,10 @@ export const toOperationRaw = (
transactionSequenceNumber,
accountId,
hasFailed,
contract,
operator,
standard,
tokenId,
}: Operation,
preserveSubOperation?: boolean
): OperationRaw => {
Expand Down Expand Up @@ -142,6 +149,10 @@ export const toOperationRaw = (
date: date.toISOString(),
value: value.toFixed(),
fee: fee.toString(),
contract,
operator,
standard,
tokenId,
};

if (transactionSequenceNumber !== undefined) {
Expand All @@ -160,6 +171,10 @@ export const toOperationRaw = (
copy.internalOperations = internalOperations.map((o) => toOperationRaw(o));
}

if (nftOperations) {
copy.nftOperations = nftOperations.map((o) => toOperationRaw(o));
}

return copy;
};
export const inferSubOperations = (
Expand Down Expand Up @@ -198,6 +213,7 @@ export const fromOperationRaw = (
extra,
subOperations,
internalOperations,
nftOperations,
id,
hash,
type,
Expand All @@ -207,6 +223,10 @@ export const fromOperationRaw = (
blockHash,
transactionSequenceNumber,
hasFailed,
contract,
operator,
standard,
tokenId,
}: OperationRaw,
accountId: string,
subAccounts?: SubAccount[] | null | undefined
Expand Down Expand Up @@ -238,6 +258,10 @@ export const fromOperationRaw = (
value: new BigNumber(value),
fee: new BigNumber(fee),
extra: e || {},
contract,
operator,
standard,
tokenId,
};

if (transactionSequenceNumber !== undefined) {
Expand All @@ -262,6 +286,12 @@ export const fromOperationRaw = (
);
}

if (nftOperations) {
res.nftOperations = nftOperations.map((o) =>
fromOperationRaw(o, o.accountId)
);
}

return res;
};
export const toTronResourcesRaw = ({
Expand Down Expand Up @@ -677,6 +707,7 @@ export function fromAccountRaw(rawAccount: AccountRaw): Account {
polkadotResources,
elrondResources,
cryptoOrgResources,
nfts,
} = rawAccount;
const subAccounts =
subAccountsRaw &&
Expand Down Expand Up @@ -734,6 +765,7 @@ export function fromAccountRaw(rawAccount: AccountRaw): Account {
swapHistory: [],
syncHash,
balanceHistoryCache: balanceHistoryCache || emptyHistoryCache,
nfts: nfts?.map((n) => fromNFTRaw(n)),
};
res.balanceHistoryCache = generateHistoryFromOperations(res);

Expand Down Expand Up @@ -834,6 +866,7 @@ export function toAccountRaw({
polkadotResources,
elrondResources,
cryptoOrgResources,
nfts,
}: Account): AccountRaw {
const res: AccountRaw = {
id,
Expand All @@ -857,6 +890,7 @@ export function toAccountRaw({
lastSyncDate: lastSyncDate.toISOString(),
balance: balance.toFixed(),
spendableBalance: spendableBalance.toFixed(),
nfts: nfts?.map((n) => toNFTRaw(n)),
};

if (balanceHistory) {
Expand Down Expand Up @@ -914,3 +948,21 @@ export function toAccountRaw({
}
return res;
}

export function toNFTRaw({ id, tokenId, amount, collection }: NFT): NFTRaw {
return {
id,
tokenId,
amount: amount.toFixed(),
collection,
};
}

export function fromNFTRaw({ id, tokenId, amount, collection }: NFTRaw): NFT {
return {
id,
tokenId,
amount: new BigNumber(amount),
collection,
};
}
42 changes: 40 additions & 2 deletions src/api/Ethereum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import invariant from "invariant";
import { BigNumber } from "bignumber.js";
import { LedgerAPINotAvailable } from "@ledgerhq/errors";
import JSONBigNumber from "@ledgerhq/json-bignumber";
import type { CryptoCurrency } from "../types";
import type { CryptoCurrency, NFTMetadataResponse } from "../types";
import type { EthereumGasLimitRequest } from "../families/ethereum/types";
import network from "../network";
import { blockchainBaseURL } from "./Ledger";
import { FeeEstimationFailed } from "../errors";
import { makeLRUCache } from "../cache";
import { getEnv } from "../env";

export type Block = {
height: BigNumber;
}; // TODO more fields actually
Expand Down Expand Up @@ -37,6 +39,22 @@ export type Tx = {
}>;
truncated: boolean;
};
erc721_transfer_events?: Array<{
contract: string;
sender: string;
receiver: string;
token_id: string;
}>;
erc1155_transfer_events?: Array<{
contract: string;
sender: string;
operator: string;
receiver: string;
transfers: Array<{
id: string;
value: string;
}>;
}>;
actions?: Array<{
from: string;
to: string;
Expand All @@ -59,6 +77,12 @@ export type ERC20BalanceOutput = Array<{
contract: string;
balance: BigNumber;
}>;
export type NFTMetadataInput = Readonly<
Array<{
contract: string;
tokenId: string;
}>
>;
export type API = {
getTransactions: (
address: string,
Expand All @@ -72,6 +96,7 @@ export type API = {
getAccountNonce: (address: string) => Promise<number>;
broadcastTransaction: (signedTransaction: string) => Promise<string>;
getERC20Balances: (input: ERC20BalancesInput) => Promise<ERC20BalanceOutput>;
getNFTMetadata: (input: NFTMetadataInput) => Promise<NFTMetadataResponse[]>;
getAccountBalance: (address: string) => Promise<BigNumber>;
roughlyEstimateGasLimit: (address: string) => Promise<BigNumber>;
getERC20ApprovalsPerContract: (
Expand Down Expand Up @@ -107,7 +132,10 @@ export const apiForCurrency = (currency: CryptoCurrency): API => {
let { data } = await network({
method: "GET",
url: URL.format({
pathname: `${baseURL}/addresses/${address}/transactions`,
pathname:
getEnv("NFT") && currency.ticker === "ETH"
? `https://explorers.api-01.live.ledger-stg.com/blockchain/v3/eth/addresses/${address}/transactions`
: `${baseURL}/addresses/${address}/transactions`,
query: {
batch_size,
noinput: true,
Expand Down Expand Up @@ -178,6 +206,16 @@ export const apiForCurrency = (currency: CryptoCurrency): API => {
return data;
},

async getNFTMetadata(input) {
const { data }: { data: NFTMetadataResponse[] } = await network({
method: "POST",
url: getEnv("NFT_ETH_METADATA_SERVICE"),
data: input,
});

return data;
},

async getERC20ApprovalsPerContract(owner, contract) {
try {
const { data } = await network({
Expand Down
35 changes: 35 additions & 0 deletions src/bridge/jsHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import isEqual from "lodash/isEqual";
import setWith from "lodash/setWith";
import { BigNumber } from "bignumber.js";
import { Observable, from } from "rxjs";
import { log } from "@ledgerhq/logs";
Expand Down Expand Up @@ -34,6 +35,7 @@ import type {
SyncConfig,
CryptoCurrency,
DerivationMode,
NFT,
} from "../types";
import type { CurrencyBridge, AccountBridge } from "../types/bridge";
import getAddress from "../hw/getAddress";
Expand Down Expand Up @@ -116,6 +118,39 @@ Operation[] {
return all;
}

export const mergeNfts = (
oldNfts: NFT[] | undefined,
newNfts: NFT[] | undefined
): NFT[] => {
if (!newNfts?.length) return oldNfts ?? [];

// Getting a map of id => NFT
const newNftsPerId: Record<string, NFT> = (newNfts as NFT[]).reduce(
(acc, curr) => setWith(acc, curr.id, curr, Object),
{}
);

// copying the argument to avoid mutating it
const nfts = oldNfts?.slice() ?? [];
for (let i = 0; i < nfts.length; i++) {
const nft = nfts[i];
// The NFTs are the same, do don't anything
if (!newNftsPerId[nft.id] || isEqual(nft, newNftsPerId[nft.id])) {
// NFT already in, deleting it from the newNfts to keep only the un-added ones at the end
delete newNftsPerId[nft.id];
continue;
}

// Use the new NFT instead (as a copy cause we're deleting the reference just after)
nfts[i] = Object.assign({}, newNftsPerId[nft.id]);
// Delete it from the newNfts to keep only the un-added ones at the end
delete newNftsPerId[nft.id];
}

// Adding the rest of newNfts that was not already in oldNfts
return nfts.concat(Object.values(newNftsPerId));
};

export const makeSync =
(
getAccountShape: GetAccountShape,
Expand Down
5 changes: 3 additions & 2 deletions src/csvExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { BigNumber } from "bignumber.js";
import type { Currency, Account, AccountLike, Operation } from "./types";
import { formatCurrencyUnit } from "./currencies";
import { getAccountCurrency, getMainAccount, flattenAccounts } from "./account";
import { flattenOperationWithInternals } from "./operation";
import { flattenOperationWithInternalsAndNfts } from "./operation";
import { calculate } from "./countervalues/logic";
import type { CounterValuesState } from "./countervalues/types";

Expand Down Expand Up @@ -143,7 +143,8 @@ const accountRows = (
): Array<string[]> =>
account.operations
.reduce(
(ops: Operation[], op) => ops.concat(flattenOperationWithInternals(op)),
(ops: Operation[], op) =>
ops.concat(flattenOperationWithInternalsAndNfts(op)),
[]
)
.map((operation) =>
Expand Down
11 changes: 11 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,17 @@ const envDefinitions = {
parser: stringParser,
desc: "mock the server response for the exchange KYC check, options are 'open', 'pending', 'closed' or 'approved'.",
},
NFT: {
def: false,
parser: boolParser,
desc: "synchronizing nfts",
},
NFT_ETH_METADATA_SERVICE: {
// FIXME LL-8001
def: "https://nft.staging.aws.ledger.fr/v1/ethereum/1/contracts/tokens/infos",
parser: stringParser,
desc: "service uri used to get the metadata of an nft",
},
OPERATION_ADDRESSES_LIMIT: {
def: 100,
parser: intParser,
Expand Down
8 changes: 8 additions & 0 deletions src/families/ethereum/signOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { getGasLimit, buildEthereumTx } from "./transaction";
import { apiForCurrency } from "../../api/Ethereum";
import { withDevice } from "../../hw/deviceAccess";
import { modes } from "./modules";
import { getEnv } from "../../env";
export const signOperation = ({
account,
deviceId,
Expand Down Expand Up @@ -61,6 +62,13 @@ export const signOperation = ({
"0x" + (tx.value.toString("hex") || "0")
);
const eth = new Eth(transport);
if (getEnv("NFT")) {
eth.setLoadConfig({
// FIXME drop this after LL-8001
nftExplorerBaseURL:
"https://nft.staging.aws.ledger.fr/v1/ethereum",
});
}
// FIXME this part is still required for compound to correctly display info on the device
const addrs =
(fillTransactionDataResult &&
Expand Down
Loading