-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(permit): calculate permit info (#299)
* 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
1 parent
58f8cef
commit 0fdebef
Showing
15 changed files
with
5,950 additions
and
159 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,4 +3,9 @@ build | |
# MacOS | ||
.DS_Store | ||
# Intellij | ||
.idea | ||
.idea | ||
# Vscode | ||
.vscode | ||
# yalc | ||
.yalc | ||
yalc.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"semi": false, | ||
"singleQuote": true, | ||
"printWidth": 120 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 🏁`)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}, {}) | ||
} |
Oops, something went wrong.