Skip to content
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

feat(endpoint): Keybase signatures #34

Open
wants to merge 3 commits into
base: master
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
87 changes: 71 additions & 16 deletions src/assets/doc_output.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "[email protected]",
"license": "MIT License",
"x_logo": {
"url": "./src/assets/logo.svg",
"backgroundColor": "#FFFFFF"
}
},
"host": "http://localhost:8080",
Expand All @@ -18,6 +20,14 @@
"consumes": [],
"produces": [],
"paths": {
"/": {
"get": {
"tags": [],
"description": "",
"parameters": [],
"responses": {}
}
},
"/{networkName}/{contractAddress(0x[a-fA-F0-9]{40})}/{tokenId}": {
"get": {
"tags": [],
Expand All @@ -28,21 +38,25 @@
"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",
"in": "path",
"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": {
Expand All @@ -65,21 +79,26 @@
"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",
"in": "path",
"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": {
Expand All @@ -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",
Expand All @@ -117,25 +136,58 @@
],
"responses": {
"200": {
"description": "Avatar metadata object"
"description": "OK"
},
"404": {
"description": "Not Found"
},
"501": {
"description": "Not Implemented"
}
}
}
},
"/{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",
Expand All @@ -147,10 +199,13 @@
],
"responses": {
"200": {
"description": "Avatar image file"
"description": "OK"
},
"404": {
"description": "Not Found"
},
"501": {
"description": "Not Implemented"
}
}
}
Expand Down
13 changes: 1 addition & 12 deletions src/avatar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
39 changes: 38 additions & 1 deletion src/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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',
});
}
}
});
}
13 changes: 13 additions & 0 deletions src/error.ts
Original file line number Diff line number Diff line change
@@ -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 {}
73 changes: 73 additions & 0 deletions src/keybase.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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<KeybaseSignatures> {
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<any> {
const keybase = new Keybase(provider, name);
return await keybase.getSignatures();
}