Merge pull request #1 from oceanprotocol/initial-setup

Creating API for requests to ENS
This commit is contained in:
Jamie Hewitt 2022-08-26 17:13:55 +03:00 committed by GitHub
commit 4eba08b60c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 8582 additions and 0 deletions

1
.env.example Normal file
View File

@ -0,0 +1 @@
#INFURA_PROJECT_ID="xxx"

16
.eslintrc Normal file
View File

@ -0,0 +1,16 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": ["./tsconfig.json"]
},
"extends": [
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
],
"plugins": ["@typescript-eslint", "prettier"],
"rules": {
"@typescript-eslint/explicit-function-return-type": "off"
},
"env": { "es6": true, "node": true }
}

1
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1 @@
* @jamiehewitt15 @kremalicious

8
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: npm
directory: '/'
schedule:
interval: monthly
time: '03:00'
timezone: Europe/Berlin

48
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,48 @@
name: 'CI'
on:
push:
branches:
- main
tags:
- '**'
pull_request:
branches:
- '**'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: Cache node_modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-lint-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-lint-${{ env.cache-name }}-
- run: npm ci
- run: npm run lint
test_integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: Cache node_modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-lint-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-lint-${{ env.cache-name }}-
- run: npm ci
- run: npm run test:integration

27
.github/workflows/publish.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: 'Publish'
on:
push:
tags:
- '**'
jobs:
npm:
runs-on: ubuntu-latest
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
registry-url: https://registry.npmjs.org/
- run: npm ci
# pre-releases, triggered by `next` as part of git tag
- run: npm publish --tag next
if: ${{ contains(github.ref, 'next') }}
# production releases
- run: npm publish
if: ${{ !contains(github.ref, 'next') }}

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
.env
.vercel
dist

1
.husky/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
_

4
.husky/pre-commit Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
node_modules/.bin/pretty-quick --staged

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"tabWidth": 2,
"endOfLine": "auto"
}

7
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"wix.vscode-import-cost"
]
}

17
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"search.exclude": {
"**/.cache": true,
"**/public": true
}
}

100
README.md
View File

@ -1 +1,101 @@
# Proxy API for ENS requests
## Running Locally
```
npm install
npm i -g vercel
vercel dev
```
## Example Requests
### Get Ens Name
```
GET http://localhost:3000/api/name?accountId=0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0
```
Example response:
```
{
"name": "jellymcjellyfish.eth"
}
```
### Get Ens Address
```
GET http://localhost:3000/api/address?name=jellymcjellyfish.eth
```
Example response:
```
{
"address": "0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0"
}
```
### Get Ens Text Records
```
GET http://localhost:3000/api/text?name=jellymcjellyfish.eth
```
Example response:
```
{
"records": [
{
"key": "url",
"value": "https://oceanprotocol.com"
},
{
"key": "avatar",
"value": "https://raw.githubusercontent.com/oceanprotocol/art/main/logo/favicon-white.png"
},
{
"key": "com.twitter",
"value": "oceanprotocol"
},
{
"key": "com.github",
"value": "oceanprotocol"
}
]
}
```
### Get ENS Profile
```
GET http://localhost:3000/api/profile?address=0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0
```
Example response:
```
{
"profile": {
"name": "jellymcjellyfish.eth",
"avatar": "https://metadata.ens.domains/mainnet/avatar/jellymcjellyfish.eth",
"links": [
{
"key": "url",
"value": "https://oceanprotocol.com"
},
{
"key": "com.twitter",
"value": "oceanprotocol"
},
{
"key": "com.github",
"value": "oceanprotocol"
}
]
}
}
```

9
api/_utils.ts Normal file
View File

@ -0,0 +1,9 @@
import { ethers } from 'ethers'
export async function getProvider(): Promise<any> {
const provider = new ethers.providers.InfuraProvider(
'homestead',
process.env.INFURA_PROJECT_ID
)
return provider
}

18
api/address.ts Normal file
View File

@ -0,0 +1,18 @@
import type { VercelRequest, VercelResponse } from '@vercel/node'
import { getProvider } from './_utils'
export default async function getEnsAddress(
request: VercelRequest,
response: VercelResponse
) {
try {
const ensName = request.query.name
const provider = await getProvider()
const address = await provider.resolveName(ensName)
response.setHeader('Cache-Control', 'max-age=0, s-maxage=86400')
response.status(200).send({ address })
} catch (error) {
response.send({ error })
}
}

27
api/name.ts Normal file
View File

