Skip to content

Commit

Permalink
feat(permit): calculate permit info (#299)
Browse files Browse the repository at this point in the history
* chore: add temporary package - to be replaced with the correctly scoped one when available

* chore: add node support

* WIP etc etc

* wip 2

* chore: use actual permit-utils npm pkg

* chore: update .gitignore

* chore: update tsconfig

* chore: update imports

* chore: added commands for running fetcher for each network

* refactor: rename calculatePermitInfo to fetchPermitInfo

* chore: exit if chainId is not set

* chore: change to script folder on execution

* refactor: use imported fns from node:process

* chore: add documentation to script

* refactor: split code into smaller parts

* chore: checked against latest token list

* chore: add comment regarding const address

* refactor: use `.ts` file extensions instead of `.js`

* refactor: formatted files nicely

* fix: remove localhost yarnlock
  • Loading branch information
alfetopito authored Nov 3, 2023
1 parent 58f8cef commit 0fdebef
Show file tree
Hide file tree
Showing 15 changed files with 5,950 additions and 159 deletions.
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,9 @@ build
# MacOS
.DS_Store
# Intellij
.idea
.idea
# Vscode
.vscode
# yalc
.yalc
yalc.lock
5 changes: 5 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 120
}
14 changes: 11 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,27 @@
"public": "copyfiles src/public/*.json build/lists -f",
"workflowHelper": "python3 src/scripts/workflow_helper.py",
"validate": "ajv -s node_modules/@uniswap/token-lists/dist/tokenlist.schema.json -d src/public/CowSwap.json -c ajv-formats --errors text",
"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",
"test": "node --test"
},
"license": "(MIT OR Apache-2.0)",
"dependencies": {
"@cowprotocol/permit-utils": "0.0.1-RC.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",
"node-fetch": "^3.3.0"
"node-fetch": "^3.3.0",
"ts-node": "^10.9.1"
},
"devDependencies": {
"@types/node": "^18.11.11",
"copyfiles": "^2.4.1"
"@types/node": "^20.8.7",
"copyfiles": "^2.4.1",
"eslint": "^8.52.0",
"prettier": "^3.0.3",
"typescript": "^5.2.2"
}
}
13 changes: 13 additions & 0 deletions src/permitInfo/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { env } from 'node:process'
import { join } from 'node:path'

// CoW protocol contract address. Could be any address in theory for checking the token is permittable
export const SPENDER_ADDRESS = '0xC92E8bdf79f0507f65a392b0ab4667716BFE0110'

export const DEFAULT_RPC_URLS: Record<number, string> = {
1: 'https://mainnet.infura.io/v3/' + env.INFURA_API_KEY,
5: 'https://goerli.infura.io/v3/' + env.INFURA_API_KEY,
100: 'https://rpc.gnosischain.com',
}

export const BASE_PATH = join('..', 'public')
136 changes: 136 additions & 0 deletions src/permitInfo/fetchPermitInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/**
* Fetch permit info for given network
*
* ChainId is the required first cli argument
*
* TokenListPath is the optional second cli argument
* By default, checks against the respective default token list
* - `src/public/CowSwap.json` for mainnet and gnosis chain
* - `src/public/CowSwapGoerli.json` for goerli
*
* RpcUrl is the optional third cli argument
* By default, it'll use Infura for mainnet and goerli.
* In that case, `INFURA_API_KEY` env var must be set.
*
* Minimal example:
* $ ts-node fetchPermitInfo 100
*
* Minimal example using default INFURA rpc:
* $ INFURA_API_KEY=0000000...111 ts-node fetchPermitInfo 1
*
* With optional params
* $ ts-node fetchPermitInfo 1 otherTokenList.json https://my.rpc.endpoint
*
* @arg chainId - required, first positional argument
* @arg tokenListPath - optional, second positional argument
* @arg rpcUrl - optional, third positional argument
*/

import { getTokenPermitInfo, PermitInfo } from '@cowprotocol/permit-utils'
import * as path from 'node:path'
import { readFileSync, writeFileSync } from 'node:fs'
import { JsonRpcProvider } from '@ethersproject/providers'
import { argv, chdir, exit } from 'node:process'
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'

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

if (!chainId) {
console.error('ChainId is missing. Invoke the script with the chainId as the first parameter.')
exit(1)
}

// Change to script dir so relative paths work properly
chdir(path.dirname(scriptPath))

async function fetchPermitInfo(
chainId: number,
tokenListPath: string | undefined,
rpcUrl: string | undefined,
): Promise<void> {
// Load existing permitInfo.json file for given chainId
const permitInfoPath = path.join(BASE_PATH, `PermitInfo.${chainId}.json`)

let allPermitInfo: Record<string, PermitInfo> = {}

// Load existing permitInfo.json file for given chainId if it exists
try {
allPermitInfo = JSON.parse(readFileSync(permitInfoPath, 'utf8')) as Record<string, PermitInfo>
} catch (_) {
// File doesn't exist. It'll be created later on.
}

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

// Load tokens info from a token list
const tokens = getTokens(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)
})

// 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
}
})

try {
writeFileSync(permitInfoPath, JSON.stringify(sortPermitInfo(allPermitInfo), undefined, 2))
} catch (e) {
console.error(`Failed to write file ${permitInfoPath}`, e)
}
}

