Skip to content
Merged
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
72 changes: 72 additions & 0 deletions data/docs/get-domains.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Get Domains

This GET route retrieves all domains where the specified address is an admin. It returns information about each domain including address, contenthash, text records, and coin types.

## Parameters

| Parameter | Type | Required | Description |
| --------------- | ------ | -------- | ----------------------------------------------------------------------------------- |
| `admin-address` | string | Yes | The Ethereum address that is an admin of the domains to retrieve. |
| `limit` | number | No | The number of domains to return (default: 50, max: 1000). |
| `offset` | number | No | The number of domains to skip (default: 0). |
| `text_records` | string | No | Set to "0" to exclude text records and coin types from the response (default: "1"). |

## Response

The response is an array of domain objects with the following properties:

| Property | Type | Description |
| -------------- | ------ | ------------------------------------------------------------------------------- |
| `domain` | string | The domain name (e.g., "namestone.xyz"). |
| `address` | string | The Ethereum address the domain resolves to. |
| `contenthash` | string | The IPFS or IPNS contenthash for the domain's website, if set. |
| `text_records` | object | An object containing key-value pairs of the domain's text records (if included).|
| `coin_types` | object | An object containing key-value pairs of L2 chains and their addresses (if included). |

## Error Codes

| Status Code | Description |
| ----------- | ------------------------------------------------- |
| 400 | Bad request. Invalid parameters or network. |
| 500 | Server error. |

## Curl Example

```
curl -X GET \
-H 'Content-Type: application/json' \
'https://namestone.com/api/public_v1/get-domains?admin-address=0x534631Bcf33BDb069fB20A93d2fdb9e4D4dD42CF'
```

## Example Response

```json
[
{
"domain": "namestone.xyz",
"address": "0x534631Bcf33BDb069fB20A93d2fdb9e4D4dD42CF",
"contenthash": "ipfs://QmUbTVz1L4uEvAPg5QcSu8Rifq2CtTc4SYmasXLAYkFQbp",
"text_records": {
"com.twitter": "namestonehq",
"com.github": "resolverworks",
"url": "https://www.namestone.xyz",
"description": "Namestone ENS Resolver"
},
"coin_types": {
"60": "0x534631Bcf33BDb069fB20A93d2fdb9e4D4dD42CF",
"2147483785": "0x534631Bcf33BDb069fB20A93d2fdb9e4D4dD42CF"
}
},
{
"domain": "example.eth",
"address": "0xA47632346786AD59c8590Bd4898D84B4eAB97644",
"contenthash": null,
"text_records": {
"description": "Example domain"
},
"coin_types": {}
}
]
```

This endpoint is particularly useful for domain administrators who need to manage multiple domains and want to retrieve a comprehensive list of all domains under their administration.
2 changes: 2 additions & 0 deletions jest.env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import dotenv from "dotenv";
dotenv.config({ path: ".env.test" });
88 changes: 88 additions & 0 deletions pages/api/[network]/add-admin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import sql from "../../../lib/db";
import {
checkApiKey,
getNetwork,
getClientIp,
} from "../../../utils/ServerUtils";
import Cors from "micro-cors";
import { normalize } from "viem/ens";

const cors = Cors({
allowMethods: ["GET", "HEAD", "POST"],
origin: "*",
});

