Skip to content

Commit

Permalink
feat: DefiLlama validator for oss-directory (#2791)
Browse files Browse the repository at this point in the history
* This will check if the URLs are well formed
* It will also check whether the slugs are valid against the DefiLlama
  API
* Then I added the validator to the GitHub app so that it can generate
  error messages in PR comments
  • Loading branch information
ryscheng authored Jan 17, 2025
1 parent 786fc94 commit 9c5885d
Show file tree
Hide file tree
Showing 8 changed files with 869 additions and 316 deletions.
10 changes: 7 additions & 3 deletions lib/oss-artifact-validators/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"author": "Kariba Labs",
"license": "Apache-2.0",
"private": false,
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"type": "module",
"repository": {
"type": "git",
Expand All @@ -19,11 +19,15 @@
"build": "tsc",
"lint": "tsc --noEmit && pnpm lint:eslint && pnpm lint:prettier",
"lint:eslint": "eslint --ignore-path ../../.gitignore --max-warnings 0 .",
"lint:prettier": "prettier --ignore-path ../../.gitignore --log-level warn --check **/*.{js,jsx,ts,tsx,sol,md,json}"
"lint:prettier": "prettier --ignore-path ../../.gitignore --log-level warn --check **/*.{js,jsx,ts,tsx,sol,md,json}",
"test": "pnpm build && node --experimental-vm-modules node_modules/jest/bin/jest.js dist/"
},
"keywords": [],
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/node": "^20.14.10",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"typescript": "^5.5.3"
},
Expand Down
5 changes: 5 additions & 0 deletions lib/oss-artifact-validators/src/common/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
interface GenericValidator {
isValid(addr: string): Promise<boolean>;
}

export { GenericValidator };
1 change: 1 addition & 0 deletions lib/oss-artifact-validators/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./onchain/evm.js";
export * from "./web/defillama.js";
51 changes: 51 additions & 0 deletions lib/oss-artifact-validators/src/web/defillama.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { DefiLlamaValidator } from "./defillama.js";

const DEFILLAMA_API_TIMEOUT = 10000; // 10s

describe("sum module", () => {
let v: DefiLlamaValidator;
beforeEach(() => {
v = new DefiLlamaValidator();
});

test("isValidUrl", () => {
expect(v.isValidUrl("abc")).toBe(false);
expect(v.isValidUrl("https://www.opensource.observer")).toBe(false);
expect(v.isValidUrl("https://defillama.com/protocol")).toBe(false);
expect(v.isValidUrl("https://defillama.com/protocol/")).toBe(false);
expect(v.isValidUrl("https://defillama.com/protocol/slug")).toBe(true);
expect(v.isValidUrl("https://defillama.com/protocol/slug/")).toBe(true);
expect(v.isValidUrl("https://defillama.com/protocol/slug/extra")).toBe(
false,
);
});

test(
"isValidSlug",
async () => {
expect(await v.isValidSlug("INVALID-SLUG")).toBe(false);
expect(await v.isValidSlug("uniswap-v3")).toBe(true);
},
DEFILLAMA_API_TIMEOUT,
);

test(
"isValid",
async () => {
expect(await v.isValid("https://www.opensource.observer")).toBe(false);
expect(await v.isValid("https://defillama.com/protocol/slug")).toBe(
false,
);
expect(await v.isValid("https://defillama.com/protocol/uniswap-v3")).toBe(
true,
);
},
DEFILLAMA_API_TIMEOUT,
);

test("getSlug", () => {
expect(v.getSlug("https://defillama.com/protocol/uniswap-v3")).toBe(
"uniswap-v3",
);
});
});
88 changes: 88 additions & 0 deletions lib/oss-artifact-validators/src/web/defillama.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { GenericValidator } from "../common/interfaces.js";

const DEFILLAMA_URL_PREFIX = "https://defillama.com/protocol/";
const DEFILLAMA_API_PROTOCOLS = "https://api.llama.fi/protocols";

