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

add analytics to the gateway #3

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node: ['16.7.x', '18.x']
node: ['18.x', '20.x']
os: [ubuntu-latest, windows-latest, macOS-latest]

steps:
Expand Down
56 changes: 40 additions & 16 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node: ['16.7.x', '18.15.x']
node: ['18.x']
os: [ubuntu-latest, macOS-latest]

steps:
Expand All @@ -38,26 +38,50 @@ jobs:
path: './contracts'
ref: '6b9aaae963f71792ab1a75de61d5151ff1d1b7e3'

# - name: Run local node on Windows
# if: runner.os == 'Windows'
# run: cd contracts; yarn --network-timeout 100000; $env:HARDHAT_DISABLE_TELEMETRY_PROMPT = "true"; $currentDir = (Get-Location).Path; $job = Start-Job -ScriptBlock { param($dir) Set-Location -Path $dir; npx hardhat node --hostname '127.0.0.1' } -ArgumentList $currentDir; Start-Sleep -Seconds 50; Receive-Job -Job $job -Keep
# env:
# BATCH_GATEWAY_URLS: '["https://universal-offchain-unwrapper.ens-cf.workers.dev/"]'
# DOH_GATEWAY_URL: 'https://cloudflare-dns.com/dns-query'

- name: Run local node on Unix
if: runner.os != 'Windows'
run: cd ./contracts && yarn && npx hardhat node --hostname 127.0.0.1 &
- name: Run local node
run: |
cd ./contracts
yarn
npx hardhat node --hostname 127.0.0.1 > hardhat_output.log 2>&1 &
echo $! > hardhat_pid.txt
env:
BATCH_GATEWAY_URLS: '["https://universal-offchain-unwrapper.ens-cf.workers.dev/"]'
DOH_GATEWAY_URL: 'https://cloudflare-dns.com/dns-query'

- name: Wait for local node
uses: iFaxity/[email protected]
with:
timeout: 900000
window: 2000
resource: http://127.0.0.1:8545
run: |
timeout=300
while true; do
if [ $timeout -le 0 ]; then
echo "Timeout waiting for Hardhat node"
cat hardhat_output.log
exit 1
fi
if nc -z localhost 8545 2>/dev/null; then
echo "Hardhat node is up and running"
break
fi
echo "Waiting for Hardhat node... ($timeout seconds left)"
sleep 5
timeout=$((timeout - 5))
done
shell: bash

- name: Test
run: yarn test --ci --coverage --maxWorkers=2

- name: Upload Hardhat logs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: hardhat-logs
path: ./contracts/hardhat_output.log

- name: Cleanup Hardhat process
if: always()
run: |
if [ -f ./contracts/hardhat_pid.txt ]; then
pid=$(cat ./contracts/hardhat_pid.txt)
kill $pid 2>/dev/null || true
fi
shell: bash
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
.DS_Store
.dev.vars
.env
.wrangler
node_modules
coverage
dist
5 changes: 1 addition & 4 deletions jest.setup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
import { TextEncoder, TextDecoder } from 'util';

global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder as any;
// any jest related setup here
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,12 @@
}
],
"devDependencies": {
"@cloudflare/workers-types": "^4.20230214.0",
"@cloudflare/workers-types": "^4.20240909.0",
"@ensdomains/ens-contracts": "^0.0.21",
"@types/chai": "^4.3.4",
"@types/chai-as-promised": "^7.1.5",
"@types/dns-packet": "^5.2.4",
"@types/dns-packet": "^5.6.4",
"@types/express": "^4.17.21",
"@types/supertest": "^2.0.11",
"bundlesize2": "^0.0.31",
"chai": "^4.3.7",
Expand All @@ -69,17 +70,18 @@
"tsdx": "^0.14.1",
"tslib": "^2.5.0",
"typescript": "^4.9.5",
"wrangler": "^2.0.7"
"wrangler": "^3.78.2"
},
"dependencies": {
"@chainlink/ccip-read-server": "^0.2.1",
"@ensdomains/ccip-read-cf-worker": "^0.0.1",
"@ensdomains/ccip-read-cf-worker": "^0.0.3",
"@ensdomains/dnsprovejs": "^0.4.1",
"@ensdomains/server-analytics": "^0.0.1",
"dotenv": "^16.0.3",
"ethers": "^5.7.2",
"node-fetch": "2.6.1"
},
"volta": {
"node": "16.15.1"
"node": "20.17.0"
}
}
72 changes: 59 additions & 13 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@ import { DNSProver } from '@ensdomains/dnsprovejs';
import { ethers } from 'ethers';
import * as packet from 'dns-packet';
import * as qTypes from 'dns-packet/types';
import { serializeError } from './utils';

