Skip to content

Commit

Permalink
feat(permit): with token name (#312)
Browse files Browse the repository at this point in the history
* feat: update schema

* chore: update unit tests to match new schema

* refactor: rename getTokens to getTokensFromTokenList

* feat: update all permits to new format

* feat: add option to recheck unsupported tokens

* chore: do not use name from token list

* refactor: simplify conditional

* feat: update sort fn

* chore: make name and symbol optional

* chore: fix conditional

* refactor: move getUnsupportedTokensFromPermitInfo to utils fn

* chore: use p-throttle for limiting how often the permit check is done and avoid rpc failures

* chore: remove unnecessary optional operators

* chore: update to latest permit-utils, which doesn't work as it's not published yet

* chore: 4 more permittable tokens found...

* chore: 2 more tokens...

* chore: add p-retry for retrying :)

* chore: retry on exceptions and connection errors

* chore: also retry on rpc connection failures

* feat: use published version of @cowprotocol/permit-utils

* chore: ran against https://tokens.honeyswap.org list

* Set the original name of the contracts: result from running the script

---------

Co-authored-by: Anxo Rodriguez <[email protected]>
  • Loading branch information
alfetopito and anxolin authored Dec 7, 2023
1 parent 4779ac4 commit 8f10869
Show file tree
Hide file tree
Showing 12 changed files with 5,815 additions and 1,290 deletions.
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,25 @@
"fetchPermitInfo:mainnet": "ts-node src/permitInfo/fetchPermitInfo.ts 1",
"fetchPermitInfo:gnosis": "ts-node src/permitInfo/fetchPermitInfo.ts 100",
"fetchPermitInfo:goerli": "ts-node src/permitInfo/fetchPermitInfo.ts 5",
"recheckPermitInfo:mainnet": "ts-node src/permitInfo/fetchPermitInfo.ts 1 '' '' true",
"recheckPermitInfo:gnosis": "ts-node src/permitInfo/fetchPermitInfo.ts 100 '' '' true",
"recheckPermitInfo:goerli": "ts-node src/permitInfo/fetchPermitInfo.ts 5 '' '' true",
"test": "node --test"
},
"license": "(MIT OR Apache-2.0)",
"dependencies": {
"@cowprotocol/permit-utils": "0.0.1-RC.1",
"@cowprotocol/permit-utils": "^0.0.1",
"@uniswap/token-lists": "^1.0.0-beta.33",
"ajv": "^8.12.0",
"ajv-cli": "^5.0.0",
"ajv-formats": "^2.1.1",
"axios": "^1.0.0",
"ts-node": "^10.9.1",
"exponential-backoff": "^3.1.1",
"lodash": "^4.17.21",
"node-fetch": "^3.3.0"
"node-fetch": "^3.3.0",
"p-retry": "^6.1.0",
"p-throttle": "^5.1.0",
"ts-node": "^10.9.1"
},
"devDependencies": {
"@types/node": "^20.8.7",
Expand Down
88 changes: 60 additions & 28 deletions src/permitInfo/fetchPermitInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,17 @@
* @arg chainId - required, first positional argument
* @arg tokenListPath - optional, second positional argument
* @arg rpcUrl - optional, third positional argument
* @arg recheckUnsupported - optional, fourth positional argument
*/

import { getTokenPermitInfo, PermitInfo } from '@cowprotocol/permit-utils'
import pThrottle from 'p-throttle'
import pRetry from 'p-retry'
import {
getTokenPermitInfo,
GetTokenPermitIntoResult,
isSupportedPermitInfo,
PermitInfo,
} from '@cowprotocol/permit-utils'
import * as path from 'node:path'
import { readFileSync, writeFileSync } from 'node:fs'
import { JsonRpcProvider } from '@ethersproject/providers'
Expand All @@ -35,11 +43,12 @@ import { BASE_PATH, SPENDER_ADDRESS } from './const.ts'
import { sortPermitInfo } from './utils/sortPermitInfo.ts'
import { getProvider } from './utils/getProvider.ts'
import { Token } from './types.ts'
import { getTokens } from './utils/getTokens.ts'
import { getTokensFromTokenList } from './utils/getTokensFromTokenList.ts'
import { getUnsupportedTokensFromPermitInfo } from './utils/getUnsupportedTokensFromPermitInfo.ts'

// TODO: maybe make the args nicer?
// Get args from cli: chainId, optional token lists path, optional rpcUrl
const [, scriptPath, chainId, tokenListPath, rpcUrl] = argv
// Get args from cli: chainId, optional token lists path, optional rpcUrl, optional recheckUnsupported flag
const [, scriptPath, chainId, tokenListPath, rpcUrl, recheckUnsupported] = argv

if (!chainId) {
console.error('ChainId is missing. Invoke the script with the chainId as the first parameter.')
Expand All @@ -49,10 +58,12 @@ if (!chainId) {
// Change to script dir so relative paths work properly
chdir(path.dirname(scriptPath))


async function fetchPermitInfo(
chainId: number,
tokenListPath: string | undefined,
rpcUrl: string | undefined,
recheckUnsupported: boolean = false,
): Promise<void> {
// Load existing permitInfo.json file for given chainId
const permitInfoPath = path.join(BASE_PATH, `PermitInfo.${chainId}.json`)
Expand All @@ -64,32 +75,41 @@ async function fetchPermitInfo(
allPermitInfo = JSON.parse(readFileSync(permitInfoPath, 'utf8')) as Record<string, PermitInfo>
} catch (_) {
// File doesn't exist. It'll be created later on.
if (recheckUnsupported) {
console.error('recheck option set without existing permitInfo. There is nothing to recheck')
exit(1)
}
}

// Build provider instance
const provider = getProvider(chainId, rpcUrl)

// Load tokens info from a token list
const tokens = getTokens(chainId, tokenListPath)
const tokens = recheckUnsupported
? getUnsupportedTokensFromPermitInfo(chainId, allPermitInfo)
: getTokensFromTokenList(chainId, tokenListPath)

// Create a list of promises to check all tokens
const fetchAllPermits = tokens.map((token) => {
const existingInfo = allPermitInfo[token.address.toLowerCase()]

return _fetchPermitInfo(chainId, provider, token, existingInfo)
return pRetry(async () => _fetchPermitInfo(chainId, provider, token, existingInfo, recheckUnsupported), {
retries: 3,
})
})

// Await for all of them to complete
const fetchedPermits = await Promise.allSettled(fetchAllPermits)

// Iterate over each result
fetchedPermits.forEach((result) => {
// Ignore failed or the ones where the value is falsy
if (result.status === 'fulfilled' && result.value) {
const [address, permitInfo] = result.value

// Store result
allPermitInfo[address] = permitInfo
} else if (result.status === 'rejected') {
console.log(`[fetchedPermits] Failed to fetch info:`, result.reason)
}
})

Expand All @@ -100,37 +120,49 @@ async function fetchPermitInfo(
}
}

// Fn can only be called 2x/second
const throttle = pThrottle({
limit: 2,
interval: 1000,
})

const throttledGetTokenPermitInfo = throttle(getTokenPermitInfo)

async function _fetchPermitInfo(
chainId: number,
provider: JsonRpcProvider,
token: Token,
existing: PermitInfo | undefined,
recheckUnsupported: boolean,
): Promise<undefined | [string, PermitInfo]> {
if (existing !== undefined) {
console.info(`Token ${token.symbol}: already known, skipping`, existing)
} else if (token.chainId !== chainId) {
console.info(`Token ${token.symbol}: belongs to a different network (${token.chainId}), skipping`)
const tokenId = token.symbol || token.name || token.address

if (token.chainId !== chainId) {
console.info(`Token ${tokenId}: belongs to a different network (${token.chainId}), skipping`)
} else if (isSupportedPermitInfo(existing) || (existing && !recheckUnsupported)) {
console.info(`Token ${tokenId}: already known, skipping`, existing)
} else {
try {
const response = await getTokenPermitInfo({
chainId,
provider,
spender: SPENDER_ADDRESS,
tokenAddress: token.address,
tokenName: token.name,
})
console.info(`Token ${token.symbol}:`, response)

// Ignore error responses
if (!(typeof response === 'object' && 'error' in response)) {
return [token.address.toLowerCase(), response]
const response: GetTokenPermitIntoResult = await throttledGetTokenPermitInfo({
chainId,
provider,
spender: SPENDER_ADDRESS,
tokenAddress: token.address,
tokenName: token.name,
})

if ('error' in response) {
if (/ETIMEDOUT|RPC connection error/.test(response.error)) {
// Throw, so it can be retried on connection errors
throw new Error(response.error)
}
} catch (e) {
// Ignore failures
console.info(`Failed ${token.symbol}:`, e)
// Non connection related error, stop it here
console.info(`Non-retryable failure for token ${tokenId}:`, response)
} else {
console.info(`Token ${tokenId}:`, response)
return [token.address.toLowerCase(), response]
}
}
}

// Execute the script
fetchPermitInfo(+chainId, tokenListPath, rpcUrl).then(() => console.info(`Done 🏁`))
fetchPermitInfo(+chainId, tokenListPath, rpcUrl, !!recheckUnsupported).then(() => console.info(`Done 🏁`))
49 changes: 23 additions & 26 deletions src/permitInfo/permitInfo.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,32 @@
"type": "object",
"patternProperties": {
"^0x[a-fA-F0-9]{40}$": {
"oneOf": [
{
"type": "object",
"title": "PermitInfo",
"description": "Individual permit info when a token is known to be permittable",
"properties": {
"version": {
"type": "string",
"description": "Optional version number, as a string",
"pattern": "^\\d+$"
},
"type": {
"type": "string",
"description": "Type of permit",
"enum": [
"eip-2612",
"dai-like"
]
}
},
"required": [
"type"
"type": "object",
"title": "PermitInfo",
"description": "Individual permit info when a token is known to be permittable",
"properties": {
"version": {
"type": "string",
"description": "Optional version, natural number > 0, as a string",
"pattern": "^\\d+$"
},
"type": {
"type": "string",
"description": "Type of permit",
"enum": [
"unsupported",
"eip-2612",
"dai-like"
]
},
{
"type": "boolean",
"description": "When a token is known to not be permittable",
"const": false
"name": {
"type": "string",
"description": "Token name as defined in the contract"
}
},
"required": [
"type",
"name"
]
}
},
Expand Down
41 changes: 39 additions & 2 deletions src/permitInfo/permitInfo.schema.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ describe('The permitInfo schema', () => {
})

describe('Valid PermitInfo data', () => {
it('should be valid with `false` value', () => {
it('should be valid with `unsupported` type', () => {
const data = {
'0x0000000000000000000000000000000000000000': false,
'0x0000000000000000000000000000000000000000': {
type: 'unsupported',
name: 'tokenName'
},
}

const ajv = new Ajv()
Expand All @@ -29,6 +32,7 @@ describe('Valid PermitInfo data', () => {
const data = {
'0x0000000000000000000000000000000000000000': {
type: 'eip-2612',
name: 'tokenName'
},
}

Expand All @@ -44,6 +48,7 @@ describe('Valid PermitInfo data', () => {
'0x0000000000000000000000000000000000000000': {
type: 'eip-2612',
version: '1',
name: 'tokenName'
},
}

Expand All @@ -58,6 +63,7 @@ describe('Valid PermitInfo data', () => {
const data = {
'0x0000000000000000000000000000000000000000': {
type: 'dai-like',
name: 'tokenName'
},
}

Expand All @@ -73,6 +79,7 @@ describe('Valid PermitInfo data', () => {
'0x0000000000000000000000000000000000000000': {
type: 'dai-like',
version: '2',
name: 'tokenName'
},
}

Expand All @@ -90,6 +97,7 @@ describe('Invalid PermitInfo data', () => {
'0x0000000000000000000000000000000000000000': {
type: 'eip-2612',
version: 1,
name: 'tokenName'
},
}

Expand All @@ -105,6 +113,21 @@ describe('Invalid PermitInfo data', () => {
'0x0000000000000000000000000000000000000000': {
type: 'eip-2612',
version: '1.1',
name: 'tokenName'
},
}

const ajv = new Ajv()
const result = ajv.validate(schema, data)

assert.strictEqual(result, false)
assert.notEqual(ajv.errors, null)
})

it('should be invalid without `name`', () => {
const data = {
'0x0000000000000000000000000000000000000000': {
type: 'eip-2612',
},
}

Expand Down Expand Up @@ -143,6 +166,7 @@ describe('Invalid PermitInfo data', () => {
const data = {
'0x0000000000000000000000000000000000000000': {
type: 'non-existent',
name: 'tokenName'
},
}

Expand All @@ -152,4 +176,17 @@ describe('Invalid PermitInfo data', () => {
assert.strictEqual(result, false)
assert.notEqual(ajv.errors, null)
})


it('should be invalid with `false` value', () => {
const data = {
'0x0000000000000000000000000000000000000000': false,
}

const ajv = new Ajv()
const result = ajv.validate(schema, data)

assert.strictEqual(result, false)
assert.notEqual(ajv.errors, null)
})
})
4 changes: 2 additions & 2 deletions src/permitInfo/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type Token = {
address: string
name: string
chainId: number
symbol: string
name?: string
symbol?: string
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { BASE_PATH } from '../const.ts'
import { Token } from '../types.ts'
import { join } from 'node:path'

export function getTokens(chainId: number, tokenListPath: string | undefined): Array<Token> {
export function getTokensFromTokenList(chainId: number, tokenListPath: string | undefined): Array<Token> {
const filePath = tokenListPath
? tokenListPath
: join(BASE_PATH, chainId === 5 ? 'CowSwapGoerli.json' : 'CowSwap.json')
Expand Down
17 changes: 17 additions & 0 deletions src/permitInfo/utils/getUnsupportedTokensFromPermitInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { isSupportedPermitInfo, PermitInfo } from '@cowprotocol/permit-utils'
import { Token } from '../types.js'

export function getUnsupportedTokensFromPermitInfo(
chainId: number,
allPermitInfo: Record<string, PermitInfo>,
): Token[] {
const tokens = []

for (const [k, v] of Object.entries(allPermitInfo)) {
if (!isSupportedPermitInfo(v)) {
tokens.push({ address: k, name: v?.name, chainId })
}
}

return tokens
}
Loading

0 comments on commit 8f10869

Please sign in to comment.