Просмотр исходного кода

Run Github actions to validate new PRs (#71)

* validate format and dups

* turn off debug

* remove debug logs

* against main

* fetch depth

* resolve review comments
mpng 2 лет назад
Родитель
Сommit
bd45d6846c
9 измененных файлов с 345 добавлено и 0 удалено
  1. 29 0
      .github/workflows/validate-PR.yml
  2. 3 0
      .gitignore
  3. 28 0
      package.json
  4. 24 0
      src/get-validated.ts
  5. 55 0
      src/main.ts
  6. 64 0
      src/parse.ts
  7. 54 0
      src/types/types.ts
  8. 72 0
      src/validate.ts
  9. 16 0
      tsconfig.json

+ 29 - 0
.github/workflows/validate-PR.yml

@@ -0,0 +1,29 @@
+name: validate-PR
+
+on:
+  # push:
+  pull_request:
+  workflow_dispatch:
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3 # checkout the repository content to github runner
+        with:
+          fetch-depth: 0
+      - uses: actions/setup-node@v3
+
+      # - name: Dump GitHub context
+      #   id: github_context_step
+      #   run: echo '${{ toJSON(github) }}'
+
+      ## for local act testing
+      # - run: npm install -g yarn
+      
+      - run: yarn install
+
+      - name: Validate PR
+        run: yarn dev
+
+

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+node_modules
+.env
+dist

+ 28 - 0
package.json

@@ -0,0 +1,28 @@
+{
+  "name": "token-list",
+  "version": "1.0.0",
+  "main": "index.js",
+  "repository": "https://github.com/jup-ag/token-list.git",
+  "author": "raccoons",
+  "license": "MIT",
+  "scripts": {
+    "dev": "yarn build && node dist/main.js",
+    "build": "yarn tsc",
+    "format": "yarn prettier --write **/*.ts",
+    "format-check": "yarn prettier --check **/*.ts",
+    "lint": "eslint src/**/*.ts"
+  },
+  "dependencies": {
+    "@actions/core": "^1.10.0",
+    "@actions/exec": "^1.1.1",
+    "@actions/github": "^5.1.1",
+    "@solana/web3.js": "^1.73.2",
+    "node-fetch": "^2.6.6"
+  },
+  "devDependencies": {
+    "@types/node": "^18.13.0",
+    "@types/node-fetch": "^2.6.2",
+    "csv-parse": "^5.3.5",
+    "typescript": "^4.9.5"
+  }
+}

+ 24 - 0
src/get-validated.ts

@@ -0,0 +1,24 @@
+import { Token, ValidatedSet } from "./types/types";
+import fetch from "node-fetch";
+
+/** Read existing validated tokens into Set */
+export async function getValidated(): Promise<ValidatedSet> {
+  const names = new Set<string>();
+  const symbols = new Set<string>();
+  const mints = new Set<string>();
+  const logoURL = new Set<string>();
+
+  try {
+    const data = await fetch(`https://token.jup.ag/strict`)
+    const tokens = await data.json()
+    tokens.forEach((token: Token) => {
+      names.add(token.name);
+      symbols.add(token.symbol);
+      mints.add(token.address);
+      logoURL.add(token.logoURI)
+    });
+    return { names, symbols, mints, logoURL };
+  } catch (error: any) {
+    throw new Error("Failed to fetch validated tokens");
+  }
+}

+ 55 - 0
src/main.ts

@@ -0,0 +1,55 @@
+import * as core from "@actions/core";
+import { exec } from "@actions/exec";
+import { parseGitPatch } from "./parse";
+import { validateGitPatch } from "./validate";
+import { getValidated } from "./get-validated";
+import { ValidatedSet, ValidationError } from "./types/types";
+
+async function run(): Promise<void> {
+
+  let gitDiff = "";
+  let gitDiffError = "";
+
+  try {
+    await exec("git", ["diff", "origin/main", "-U0", "--color=never"], {
+      listeners: {
+        stdout: (data: Buffer) => {
+          gitDiff += data.toString();
+        },
+        stderr: (data: Buffer) => {
+          gitDiffError += data.toString();
+        },
+      },
+    });
+  } catch (error: any) {
+    core.setFailed(error.message);
+  }
+
+  if (gitDiffError) {
+    core.setFailed(gitDiffError);
+  }
+
+  // core.debug(`Git diff: ${gitDiff}`)
+
+  let validatedSet: ValidatedSet;
+  try {
+    validatedSet = await getValidated();
+
+    const errors: ValidationError[][] = []
+
+    parseGitPatch(gitDiff).forEach((patch) => {
+      const patchErrors = validateGitPatch(patch, validatedSet);
+      if (patchErrors && patchErrors.length > 0) {
+        errors.push(patchErrors);
+      }
+    });
+        
+    if (errors.length > 0) {
+      core.setFailed(errors.join(","));
+    }
+  } catch (error: any) {
+    core.setFailed(error.message);
+  }
+}
+
+run();

+ 64 - 0
src/parse.ts

@@ -0,0 +1,64 @@
+import {Patch} from './types/types';
+
+export function parseGitPatch(patch: string): Patch[] {
+  const lines = patch.split('\n');
+
+  let currentFiles: [string, string];
+  let currentPatch: Patch | undefined;
+  const patches: Patch[] = [];
+
+  // Need to parse this line by line
+  lines.forEach(line => {
+    const matches = line.match(/^diff --git a\/(.*?) b\/(.*)$/m);
+
+    if (matches) {
+      currentFiles = [matches[1], matches[2]];
+      return;
+    }
+
+    const patchMatches = line.match(
+      /^@@ -(\d+)(?:,|)(\d*) \+(\d+)(?:,|)(\d*) @@/
+    );
+
+    if (patchMatches) {
+      // push old patch
+      if (currentPatch) {
+        patches.push(currentPatch);
+      }
+
+      currentPatch = {
+        removed: {
+          file: currentFiles[0],
+          start: Number(patchMatches[1]),
+          end: Number(patchMatches[1]) + Number(patchMatches[2]),
+          lines: [],
+        },
+        added: {
+          file: currentFiles[1],
+          start: Number(patchMatches[3]),
+          end: Number(patchMatches[3]) + Number(patchMatches[4]),
+          lines: [],
+        },
+      };
+      return;
+    }
+
+    const contentMatches = line.match(/^(-|\+)(.*)$/);
+
+    if (contentMatches) {
+      // This can match `--- a/<file>` and `+++ b/<file>`, so ignore if no `currentPatch` object
+      if (!currentPatch) {
+        return;
+      }
+
+      const patchType = contentMatches[1] === '-' ? 'removed' : 'added';
+      currentPatch[patchType].lines.push(contentMatches[2]);
+    }
+  });
+
+  if (currentPatch) {
+    patches.push(currentPatch);
+  }
+
+  return patches;
+}

+ 54 - 0
src/types/types.ts

@@ -0,0 +1,54 @@
+type PatchObj = {
+  /**
+   * File where this patch occurs
+   */
+  file: string;
+
+  /**
+   * Starting line number of patch
+   */
+  start: number;
+
+  /**
+   * Ending line number of patch
+   */
+  end: number;
+
+  /**
+   * The patch contents
+   */
+  lines: string[];
+};
+
+export type Patch = {
+  removed: PatchObj;
+  added: PatchObj;
+};
+
+export type Token = {
+  name: string;
+  symbol: string;
+  address: string;
+  decimals:number;
+  logoURI: string;
+};
+
+export type ValidatedSet = {
+  mints: Set<string>;
+  names: Set<string>;
+  symbols: Set<string>;
+  logoURL: Set<string>;
+}
+
+export enum ValidationError {
+  UNRELATED_FILE = "Changes made to unrelated files",
+  UNRELATED_CODE = "Changes to unrelated code are not allowed",
+  MULTIPLE_TOKENS = "Only one token can be added at a time",
+  DUPLICATE_NAME = "Token name already exists",
+  DUPLICATE_SYMBOL = "Token symbol already exists",
+  DUPLICATE_MINT = "Mint already exists",
+  INVALID_MINT = "Invalid mint address, not on ed25519 curve",
+  INVALID_DECIMALS = "Invalid decimals",
+  INVALID_IMAGE_URL = "Invalid image URL",
+  INVALID_COMMUNITY_VALIDATED = "Invalid community validated"
+}

+ 72 - 0
src/validate.ts

@@ -0,0 +1,72 @@
+import { Patch, ValidatedSet, ValidationError } from "./types/types";
+import { PublicKey } from "@solana/web3.js";
+
+
+export const VALIDATED_TOKENS_FILE = "validated-tokens.csv";
+
+function isValidatedFile(file: string) {
+  return file === VALIDATED_TOKENS_FILE;
+}
+
+export function validateGitPatch(patch: Patch, validatedSet: ValidatedSet): ValidationError[] {
+  // console.log("Processing patch", patch);
+  const errors: ValidationError[] = [];
+
+  // Flag changes to unrelated files
+  if (
+    !isValidatedFile(patch.removed.file) ||
+    !isValidatedFile(patch.added.file)
+  ) {
+    // errors.push(ValidationError.UNRELATED_FILE);
+    // return errors;
+
+    //TODO: Put this back after this PR is merged
+    return []
+  }
+
+  // Flag removals
+  if (patch.removed.lines.length > 0) {
+    errors.push(ValidationError.UNRELATED_CODE);
+  }
+
+  // Flag multiple line additions
+  if (patch.added.lines.length > 1) {
+    errors.push(ValidationError.MULTIPLE_TOKENS);
+  }
+
+  const [tokenName, symbol, mint, decimals, imageURL, isCommunity] =
+    patch.added.lines[0].split(",");
+
+  // Flag duplicates
+  if (validatedSet.names.has(tokenName)) {
+      errors.push(ValidationError.DUPLICATE_NAME);
+  }
+  
+  if (validatedSet.symbols.has(symbol)) {
+      errors.push(ValidationError.DUPLICATE_SYMBOL);
+  }
+  
+  if (validatedSet.mints.has(mint)) {
+      errors.push(ValidationError.DUPLICATE_MINT);
+  }
+
+  // Flag invalid mint address
+  if(!PublicKey.isOnCurve(new PublicKey(mint))) {
+      errors.push(ValidationError.INVALID_MINT);
+  } 
+
+  if (isNaN(Number(decimals)) || Number(decimals) < 0 || Number(decimals) > 9) {
+    errors.push(ValidationError.INVALID_DECIMALS);
+  }
+
+  if (isCommunity !== "true") {
+    errors.push(ValidationError.INVALID_COMMUNITY_VALIDATED);
+  }
+
+  // TODO: match with onchain data
+  // ....
+  // ...
+
+  // console.log("Patch Errors", errors);
+  return errors;
+}

+ 16 - 0
tsconfig.json

@@ -0,0 +1,16 @@
+{
+    "compilerOptions": {
+      "outDir": "./dist",
+      "rootDir": "./src",
+      "target": "ES2015" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
+      "module": "commonjs" /* Specify what module code is generated. */,
+      "resolveJsonModule": true,
+      "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
+      "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
+      "strict": true /* Enable all strict type-checking options. */,
+      "skipLibCheck": true /* Skip type checkng all .d.ts files. */,
+      "allowJs": true,
+  },
+  "include": ["src/**/*"],
+  "exclude": ["node_modules"]
+}