Skip to content

Commit

Permalink
feat: precompile ajv validation (Uniswap#273)
Browse files Browse the repository at this point in the history
* wip: build validator functions before runtime

* set esm to true

* make sure folder sticks around

* eslint

* PR feedback

* return the right values

* update error message

* style(lint): lint action with ESLint

* make formats a prod dep not dev dep

* export functions

Co-authored-by: Lint Action <[email protected]>
  • Loading branch information
cmcewen and Lint Action authored Oct 13, 2022
1 parent cae0743 commit 69cd486
Show file tree
Hide file tree
Showing 9 changed files with 77 additions and 35 deletions.
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,8 @@ yarn-error.log*
notes.txt

# tests
/coverage
/coverage

# generated ajv schema
/src/__generated__/*
!/src/__generated__/.gitkeep
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"i18n:extract": "lingui extract --locale en-US",
"i18n:compile": "yarn i18n:extract && lingui compile",
"i18n:pseudo": "lingui extract --locale pseudo && lingui compile",
"prepare": "yarn contracts:compile && yarn i18n:compile",
"ajv:compile": "node scripts/compileAjvValidators.js",
"prepare": "yarn contracts:compile && yarn i18n:compile && yarn ajv:compile",
"prepublishOnly": "yarn build",
"start": "cosmos",
"typecheck": "tsc -p tsconfig.json",
Expand Down Expand Up @@ -77,7 +78,8 @@
"@web3-react/types": "8.0.20-beta.0",
"@web3-react/url": "8.0.25-beta.0",
"@web3-react/walletconnect": "8.0.35-beta.0",
"ajv": "^6.12.3",
"ajv": "^8.11.0",
"ajv-formats": "^2.1.1",
"cids": "^1.0.0",
"ethers": "^5.6.1",
"immer": "^9.0.6",
Expand Down
20 changes: 20 additions & 0 deletions scripts/compileAjvValidators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* eslint-disable */

const fs = require('fs')
const path = require('path')
const Ajv = require('ajv')
const standaloneCode = require('ajv/dist/standalone').default
const addFormats = require('ajv-formats')
const schema = require('@uniswap/token-lists/dist/tokenlist.schema.json')

const ajv = new Ajv({ code: { source: true, esm: true } })
addFormats(ajv)
const validate = ajv.compile(schema)
let moduleCode = standaloneCode(ajv, validate)
fs.writeFileSync(path.join(__dirname, '../src/__generated__/validateTokenList.js'), moduleCode)

const ajv2 = new Ajv({ code: { source: true, esm: true } })
addFormats(ajv2)
const validate2 = ajv2.compile({ ...schema, required: ['tokens'] })
let moduleCode2 = standaloneCode(ajv2, validate2)
fs.writeFileSync(path.join(__dirname, '../src/__generated__/validateTokens.js'), moduleCode2)
Empty file added src/__generated__/.gitkeep
Empty file.
2 changes: 1 addition & 1 deletion src/hooks/useTokenList/fetchTokenList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import contenthashToUri from 'utils/contenthashToUri'
import parseENSAddress from 'utils/parseENSAddress'
import uriToHttp from 'utils/uriToHttp'

import validateTokenList from './validateTokenList'
import { validateTokenList } from './validateTokenList'

const listCache = new Map<string, TokenList>()

Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useTokenList/validateTokenList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const INLINE_TOKEN_LIST = [

describe('validateTokens', () => {
it('throws on invalid tokens', async () => {
await expect(validateTokens([INVALID_TOKEN])).rejects.toThrowError(/^Token list failed validation:.*address/)
await expect(validateTokens([INVALID_TOKEN])).rejects.toThrowError(/^Tokens failed validation:.*address/)
})

it('validates the passed token info', async () => {
Expand Down
58 changes: 33 additions & 25 deletions src/hooks/useTokenList/validateTokenList.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,62 @@
import type { TokenInfo, TokenList } from '@uniswap/token-lists'
import type { Ajv, ValidateFunction } from 'ajv'
import type { ValidateFunction } from 'ajv'

enum ValidationSchema {
LIST = 'list',
TOKENS = 'tokens',
}

const validator = new Promise<Ajv>(async (resolve) => {
const [ajv, schema] = await Promise.all([import('ajv'), import('@uniswap/token-lists/src/tokenlist.schema.json')])
const validator = new ajv.default({ allErrors: true })
.addSchema(schema, ValidationSchema.LIST)
// Adds a meta scheme of Pick<TokenList, 'tokens'>
.addSchema(
{
...schema,
$id: schema.$id + '#tokens',
required: ['tokens'],
},
ValidationSchema.TOKENS
)
resolve(validator)
})

function getValidationErrors(validate: ValidateFunction | undefined): string {
return (
validate?.errors?.map((error) => [error.dataPath, error.message].filter(Boolean).join(' ')).join('; ') ??
validate?.errors?.map((error) => [error.instancePath, error.message].filter(Boolean).join(' ')).join('; ') ??
'unknown error'
)
}

async function validate(schema: ValidationSchema, data: unknown): Promise<unknown> {
let validatorImport
switch (schema) {
case ValidationSchema.LIST:
validatorImport = import('__generated__/validateTokenList')
break
case ValidationSchema.TOKENS:
validatorImport = import('__generated__/validateTokens')
break
default:
throw new Error('No validation function specified for schema')
break
}

const [, validatorModule] = await Promise.all([import('ajv'), validatorImport])
const validator = (await validatorModule.default) as ValidateFunction
if (validator?.(data)) {
return data
}
throw new Error(getValidationErrors(validator))
}

/**
* Validates an array of tokens.
* @param json the TokenInfo[] to validate
*/
export async function validateTokens(json: TokenInfo[]): Promise<TokenInfo[]> {
const validate = (await validator).getSchema(ValidationSchema.TOKENS)
if (validate?.({ tokens: json })) {
try {
await validate(ValidationSchema.TOKENS, { tokens: json })
return json
} catch (err) {
throw new Error(`Tokens failed validation: ${err.message}`)
}
throw new Error(`Token list failed validation: ${getValidationErrors(validate)}`)
}

/**
* Validates a token list.
* @param json the TokenList to validate
*/
export default async function validateTokenList(json: TokenList): Promise<TokenList> {
const validate = (await validator).getSchema(ValidationSchema.LIST)
if (validate?.(json)) {
export async function validateTokenList(json: TokenList): Promise<TokenList> {
try {
await validate(ValidationSchema.LIST, json)
return json
} catch (err) {
throw new Error(`Token list failed validation: ${err.message}`)
}
throw new Error(`Token list failed validation: ${getValidationErrors(validate)}`)
}
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type {
WidgetEventHandlers,
} from 'hooks/useSyncWidgetEventHandlers'
export { EMPTY_TOKEN_LIST, UNISWAP_TOKEN_LIST } from 'hooks/useTokenList'
export { validateTokenList, validateTokens } from 'hooks/useTokenList/validateTokenList'
export type { JsonRpcConnectionMap } from 'hooks/web3/useJsonRpcUrlsMap'
export type {
OnAmountChange,
Expand Down
17 changes: 12 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4272,12 +4272,19 @@ aggregate-error@^3.0.0:
clean-stack "^2.0.0"
indent-string "^4.0.0"

ajv-formats@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520"
integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==
dependencies:
ajv "^8.0.0"

ajv-keywords@^3.5.2:
version "3.5.2"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==

ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5:
ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5:
version "6.12.6"
resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
Expand All @@ -4287,10 +4294,10 @@ ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"

ajv@^8.0.1:
version "8.6.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.2.tgz#2fb45e0e5fcbc0813326c1c3da535d1881bb0571"
integrity sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==
ajv@^8.0.0, ajv@^8.0.1, ajv@^8.11.0:
version "8.11.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f"
integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==
dependencies:
fast-deep-equal "^3.1.1"
json-schema-traverse "^1.0.0"
Expand Down

0 comments on commit 69cd486

Please sign in to comment.