async function _fetchPermitInfo(
chainId: number,
provider: JsonRpcProvider,
token: Token,
existing: PermitInfo | undefined,
): 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`)
} 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]
}
} catch (e) {
// Ignore failures
console.info(`Failed ${token.symbol}:`, e)
}
}
}

// Execute the script
fetchPermitInfo(+chainId, tokenListPath, rpcUrl).then(() => console.info(`Done 🏁`))
42 changes: 18 additions & 24 deletions src/permitInfo/permitInfo.schema.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {describe, it} from 'node:test'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import Ajv from 'ajv'

import schema from './permitInfo.schema.json' assert {type: 'json'}

import schema from './permitInfo.schema.json' assert { type: 'json' }

describe('The permitInfo schema', () => {
it('should be valid', () => {
Expand All @@ -14,10 +13,9 @@ describe('The permitInfo schema', () => {
})

describe('Valid PermitInfo data', () => {

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

const ajv = new Ajv()
Expand All @@ -30,8 +28,8 @@ describe('Valid PermitInfo data', () => {
it('should be valid with `eip-2612` and no version', () => {
const data = {
'0x0000000000000000000000000000000000000000': {
type: 'eip-2612'
}
type: 'eip-2612',
},
}

const ajv = new Ajv()
Expand All @@ -46,7 +44,7 @@ describe('Valid PermitInfo data', () => {
'0x0000000000000000000000000000000000000000': {
type: 'eip-2612',
version: '1',
}
},
}

const ajv = new Ajv()
Expand All @@ -59,8 +57,8 @@ describe('Valid PermitInfo data', () => {
it('should be valid with `dai-like` and no version', () => {
const data = {
'0x0000000000000000000000000000000000000000': {
type: 'dai-like'
}
type: 'dai-like',
},
}

const ajv = new Ajv()
Expand All @@ -74,8 +72,8 @@ describe('Valid PermitInfo data', () => {
const data = {
'0x0000000000000000000000000000000000000000': {
type: 'dai-like',
version: "2"
}
version: '2',
},
}

const ajv = new Ajv()
Expand All @@ -92,7 +90,7 @@ describe('Invalid PermitInfo data', () => {
'0x0000000000000000000000000000000000000000': {
type: 'eip-2612',
version: 1,
}
},
}

const ajv = new Ajv()
Expand All @@ -106,8 +104,8 @@ describe('Invalid PermitInfo data', () => {
const data = {
'0x0000000000000000000000000000000000000000': {
type: 'eip-2612',
version: '1.1'
}
version: '1.1',
},
}

const ajv = new Ajv()
Expand All @@ -119,21 +117,19 @@ describe('Invalid PermitInfo data', () => {

it('should be invalid with non address key', () => {
const data = {
'not an address': false
'not an address': false,
}

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


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

it('should be invalid with `true` value', () => {

const data = {
'0x0000000000000000000000000000000000000000': true
'0x0000000000000000000000000000000000000000': true,
}

const ajv = new Ajv()
Expand All @@ -144,11 +140,10 @@ describe('Invalid PermitInfo data', () => {
})

it('should be invalid with non existent type', () => {

const data = {
'0x0000000000000000000000000000000000000000': {
type: 'non-existent'
}
type: 'non-existent',
},
}

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

})
})
6 changes: 6 additions & 0 deletions src/permitInfo/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type Token = {
address: string
name: string
chainId: number
symbol: string
}
18 changes: 18 additions & 0 deletions src/permitInfo/utils/getProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { JsonRpcProvider } from '@ethersproject/providers'
import { DEFAULT_RPC_URLS } from '../const.ts'
import { env } from 'node:process'
import { ethers } from 'ethers'

export function getProvider(chainId: number, rpcUrl: string | undefined): JsonRpcProvider {
const rpcEndpoint = rpcUrl ? rpcUrl : DEFAULT_RPC_URLS[chainId]

if (!rpcEndpoint) {
throw new Error(`No RPC found for network ${chainId}`)
}

if (!rpcUrl && (chainId === 1 || chainId === 5) && !env.INFURA_API_KEY) {
throw new Error(`INFURA_API_KEY is required`)
}

return new ethers.providers.JsonRpcProvider(rpcEndpoint)
}
12 changes: 12 additions & 0 deletions src/permitInfo/utils/getTokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { readFileSync } from 'node:fs'
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> {
const filePath = tokenListPath
? tokenListPath
: join(BASE_PATH, chainId === 5 ? 'CowSwapGoerli.json' : 'CowSwap.json')

return JSON.parse(readFileSync(filePath, 'utf-8')).tokens
}
23 changes: 23 additions & 0 deletions src/permitInfo/utils/sortPermitInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { PermitInfo } from '@cowprotocol/permit-utils'

export function sortPermitInfo(allPermitInfo: Record<string, PermitInfo>): Record<string, PermitInfo> {
// Create a new obj with the keys sorted
return Object.keys(allPermitInfo)
.sort((a, b) => {
const pa = allPermitInfo[a]
const pb = allPermitInfo[b]

// If either both or none have permit info, sort by key
if ((pa && pb) || (!pa && !pb)) {
return a > b ? 1 : -1
}
// Otherwise, tokens with permit info go in top
return pb ? 1 : -1
})
.reduce((acc, address) => {
// Create a new object with the keys in the sorted order
acc[address] = allPermitInfo[address]

return acc
}, {})
}
Loading

0 comments on commit 0fdebef

Please sign in to comment.