feat: add tokenlist validation (#6504)

* feat: add tokenlist validation

* use alternative for spread operator

* maintain tokenlists version and use original ajv version

* bump ajv

* Revert "bump ajv"

This reverts commit b9d2dd61c61f4aeedc00faf034c5a6e89b0d28fc.

* rename vars in validator

* update gitignore

* nit fixes

* test

* add ^ back

* remove ^

* removed and readded ajv

* try require.resolve

* Revert "try require.resolve"

This reverts commit 62f58bcb7f02f1ead430a4c94ca3428a60d4d4c5.

* bump eslint-config

* yarn lock merge conflict

* bring back spread operator

* remove redundant lint ignore
This commit is contained in:
matteen 2023-05-09 16:34:15 -04:00 committed by GitHub
parent 5a7a041f12
commit 709a70652f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 137 additions and 9 deletions

6
.gitignore vendored

@ -8,8 +8,10 @@
/src/locales/**/en-US.po /src/locales/**/en-US.po
/src/locales/**/pseudo.po /src/locales/**/pseudo.po
# generated graphql types # generated files
/src/graphql/**/__generated__ /src/**/__generated__
# schema
schema.graphql schema.graphql
# dependencies # dependencies

@ -16,7 +16,8 @@
"i18n:extract": "lingui extract --locale en-US", "i18n:extract": "lingui extract --locale en-US",
"i18n:compile": "yarn i18n:extract && lingui compile", "i18n:compile": "yarn i18n:extract && lingui compile",
"i18n:pseudo": "lingui extract --locale pseudo && lingui compile", "i18n:pseudo": "lingui extract --locale pseudo && lingui compile",
"prepare": "yarn contracts:compile && yarn graphql:fetch && yarn graphql:generate && yarn i18n:compile", "ajv:compile": "node scripts/compile-ajv-validators.js",
"prepare": "yarn contracts:compile && yarn graphql:fetch && yarn graphql:generate && yarn i18n:compile && yarn ajv:compile",
"start": "craco start", "start": "craco start",
"build": "craco build", "build": "craco build",
"build:e2e": "REACT_APP_CSP_ALLOW_UNSAFE_EVAL=true REACT_APP_ADD_COVERAGE_INSTRUMENTATION=true craco build", "build:e2e": "REACT_APP_CSP_ALLOW_UNSAFE_EVAL=true REACT_APP_ADD_COVERAGE_INSTRUMENTATION=true craco build",
@ -96,8 +97,8 @@
"@types/ua-parser-js": "^0.7.35", "@types/ua-parser-js": "^0.7.35",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@types/wcag-contrast": "^3.0.0", "@types/wcag-contrast": "^3.0.0",
"@uniswap/eslint-config": "^1.2.0",
"@uniswap/default-token-list": "^9.4.0", "@uniswap/default-token-list": "^9.4.0",
"@uniswap/eslint-config": "^1.1.1",
"@vanilla-extract/babel-plugin": "^1.1.7", "@vanilla-extract/babel-plugin": "^1.1.7",
"@vanilla-extract/jest-transform": "^1.1.1", "@vanilla-extract/jest-transform": "^1.1.1",
"@vanilla-extract/webpack-plugin": "^2.1.11", "@vanilla-extract/webpack-plugin": "^2.1.11",
@ -190,6 +191,8 @@
"@web3-react/types": "^8.2.0", "@web3-react/types": "^8.2.0",
"@web3-react/url": "^8.2.0", "@web3-react/url": "^8.2.0",
"@web3-react/walletconnect": "^8.2.0", "@web3-react/walletconnect": "^8.2.0",
"ajv": "^8.11.0",
"ajv-formats": "^2.1.1",
"array.prototype.flat": "^1.2.4", "array.prototype.flat": "^1.2.4",
"array.prototype.flatmap": "^1.2.4", "array.prototype.flatmap": "^1.2.4",
"cids": "^1.0.0", "cids": "^1.0.0",

@ -0,0 +1,20 @@
/* eslint-env node */
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 tokenListAjv = new Ajv({ code: { source: true, esm: true } })
addFormats(tokenListAjv)
const validateTokenList = tokenListAjv.compile(schema)
let tokenListModuleCode = standaloneCode(tokenListAjv, validateTokenList)
fs.writeFileSync(path.join(__dirname, '../src/utils/__generated__/validateTokenList.js'), tokenListModuleCode)
const tokensAjv = new Ajv({ code: { source: true, esm: true } })
addFormats(tokensAjv)
const validateTokens = tokensAjv.compile({ ...schema, required: ['tokens'] })
let tokensModuleCode = standaloneCode(tokensAjv, validateTokens)
fs.writeFileSync(path.join(__dirname, '../src/utils/__generated__/validateTokens.js'), tokensModuleCode)

@ -1,8 +1,8 @@
import type { TokenList } from '@uniswap/token-lists' import type { TokenList } from '@uniswap/token-lists'
import { validateTokenList } from '@uniswap/widgets'
import contenthashToUri from 'lib/utils/contenthashToUri' import contenthashToUri from 'lib/utils/contenthashToUri'
import parseENSAddress from 'lib/utils/parseENSAddress' import parseENSAddress from 'lib/utils/parseENSAddress'
import uriToHttp from 'lib/utils/uriToHttp' import uriToHttp from 'lib/utils/uriToHttp'
import { validateTokenList } from 'utils/validateTokenList'
export const DEFAULT_TOKEN_LIST = 'https://gateway.ipfs.io/ipns/tokens.uniswap.org' export const DEFAULT_TOKEN_LIST = 'https://gateway.ipfs.io/ipns/tokens.uniswap.org'

0
src/utils/__generated__/.gitkeep generated Normal file

@ -0,0 +1,42 @@
import { TokenInfo } from '@uniswap/token-lists'
import { validateTokens } from './validateTokenList'
const INVALID_TOKEN: TokenInfo = {
name: 'Dai Stablecoin',
address: '0xD3ADB33F',
symbol: 'DAI',
decimals: 18,
chainId: 1,
}
const INLINE_TOKEN_LIST = [
{
name: 'Dai Stablecoin',
address: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
symbol: 'DAI',
decimals: 18,
chainId: 1,
logoURI:
'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png',
},
{
name: 'USDCoin',
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
symbol: 'USDC',
decimals: 6,
chainId: 1,
logoURI:
'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png',
},
]
describe('validateTokens', () => {
it('throws on invalid tokens', async () => {
await expect(validateTokens([INVALID_TOKEN])).rejects.toThrow(/^Tokens failed validation:.*address/)
})
it('validates the passed token info', async () => {
await expect(validateTokens(INLINE_TOKEN_LIST)).resolves.toBe(INLINE_TOKEN_LIST)
})
})

@ -0,0 +1,61 @@
import type { TokenInfo, TokenList } from '@uniswap/token-lists'
import type { ValidateFunction } from 'ajv'
enum ValidationSchema {
LIST = 'list',
TOKENS = 'tokens',
}
function getValidationErrors(validate: ValidateFunction | undefined): string {
return (
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('utils/__generated__/validateTokenList')
break
case ValidationSchema.TOKENS:
validatorImport = import('utils/__generated__/validateTokens')
break
default:
throw new Error('No validation function specified for token list schema')
}
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[]> {
try {
await validate(ValidationSchema.TOKENS, { tokens: json })
return json
} catch (error) {
throw new Error(`Tokens failed validation: ${error.message}`)
}
}
/**
* Validates a token list.
* @param json the TokenList to validate
*/
export async function validateTokenList(json: TokenList): Promise<TokenList> {
try {
await validate(ValidationSchema.LIST, json)
return json
} catch (error) {
throw new Error(`Token list failed validation: ${error.message}`)
}
}

@ -5172,10 +5172,10 @@
resolved "https://registry.yarnpkg.com/@uniswap/default-token-list/-/default-token-list-9.4.0.tgz#60c4d249b3be5d0123480e5a4fd092a1fd10ef0c" resolved "https://registry.yarnpkg.com/@uniswap/default-token-list/-/default-token-list-9.4.0.tgz#60c4d249b3be5d0123480e5a4fd092a1fd10ef0c"
integrity sha512-PsYsEPpFs0CIjWHel2FDPo6T/EkZ+RuH9NfX55FbMbZMTuw9TSQEfl8p1fYt6KAMWi0Zy/Mfqp16y+OiYJyoBA== integrity sha512-PsYsEPpFs0CIjWHel2FDPo6T/EkZ+RuH9NfX55FbMbZMTuw9TSQEfl8p1fYt6KAMWi0Zy/Mfqp16y+OiYJyoBA==
"@uniswap/eslint-config@^1.1.1": "@uniswap/eslint-config@^1.2.0":
version "1.1.1" version "1.2.0"
resolved "https://registry.yarnpkg.com/@uniswap/eslint-config/-/eslint-config-1.1.1.tgz#83d2633dad6507ca58bc11c8892035812059488e" resolved "https://registry.yarnpkg.com/@uniswap/eslint-config/-/eslint-config-1.2.0.tgz#1d9565c5ae5982fc0e3585b272e15b89b64b2db0"
integrity sha512-vND/oJTmGCxYpY0E+iXWZT0mJT21/Ryf48yLP0kyWNnXzB5WyJuqei2gHx3nr+HWwRNF6aEbToRr9jNh+J3uBg== integrity sha512-hxCmKRbCckQql+/ZKiZBpIm7btexTZ2SRq3lcg3MHDB3VurdIycKSIBGSyxJxAryMC2b83/SubiZ8wNCjP55sQ==
dependencies: dependencies:
"@rushstack/eslint-patch" "^1.2.0" "@rushstack/eslint-patch" "^1.2.0"
"@typescript-eslint/eslint-plugin" "^5.45.1" "@typescript-eslint/eslint-plugin" "^5.45.1"