@ -0,0 +1,27 @@
import type { VercelRequest, VercelResponse } from '@vercel/node'
import { getProvider } from './_utils'
export async function getEnsName(accountId: string) {
const provider = await getProvider()
let name = await provider.lookupAddress(accountId)
// Check to be sure the reverse record is correct.
const reverseAccountId = await provider.resolveName(name)
if (accountId.toLowerCase() !== reverseAccountId.toLowerCase()) name = null
return name
}
export default async function nameApi(
request: VercelRequest,
response: VercelResponse
) {
try {
const accountId = String(request.query.accountId)
const name = await getEnsName(accountId)
response.setHeader('Cache-Control', 'max-age=0, s-maxage=86400')
response.status(200).send({ name })
} catch (error) {
response.send({ error })
}
}

72
api/profile.ts Normal file
View File

@ -0,0 +1,72 @@
import type { VercelRequest, VercelResponse } from '@vercel/node'
import { getEnsName } from './name'
import { getEnsTextRecords } from './text'
interface ProfileLink {
key: string
value: string
}
interface Profile {
name: string
url?: string
avatar?: string
description?: string
links?: ProfileLink[]
}
function getEnsAvatar(ensName: string): string {
return ensName
? `https://metadata.ens.domains/mainnet/avatar/${ensName}`
: 'null'
}
export async function getEnsProfile(accountId: string): Promise<Profile> {
const name = await getEnsName(accountId)
if (!name) return { name: 'null' }
const records = await getEnsTextRecords(name)
if (!records) return { name }
const avatar = records.filter((record) => record.key === 'avatar')[0]?.value
const description = records.filter(
(record) => record.key === 'description'
)[0]?.value
// filter out what we need from the fetched text records
const linkKeys = [
'url',
'com.twitter',
'com.github',
'org.telegram',
'com.discord',
'com.reddit'
]
const links: ProfileLink[] = records.filter((record) =>
linkKeys.includes(record.key)
)
const profile: Profile = {
name,
...(avatar && { avatar: getEnsAvatar(name) }),
...(description && { description }),
...(links.length > 0 && { links })
}
return profile
}
export default async function EnsProfileApi(
request: VercelRequest,
response: VercelResponse
) {
try {
const accountId = String(request.query.address)
const profile = await getEnsProfile(accountId)
response.setHeader('Cache-Control', 'max-age=0, s-maxage=86400')
response.status(200).send({ profile })
} catch (error) {
response.send({ error })
}
}

44
api/text.ts Normal file
View File

@ -0,0 +1,44 @@
import type { VercelRequest, VercelResponse } from '@vercel/node'
import { getProvider } from './_utils'
export async function getEnsTextRecords(
ensName: string
): Promise<{ key: string; value: string }[] | null> {
const texts = [
'url',
'avatar',
'com.twitter',
'com.github',
'org.telegram',
'com.discord',
'com.reddit'
]
const records = []
const provider = await getProvider()
const resolver = await provider.getResolver(ensName)
if (!resolver) return null
for (let index = 0; index < texts?.length; index++) {
const key = texts[index]
const value = await resolver.getText(key)
value && records.push({ key, value })
}
return records
}
export default async function ensTextApi(
request: VercelRequest,
response: VercelResponse
) {
try {
const ensName = String(request.query.name)
const records = await getEnsTextRecords(ensName)
response.setHeader('Cache-Control', 'max-age=0, s-maxage=86400')
response.status(200).send({ records })
} catch (error) {
response.send({ error })
}
}

7965
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

40
package.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "ocean-ens-proxy",
"description": "Ocean Protocol ENS Proxy Server",
"version": "0.0.0",
"author": "Ocean Protocol <devops@oceanprotocol.com>",
"license": "Apache-2.0",
"scripts": {
"lint": "eslint --ignore-path .gitignore --ext .js --ext .ts --ext .tsx . && npm run type-check",
"clean": "rm -rf ./dist",
"format": "prettier --ignore-path .gitignore './**/*.{css,yml,js,ts,tsx,json}' --write",
"test:format": "npm run format && npm run lint",
"test:integration": "ts-mocha -p test/tsconfig.json --exit test/**/*.test.ts",
"test": "test:format && test:integration",
"type-check": "tsc --noEmit"
},
"dependencies": {
"ethers": "^5.7.0"
},
"devDependencies": {
"@types/chai": "^4.3.3",
"@types/glob": "^7.2.0",
"@types/mocha": "^9.1.1",
"@types/test-listen": "^1.1.0",
"@typescript-eslint/eslint-plugin": "^5.28.0",
"@typescript-eslint/parser": "^5.29.0",
"@vercel/node": "^2.5.8",
"axios": "^0.27.2",
"chai": "^4.3.6",
"eslint": "^8.18.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
"husky": "^7.0.0",
"prettier": "^2.7.1",
"pretty-quick": "^3.1.3",
"test-listen": "^1.1.0",
"ts-mocha": "^10.0.0",
"typescript": "4.7.3",
"vercel-node-server": "^2.2.1"
}
}