class DefiLlamaValidator implements GenericValidator {
private _validSlugs: string[];

constructor() {
this._validSlugs = [];
}

private async _refreshValidSlugs() {
if (this._validSlugs.length > 0) {
return;
}
const protocolsResult = await fetch(DEFILLAMA_API_PROTOCOLS);
const protocols = await protocolsResult.json();
this._validSlugs = protocols
.map((p: any) => p.slug)
.filter((s: string) => !!s);
}

/**
* Naive implementation of getting the slug in a DefiLlama URL
* This can definitely be improved
* @param url
* @returns
*/
getPath(u: string): string {
const noTrailingSlash = u.endsWith("/") ? u.slice(0, -1) : u;
const noPrefix = noTrailingSlash.startsWith(DEFILLAMA_URL_PREFIX)
? noTrailingSlash.replace(DEFILLAMA_URL_PREFIX, "")
: noTrailingSlash;
return noPrefix;
}

/**
* Just check whether the URL is properly formed locally
*/
isValidUrl(addr: string): boolean {
if (!addr.startsWith(DEFILLAMA_URL_PREFIX)) {
//Invalid DefiLlama URL: URLs must begin with ${DEFILLAMA_URL_PREFIX}
return false;
}
const path = this.getPath(addr);
if (path.includes("/")) {
//Invalid DefiLlama URL: URL must point to root protocol address
return false;
}
return true;
}

/**
* Check whether the slug is valid by querying the DefiLlama API
*/
async isValidSlug(slug: string): Promise<boolean> {
await this._refreshValidSlugs();
const found = this._validSlugs.find((s: string) => s === slug);
return !!found;
}

/**
* Checks any arbitrary string to see if it is a valid DefiLlama URL
* @param addr
* @returns
*/
async isValid(addr: string): Promise<boolean> {
const validUrl = this.isValidUrl(addr);
const slug = this.getPath(addr);
const validSlug = await this.isValidSlug(slug);
return validUrl && validSlug;
}

/**
* Get the slug from a DefiLlama URL
*/
getSlug(addr: string): string {
const validUrl = this.isValidUrl(addr);
if (!validUrl) {
throw new Error("Invalid DefiLlama URL");
}
const slug = this.getPath(addr);
return slug;
}
}

export { DefiLlamaValidator };
2 changes: 0 additions & 2 deletions lib/oss-artifact-validators/test/test.ts

This file was deleted.

