Skip to content
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"canvas": "^2.9.0",
"compression": "^1.7.4",
"cors": "^2.8.5",
"data-urls": "^3.0.2",
"dotenv": "^16.0.0",
"eslint": "^6.7.2",
"ethers": "^5.5.4",
Expand Down
12 changes: 9 additions & 3 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ router.get('/clear/:type/:id', async (req, res) => {
}
});

router.get('/:type/:id', async (req, res) => {
const { type, id } = req.params;
router.get('/:type/:id/:subId?', async (req, res) => {
const { type, id, subId } = req.params;

const { address, network, w, h, fallback } = await parseQuery(id, type, req.query);
const key1 = getCacheKey({
type,
network,
address,
subId,
w: constants.max,
h: constants.max,
fallback
Expand All @@ -37,6 +38,7 @@ router.get('/:type/:id', async (req, res) => {
let currentResolvers = constants.resolvers.avatar;
if (type === 'token') currentResolvers = constants.resolvers.token;
if (type === 'space') currentResolvers = constants.resolvers.space;
if (type === 'nft') currentResolvers = constants.resolvers.nft;
currentResolvers = [fallback, ...currentResolvers];

// Check resized cache
Expand All @@ -55,7 +57,11 @@ router.get('/:type/:id', async (req, res) => {
// console.log('Got base cache');
} else {
console.log('No cache for', key1, base);
const p = currentResolvers.map(r => resolvers[r](address, network));

const extraArgs: any[] = [];
if (type === 'nft') extraArgs.push(subId);

const p = currentResolvers.map(r => resolvers[r](address, ...extraArgs, network));
const files = await Promise.all(p);
files.forEach(file => {
if (file) file1 = file;
Expand Down
3 changes: 2 additions & 1 deletion src/constants.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"resolvers": {
"avatar": ["selfid", "lens", "ens", "snapshot"],
"token": ["trustwallet", "zapper"],
"space": ["space"]
"space": ["space"],
"nft": ["nft"]
}
}
4 changes: 3 additions & 1 deletion src/resolvers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import space from './space';
import selfid from './selfid';
import lens from './lens';
import zapper from './zapper';
import nft from './nft';

export default {
blockie,
Expand All @@ -17,5 +18,6 @@ export default {
space,
selfid,
lens,
zapper
zapper,
nft
};
91 changes: 91 additions & 0 deletions src/resolvers/nft.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import axios from 'axios';
import parseDataURL from 'data-urls';
import { getAddress } from '@ethersproject/address';
import { Contract } from '@ethersproject/contracts';
import { StaticJsonRpcProvider } from '@ethersproject/providers';
import { getUrl, resize } from '../utils';
import { max } from '../constants.json';

const provider = new StaticJsonRpcProvider('https://brovider.xyz/1');

const abis = {
erc721: ['function tokenURI(uint256 tokenId) view returns (string)'],
erc1155: ['function uri(uint256 id) view returns (string)']
};

async function resolveErc721(address: string, tokenId: string) {
const contract = new Contract(getAddress(address), abis.erc721, provider);
const data = await contract.tokenURI(tokenId);

const parsedMetadata = parseDataURL(data);

let metadata;
if (parsedMetadata && parsedMetadata.mimeType.toString() === 'application/json') {
metadata = JSON.parse(Buffer.from(parsedMetadata.body).toString('utf-8'));
} else {
const url = getUrl(data);
metadata = (await axios.get(url)).data;
}

if (!metadata.image) {
throw new Error('Image not found');
}

const parsedImage = parseDataURL(metadata.image);
if (parsedImage) {
return Buffer.from(parsedImage.body);
}

const url = getUrl(metadata.image);
return (await axios({ url, responseType: 'arraybuffer' })).data as Buffer;
}

async function resolveErc1155(address: string, tokenId: string) {
const contract = new Contract(getAddress(address), abis.erc1155, provider);
const data = await contract.uri(tokenId);

const replacementId =
tokenId.length === 64
? tokenId
: BigInt(tokenId)
.toString(16)
.padStart(64, '0');

const parsedMetadata = parseDataURL(data);

let metadataString;
if (parsedMetadata && parsedMetadata.mimeType.toString() === 'application/json') {
metadataString = Buffer.from(parsedMetadata.body).toString('utf-8');
} else {
const uniqueData = data.replaceAll('{id}', replacementId);
const url = getUrl(uniqueData);
metadataString = JSON.stringify((await axios.get(url)).data);
}

const metadata = JSON.parse(metadataString.replaceAll('{id}', replacementId));

if (!metadata.image) {
throw new Error('Image not found');
}

const parsedImage = parseDataURL(metadata.image);
if (parsedImage) {
return Buffer.from(parsedImage.body);
}

const url = getUrl(metadata.image);
return (await axios({ url, responseType: 'arraybuffer' })).data as Buffer;
}

export default async function resolve(address: string, tokenId: string) {
try {
const input = await Promise.any([
resolveErc721(address, tokenId),
resolveErc1155(address, tokenId)
]);

return await resize(input, max, max, 'contain');
} catch (e) {
return false;
}
}
13 changes: 10 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ export function sha256(str) {
.digest('hex');
}

export async function resize(input, w, h) {
export async function resize(input, w, h, fit = 'cover') {
return sharp(input)
.resize(w, h)
.resize(w, h, {
fit
})
.webp({ lossless: true })
.toBuffer();
}
Expand Down Expand Up @@ -82,19 +84,24 @@ export function getCacheKey({
type,
network,
address,
subId,
w,
h,
fallback
}: {
type: string;
network: string;
address: string;
subId?: string;
w: number;
h: number;
fallback: string;
}) {
if (fallback === 'blockie') return sha256(JSON.stringify({ type, network, address, w, h }));
return sha256(JSON.stringify({ type, network, address, w, h, fallback }));
const blob: Record<string, any> = { type, network, address, w, h, fallback };
if (type === 'nft') blob.tokenId = subId;

return sha256(JSON.stringify(blob));
}

export function setHeader(res) {
Expand Down
40 changes: 40 additions & 0 deletions test/unit/resolvers/nft.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import resolvers from '../../../src/resolvers';

describe('resolvers', () => {
describe('nft', () => {
describe('erc721', () => {
it('should resolve on-chain metadata', async () => {
const result = await resolvers.nft('0x29b4ea6b1164c7cd8a3a0a1dc4ad88d1e0589124', '6364');

expect(result).toBeInstanceOf(Buffer);
expect((result as Buffer).length).toBeGreaterThan(500);
}, 15000);

it('should resolve IPFS metadata', async () => {
const result = await resolvers.nft('0x7f8162f4ffe3db46cd3b0626dab699506c0ff63a', '6386');

expect(result).toBeInstanceOf(Buffer);
expect((result as Buffer).length).toBeGreaterThan(500);
}, 15000);
});

describe('erc1155', () => {
it('should resolve IPFS metadata', async () => {
const result = await resolvers.nft('0x3b1417c1f204607deda4767929497256e4ff540c', '1');

expect(result).toBeInstanceOf(Buffer);
expect((result as Buffer).length).toBeGreaterThan(500);
}, 15000);

it('should resolve OpeanSea metadata', async () => {
const result = await resolvers.nft(
'0x495f947276749Ce646f68AC8c248420045cb7b5e',
'71349417930267003648058267821921373972951788320258492784107927381794011217921'
);

expect(result).toBeInstanceOf(Buffer);
expect((result as Buffer).length).toBeGreaterThan(500);
}, 15000);
});
});
});
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3403,7 +3403,7 @@ cssstyle@^2.3.0:
dependencies:
cssom "~0.3.6"

data-urls@^3.0.1:
data-urls@^3.0.1, data-urls@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143"
integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==
Expand Down