async function handler(req, res) {
const network = getNetwork(req);
if (!network) {
return res.status(400).json({ error: "Invalid network" });
}
const { headers } = req;

// Check required parameters
let body = req.body;
if (typeof body === "string") {
body = JSON.parse(body);
}
if (!body.domain) {
return res.status(400).json({ error: "Missing domain" });
}
if (!body.admin_address) {
return res.status(400).json({ error: "Missing admin_address" });
}

let domain;
let admin_address = body.admin_address;
try {
domain = normalize(body.domain);
} catch (e) {
return res.status(400).json({ error: "Invalid ens domain" });
}

// Check API key
const allowedApi = await checkApiKey(
headers.authorization || req.query.api_key,
domain
);
if (!allowedApi) {
return res
.status(401)
.json({ error: "You are not authorized to use this endpoint" });
}

try {
// Check if domain exists
const domainQuery = await sql`
select id from domain where name = ${domain} and network = ${network} limit 1`;

if (domainQuery.length === 0) {
return res.status(400).json({ error: "Domain does not exist" });
}

const domainId = domainQuery[0].id;

// Insert admin to admin table
await sql`
insert into admin (domain_id, address)
values (${domainId}, ${admin_address})
on conflict (domain_id, address) do nothing;
`;
} catch (error) {
console.error("Error adding admin:", error);
return res.status(500).json({ error: "Internal server error" });
}

// log user engagement
const clientIp = getClientIp(req);
const jsonPayload = JSON.stringify({
body: body,
ip_address: clientIp,
});
await sql`
insert into user_engagement (address, name, details)
values (${admin_address},'add_admin', ${jsonPayload})`;

return res.status(200).json({ success: true });
}

export default cors(handler);
131 changes: 131 additions & 0 deletions pages/api/[network]/add-admin.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { createRequest, createResponse } from "node-mocks-http";
import handler from "./add-admin";
import sqlForTests from "../../../test_utils/mock_db";
import {
setupTestDatabase,
teardownTestDatabase,
} from "../../../test_utils/test_db_setup";

const TEST_DOMAIN = "test-admin.eth";
const TEST_ADMIN_ADDRESS = "0xAdminTestAddress123456789012345678901234";
const TEST_API_KEY = "fake-test-api-key";
const TEST_NETWORK = "mainnet";

describe("add-admin API E2E", () => {
let domainId;

beforeAll(async () => {
process.env.TEST_API_KEY = TEST_API_KEY;
await setupTestDatabase();
// Insert domain
const [domain] = await sqlForTests`
INSERT INTO domain (name, network) VALUES (${TEST_DOMAIN}, ${TEST_NETWORK}) RETURNING id
`;
domainId = domain.id;
// Insert API key
await sqlForTests`
INSERT INTO api_key (domain_id, key) VALUES (${domainId}, ${TEST_API_KEY})
`;
});

afterAll(async () => {
await sqlForTests`DELETE FROM admin WHERE domain_id = ${domainId}`;
await sqlForTests`DELETE FROM api_key WHERE domain_id = ${domainId}`;
await sqlForTests`DELETE FROM domain WHERE id = ${domainId}`;
await teardownTestDatabase();
});

test("successfully adds an admin", async () => {
try {
const req = createRequest({
method: "POST",
headers: { authorization: TEST_API_KEY },
body: { domain: TEST_DOMAIN, admin_address: TEST_ADMIN_ADDRESS },
query: { network: "public_v1" },
});
const res = createResponse();
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
expect(JSON.parse(res._getData())).toEqual({ success: true });
// Check DB
const admins =
await sqlForTests`SELECT * FROM admin WHERE domain_id = ${domainId} AND address = ${TEST_ADMIN_ADDRESS}`;
expect(admins.length).toBe(1);
} catch (err) {
console.error("Test error (successfully adds an admin):", err);
throw err;
}
});

test("missing domain returns 400", async () => {
const req = createRequest({
method: "POST",
headers: { authorization: TEST_API_KEY },
body: { admin_address: TEST_ADMIN_ADDRESS },
query: { network: "public_v1" },
});
const res = createResponse();
await handler(req, res);
expect(res._getStatusCode()).toBe(400);
expect(JSON.parse(res._getData())).toEqual({ error: "Missing domain" });
});

test("missing admin_address returns 400", async () => {
const req = createRequest({
method: "POST",
headers: { authorization: TEST_API_KEY },
body: { domain: TEST_DOMAIN },
query: { network: "public_v1" },
});
const res = createResponse();
await handler(req, res);
expect(res._getStatusCode()).toBe(400);
expect(JSON.parse(res._getData())).toEqual({
error: "Missing admin_address",
});
});

test("invalid domain returns 400", async () => {
const req = createRequest({
method: "POST",
headers: { authorization: TEST_API_KEY },
body: { domain: "invalid domain!@#", admin_address: TEST_ADMIN_ADDRESS },
query: { network: "public_v1" },
});
const res = createResponse();
await handler(req, res);
expect(res._getStatusCode()).toBe(400);
expect(JSON.parse(res._getData())).toEqual({ error: "Invalid ens domain" });
});

test("unauthorized API key returns 401", async () => {
const req = createRequest({
method: "POST",
headers: { authorization: "wrong-key" },
body: { domain: TEST_DOMAIN, admin_address: TEST_ADMIN_ADDRESS },
query: { network: "public_v1" },
});
const res = createResponse();
await handler(req, res);
expect(res._getStatusCode()).toBe(401);
expect(JSON.parse(res._getData())).toEqual({
error: "You are not authorized to use this endpoint",
});
});

test("duplicate admin does not error", async () => {
// Add once
await sqlForTests`INSERT INTO admin (domain_id, address) VALUES (${domainId}, ${TEST_ADMIN_ADDRESS}) ON CONFLICT DO NOTHING`;
// Add again via API
const req = createRequest({
method: "POST",
headers: { authorization: TEST_API_KEY },
body: { domain: TEST_DOMAIN, admin_address: TEST_ADMIN_ADDRESS },
query: { network: "public_v1" },
});
const res = createResponse();
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
expect(JSON.parse(res._getData())).toEqual({ success: true });
});
});
90 changes: 90 additions & 0 deletions pages/api/[network]/delete-admin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import sql from "../../../lib/db";
import {
checkApiKey,
getNetwork,
getClientIp,
} from "../../../utils/ServerUtils";
import Cors from "micro-cors";
import { normalize } from "viem/ens";

