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:
parent
5a7a041f12
commit
709a70652f
6
.gitignore
vendored
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",
|
||||||
|
20
scripts/compile-ajv-validators.js
Normal file
20
scripts/compile-ajv-validators.js
Normal file
@ -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
src/utils/__generated__/.gitkeep
generated
Normal file
42
src/utils/validateTokenList.test.ts
Normal file
42
src/utils/validateTokenList.test.ts
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)
|
||||||
|
})
|
||||||
|
})
|
61
src/utils/validateTokenList.ts
Normal file
61
src/utils/validateTokenList.ts
Normal file
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user