143
test/api.test.ts Normal file
View File

@ -0,0 +1,143 @@
import addressApi from '../api/address'
import nameApi from '../api/name'
import profileApi from '../api/profile'
import textApi from '../api/text'
import { createServer } from 'vercel-node-server'
import listen from 'test-listen'
// import express from 'express'
// import request from 'supertest'
import axios from 'axios'
// import type { VercelRequest, VercelResponse } from '@vercel/node'
import { assert } from 'chai'
let server: any
let url: string
const name = 'jellymcjellyfish.eth'
const accountId = '0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0'
const invalid = 'lkdfjslkdfjpeoijf3423'
describe('Testing ENS proxy API endpoints', function () {
this.timeout(25000)
it('Requesting address should return the expected response', async () => {
server = createServer(addressApi)
url = await listen(server)
const response = await axios.get(url, {
params: {
name
}
})
assert(response.status === 200)
assert(response.data.address === accountId)
})
it('Requesting ENS name should return the expected response', async () => {
server = createServer(nameApi)
url = await listen(server)
const response = await axios.get(url, {
params: {
accountId
}
})
assert(response.status === 200)
assert(response.data.name === name)
})
it('Requesting text records should return the expected response', async () => {
server = createServer(textApi)
url = await listen(server)
const response = await axios.get(url, {
params: {
name
}
})
assert(response.status === 200)
assert(
response.data.records[0].value === 'https://oceanprotocol.com',
'Wrong URL'
)
assert(
response.data.records[1].value ===
'https://raw.githubusercontent.com/oceanprotocol/art/main/logo/favicon-white.png',
'Wrong avatar'
)
assert(response.data.records[2].value === 'oceanprotocol', 'Wrong link 1')
assert(response.data.records[3].value === 'oceanprotocol', 'wrong link 2')
assert(response.data.records[0].key === 'url', 'Wrong URL')
assert(response.data.records[1].key === 'avatar', 'Wrong avatar')
assert(response.data.records[2].key === 'com.twitter', 'Wrong link 1')
assert(response.data.records[3].key === 'com.github', 'wrong link 2')
})
it('Requesting user profile should return the expected response', async () => {
server = createServer(profileApi)
url = await listen(server)
const response = await axios.get(url, {
params: {
address: accountId
}
})
assert(response.status === 200)
assert(response.data.profile.name === name)
assert(
response.data.profile.avatar ===
'https://metadata.ens.domains/mainnet/avatar/jellymcjellyfish.eth'
)
assert(response.data.profile.links[0].value === 'https://oceanprotocol.com')
assert(response.data.profile.links[1].value === 'oceanprotocol')
assert(response.data.profile.links[2].value === 'oceanprotocol')
assert(response.data.profile.links[0].key === 'url')
assert(response.data.profile.links[1].key === 'com.twitter')
assert(response.data.profile.links[2].key === 'com.github')
})
// Tests with incorrect address or name
it('Requesting address should return status 200 with invalid name', async () => {
server = createServer(addressApi)
url = await listen(server)
const response = await axios.get(url, {
params: {
name: invalid
}
})
assert(response.status === 200)
})
it('Requesting name should return status 200 with invalid accountId', async () => {
server = createServer(nameApi)
url = await listen(server)
const response = await axios.get(url, {
params: {
accountId: invalid
}
})
assert(response.status === 200)
})
it('Requesting text records should return status 200 with invalid name', async () => {
server = createServer(textApi)
url = await listen(server)
const response = await axios.get(url, {
params: {
name: invalid
}
})
assert(response.status === 200)
})
it('Requesting profile should return status 200 with invalid name', async () => {
server = createServer(profileApi)
url = await listen(server)
const response = await axios.get(url, {
params: {
address: invalid
}
})
assert(response.status === 200)
})
})
after(() => {
server.close()
})

9
test/tsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"resolveJsonModule": true,
"lib": ["es6", "es7"],
"noUnusedLocals": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
}
}

14
tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"strict": true,
"noEmit": true,
"module": "esnext",
"moduleResolution": "node",
"isolatedModules": true,
"allowSyntheticDefaultImports": true
},
"exclude": ["node_modules"],
"include": ["**/*.ts", "**/*.tsx"]
}