Skip to content

Commit a24ee24

Browse files
authored
Fix metadata validation (#2)
* update package files * fix missing renderer dep * committing graphql schemas for testing * fix metadata validation * Add .circleci/config.yml * fix repo package.json
1 parent cc46dad commit a24ee24

File tree

8 files changed

+100
-36
lines changed

8 files changed

+100
-36
lines changed

README.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ This library consists of a data fetch class and associated React hooks to load N
88

99

1010
Install:
11-
```
11+
```bash
1212
yarn add @zoralabs/nft-hooks
1313
```
1414

1515
Then you can import and use the hooks in your react application:
1616

17-
```
17+
```ts
1818
import {useNFT, useNFTMetadata} from "@zoralabs/nft-hooks";
1919

2020
function MyNFT() {
@@ -39,6 +39,21 @@ function MyNFT() {
3939
| [useNFTMetadata](docs/useNFTMetadata.md) | Fetches NFT metadata from a URL |
4040
| [useNFTContent](docs/useNFTContent.md) | Fetches text content from server for rendering from content URL |
4141

42+
### Configuration:
43+
44+
To set the network configuration, wrap the hooks used with the `NFTFetchConfiguration` component.
45+
46+
```ts
47+
import {Networks, NFTFetchConfiguration} from '@zoralabs/nft-hooks';
48+
49+
function NFTGallery() {
50+
return (
51+
<NFTFetchConfiguration network={Networks.MAINNET}>
52+
<NFTList>
53+
</NFTFetchConfiguration>
54+
);
55+
}
56+
```
4257

4358
### Development:
4459

docs/useNFTMetadata.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ There is a chance this request could fail when the server does not allow cross-o
55
Requests are set with a 10 second timeout to allow showing the user an error message instead of an
66
indefinite loader.
77
Metadata information can be found in the [zora metadata schema repo](https://github.com/ourzora/media-metadata-schemas).
8+
Metadata validation can be ignored optionally by passing in a second argument `{allowInvalid: true}`, otherwise, an
9+
error will be thrown for invalid metadata.
810

911
Hook result type:
1012
```ts

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module.exports = {
22
testEnvironment: 'jsdom',
33
testRegex: "/tests/.*\\.(test|spec)?\\.(ts|tsx)$",
44
moduleFileExtensions: ['js', 'ts', 'tsx', 'json', 'gql', 'graphql'],
5+
modulePaths: ["<rootDir>/src"],
56
transform: {
67
"^.+\\.(j|t)sx?$": ["ts-jest"],
78
"\\.(gql|graphql)$": ["@jagi/jest-transform-graphql"],

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zoralabs/nft-hooks",
3-
"version": "0.0.1-pre",
3+
"version": "0.1.0-pre",
44
"description": "Generic Rendering Component for zNFTs",
55
"repository": "https://github.com/our-zora/nft-hooks",
66
"homepage": "https://github.com/our-zora/nft-hooks#README",
@@ -10,6 +10,9 @@
1010
"typings": "dist/src/index.d.ts",
1111
"source": "src/index.ts",
1212
"main": "dist/src/index.js",
13+
"files": [
14+
"dist"
15+
],
1316
"devDependencies": {
1417
"@graphql-codegen/cli": "^1.21.4",
1518
"@graphql-codegen/typescript": "^1.22.0",
@@ -52,7 +55,7 @@
5255
},
5356
"dependencies": {
5457
"@graphql-tools/mock": "^8.1.1",
55-
"@zoralabs/media-metadata-schemas": "^0.1.2",
58+
"@zoralabs/media-metadata-schemas": "^0.1.3",
5659
"big.js": "^6.1.0",
5760
"cross-fetch": "^3.1.4",
5861
"dataloader": "^2.0.0",

src/fetcher/MediaFetchAgent.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import DataLoader from 'dataloader';
22
import { GraphQLClient } from 'graphql-request';
3-
// TODO(iain): Fix validator
4-
// import {Validator} from "@zoralabs/media-metadata-schemas";
3+
import { Validator } from '@zoralabs/media-metadata-schemas';
54

65
import { RequestError } from './RequestError';
76
import {
@@ -104,10 +103,14 @@ export class MediaFetchAgent {
104103
*/
105104
async loadMetadata(url: string): Promise<MetadataResultType> {
106105
const metadata = await this.loaders.metadataLoader.load(url);
107-
// TODO(iain): fix imports
108-
// const validator = new Validator(metadata.version);
109-
// const valid = validator.validate(metadata);
110-
const valid = true;
106+
107+
let valid = false;
108+
try {
109+
const validator = new Validator(metadata.version);
110+
valid = validator.validate(metadata);
111+
} catch (e) {
112+
// skip validator errors
113+
}
111114
return { metadata, valid };
112115
}
113116

src/hooks/useNFTMetadata.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@ export type useNFTMetadataType = {
1717
* @param uri URI of metadata to fetch
1818
* @returns @type useNFTMetadataType
1919
*/
20-
export function useNFTMetadata(uri: string): useNFTMetadataType {
20+
export function useNFTMetadata(
21+
uri: string,
22+
{ allowInvalid } = { allowInvalid: false }
23+
): useNFTMetadataType {
2124
const [metadata, setMetadata] = useState<any>();
2225
const [error, setError] = useState<string | undefined>();
2326

2427
useCallbackFetch(uri, async (fetchAgent, uri) => {
2528
try {
2629
const { metadata, valid } = await fetchAgent.loadMetadata(uri);
27-
if (!valid) {
30+
if (!allowInvalid && !valid) {
2831
throw new RequestError('Metadata Invalid: Metadata could not be validated.');
2932
}
3033
setMetadata(metadata);

tests/useNFTMetadata.test.ts

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
1-
import { renderHook } from "@testing-library/react-hooks";
1+
import { renderHook } from '@testing-library/react-hooks';
22

3-
import fetchMock from "./setupFetchMock";
3+
import fetchMock from './setupFetchMock';
44

5-
import { useNFTMetadata } from "../src";
6-
import { defaultFetchAgent } from "../src/context/NFTFetchContext";
5+
import { useNFTMetadata } from '../src';
6+
import { defaultFetchAgent } from '../src/context/NFTFetchContext';
77

8-
describe("useNFTContent", () => {
8+
describe('useNFTContent', () => {
99
afterEach(() => {
1010
defaultFetchAgent.clearCache();
1111
fetchMock.reset();
1212
});
1313

14-
it("loads text content for NFT from server", async () => {
15-
fetchMock.get(
16-
"https://ipfs.io/ipfs/IPFS_SHA_EXAMPLE",
17-
{ name: "test", description: "test" },
18-
);
14+
it('loads text content for NFT from server', async () => {
15+
fetchMock.get('https://ipfs.io/ipfs/IPFS_SHA_EXAMPLE', {
16+
name: 'test',
17+
description: 'test',
18+
mimeType: 'text/plain',
19+
version: 'zora-20210101',
20+
});
1921

2022
const { waitFor, result } = renderHook(() =>
21-
useNFTMetadata("https://ipfs.io/ipfs/IPFS_SHA_EXAMPLE")
23+
useNFTMetadata('https://ipfs.io/ipfs/IPFS_SHA_EXAMPLE')
2224
);
2325

2426
await waitFor(() => result.current.loading === false);
@@ -27,37 +29,72 @@ describe("useNFTContent", () => {
2729
expect(result.current.loading).toBeFalsy();
2830
expect(result.current.metadata).toEqual({
2931
description: 'test',
32+
mimeType: 'text/plain',
33+
version: 'zora-20210101',
34+
name: 'test',
35+
});
36+
});
37+
38+
it('throws an error for invalid metadata', async () => {
39+
fetchMock.get('https://ipfs.io/ipfs/IPFS_SHA_EXAMPLE', { name: 'test' });
40+
41+
const { waitFor, result } = renderHook(() =>
42+
useNFTMetadata('https://ipfs.io/ipfs/IPFS_SHA_EXAMPLE')
43+
);
44+
45+
await waitFor(() => result.current.loading === false);
46+
47+
expect(result.current.error).toEqual(
48+
'RequestError: Metadata Invalid: Metadata could not be validated.'
49+
);
50+
expect(result.current.loading).toBeFalsy();
51+
expect(result.current.metadata).toBeUndefined();
52+
});
53+
54+
it('allows invalid metadata when validation exception disabled', async () => {
55+
fetchMock.get('https://ipfs.io/ipfs/IPFS_SHA_EXAMPLE', { name: 'test' });
56+
57+
const { waitFor, result } = renderHook(() =>
58+
useNFTMetadata('https://ipfs.io/ipfs/IPFS_SHA_EXAMPLE', { allowInvalid: true })
59+
);
60+
61+
await waitFor(() => result.current.loading === false);
62+
63+
expect(result.current.error).toBeUndefined();
64+
expect(result.current.loading).toBeFalsy();
65+
expect(result.current.metadata).toEqual({
3066
name: 'test',
3167
});
3268
});
33-
it("returns error when metadata does not exist", async () => {
34-
fetchMock.get("https://ipfs.io/ipfs/IPFS_SHA_EXAMPLE", "Not Found", {
69+
70+
it('returns error when metadata does not exist', async () => {
71+
fetchMock.get('https://ipfs.io/ipfs/IPFS_SHA_EXAMPLE', 'Not Found', {
3572
response: { status: 404 },
3673
});
3774

3875
const { waitFor, result } = renderHook(() =>
39-
useNFTMetadata("https://ipfs.io/ipfs/IPFS_SHA_EXAMPLE")
76+
useNFTMetadata('https://ipfs.io/ipfs/IPFS_SHA_EXAMPLE')
4077
);
4178

4279
await waitFor(() => result.current.loading === false);
4380

44-
expect(result.current.error).toEqual("RequestError: Request Status = 404");
81+
expect(result.current.error).toEqual('RequestError: Request Status = 404');
4582
expect(result.current.loading).toBeFalsy();
4683
expect(result.current.metadata).toBeUndefined();
4784
});
48-
it("throws exception for invalid json", async () => {
49-
fetchMock.get("https://ipfs.io/ipfs/IPFS_SHA_EXAMPLE", "INVALID JSON", {
50-
response: { headers: { "content-type": "application/json" } },
85+
it('throws exception for invalid json', async () => {
86+
fetchMock.get('https://ipfs.io/ipfs/IPFS_SHA_EXAMPLE', 'INVALID JSON', {
87+
response: { headers: { 'content-type': 'application/json' } },
5188
});
5289

5390
const { waitFor, result } = renderHook(() =>
54-
useNFTMetadata("https://ipfs.io/ipfs/IPFS_SHA_EXAMPLE")
91+
useNFTMetadata('https://ipfs.io/ipfs/IPFS_SHA_EXAMPLE')
5592
);
5693

5794
await waitFor(() => result.current.loading === false);
5895

5996
expect(result.current.error).toEqual(
60-
"RequestError: Cannot read JSON metadata from IPFS"
97+
'RequestError: Cannot read JSON metadata from IPFS'
6198
);
6299
expect(result.current.loading).toBeFalsy();
63100
expect(result.current.metadata).toBeUndefined();

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,10 +1595,10 @@
15951595
dependencies:
15961596
tslib "^2.1.0"
15971597

1598-
"@zoralabs/media-metadata-schemas@^0.1.2":
1599-
version "0.1.2"
1600-
resolved "https://registry.yarnpkg.com/@zoralabs/media-metadata-schemas/-/media-metadata-schemas-0.1.2.tgz#b6f3532e7bca95197fa8a7ca136ad70dbdce1d2d"
1601-
integrity sha512-QcvPNb4RkdU9xKfOxnmKRKKVI4Z5oammiz2wYHmqzOvkIgHvm7tjV/MQcTelaRJfjksYx8k6bqsmbSbKIMUJTQ==
1598+
"@zoralabs/media-metadata-schemas@^0.1.3":
1599+
version "0.1.3"
1600+
resolved "https://registry.yarnpkg.com/@zoralabs/media-metadata-schemas/-/media-metadata-schemas-0.1.3.tgz#082091b70ad1bd5e3b235afed391eefb0de83770"
1601+
integrity sha512-nyB/igVrifY2/AQgeq5eg081Q17VTFh7YP5Axda5v6CpNbSYSjAfgSFGSPG7Ho8t+T24wWBane8ainJGPP25mA==
16021602
dependencies:
16031603
"@types/jsonschema" "^1.1.1"
16041604
jsonschema "^1.4.0"

0 commit comments

Comments
 (0)