Skip to content

feat(ledger-browser): handle ERC721 token metadata #3894

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ CREATE TABLE IF NOT EXISTS ethereum.token_erc721
token_address text COLLATE pg_catalog."default" NOT NULL,
uri text COLLATE pg_catalog."default" NOT NULL,
token_id numeric NOT NULL,
nft_name text COLLATE pg_catalog."default" NOT NULL,
nft_description text COLLATE pg_catalog."default" NOT NULL,
nft_image text COLLATE pg_catalog."default" NOT NULL,
last_owner_change timestamp without time zone NOT NULL DEFAULT now(),
CONSTRAINT token_erc721_pkey1 PRIMARY KEY (id),
CONSTRAINT token_erc721_contract_tokens_unique UNIQUE (token_address, token_id),
Expand Down Expand Up @@ -437,6 +440,9 @@ BEGIN
token_transfer.token_address,
'',
token_transfer.token_id,
'',
'',
'',
token_transfer.created_at
);
ELSE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ export interface Database {
uri: string;
token_id: number;
id: string;
nft_name: string;
nft_description: string;
nft_image: string;
last_owner_change: string;
};
Insert: {
Expand All @@ -70,6 +73,9 @@ export interface Database {
uri: string;
token_id: number;
id?: string;
nft_name?: string;
nft_description?: string;
nft_image?: string;
last_owner_change?: string;
};
Update: {
Expand All @@ -78,6 +84,9 @@ export interface Database {
uri?: string;
token_id?: number;
id?: string;
nft_name?: string;
nft_description?: string;
nft_image?: string;
last_owner_change?: string;
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,13 +298,21 @@ export default class PostgresDatabaseClient {

this.log.debug("Insert ERC721 token if not present yet:", token);
const insertResponse = await this.client.query(
`INSERT INTO ethereum.token_erc721("account_address", "token_address", "uri", "token_id")
VALUES ($1, $2, $3, $4)
`INSERT INTO ethereum.token_erc721("account_address", "token_address", "uri", "token_id", "nft_name", "nft_description", "nft_image")
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT ON CONSTRAINT token_erc721_contract_tokens_unique
DO
UPDATE SET account_address = EXCLUDED.account_address;
`,
[token.account_address, token.token_address, token.uri, token.token_id],
[
token.account_address,
token.token_address,
token.uri,
token.token_id,
token.nft_name,
token.nft_description,
token.nft_image,
],
);
this.log.debug(
`Inserted ${insertResponse.rowCount} rows into table token_erc721`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { v4 as uuidv4 } from "uuid";
import type { TransactionInfo, TransactionReceipt } from "web3";
import type { Express } from "express";
import type { Subscription } from "rxjs";
import axios from "axios";

/**
* Constructor parameter for Ethereum persistence plugin.
Expand Down Expand Up @@ -238,7 +239,6 @@ export class PluginPersistenceEthereum
);
return false;
}

if (parseInt(ownerAddress, 16) === 0) {
this.log.debug(`Found token ID ${tokenId} without the owner - stop.`);
return false;
Expand All @@ -249,17 +249,50 @@ export class PluginPersistenceEthereum
const checkedOwnerAddress = normalizeAddress(ownerAddress);
const tokenUri = await tokenClient.tokenURI(tokenId);

// Fetch token metadata using axios
const metadataResponse = await axios.get(tokenUri);

// Get and check token metadata
const metadata = metadataResponse.data;
if (typeof metadata.properties.name.description !== "string") {
throw new Error(
`Invalid type of 'name' from asset metadata for token ID: ${tokenId}`,
);
}
if (typeof metadata.properties.description.description !== "string") {
throw new Error(
`Invalid type of 'description' from asset metadata for token ID: ${tokenId}`,
);
}
if (typeof metadata.properties.image.description !== "string") {
throw new Error(
`Invalid type of 'image' from asset metadata for token ID: ${tokenId}`,
);
}

// Upsert token information in DB
await this.dbClient.upsertTokenERC721({
account_address: checkedOwnerAddress,
token_address: normalizeAddress(tokenClient.address),
uri: tokenUri,
token_id: tokenId,
nft_name: metadata.properties.name.description,
nft_description: metadata.properties.description.description,
nft_image: metadata.properties.image.description,
});
} catch (err) {
this.log.error(`Could not store issued ERC721 token: ID ${tokenId}`, err);
// We return true since failure here means that there might be more tokens to synchronize
if (err instanceof SyntaxError) {
this.log.error("Error parsing JSON:", err.message);
} else if (axios.isAxiosError(err)) {
this.log.error(`Error fetching metadata using axios: ${err.response}`);
} else {
this.log.error(
`Could not store issued ERC721 token: ID ${tokenId}`,
err,
);
}
}

// We return true since failure here means that there might be more tokens to synchronize
return true;
}

Expand Down Expand Up @@ -847,7 +880,6 @@ export class PluginPersistenceEthereum
);

const tokenClient = new TokenClientERC721(this.apiClient, checkedAddress);

try {
await this.dbClient.insertTokenMetadataERC721({
address: checkedAddress,
Expand All @@ -860,7 +892,6 @@ export class PluginPersistenceEthereum
getRuntimeErrorCause(err),
);
}

await this.refreshMonitoredTokens();
}

Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import { pruneDockerAllIfGithubAction } from "@hyperledger/cactus-test-tooling";
import "jest-extended";
import http from "http";
import { AddressInfo } from "net";
import express from "express";
import express, { Request, Response } from "express";
import bodyParser from "body-parser";
import { Server as SocketIoServer } from "socket.io";
import { v4 as uuidV4 } from "uuid";
Expand Down Expand Up @@ -94,6 +94,11 @@ describe("Ethereum persistence plugin tests", () => {
});
let connector: PluginLedgerConnectorEthereum;

// setting for stimulating about gathering ERC721 metadata from TokenUri()
const erc721MetadataApp = express();
const erc721MetadataPort = 3000;
let erc721MetadataServer: http.Server;

//////////////////////////////////
// Helper Functions
//////////////////////////////////
Expand Down Expand Up @@ -296,6 +301,35 @@ describe("Ethereum persistence plugin tests", () => {
const apiConfig = new Configuration({ basePath: apiHost });
const apiClient = new EthereumApiClient(apiConfig);

// activate a server for for stimulating about gathering ERC721 metadata from TokenUri()
erc721MetadataApp.get("/metadata/:id", (req: Request, res: Response) => {
// get id parameter from route
const { id } = req.params;
const metadata = {
title: "Asset Metadata",
type: "object",
properties: {
name: {
type: "string",
description: `NFT${id}`,
},
description: {
type: "string",
description: `NFT${id} description`,
},
image: {
type: "string",
description: `https://example.com/${id}`,
},
},
};
res.json(metadata);
});
erc721MetadataServer = http.createServer(erc721MetadataApp);
erc721MetadataServer.listen(erc721MetadataPort, "127.0.0.1", () => {
log.info(`ERC721 TokenUri server is running at http://localhost:${port}`);
});