115 changes: 45 additions & 70 deletions ops/external-prs/src/ossd/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import * as repl from "repl";
import columnify from "columnify";
import { BigQueryOptions } from "@google-cloud/bigquery";
import {
DefiLlamaValidator,
EVMNetworkValidator,
EthereumValidator,
ArbitrumValidator,
Expand Down Expand Up @@ -304,7 +305,10 @@ class OSSDirectoryPullRequest {
private db: duckdb.Database;
private args: OSSDirectoryPullRequestArgs;
private changes: ChangeSummary;
private validators: Partial<Record<BlockchainNetwork, EVMNetworkValidator>>;
private blockchainValidators: Partial<
Record<BlockchainNetwork, EVMNetworkValidator>
>;
private defillamaValidator: DefiLlamaValidator;

static async init(args: OSSDirectoryPullRequestArgs) {
const pr = new OSSDirectoryPullRequest(args);
Expand All @@ -314,35 +318,36 @@ class OSSDirectoryPullRequest {

private constructor(args: OSSDirectoryPullRequestArgs) {
this.args = args;
this.validators = {};
this.blockchainValidators = {};
}

async loadValidators(urls: RpcUrlArgs) {
const googleProjectId = process.env.GOOGLE_PROJECT_ID;
const bqOptions: BigQueryOptions = {
...(googleProjectId ? { projectId: googleProjectId } : {}),
};
this.validators["any_evm"] = EthereumValidator({
this.defillamaValidator = new DefiLlamaValidator();
this.blockchainValidators["any_evm"] = EthereumValidator({
rpcUrl: urls.mainnetRpcUrl,
bqOptions,
});

this.validators["mainnet"] = EthereumValidator({
this.blockchainValidators["mainnet"] = EthereumValidator({
rpcUrl: urls.mainnetRpcUrl,
bqOptions,
});

this.validators["arbitrum_one"] = ArbitrumValidator({
this.blockchainValidators["arbitrum_one"] = ArbitrumValidator({
rpcUrl: urls.arbitrumRpcUrl,
bqOptions,
});

this.validators["base"] = BaseValidator({
this.blockchainValidators["base"] = BaseValidator({
rpcUrl: urls.baseRpcUrl,
bqOptions,
});

this.validators["optimism"] = OptimismValidator({
this.blockchainValidators["optimism"] = OptimismValidator({
rpcUrl: urls.optimismRpcUrl,
bqOptions,
});
Expand Down Expand Up @@ -623,7 +628,7 @@ class OSSDirectoryPullRequest {
const address = item.address;
for (const network of item.networks) {
const validator =
this.validators[uncheckedCast<BlockchainNetwork>(network)];
this.blockchainValidators[uncheckedCast<BlockchainNetwork>(network)];
if (!validator) {
results.addWarning(
`no automated validators exist on ${network} to check tags=[${item.tags}]. Please check manually.`,
Expand Down Expand Up @@ -694,69 +699,39 @@ class OSSDirectoryPullRequest {

for (const item of this.changes.artifacts.toValidate.defillama) {
console.log(item);
/**
const address = item.address;
for (const network of item.networks) {
const validator =
this.validators[uncheckedCast<BlockchainNetwork>(network)];
if (!validator) {
results.addWarning(
`no automated validators exist on ${network} to check tags=[${item.tags}]. Please check manually.`,
address,
{ network },
);
//throw new Error(`No validator found for network "${network}"`);
continue;
}
logger.info({
message: `validating address ${address} on ${network} for [${item.tags}]`,
address: address,
network: network,
tags: item.tags,
});
for (const rawTag of item.tags) {
const tag = uncheckedCast<BlockchainTag>(rawTag);
const genericChecker = async (fn: () => Promise<boolean>) => {
if (!(await fn())) {
results.addError(
`${address} is not a ${tag} on ${network}`,
address,
{ address, tag, network },
);
} else {
results.addSuccess(
`${address} is a '${tag}' on ${network}`,
address,
{ address, tag, network },
);
}
};
if (tag === "eoa") {
await genericChecker(() => validator.isEOA(address));
} else if (tag === "contract") {
if (network === "any_evm") {
results.addWarning(
`addresses with the 'contract' tag should enumerate all networks that it is deployed on, rather than use 'any_evm'`,
address,
{ address, tag, network },
);
} else {
await genericChecker(() => validator.isContract(address));
}
} else if (tag === "deployer") {
await genericChecker(() => validator.isDeployer(address));
} else {
results.addWarning(
`missing validator for ${tag} on ${network}`,
address,
{ tag, network },
);
}
}
const urlValue = item.url_value;
const urlType = item.url_type;
logger.info({
message: `validating DefiLlama ${urlValue}`,
url: urlValue,
type: urlType,
});
if (!this.defillamaValidator.isValidUrl(urlValue)) {
results.addError(
`${urlValue} is not a valid DefiLlama URL`,
urlValue,
item,
);
} else {
results.addSuccess(
`${urlValue} is a valid DefiLlama URL`,
urlValue,
item,
);
}
if (!(await this.defillamaValidator.isValid(urlValue))) {
results.addError(
`${urlValue} is not a valid DefiLlama slug`,
urlValue,
item,
);
} else {
results.addSuccess(
`${urlValue} is a valid DefiLlama slug`,
urlValue,
item,
);
}
*/
}

// Render the results to GitHub PR
Expand Down
Loading

0 comments on commit 9c5885d

Please sign in to comment.