export function makeApp(
sendQuery: ConstructorParameters<typeof DNSProver>[0],
path: string,
Server: any
Server: any,
trackEvent?: Function
) {
const prover = new DNSProver(sendQuery);
const emptyRRSet = [
[
{
rrset: [],
sig: [],
},
],
];

const server = new Server();
const abi = [
Expand All @@ -19,20 +29,56 @@ export function makeApp(
type: 'resolve',
func: async (args: ethers.utils.Result) => {
const [name, qtype] = args;
const decodedName = packet.name.decode(
const decodedName = (packet as any).name.decode(
Buffer.from(name.slice(2), 'hex')
);
const result = await prover.queryWithProof(
qTypes.toString(qtype),
decodedName
);
const ret = Array.prototype
.concat(result.proofs, [result.answer])
.map(entry => ({
rrset: entry.toWire(),
sig: entry.signature.data.signature,
}));
return [ret];

if (
decodedName.split('.').length < 2 ||
decodedName.startsWith('.') ||
decodedName.endsWith('.')
) {
return emptyRRSet;
}

if (trackEvent) {
trackEvent(
'resolve',
{
props: { name: decodedName, qtype: qTypes.toString(qtype) },
},
true
);
}

try {
const result = await prover.queryWithProof(
qTypes.toString(qtype),
decodedName
);
if (!result) {
return emptyRRSet;
}
const ret = Array.prototype
.concat(result.proofs, [result.answer])
.map(entry => ({
rrset: entry.toWire(),
sig: entry.signature.data.signature,
}));
return [ret];
} catch (error) {
if (trackEvent) {
trackEvent(
'error',
{
props: { name: decodedName, message: serializeError(error) },
},
true
);
}

return emptyRRSet;
}
},
},
]);
Expand Down
51 changes: 51 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
export function serializeError(error: any) {
if (error instanceof Error) {
const errorObject = {
...(error.message && {
message: JSON.stringify(error.message),
}),
...(error.stack && { stack: error.stack }),
};
return JSON.stringify(errorObject);
} else {
return JSON.stringify(error);
}
}

type DNSRecord = {
rrset: string;
sig: string;
};

function hexToAscii(hex: string): string {
let result = '';
for (let i = 0; i < hex.length; i += 2) {
const part = parseInt(hex.substring(i, i + 2), 16);
if (part) result += String.fromCharCode(part);
}
return result;
}

export function extractENSRecord(dnsRecords: DNSRecord[]): string[] {
const txtPrefix = '0x0010'; // 16
const txtRecords: string[] = [];

for (const record of dnsRecords) {
if (record.rrset.startsWith(txtPrefix)) {
const contentStart = txtPrefix.length;
const rawContent = record.rrset.slice(contentStart);
let asciiContent = hexToAscii(rawContent);

asciiContent = asciiContent.split('\t').join();

const parts = asciiContent.split(',');
for (const part of parts) {
if (part.includes('ENS1')) {
txtRecords.push(part.slice(2));
}
}
}
}

return txtRecords;
}
72 changes: 67 additions & 5 deletions src/worker.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,79 @@
import {
Request as CFWRequest,
ExecutionContext,
} from '@cloudflare/workers-types';
import { Server } from '@ensdomains/ccip-read-cf-worker';
import { PropsDecoder, Tracker } from '@ensdomains/server-analytics';
import { dohQuery } from '@ensdomains/dnsprovejs';
import { ethers } from 'ethers';
import { makeApp } from './app';
import { extractENSRecord } from './utils';

const routeHandler = (env: any) => {
interface ENV {
DOH_GATEWAY_URL: string;
PLAUSIBLE_BASE_URL: string;
}

const abi_RRSetWithSignature = [
ethers.utils.ParamType.from({
components: [
{ type: 'bytes', name: 'rrset' },
{ type: 'bytes', name: 'sig' },
],
type: 'tuple[]',
}),
];

const tracker = new Tracker<CFWRequest>(
'ccip-read-dns-worker.ens-cf.workers.dev',
{
enableLogging: true,
}
);

const routeHandler = (env: ENV, trackEvent?: Function) => {
const { DOH_GATEWAY_URL } = env;
const app = makeApp(dohQuery(DOH_GATEWAY_URL as string), '/', Server);
const app = makeApp(
dohQuery(DOH_GATEWAY_URL as string),
'/',
Server,
trackEvent
);
console.log(`Serving with DoH Resolver ${DOH_GATEWAY_URL}`);
return app;
};

const propsDecoder: PropsDecoder<CFWRequest> = (
_: CFWRequest | unknown,
data?: string
) => {
if (!data) return {};

const decodedData = ethers.utils.defaultAbiCoder.decode(
abi_RRSetWithSignature,
data
)[0];
const structuredData = decodedData.map((item: string[]) => ({
rrset: item[0],
sig: item[1],
}));
return { result: extractENSRecord(structuredData) };
};

module.exports = {
fetch: async function(request: Request, env: any, _context: any) {
const router = routeHandler(env);
return await router.handle(request);
fetch: async function(
request: CFWRequest,
env: ENV,
_context: ExecutionContext
) {
if (env.PLAUSIBLE_BASE_URL) {
tracker.apiEndpoint = env.PLAUSIBLE_BASE_URL;
}
await tracker.trackEvent(request, 'request', {}, true);
await tracker.trackPageview(request, {}, true);
const router = routeHandler(env, tracker.trackEvent.bind(tracker, request));
return router
.handle(request)
.then(tracker.logResult.bind(this, propsDecoder, request));
},
};
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"compilerOptions": {
"module": "esnext",
"lib": ["dom", "esnext"],
"types": ["jest", "@cloudflare/workers-types"],
"importHelpers": true,
// output .d.ts declaration files for consumers
"declaration": true,
Expand Down
Loading
Loading