// Create Ethereum persistence plugin
instanceId = "functional-test";
DatabaseClientMock.mockClear();
Expand Down Expand Up @@ -334,6 +368,11 @@ describe("Ethereum persistence plugin tests", () => {
await ledger.destroy();
}

if (erc721MetadataServer) {
log.info("Stop ERC721 TokenUri server...");
await erc721MetadataServer.close();
}

log.info("Prune Docker...");
await pruneDockerAllIfGithubAction({ logLevel: testLogLevel });
}, setupTimeout);
Expand Down Expand Up @@ -555,6 +594,9 @@ describe("Ethereum persistence plugin tests", () => {
expect([1, 2, 3]).toInclude(token.token_id);
expect(token.account_address).toBeTruthy();
expect(token.uri).toBeTruthy();
expect(token.nft_name).toBeTruthy();
expect(token.nft_description).toBeTruthy();
expect(token.nft_image).toBeTruthy();
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,9 @@ describe("Ethereum persistence PostgreSQL PostgresDatabaseClient tests", () => {
token_address: contractAddress,
uri: "test.uri",
token_id: 1,
nft_name: "NFT",
nft_description: "NFT description",
nft_image: "test.uri",
};
await dbClient.upsertTokenERC721(issuedToken);

Expand Down Expand Up @@ -620,6 +623,9 @@ describe("Ethereum persistence PostgreSQL PostgresDatabaseClient tests", () => {
token_address: contractAddress,
uri: issuedTokenUri,
token_id: 1,
nft_name: "NFT",
nft_description: "NFT description",
nft_image: issuedTokenUri,
});

// Transfer our token
Expand Down Expand Up @@ -693,6 +699,9 @@ describe("Ethereum persistence PostgreSQL PostgresDatabaseClient tests", () => {
token_address: contractAddress,
uri: issuedTokenUri,
token_id: 1,
nft_name: "NFT",
nft_description: "NFT description",
nft_image: issuedTokenUri,
});

// Transfer our token
Expand Down Expand Up @@ -737,6 +746,9 @@ describe("Ethereum persistence PostgreSQL PostgresDatabaseClient tests", () => {
token_address: contractAddress,
uri: issuedTokenUri,
token_id: "1",
nft_name: "NFT",
nft_description: "NFT description",
nft_image: issuedTokenUri,
});
});
});
Loading