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

[LIVE-1174] - Feature: Upgrade NFT Architecture #1805

Merged
merged 20 commits into from
Apr 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9798040
Fix wrong NFTResource typing
lambertkevin Mar 8, 2022
1e03679
Update NFT types to ProtoNFT
lambertkevin Mar 8, 2022
cb27b77
Update NFT Id to contain currency
lambertkevin Mar 8, 2022
581d671
Update NFT Helpers for new model
lambertkevin Mar 8, 2022
53493c5
Update Eth API metadata call to include chainId
lambertkevin Mar 8, 2022
221932c
Move nft metadata resolution to bridge
lambertkevin Mar 12, 2022
6df51c9
Update NftMetadaProvider logic to use bridge
lambertkevin Mar 12, 2022
fdadd98
Update prepare tx of ERC721/1155 w/ new model
lambertkevin Mar 12, 2022
53f50c3
Update CLI for new NFT model
lambertkevin Mar 12, 2022
011a52c
Add getNftCapabilities to nft support
lambertkevin Mar 23, 2022
b0ecf5d
Naming + fixing ERC1155 quantity potentially falsy
lambertkevin Mar 24, 2022
1001c2b
Make CLI use the LLC branch hash
lambertkevin Mar 24, 2022
a3719f7
Add type to nftMetadataResolver param + use of sync metadata
lambertkevin Mar 24, 2022
486a2e7
Remove useless import
lambertkevin Mar 28, 2022
6ec2cab
Remove useless comment in CLI formatters
lambertkevin Mar 28, 2022
23323bf
Add comment to nftsByCollection helper
lambertkevin Mar 28, 2022
a3b7104
Add comments + types to NFT metadata call batchers
lambertkevin Mar 28, 2022
76a8f10
Fix sync metadata resolution for Eth family
lambertkevin Mar 28, 2022
1effe19
Add return type to nftMetadataResolver + decodeNftId
lambertkevin Mar 30, 2022
44fd956
Merge branch 'release/v22.0.0' into feat/upgrade-nft-architecture
lambertkevin Apr 5, 2022
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
2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"@ledgerhq/hw-transport-mocker": "6.24.1",
"@ledgerhq/hw-transport-node-hid": "6.24.1",
"@ledgerhq/hw-transport-node-speculos": "6.24.1",
"@ledgerhq/live-common": "https://github.com/LedgerHQ/ledger-live-common.git#082c946830a332f764f3c95c0eb9c0572b493825",
"@ledgerhq/live-common": "https://github.com/LedgerHQ/ledger-live-common.git#31177aae3a559e17f8452ed8dc6e010cd1d6f41e",
This conversation was marked as resolved.
Show resolved Hide resolved
"@ledgerhq/logs": "6.10.0",
"@walletconnect/client": "^1.7.1",
"asciichart": "^1.5.25",
Expand Down
26 changes: 18 additions & 8 deletions cli/src/commands/sync.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { map, switchMap } from "rxjs/operators";
import { accountFormatters } from "@ledgerhq/live-common/lib/account";
import { metadataCallBatcher } from "@ledgerhq/live-common/lib/nft";
import {
accountFormatters,
decodeAccountId,
} from "@ledgerhq/live-common/lib/account";
import { getCryptoCurrencyById } from "@ledgerhq/live-common/lib/currencies";
import { getCurrencyBridge } from "@ledgerhq/live-common/lib/bridge";
import { scan, scanCommonOpts } from "../scan";
import type { ScanCommonOpts } from "../scan";
export default {
Expand All @@ -21,23 +25,29 @@ export default {
}
) =>
scan(opts).pipe(
switchMap(async (account) =>
account.nfts?.length
switchMap(async (account) => {
const { currencyId } = decodeAccountId(account.id);
const currency = getCryptoCurrencyById(currencyId);
const currencyBridge = getCurrencyBridge(currency);
const { nftMetadataResolver } = currencyBridge;

return account.nfts?.length && nftMetadataResolver
? {
...account,
nfts: await Promise.all(
account.nfts.map(async (nft) => {
const { result: metadata } = await metadataCallBatcher.load({
contract: nft.collection.contract,
const { result: metadata } = await nftMetadataResolver({
contract: nft.contract,
tokenId: nft.tokenId,
currencyId: nft.currencyId,
});

return { ...nft, metadata };
})
).catch(() => account.nfts),
}
: account
),
: account;
}),
map((account) =>
(accountFormatters[opts.format] || accountFormatters.default)(account)
)
Expand Down
20 changes: 8 additions & 12 deletions cli/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1087,18 +1087,14 @@
dependencies:
bignumber.js "^9.0.1"
json-rpc-2.0 "^0.2.16"

"@ledgerhq/live-common@https://github.com/LedgerHQ/ledger-live-common.git#082c946830a332f764f3c95c0eb9c0572b493825":
version "21.35.0"
resolved "https://github.com/LedgerHQ/ledger-live-common.git#082c946830a332f764f3c95c0eb9c0572b493825"
dependencies:
"@celo/contractkit" "^1.5.2"
"@celo/wallet-base" "^1.5.2"
"@celo/wallet-ledger" "^1.5.2"
"@cosmjs/crypto" "^0.26.5"
"@cosmjs/ledger-amino" "^0.26.5"
"@cosmjs/proto-signing" "^0.26.5"
"@cosmjs/stargate" "^0.26.5"

"@ledgerhq/live-common@https://github.com/LedgerHQ/ledger-live-common.git#31177aae3a559e17f8452ed8dc6e010cd1d6f41e":
version "21.32.4"
resolved "https://github.com/LedgerHQ/ledger-live-common.git#31177aae3a559e17f8452ed8dc6e010cd1d6f41e"
dependencies:
"@celo/contractkit" "^1.5.1"
"@celo/wallet-base" "^1.5.1"
"@celo/wallet-ledger" "^1.5.1"
"@crypto-com/chain-jslib" "0.0.19"
"@ethereumjs/common" "^2.6.2"
"@ethereumjs/tx" "^3.5.0"
Expand Down
14 changes: 6 additions & 8 deletions src/account/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
getAccountName,
getAccountUnit,
} from ".";
import type { Account, Operation, Unit } from "../types";
import type { Account, Operation, ProtoNFT, Unit } from "../types";
import { getOperationAmountNumberWithInternals } from "../operation";
import { formatCurrencyUnit } from "../currencies";
import { getOperationAmountNumber } from "../operation";
Expand Down Expand Up @@ -138,13 +138,11 @@ const cliFormat = (account, level?: string) => {
const NFTCollections = nftsByCollections(nfts);

str += "\n";
str += `NFT Collections (${NFTCollections.length}) `;
str += `NFT Collections (${Object.keys(NFTCollections).length}) `;
str += "\n";

str += NFTCollections.map(
// nfts are set to any because there not just NFT, we added a metadata prop on the fly
// in the first step of the Rxjs flow to avoid having some async code here
({ contract, nfts }: { contract: string; nfts: any[] }) => {
str += Object.entries(NFTCollections)
.map(([contract, nfts]: [string, ProtoNFT[]]) => {
const tokenName = nfts?.[0]?.metadata?.tokenName;
const { bold, magenta, cyan, reverse } = styling;

Expand All @@ -162,8 +160,8 @@ const cliFormat = (account, level?: string) => {
)
.join()
);
}
).join("\n");
})
.join("\n");
}

if (level === "basic") return str;
Expand Down
34 changes: 28 additions & 6 deletions src/account/serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import type {
OperationRaw,
SubAccount,
SubAccountRaw,
NFT,
NFTRaw,
ProtoNFT,
ProtoNFTRaw,
} from "../types";
import type { TronResources, TronResourcesRaw } from "../families/tron/types";
import {
Expand Down Expand Up @@ -949,20 +949,42 @@ export function toAccountRaw({
return res;
}

export function toNFTRaw({ id, tokenId, amount, collection }: NFT): NFTRaw {
export function toNFTRaw({
id,
tokenId,
amount,
contract,
standard,
currencyId,
metadata,
}: ProtoNFT): ProtoNFTRaw {
return {
id,
tokenId,
amount: amount.toFixed(),
collection,
contract,
standard,
currencyId,
metadata,
};
}

export function fromNFTRaw({ id, tokenId, amount, collection }: NFTRaw): NFT {
export function fromNFTRaw({
id,
tokenId,
amount,
contract,
standard,
currencyId,
metadata,
}: ProtoNFTRaw): ProtoNFT {
return {
id,
tokenId,
amount: new BigNumber(amount),
collection,
contract,
standard,
currencyId,
metadata,
};
}
13 changes: 8 additions & 5 deletions src/api/Ethereum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,10 @@ export type API = {
getAccountNonce: (address: string) => Promise<number>;
broadcastTransaction: (signedTransaction: string) => Promise<string>;
getERC20Balances: (input: ERC20BalancesInput) => Promise<ERC20BalanceOutput>;
getNFTMetadata: (input: NFTMetadataInput) => Promise<NFTMetadataResponse[]>;
getNFTMetadata: (
input: NFTMetadataInput,
chainId: string
) => Promise<NFTMetadataResponse[]>;
getAccountBalance: (address: string) => Promise<BigNumber>;
roughlyEstimateGasLimit: (address: string) => Promise<BigNumber>;
getERC20ApprovalsPerContract: (
Expand Down Expand Up @@ -205,12 +208,12 @@ export const apiForCurrency = (currency: CryptoCurrency): API => {
return data;
},

async getNFTMetadata(input) {
async getNFTMetadata(input, chainId) {
const { data }: { data: NFTMetadataResponse[] } = await network({
method: "POST",
url:
getEnv("NFT_ETH_METADATA_SERVICE") +
"/v1/ethereum/1/contracts/tokens/infos",
url: `${getEnv(
"NFT_ETH_METADATA_SERVICE"
)}/v1/ethereum/${chainId}/contracts/tokens/infos`,
data: input,
});

Expand Down
9 changes: 6 additions & 3 deletions src/bridge/jsHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import type {
SyncConfig,
CryptoCurrency,
DerivationMode,
NFT,
ProtoNFT,
} from "../types";
import type { CurrencyBridge, AccountBridge } from "../types/bridge";
import getAddress from "../hw/getAddress";
Expand Down Expand Up @@ -137,9 +137,12 @@ Operation[] {
return all;
}

export const mergeNfts = (oldNfts: NFT[], newNfts: NFT[]): NFT[] => {
export const mergeNfts = (
oldNfts: ProtoNFT[],
newNfts: ProtoNFT[]
): ProtoNFT[] => {
// Getting a map of id => NFT
const newNftsPerId: Record<string, NFT> = {};
const newNftsPerId: Record<string, ProtoNFT> = {};
newNfts.forEach((n) => {
newNftsPerId[n.id] = n;
});
Expand Down
2 changes: 2 additions & 0 deletions src/families/ethereum/bridge/js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { signOperation } from "../signOperation";
import { modes } from "../modules";
import postSyncPatch from "../postSyncPatch";
import { inferDynamicRange } from "../../../range";
import nftMetadataResolver from "../nftMetadataResolver";

const receive = makeAccountBridgeReceive();

Expand Down Expand Up @@ -210,6 +211,7 @@ const currencyBridge: CurrencyBridge = {
preload,
hydrate,
scanAccounts,
nftMetadataResolver,
};
const accountBridge: AccountBridge<Transaction> = {
createTransaction,
Expand Down
18 changes: 9 additions & 9 deletions src/families/ethereum/modules/erc1155.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import type { ModeModule, Transaction } from "../types";
import type { Account } from "../../../types";
import { prepareTransaction } from "./erc721";

const notOwnedNft = createCustomErrorClass("NotOwnedNft");
const notEnoughNftOwned = createCustomErrorClass("NotEnoughNftOwned");
const notTokenIdsProvided = createCustomErrorClass("NotTokenIdsProvided");
const quantityNeedsToBePositive = createCustomErrorClass(
const NotOwnedNft = createCustomErrorClass("NotOwnedNft");
const NotEnoughNftOwned = createCustomErrorClass("NotEnoughNftOwned");
const NotTokenIdsProvided = createCustomErrorClass("NotTokenIdsProvided");
const QuantityNeedsToBePositive = createCustomErrorClass(
"QuantityNeedsToBePositive"
);

Expand Down Expand Up @@ -47,8 +47,8 @@ const erc1155Transfer: ModeModule = {
}

t.quantities?.forEach((quantity) => {
if (quantity.isLessThan(1)) {
result.errors.amount = new quantityNeedsToBePositive();
if (!quantity || quantity.isLessThan(1)) {
result.errors.amount = new QuantityNeedsToBePositive();
}
});

Expand All @@ -62,15 +62,15 @@ const erc1155Transfer: ModeModule = {
const transferQuantity = Number(t.quantities?.[index]);

if (!nft) {
return new notOwnedNft();
return new NotOwnedNft();
}

if (transferQuantity && !nft.amount.gte(transferQuantity)) {
return new notEnoughNftOwned();
return new NotEnoughNftOwned();
}

return true;
}, true as true | Error) || new notTokenIdsProvided();
}, true as true | Error) || new NotTokenIdsProvided();

if (!enoughTokensOwned || enoughTokensOwned instanceof Error) {
result.errors.amount = enoughTokensOwned;
Expand Down
23 changes: 12 additions & 11 deletions src/families/ethereum/modules/erc721.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { ModeModule, Transaction } from "../types";
import type { Account } from "../../../types";
import { apiForCurrency } from "../../../api/Ethereum";

const notOwnedNft = createCustomErrorClass("NotOwnedNft");
const NotOwnedNft = createCustomErrorClass("NotOwnedNft");

export type Modes = "erc721.transfer";

Expand All @@ -23,12 +23,15 @@ export async function prepareTransaction(
const { collection, collectionName, tokenIds } = transaction;
if (collection && tokenIds && typeof collectionName === "undefined") {
const api = apiForCurrency(account.currency);
const [{ status, result }] = await api.getNFTMetadata([
{
contract: collection,
tokenId: tokenIds[0],
},
]);
const [{ status, result }] = await api.getNFTMetadata(
[
{
contract: collection,
tokenId: tokenIds[0],
},
],
account.currency?.ethereumLikeInfo?.chainId?.toString() || "1"
);
let collectionName = ""; // default value fallback if issue
if (status === 200) {
collectionName = result?.tokenName || "";
Expand Down Expand Up @@ -66,12 +69,10 @@ const erc721Transfer: ModeModule = {

if (
!a.nfts?.find?.(
(n) =>
n.tokenId === t.tokenIds?.[0] &&
n.collection.contract === t.collection
(n) => n.tokenId === t.tokenIds?.[0] && n.contract === t.collection
)
) {
result.errors.amount = new notOwnedNft();
result.errors.amount = new NotOwnedNft();
}
}
},
Expand Down
17 changes: 10 additions & 7 deletions src/families/ethereum/nft.merging.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import "../../__tests__/test-helpers/setup";
import BigNumber from "bignumber.js";
import { toNFTRaw } from "../../account";
import type { NFT } from "../../types";
import type { ProtoNFT } from "../../types";
import { mergeNfts } from "../../bridge/jsHelpers";
import { encodeNftId } from "../../nft";

describe("nft merging", () => {
const makeNFT = (tokenId: string, contract: string, amount: number): NFT => ({
id: encodeNftId("test", contract, tokenId),
const makeNFT = (
tokenId: string,
contract: string,
amount: number
): ProtoNFT => ({
id: encodeNftId("test", contract, tokenId, "ethereum"),
tokenId,
amount: new BigNumber(amount),
collection: {
contract,
standard: "erc721",
},
contract,
standard: "ERC721",
currencyId: "ethereum",
});
const oldNfts = [
makeNFT("1", "contract1", 10),
Expand Down
Loading