const cors = Cors({
allowMethods: ["GET", "HEAD", "POST"],
origin: "*",
});

async function handler(req, res) {
const network = getNetwork(req);
if (!network) {
return res.status(400).json({ error: "Invalid network" });
}
const { headers } = req;

// Check required parameters
let body = req.body;
if (typeof body === "string") {
body = JSON.parse(body);
}
if (!body.domain) {
return res.status(400).json({ error: "Missing domain" });
}
if (!body.admin_address) {
return res.status(400).json({ error: "Missing admin_address" });
}

let domain;
let admin_address = body.admin_address;
try {
domain = normalize(body.domain);
} catch (e) {
return res.status(400).json({ error: "Invalid ens domain" });
}

// Check API key
const allowedApi = await checkApiKey(
headers.authorization || req.query.api_key,
domain
);
if (!allowedApi) {
return res
.status(401)
.json({ error: "You are not authorized to use this endpoint" });
}

try {
// Check if domain exists
const domainQuery = await sql`
select id from domain where name = ${domain} and network = ${network} limit 1`;

if (domainQuery.length === 0) {
return res.status(400).json({ error: "Domain does not exist" });
}

const domainId = domainQuery[0].id;

// Delete admin from admin table
const result = await sql`
delete from admin where domain_id = ${domainId} and address = ${admin_address};
`;

if (result.count === 0) {
return res.status(404).json({ error: "Admin not found for this domain" });
}
} catch (error) {
console.error("Error deleting admin:", error);
return res.status(500).json({ error: "Internal server error" });
}

// log user engagement
const clientIp = getClientIp(req);
const jsonPayload = JSON.stringify({
body: body,
ip_address: clientIp,
});
await sql`
insert into user_engagement (address, name, details)
values (${admin_address},'delete_admin', ${jsonPayload})`;

return res.status(200).json({ success: true });
}

export default cors(handler);
Loading