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/**/pseudo.po
|
||||
|
||||
# generated graphql types
|
||||
/src/graphql/**/__generated__
|
||||
# generated files
|
||||
/src/**/__generated__
|
||||
|
||||
# schema
|
||||
schema.graphql
|
||||
|
||||
# dependencies
|
||||
|
@ -16,7 +16,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 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",
|
||||
"build": "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/uuid": "^8.3.4",
|
||||
"@types/wcag-contrast": "^3.0.0",
|
||||
"@uniswap/eslint-config": "^1.2.0",
|
||||
"@uniswap/default-token-list": "^9.4.0",
|
||||
"@uniswap/eslint-config": "^1.1.1",
|
||||
"@vanilla-extract/babel-plugin": "^1.1.7",
|
||||
"@vanilla-extract/jest-transform": "^1.1.1",
|
||||
"@vanilla-extract/webpack-plugin": "^2.1.11",
|
||||
@ -190,6 +191,8 @@
|
||||
"@web3-react/types": "^8.2.0",
|
||||
"@web3-react/url": "^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.flatmap": "^1.2.4",
|
||||
"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 { validateTokenList } from '@uniswap/widgets'
|
||||
import contenthashToUri from 'lib/utils/contenthashToUri'
|
||||
import parseENSAddress from 'lib/utils/parseENSAddress'
|
||||
import uriToHttp from 'lib/utils/uriToHttp'
|
||||
import { validateTokenList } from 'utils/validateTokenList'
|
||||
|
||||
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"
|
||||
integrity sha512-PsYsEPpFs0CIjWHel2FDPo6T/EkZ+RuH9NfX55FbMbZMTuw9TSQEfl8p1fYt6KAMWi0Zy/Mfqp16y+OiYJyoBA==
|
||||
|
||||
"@uniswap/eslint-config@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/eslint-config/-/eslint-config-1.1.1.tgz#83d2633dad6507ca58bc11c8892035812059488e"
|
||||
integrity sha512-vND/oJTmGCxYpY0E+iXWZT0mJT21/Ryf48yLP0kyWNnXzB5WyJuqei2gHx3nr+HWwRNF6aEbToRr9jNh+J3uBg==
|
||||
"@uniswap/eslint-config@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/eslint-config/-/eslint-config-1.2.0.tgz#1d9565c5ae5982fc0e3585b272e15b89b64b2db0"
|
||||
integrity sha512-hxCmKRbCckQql+/ZKiZBpIm7btexTZ2SRq3lcg3MHDB3VurdIycKSIBGSyxJxAryMC2b83/SubiZ8wNCjP55sQ==
|
||||
dependencies:
|
||||
"@rushstack/eslint-patch" "^1.2.0"
|
||||
"@typescript-eslint/eslint-plugin" "^5.45.1"
|
||||
|
Loading…
Reference in New Issue
Block a user