diff --git a/package.json b/package.json index ab97db6..514dd51 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "@adraffy/ens-normalize": "^1.10.0", - "@ensdomains/ens-avatar": "^1.0.0-alpha.2.ethers.6", + "@ensdomains/ens-avatar": "^1.0.0-alpha.3.ethers.6", "@ensdomains/ensjs": "^3.7.0", "@types/lodash": "^4.14.170", "btoa": "^1.2.1", diff --git a/src/controller/avatarImage.ts b/src/controller/avatarImage.ts index 17d0c99..f4b5565 100644 --- a/src/controller/avatarImage.ts +++ b/src/controller/avatarImage.ts @@ -11,6 +11,7 @@ import { import { RESPONSE_TIMEOUT } from '../config'; import { getAvatarImage } from '../service/avatar'; import getNetwork, { NetworkName } from '../service/network'; +import createDocumentfromTemplate from '../template-document'; export async function avatarImage(req: Request, res: Response) { // #swagger.description = 'ENS avatar image' @@ -29,6 +30,15 @@ export async function avatarImage(req: Request, res: Response) { description: 'Image file' } */ if (!res.headersSent) { + if (req.header('sec-fetch-dest') === 'document') { + const documentTemplate = createDocumentfromTemplate({ buffer, metadata: { name, network: networkName }, mimeType }); + res + .writeHead(200, { + 'Content-Type': 'text/html', + }) + .end(documentTemplate); + return; + } res .writeHead(200, { 'Content-Type': mimeType, diff --git a/src/controller/ensImage.ts b/src/controller/ensImage.ts index 8de06b9..644f3fa 100644 --- a/src/controller/ensImage.ts +++ b/src/controller/ensImage.ts @@ -10,6 +10,7 @@ import { RESPONSE_TIMEOUT } from '../config'; import { checkContract } from '../service/contract'; import { getDomain } from '../service/domain'; import getNetwork, { NetworkName } from '../service/network'; +import createDocumentfromTemplate from '../template-document'; /* istanbul ignore next */ export async function ensImage(req: Request, res: Response) { @@ -39,6 +40,15 @@ export async function ensImage(req: Request, res: Response) { version ); if (result.image_url) { + if (req.header('sec-fetch-dest') === 'document') { + const documentTemplate = createDocumentfromTemplate({ metadata: {...result, network: networkName }}); + res + .writeHead(200, { + 'Content-Type': 'text/html', + }) + .end(documentTemplate); + return; + } const base64 = result.image_url.replace('data:image/svg+xml;base64,', ''); const buffer = Buffer.from(base64, 'base64'); if (!res.headersSent) { diff --git a/src/controller/ensMetadata.ts b/src/controller/ensMetadata.ts index 7ec0cb4..db4da19 100644 --- a/src/controller/ensMetadata.ts +++ b/src/controller/ensMetadata.ts @@ -90,7 +90,8 @@ export async function ensMetadata(req: Request, res: Response) { ETH_REGISTRY_ABI, provider ); - if (!tokenId || !version) { + + if (!tokenId || version?.valueOf() === undefined) { throw 'Missing parameters to construct namehash'; } const _namehash = constructEthNameHash(tokenId, version as Version); diff --git a/src/index.ts b/src/index.ts index f710719..56238a4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,7 @@ const setCacheHeader = function ( `public, max-age=${period}, s-maxage=${period}` ); } + res.append('Vary', 'Sec-Fetch-Dest'); next(); }; diff --git a/src/service/avatar.ts b/src/service/avatar.ts index db2e8f9..eb738c2 100644 --- a/src/service/avatar.ts +++ b/src/service/avatar.ts @@ -64,7 +64,7 @@ export class AvatarMetadata { this.uri = uri; } - async getImage() { + async getImage(): Promise<[Buffer, string]> { let avatarURI; try { avatarURI = await this.avtResolver.getAvatar(this.uri, { @@ -102,7 +102,7 @@ export class AvatarMetadata { assert(!!response, 'Response is empty'); - const mimeType = response?.headers.get('Content-Type'); + const mimeType = response?.headers.get('Content-Type') || ''; const data = await response?.buffer(); if (mimeType?.includes('svg') || isSvg(data.toString())) { @@ -189,7 +189,7 @@ export async function getAvatarMeta( export async function getAvatarImage( provider: JsonRpcProvider, name: string -): Promise { +): Promise<[Buffer, string]> { const avatar = new AvatarMetadata(provider, name); return await avatar.getImage(); } diff --git a/src/service/rasterize.ts b/src/service/rasterize.ts index 8fe9133..f27b02e 100644 --- a/src/service/rasterize.ts +++ b/src/service/rasterize.ts @@ -1,7 +1,7 @@ import { GoogleAuth } from 'google-auth-library'; const auth = new GoogleAuth(); -const grRasterize = 'https://rasterize-y3ur7hmkna-uc.a.run.app/rasterize' +const grRasterize = 'https://us-central1-ens-metadata-service.cloudfunctions.net/rasterize' export function rasterize( contractAddress: string, diff --git a/src/template-document.ts b/src/template-document.ts new file mode 100644 index 0000000..676fe5d --- /dev/null +++ b/src/template-document.ts @@ -0,0 +1,419 @@ +import { CANVAS_FONT_PATH } from './config'; +import { importFont } from './utils/importFont'; + +const fontSatoshiBold = importFont(CANVAS_FONT_PATH, 'font/truetype'); + +interface DocumentMetadata { + name: string; + network: string; + image_url?: string; +} + +interface DocumentTemplateFields { + buffer?: Buffer; + metadata: DocumentMetadata; + mimeType?: string; +} + +export default function createDocumentfromTemplate({ + buffer, + metadata, + mimeType, +}: DocumentTemplateFields) { + if (!metadata && !buffer) { + throw 'Either image url, or image buffer needs to be provided for the document template'; + } + const image = + (metadata && metadata.image_url) || + (buffer && + `data:${mimeType};base64,${Buffer.from(buffer).toString('base64')}`); + return ` + + + + + ${metadata.name} + + + + +
+
+ ${ + mimeType !== 'image/svg+xml' + ? `${metadata.name}` + : buffer + } +
+
+
+
+ +
+
path Parameters
+ + + + + + + ${ + buffer + ? '' + : ` + + + ` + } + ${ + buffer + ? ` + + + ` + : ` + + + ` + } + +
+ + networkName +
required
+
+
+
+ + string + (networkName) +
+
+ Enum: +
"mainnet"
+
"sepolia"
+
+
+
+

Name of the chain to query for.

+
+
+
+
+ + contractAddress +
required
+
+
+
+ + string + (contractAddress) +
+
+ Example: +
0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85
+
+
+
+
+
+
+ + name +
required
+
+
+
+ + string + (ensName) +
+
+ Example:
${metadata.name}
+
+
+
+

ENS Name

+
+
+
+
+ + tokenId +
required
+
+
+
+ + string + (tokenId / ENS name) +
+
+ Example: +
+
4221908525551133525058944220830153...
/
${metadata.name}
+
+
+
+

TokenID = Labelhash(v1) /Namehash(v2) of your ENS name.

+

+ More: + https://docs.ens.domains/contract-api-reference/name-processing#hashing-names +

+
+
+
+
+
+
+

Responses

+
+ + + + +
+
+
+
+
+
+ + +`; +} diff --git a/yarn.lock b/yarn.lock index 8bdfe66..db7b7e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -250,10 +250,10 @@ dns-packet "^5.6.1" typescript-logging "^1.0.1" -"@ensdomains/ens-avatar@^1.0.0-alpha.2.ethers.6": - version "1.0.0-alpha.2.ethers.6" - resolved "https://registry.yarnpkg.com/@ensdomains/ens-avatar/-/ens-avatar-1.0.0-alpha.2.ethers.6.tgz#b406c20fec6fa98e3427e92970686c8cdafbddeb" - integrity sha512-lx8Ggmp6uLPQHzgunMvVUG3MfMLhyhSoY03Fb5HyjAK39OjYl7pMB+nmWnh+l4foKztMqXBfXLJ9WOYls5AyAw== +"@ensdomains/ens-avatar@^1.0.0-alpha.3.ethers.6": + version "1.0.0-alpha.3.ethers.6" + resolved "https://registry.yarnpkg.com/@ensdomains/ens-avatar/-/ens-avatar-1.0.0-alpha.3.ethers.6.tgz#7a0707a4608044340427d21cb27ed317563abe89" + integrity sha512-CLO2xIace+9pZygKbw+6Kt1qSwhx86m/L9hpQSEjqbOqPX2Wph4LVX84D07bFzI7oTRKKWobojIFYqNRd0JQ3Q== dependencies: "@ethersproject/contracts" "^5.7.0" "@ethersproject/providers" "^5.7.0"