diff --git a/src/assets/doc_output.json b/src/assets/doc_output.json index 2ee5d71..202e2b4 100644 --- a/src/assets/doc_output.json +++ b/src/assets/doc_output.json @@ -4,9 +4,11 @@ "version": "0.0.1-alpha.0", "title": "ENS Metadata Service", "description": "Set of endpoints to query ENS metadata and more", - "x-logo": { - "url": "/assets/logo.svg", - "altText": "Ethereum Name Service" + "contact": "contact@ens.domains", + "license": "MIT License", + "x_logo": { + "url": "./src/assets/logo.svg", + "backgroundColor": "#FFFFFF" } }, "host": "http://localhost:8080", @@ -18,6 +20,14 @@ "consumes": [], "produces": [], "paths": { + "/": { + "get": { + "tags": [], + "description": "", + "parameters": [], + "responses": {} + } + }, "/{networkName}/{contractAddress(0x[a-fA-F0-9]{40})}/{tokenId}": { "get": { "tags": [], @@ -28,14 +38,13 @@ "in": "path", "required": true, "type": "string", - "description": "Name of the chain to query for. (mainnet | rinkeby | ropsten | goerli ...)" + "description": "Name of the chain to query for. (mainnet|rinkeby|ropsten|goerli...)" }, { - "name": "contractAddress", + "name": "contractAddress(0x[a-fA-F0-9]{40})", "in": "path", "required": true, - "type": "string", - "description": "Contract Address of your stored NFT" + "type": "string" }, { "name": "tokenId", @@ -43,6 +52,11 @@ "required": true, "type": "string", "description": "Namehash(v1) /Labelhash(v2) of your ENS name.\n\nMore: https://docs.ens.domains/contract-api-reference/name-processing#hashing-names" + }, + { + "name": "{}", + "in": "query", + "type": "string" } ], "responses": { @@ -65,14 +79,13 @@ "in": "path", "required": true, "type": "string", - "description": "Name of the chain to query for. (mainnet | rinkeby | ropsten | goerli ...)" + "description": "Name of the chain to query for. (mainnet|rinkeby|ropsten|goerli...)" }, { - "name": "contractAddress", + "name": "contractAddress(0x[a-fA-F0-9]{40})", "in": "path", "required": true, - "type": "string", - "description": "Contract Address of your stored NFT" + "type": "string" }, { "name": "tokenId", @@ -80,6 +93,12 @@ "required": true, "type": "string", "description": "Namehash(v1) /Labelhash(v2) of your ENS name.\n\nMore: https://docs.ens.domains/contract-api-reference/name-processing#hashing-names" + }, + { + "name": "contractAddress", + "description": "Contract address which stores the NFT indicated by the tokenId", + "in": "query", + "type": "string" } ], "responses": { @@ -105,7 +124,7 @@ "in": "path", "required": true, "type": "string", - "description": "Name of the chain to query for. (mainnet | rinkeby | ropsten | goerli ...)" + "description": "Name of the chain to query for. (mainnet|rinkeby|ropsten|goerli...)" }, { "name": "name", @@ -117,10 +136,13 @@ ], "responses": { "200": { - "description": "Avatar metadata object" + "description": "OK" }, "404": { "description": "Not Found" + }, + "501": { + "description": "Not Implemented" } } } @@ -128,14 +150,44 @@ "/{networkName}/avatar/{name}": { "get": { "tags": [], - "description": "ENS avatar image", + "description": "ENS avatar record", + "parameters": [ + { + "name": "networkName", + "in": "path", + "required": true, + "type": "string", + "description": "Name of the chain to query for. (mainnet|rinkeby|ropsten|goerli...)" + }, + { + "name": "name", + "in": "path", + "required": true, + "type": "string", + "description": "ENS name" + } + ], + "responses": { + "404": { + "description": "Not Found" + }, + "501": { + "description": "Not Implemented" + } + } + } + }, + "/{networkName}/keybase/{name}": { + "get": { + "tags": [], + "description": "ENS Keybase signatures", "parameters": [ { "name": "networkName", "in": "path", "required": true, "type": "string", - "description": "Name of the chain to query for. (mainnet | rinkeby | ropsten | goerli ...)" + "description": "Name of the chain to query for. (mainnet|rinkeby|ropsten|goerli...)" }, { "name": "name", @@ -147,10 +199,13 @@ ], "responses": { "200": { - "description": "Avatar image file" + "description": "OK" }, "404": { "description": "Not Found" + }, + "501": { + "description": "Not Implemented" } } } diff --git a/src/avatar.ts b/src/avatar.ts index ef4b1c8..05ca4c7 100644 --- a/src/avatar.ts +++ b/src/avatar.ts @@ -3,18 +3,7 @@ import { ethers } from 'ethers'; import fetch from 'node-fetch'; import { BaseError } from './base'; import { INFURA_API_KEY } from './config'; - -export interface ResolverNotFound {} -export class ResolverNotFound extends BaseError {} - -export interface TextRecordNotFound {} -export class TextRecordNotFound extends BaseError {} - -export interface RetrieveURIFailed {} -export class RetrieveURIFailed extends BaseError {} - -export interface UnsupportedNamespace {} -export class UnsupportedNamespace extends BaseError {} +import { ResolverNotFound, TextRecordNotFound, RetrieveURIFailed, UnsupportedNamespace } from './error' interface HostMeta { chain_id?: number; diff --git a/src/endpoint.ts b/src/endpoint.ts index fc6617f..d83f4ac 100644 --- a/src/endpoint.ts +++ b/src/endpoint.ts @@ -5,11 +5,14 @@ import { checkContract, ContractMismatchError } from './contract'; import { getAvatarImage, getAvatarMeta, +} from './avatar'; +import { ResolverNotFound, TextRecordNotFound, UnsupportedNamespace, -} from './avatar'; +} from './error'; import getNetwork, { UnsupportedNetwork } from './network'; +import {getKeybaseSignatures, InvalidKeybaseSignatureFormat} from './keybase' export default function (app: Express) { app.get('/', (_req, res) => { @@ -197,4 +200,38 @@ export default function (app: Express) { } } }); + + app.get('/:networkName/keybase/:name', async function (req, res) { + // #swagger.description = 'ENS Keybase signatures' + // #swagger.parameters['networkName'] = { description: 'Name of the chain to query for. (mainnet|rinkeby|ropsten|goerli...)' } + // #swagger.parameters['name'] = { description: 'ENS name' } + const { name, networkName } = req.params; + try { + const { provider } = getNetwork(networkName); + const meta = await getKeybaseSignatures(provider, name); + + res.status(200).json(meta); + } catch (error: any) { + if ( + error instanceof ResolverNotFound || + error instanceof TextRecordNotFound + ) { + res.status(404).json({ + message: error.message, + }); + } else if (error instanceof UnsupportedNetwork) { + res.status(501).json({ + message: error.message, + }); + } else if (error instanceof InvalidKeybaseSignatureFormat) { + res.status(400).json({ + message: error.message, + }); + } else { + res.status(500).json({ + message: 'something went wrong', + }); + } + } + }); } diff --git a/src/error.ts b/src/error.ts new file mode 100644 index 0000000..3a915ff --- /dev/null +++ b/src/error.ts @@ -0,0 +1,13 @@ +import {BaseError} from './base' + +export interface ResolverNotFound {} +export class ResolverNotFound extends BaseError {} + +export interface TextRecordNotFound {} +export class TextRecordNotFound extends BaseError {} + +export interface RetrieveURIFailed {} +export class RetrieveURIFailed extends BaseError {} + +export interface UnsupportedNamespace {} +export class UnsupportedNamespace extends BaseError {} \ No newline at end of file diff --git a/src/keybase.ts b/src/keybase.ts new file mode 100644 index 0000000..bf8cbff --- /dev/null +++ b/src/keybase.ts @@ -0,0 +1,73 @@ +import { strict as assert } from 'assert'; +import { ethers } from 'ethers'; +import { ResolverNotFound, TextRecordNotFound } from './error' +import {BaseError} from './base' + + +export interface InvalidKeybaseSignatureFormat {} +export class InvalidKeybaseSignatureFormat extends BaseError {} + +export interface KeybaseSignatures { + signatures: KeybaseSignature[]; +} + +export interface KeybaseSignature { + kb_username: string; + sig_hash: string; +} + +export class Keybase { + defaultProvider: ethers.providers.EnsProvider; + name: string; + + constructor(provider: ethers.providers.EnsProvider, name: string) { + this.defaultProvider = provider; + this.name = name; + } + + async getRecord(): Promise { + try { + // retrieve resolver by ENS name + var resolver = await this.defaultProvider.getResolver(this.name); + } catch (e) { + throw new ResolverNotFound('There is no resolver set under given address'); + } + + try { + // determine and return if any Keybase signature stored as a text record + var record = await resolver.getText('io.keybase'); + + assert(record, 'Keybase signature is empty'); + } catch (e) { + throw new TextRecordNotFound('There is no io.keybase record under given address'); + } + + return record; + } + + async getSignatures(): Promise { + const record = await this.getRecord(); + const parts = record.split(':'); + + if (parts.length !== 2) { + throw new InvalidKeybaseSignatureFormat("signature should have `username:hash` format") + } + + const username = parts[0]; + const hash = parts[1]; + + return { + signatures: [ + { + kb_username: username, + sig_hash: hash + } + ] + }; + } +} + +export async function getKeybaseSignatures(provider: ethers.providers.EnsProvider, name: string): Promise { + const keybase = new Keybase(provider, name); + return await keybase.getSignatures(); +}