├── .env.sample ├── .eslintrc.js ├── .github ├── scripts │ └── generateMatrix.ts └── workflows │ ├── ci.yml │ ├── cron.yml │ ├── generate-token-lists.yml │ └── pr-title-check.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.js ├── __test__ ├── integration │ └── validateLists.test.ts └── unit │ ├── getVersion.test.ts │ ├── getVersionMockup.ts │ ├── schema │ ├── arbify.tokenlist.json │ ├── arblistDecimalsTooHigh.tokenlist.json │ ├── arblistDecimalsTooLow.tokenlist.json │ ├── arblistNameTooLong.tokenlist.json │ ├── arblistSymbolTooLong.tokenlist.json │ ├── arblistWrongAddress.tokenlist.json │ ├── arblistWrongChainId.tokenlist.json │ ├── arblistWrongVersion.tokenlist.json │ └── uniswap.tokenlist.json │ ├── token_list_gen.test.ts │ ├── utils.test.ts │ └── validateTokenList.test.ts ├── jest.config.js ├── package.json ├── readme.md ├── src ├── Assets │ ├── 42161_arbitrum_native_token_list.json │ ├── coingecko_uris.json │ └── logo_uris.json ├── PermitTokens │ ├── daiPermitTokenAbi.json │ ├── multicallAbi.json │ ├── permitSignature.ts │ └── permitTokenAbi.json ├── WarningList │ └── warningTokens.json ├── commands │ ├── allTokensList.ts │ ├── arbify.ts │ ├── full.ts │ └── update.ts ├── customNetworks.ts ├── init.ts ├── lib │ ├── constants.ts │ ├── getVersion.ts │ ├── graph.ts │ ├── instantiate_bridge.ts │ ├── options.ts │ ├── store.ts │ ├── token_list_gen.ts │ ├── types.ts │ ├── utils.ts │ └── validateTokenList.ts ├── main.ts ├── scripts │ └── fetchOrbitChainsData.ts └── setupTests.ts ├── tsconfig.json ├── update_all └── yarn.lock /.env.sample: -------------------------------------------------------------------------------- 1 | INFURA_KEY= 2 | ARB_ONE_RPC="https://arbitrum.infura.io/v3/${INFURA_KEY}" 3 | ARB_SEPOLIA_RPC="https://arbitrum-sepolia.infura.io/v3/${INFURA_KEY}" 4 | MAINNET_RPC="https://mainnet.infura.io/v3/${INFURA_KEY}" 5 | SEPOLIA_RPC="https://sepolia.infura.io/v3/${INFURA_KEY}" 6 | HOLESKY_RPC="https://holesky.infura.io/v3/${INFURA_KEY}" 7 | ARB_NOVA_RPC="https://nova.arbitrum.io/rpc" 8 | BASE_RPC="https://base-mainnet.infura.io/v3/${INFURA_KEY}" 9 | BASE_SEPOLIA_RPC="https://base-sepolia.infura.io/v3/${INFURA_KEY}" 10 | L2_GATEWAY_SUBGRAPH_URL= 11 | L2_GATEWAY_SEPOLIA_SUBGRAPH_URL= 12 | l2NetworkID=42161 13 | PORT=3000 -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | ignorePatterns: ['dist'], 8 | extends: ['@offchainlabs/eslint-config-typescript/base'], 9 | parserOptions: { 10 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 11 | sourceType: 'module', // Allows for the use of imports 12 | }, 13 | rules: { 14 | '@typescript-eslint/no-unused-vars': [ 15 | 'error', 16 | { ignoreRestSiblings: true }, 17 | ], 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /.github/scripts/generateMatrix.ts: -------------------------------------------------------------------------------- 1 | import { customNetworks } from '../../src/customNetworks'; 2 | 3 | type Command = { 4 | name: string; 5 | paths: string[]; 6 | version: boolean; 7 | command: string; 8 | }; 9 | 10 | const arbitrumCommands: Command[] = [ 11 | // Arb1 12 | { 13 | name: 'Arb1 FullList', 14 | paths: ['ArbTokenLists/arbed_full.json'], 15 | version: false, 16 | command: 17 | 'yarn fullList --l2NetworkID 42161 --newArbifiedList ./src/ArbTokenLists/arbed_full.json --skipValidation', 18 | }, 19 | { 20 | name: 'Arb1 Arbify Uniswap', 21 | paths: [ 22 | 'ArbTokenLists/arbed_uniswap_labs.json', 23 | 'ArbTokenLists/arbed_uniswap_labs_default.json', 24 | ], 25 | version: true, 26 | command: 27 | 'yarn arbify --l2NetworkID 42161 --prevArbifiedList https://tokenlist.arbitrum.io/ArbTokenLists/arbed_uniswap_labs.json --tokenList https://tokens.uniswap.org --newArbifiedList ./src/ArbTokenLists/arbed_uniswap_labs.json && cp ./src/ArbTokenLists/arbed_uniswap_labs.json ./src/ArbTokenLists/arbed_uniswap_labs_default.json', 28 | }, 29 | { 30 | name: 'Arb1 Arbify CMC', 31 | paths: ['ArbTokenLists/arbed_coinmarketcap.json'], 32 | version: true, 33 | command: 34 | 'yarn arbify --l2NetworkID 42161 --prevArbifiedList https://tokenlist.arbitrum.io/ArbTokenLists/arbed_coinmarketcap.json --tokenList https://api.coinmarketcap.com/data-api/v3/uniswap/all.json --newArbifiedList ./src/ArbTokenLists/arbed_coinmarketcap.json', 35 | }, 36 | { 37 | name: 'Arb1 Arbify CoinGecko', 38 | paths: ['ArbTokenLists/arbed_coingecko.json'], 39 | version: true, 40 | command: 41 | 'yarn arbify --l2NetworkID 42161 --prevArbifiedList https://tokenlist.arbitrum.io/ArbTokenLists/arbed_coingecko.json --tokenList https://tokens.coingecko.com/uniswap/all.json --newArbifiedList ./src/ArbTokenLists/arbed_coingecko.json', 42 | }, 43 | { 44 | name: 'Arb1 Update Whitelist', 45 | paths: ['ArbTokenLists/arbed_arb_whitelist_era.json'], 46 | version: true, 47 | command: 48 | 'yarn update --l2NetworkID 42161 --prevArbifiedList https://tokenlist.arbitrum.io/ArbTokenLists/arbed_arb_whitelist_era.json --tokenList https://tokenlist.arbitrum.io/ArbTokenLists/arbed_arb_whitelist_era.json --includeOldDataFields true --newArbifiedList ./src/ArbTokenLists/arbed_arb_whitelist_era.json', 49 | }, 50 | // Arb Nova 51 | { 52 | name: 'ArbNova Arbify Uniswap', 53 | paths: [ 54 | 'ArbTokenLists/42170_arbed_uniswap_labs.json', 55 | 'ArbTokenLists/42170_arbed_uniswap_labs_default.json', 56 | ], 57 | version: true, 58 | command: 59 | 'yarn arbify --l2NetworkID 42170 --prevArbifiedList https://tokenlist.arbitrum.io/ArbTokenLists/42170_arbed_uniswap_labs_default.json --newArbifiedList ./src/ArbTokenLists/42170_arbed_uniswap_labs.json --tokenList https://tokens.uniswap.org && cp ./src/ArbTokenLists/42170_arbed_uniswap_labs.json ./src/ArbTokenLists/42170_arbed_uniswap_labs_default.json', 60 | }, 61 | { 62 | name: 'ArbNova Arbify CMC', 63 | paths: ['ArbTokenLists/42170_arbed_coinmarketcap.json'], 64 | version: true, 65 | command: 66 | 'yarn arbify --l2NetworkID 42170 --prevArbifiedList https://tokenlist.arbitrum.io/ArbTokenLists/42170_arbed_coinmarketcap.json --tokenList https://api.coinmarketcap.com/data-api/v3/uniswap/all.json --newArbifiedList ./src/ArbTokenLists/42170_arbed_coinmarketcap.json', 67 | }, 68 | { 69 | name: 'ArbNova Arbify CoinGecko', 70 | paths: ['ArbTokenLists/42170_arbed_coingecko.json'], 71 | version: true, 72 | command: 73 | 'yarn arbify --l2NetworkID 42170 --prevArbifiedList https://tokenlist.arbitrum.io/ArbTokenLists/42170_arbed_coingecko.json --tokenList https://tokens.coingecko.com/uniswap/all.json --newArbifiedList ./src/ArbTokenLists/42170_arbed_coingecko.json', 74 | }, 75 | // ArbSepolia 76 | { 77 | name: 'ArbSepolia Arbify Uniswap', 78 | paths: ['ArbTokenLists/421614_arbed_uniswap_labs.json'], 79 | version: true, 80 | command: 81 | 'yarn arbify --l2NetworkID 421614 --prevArbifiedList https://tokenlist.arbitrum.io/ArbTokenLists/421614_arbed_uniswap_labs.json --tokenList https://tokens.uniswap.org --newArbifiedList ./src/ArbTokenLists/421614_arbed_uniswap_labs.json', 82 | }, 83 | { 84 | name: 'ArbSepolia Arbify CoinGecko', 85 | paths: ['ArbTokenLists/421614_arbed_coingecko.json'], 86 | version: true, 87 | command: 88 | 'yarn arbify --l2NetworkID 421614 --prevArbifiedList https://tokenlist.arbitrum.io/ArbTokenLists/421614_arbed_coingecko.json --tokenList https://tokens.coingecko.com/uniswap/all.json --newArbifiedList ./src/ArbTokenLists/421614_arbed_coingecko.json', 89 | }, 90 | ]; 91 | 92 | const orbitCommands: Command[] = []; 93 | 94 | async function addCommand({ 95 | chainId, 96 | name, 97 | path, 98 | inputList, 99 | }: { 100 | chainId: number; 101 | name: string; 102 | path: string; 103 | inputList: string; 104 | }): Promise { 105 | const url = `https://tokenlist.arbitrum.io/${path}`; 106 | const requiresFirstTimeGeneration = await fetch(url) 107 | .then((response) => response.json()) 108 | .then(() => false) 109 | .catch(() => true); 110 | 111 | const previousListFlag = requiresFirstTimeGeneration 112 | ? '--ignorePreviousList' 113 | : `--prevArbifiedList ${url}`; 114 | 115 | return { 116 | name, 117 | paths: [path], 118 | version: true, 119 | command: `yarn arbify --l2NetworkID ${chainId} ${previousListFlag} --tokenList ${inputList} --newArbifiedList ./src/${path}`, 120 | }; 121 | } 122 | 123 | function getUniswapTokenListFromParentChainId(chainId: number) { 124 | return { 125 | // L1 126 | 1: 'https://tokens.uniswap.org', 127 | 11155111: 'https://tokens.uniswap.org', 128 | 17000: 'https://tokens.uniswap.org', 129 | // Arbitrum 130 | 42161: 131 | 'https://tokenlist.arbitrum.io/ArbTokenLists/arbed_uniswap_labs.json', 132 | 42170: 133 | 'https://tokenlist.arbitrum.io/ArbTokenLists/42170_arbed_uniswap_labs.json', 134 | 421614: 135 | 'https://tokenlist.arbitrum.io/ArbTokenLists/421614_arbed_uniswap_labs.json', 136 | // Base 137 | 8453: 'https://tokenlist.arbitrum.io/ArbTokenLists/8453_uniswap_labs.json', 138 | 84532: 139 | 'https://tokenlist.arbitrum.io/ArbTokenLists/84532_uniswap_labs.json', 140 | }[chainId]; 141 | } 142 | (async () => { 143 | for (let { name, chainId, parentChainId } of customNetworks) { 144 | const inputUniswapTokenList = 145 | getUniswapTokenListFromParentChainId(parentChainId); 146 | 147 | if (!inputUniswapTokenList) { 148 | throw new Error( 149 | `Uniswap token list on parent chain doesn't exist for ${name} (${chainId})`, 150 | ); 151 | } 152 | 153 | orbitCommands.push( 154 | await addCommand({ 155 | name: `${name} Arbify Uniswap`, 156 | chainId, 157 | path: `ArbTokenLists/${chainId}_arbed_uniswap_labs.json`, 158 | inputList: inputUniswapTokenList, 159 | }), 160 | ); 161 | 162 | // For L3 settling on ArbOne, generate arbified native token list 163 | if (parentChainId === 42161) { 164 | orbitCommands.push( 165 | await addCommand({ 166 | name: `${name} Arbify L2 native list`, 167 | chainId, 168 | path: `ArbTokenLists/${chainId}_arbed_native_list.json`, 169 | inputList: `./src/Assets/${parentChainId}_arbitrum_native_token_list.json`, 170 | }), 171 | ); 172 | } 173 | } 174 | 175 | const matrix: Record<'include', Command[]> = { 176 | include: arbitrumCommands.concat(orbitCommands), 177 | }; 178 | 179 | console.log(JSON.stringify(matrix, null, 0)); 180 | })(); 181 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: ['master'] 6 | push: 7 | branches: ['master'] 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | env: 12 | INFURA_KEY: '${{ secrets.INFURA_KEY }}' 13 | MAINNET_RPC: 'https://mainnet.infura.io/v3/${{ secrets.INFURA_KEY }}' 14 | ARB_ONE_RPC: 'https://arbitrum-mainnet.infura.io/v3/${{ secrets.INFURA_KEY }}' 15 | ARB_SEPOLIA_RPC: 'https://arbitrum-sepolia.infura.io/v3/${{ secrets.INFURA_KEY }}' 16 | ARB_NOVA_RPC: 'https://nova.arbitrum.io/rpc' 17 | 18 | jobs: 19 | check-formatting: 20 | name: 'Check Formatting' 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | 26 | - name: Install node_modules 27 | uses: OffchainLabs/actions/node-modules/install@main 28 | 29 | - name: Generate orbit chains data 30 | run: yarn generate:orbitChainsData 31 | 32 | - name: Check formatting with Prettier 33 | run: yarn prettier:check 34 | 35 | integration-test: 36 | name: 'Integration tests' 37 | runs-on: ubuntu-latest 38 | environment: CI 39 | env: 40 | INFURA_KEY: '${{ secrets.INFURA_KEY }}' 41 | MAINNET_RPC: 'https://mainnet.infura.io/v3/${{ secrets.INFURA_KEY }}' 42 | SEPOLIA_RPC: 'https://sepolia.infura.io/v3/${{ secrets.INFURA_KEY }}' 43 | L2_GATEWAY_SUBGRAPH_URL: '${{ secrets.L2_GATEWAY_SUBGRAPH_URL }}' 44 | L2_GATEWAY_SEPOLIA_SUBGRAPH_URL: '${{ secrets.L2_GATEWAY_SEPOLIA_SUBGRAPH_URL }}' 45 | 46 | steps: 47 | - name: Checkout 48 | uses: actions/checkout@v4 49 | 50 | - name: Install node_modules 51 | uses: OffchainLabs/actions/node-modules/install@main 52 | 53 | - name: Generate orbit chains data 54 | run: yarn generate:orbitChainsData 55 | 56 | - name: Test 57 | run: yarn test:integration 58 | 59 | unit-test: 60 | name: 'Unit tests' 61 | runs-on: ubuntu-latest 62 | environment: CI 63 | env: 64 | INFURA_KEY: '${{ secrets.INFURA_KEY }}' 65 | MAINNET_RPC: 'https://mainnet.infura.io/v3/${{ secrets.INFURA_KEY }}' 66 | SEPOLIA_RPC: 'https://sepolia.infura.io/v3/${{ secrets.INFURA_KEY }}' 67 | L2_GATEWAY_SUBGRAPH_URL: '${{ secrets.L2_GATEWAY_SUBGRAPH_URL }}' 68 | L2_GATEWAY_SEPOLIA_SUBGRAPH_URL: '${{ secrets.L2_GATEWAY_SEPOLIA_SUBGRAPH_URL }}' 69 | 70 | steps: 71 | - name: Checkout 72 | uses: actions/checkout@v4 73 | 74 | - name: Install node_modules 75 | uses: OffchainLabs/actions/node-modules/install@main 76 | 77 | - name: Generate orbit chains data 78 | run: yarn generate:orbitChainsData 79 | 80 | - name: Test 81 | run: yarn test:unit 82 | 83 | generate-token-lists: 84 | uses: ./.github/workflows/generate-token-lists.yml 85 | with: 86 | environment: 'Test' 87 | secrets: inherit 88 | -------------------------------------------------------------------------------- /.github/workflows/cron.yml: -------------------------------------------------------------------------------- 1 | name: Cron 2 | 3 | on: 4 | schedule: 5 | - cron: '0/10 * * * *' 6 | 7 | jobs: 8 | generate-token-lists: 9 | uses: ./.github/workflows/generate-token-lists.yml 10 | with: 11 | environment: 'CI' 12 | secrets: inherit 13 | -------------------------------------------------------------------------------- /.github/workflows/generate-token-lists.yml: -------------------------------------------------------------------------------- 1 | name: Generate token lists 2 | 3 | on: 4 | # Allows you to run this workflow manually from the Actions tab 5 | workflow_dispatch: 6 | inputs: 7 | environment: 8 | description: 'Environment to run generation' 9 | type: environment 10 | default: Test 11 | required: true 12 | workflow_call: 13 | inputs: 14 | environment: 15 | description: 'Environment to run generation' 16 | type: string 17 | required: true 18 | default: 'Test' 19 | 20 | jobs: 21 | generate-matrix: 22 | runs-on: ubuntu-latest 23 | env: 24 | INFURA_KEY: '${{ secrets.INFURA_KEY }}' 25 | MAINNET_RPC: 'https://mainnet.infura.io/v3/${{ secrets.INFURA_KEY }}' 26 | ARB_ONE_RPC: 'https://arbitrum-mainnet.infura.io/v3/${{ secrets.INFURA_KEY }}' 27 | ARB_SEPOLIA_RPC: 'https://arbitrum-sepolia.infura.io/v3/${{ secrets.INFURA_KEY }}' 28 | ARB_NOVA_RPC: 'https://nova.arbitrum.io/rpc' 29 | outputs: 30 | matrix: ${{ steps.set-matrix.outputs.matrix }} 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | 35 | - name: Install node_modules 36 | uses: OffchainLabs/actions/node-modules/install@main 37 | 38 | - name: Generate orbit chains data 39 | run: yarn generate:orbitChainsData 40 | 41 | - id: set-matrix 42 | run: echo "matrix=$(npm run generate:matrix --silent)" >> $GITHUB_OUTPUT 43 | 44 | generate-token-lists: 45 | name: 'Generate' 46 | runs-on: ubuntu-latest 47 | environment: ${{ inputs.environment }} 48 | needs: [generate-matrix] 49 | permissions: 50 | id-token: write # need this for OIDC 51 | contents: read # This is required for actions/checkout@v2 52 | strategy: 53 | max-parallel: 5 54 | fail-fast: false 55 | matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }} 56 | 57 | env: 58 | INFURA_KEY: '${{ secrets.INFURA_KEY }}' 59 | MAINNET_RPC: 'https://mainnet.infura.io/v3/${{ secrets.INFURA_KEY }}' 60 | SEPOLIA_RPC: 'https://sepolia.infura.io/v3/${{ secrets.INFURA_KEY }}' 61 | ARB_ONE_RPC: 'https://arbitrum-mainnet.infura.io/v3/${{ secrets.INFURA_KEY }}' 62 | HOLESKY_RPC: 'https://holesky.infura.io/v3/${{ secrets.INFURA_KEY }}' 63 | ARB_SEPOLIA_RPC: 'https://arbitrum-sepolia.infura.io/v3/${{ secrets.INFURA_KEY }}' 64 | ARB_NOVA_RPC: 'https://nova.arbitrum.io/rpc' 65 | BASE_RPC: 'https://base-mainnet.infura.io/v3/${{ secrets.INFURA_KEY }}' 66 | BASE_SEPOLIA_RPC: 'https://base-sepolia.infura.io/v3/${{ secrets.INFURA_KEY }}' 67 | L2_GATEWAY_SUBGRAPH_URL: '${{ secrets.L2_GATEWAY_SUBGRAPH_URL }}' 68 | L2_GATEWAY_SEPOLIA_SUBGRAPH_URL: '${{ secrets.L2_GATEWAY_SEPOLIA_SUBGRAPH_URL }}' 69 | steps: 70 | - name: Checkout 71 | uses: actions/checkout@v4 72 | 73 | - name: Install node_modules 74 | uses: OffchainLabs/actions/node-modules/install@main 75 | 76 | - name: Generate orbit chains data 77 | run: yarn generate:orbitChainsData 78 | 79 | - name: Configure AWS credentials 80 | uses: aws-actions/configure-aws-credentials@v1-node16 81 | with: 82 | aws-region: 'us-west-2' 83 | aws-access-key-id: ${{ secrets.AWS_KEY_ID }} 84 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 85 | 86 | - name: ${{ matrix.name }} 87 | if: success() 88 | run: ${{ matrix.command }} 89 | 90 | - name: Get online version 91 | id: onlineVersion 92 | if: ${{ matrix.version == true && matrix.version == true }} 93 | run: | 94 | # Check if the list exists online (it will not exist on the first run) 95 | if curl --silent --head --fail "https://tokenlist.arbitrum.io/${{ matrix.paths[0] }}"; then 96 | # Get the version from the online list (formatted to major.minor.patch) 97 | version=$(curl https://tokenlist.arbitrum.io/${{ matrix.paths[0] }} | jq .version | jq 'join(".")') 98 | if [[ -n $version ]]; then 99 | echo "onlineVersion=$version" >> $GITHUB_OUTPUT 100 | else 101 | # Make sure failure from curl or jq fails the generation 102 | exit 1 103 | fi 104 | else 105 | # Only applies when a new list is added 106 | echo "onlineVersion=1.0.0" >> $GITHUB_OUTPUT 107 | fi 108 | 109 | - name: Backup 110 | if: ${{ matrix.version == true }} 111 | run: | 112 | paths=(${{ join(matrix.paths, ' ') }}) 113 | for path in ${paths[*]} 114 | do 115 | if [[ "${{ inputs.environment }}" == "Test" ]] 116 | then 117 | additionalPath='TestFolder/' 118 | else 119 | additionalPath='' 120 | fi 121 | 122 | # Backup online list to {version}/{path} before deploying a new one 123 | lines=$(aws s3 ls s3://${{ secrets.AWS_BUCKET }}/$additionalPath$path | wc -l) 124 | if (( $lines > 0 )); then 125 | backupCommand="aws s3 cp s3://${{ secrets.AWS_BUCKET }}/$additionalPath$path s3://${{ secrets.AWS_BUCKET }}/$additionalPath" 126 | backupCommand+=$(echo $path | awk -F'.json' '{print $1}') # Remove .json 127 | backupCommand+=/${{ steps.onlineVersion.outputs.onlineVersion }}.json 128 | $backupCommand 129 | fi 130 | done 131 | 132 | - name: Deploy (Test folder) 133 | if: ${{ inputs.environment == 'Test' }} 134 | run: aws s3 sync ./src/ s3://${{ secrets.AWS_BUCKET }}/TestFolder --exclude "*" --include "FullList/*.json" --include "ArbTokenLists/*.json" 135 | 136 | - name: Deploy 137 | if: ${{ inputs.environment == 'CI' }} 138 | run: aws s3 sync ./src/ s3://${{ secrets.AWS_BUCKET }} --exclude "*" --include "FullList/*.json" --include "ArbTokenLists/*.json" --acl "public-read" 139 | 140 | error-alerts: 141 | runs-on: ubuntu-latest 142 | environment: ${{ inputs.environment }} 143 | needs: [generate-token-lists] 144 | # Run this job if any of the job in needs array failed 145 | if: ${{ always() && contains(needs.*.result, 'failure') }} 146 | steps: 147 | - name: Post errors to Slack channel 148 | uses: slackapi/slack-github-action@v1.23.0 149 | with: 150 | channel-id: ${{ secrets.SLACK_CHANNEL_ID }} 151 | payload: | 152 | { 153 | "blocks": [ 154 | { 155 | "type": "section", 156 | "text": { 157 | "type": "mrkdwn", 158 | "text": "Token list generation failed" 159 | } 160 | }, 161 | { 162 | "type": "divider" 163 | }, 164 | { 165 | "type": "section", 166 | "text": { 167 | "type": "mrkdwn", 168 | "text": "${{ github.event.repository.html_url }}/actions/runs/${{ github.run_id }}" 169 | } 170 | } 171 | ] 172 | } 173 | env: 174 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} 175 | -------------------------------------------------------------------------------- /.github/workflows/pr-title-check.yml: -------------------------------------------------------------------------------- 1 | name: 'PR Title Check' 2 | # PR title is checked according to https://www.conventionalcommits.org/en/v1.0.0/ 3 | 4 | on: 5 | pull_request_target: 6 | types: 7 | - opened 8 | - edited 9 | - synchronize 10 | 11 | jobs: 12 | main: 13 | name: Validate PR title 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: amannn/action-semantic-pull-request@v5 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | with: 20 | subjectPattern: '^.{0,50}$' 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | src/ArbTokenLists/* 4 | src/FullList/* 5 | dist 6 | !src/ArbTokenLists/arbed_arb_whitelist_era.json 7 | 8 | 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | src/Assets/orbitChainsData.json -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | src/ArbTokenLists/* -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('@offchainlabs/prettier-config'), 3 | }; 4 | -------------------------------------------------------------------------------- /__test__/integration/validateLists.test.ts: -------------------------------------------------------------------------------- 1 | import { yargsInstance } from '../../src/main'; 2 | import { handler as handlerAllTokensList } from '../../src/commands/allTokensList'; 3 | import { handler as handlerArbify } from '../../src/commands/arbify'; 4 | import { handler as handlerFull } from '../../src/commands/full'; 5 | import { handler as handlerUpdate } from '../../src/commands/update'; 6 | import { Action, Args } from '../../src/lib/options'; 7 | import { ArbTokenList, EtherscanList } from '../../src/lib/types'; 8 | 9 | const handlers: { 10 | [action in Action]?: (argv: Args) => Promise; 11 | } = { 12 | [Action.AllTokensList]: handlerAllTokensList, 13 | [Action.Arbify]: handlerArbify, 14 | [Action.Full]: handlerFull, 15 | [Action.Update]: handlerUpdate, 16 | }; 17 | const runCommand = async (command: Action, options: string[]) => { 18 | const argv = await yargsInstance.parseAsync(['_', command, ...options]); 19 | return handlers[command]!(argv); 20 | }; 21 | const compareLists = ( 22 | l1: ArbTokenList | EtherscanList, 23 | l2: ArbTokenList | EtherscanList, 24 | ) => { 25 | // Both lists are EtherscanList 26 | if ('timestamp' in l1 && 'timestamp' in l2) { 27 | const { timestamp: t1, version: v1, tags: tags1, ...list1 } = l1; 28 | const { timestamp: t2, version: v2, tags: tags2, ...list2 } = l2; 29 | /** 30 | * Lists are stored using JSON.stringify which removes property with undefined values 31 | * We use stringify then parse here to get the same list 32 | */ 33 | return expect(list1).toMatchObject(list2); 34 | } 35 | 36 | return expect(JSON.parse(JSON.stringify(l1))).toMatchObject(l2); 37 | }; 38 | 39 | // check for top-level duplicate token (i.e. same adddress on the same chain) 40 | const findDuplicateTokens = (arbTokenList: ArbTokenList) => { 41 | const appearanceCount: { 42 | [asdf: string]: number; 43 | } = {}; 44 | 45 | arbTokenList.tokens.forEach((token) => { 46 | const uniqueID = `${token.address},,${token.chainId}`; 47 | if (appearanceCount[uniqueID]) { 48 | appearanceCount[uniqueID]++; 49 | } else { 50 | appearanceCount[uniqueID] = 1; 51 | } 52 | }); 53 | return Object.keys(appearanceCount).filter((uniqueID) => { 54 | return appearanceCount[uniqueID] > 1; 55 | }); 56 | }; 57 | 58 | const testNoDuplicates = (arbTokenList: ArbTokenList) => { 59 | const dups = findDuplicateTokens(arbTokenList); 60 | expect(dups).toMatchObject([]); 61 | }; 62 | 63 | describe('Token Lists', () => { 64 | jest.setTimeout(200_000); 65 | 66 | describe('Arbify token lists', () => { 67 | describe('Arb1', () => { 68 | it('Uniswap', async () => { 69 | expect.assertions(2); 70 | const [localList, onlineList] = await Promise.all([ 71 | runCommand(Action.Arbify, [ 72 | '--l2NetworkID=42161', 73 | '--tokenList=https://tokens.uniswap.org', 74 | '--prevArbifiedList=https://tokenlist.arbitrum.io/ArbTokenLists/arbed_uniswap_labs.json', 75 | '--newArbifiedList=./src/ArbTokenLists/arbed_uniswap_labs.json', 76 | ]), 77 | fetch( 78 | 'https://tokenlist.arbitrum.io/ArbTokenLists/arbed_uniswap_labs.json', 79 | ).then((response) => response.json()), 80 | ]); 81 | 82 | testNoDuplicates(localList as ArbTokenList); 83 | compareLists(localList, onlineList); 84 | }); 85 | 86 | it.skip('Gemini', async () => { 87 | expect.assertions(2); 88 | const [localList, onlineList] = await Promise.all([ 89 | runCommand(Action.Arbify, [ 90 | '--l2NetworkID=42161', 91 | '--tokenList=https://www.gemini.com/uniswap/manifest.json', 92 | '--prevArbifiedList=https://tokenlist.arbitrum.io/ArbTokenLists/arbed_gemini_token_list.json', 93 | '--newArbifiedList=./src/ArbTokenLists/arbed_gemini_token_list.json', 94 | ]), 95 | fetch( 96 | 'https://tokenlist.arbitrum.io/ArbTokenLists/arbed_gemini_token_list.json', 97 | ).then((response) => response.json()), 98 | ]); 99 | 100 | testNoDuplicates(localList as ArbTokenList); 101 | compareLists(localList, onlineList); 102 | }); 103 | 104 | it('CMC', async () => { 105 | expect.assertions(2); 106 | const [localList, onlineList] = await Promise.all([ 107 | runCommand(Action.Arbify, [ 108 | '--l2NetworkID=42161', 109 | '--tokenList=https://api.coinmarketcap.com/data-api/v3/uniswap/all.json', 110 | '--prevArbifiedList=https://tokenlist.arbitrum.io/ArbTokenLists/arbed_coinmarketcap.json', 111 | '--newArbifiedList=./src/ArbTokenLists/arbed_coinmarketcap.json', 112 | ]), 113 | fetch( 114 | 'https://tokenlist.arbitrum.io/ArbTokenLists/arbed_coinmarketcap.json', 115 | ).then((response) => response.json()), 116 | ]); 117 | 118 | testNoDuplicates(localList as ArbTokenList); 119 | compareLists(localList, onlineList); 120 | }); 121 | 122 | it('CoinGecko', async () => { 123 | expect.assertions(2); 124 | const [localList, onlineList] = await Promise.all([ 125 | runCommand(Action.Arbify, [ 126 | '--l2NetworkID=42161', 127 | '--tokenList=https://tokens.coingecko.com/uniswap/all.json', 128 | '--prevArbifiedList=https://tokenlist.arbitrum.io/ArbTokenLists/arbed_coingecko.json', 129 | '--newArbifiedList=./src/ArbTokenLists/arbed_coingecko.json', 130 | ]), 131 | fetch( 132 | 'https://tokenlist.arbitrum.io/ArbTokenLists/arbed_coingecko.json', 133 | ).then((response) => response.json()), 134 | ]); 135 | 136 | testNoDuplicates(localList as ArbTokenList); 137 | compareLists(localList, onlineList); 138 | }); 139 | }); 140 | 141 | describe('Arb Nova', () => { 142 | it('Uniswap', async () => { 143 | expect.assertions(2); 144 | const [localList, onlineList] = await Promise.all([ 145 | runCommand(Action.Arbify, [ 146 | '--l2NetworkID=42170', 147 | '--tokenList=https://tokens.uniswap.org', 148 | '--prevArbifiedList=https://tokenlist.arbitrum.io/ArbTokenLists/42170_arbed_uniswap_labs.json', 149 | '--newArbifiedList=./src/ArbTokenLists/42170_arbed_uniswap_labs.json', 150 | ]), 151 | fetch( 152 | 'https://tokenlist.arbitrum.io/ArbTokenLists/42170_arbed_uniswap_labs.json', 153 | ).then((response) => response.json()), 154 | ]); 155 | 156 | testNoDuplicates(localList as ArbTokenList); 157 | compareLists(localList, onlineList); 158 | }); 159 | 160 | it.skip('Gemini', async () => { 161 | expect.assertions(2); 162 | const [localList, onlineList] = await Promise.all([ 163 | runCommand(Action.Arbify, [ 164 | '--l2NetworkID=42170', 165 | '--tokenList=https://www.gemini.com/uniswap/manifest.json', 166 | '--prevArbifiedList=https://tokenlist.arbitrum.io/ArbTokenLists/42170_arbed_gemini_token_list.json', 167 | '--newArbifiedList=./src/ArbTokenLists/42170_arbed_gemini_token_list.json', 168 | ]), 169 | fetch( 170 | 'https://tokenlist.arbitrum.io/ArbTokenLists/42170_arbed_gemini_token_list.json', 171 | ).then((response) => response.json()), 172 | ]); 173 | 174 | testNoDuplicates(localList as ArbTokenList); 175 | compareLists(localList, onlineList); 176 | }); 177 | 178 | it('CMC', async () => { 179 | expect.assertions(2); 180 | const [localList, onlineList] = await Promise.all([ 181 | runCommand(Action.Arbify, [ 182 | '--l2NetworkID=42170', 183 | '--tokenList=https://api.coinmarketcap.com/data-api/v3/uniswap/all.json', 184 | '--prevArbifiedList=https://tokenlist.arbitrum.io/ArbTokenLists/42170_arbed_coinmarketcap.json', 185 | '--newArbifiedList=./src/ArbTokenLists/42170_arbed_coinmarketcap.json', 186 | ]), 187 | fetch( 188 | 'https://tokenlist.arbitrum.io/ArbTokenLists/42170_arbed_coinmarketcap.json', 189 | ).then((response) => response.json()), 190 | ]); 191 | 192 | testNoDuplicates(localList as ArbTokenList); 193 | compareLists(localList, onlineList); 194 | }); 195 | 196 | it('CoinGecko', async () => { 197 | expect.assertions(2); 198 | const [localList, onlineList] = await Promise.all([ 199 | runCommand(Action.Arbify, [ 200 | '--l2NetworkID=42170', 201 | '--tokenList=https://tokens.coingecko.com/uniswap/all.json', 202 | '--prevArbifiedList=https://tokenlist.arbitrum.io/ArbTokenLists/42170_arbed_coingecko.json', 203 | '--newArbifiedList=./src/ArbTokenLists/42170_arbed_coingecko.json', 204 | ]), 205 | fetch( 206 | 'https://tokenlist.arbitrum.io/ArbTokenLists/42170_arbed_coingecko.json', 207 | ).then((response) => response.json()), 208 | ]); 209 | 210 | testNoDuplicates(localList as ArbTokenList); 211 | compareLists(localList, onlineList); 212 | }); 213 | }); 214 | 215 | describe('Arb Sepolia', () => { 216 | it('Uniswap', async () => { 217 | expect.assertions(2); 218 | const [localList, onlineList] = await Promise.all([ 219 | runCommand(Action.Arbify, [ 220 | '--l2NetworkID=421614', 221 | '--tokenList=https://tokens.uniswap.org', 222 | '--prevArbifiedList=https://tokenlist.arbitrum.io/ArbTokenLists/421614_arbed_uniswap_labs.json', 223 | '--newArbifiedList=./src/ArbTokenLists/421614_arbed_uniswap_labs.json', 224 | ]), 225 | fetch( 226 | 'https://tokenlist.arbitrum.io/ArbTokenLists/421614_arbed_uniswap_labs.json', 227 | ).then((response) => response.json()), 228 | ]); 229 | 230 | testNoDuplicates(localList as ArbTokenList); 231 | compareLists(localList, onlineList); 232 | }); 233 | 234 | it('CoinGecko', async () => { 235 | expect.assertions(2); 236 | const [localList, onlineList] = await Promise.all([ 237 | runCommand(Action.Arbify, [ 238 | '--l2NetworkID=421614', 239 | '--tokenList=https://tokens.coingecko.com/uniswap/all.json', 240 | '--prevArbifiedList=https://tokenlist.arbitrum.io/ArbTokenLists/421614_arbed_coingecko.json', 241 | '--newArbifiedList=./src/ArbTokenLists/421614_arbed_coingecko.json', 242 | ]), 243 | fetch( 244 | 'https://tokenlist.arbitrum.io/ArbTokenLists/421614_arbed_coingecko.json', 245 | ).then((response) => response.json()), 246 | ]); 247 | 248 | testNoDuplicates(localList as ArbTokenList); 249 | compareLists(localList, onlineList); 250 | }); 251 | }); 252 | }); 253 | 254 | describe('Update token lists', () => { 255 | it('should return the same list as the online version', async () => { 256 | expect.assertions(2); 257 | const [localList, onlineList] = await Promise.all([ 258 | runCommand(Action.Update, [ 259 | '--l2NetworkID=42161', 260 | '--tokenList=https://tokenlist.arbitrum.io/ArbTokenLists/arbed_arb_whitelist_era.json', 261 | '--includeOldDataFields=true', 262 | '--prevArbifiedList=https://tokenlist.arbitrum.io/ArbTokenLists/arbed_arb_whitelist_era.json', 263 | '--newArbifiedList=./src/ArbTokenLists/arbed_arb_whitelist_era.json', 264 | ]), 265 | fetch( 266 | 'https://tokenlist.arbitrum.io/ArbTokenLists/arbed_arb_whitelist_era.json', 267 | ).then((response) => response.json()), 268 | ]); 269 | 270 | testNoDuplicates(localList as ArbTokenList); 271 | compareLists(localList, onlineList); 272 | }); 273 | }); 274 | 275 | describe('allTokensList', () => { 276 | it.skip('should generate allTokensList for a given network', async () => { 277 | expect.assertions(2); 278 | const [localList, onlineList] = await Promise.all([ 279 | runCommand(Action.AllTokensList, [ 280 | '--l2NetworkID=421613', 281 | '--tokenList=full', 282 | '--ignorePreviousList=true', 283 | '--newArbifiedList=./src/ArbTokenLists/421613_arbed_full.json', 284 | ]), 285 | fetch( 286 | 'https://tokenlist.arbitrum.io/ArbTokenLists/421613_arbed_full.json', 287 | ).then((response) => response.json()), 288 | ]); 289 | 290 | testNoDuplicates(localList as ArbTokenList); 291 | compareLists(localList, onlineList); 292 | }); 293 | }); 294 | 295 | describe('External lists tests', () => { 296 | it.skip('External lists: check no duplicates', async () => { 297 | const lists = [ 298 | // 'https://tokenlist.arbitrum.io/ArbTokenLists/arbed_gemini_token_list.json', 299 | 'https://tokenlist.arbitrum.io/ArbTokenLists/arbed_coinmarketcap.json', 300 | 'https://tokenlist.arbitrum.io/ArbTokenLists/42170_arbed_uniswap_labs_default.json', 301 | 'https://tokenlist.arbitrum.io/ArbTokenLists/arbed_uniswap_labs_list.json', 302 | 'https://tokenlist.arbitrum.io/ArbTokenLists/arbed_arb_whitelist_era.json', 303 | 'https://tokenlist.arbitrum.io/ArbTokenLists/421613_arbed_coinmarketcap.json', 304 | 'https://tokenlist.arbitrum.io/ArbTokenLists/42170_arbed_coinmarketcap.json', 305 | // 'https://tokenlist.arbitrum.io/ArbTokenLists/42170_arbed_gemini_token_list.json', 306 | ]; 307 | expect.assertions(lists.length); 308 | 309 | for (const list of lists) { 310 | const res = await fetch(list); 311 | const data = (await res.json()) as ArbTokenList; 312 | 313 | testNoDuplicates(data as ArbTokenList); 314 | } 315 | }); 316 | }); 317 | }); 318 | -------------------------------------------------------------------------------- /__test__/unit/getVersion.test.ts: -------------------------------------------------------------------------------- 1 | import { getVersion } from '../../src/lib/getVersion'; 2 | import { 3 | baseList, 4 | majorList, 5 | minorList, 6 | patchList, 7 | withoutExtensions, 8 | } from './getVersionMockup'; 9 | 10 | describe('getVersion', () => { 11 | it('Should return 1.0.0 version if no previous list is passed', () => { 12 | const version = getVersion(null, baseList().tokens); 13 | expect(version).toStrictEqual({ 14 | major: 1, 15 | minor: 0, 16 | patch: 0, 17 | }); 18 | 19 | const versionWithoutExtensions = getVersion( 20 | null, 21 | withoutExtensions(baseList().tokens), 22 | ); 23 | expect(versionWithoutExtensions).toStrictEqual({ 24 | major: 1, 25 | minor: 0, 26 | patch: 0, 27 | }); 28 | }); 29 | 30 | it('Should not bump version if lists are equal', () => { 31 | const version = getVersion(baseList(), baseList().tokens); 32 | expect(version).toStrictEqual({ 33 | major: 2, 34 | minor: 3, 35 | patch: 4, 36 | }); 37 | 38 | const prevList = baseList(); 39 | const versionWithoutExtensions = getVersion( 40 | { 41 | ...prevList, 42 | tokens: withoutExtensions(prevList.tokens), 43 | }, 44 | withoutExtensions(baseList().tokens), 45 | ); 46 | expect(versionWithoutExtensions).toStrictEqual({ 47 | major: 2, 48 | minor: 3, 49 | patch: 4, 50 | }); 51 | }); 52 | 53 | it('Should bump patch version if extensions are different', () => { 54 | const [prevList, newList] = patchList(); 55 | const version = getVersion(prevList, newList); 56 | expect(version).toStrictEqual({ 57 | major: 2, 58 | minor: 3, 59 | patch: 5, 60 | }); 61 | 62 | const versionWithoutExtensions = getVersion( 63 | { 64 | ...prevList, 65 | tokens: withoutExtensions(prevList.tokens), 66 | }, 67 | withoutExtensions(newList), 68 | ); 69 | expect(versionWithoutExtensions).toStrictEqual({ 70 | major: 2, 71 | minor: 3, 72 | patch: 4, 73 | }); 74 | }); 75 | 76 | it('Should bump minor version if extensions are added', () => { 77 | const [prevList, newList] = minorList(); 78 | const version = getVersion(prevList, newList); 79 | expect(version).toStrictEqual({ 80 | major: 2, 81 | minor: 4, 82 | patch: 0, 83 | }); 84 | 85 | const versionWithoutExtensions = getVersion( 86 | { 87 | ...prevList, 88 | tokens: withoutExtensions(prevList.tokens), 89 | }, 90 | withoutExtensions(newList), 91 | ); 92 | expect(versionWithoutExtensions).toStrictEqual({ 93 | major: 2, 94 | minor: 3, 95 | patch: 4, 96 | }); 97 | }); 98 | 99 | it('Should bump major version if extensions are removed', () => { 100 | const [prevList, newList] = majorList(); 101 | const version = getVersion(prevList, newList); 102 | expect(version).toStrictEqual({ 103 | major: 3, 104 | minor: 0, 105 | patch: 0, 106 | }); 107 | 108 | const versionWithoutExtensions = getVersion( 109 | { 110 | ...prevList, 111 | tokens: withoutExtensions(prevList.tokens), 112 | }, 113 | withoutExtensions(newList), 114 | ); 115 | expect(versionWithoutExtensions).toStrictEqual({ 116 | major: 2, 117 | minor: 3, 118 | patch: 4, 119 | }); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /__test__/unit/getVersionMockup.ts: -------------------------------------------------------------------------------- 1 | import { ArbTokenInfo, ArbTokenList } from '../../src/lib/types'; 2 | 3 | const common = { 4 | name: 'Arbed Uniswap', 5 | timestamp: '2023-01-17T13:21:35.005Z', 6 | version: { 7 | major: 2, 8 | minor: 3, 9 | patch: 4, 10 | }, 11 | }; 12 | 13 | type Mutable = { -readonly [P in keyof T]: T[P] }; 14 | type Mock = ArbTokenList & { 15 | tokens: Mutable; 16 | }; 17 | export function baseList(): Mock { 18 | return { 19 | ...common, 20 | tokens: [ 21 | { 22 | chainId: 42161, 23 | address: '0x6314C31A7a1652cE482cffe247E9CB7c3f4BB9aF', 24 | name: '1INCH Token', 25 | symbol: '1INCH', 26 | decimals: 18, 27 | logoURI: 28 | 'https://assets.coingecko.com/coins/images/13469/thumb/1inch-token.png?1608803028', 29 | extensions: { 30 | bridgeInfo: { 31 | '1': { 32 | tokenAddress: '0x111111111117dc0aa78b770fa6a738034120c302', 33 | originBridgeAddress: '0x09e9222E96E7B4AE2a407B98d48e330053351EEe', 34 | destBridgeAddress: '0xa3a7b6f88361f48403514059f1f16c8e78d60eec', 35 | }, 36 | }, 37 | }, 38 | }, 39 | { 40 | chainId: 42161, 41 | address: '0xba5DdD1f9d7F570dc94a51479a000E3BCE967196', 42 | name: 'Aave Token', 43 | symbol: 'AAVE', 44 | decimals: 18, 45 | logoURI: 46 | 'https://assets.coingecko.com/coins/images/12645/thumb/AAVE.png?1601374110', 47 | extensions: { 48 | bridgeInfo: { 49 | '1': { 50 | tokenAddress: '0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9', 51 | originBridgeAddress: '0x09e9222E96E7B4AE2a407B98d48e330053351EEe', 52 | destBridgeAddress: '0xa3a7b6f88361f48403514059f1f16c8e78d60eec', 53 | }, 54 | }, 55 | }, 56 | }, 57 | { 58 | chainId: 42161, 59 | address: '0xeC76E8fe6e2242e6c2117caA244B9e2DE1569923', 60 | name: 'AIOZ Network', 61 | symbol: 'AIOZ', 62 | decimals: 18, 63 | logoURI: 64 | 'https://assets.coingecko.com/coins/images/14631/thumb/aioz_logo.png?1617413126', 65 | extensions: { 66 | bridgeInfo: { 67 | '1': { 68 | tokenAddress: '0x626e8036deb333b408be468f951bdb42433cbf18', 69 | originBridgeAddress: '0x09e9222E96E7B4AE2a407B98d48e330053351EEe', 70 | destBridgeAddress: '0xa3a7b6f88361f48403514059f1f16c8e78d60eec', 71 | }, 72 | }, 73 | }, 74 | }, 75 | ], 76 | }; 77 | } 78 | 79 | export function withoutExtensions(tokens: ArbTokenInfo[]) { 80 | tokens.forEach((token) => { 81 | delete token.extensions; 82 | }); 83 | 84 | return tokens; 85 | } 86 | 87 | export function patchList() { 88 | const prevList = baseList(); 89 | const newList = baseList(); 90 | 91 | newList.tokens[0].extensions!.bridgeInfo[1].tokenAddress = 92 | 'different token address'; 93 | return [prevList, newList.tokens] as const; 94 | } 95 | 96 | export function minorList() { 97 | const prevList = baseList(); 98 | const newList = baseList(); 99 | 100 | delete prevList.tokens[2].extensions; 101 | newList.tokens[1].extensions!.bridgeInfo[1].tokenAddress = 102 | 'different token address'; 103 | 104 | // PrevList: [tokenWithExtension, tokenWithExtension, tokenWithoutExtension] 105 | // NewList: [tokenWithExtension, tokenWithUpdatedExtension, tokenWithExtension] 106 | return [prevList, newList.tokens] as const; 107 | } 108 | 109 | export function majorList() { 110 | const [prevList, newListTokens] = minorList(); 111 | 112 | delete newListTokens[0].extensions; 113 | // PrevList: [tokenWithExtension, tokenWithExtension, tokenWithoutExtension] 114 | // NewList: [tokenWithoutExtension, tokenWithUpdatedExtension, tokenWithExtension] 115 | return [prevList, newListTokens] as const; 116 | } 117 | -------------------------------------------------------------------------------- /__test__/unit/schema/uniswap.tokenlist.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "My Token List", 3 | "logoURI": "ipfs://QmUSNbwUxUYNMvMksKypkgWs8unSm8dX2GjCPBVGZ7GGMr", 4 | "keywords": ["audited", "verified", "special tokens"], 5 | "tags": { 6 | "stablecoin": { 7 | "name": "Stablecoin", 8 | "description": "Tokens that are fixed to an external asset, e.g. the US dollar" 9 | }, 10 | "compound": { 11 | "name": "Compound Finance", 12 | "description": "Tokens that earn interest on compound.finance" 13 | } 14 | }, 15 | "timestamp": "2020-06-12T00:00:00+00:00", 16 | "tokens": [ 17 | { 18 | "chainId": 1, 19 | "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 20 | "symbol": "USDC", 21 | "name": "USD Coin", 22 | "decimals": 6, 23 | "logoURI": "ipfs://QmXfzKRvjZz3u5JRgC4v5mGVbm9ahrUiB4DgzHBsnWbTMM", 24 | "tags": ["stablecoin"] 25 | }, 26 | { 27 | "chainId": 1, 28 | "address": "0x39AA39c021dfbaE8faC545936693aC917d5E7563", 29 | "symbol": "cUSDC", 30 | "name": "Compound USD Coin", 31 | "decimals": 8, 32 | "logoURI": "ipfs://QmUSNbwUxUYNMvMksKypkgWs8unSm8dX2GjCPBVGZ7GGMr", 33 | "tags": ["compound"] 34 | } 35 | ], 36 | "version": { 37 | "major": 1, 38 | "minor": 0, 39 | "patch": 0 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /__test__/unit/token_list_gen.test.ts: -------------------------------------------------------------------------------- 1 | import { arbListtoEtherscanList } from '../../src/lib/token_list_gen'; 2 | import arblist from './schema/arbify.tokenlist.json'; 3 | 4 | describe('token_list_gen Test', () => { 5 | describe('arbListtoEtherscanList test', () => { 6 | it('Should return etherscanlist when use correct arblist', () => { 7 | expect(() => { 8 | arbListtoEtherscanList(arblist); 9 | }).not.toThrow(Error); 10 | }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /__test__/unit/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { removeInvalidTokensFromList } from '../../src/lib/utils'; 2 | import arblist from './schema/arbify.tokenlist.json'; 3 | import arblistDecimalsTooLow from './schema/arblistDecimalsTooLow.tokenlist.json'; 4 | import arblistWrongVersion from './schema/arblistWrongVersion.tokenlist.json'; 5 | 6 | describe('utils Test', () => { 7 | describe('removeInvalidTokensFromList Test', () => { 8 | it('Should return same when use correct list', () => { 9 | expect(removeInvalidTokensFromList(arblist)).toEqual(arblist); 10 | }); 11 | 12 | it('Should remove wrong token when use incorrect list', () => { 13 | const listToBeFixed = JSON.parse( 14 | JSON.stringify(arblistDecimalsTooLow), 15 | ) as typeof arblistDecimalsTooLow; 16 | const correctList = removeInvalidTokensFromList(listToBeFixed); 17 | expect(correctList.tokens.length).toBeLessThan( 18 | arblistDecimalsTooLow.tokens.length, 19 | ); 20 | correctList.tokens.forEach((tokenInfo) => { 21 | expect(tokenInfo.name).not.toEqual('blah blah '); 22 | }); 23 | }); 24 | 25 | it('Should throw Error when issues happen outside of tokens', () => { 26 | const listToBeFixed = JSON.parse( 27 | JSON.stringify(arblistWrongVersion), 28 | ) as typeof arblistWrongVersion; 29 | expect(() => { 30 | removeInvalidTokensFromList(listToBeFixed); 31 | }).toThrow('Data does not confirm to token list schema; not sure why'); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__test__/unit/validateTokenList.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | tokenListIsValid, 3 | validateTokenListWithErrorThrowing, 4 | } from '../../src/lib/validateTokenList'; 5 | import uniswapExample from './schema/uniswap.tokenlist.json'; 6 | import arblist from './schema/arbify.tokenlist.json'; 7 | import arblistDecimalsTooLow from './schema/arblistDecimalsTooLow.tokenlist.json'; 8 | import arblistDecimalsTooHigh from './schema/arblistDecimalsTooHigh.tokenlist.json'; 9 | import arblistNameTooLong from './schema/arblistNameTooLong.tokenlist.json'; 10 | import arblistSymbolTooLong from './schema/arblistSymbolTooLong.tokenlist.json'; 11 | import arblistWrongAddress from './schema/arblistWrongAddress.tokenlist.json'; 12 | import arblistWrongChainId from './schema/arblistWrongChainId.tokenlist.json'; 13 | 14 | describe('validateTokenList Test', () => { 15 | describe('TokenListIsValid Test', () => { 16 | it('Should return true when list is valid (Uniswap Example)', () => { 17 | expect(tokenListIsValid(uniswapExample)).toBeTruthy(); 18 | }); 19 | 20 | it('Should return true when list is valid (Arbify tokenlist Example)', () => { 21 | expect(tokenListIsValid(arblist)).toBeTruthy(); 22 | }); 23 | 24 | it('Should return false when list is invalid (Decimals not right)', () => { 25 | expect(tokenListIsValid(arblistDecimalsTooLow)).toBeFalsy(); 26 | expect(tokenListIsValid(arblistDecimalsTooHigh)).toBeFalsy(); 27 | }); 28 | 29 | it('Should return false when list is invalid (Name too long)', () => { 30 | expect(tokenListIsValid(arblistNameTooLong)).toBeFalsy(); 31 | }); 32 | 33 | it('Should return false when list is invalid (Address not right)', () => { 34 | expect(tokenListIsValid(arblistSymbolTooLong)).toBeFalsy(); 35 | }); 36 | 37 | it('Should return false when list is invalid (Symbol too long)', () => { 38 | expect(tokenListIsValid(arblistWrongAddress)).toBeFalsy(); 39 | }); 40 | 41 | it('Should return false when list is invalid (Wrong chainId)', () => { 42 | expect(tokenListIsValid(arblistWrongChainId)).toBeFalsy(); 43 | }); 44 | }); 45 | 46 | describe('validateTokenListWithErrorThrowing Test', () => { 47 | const errorCode = 48 | 'Data does not conform to token list schema; not sure why'; 49 | 50 | it('Should return true when list is valid (Uniswap Example)', () => { 51 | expect(validateTokenListWithErrorThrowing(uniswapExample)).toBeTruthy(); 52 | }); 53 | 54 | it('Should return true when list is valid (Arbify tokenlist Example)', () => { 55 | expect(validateTokenListWithErrorThrowing(arblist)).toBeTruthy(); 56 | }); 57 | 58 | it('Should return false when list is invalid (Decimals not right)', () => { 59 | expect(() => { 60 | validateTokenListWithErrorThrowing(arblistDecimalsTooLow); 61 | }).toThrow(errorCode); 62 | expect(() => { 63 | validateTokenListWithErrorThrowing(arblistDecimalsTooHigh); 64 | }).toThrow(errorCode); 65 | }); 66 | 67 | it('Should return false when list is invalid (Name too long)', () => { 68 | expect(() => { 69 | validateTokenListWithErrorThrowing(arblistNameTooLong); 70 | }).toThrow(errorCode); 71 | }); 72 | 73 | it('Should return false when list is invalid (Address not right)', () => { 74 | expect(() => { 75 | validateTokenListWithErrorThrowing(arblistSymbolTooLong); 76 | }).toThrow(errorCode); 77 | }); 78 | 79 | it('Should return false when list is invalid (Symbol too long)', () => { 80 | expect(() => { 81 | validateTokenListWithErrorThrowing(arblistWrongAddress); 82 | }).toThrow(errorCode); 83 | }); 84 | 85 | it('Should return false when list is invalid (Wrong chainId)', () => { 86 | expect(() => { 87 | validateTokenListWithErrorThrowing(arblistWrongChainId); 88 | }).toThrow(errorCode); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | setupFiles: ['/src/setupTests.ts'], 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arb-token-lists", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "generate:matrix": "ts-node .github/scripts/generateMatrix.ts", 8 | "generate:orbitChainsData": "ts-node src/scripts/fetchOrbitChainsData.ts && yarn prettier 'src/Assets/orbitChainsData.json' --write", 9 | "postinstall": "yarn generate:orbitChainsData", 10 | "build": "tsc", 11 | "main": "ts-node src/main.ts", 12 | "lint": "tsc --noEmit && eslint 'src/**/*.{js,ts,tsx}'", 13 | "prettier:format": "yarn prettier './**/*.{js,json,md,ts,yml}' --write && yarn run lint --fix", 14 | "prettier:check": "yarn prettier './**/*.{js,json,md,ts,yml}' --check && yarn run lint --fix", 15 | "update": "yarn ts-node src/main.ts update", 16 | "arbify": "yarn ts-node src/main.ts arbify", 17 | "permit": "yarn ts-node src/main.ts permit", 18 | "fullList": "yarn ts-node src/main.ts full --tokenList full --ignorePreviousList", 19 | "allTokensList": "yarn ts-node src/main.ts alltokenslist --tokenList full", 20 | "updateNova": "yarn ts-node src/main.ts update --l2NetworkID 42170", 21 | "novaify": "yarn ts-node src/main.ts arbify --l2NetworkID 42170", 22 | "fullNova": "yarn ts-node src/main.ts full --tokenList full --l2NetworkID 42170", 23 | "test": "jest --runInBand --silent", 24 | "test:integration": "yarn test --ci __test__/integration", 25 | "test:unit": "yarn test --ci __test__/unit" 26 | }, 27 | "dependencies": { 28 | "@arbitrum/sdk": "^4.0.2", 29 | "@types/jest": "^29.2.5", 30 | "@uniswap/token-lists": "^1.0.0-beta.33", 31 | "ajv": "^8.12.0", 32 | "ajv-formats": "^2.1.1", 33 | "axios": "^0.23.0", 34 | "axios-retry": "^3.4.0", 35 | "better-ajv-errors": "^1.1.2", 36 | "dotenv": "^16.0.3", 37 | "dotenv-expand": "^10.0.0", 38 | "graphql": "^15.6.1", 39 | "graphql-request": "^3.6.1", 40 | "ts-node": "^10.8.1", 41 | "typescript": "^4.7.3", 42 | "yargs": "^17.2.1" 43 | }, 44 | "devDependencies": { 45 | "@offchainlabs/eslint-config-typescript": "^0.2.1", 46 | "@offchainlabs/prettier-config": "^0.2.0", 47 | "@types/node": "^16.11.1", 48 | "@types/yargs": "^17.0.4", 49 | "@typescript-eslint/eslint-plugin": "^5.48.2", 50 | "@typescript-eslint/parser": "^5.48.2", 51 | "eslint": "^8.32.0", 52 | "eslint-config-prettier": "^8.6.0", 53 | "eslint-plugin-jest": "^27.2.1", 54 | "eslint-plugin-jsx-a11y": "^6.7.1", 55 | "eslint-plugin-prettier": "^4.2.1", 56 | "eslint-plugin-react": "^7.32.1", 57 | "eslint-plugin-react-hooks": "^4.6.0", 58 | "jest": "^29.3.1", 59 | "prettier": "^2.8.3", 60 | "ts-jest": "^29.0.3" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ### Setup 2 | 3 | From root: 4 | 5 | 1. `yarn install` 6 | 2. `cd packages/cli; cp .env.sample .env` 7 | 3. In `.env`, either set `MAINNET_RPC`, `GOERLI_RPC`, and `SEPOLIA_RPC` var or `INFURA_KEY` 8 | 9 | ### Arbify an L1 Token List 10 | 11 | `yarn arbify --tokenList https://gateway.ipfs.io/ipns/tokens.uniswap.org --l2NetworkID 42161 --newArbifiedList ./src/ArbTokenLists/arbed_uniswap_labs.json` 12 | 13 | Note that a local list can also be used, i.e.: 14 | 15 | `yarn arbify --tokenList ./src/SourceLists/my_l1_list.json --l2NetworkID 42161` 16 | 17 | ### Generate Full List 18 | 19 | `yarn fullList` 20 | -------------------------------------------------------------------------------- /src/Assets/42161_arbitrum_native_token_list.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Arb1 Tokens by Offchain Labs", 3 | "timestamp": "2024-05-29T09:32:20.831Z", 4 | "version": { 5 | "major": 1, 6 | "minor": 0, 7 | "patch": 0 8 | }, 9 | "tokens": [ 10 | { 11 | "chainId": 42161, 12 | "address": "0xaf88d065e77c8cc2239327c5edb3a432268e5831", 13 | "name": "USD Coin", 14 | "symbol": "USDC", 15 | "decimals": 6, 16 | "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/arbitrum/assets/0xaf88d065e77c8cC2239327C5EDb3A432268e5831/logo.png" 17 | }, 18 | { 19 | "chainId": 42161, 20 | "address": "0x2416092f143378750bb29b79ed961ab195cceea5", 21 | "name": "Renzo Restaked ETH", 22 | "symbol": "ezETH", 23 | "decimals": 18, 24 | "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xbf5495Efe5DB9ce00f80364C8B423567e58d2110/logo.png" 25 | }, 26 | { 27 | "chainId": 42161, 28 | "address": "0x4186BFC76E2E237523CBC30FD220FE055156b41F", 29 | "name": "KelpDao Restaked ETH", 30 | "symbol": "rsETH", 31 | "decimals": 18, 32 | "logoURI": "https://assets.coingecko.com/coins/images/33800/large/Icon___Dark.png" 33 | } 34 | ], 35 | "logoURI": "https://avatars.githubusercontent.com/u/43838009" 36 | } 37 | -------------------------------------------------------------------------------- /src/Assets/logo_uris.json: -------------------------------------------------------------------------------- 1 | { 2 | "0x0000000000085d4780b73119b644ae5ecd22b376": "https://images.prismic.io/tusd-homepage/fb4d581a-95ed-404c-b9de-7ab1365c1386_%E5%9B%BE%E5%B1%82+1.png", 3 | "0x0391d2021f89dc339f60fff84546ea23e337750f": "https://cryptologos.cc/logos/barnbridge-bond-logo.png", 4 | "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2": "https://cryptologos.cc/logos/sushiswap-sushi-logo.png", 5 | "0xd533a949740bb3306d119cc777fa900ba034cd52": "https://cryptologos.cc/logos/curve-dao-token-crv-logo.png", 6 | "0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e": "https://cryptologos.cc/logos/yearn-finance-yfi-logo.png", 7 | "0x2ba592f78db6436527729929aaf6c908497cb200": "https://cryptologos.cc/logos/cream-finance-cream-logo.png", 8 | "0x7645ddfeeceda57e41f92679c4acd83c56a81d14": "https://cryptologos.cc/logos/zel-flux-logo.png", 9 | "0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2": "https://cryptologos.cc/logos/maker-mkr-logo.png", 10 | "0xba100000625a3754423978a60c9317c58a424e3d": "https://cryptologos.cc/logos/balancer-bal-logo.png", 11 | "0xbbbbca6a901c926f240b89eacb641d8aec7aeafd": "https://cryptologos.cc/logos/loopring-lrc-logo.png", 12 | "0xddb3422497e61e13543bea06989c0789117555c5": "https://cryptologos.cc/logos/coti-coti-logo.png", 13 | "0x43dfc4159d86f3a37a5a4b3d4580b888ad7d4ddd": "https://etherscan.io/token/images/dodo_32.png", 14 | "0x03ab458634910aad20ef5f1c8ee96f1d6ac54919": "https://etherscan.io/token/images/raireflexindex_32.png", 15 | "0x043c308bb8a5ae96d0093444be7f56459f1340b1": "https://etherscan.io/token/images/sumswap_32.png", 16 | "0x090185f2135308bad17527004364ebcc2d37e5f6": "https://etherscan.io/token/images/spelltoken_32.png", 17 | "0x0a5e677a6a24b2f1a2bf4f3bffc443231d2fdec8": "https://etherscan.io/token/images/dforceusd_32.png", 18 | "0x0e192d382a36de7011f795acc4391cd302003606": "https://etherscan.io/token/images/futureswap2_32.png", 19 | "0x0ff5a8451a839f5f0bb3562689d9a44089738d11": "https://etherscan.io/token/images/dopexrebate_32.png", 20 | "0x27c4af9a860c4cadc358005f8b48140b2e434a7b": "https://etherscan.io/token/images/validator_32.png", 21 | "0x3258cd8134b6b28e814772dd91d5ecceea512818": "https://etherscan.io/token/images/farmland_32.png", 22 | "0x3472a5a71965499acd81997a54bba8d852c6e53d": "https://etherscan.io/token/images/badger_32.png", 23 | "0x41a3dba3d677e573636ba691a70ff2d606c29666": "https://etherscan.io/token/images/blanktoken_32.png", 24 | "0x429876c4a6f89fb470e92456b8313879df98b63c": "https://etherscan.io/token/images/cryptionnetwork_32.png", 25 | "0x429881672b9ae42b8eba0e26cd9c73711b891ca5": "https://etherscan.io/token/images/pickle_32.png", 26 | "0x4e0fca55a6c3a94720ded91153a27f60e26b9aa8": "https://etherscan.io/token/images/boostcoin_32.png", 27 | "0x7b35ce522cb72e4077baeb96cb923a5529764a00": "https://etherscan.io/token/images/impermax_32.png", 28 | "0x8d610e20481f4c4f3acb87bba9c46bef7795fdfe": "https://etherscan.io/token/images/unitynetwork_32.png", 29 | "0x9695e0114e12c0d3a3636fab5a18e6b737529023": "https://etherscan.io/token/images/dfynnetwork_32.png", 30 | "0x97872eafd79940c7b24f7bcc1eadb1457347adc9": "https://etherscan.io/token/images/strips_32.png", 31 | "0x9b99cca871be05119b2012fd4474731dd653febe": "https://etherscan.io/token/images/antimatter_32.png", 32 | "0xb683d83a532e2cb7dfa5275eed3698436371cc9f": "https://etherscan.io/token/images/btu_32.png", 33 | "0xb986f3a2d91d3704dc974a24fb735dcc5e3c1e70": "https://etherscan.io/token/images/dforceeur_32.png", 34 | "0xbaac2b4491727d78d2b78815144570b9f2fe8899": "https://etherscan.io/token/images/thedogenft_32.png", 35 | "0xca1207647ff814039530d7d35df0e1dd2e91fa84": "https://etherscan.io/token/images/dHedge_32.png", 36 | "0xd291e7a03283640fdc51b121ac401383a46cc623": "https://etherscan.io/token/images/RariGovernanceToken_32.png", 37 | "0xdddddd4301a082e62e84e43f474f044423921918": "https://etherscan.io/token/images/dvf_32.png", 38 | "0xdeeb6091a5adc78fa0332bee5a38a8908b6b566e": "https://etherscan.io/token/images/tkdcoop_32.png", 39 | "0xdfdb7f72c1f195c5951a234e8db9806eb0635346": "https://etherscan.io/token/images/feistydoge_32.png", 40 | "0xeec2be5c91ae7f8a338e1e5f3b5de49d07afdc81": "https://etherscan.io/token/images/dopexgovernance_32.png", 41 | "0xc18360217d8f7ab5e7c516566761ea12ce7f9d72": "https://etherscan.io/token/images/ens2_32.png", 42 | "0xf8e9f10c22840b613cda05a0c5fdb59a4d6cd7ef": "https://etherscan.io/token/images/dogsofelon_32.png" 43 | } 44 | -------------------------------------------------------------------------------- /src/PermitTokens/daiPermitTokenAbi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "uint256", 6 | "name": "chainId_", 7 | "type": "uint256" 8 | } 9 | ], 10 | "stateMutability": "nonpayable", 11 | "type": "constructor" 12 | }, 13 | { 14 | "anonymous": false, 15 | "inputs": [ 16 | { 17 | "indexed": true, 18 | "internalType": "address", 19 | "name": "src", 20 | "type": "address" 21 | }, 22 | { 23 | "indexed": true, 24 | "internalType": "address", 25 | "name": "guy", 26 | "type": "address" 27 | }, 28 | { 29 | "indexed": false, 30 | "internalType": "uint256", 31 | "name": "wad", 32 | "type": "uint256" 33 | } 34 | ], 35 | "name": "Approval", 36 | "type": "event" 37 | }, 38 | { 39 | "anonymous": false, 40 | "inputs": [ 41 | { 42 | "indexed": true, 43 | "internalType": "address", 44 | "name": "src", 45 | "type": "address" 46 | }, 47 | { 48 | "indexed": true, 49 | "internalType": "address", 50 | "name": "dst", 51 | "type": "address" 52 | }, 53 | { 54 | "indexed": false, 55 | "internalType": "uint256", 56 | "name": "wad", 57 | "type": "uint256" 58 | } 59 | ], 60 | "name": "Transfer", 61 | "type": "event" 62 | }, 63 | { 64 | "inputs": [], 65 | "name": "DOMAIN_SEPARATOR", 66 | "outputs": [ 67 | { 68 | "internalType": "bytes32", 69 | "name": "", 70 | "type": "bytes32" 71 | } 72 | ], 73 | "stateMutability": "view", 74 | "type": "function" 75 | }, 76 | { 77 | "inputs": [], 78 | "name": "PERMIT_TYPEHASH", 79 | "outputs": [ 80 | { 81 | "internalType": "bytes32", 82 | "name": "", 83 | "type": "bytes32" 84 | } 85 | ], 86 | "stateMutability": "view", 87 | "type": "function" 88 | }, 89 | { 90 | "inputs": [ 91 | { 92 | "internalType": "address", 93 | "name": "", 94 | "type": "address" 95 | }, 96 | { 97 | "internalType": "address", 98 | "name": "", 99 | "type": "address" 100 | } 101 | ], 102 | "name": "allowance", 103 | "outputs": [ 104 | { 105 | "internalType": "uint256", 106 | "name": "", 107 | "type": "uint256" 108 | } 109 | ], 110 | "stateMutability": "view", 111 | "type": "function" 112 | }, 113 | { 114 | "inputs": [ 115 | { 116 | "internalType": "address", 117 | "name": "usr", 118 | "type": "address" 119 | }, 120 | { 121 | "internalType": "uint256", 122 | "name": "wad", 123 | "type": "uint256" 124 | } 125 | ], 126 | "name": "approve", 127 | "outputs": [ 128 | { 129 | "internalType": "bool", 130 | "name": "", 131 | "type": "bool" 132 | } 133 | ], 134 | "stateMutability": "nonpayable", 135 | "type": "function" 136 | }, 137 | { 138 | "inputs": [ 139 | { 140 | "internalType": "address", 141 | "name": "", 142 | "type": "address" 143 | } 144 | ], 145 | "name": "balanceOf", 146 | "outputs": [ 147 | { 148 | "internalType": "uint256", 149 | "name": "", 150 | "type": "uint256" 151 | } 152 | ], 153 | "stateMutability": "view", 154 | "type": "function" 155 | }, 156 | { 157 | "inputs": [ 158 | { 159 | "internalType": "address", 160 | "name": "usr", 161 | "type": "address" 162 | }, 163 | { 164 | "internalType": "uint256", 165 | "name": "wad", 166 | "type": "uint256" 167 | } 168 | ], 169 | "name": "burn", 170 | "outputs": [], 171 | "stateMutability": "nonpayable", 172 | "type": "function" 173 | }, 174 | { 175 | "inputs": [], 176 | "name": "decimals", 177 | "outputs": [ 178 | { 179 | "internalType": "uint8", 180 | "name": "", 181 | "type": "uint8" 182 | } 183 | ], 184 | "stateMutability": "view", 185 | "type": "function" 186 | }, 187 | { 188 | "inputs": [ 189 | { 190 | "internalType": "address", 191 | "name": "guy", 192 | "type": "address" 193 | } 194 | ], 195 | "name": "deny", 196 | "outputs": [], 197 | "stateMutability": "nonpayable", 198 | "type": "function" 199 | }, 200 | { 201 | "inputs": [ 202 | { 203 | "internalType": "address", 204 | "name": "usr", 205 | "type": "address" 206 | }, 207 | { 208 | "internalType": "uint256", 209 | "name": "wad", 210 | "type": "uint256" 211 | } 212 | ], 213 | "name": "mint", 214 | "outputs": [], 215 | "stateMutability": "nonpayable", 216 | "type": "function" 217 | }, 218 | { 219 | "inputs": [ 220 | { 221 | "internalType": "address", 222 | "name": "src", 223 | "type": "address" 224 | }, 225 | { 226 | "internalType": "address", 227 | "name": "dst", 228 | "type": "address" 229 | }, 230 | { 231 | "internalType": "uint256", 232 | "name": "wad", 233 | "type": "uint256" 234 | } 235 | ], 236 | "name": "move", 237 | "outputs": [], 238 | "stateMutability": "nonpayable", 239 | "type": "function" 240 | }, 241 | { 242 | "inputs": [], 243 | "name": "name", 244 | "outputs": [ 245 | { 246 | "internalType": "string", 247 | "name": "", 248 | "type": "string" 249 | } 250 | ], 251 | "stateMutability": "view", 252 | "type": "function" 253 | }, 254 | { 255 | "inputs": [ 256 | { 257 | "internalType": "address", 258 | "name": "", 259 | "type": "address" 260 | } 261 | ], 262 | "name": "nonces", 263 | "outputs": [ 264 | { 265 | "internalType": "uint256", 266 | "name": "", 267 | "type": "uint256" 268 | } 269 | ], 270 | "stateMutability": "view", 271 | "type": "function" 272 | }, 273 | { 274 | "inputs": [ 275 | { 276 | "internalType": "address", 277 | "name": "holder", 278 | "type": "address" 279 | }, 280 | { 281 | "internalType": "address", 282 | "name": "spender", 283 | "type": "address" 284 | }, 285 | { 286 | "internalType": "uint256", 287 | "name": "nonce", 288 | "type": "uint256" 289 | }, 290 | { 291 | "internalType": "uint256", 292 | "name": "expiry", 293 | "type": "uint256" 294 | }, 295 | { 296 | "internalType": "bool", 297 | "name": "allowed", 298 | "type": "bool" 299 | }, 300 | { 301 | "internalType": "uint8", 302 | "name": "v", 303 | "type": "uint8" 304 | }, 305 | { 306 | "internalType": "bytes32", 307 | "name": "r", 308 | "type": "bytes32" 309 | }, 310 | { 311 | "internalType": "bytes32", 312 | "name": "s", 313 | "type": "bytes32" 314 | } 315 | ], 316 | "name": "permit", 317 | "outputs": [], 318 | "stateMutability": "nonpayable", 319 | "type": "function" 320 | }, 321 | { 322 | "inputs": [ 323 | { 324 | "internalType": "address", 325 | "name": "usr", 326 | "type": "address" 327 | }, 328 | { 329 | "internalType": "uint256", 330 | "name": "wad", 331 | "type": "uint256" 332 | } 333 | ], 334 | "name": "pull", 335 | "outputs": [], 336 | "stateMutability": "nonpayable", 337 | "type": "function" 338 | }, 339 | { 340 | "inputs": [ 341 | { 342 | "internalType": "address", 343 | "name": "usr", 344 | "type": "address" 345 | }, 346 | { 347 | "internalType": "uint256", 348 | "name": "wad", 349 | "type": "uint256" 350 | } 351 | ], 352 | "name": "push", 353 | "outputs": [], 354 | "stateMutability": "nonpayable", 355 | "type": "function" 356 | }, 357 | { 358 | "inputs": [ 359 | { 360 | "internalType": "address", 361 | "name": "guy", 362 | "type": "address" 363 | } 364 | ], 365 | "name": "rely", 366 | "outputs": [], 367 | "stateMutability": "nonpayable", 368 | "type": "function" 369 | }, 370 | { 371 | "inputs": [], 372 | "name": "symbol", 373 | "outputs": [ 374 | { 375 | "internalType": "string", 376 | "name": "", 377 | "type": "string" 378 | } 379 | ], 380 | "stateMutability": "view", 381 | "type": "function" 382 | }, 383 | { 384 | "inputs": [], 385 | "name": "totalSupply", 386 | "outputs": [ 387 | { 388 | "internalType": "uint256", 389 | "name": "", 390 | "type": "uint256" 391 | } 392 | ], 393 | "stateMutability": "view", 394 | "type": "function" 395 | }, 396 | { 397 | "inputs": [ 398 | { 399 | "internalType": "address", 400 | "name": "dst", 401 | "type": "address" 402 | }, 403 | { 404 | "internalType": "uint256", 405 | "name": "wad", 406 | "type": "uint256" 407 | } 408 | ], 409 | "name": "transfer", 410 | "outputs": [ 411 | { 412 | "internalType": "bool", 413 | "name": "", 414 | "type": "bool" 415 | } 416 | ], 417 | "stateMutability": "nonpayable", 418 | "type": "function" 419 | }, 420 | { 421 | "inputs": [ 422 | { 423 | "internalType": "address", 424 | "name": "src", 425 | "type": "address" 426 | }, 427 | { 428 | "internalType": "address", 429 | "name": "dst", 430 | "type": "address" 431 | }, 432 | { 433 | "internalType": "uint256", 434 | "name": "wad", 435 | "type": "uint256" 436 | } 437 | ], 438 | "name": "transferFrom", 439 | "outputs": [ 440 | { 441 | "internalType": "bool", 442 | "name": "", 443 | "type": "bool" 444 | } 445 | ], 446 | "stateMutability": "nonpayable", 447 | "type": "function" 448 | }, 449 | { 450 | "inputs": [], 451 | "name": "version", 452 | "outputs": [ 453 | { 454 | "internalType": "string", 455 | "name": "", 456 | "type": "string" 457 | } 458 | ], 459 | "stateMutability": "view", 460 | "type": "function" 461 | }, 462 | { 463 | "inputs": [ 464 | { 465 | "internalType": "address", 466 | "name": "", 467 | "type": "address" 468 | } 469 | ], 470 | "name": "wards", 471 | "outputs": [ 472 | { 473 | "internalType": "uint256", 474 | "name": "", 475 | "type": "uint256" 476 | } 477 | ], 478 | "stateMutability": "view", 479 | "type": "function" 480 | } 481 | ] 482 | -------------------------------------------------------------------------------- /src/PermitTokens/multicallAbi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "components": [ 6 | { 7 | "internalType": "address", 8 | "name": "target", 9 | "type": "address" 10 | }, 11 | { 12 | "internalType": "bytes", 13 | "name": "callData", 14 | "type": "bytes" 15 | } 16 | ], 17 | "internalType": "struct Multicall2.Call[]", 18 | "name": "calls", 19 | "type": "tuple[]" 20 | } 21 | ], 22 | "name": "aggregate", 23 | "outputs": [ 24 | { 25 | "internalType": "uint256", 26 | "name": "blockNumber", 27 | "type": "uint256" 28 | }, 29 | { 30 | "internalType": "bytes[]", 31 | "name": "returnData", 32 | "type": "bytes[]" 33 | } 34 | ], 35 | "stateMutability": "nonpayable", 36 | "type": "function" 37 | }, 38 | { 39 | "inputs": [ 40 | { 41 | "components": [ 42 | { 43 | "internalType": "address", 44 | "name": "target", 45 | "type": "address" 46 | }, 47 | { 48 | "internalType": "bytes", 49 | "name": "callData", 50 | "type": "bytes" 51 | } 52 | ], 53 | "internalType": "struct Multicall2.Call[]", 54 | "name": "calls", 55 | "type": "tuple[]" 56 | } 57 | ], 58 | "name": "blockAndAggregate", 59 | "outputs": [ 60 | { 61 | "internalType": "uint256", 62 | "name": "blockNumber", 63 | "type": "uint256" 64 | }, 65 | { 66 | "internalType": "bytes32", 67 | "name": "blockHash", 68 | "type": "bytes32" 69 | }, 70 | { 71 | "components": [ 72 | { 73 | "internalType": "bool", 74 | "name": "success", 75 | "type": "bool" 76 | }, 77 | { 78 | "internalType": "bytes", 79 | "name": "returnData", 80 | "type": "bytes" 81 | } 82 | ], 83 | "internalType": "struct Multicall2.Result[]", 84 | "name": "returnData", 85 | "type": "tuple[]" 86 | } 87 | ], 88 | "stateMutability": "nonpayable", 89 | "type": "function" 90 | }, 91 | { 92 | "inputs": [ 93 | { 94 | "internalType": "uint256", 95 | "name": "blockNumber", 96 | "type": "uint256" 97 | } 98 | ], 99 | "name": "getBlockHash", 100 | "outputs": [ 101 | { 102 | "internalType": "bytes32", 103 | "name": "blockHash", 104 | "type": "bytes32" 105 | } 106 | ], 107 | "stateMutability": "view", 108 | "type": "function" 109 | }, 110 | { 111 | "inputs": [], 112 | "name": "getBlockNumber", 113 | "outputs": [ 114 | { 115 | "internalType": "uint256", 116 | "name": "blockNumber", 117 | "type": "uint256" 118 | } 119 | ], 120 | "stateMutability": "view", 121 | "type": "function" 122 | }, 123 | { 124 | "inputs": [], 125 | "name": "getCurrentBlockCoinbase", 126 | "outputs": [ 127 | { 128 | "internalType": "address", 129 | "name": "coinbase", 130 | "type": "address" 131 | } 132 | ], 133 | "stateMutability": "view", 134 | "type": "function" 135 | }, 136 | { 137 | "inputs": [], 138 | "name": "getCurrentBlockDifficulty", 139 | "outputs": [ 140 | { 141 | "internalType": "uint256", 142 | "name": "difficulty", 143 | "type": "uint256" 144 | } 145 | ], 146 | "stateMutability": "view", 147 | "type": "function" 148 | }, 149 | { 150 | "inputs": [], 151 | "name": "getCurrentBlockGasLimit", 152 | "outputs": [ 153 | { 154 | "internalType": "uint256", 155 | "name": "gaslimit", 156 | "type": "uint256" 157 | } 158 | ], 159 | "stateMutability": "view", 160 | "type": "function" 161 | }, 162 | { 163 | "inputs": [], 164 | "name": "getCurrentBlockTimestamp", 165 | "outputs": [ 166 | { 167 | "internalType": "uint256", 168 | "name": "timestamp", 169 | "type": "uint256" 170 | } 171 | ], 172 | "stateMutability": "view", 173 | "type": "function" 174 | }, 175 | { 176 | "inputs": [ 177 | { 178 | "internalType": "address", 179 | "name": "addr", 180 | "type": "address" 181 | } 182 | ], 183 | "name": "getEthBalance", 184 | "outputs": [ 185 | { 186 | "internalType": "uint256", 187 | "name": "balance", 188 | "type": "uint256" 189 | } 190 | ], 191 | "stateMutability": "view", 192 | "type": "function" 193 | }, 194 | { 195 | "inputs": [], 196 | "name": "getLastBlockHash", 197 | "outputs": [ 198 | { 199 | "internalType": "bytes32", 200 | "name": "blockHash", 201 | "type": "bytes32" 202 | } 203 | ], 204 | "stateMutability": "view", 205 | "type": "function" 206 | }, 207 | { 208 | "inputs": [ 209 | { 210 | "internalType": "bool", 211 | "name": "requireSuccess", 212 | "type": "bool" 213 | }, 214 | { 215 | "components": [ 216 | { 217 | "internalType": "address", 218 | "name": "target", 219 | "type": "address" 220 | }, 221 | { 222 | "internalType": "bytes", 223 | "name": "callData", 224 | "type": "bytes" 225 | } 226 | ], 227 | "internalType": "struct Multicall2.Call[]", 228 | "name": "calls", 229 | "type": "tuple[]" 230 | } 231 | ], 232 | "name": "tryAggregate", 233 | "outputs": [ 234 | { 235 | "components": [ 236 | { 237 | "internalType": "bool", 238 | "name": "success", 239 | "type": "bool" 240 | }, 241 | { 242 | "internalType": "bytes", 243 | "name": "returnData", 244 | "type": "bytes" 245 | } 246 | ], 247 | "internalType": "struct Multicall2.Result[]", 248 | "name": "returnData", 249 | "type": "tuple[]" 250 | } 251 | ], 252 | "stateMutability": "nonpayable", 253 | "type": "function" 254 | }, 255 | { 256 | "inputs": [ 257 | { 258 | "internalType": "bool", 259 | "name": "requireSuccess", 260 | "type": "bool" 261 | }, 262 | { 263 | "components": [ 264 | { 265 | "internalType": "address", 266 | "name": "target", 267 | "type": "address" 268 | }, 269 | { 270 | "internalType": "bytes", 271 | "name": "callData", 272 | "type": "bytes" 273 | } 274 | ], 275 | "internalType": "struct Multicall2.Call[]", 276 | "name": "calls", 277 | "type": "tuple[]" 278 | } 279 | ], 280 | "name": "tryAggregateGasRation", 281 | "outputs": [ 282 | { 283 | "components": [ 284 | { 285 | "internalType": "bool", 286 | "name": "success", 287 | "type": "bool" 288 | }, 289 | { 290 | "internalType": "bytes", 291 | "name": "returnData", 292 | "type": "bytes" 293 | } 294 | ], 295 | "internalType": "struct Multicall2.Result[]", 296 | "name": "returnData", 297 | "type": "tuple[]" 298 | } 299 | ], 300 | "stateMutability": "nonpayable", 301 | "type": "function" 302 | }, 303 | { 304 | "inputs": [ 305 | { 306 | "internalType": "bool", 307 | "name": "requireSuccess", 308 | "type": "bool" 309 | }, 310 | { 311 | "components": [ 312 | { 313 | "internalType": "address", 314 | "name": "target", 315 | "type": "address" 316 | }, 317 | { 318 | "internalType": "bytes", 319 | "name": "callData", 320 | "type": "bytes" 321 | } 322 | ], 323 | "internalType": "struct Multicall2.Call[]", 324 | "name": "calls", 325 | "type": "tuple[]" 326 | } 327 | ], 328 | "name": "tryBlockAndAggregate", 329 | "outputs": [ 330 | { 331 | "internalType": "uint256", 332 | "name": "blockNumber", 333 | "type": "uint256" 334 | }, 335 | { 336 | "internalType": "bytes32", 337 | "name": "blockHash", 338 | "type": "bytes32" 339 | }, 340 | { 341 | "components": [ 342 | { 343 | "internalType": "bool", 344 | "name": "success", 345 | "type": "bool" 346 | }, 347 | { 348 | "internalType": "bytes", 349 | "name": "returnData", 350 | "type": "bytes" 351 | } 352 | ], 353 | "internalType": "struct Multicall2.Result[]", 354 | "name": "returnData", 355 | "type": "tuple[]" 356 | } 357 | ], 358 | "stateMutability": "nonpayable", 359 | "type": "function" 360 | } 361 | ] 362 | -------------------------------------------------------------------------------- /src/PermitTokens/permitSignature.ts: -------------------------------------------------------------------------------- 1 | import { BigNumberish, Contract, Wallet, utils, constants } from 'ethers'; 2 | import { ArbTokenInfo, ArbTokenList } from '../lib/types'; 3 | 4 | import permitTokenAbi from '../PermitTokens/permitTokenAbi.json'; 5 | import daiPermitTokenAbi from '../PermitTokens/daiPermitTokenAbi.json'; 6 | import multicallAbi from '../PermitTokens/multicallAbi.json'; 7 | import { getNetworkConfig } from '../lib/instantiate_bridge'; 8 | import { getChunks, promiseRetrier } from '../lib/utils'; 9 | 10 | async function getPermitSig( 11 | wallet: Wallet, 12 | token: Contract, 13 | spender: string, 14 | value: BigNumberish, 15 | deadline: BigNumberish, 16 | optional?: { 17 | nonce?: number; 18 | name?: string; 19 | chainId?: number; 20 | version?: string; 21 | }, 22 | ) { 23 | // TODO: check that error is that function instead available (differentiate network fails) 24 | const [nonce, name, version, chainId] = await Promise.all([ 25 | optional?.nonce ?? token.nonces(wallet.address).catch(() => 0), 26 | optional?.name ?? token.name().catch(() => ''), 27 | optional?.version ?? '1', 28 | optional?.chainId ?? wallet.getChainId(), 29 | ]); 30 | 31 | const domain = { 32 | name: name, 33 | version: version, 34 | chainId: chainId, 35 | verifyingContract: token.address, 36 | }; 37 | 38 | const types = { 39 | Permit: [ 40 | { name: 'owner', type: 'address' }, 41 | { name: 'spender', type: 'address' }, 42 | { name: 'value', type: 'uint256' }, 43 | { name: 'nonce', type: 'uint256' }, 44 | { name: 'deadline', type: 'uint256' }, 45 | ], 46 | }; 47 | 48 | const message = { 49 | owner: wallet.address, 50 | spender: spender, 51 | value: value, 52 | nonce: nonce, 53 | deadline: deadline, 54 | }; 55 | 56 | const sig = await wallet._signTypedData(domain, types, message); 57 | return sig; 58 | } 59 | 60 | async function getPermitSigNoVersion( 61 | wallet: Wallet, 62 | token: Contract, 63 | spender: string, 64 | value: BigNumberish, 65 | deadline: BigNumberish, 66 | optional?: { nonce?: number; name?: string; chainId?: number }, 67 | ) { 68 | // TODO: check that error is that function instead available (differentiate network fails) 69 | const [nonce, name, chainId] = await Promise.all([ 70 | optional?.nonce ?? token.nonces(wallet.address).catch(() => 0), 71 | optional?.name ?? token.name().catch(() => ''), 72 | optional?.chainId ?? wallet.getChainId(), 73 | ]); 74 | 75 | const domain = { 76 | name: name, 77 | chainId: chainId, 78 | verifyingContract: token.address, 79 | }; 80 | 81 | const types = { 82 | Permit: [ 83 | { name: 'owner', type: 'address' }, 84 | { name: 'spender', type: 'address' }, 85 | { name: 'value', type: 'uint256' }, 86 | { name: 'nonce', type: 'uint256' }, 87 | { name: 'deadline', type: 'uint256' }, 88 | ], 89 | }; 90 | 91 | const message = { 92 | owner: wallet.address, 93 | spender: spender, 94 | value: value, 95 | nonce: nonce, 96 | deadline: deadline, 97 | }; 98 | 99 | const sig = await wallet._signTypedData(domain, types, message); 100 | return sig; 101 | } 102 | 103 | async function getDaiLikePermitSignature( 104 | wallet: Wallet, 105 | token: Contract, 106 | spender: string, 107 | deadline: BigNumberish, 108 | optional?: { nonce?: number; name?: string; chainId?: number }, 109 | ): Promise<[string, number]> { 110 | // TODO: check that error is that function instead available (differentiate network fails) 111 | const [nonce, name, chainId] = await Promise.all([ 112 | optional?.nonce ?? token.nonces(wallet.address).catch(() => 0), 113 | optional?.name ?? token.name().catch(() => ''), 114 | optional?.chainId ?? wallet.getChainId(), 115 | ]); 116 | 117 | const domain = { 118 | name: name, 119 | version: '1', 120 | chainId: chainId, 121 | verifyingContract: token.address, 122 | }; 123 | 124 | const types = { 125 | Permit: [ 126 | { name: 'holder', type: 'address' }, 127 | { name: 'spender', type: 'address' }, 128 | { name: 'nonce', type: 'uint256' }, 129 | { name: 'expiry', type: 'uint256' }, 130 | { name: 'allowed', type: 'bool' }, 131 | ], 132 | }; 133 | 134 | const message = { 135 | holder: wallet.address, 136 | spender: spender, 137 | nonce: nonce, 138 | expiry: deadline, 139 | allowed: true, 140 | }; 141 | 142 | const sig = await wallet._signTypedData(domain, types, message); 143 | return [sig, nonce]; 144 | } 145 | 146 | enum PermitTypes { 147 | Standard = 'Standard Permit', 148 | NoVersionInDomain = 'No Version in Domain', 149 | DaiLike = 'Dai-Like Sig/Permit', 150 | NoPermit = 'No Permit Enabled', 151 | } 152 | 153 | export const addPermitTags = async ( 154 | tokenList: ArbTokenList, 155 | ): Promise => { 156 | console.log('Adding permit tags'); 157 | const { l1, l2 } = await getNetworkConfig(); 158 | 159 | const value = utils.parseUnits('1.0', 18); 160 | const deadline = constants.MaxUint256; 161 | 162 | type Call = { 163 | tokenIndex: number; 164 | target: string; 165 | callData: string; 166 | }; 167 | const l1Calls: Array = []; 168 | const l2Calls: Array = []; 169 | 170 | const permitTokenInfo: ArbTokenInfo[] = [...tokenList.tokens]; 171 | 172 | for (let i = 0; i < permitTokenInfo.length; i++) { 173 | const curr = permitTokenInfo[i]; 174 | const isL1Token = curr.chainId === l2.network.parentChainId; 175 | const isL2Token = curr.chainId === l2.network.chainId; 176 | if (!isL1Token && !isL2Token) continue; 177 | 178 | const provider = isL1Token ? l1.provider : l2.provider; 179 | const wallet = Wallet.createRandom().connect(provider); 180 | const spender = Wallet.createRandom().connect(provider); 181 | 182 | const tokenContract = new Contract( 183 | curr.address, 184 | permitTokenAbi['abi'], 185 | wallet, 186 | ); 187 | 188 | const signature = await getPermitSig( 189 | wallet, 190 | tokenContract, 191 | spender.address, 192 | value, 193 | deadline, 194 | ); 195 | const { v, r, s } = utils.splitSignature(signature); 196 | const iface = new utils.Interface(permitTokenAbi['abi']); 197 | const callData = iface.encodeFunctionData('permit', [ 198 | wallet.address, 199 | spender.address, 200 | value, 201 | deadline, 202 | v, 203 | r, 204 | s, 205 | ]); 206 | 207 | // Permit no version 208 | const signatureNoVersion = await getPermitSigNoVersion( 209 | wallet, 210 | tokenContract, 211 | spender.address, 212 | value, 213 | deadline, 214 | ); 215 | const { v: vNo, r: rNo, s: sNo } = utils.splitSignature(signatureNoVersion); 216 | const callDataNoVersion = iface.encodeFunctionData('permit', [ 217 | wallet.address, 218 | spender.address, 219 | value, 220 | deadline, 221 | vNo, 222 | rNo, 223 | sNo, 224 | ]); 225 | 226 | // DAI permit 227 | const daiTokenContract = new Contract( 228 | curr.address, 229 | daiPermitTokenAbi, 230 | wallet, 231 | ); 232 | const signatureDAI = await getDaiLikePermitSignature( 233 | wallet, 234 | daiTokenContract, 235 | spender.address, 236 | deadline, 237 | ); 238 | const { v: vDAI, r: rDAI, s: sDAI } = utils.splitSignature(signatureDAI[0]); 239 | const ifaceDAI = new utils.Interface(daiPermitTokenAbi); 240 | const callDataDAI = ifaceDAI.encodeFunctionData('permit', [ 241 | wallet.address, 242 | spender.address, 243 | signatureDAI[1], 244 | deadline, 245 | true, 246 | vDAI, 247 | rDAI, 248 | sDAI, 249 | ]); 250 | 251 | (isL1Token ? l1Calls : l2Calls).push( 252 | { 253 | tokenIndex: i, 254 | target: curr.address, 255 | callData: callData, // normal permit 256 | }, 257 | { 258 | tokenIndex: i, 259 | target: curr.address, 260 | callData: callDataNoVersion, // no version permit 261 | }, 262 | { 263 | tokenIndex: i, 264 | target: curr.address, 265 | callData: callDataDAI, // DAI permit 266 | }, 267 | ); 268 | } 269 | 270 | const handleCalls = async (calls: Array, layer: 1 | 2) => { 271 | const tokenBridge = l2.network.tokenBridge; 272 | 273 | if (!tokenBridge) { 274 | throw new Error('Child network is missing tokenBridge'); 275 | } 276 | 277 | // TODO: use SDKs multicaller 278 | let multiCallAddr = 279 | tokenBridge[layer === 2 ? 'childMultiCall' : 'parentMultiCall']; 280 | const isL1Mainnet = layer === 1 && l2.network.parentChainId === 1; 281 | if (isL1Mainnet) 282 | multiCallAddr = '0x1b193bedb0b0a29c5759355d4193cb2838d2e170'; 283 | 284 | const provider = (layer === 1 ? l1 : l2).provider; 285 | const multicall = new Contract(multiCallAddr, multicallAbi, provider); 286 | // get array of results from tryAggregate 287 | const tryPermit = []; 288 | for (const chunk of getChunks(calls, 10)) { 289 | console.log('handling chunk of size', chunk.length); 290 | const curr = promiseRetrier(() => 291 | multicall.callStatic[ 292 | isL1Mainnet ? 'tryAggregateGasRation' : 'tryAggregate' 293 | ]( 294 | false, 295 | chunk.map((curr) => ({ 296 | target: curr.target, 297 | callData: curr.callData, 298 | })), 299 | ), 300 | ); 301 | tryPermit.push(...(await curr)); 302 | } 303 | tryPermit.flat(1); 304 | 305 | for (let i = 0; i < tryPermit.length; i += 3) { 306 | let tag; 307 | if (tryPermit[i].success === true) { 308 | tag = PermitTypes.Standard; 309 | } else if (tryPermit[i + 1].success === true) { 310 | tag = PermitTypes.NoVersionInDomain; 311 | } else if (tryPermit[i + 2].success === true) { 312 | tag = PermitTypes.DaiLike; 313 | } else { 314 | tag = PermitTypes.NoPermit; 315 | } 316 | const originalIndex = calls[i].tokenIndex; 317 | const info = permitTokenInfo[originalIndex]; 318 | 319 | // add to existing token lists w tags for all tokens (permit or no permit) 320 | const tags = info.tags ?? []; 321 | tags.push(tag); 322 | 323 | permitTokenInfo[originalIndex] = { 324 | ...permitTokenInfo[originalIndex], 325 | tags, 326 | }; 327 | } 328 | }; 329 | 330 | await handleCalls(l1Calls, 1); 331 | await handleCalls(l2Calls, 2); 332 | 333 | return { 334 | ...tokenList, 335 | tokens: permitTokenInfo, 336 | }; 337 | }; 338 | -------------------------------------------------------------------------------- /src/PermitTokens/permitTokenAbi.json: -------------------------------------------------------------------------------- 1 | { 2 | "abi": [ 3 | { 4 | "inputs": [ 5 | { 6 | "internalType": "string", 7 | "name": "name", 8 | "type": "string" 9 | }, 10 | { 11 | "internalType": "string", 12 | "name": "symbol", 13 | "type": "string" 14 | } 15 | ], 16 | "stateMutability": "nonpayable", 17 | "type": "constructor" 18 | }, 19 | { 20 | "anonymous": false, 21 | "inputs": [ 22 | { 23 | "indexed": true, 24 | "internalType": "address", 25 | "name": "owner", 26 | "type": "address" 27 | }, 28 | { 29 | "indexed": true, 30 | "internalType": "address", 31 | "name": "spender", 32 | "type": "address" 33 | }, 34 | { 35 | "indexed": false, 36 | "internalType": "uint256", 37 | "name": "value", 38 | "type": "uint256" 39 | } 40 | ], 41 | "name": "Approval", 42 | "type": "event" 43 | }, 44 | { 45 | "anonymous": false, 46 | "inputs": [ 47 | { 48 | "indexed": true, 49 | "internalType": "address", 50 | "name": "from", 51 | "type": "address" 52 | }, 53 | { 54 | "indexed": true, 55 | "internalType": "address", 56 | "name": "to", 57 | "type": "address" 58 | }, 59 | { 60 | "indexed": false, 61 | "internalType": "uint256", 62 | "name": "value", 63 | "type": "uint256" 64 | } 65 | ], 66 | "name": "Transfer", 67 | "type": "event" 68 | }, 69 | { 70 | "inputs": [], 71 | "name": "DOMAIN_SEPARATOR", 72 | "outputs": [ 73 | { 74 | "internalType": "bytes32", 75 | "name": "", 76 | "type": "bytes32" 77 | } 78 | ], 79 | "stateMutability": "view", 80 | "type": "function" 81 | }, 82 | { 83 | "inputs": [ 84 | { 85 | "internalType": "address", 86 | "name": "owner", 87 | "type": "address" 88 | }, 89 | { 90 | "internalType": "address", 91 | "name": "spender", 92 | "type": "address" 93 | } 94 | ], 95 | "name": "allowance", 96 | "outputs": [ 97 | { 98 | "internalType": "uint256", 99 | "name": "", 100 | "type": "uint256" 101 | } 102 | ], 103 | "stateMutability": "view", 104 | "type": "function" 105 | }, 106 | { 107 | "inputs": [ 108 | { 109 | "internalType": "address", 110 | "name": "spender", 111 | "type": "address" 112 | }, 113 | { 114 | "internalType": "uint256", 115 | "name": "amount", 116 | "type": "uint256" 117 | } 118 | ], 119 | "name": "approve", 120 | "outputs": [ 121 | { 122 | "internalType": "bool", 123 | "name": "", 124 | "type": "bool" 125 | } 126 | ], 127 | "stateMutability": "nonpayable", 128 | "type": "function" 129 | }, 130 | { 131 | "inputs": [ 132 | { 133 | "internalType": "address", 134 | "name": "account", 135 | "type": "address" 136 | } 137 | ], 138 | "name": "balanceOf", 139 | "outputs": [ 140 | { 141 | "internalType": "uint256", 142 | "name": "", 143 | "type": "uint256" 144 | } 145 | ], 146 | "stateMutability": "view", 147 | "type": "function" 148 | }, 149 | { 150 | "inputs": [], 151 | "name": "decimals", 152 | "outputs": [ 153 | { 154 | "internalType": "uint8", 155 | "name": "", 156 | "type": "uint8" 157 | } 158 | ], 159 | "stateMutability": "view", 160 | "type": "function" 161 | }, 162 | { 163 | "inputs": [ 164 | { 165 | "internalType": "address", 166 | "name": "spender", 167 | "type": "address" 168 | }, 169 | { 170 | "internalType": "uint256", 171 | "name": "subtractedValue", 172 | "type": "uint256" 173 | } 174 | ], 175 | "name": "decreaseAllowance", 176 | "outputs": [ 177 | { 178 | "internalType": "bool", 179 | "name": "", 180 | "type": "bool" 181 | } 182 | ], 183 | "stateMutability": "nonpayable", 184 | "type": "function" 185 | }, 186 | { 187 | "inputs": [ 188 | { 189 | "internalType": "address", 190 | "name": "spender", 191 | "type": "address" 192 | }, 193 | { 194 | "internalType": "uint256", 195 | "name": "addedValue", 196 | "type": "uint256" 197 | } 198 | ], 199 | "name": "increaseAllowance", 200 | "outputs": [ 201 | { 202 | "internalType": "bool", 203 | "name": "", 204 | "type": "bool" 205 | } 206 | ], 207 | "stateMutability": "nonpayable", 208 | "type": "function" 209 | }, 210 | { 211 | "inputs": [], 212 | "name": "name", 213 | "outputs": [ 214 | { 215 | "internalType": "string", 216 | "name": "", 217 | "type": "string" 218 | } 219 | ], 220 | "stateMutability": "view", 221 | "type": "function" 222 | }, 223 | { 224 | "inputs": [ 225 | { 226 | "internalType": "address", 227 | "name": "owner", 228 | "type": "address" 229 | } 230 | ], 231 | "name": "nonces", 232 | "outputs": [ 233 | { 234 | "internalType": "uint256", 235 | "name": "", 236 | "type": "uint256" 237 | } 238 | ], 239 | "stateMutability": "view", 240 | "type": "function" 241 | }, 242 | { 243 | "inputs": [ 244 | { 245 | "internalType": "address", 246 | "name": "owner", 247 | "type": "address" 248 | }, 249 | { 250 | "internalType": "address", 251 | "name": "spender", 252 | "type": "address" 253 | }, 254 | { 255 | "internalType": "uint256", 256 | "name": "value", 257 | "type": "uint256" 258 | }, 259 | { 260 | "internalType": "uint256", 261 | "name": "deadline", 262 | "type": "uint256" 263 | }, 264 | { 265 | "internalType": "uint8", 266 | "name": "v", 267 | "type": "uint8" 268 | }, 269 | { 270 | "internalType": "bytes32", 271 | "name": "r", 272 | "type": "bytes32" 273 | }, 274 | { 275 | "internalType": "bytes32", 276 | "name": "s", 277 | "type": "bytes32" 278 | } 279 | ], 280 | "name": "permit", 281 | "outputs": [], 282 | "stateMutability": "nonpayable", 283 | "type": "function" 284 | }, 285 | { 286 | "inputs": [], 287 | "name": "symbol", 288 | "outputs": [ 289 | { 290 | "internalType": "string", 291 | "name": "", 292 | "type": "string" 293 | } 294 | ], 295 | "stateMutability": "view", 296 | "type": "function" 297 | }, 298 | { 299 | "inputs": [], 300 | "name": "totalSupply", 301 | "outputs": [ 302 | { 303 | "internalType": "uint256", 304 | "name": "", 305 | "type": "uint256" 306 | } 307 | ], 308 | "stateMutability": "view", 309 | "type": "function" 310 | }, 311 | { 312 | "inputs": [ 313 | { 314 | "internalType": "address", 315 | "name": "to", 316 | "type": "address" 317 | }, 318 | { 319 | "internalType": "uint256", 320 | "name": "amount", 321 | "type": "uint256" 322 | } 323 | ], 324 | "name": "transfer", 325 | "outputs": [ 326 | { 327 | "internalType": "bool", 328 | "name": "", 329 | "type": "bool" 330 | } 331 | ], 332 | "stateMutability": "nonpayable", 333 | "type": "function" 334 | }, 335 | { 336 | "inputs": [ 337 | { 338 | "internalType": "address", 339 | "name": "from", 340 | "type": "address" 341 | }, 342 | { 343 | "internalType": "address", 344 | "name": "to", 345 | "type": "address" 346 | }, 347 | { 348 | "internalType": "uint256", 349 | "name": "amount", 350 | "type": "uint256" 351 | } 352 | ], 353 | "name": "transferFrom", 354 | "outputs": [ 355 | { 356 | "internalType": "bool", 357 | "name": "", 358 | "type": "bool" 359 | } 360 | ], 361 | "stateMutability": "nonpayable", 362 | "type": "function" 363 | } 364 | ] 365 | } 366 | -------------------------------------------------------------------------------- /src/WarningList/warningTokens.json: -------------------------------------------------------------------------------- 1 | { 2 | "0x383518188c0c6d7730d91b2c03a03c837814a899": { 3 | "address": "0x383518188c0c6d7730d91b2c03a03c837814a899", 4 | "type": 0 5 | }, 6 | "0x0e2298e3b3390e3b945a5456fbf59ecc3f55da16": { 7 | "address": "0x0e2298e3b3390e3b945a5456fbf59ecc3f55da16", 8 | "type": 0 9 | }, 10 | "0xd46ba6d942050d489dbd938a2c909a5d5039a161": { 11 | "address": "0xd46ba6d942050d489dbd938a2c909a5d5039a161", 12 | "type": 0 13 | }, 14 | "0x798d1be841a82a273720ce31c822c61a67a601c3": { 15 | "address": "0x798d1be841a82a273720ce31c822c61a67a601c3", 16 | "type": 0 17 | }, 18 | "0xe8b251822d003a2b2466ee0e38391c2db2048739": { 19 | "address": "0xe8b251822d003a2b2466ee0e38391c2db2048739", 20 | "type": 0 21 | }, 22 | "0x3d1e3c5f658d74c585267350cac22fd44e8d951c": { 23 | "address": "0x3d1e3c5f658d74c585267350cac22fd44e8d951c", 24 | "type": 0 25 | }, 26 | "0x67c597624b17b16fb77959217360b7cd18284253": { 27 | "address": "0x67c597624b17b16fb77959217360b7cd18284253", 28 | "type": 0 29 | }, 30 | "0x1c7bbadc81e18f7177a95eb1593e5f5f35861b10": { 31 | "address": "0x1c7bbadc81e18f7177a95eb1593e5f5f35861b10", 32 | "type": 0 33 | }, 34 | "0x68a118ef45063051eac49c7e647ce5ace48a68a5": { 35 | "address": "0x68a118ef45063051eac49c7e647ce5ace48a68a5", 36 | "type": 0 37 | }, 38 | "0x2f6081e3552b1c86ce4479b80062a1dda8ef23e3": { 39 | "address": "0x2f6081e3552b1c86ce4479b80062a1dda8ef23e3", 40 | "type": 0 41 | }, 42 | "0x07150e919b4de5fd6a63de1f9384828396f25fdc": { 43 | "address": "0x07150e919b4de5fd6a63de1f9384828396f25fdc", 44 | "type": 0 45 | }, 46 | "0x39795344cbcc76cc3fb94b9d1b15c23c2070c66d": { 47 | "address": "0x39795344cbcc76cc3fb94b9d1b15c23c2070c66d", 48 | "type": 0 49 | }, 50 | "0x9248c485b0b80f76da451f167a8db30f33c70907": { 51 | "address": "0x9248c485b0b80f76da451f167a8db30f33c70907", 52 | "type": 0 53 | }, 54 | "0x7777770f8a6632ff043c8833310e245eba9209e6": { 55 | "address": "0x7777770f8a6632ff043c8833310e245eba9209e6", 56 | "type": 0 57 | }, 58 | "0x3936ad01cf109a36489d93cabda11cf062fd3d48": { 59 | "address": "0x3936ad01cf109a36489d93cabda11cf062fd3d48", 60 | "type": 0 61 | }, 62 | "0x3fa807b6f8d4c407e6e605368f4372d14658b38c": { 63 | "address": "0x3fa807b6f8d4c407e6e605368f4372d14658b38c", 64 | "type": 0 65 | }, 66 | "0x15e4132dcd932e8990e794d1300011a472819cbd": { 67 | "address": "0x15e4132dcd932e8990e794d1300011a472819cbd", 68 | "type": 0 69 | }, 70 | "0x5166d4ce79b9bf7df477da110c560ce3045aa889": { 71 | "address": "0x5166d4ce79b9bf7df477da110c560ce3045aa889", 72 | "type": 0 73 | }, 74 | "0xf911a7ec46a2c6fa49193212fe4a2a9b95851c27": { 75 | "address": "0xf911a7ec46a2c6fa49193212fe4a2a9b95851c27", 76 | "type": 0 77 | }, 78 | "0x04f2694c8fcee23e8fd0dfea1d4f5bb8c352111f": { 79 | "address": "0x04f2694c8fcee23e8fd0dfea1d4f5bb8c352111f", 80 | "type": 0 81 | }, 82 | "0xfc1e690f61efd961294b3e1ce3313fbd8aa4f85d": { 83 | "address": "0xfc1e690f61efd961294b3e1ce3313fbd8aa4f85d", 84 | "type": 1 85 | }, 86 | "0x4da9b813057d04baef4e5800e36083717b4a0341": { 87 | "address": "0x4da9b813057d04baef4e5800e36083717b4a0341", 88 | "type": 1 89 | }, 90 | "0x9ba00d6856a4edf4665bca2c2309936572473b7e": { 91 | "address": "0x9ba00d6856a4edf4665bca2c2309936572473b7e", 92 | "type": 1 93 | }, 94 | "0x71fc860f7d3a592a4a98740e39db31d25db65ae8": { 95 | "address": "0x71fc860f7d3a592a4a98740e39db31d25db65ae8", 96 | "type": 1 97 | }, 98 | "0x625ae63000f46200499120b906716420bd059240": { 99 | "address": "0x625ae63000f46200499120b906716420bd059240", 100 | "type": 1 101 | }, 102 | "0x7d2d3688df45ce7c552e19c27e007673da9204b8": { 103 | "address": "0x7d2d3688df45ce7c552e19c27e007673da9204b8", 104 | "type": 1 105 | }, 106 | "0xe1ba0fb44ccb0d11b80f92f4f8ed94ca3ff51d00": { 107 | "address": "0xe1ba0fb44ccb0d11b80f92f4f8ed94ca3ff51d00", 108 | "type": 1 109 | }, 110 | "0x3a3a65aab0dd2a17e3f1947ba16138cd37d08c04": { 111 | "address": "0x3a3a65aab0dd2a17e3f1947ba16138cd37d08c04", 112 | "type": 1 113 | }, 114 | "0xa64bd6c70cb9051f6a9ba1f163fdc07e0dfb5f84": { 115 | "address": "0xa64bd6c70cb9051f6a9ba1f163fdc07e0dfb5f84", 116 | "type": 1 117 | }, 118 | "0x9d91be44c06d373a8a226e1f3b146956083803eb": { 119 | "address": "0x9d91be44c06d373a8a226e1f3b146956083803eb", 120 | "type": 1 121 | }, 122 | "0x71010a9d003445ac60c4e6a7017c1e89a477b438": { 123 | "address": "0x71010a9d003445ac60c4e6a7017c1e89a477b438", 124 | "type": 1 125 | }, 126 | "0x7deb5e830be29f91e298ba5ff1356bb7f8146998": { 127 | "address": "0x7deb5e830be29f91e298ba5ff1356bb7f8146998", 128 | "type": 1 129 | }, 130 | "0x6fce4a401b6b80ace52baaefe4421bd188e76f6f": { 131 | "address": "0x6fce4a401b6b80ace52baaefe4421bd188e76f6f", 132 | "type": 1 133 | }, 134 | "0x6fb0855c404e09c47c3fbca25f08d4e41f9f062f": { 135 | "address": "0x6fb0855c404e09c47c3fbca25f08d4e41f9f062f", 136 | "type": 1 137 | }, 138 | "0x328c4c80bc7aca0834db37e6600a6c49e12da4de": { 139 | "address": "0x328c4c80bc7aca0834db37e6600a6c49e12da4de", 140 | "type": 1 141 | }, 142 | "0xfc4b8ed459e00e5400be803a9bb3954234fd50e3": { 143 | "address": "0xfc4b8ed459e00e5400be803a9bb3954234fd50e3", 144 | "type": 1 145 | }, 146 | "0x6ee0f7bb50a54ab5253da0667b0dc2ee526c30a8": { 147 | "address": "0x6ee0f7bb50a54ab5253da0667b0dc2ee526c30a8", 148 | "type": 1 149 | }, 150 | "0x712db54daa836b53ef1ecbb9c6ba3b9efb073f40": { 151 | "address": "0x712db54daa836b53ef1ecbb9c6ba3b9efb073f40", 152 | "type": 1 153 | }, 154 | "0x69948cc03f478b95283f7dbf1ce764d0fc7ec54c": { 155 | "address": "0x69948cc03f478b95283f7dbf1ce764d0fc7ec54c", 156 | "type": 1 157 | }, 158 | "0x12e51e77daaa58aa0e9247db7510ea4b46f9bead": { 159 | "address": "0x12e51e77daaa58aa0e9247db7510ea4b46f9bead", 160 | "type": 1 161 | }, 162 | "0xba3d9687cf50fe253cd2e1cfeede1d6787344ed5": { 163 | "address": "0xba3d9687cf50fe253cd2e1cfeede1d6787344ed5", 164 | "type": 1 165 | }, 166 | "0xb124541127a0a657f056d9dd06188c4f1b0e5aab": { 167 | "address": "0xb124541127a0a657f056d9dd06188c4f1b0e5aab", 168 | "type": 1 169 | }, 170 | "0x6179078872605396ee62960917128f9477a5ddbb": { 171 | "address": "0x6179078872605396ee62960917128f9477a5ddbb", 172 | "type": 1 173 | }, 174 | "0x048930eec73c91b44b0844aeacdebadc2f2b6efb": { 175 | "address": "0x048930eec73c91b44b0844aeacdebadc2f2b6efb", 176 | "type": 1 177 | }, 178 | "0xe02b2ad63eff3ac1d5827cbd7ab9dd3dac4f4ad0": { 179 | "address": "0xe02b2ad63eff3ac1d5827cbd7ab9dd3dac4f4ad0", 180 | "type": 1 181 | }, 182 | "0xb977ee318010a5252774171494a1bcb98e7fab65": { 183 | "address": "0xb977ee318010a5252774171494a1bcb98e7fab65", 184 | "type": 1 185 | }, 186 | "0xbbbb7f2ac04484f7f04a2c2c16f20479791bbb44": { 187 | "address": "0xbbbb7f2ac04484f7f04a2c2c16f20479791bbb44", 188 | "type": 1 189 | }, 190 | "0x1d0e53a0e524e3cc92c1f0f33ae268fff8d7e7a5": { 191 | "address": "0x1d0e53a0e524e3cc92c1f0f33ae268fff8d7e7a5", 192 | "type": 1 193 | }, 194 | "0x84bbcab430717ff832c3904fa6515f97fc63c76f": { 195 | "address": "0x84bbcab430717ff832c3904fa6515f97fc63c76f", 196 | "type": 1 197 | }, 198 | "0xc88ebbf7c523f38ef3eb8a151273c0f0da421e63": { 199 | "address": "0xc88ebbf7c523f38ef3eb8a151273c0f0da421e63", 200 | "type": 1 201 | }, 202 | "0x8c69f7a4c9b38f1b48005d216c398efb2f1ce3e4": { 203 | "address": "0x8c69f7a4c9b38f1b48005d216c398efb2f1ce3e4", 204 | "type": 1 205 | }, 206 | "0x9548db8b1ca9b6c757485e7861918b640390169c": { 207 | "address": "0x9548db8b1ca9b6c757485e7861918b640390169c", 208 | "type": 1 209 | }, 210 | "0x3ed3b47dd13ec9a98b44e6204a523e766b225811": { 211 | "address": "0x3ed3b47dd13ec9a98b44e6204a523e766b225811", 212 | "type": 1 213 | }, 214 | "0x9ff58f4ffb29fa2266ab25e75e2a8b3503311656": { 215 | "address": "0x9ff58f4ffb29fa2266ab25e75e2a8b3503311656", 216 | "type": 1 217 | }, 218 | "0x030ba81f1c18d280636f32af80b9aad02cf0854e": { 219 | "address": "0x030ba81f1c18d280636f32af80b9aad02cf0854e", 220 | "type": 1 221 | }, 222 | "0x5165d24277cd063f5ac44efd447b27025e888f37": { 223 | "address": "0x5165d24277cd063f5ac44efd447b27025e888f37", 224 | "type": 1 225 | }, 226 | "0xdf7ff54aacacbff42dfe29dd6144a69b629f8c9e": { 227 | "address": "0xdf7ff54aacacbff42dfe29dd6144a69b629f8c9e", 228 | "type": 1 229 | }, 230 | "0xb9d7cb55f463405cdfbe4e90a6d2df01c2b92bf1": { 231 | "address": "0xb9d7cb55f463405cdfbe4e90a6d2df01c2b92bf1", 232 | "type": 1 233 | }, 234 | "0xffc97d72e13e01096502cb8eb52dee56f74dad7b": { 235 | "address": "0xffc97d72e13e01096502cb8eb52dee56f74dad7b", 236 | "type": 1 237 | }, 238 | "0x05ec93c0365baaeabf7aeffb0972ea7ecdd39cf1": { 239 | "address": "0x05ec93c0365baaeabf7aeffb0972ea7ecdd39cf1", 240 | "type": 1 241 | }, 242 | "0xa361718326c15715591c299427c62086f69923d9": { 243 | "address": "0xa361718326c15715591c299427c62086f69923d9", 244 | "type": 1 245 | }, 246 | "0x028171bca77440897b824ca71d1c56cac55b68a3": { 247 | "address": "0x028171bca77440897b824ca71d1c56cac55b68a3", 248 | "type": 1 249 | }, 250 | "0xac6df26a590f08dcc95d5a4705ae8abbc88509ef": { 251 | "address": "0xac6df26a590f08dcc95d5a4705ae8abbc88509ef", 252 | "type": 1 253 | }, 254 | "0x39c6b3e42d6a679d7d776778fe880bc9487c2eda": { 255 | "address": "0x39c6b3e42d6a679d7d776778fe880bc9487c2eda", 256 | "type": 1 257 | }, 258 | "0xa06bc25b5805d5f8d82847d191cb4af5a3e873e0": { 259 | "address": "0xa06bc25b5805d5f8d82847d191cb4af5a3e873e0", 260 | "type": 1 261 | }, 262 | "0xa685a61171bb30d4072b338c80cb7b2c865c873e": { 263 | "address": "0xa685a61171bb30d4072b338c80cb7b2c865c873e", 264 | "type": 1 265 | }, 266 | "0xc713e5e149d5d0715dcd1c156a020976e7e56b88": { 267 | "address": "0xc713e5e149d5d0715dcd1c156a020976e7e56b88", 268 | "type": 1 269 | }, 270 | "0xcc12abe4ff81c9378d670de1b57f8e0dd228d77a": { 271 | "address": "0xcc12abe4ff81c9378d670de1b57f8e0dd228d77a", 272 | "type": 1 273 | }, 274 | "0x35f6b052c598d933d69a4eec4d04c73a191fe6c2": { 275 | "address": "0x35f6b052c598d933d69a4eec4d04c73a191fe6c2", 276 | "type": 1 277 | }, 278 | "0x6c5024cd4f8a59110119c56f8933403a539555eb": { 279 | "address": "0x6c5024cd4f8a59110119c56f8933403a539555eb", 280 | "type": 1 281 | }, 282 | "0x101cc05f4a51c0319f570d5e146a8c625198e636": { 283 | "address": "0x101cc05f4a51c0319f570d5e146a8c625198e636", 284 | "type": 1 285 | }, 286 | "0xbcca60bb61934080951369a648fb03df4f96263c": { 287 | "address": "0xbcca60bb61934080951369a648fb03df4f96263c", 288 | "type": 1 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/commands/allTokensList.ts: -------------------------------------------------------------------------------- 1 | import { ArbTokenList } from '../lib/types'; 2 | import { generateFullListFormatted } from '../lib/token_list_gen'; 3 | import { addPermitTags } from '../PermitTokens/permitSignature'; 4 | import { writeToFile } from '../lib/store'; 5 | import { Action, Args } from '../lib/options'; 6 | 7 | export const command = Action.AllTokensList; 8 | 9 | export const describe = 'All tokens list'; 10 | 11 | export const handler = async (argvs: Args) => { 12 | let tokenList: ArbTokenList = await generateFullListFormatted(); 13 | if (argvs.includePermitTags) tokenList = await addPermitTags(tokenList); 14 | writeToFile(tokenList, argvs.newArbifiedList); 15 | return tokenList; 16 | }; 17 | -------------------------------------------------------------------------------- /src/commands/arbify.ts: -------------------------------------------------------------------------------- 1 | import { ArbTokenList } from '../lib/types'; 2 | import { addPermitTags } from '../PermitTokens/permitSignature'; 3 | import { writeToFile } from '../lib/store'; 4 | import { Action, Args } from '../lib/options'; 5 | import { arbifyL1List } from '../lib/token_list_gen'; 6 | 7 | export const command = Action.Arbify; 8 | 9 | export const describe = 'Arbify'; 10 | 11 | export const handler = async (argvs: Args) => { 12 | const includeOldDataFields = !!argvs.includeOldDataFields; 13 | 14 | const { newList } = await arbifyL1List(argvs.tokenList, { 15 | includeOldDataFields, 16 | ignorePreviousList: argvs.ignorePreviousList, 17 | prevArbifiedList: argvs.prevArbifiedList, 18 | }); 19 | let tokenList: ArbTokenList = newList; 20 | 21 | if (argvs.includePermitTags) tokenList = await addPermitTags(tokenList); 22 | writeToFile(tokenList, argvs.newArbifiedList); 23 | return tokenList; 24 | }; 25 | -------------------------------------------------------------------------------- /src/commands/full.ts: -------------------------------------------------------------------------------- 1 | import { generateFullList } from '../lib/token_list_gen'; 2 | import { writeToFile } from '../lib/store'; 3 | import { Action, Args } from '../lib/options'; 4 | 5 | export const command = Action.Full; 6 | 7 | export const describe = 'Full'; 8 | 9 | export const handler = async (argvs: Args) => { 10 | if (argvs.tokenList !== 'full') 11 | throw new Error("expected --tokenList 'full'"); 12 | if (argvs.includePermitTags) 13 | throw new Error('full list mode does not support permit tagging'); 14 | const tokenList = await generateFullList(); 15 | writeToFile(tokenList, argvs.newArbifiedList); 16 | return tokenList; 17 | }; 18 | -------------------------------------------------------------------------------- /src/commands/update.ts: -------------------------------------------------------------------------------- 1 | import { ArbTokenList } from '../lib/types'; 2 | import { updateArbifiedList } from '../lib/token_list_gen'; 3 | import { addPermitTags } from '../PermitTokens/permitSignature'; 4 | import { writeToFile } from '../lib/store'; 5 | import { Action, Args } from '../lib/options'; 6 | 7 | export const command = Action.Update; 8 | 9 | export const describe = 'Update'; 10 | 11 | export const handler = async (argvs: Args) => { 12 | const includeOldDataFields = !!argvs.includeOldDataFields; 13 | const { newList } = await updateArbifiedList(argvs.tokenList, { 14 | includeOldDataFields, 15 | ignorePreviousList: argvs.ignorePreviousList, 16 | prevArbifiedList: argvs.prevArbifiedList, 17 | }); 18 | let tokenList: ArbTokenList = newList; 19 | 20 | if (argvs.includePermitTags) tokenList = await addPermitTags(tokenList); 21 | writeToFile(tokenList, argvs.newArbifiedList); 22 | return tokenList; 23 | }; 24 | -------------------------------------------------------------------------------- /src/customNetworks.ts: -------------------------------------------------------------------------------- 1 | import { ArbitrumNetwork } from '@arbitrum/sdk'; 2 | import orbitChainsData from './Assets/orbitChainsData.json'; 3 | 4 | const excludedNetworksIds: number[] = []; 5 | 6 | export const customNetworks = ( 7 | orbitChainsData.data as ArbitrumNetwork[] 8 | ).filter((chain) => !excludedNetworksIds.includes(chain.chainId)); 9 | 10 | const orbitChainsRpc = orbitChainsData.data.reduce((acc, chain) => { 11 | acc[chain.chainId] = chain.rpcUrl; 12 | return acc; 13 | }, {} as Record); 14 | 15 | if (!process.env.ARB_ONE_RPC) { 16 | throw new Error('process.env.ARB_ONE_RPC was not set'); 17 | } 18 | if (!process.env.ARB_NOVA_RPC) { 19 | throw new Error('process.env.ARB_NOVA_RPC was not set'); 20 | } 21 | if (!process.env.ARB_SEPOLIA_RPC) { 22 | throw new Error('process.env.ARB_SEPOLIA_RPC was not set'); 23 | } 24 | 25 | export const rpcs: Record = { 26 | // Arbitrum networks 27 | 42161: process.env.ARB_ONE_RPC, 28 | 42170: process.env.ARB_NOVA_RPC, 29 | 421614: process.env.ARB_SEPOLIA_RPC, 30 | // Orbit chains 31 | ...orbitChainsRpc, 32 | }; 33 | -------------------------------------------------------------------------------- /src/init.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | import dotenvExpand from 'dotenv-expand'; 3 | import { registerCustomArbitrumNetwork } from '@arbitrum/sdk'; 4 | 5 | const myEnv = dotenv.config(); 6 | dotenvExpand.expand(myEnv); 7 | 8 | import { customNetworks } from './customNetworks'; 9 | 10 | (async () => { 11 | customNetworks.forEach((network) => registerCustomArbitrumNetwork(network)); 12 | })(); 13 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export interface L2ToL1GatewayAddresses { 2 | [contractAddress: string]: string; 3 | } 4 | 5 | const objKeyAndValToLowerCase = (obj: { [key: string]: string }) => 6 | Object.keys(obj).reduce((acc: { [key: string]: string }, key) => { 7 | acc[key.toLowerCase()] = obj[key].toLowerCase(); 8 | return acc; 9 | }, {}); 10 | 11 | // TODO: read these values from the gateway or a subgraph 12 | export const l2ToL1GatewayAddresses: L2ToL1GatewayAddresses = 13 | objKeyAndValToLowerCase({ 14 | // L2 ERC20 Gateway mainnet 15 | '0x09e9222e96e7b4ae2a407b98d48e330053351eee': 16 | '0xa3A7B6F88361F48403514059F1F16C8E78d60EeC', 17 | // L2 Arb-Custom Gateway mainnet 18 | '0x096760f208390250649e3e8763348e783aef5562': 19 | '0xcEe284F754E854890e311e3280b767F80797180d', 20 | // L2 weth mainnet 21 | '0x6c411ad3e74de3e7bd422b94a27770f5b86c623b': 22 | '0xd92023E9d9911199a6711321D1277285e6d4e2db', 23 | // L2 dai gateway mainnet 24 | '0x467194771dae2967aef3ecbedd3bf9a310c76c65': 25 | '0xd3b5b60020504bc3489d6949d545893982ba3011', 26 | // graph gateway arb1 27 | '0x65E1a5e8946e7E87d9774f5288f41c30a99fD302': 28 | '0x01cDC91B0A9bA741903aA3699BF4CE31d6C5cC06', 29 | 30 | // L2 ERC20 Gateway rinkeby 31 | '0x195c107f3f75c4c93eba7d9a1312f19305d6375f': 32 | '0x91169Dbb45e6804743F94609De50D511C437572E', 33 | // L2 Arb-Custom Gateway rinkeby 34 | '0x9b014455acc2fe90c52803849d0002aeec184a06': 35 | '0x917dc9a69F65dC3082D518192cd3725E1Fa96cA2', 36 | // L2 Weth Gateway rinkeby 37 | '0xf94bc045c4e926cc0b34e8d1c41cd7a043304ac9': 38 | '0x81d1a19cf7071732D4313c75dE8DD5b8CF697eFD', 39 | // old L2 weth gateway in rinkeby? we can prob remove this 40 | '0xf90eb31045d5b924900aff29344deb42eae0b087': 41 | '0x81d1a19cf7071732D4313c75dE8DD5b8CF697eFD', 42 | // livepeer gateway mainnet 43 | '0x6d2457a4ad276000a615295f7a80f79e48ccd318': 44 | '0x6142f1C8bBF02E6A6bd074E8d564c9A5420a0676', 45 | // Lido gateway Arb1 46 | '0x07d4692291b9e30e326fd31706f686f83f331b82': 47 | '0x0f25c1dc2a9922304f2eac71dca9b07e310e8e5a', 48 | 49 | // 421613: arbstandard gateway: 50 | '0x2ec7bc552ce8e51f098325d2fcf0d3b9d3d2a9a2': 51 | '0x715D99480b77A8d9D603638e593a539E21345FdF', 52 | 53 | // 421613: custom gateway: 54 | '0x8b6990830cF135318f75182487A4D7698549C717': 55 | '0x9fDD1C4E4AA24EEc1d913FABea925594a20d43C7', 56 | 57 | // 421613: WETH gateway: 58 | '0xf9F2e89c8347BD96742Cc07095dee490e64301d6': 59 | '0x6e244cD02BBB8a6dbd7F626f05B2ef82151Ab502', 60 | 61 | // 421613 graph gateway: 62 | '0xef2757855d2802bA53733901F90C91645973f743': 63 | '0xc82fF7b51c3e593D709BA3dE1b3a0d233D1DEca1', 64 | }); 65 | 66 | // nova 67 | export const l2ToL1GatewayAddressesNova: L2ToL1GatewayAddresses = 68 | objKeyAndValToLowerCase({ 69 | // L2 ERC20 Gateway mainnet 70 | '0xcf9bab7e53dde48a6dc4f286cb14e05298799257': 71 | '0xb2535b988dce19f9d71dfb22db6da744acac21bf', 72 | // L2 Arb-Custom Gatewa mainnet 73 | '0xbf544970e6bd77b21c6492c281ab60d0770451f4': 74 | '0x23122da8c581aa7e0d07a36ff1f16f799650232f', 75 | // L2 weth mainnet 76 | '0x7626841cb6113412f9c88d3adc720c9fac88d9ed': 77 | '0xe4e2121b479017955be0b175305b35f312330bae', 78 | 79 | // L2 dai gateway mainnet 80 | '0x10e6593cdda8c58a1d0f14c5164b376352a55f2f': 81 | '0x97f63339374fce157aa8ee27830172d2af76a786', 82 | }); 83 | 84 | export const excludeList = [ 85 | '0x0CE51000d5244F1EAac0B313a792D5a5f96931BF', //rkr 86 | '0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f', //in 87 | '0xEDA6eFE5556e134Ef52f2F858aa1e81c84CDA84b', // bad cap 88 | '0xe54942077Df7b8EEf8D4e6bCe2f7B58B0082b0cd', // swapr 89 | '0x282db609e787a132391eb64820ba6129fceb2695', // amy 90 | '0x99d8a9c45b2eca8864373a26d1459e3dff1e17f3', // mim 91 | '0x106538cc16f938776c7c180186975bca23875287', // remove once bridged (basv2) 92 | '0xB4A3B0Faf0Ab53df58001804DdA5Bfc6a3D59008', // spera 93 | // "0x960b236a07cf122663c4303350609a66a7b288c0", //aragon old 94 | ].map((s) => s.toLowerCase()); 95 | 96 | export const BridgedUSDCContractAddressArb1 = 97 | '0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8'; 98 | 99 | export const SEVEN_DAYS_IN_SECONDS = 7 * 24 * 60 * 60; 100 | -------------------------------------------------------------------------------- /src/lib/getVersion.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Version, 3 | VersionUpgrade, 4 | diffTokenLists, 5 | minVersionBump, 6 | nextVersion, 7 | } from '@uniswap/token-lists'; 8 | import { ArbTokenInfo, ArbTokenList } from './types'; 9 | import { isDeepStrictEqual } from 'util'; 10 | 11 | function createTokensMap(tokens: ArbTokenInfo[]) { 12 | return tokens 13 | .filter((token) => token.extensions) 14 | .reduce((acc, value) => { 15 | acc.set(value.address, value.extensions); 16 | return acc; 17 | }, new Map()); 18 | } 19 | 20 | function getVersionWithExtensions( 21 | prevArbTokenList: ArbTokenList, 22 | arbifiedTokenList: ArbTokenInfo[], 23 | versionBump: VersionUpgrade, 24 | ) { 25 | const prevTokens = createTokensMap(prevArbTokenList.tokens); 26 | const newTokens = createTokensMap(arbifiedTokenList); 27 | 28 | if (newTokens.size > prevTokens.size) { 29 | return nextVersion(prevArbTokenList.version, VersionUpgrade.MINOR); 30 | } 31 | 32 | if (newTokens.size < prevTokens.size) { 33 | return nextVersion(prevArbTokenList.version, VersionUpgrade.MAJOR); 34 | } 35 | 36 | versionBump = VersionUpgrade.NONE; 37 | for (const [key] of prevTokens) { 38 | const prevExtension = prevTokens.get(key); 39 | const newExtensions = newTokens.get(key); 40 | 41 | // Extensions were removed, bump to major 42 | if (prevExtension && !newExtensions) { 43 | return nextVersion(prevArbTokenList.version, VersionUpgrade.MAJOR); 44 | } 45 | 46 | // Extensions were added, bump to minor. 47 | if (!prevExtension && newExtensions) { 48 | versionBump = VersionUpgrade.MINOR; 49 | } 50 | 51 | if (!isDeepStrictEqual(prevExtension, newExtensions)) { 52 | // If versionBump was changed to MINOR, don't change it 53 | if (versionBump !== VersionUpgrade.MINOR) { 54 | versionBump = VersionUpgrade.PATCH; 55 | } 56 | } 57 | } 58 | 59 | return nextVersion(prevArbTokenList.version, versionBump); 60 | } 61 | 62 | function getVersion( 63 | prevArbTokenList: ArbTokenList | null | undefined, 64 | arbifiedTokenList: ArbTokenInfo[], 65 | ): Version { 66 | if (!prevArbTokenList) { 67 | return { 68 | major: 1, 69 | minor: 0, 70 | patch: 0, 71 | }; 72 | } 73 | 74 | const versionBump = minVersionBump( 75 | prevArbTokenList.tokens, 76 | arbifiedTokenList, 77 | ); 78 | const diff = diffTokenLists(prevArbTokenList.tokens, arbifiedTokenList); 79 | 80 | // Uniswap bump version to PATCH if any token has extensions property 81 | const chainIds = Object.keys(diff.changed); 82 | const isOnlyExtensionsDiff = chainIds.every((chainId) => { 83 | const changes = new Set(...Object.values(diff.changed[Number(chainId)])); // Use set to remove all duplicates 84 | return changes.size === 1 && changes.has('extensions'); 85 | }); 86 | 87 | // If we only have ['extensions'] changes, we need to check if the change is a false positive 88 | if (!isOnlyExtensionsDiff || versionBump !== VersionUpgrade.PATCH) { 89 | return nextVersion(prevArbTokenList.version, versionBump); 90 | } 91 | 92 | return getVersionWithExtensions( 93 | prevArbTokenList, 94 | arbifiedTokenList, 95 | versionBump, 96 | ); 97 | } 98 | 99 | export { getVersion }; 100 | -------------------------------------------------------------------------------- /src/lib/graph.ts: -------------------------------------------------------------------------------- 1 | import { request, gql } from 'graphql-request'; 2 | import { isNetwork } from './utils'; 3 | import { GraphTokenResult, GraphTokensResult } from './types'; 4 | import { excludeList } from './constants'; 5 | 6 | if (!process.env.L2_GATEWAY_SUBGRAPH_URL) { 7 | throw new Error('process.env.L2_GATEWAY_SUBGRAPH_URL is not defined'); 8 | } 9 | const apolloL2GatewaysClient = process.env.L2_GATEWAY_SUBGRAPH_URL; 10 | 11 | if (!process.env.L2_GATEWAY_SEPOLIA_SUBGRAPH_URL) { 12 | throw new Error('process.env.L2_GATEWAY_SEPOLIA_SUBGRAPH_URL is not defined'); 13 | } 14 | const apolloL2GatewaysSepoliaClient = 15 | process.env.L2_GATEWAY_SEPOLIA_SUBGRAPH_URL; 16 | 17 | const chainIdToGraphClientUrl = (chainID: string) => { 18 | switch (chainID) { 19 | case '42161': 20 | return apolloL2GatewaysClient; 21 | case '421614': 22 | return apolloL2GatewaysSepoliaClient; 23 | default: 24 | throw new Error('Unsupported chain'); 25 | } 26 | }; 27 | 28 | const isGraphTokenResult = (obj: GraphTokenResult) => { 29 | if (!obj) { 30 | throw new Error('Graph result: undefined'); 31 | } 32 | const expectedKeys = ['joinTableEntry', 'l1TokenAddr']; 33 | const actualKeys = new Set(Object.keys(obj)); 34 | 35 | if (!expectedKeys.every((key) => actualKeys.has(key))) { 36 | throw new Error('Graph result: missing top level key'); 37 | } 38 | const joinTableEntry = obj.joinTableEntry[0]; 39 | if (!joinTableEntry) { 40 | throw new Error('Graph result: no joinTableEntry'); 41 | } 42 | if (!joinTableEntry.gateway.gatewayAddr) { 43 | throw new Error('Graph result: could not get gateway address'); 44 | } 45 | }; 46 | 47 | export const getTokens = async ( 48 | tokenList: { addr: string; logo: string | undefined }[], 49 | _networkID: string | number, 50 | ): Promise> => { 51 | const { isNova } = isNetwork(); 52 | if (isNova) { 53 | console.warn('empty subgraph for nova'); 54 | return []; 55 | } 56 | const networkID = 57 | typeof _networkID === 'number' ? _networkID.toString() : _networkID; 58 | const clientUrl = chainIdToGraphClientUrl(networkID); 59 | // lazy solution for big lists for now; we'll have to paginate once we have > 500 tokens registed 60 | if (tokenList.length > 500) { 61 | const allTokens = await getAllTokens(networkID); 62 | const allTokenAddresses = new Set( 63 | allTokens.map((token) => token.l1TokenAddr.toLowerCase()), 64 | ); 65 | tokenList = tokenList.filter((token) => 66 | allTokenAddresses.has(token.addr.toLowerCase()), 67 | ); 68 | if (tokenList.length > 500) 69 | throw new Error('Too many tokens for graph query'); 70 | } 71 | const formattedAddresses = tokenList 72 | .map((token) => `"${token.addr}"`.toLowerCase()) 73 | .join(','); 74 | const query = gql` 75 | { 76 | tokens(first: 500, skip: 0, where:{ 77 | id_in:[${formattedAddresses}] 78 | }) { 79 | l1TokenAddr: id 80 | joinTableEntry: gateway( 81 | first: 1 82 | orderBy: l2BlockNum 83 | orderDirection: desc 84 | ) { 85 | id 86 | l2BlockNum 87 | token { 88 | tokenAddr: id 89 | } 90 | gateway { 91 | gatewayAddr: id 92 | } 93 | } 94 | } 95 | } 96 | `; 97 | 98 | const { tokens } = (await request(clientUrl, query)) as GraphTokensResult; 99 | tokens.map((token) => isGraphTokenResult(token)); 100 | 101 | return tokens.filter( 102 | (token) => !excludeList.includes(token.l1TokenAddr.toLowerCase()), 103 | ); 104 | }; 105 | 106 | export const getAllTokens = async ( 107 | _networkID: string | number, 108 | ): Promise> => { 109 | const networkID = 110 | typeof _networkID === 'number' ? _networkID.toString() : _networkID; 111 | const clientUrl = chainIdToGraphClientUrl(networkID); 112 | const query = gql` 113 | { 114 | tokens(first: 500, skip: 0) { 115 | l1TokenAddr: id 116 | joinTableEntry: gateway( 117 | first: 1 118 | orderBy: l2BlockNum 119 | orderDirection: desc 120 | ) { 121 | id 122 | l2BlockNum 123 | token { 124 | tokenAddr: id 125 | } 126 | gateway { 127 | gatewayAddr: id 128 | } 129 | } 130 | } 131 | } 132 | `; 133 | 134 | const { tokens } = (await request(clientUrl, query)) as GraphTokensResult; 135 | const res = tokens.map((token) => { 136 | isGraphTokenResult(token); 137 | return { ...token }; 138 | }); 139 | 140 | return res.filter( 141 | (token) => !excludeList.includes(token.l1TokenAddr.toLowerCase()), 142 | ); 143 | }; 144 | -------------------------------------------------------------------------------- /src/lib/instantiate_bridge.ts: -------------------------------------------------------------------------------- 1 | import { providers } from 'ethers'; 2 | import { 3 | ArbitrumNetwork, 4 | getArbitrumNetwork, 5 | MultiCaller, 6 | } from '@arbitrum/sdk'; 7 | import { getArgvs } from './options'; 8 | import { customNetworks, rpcs } from '../customNetworks'; 9 | 10 | const customNetworksObject = customNetworks.reduce<{ 11 | [chainId: number]: ArbitrumNetwork; 12 | }>((acc, customNetwork) => { 13 | acc[customNetwork.chainId] = customNetwork; 14 | return acc; 15 | }, {}); 16 | 17 | const allNetworks: Record = { 18 | ...customNetworksObject, 19 | [42_161]: getArbitrumNetwork(42_161), 20 | [421_614]: getArbitrumNetwork(421_614), 21 | [42_170]: getArbitrumNetwork(42_170), 22 | }; 23 | 24 | export const getNetworkConfig = async () => { 25 | const argv = getArgvs(); 26 | const networkID = argv.l2NetworkID; 27 | console.log('Using L2 networkID:', networkID); 28 | 29 | const childNetwork = allNetworks[networkID]; 30 | const childProvider = new providers.JsonRpcProvider(rpcs[networkID]); 31 | 32 | const expectedEnv = (() => { 33 | if (childNetwork.parentChainId === 1) return 'MAINNET_RPC'; 34 | else if (childNetwork.parentChainId === 11155111) return 'SEPOLIA_RPC'; 35 | else if (childNetwork.parentChainId === 42161) return 'ARB_ONE_RPC'; 36 | else if (childNetwork.parentChainId === 421614) return 'ARB_SEPOLIA_RPC'; 37 | else if (childNetwork.parentChainId === 42170) return 'ARB_NOVA_RPC'; 38 | else if (childNetwork.parentChainId === 17000) return 'HOLESKY_RPC'; 39 | else if (childNetwork.parentChainId === 8453) return 'BASE_RPC'; 40 | else if (childNetwork.parentChainId === 84532) return 'BASE_SEPOLIA_RPC'; 41 | throw new Error('No parent chain RPC detected'); 42 | })(); 43 | const parentRpc = process.env[expectedEnv]; 44 | if (!parentRpc) throw new Error(`Please set ${expectedEnv}`); 45 | 46 | const parentProvider = new providers.JsonRpcProvider(parentRpc); 47 | const parentMultiCaller = await MultiCaller.fromProvider(parentProvider); 48 | const childMulticaller = await MultiCaller.fromProvider(childProvider); 49 | 50 | return { 51 | l1: { 52 | provider: parentProvider, 53 | multiCaller: parentMultiCaller, 54 | }, 55 | l2: { 56 | network: childNetwork, 57 | provider: childProvider, 58 | multiCaller: childMulticaller, 59 | }, 60 | }; 61 | }; 62 | -------------------------------------------------------------------------------- /src/lib/options.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021, Offchain Labs, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import yargs from 'yargs'; 18 | import { hideBin } from 'yargs/helpers'; 19 | import { Arguments, InferredOptionTypes } from 'yargs'; 20 | 21 | enum Action { 22 | AllTokensList = 'alltokenslist', 23 | Arbify = 'arbify', 24 | Full = 'full', 25 | Permit = 'permit', 26 | Update = 'update', 27 | } 28 | const options = { 29 | l2NetworkID: { 30 | type: 'number', 31 | demandOption: true, 32 | }, 33 | tokenList: { 34 | type: 'string', 35 | demandOption: true, 36 | }, 37 | newArbifiedList: { 38 | type: 'string', 39 | demandOption: true, 40 | }, 41 | includeOldDataFields: { 42 | type: 'boolean', 43 | }, 44 | includePermitTags: { 45 | type: 'boolean', 46 | }, 47 | skipValidation: { 48 | type: 'boolean', 49 | default: false, 50 | }, 51 | ignorePreviousList: { 52 | type: 'boolean', 53 | default: false, 54 | }, 55 | // This is required, but setting required here would disallow setting it to false when `ignorePreviousList` is true 56 | prevArbifiedList: { 57 | type: 'string', 58 | }, 59 | } as const; 60 | 61 | const yargsInstance = yargs(hideBin(process.argv)) 62 | .options(options) 63 | .check(({ ignorePreviousList, prevArbifiedList }) => { 64 | if (ignorePreviousList && !prevArbifiedList) { 65 | return true; 66 | } 67 | 68 | if (!ignorePreviousList && prevArbifiedList) { 69 | return true; 70 | } 71 | 72 | return false; 73 | }); 74 | 75 | type Args = Arguments>; 76 | 77 | const getArgvs = () => yargsInstance.parseSync(); 78 | 79 | export { Action, options, Args, getArgvs, yargsInstance }; 80 | -------------------------------------------------------------------------------- /src/lib/store.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs'; 2 | import axios from 'axios'; 3 | 4 | import { ArbTokenList, EtherscanList } from './types'; 5 | import { isArbTokenList } from './utils'; 6 | 7 | const isValidUrl = (url: string) => { 8 | try { 9 | return Boolean(new URL(url)); 10 | } catch (e) { 11 | return false; 12 | } 13 | }; 14 | 15 | export const getPrevList = async ( 16 | arbifiedList: string | undefined, 17 | ): Promise => { 18 | const path = arbifiedList ?? ''; 19 | // If the path is an URL to a list 20 | if (isValidUrl(path)) { 21 | const list = await axios.get(path).then((response) => response.data); 22 | console.log('Prev version of Arb List found'); 23 | 24 | isArbTokenList(list); 25 | return list as ArbTokenList; 26 | } 27 | 28 | if (!existsSync(path)) { 29 | console.log("Doesn't exist an arbified list."); 30 | return undefined; 31 | } 32 | 33 | const data = readFileSync(path); 34 | console.log('Prev version of Arb List found'); 35 | 36 | const prevArbTokenList = JSON.parse(data.toString()); 37 | isArbTokenList(prevArbTokenList); 38 | return prevArbTokenList as ArbTokenList; 39 | }; 40 | 41 | export const writeToFile = ( 42 | list: ArbTokenList | EtherscanList, 43 | path: string, 44 | ) => { 45 | const fileTypeArr = path.split('.'); 46 | const fileType = fileTypeArr[fileTypeArr.length - 1]; 47 | 48 | const dirPath = path.substring(0, path.lastIndexOf('/')); 49 | 50 | if (fileType !== 'json') { 51 | throw new Error('The --newArbifiedList file should be json type.'); 52 | } 53 | 54 | if (!existsSync(dirPath)) { 55 | console.log(`Setting up token list dir at ${dirPath}`); 56 | mkdirSync(dirPath, { 57 | recursive: true, 58 | }); 59 | } 60 | 61 | writeFileSync(path, JSON.stringify(list)); 62 | console.log('Token list generated at', path); 63 | }; 64 | -------------------------------------------------------------------------------- /src/lib/token_list_gen.ts: -------------------------------------------------------------------------------- 1 | import { TokenList } from '@uniswap/token-lists'; 2 | import { getAllTokens } from './graph'; 3 | import { constants, utils } from 'ethers'; 4 | 5 | import { 6 | ArbTokenList, 7 | ArbTokenInfo, 8 | EtherscanList, 9 | EtherscanToken, 10 | GraphTokenResult, 11 | } from './types'; 12 | import { 13 | getL2TokenAddressesFromL1, 14 | getL2TokenAddressesFromL2, 15 | getTokenListObj, 16 | sanitizeNameString, 17 | sanitizeSymbolString, 18 | isNetwork, 19 | listNameToArbifiedListName, 20 | removeInvalidTokensFromList, 21 | isValidHttpUrl, 22 | getFormattedSourceURL, 23 | getL1TokenAndL2Gateway, 24 | getChunks, 25 | promiseErrorMultiplier, 26 | getL1GatewayAddress, 27 | } from './utils'; 28 | import { validateTokenListWithErrorThrowing } from './validateTokenList'; 29 | import { constants as arbConstants } from '@arbitrum/sdk'; 30 | import { getNetworkConfig } from './instantiate_bridge'; 31 | import { getPrevList } from './store'; 32 | import { getArgvs } from './options'; 33 | import { BridgedUSDCContractAddressArb1 } from './constants'; 34 | import { getVersion } from './getVersion'; 35 | 36 | export interface ArbificationOptions { 37 | overwriteCurrentList: boolean; 38 | } 39 | 40 | export const generateTokenList = async ( 41 | l1TokenList: TokenList, 42 | prevArbTokenList?: ArbTokenList | null, 43 | options?: { 44 | /** 45 | * Append all tokens from the original l1TokenList to the output list. 46 | */ 47 | includeAllL1Tokens?: boolean; 48 | /** 49 | * Append all unbridged tokens from original l1TokenList to the output list. 50 | */ 51 | includeUnbridgedL1Tokens?: boolean; 52 | getAllTokensInNetwork?: boolean; 53 | includeOldDataFields?: boolean; 54 | sourceListURL?: string; 55 | preserveListName?: boolean; 56 | }, 57 | ) => { 58 | if (options?.includeAllL1Tokens && options.includeUnbridgedL1Tokens) { 59 | throw new Error( 60 | 'Cannot include both of AllL1Tokens and UnbridgedL1Tokens since UnbridgedL1Tokens is a subset of AllL1Tokens.', 61 | ); 62 | } 63 | 64 | const name = l1TokenList.name; 65 | const mainLogoUri = l1TokenList.logoURI; 66 | 67 | const { l1, l2 } = await promiseErrorMultiplier(getNetworkConfig(), () => 68 | getNetworkConfig(), 69 | ); 70 | 71 | const { isNova } = isNetwork(); 72 | if (options && options.getAllTokensInNetwork && isNova) 73 | throw new Error('Subgraph not enabled for nova'); 74 | 75 | const l1TokenListL1Tokens = l1TokenList.tokens.filter( 76 | (token) => token.chainId === l1.provider.network.chainId, 77 | ); 78 | 79 | let tokens: GraphTokenResult[] = 80 | options && options.getAllTokensInNetwork 81 | ? await promiseErrorMultiplier(getAllTokens(l2.network.chainId), () => 82 | getAllTokens(l2.network.chainId), 83 | ) 84 | : await promiseErrorMultiplier( 85 | getL1TokenAndL2Gateway( 86 | l1TokenListL1Tokens.map((token) => ({ 87 | addr: token.address.toLowerCase(), 88 | logo: token.logoURI, 89 | })), 90 | l2.multiCaller, 91 | l2.network, 92 | ), 93 | () => 94 | getL1TokenAndL2Gateway( 95 | l1TokenListL1Tokens.map((token) => ({ 96 | addr: token.address.toLowerCase(), 97 | logo: token.logoURI, 98 | })), 99 | l2.multiCaller, 100 | l2.network, 101 | ), 102 | ); 103 | 104 | const l1TokenAddresses = 105 | options && options.getAllTokensInNetwork && !isNova 106 | ? tokens.map((curr) => curr.l1TokenAddr) 107 | : l1TokenListL1Tokens.map((token) => token.address); 108 | 109 | const tokenBridge = l2.network.tokenBridge; 110 | 111 | if (!tokenBridge) { 112 | throw new Error('Child network is missing tokenBridge'); 113 | } 114 | 115 | const intermediatel2AddressesFromL1 = []; 116 | const intermediatel2AddressesFromL2 = []; 117 | for (const addrs of getChunks(l1TokenAddresses)) { 118 | const l2AddressesFromL1Temp = await promiseErrorMultiplier( 119 | getL2TokenAddressesFromL1( 120 | addrs, 121 | l1.multiCaller, 122 | tokenBridge.parentGatewayRouter, 123 | ), 124 | () => 125 | getL2TokenAddressesFromL1( 126 | addrs, 127 | l1.multiCaller, 128 | tokenBridge.parentGatewayRouter, 129 | ), 130 | ); 131 | intermediatel2AddressesFromL1.push(l2AddressesFromL1Temp); 132 | const l2AddressesFromL2Temp = await promiseErrorMultiplier( 133 | getL2TokenAddressesFromL2( 134 | addrs, 135 | l2.multiCaller, 136 | tokenBridge.childGatewayRouter, 137 | ), 138 | () => 139 | getL2TokenAddressesFromL2( 140 | addrs, 141 | l2.multiCaller, 142 | tokenBridge.childGatewayRouter, 143 | ), 144 | ); 145 | intermediatel2AddressesFromL2.push(l2AddressesFromL2Temp); 146 | } 147 | let l2AddressesFromL1 = intermediatel2AddressesFromL1.flat(1); 148 | let l2AddressesFromL2 = intermediatel2AddressesFromL2.flat(1); 149 | 150 | const logos = l1TokenList.tokens.reduce( 151 | (acc, curr) => ((acc[curr.address.toLowerCase()] = curr.logoURI), acc), 152 | {} as { [addr: string]: string | undefined }, 153 | ); 154 | 155 | // if the l2 route hasn't been updated yet we remove the token from the bridged tokens 156 | const filteredTokens: GraphTokenResult[] = []; 157 | const filteredL2AddressesFromL1: string[] = []; 158 | const filteredL2AddressesFromL2: string[] = []; 159 | tokens.forEach((t, i) => { 160 | const l2AddressFromL1 = l2AddressesFromL1[i]; 161 | if (l2AddressFromL1 && l2AddressesFromL1[i] === l2AddressesFromL2[i]) { 162 | filteredTokens.push(t); 163 | filteredL2AddressesFromL1.push(l2AddressFromL1); 164 | filteredL2AddressesFromL2.push(l2AddressFromL1); 165 | } 166 | }); 167 | tokens = filteredTokens; 168 | l2AddressesFromL1 = filteredL2AddressesFromL1; 169 | l2AddressesFromL2 = filteredL2AddressesFromL1; 170 | 171 | const intermediateTokenData = []; 172 | for (const addrs of getChunks(l2AddressesFromL1, 100)) { 173 | const tokenDataTemp = await promiseErrorMultiplier( 174 | l2.multiCaller.getTokenData( 175 | addrs.map((t) => t || constants.AddressZero), 176 | { name: true, decimals: true, symbol: true }, 177 | ), 178 | () => 179 | l2.multiCaller.getTokenData( 180 | addrs.map((t) => t || constants.AddressZero), 181 | { name: true, decimals: true, symbol: true }, 182 | ), 183 | ); 184 | intermediateTokenData.push(tokenDataTemp); 185 | } 186 | 187 | const tokenData = intermediateTokenData.flat(1); 188 | 189 | const _arbifiedTokenList = tokens 190 | .map((t, i) => ({ 191 | token: t, 192 | l2Address: l2AddressesFromL2[i], 193 | tokenDatum: tokenData[i], 194 | })) 195 | // it's possible that even though l2AddressesFromL1[i] === l2AddressesFromL2[i] these addresses could be the zero address 196 | // this can happen if the graphql query returns an address that hasnt been bridged 197 | .filter( 198 | (t): t is typeof t & { l2Address: string } => 199 | t.l2Address != undefined && t.l2Address !== constants.AddressZero, 200 | ) 201 | .map((token) => { 202 | const l2GatewayAddress = 203 | token.token.joinTableEntry[0].gateway.gatewayAddr; 204 | const l1GatewayAddress = getL1GatewayAddress(l2GatewayAddress) ?? 'N/A'; 205 | 206 | let { name: _name, decimals, symbol: _symbol } = token.tokenDatum; 207 | 208 | // we queried the L2 token and got nothing, so token doesn't exist yet 209 | if (decimals === undefined) return undefined; 210 | 211 | _name = (() => { 212 | if (_name === undefined) 213 | throw new Error( 214 | `Unexpected undefined token name: ${JSON.stringify(token)}`, 215 | ); 216 | // if token name is empty, instead set the address as the name 217 | // we remove the initial 0x since the token list standard only allows up to 40 characters 218 | else if (_name === '') return token.token.l1TokenAddr.substring(2); 219 | // parse null terminated bytes32 strings 220 | else if (_name.length === 64) 221 | return utils.parseBytes32String('0x' + _name); 222 | else if ( 223 | token.l2Address.toLowerCase() === 224 | BridgedUSDCContractAddressArb1.toLowerCase() 225 | ) { 226 | return 'Bridged USDC'; 227 | } else return _name; 228 | })(); 229 | 230 | _symbol = (() => { 231 | if (_symbol === undefined) 232 | throw new Error( 233 | `Unexpected undefined token symbol: ${JSON.stringify(token)}`, 234 | ); 235 | // schema doesn't allow for empty symbols, and has a max length of 20 236 | else if (_symbol === '') 237 | return _name.substring(0, Math.min(_name.length, 20)); 238 | // parse null terminated bytes32 strings 239 | else if (_symbol.length === 64) 240 | return utils.parseBytes32String('0x' + _symbol); 241 | else if ( 242 | token.l2Address.toLowerCase() === 243 | BridgedUSDCContractAddressArb1.toLowerCase() 244 | ) { 245 | return 'USDC.e'; 246 | } else return _symbol; 247 | })(); 248 | 249 | const name = sanitizeNameString(_name); 250 | const symbol = sanitizeSymbolString(_symbol); 251 | 252 | const arbTokenInfo: ArbTokenInfo = { 253 | chainId: Number(l2.network.chainId), 254 | address: token.l2Address, 255 | name, 256 | symbol, 257 | decimals, 258 | logoURI: logos[token.token.l1TokenAddr], 259 | extensions: { 260 | bridgeInfo: { 261 | [l2.network.parentChainId]: { 262 | tokenAddress: token.token.l1TokenAddr, // this is the wrong address 263 | originBridgeAddress: l2GatewayAddress, 264 | destBridgeAddress: l1GatewayAddress, 265 | }, 266 | }, 267 | ...(options?.includeOldDataFields 268 | ? { 269 | l1Address: token.token.l1TokenAddr, 270 | l2GatewayAddress: l2GatewayAddress, 271 | l1GatewayAddress: l1GatewayAddress, 272 | } 273 | : {}), 274 | }, 275 | }; 276 | 277 | return arbTokenInfo; 278 | }); 279 | 280 | let arbifiedTokenList: ArbTokenInfo[] = ( 281 | await Promise.all(_arbifiedTokenList) 282 | ).filter((tokenInfo: ArbTokenInfo | undefined) => { 283 | return ( 284 | tokenInfo && 285 | tokenInfo.extensions && 286 | tokenInfo.extensions.bridgeInfo[l2.network.parentChainId] 287 | .originBridgeAddress !== arbConstants.DISABLED_GATEWAY 288 | ); 289 | }) as ArbTokenInfo[]; 290 | arbifiedTokenList.sort((a, b) => (a.symbol < b.symbol ? -1 : 1)); 291 | 292 | console.log(`List has ${arbifiedTokenList.length} bridged tokens`); 293 | 294 | const allOtherTokens = l1TokenList.tokens 295 | .filter((l1TokenInfo) => l1TokenInfo.chainId !== l2.network.chainId) 296 | .map((l1TokenInfo) => { 297 | return { 298 | chainId: +l1TokenInfo.chainId, 299 | name: l1TokenInfo.name, 300 | address: l1TokenInfo.address, 301 | symbol: l1TokenInfo.symbol, 302 | decimals: l1TokenInfo.decimals, 303 | logoURI: l1TokenInfo.logoURI, 304 | }; 305 | }); 306 | 307 | if (options?.includeAllL1Tokens) { 308 | arbifiedTokenList = arbifiedTokenList.concat(allOtherTokens); 309 | } else if (options?.includeUnbridgedL1Tokens) { 310 | const l1AddressesOfBridgedTokens = new Set( 311 | tokens.map((token) => token.l1TokenAddr.toLowerCase()), 312 | ); 313 | const unbridgedTokens = allOtherTokens 314 | .filter((l1TokenInfo) => { 315 | return ( 316 | !l1AddressesOfBridgedTokens.has(l1TokenInfo.address.toLowerCase()) && 317 | l1TokenInfo.chainId === +l2.network.parentChainId 318 | ); 319 | }) 320 | .sort((a, b) => (a.symbol < b.symbol ? -1 : 1)); 321 | console.log(`List has ${unbridgedTokens.length} unbridged tokens`); 322 | 323 | arbifiedTokenList = arbifiedTokenList.concat(unbridgedTokens); 324 | } 325 | 326 | const version = getVersion(prevArbTokenList, arbifiedTokenList); 327 | 328 | const sourceListURL = getFormattedSourceURL(options?.sourceListURL); 329 | const arbTokenList: ArbTokenList = { 330 | name: 331 | options && options.preserveListName 332 | ? name 333 | : listNameToArbifiedListName(name, l2.network.chainId), 334 | timestamp: new Date().toISOString(), 335 | version, 336 | tokens: arbifiedTokenList, 337 | logoURI: mainLogoUri, 338 | ...(sourceListURL && { 339 | tags: { 340 | sourceList: { 341 | name: 'Source list url', 342 | description: `${sourceListURL} replace __ with forwardslash`, 343 | }, 344 | }, 345 | }), 346 | }; 347 | 348 | const validationTokenList: ArbTokenList = { 349 | ...arbTokenList, 350 | tokens: arbTokenList.tokens, 351 | }; 352 | 353 | const argvs = getArgvs(); 354 | if (!argvs.skipValidation) { 355 | validateTokenListWithErrorThrowing(validationTokenList); 356 | } 357 | 358 | console.log(`Generated list with total ${arbTokenList.tokens.length} tokens`); 359 | console.log('version:', version); 360 | 361 | return arbTokenList; 362 | }; 363 | 364 | export const arbifyL1List = async ( 365 | pathOrUrl: string, 366 | { 367 | includeOldDataFields, 368 | ignorePreviousList, 369 | prevArbifiedList, 370 | }: { 371 | includeOldDataFields: boolean; 372 | ignorePreviousList: boolean; 373 | prevArbifiedList: string | undefined; 374 | }, 375 | ): Promise<{ 376 | newList: ArbTokenList; 377 | l1ListName: string; 378 | }> => { 379 | const l1TokenList = await getTokenListObj(pathOrUrl); 380 | 381 | removeInvalidTokensFromList(l1TokenList); 382 | const prevArbTokenList = ignorePreviousList 383 | ? null 384 | : await getPrevList(prevArbifiedList); 385 | 386 | // We can't arbify token list for base, we filter out non-base tokens from uniswap list 387 | const argv = getArgvs(); 388 | if (argv.l2NetworkID === 8453 || argv.l2NetworkID === 84532) { 389 | const tokens = l1TokenList.tokens 390 | .filter((token) => token.chainId === argv.l2NetworkID) 391 | .map(({ extensions, ...token }) => ({ 392 | ...token, 393 | })); 394 | 395 | const version = getVersion(prevArbTokenList, tokens); 396 | return { 397 | newList: { 398 | ...l1TokenList, 399 | name: `Uniswap labs ${argv.l2NetworkID}`, 400 | timestamp: new Date().toISOString(), 401 | version, 402 | tokens, 403 | }, 404 | l1ListName: l1TokenList.name, 405 | }; 406 | } 407 | 408 | const newList = await generateTokenList(l1TokenList, prevArbTokenList, { 409 | includeAllL1Tokens: false, 410 | includeOldDataFields, 411 | sourceListURL: isValidHttpUrl(pathOrUrl) ? pathOrUrl : undefined, 412 | }); 413 | 414 | return { 415 | newList, 416 | l1ListName: l1TokenList.name, 417 | }; 418 | }; 419 | 420 | export const updateArbifiedList = async ( 421 | pathOrUrl: string, 422 | { 423 | includeOldDataFields, 424 | ignorePreviousList, 425 | prevArbifiedList, 426 | }: { 427 | includeOldDataFields: boolean; 428 | ignorePreviousList: boolean; 429 | prevArbifiedList: string | undefined; 430 | }, 431 | ) => { 432 | const arbTokenList = await getTokenListObj(pathOrUrl); 433 | removeInvalidTokensFromList(arbTokenList); 434 | const prevArbTokenList = ignorePreviousList 435 | ? null 436 | : await getPrevList(prevArbifiedList); 437 | 438 | const newList = await generateTokenList(arbTokenList, prevArbTokenList, { 439 | includeAllL1Tokens: true, 440 | sourceListURL: isValidHttpUrl(pathOrUrl) ? pathOrUrl : undefined, 441 | includeOldDataFields, 442 | preserveListName: true, 443 | }); 444 | 445 | return { 446 | newList, 447 | }; 448 | }; 449 | 450 | export const generateFullList = async () => { 451 | const mockList: TokenList = { 452 | name: 'Full', 453 | logoURI: 'ipfs://QmTvWJ4kmzq9koK74WJQ594ov8Es1HHurHZmMmhU8VY68y', 454 | timestamp: new Date().toISOString(), 455 | version: { 456 | major: 1, 457 | minor: 0, 458 | patch: 0, 459 | }, 460 | tokens: [], 461 | }; 462 | const tokenData = await generateTokenList(mockList, undefined, { 463 | getAllTokensInNetwork: true, 464 | }); 465 | 466 | return arbListtoEtherscanList(tokenData); 467 | }; 468 | export const generateFullListFormatted = async () => { 469 | const mockList: TokenList = { 470 | name: 'Full', 471 | logoURI: 'ipfs://QmTvWJ4kmzq9koK74WJQ594ov8Es1HHurHZmMmhU8VY68y', 472 | timestamp: new Date().toISOString(), 473 | version: { 474 | major: 1, 475 | minor: 0, 476 | patch: 0, 477 | }, 478 | tokens: [], 479 | }; 480 | const allTokenList = await generateTokenList(mockList, undefined, { 481 | getAllTokensInNetwork: true, 482 | }); 483 | // log for human-readable check 484 | allTokenList.tokens.forEach((token) => { 485 | console.log(token.name, token.symbol, token.address); 486 | }); 487 | return allTokenList; 488 | }; 489 | 490 | export const arbListtoEtherscanList = ( 491 | arbList: ArbTokenList, 492 | ): EtherscanList => { 493 | const list: EtherscanList = []; 494 | arbList.tokens.forEach((tokenInfo) => { 495 | const { address: l2Address } = tokenInfo; 496 | if (tokenInfo.extensions) { 497 | // This assumes one origin chain; should be chill 498 | const originChainID = Object.keys(tokenInfo.extensions.bridgeInfo)[0]; 499 | const { tokenAddress, originBridgeAddress, destBridgeAddress } = 500 | tokenInfo.extensions.bridgeInfo[originChainID]; 501 | const data: EtherscanToken = { 502 | l1Address: tokenAddress, 503 | l2Address, 504 | l1GatewayAddress: destBridgeAddress, 505 | l2GatewayAddress: originBridgeAddress, 506 | }; 507 | list.push(data); 508 | } 509 | }); 510 | return list; 511 | }; 512 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import { TokenInfo, TokenList } from '@uniswap/token-lists'; 2 | 3 | // extensions object is allowed to be 2 levels deep, but type is out-of-date 4 | // https://github.com/Uniswap/token-lists/pull/67 5 | export interface ArbTokenInfo extends Omit { 6 | extensions?: { 7 | bridgeInfo: { 8 | [destinationChainID: string]: { 9 | tokenAddress: string; 10 | originBridgeAddress: string; 11 | destBridgeAddress: string; 12 | }; 13 | }; 14 | l1Address?: string; 15 | l2GatewayAddress?: string; 16 | l1GatewayAddress?: string; 17 | }; 18 | } 19 | 20 | export interface ArbTokenList extends Omit { 21 | tokens: ArbTokenInfo[]; 22 | } 23 | 24 | export interface EtherscanToken { 25 | l1Address: string | null; 26 | l2Address: string; 27 | l1GatewayAddress: string | null; 28 | l2GatewayAddress: string | null; 29 | } 30 | 31 | export type EtherscanList = EtherscanToken[]; 32 | 33 | export interface GraphTokenResult { 34 | joinTableEntry: [ 35 | { 36 | gateway: { 37 | gatewayAddr: string; 38 | }; 39 | }, 40 | ]; 41 | l1TokenAddr: string; 42 | } 43 | 44 | export interface GraphTokensResult { 45 | tokens: GraphTokenResult[]; 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { TokenList } from '@uniswap/token-lists'; 2 | import { readFileSync, existsSync } from 'fs'; 3 | import axios from 'axios'; 4 | import axiosRetry from 'axios-retry'; 5 | import { ArbitrumNetwork, MultiCaller } from '@arbitrum/sdk'; 6 | import { L1GatewayRouter__factory } from '@arbitrum/sdk/dist/lib/abi/factories/L1GatewayRouter__factory'; 7 | import { L2GatewayRouter__factory } from '@arbitrum/sdk/dist/lib/abi/factories/L2GatewayRouter__factory'; 8 | 9 | import { ArbTokenList, GraphTokenResult } from './types'; 10 | import path from 'path'; 11 | import { tokenListIsValid } from './validateTokenList'; 12 | import { 13 | l2ToL1GatewayAddresses, 14 | l2ToL1GatewayAddressesNova, 15 | } from './constants'; 16 | import { getArgvs } from './options'; 17 | 18 | // On failed request, retry with exponential back-off 19 | axiosRetry(axios, { 20 | retries: 5, 21 | retryCondition: () => true, 22 | retryDelay: (retryCount) => 65_000 + retryCount * 10_000, // (milliseconds) 23 | onRetry(retryCount, error) { 24 | console.log( 25 | `Request failed with ${error.code}. Retrying ${retryCount} times.`, 26 | ); 27 | }, 28 | }); 29 | 30 | export const isNetwork = () => { 31 | const argv = getArgvs(); 32 | return { 33 | isArbOne: argv.l2NetworkID === 42161, 34 | isNova: argv.l2NetworkID === 42170, 35 | isSepoliaRollup: argv.l2NetworkID === 421614, 36 | }; 37 | }; 38 | 39 | const coinGeckoBuff = readFileSync( 40 | path.resolve(__dirname, '../Assets/coingecko_uris.json'), 41 | ); 42 | const logoURIsBuff = readFileSync( 43 | path.resolve(__dirname, '../Assets/logo_uris.json'), 44 | ); 45 | 46 | const coingeckoURIs = JSON.parse(coinGeckoBuff.toString()); 47 | const logoUris = JSON.parse(logoURIsBuff.toString()); 48 | for (const address of Object.keys(logoUris)) { 49 | logoUris[address.toLowerCase()] = logoUris[address]; 50 | } 51 | 52 | export const listNameToArbifiedListName = ( 53 | name: string, 54 | childChainId: number, 55 | ) => { 56 | const prefix = 'Arbed '; 57 | 58 | let fileName = sanitizeNameString(name); 59 | if (!fileName.startsWith(prefix)) { 60 | fileName = prefix + fileName; 61 | } 62 | 63 | const baseName = fileName.split(' ').slice(0, 3).join(' ').slice(0, 20); 64 | return `${baseName} ${childChainId}`; 65 | }; 66 | 67 | export const getL1TokenAndL2Gateway = async ( 68 | tokenList: { addr: string; logo: string | undefined }[], 69 | l2Multicaller: MultiCaller, 70 | l2Network: ArbitrumNetwork, 71 | ): Promise> => { 72 | const routerData = await getL2GatewayAddressesFromL1Token( 73 | tokenList.map((curr) => curr.addr), 74 | l2Multicaller, 75 | l2Network, 76 | ); 77 | 78 | return tokenList.map((curr, i) => ({ 79 | joinTableEntry: [ 80 | { 81 | gateway: { 82 | gatewayAddr: routerData[i], 83 | }, 84 | }, 85 | ], 86 | l1TokenAddr: curr.addr, 87 | })); 88 | }; 89 | export const promiseErrorMultiplier = ( 90 | prom: Promise, 91 | handler: (err: Error) => Promise, 92 | tries = 3, 93 | verbose = false, 94 | ) => { 95 | let counter = 0; 96 | while (counter < tries) { 97 | prom = prom.catch((err) => handler(err)); 98 | counter++; 99 | } 100 | return prom.catch((err) => { 101 | if (verbose) console.error('Failed ' + tries + ' times. Giving up'); 102 | throw err; 103 | }); 104 | }; 105 | 106 | export const getL1GatewayAddress = (l2GatewayAddress: string) => { 107 | const { isNova } = isNetwork(); 108 | const l2Gateway = isNova 109 | ? l2ToL1GatewayAddressesNova[l2GatewayAddress.toLowerCase()] 110 | : l2ToL1GatewayAddresses[l2GatewayAddress.toLowerCase()]; 111 | 112 | if (l2Gateway) return l2Gateway; 113 | 114 | return undefined; 115 | }; 116 | 117 | export const getL2GatewayAddressesFromL1Token = async ( 118 | l1TokenAddresses: string[], 119 | l2Multicaller: MultiCaller, 120 | l2Network: ArbitrumNetwork, 121 | ): Promise => { 122 | const iFace = L1GatewayRouter__factory.createInterface(); 123 | 124 | const INC = 500; 125 | let index = 0; 126 | console.info( 127 | 'getL2GatewayAddressesFromL1Token for', 128 | l1TokenAddresses.length, 129 | 'tokens', 130 | ); 131 | 132 | let gateways: (string | undefined)[] = []; 133 | 134 | while (index < l1TokenAddresses.length) { 135 | console.log( 136 | 'Getting tokens', 137 | index, 138 | 'through', 139 | Math.min(index + INC, l1TokenAddresses.length), 140 | ); 141 | 142 | const l1TokenAddressesSlice = l1TokenAddresses.slice(index, index + INC); 143 | 144 | const tokenBridge = l2Network.tokenBridge; 145 | if (!tokenBridge) { 146 | throw new Error('Child network is missing tokenBridge'); 147 | } 148 | 149 | const result = await l2Multicaller.multiCall( 150 | l1TokenAddressesSlice.map((addr) => ({ 151 | encoder: () => iFace.encodeFunctionData('getGateway', [addr]), 152 | decoder: (returnData: string) => 153 | iFace.decodeFunctionResult('getGateway', returnData)[0] as string, 154 | targetAddr: tokenBridge.childGatewayRouter, 155 | })), 156 | ); 157 | gateways = gateways.concat(result); 158 | index += INC; 159 | } 160 | 161 | for (const curr of gateways) { 162 | if (typeof curr === 'undefined') throw new Error('undefined gateway!'); 163 | } 164 | 165 | return gateways as string[]; 166 | }; 167 | 168 | export const getL2TokenAddressesFromL1 = async ( 169 | l1TokenAddresses: string[], 170 | multiCaller: MultiCaller, 171 | l1GatewayRouterAddress: string, 172 | ) => { 173 | const iFace = L1GatewayRouter__factory.createInterface(); 174 | 175 | const l1TokenAddressesLowercased = l1TokenAddresses.map((address) => 176 | address.toLowerCase(), 177 | ); 178 | 179 | return await multiCaller.multiCall( 180 | l1TokenAddressesLowercased.map((addr) => ({ 181 | encoder: () => 182 | iFace.encodeFunctionData('calculateL2TokenAddress', [addr]), 183 | decoder: (returnData: string) => 184 | iFace.decodeFunctionResult( 185 | 'calculateL2TokenAddress', 186 | returnData, 187 | )[0] as string, 188 | targetAddr: l1GatewayRouterAddress, 189 | })), 190 | ); 191 | }; 192 | 193 | export const getL2TokenAddressesFromL2 = async ( 194 | l1TokenAddresses: string[], 195 | multiCaller: MultiCaller, 196 | l2GatewayRouterAddress: string, 197 | ) => { 198 | const iFace = L2GatewayRouter__factory.createInterface(); 199 | 200 | const l1TokenAddressesLowercased = l1TokenAddresses.map((address) => 201 | address.toLowerCase(), 202 | ); 203 | 204 | return await multiCaller.multiCall( 205 | l1TokenAddressesLowercased.map((addr) => ({ 206 | encoder: () => 207 | iFace.encodeFunctionData('calculateL2TokenAddress', [addr]), 208 | decoder: (returnData: string) => 209 | iFace.decodeFunctionResult( 210 | 'calculateL2TokenAddress', 211 | returnData, 212 | )[0] as string, 213 | targetAddr: l2GatewayRouterAddress, 214 | })), 215 | ); 216 | }; 217 | 218 | export const getLogoUri = async (l1TokenAddress: string) => { 219 | const l1TokenAddressLCase = l1TokenAddress.toLowerCase(); 220 | const logoUri: string | undefined = logoUris[l1TokenAddressLCase]; 221 | const coinGeckoURI: string | undefined = coingeckoURIs[l1TokenAddressLCase]; 222 | const trustWalletUri = `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${l1TokenAddress}/logo.png`; 223 | const uris = [logoUri, coinGeckoURI, trustWalletUri].filter( 224 | (x): x is string => !!x, 225 | ); 226 | 227 | for (const uri of uris) { 228 | try { 229 | const res = await axios.get(uri); 230 | if (res.status === 200) { 231 | return uri; 232 | } 233 | } catch (e) { 234 | console.log(e); 235 | } 236 | } 237 | return; 238 | }; 239 | export const getTokenListObjFromUrl = async (url: string) => { 240 | return (await axios.get(url)).data as TokenList; 241 | }; 242 | export const getTokenListObjFromLocalPath = async (path: string) => { 243 | return JSON.parse(readFileSync(path).toString()) as TokenList; 244 | }; 245 | 246 | export const removeInvalidTokensFromList = ( 247 | tokenList: ArbTokenList | TokenList, 248 | ): ArbTokenList | TokenList => { 249 | let valid = tokenListIsValid(tokenList); 250 | const startingTokenListLen = tokenList.tokens.length; 251 | 252 | if (valid) { 253 | return tokenList; 254 | } else { 255 | const tokenListCopy = JSON.parse( 256 | JSON.stringify(tokenList), 257 | ) as typeof tokenList; 258 | console.log('Invalid token list:'); 259 | while (!valid && tokenListCopy.tokens.length > 0) { 260 | const targetToken = tokenListCopy.tokens.pop(); 261 | const tokenTokenIndex = tokenListCopy.tokens.length; 262 | valid = tokenListIsValid(tokenListCopy); 263 | if (valid) { 264 | console.log('Invalid token token, removing from list', targetToken); 265 | 266 | tokenList.tokens.splice(tokenTokenIndex, 1); 267 | // pre-recursion sanity check: 268 | if (tokenList.tokens.length >= startingTokenListLen) { 269 | throw new Error( 270 | '666: removeInvalidTokensFromList failed basic sanity check', 271 | ); 272 | } 273 | return removeInvalidTokensFromList(tokenList); 274 | } 275 | } 276 | throw new Error('Data does not confirm to token list schema; not sure why'); 277 | } 278 | }; 279 | 280 | export const getTokenListObj = async (pathOrUrl: string) => { 281 | const tokenList: TokenList = await (async (pathOrUrl: string) => { 282 | const localFileExists = existsSync(pathOrUrl); 283 | const looksLikeUrl = isValidHttpUrl(pathOrUrl); 284 | if (localFileExists) { 285 | return getTokenListObjFromLocalPath(pathOrUrl); 286 | } else if (looksLikeUrl) { 287 | return await getTokenListObjFromUrl(pathOrUrl); 288 | } else { 289 | throw new Error('Could not find token list'); 290 | } 291 | })(pathOrUrl); 292 | isTokenList(tokenList); 293 | return tokenList; 294 | }; 295 | 296 | // https://stackoverflow.com/questions/5717093/check-if-a-javascript-string-is-a-url 297 | export function isValidHttpUrl(urlString: string) { 298 | let url; 299 | 300 | try { 301 | url = new URL(urlString); 302 | } catch (_) { 303 | return false; 304 | } 305 | 306 | return url.protocol === 'http:' || url.protocol === 'https:'; 307 | } 308 | 309 | export const getFormattedSourceURL = (sourceUrl?: string) => { 310 | if (!sourceUrl) return null; 311 | const urlReplaceForwardSlashes = sourceUrl.replace(/\//g, '__'); 312 | return /^[ \w\.,:]+$/.test(urlReplaceForwardSlashes) 313 | ? urlReplaceForwardSlashes 314 | : null; 315 | }; 316 | // typeguard: 317 | export const isArbTokenList = (obj: any) => { 318 | const expectedListKeys = ['name', 'timestamp', 'version', 'tokens']; 319 | const actualListKeys = new Set(Object.keys(obj)); 320 | if (!expectedListKeys.every((key) => actualListKeys.has(key))) { 321 | throw new Error( 322 | 'ArbTokenList typeguard error: required list key not included', 323 | ); 324 | } 325 | const { version, tokens } = obj; 326 | if ( 327 | !['major', 'minor', 'patch'].every((key) => { 328 | return typeof version[key] === 'number'; 329 | }) 330 | ) { 331 | throw new Error('ArbTokenList typeguard error: invalid version'); 332 | } 333 | if ( 334 | !tokens.every((token: any) => { 335 | const tokenKeys = new Set(Object.keys(token)); 336 | return ['chainId', 'address', 'name', 'decimals', 'symbol'].every( 337 | (key) => { 338 | return tokenKeys.has(key); 339 | }, 340 | ); 341 | }) 342 | ) { 343 | throw new Error('ArbTokenList typeguard error: token missing required key'); 344 | } 345 | tokens.forEach((token: any) => { 346 | if (token.extensions && token.extensions.bridgeInfo) { 347 | const { 348 | extensions: { bridgeInfo }, 349 | } = token; 350 | const bridges = Object.keys(bridgeInfo); 351 | if (!bridges.length) { 352 | throw new Error('ArbTokenList typeguard error: no bridge info found'); 353 | } 354 | const someDestinationChain = bridges[0]; 355 | const { tokenAddress, originBridgeAddress, destBridgeAddress } = 356 | bridgeInfo[someDestinationChain]; 357 | 358 | if ( 359 | ![tokenAddress, originBridgeAddress, destBridgeAddress].every((k) => k) 360 | ) { 361 | throw new Error('ArbTokenList typeguard error: missing extension'); 362 | } 363 | } 364 | }); 365 | }; 366 | 367 | // typeguard: 368 | export const isTokenList = (obj: any) => { 369 | const expectedListKeys = ['name', 'timestamp', 'version', 'tokens']; 370 | const actualListKeys = new Set(Object.keys(obj)); 371 | if (!expectedListKeys.every((key) => actualListKeys.has(key))) { 372 | throw new Error( 373 | 'tokenlist typeguard error: required list key not included', 374 | ); 375 | } 376 | const { version, tokens } = obj; 377 | if ( 378 | !['major', 'minor', 'patch'].every((key) => { 379 | return typeof version[key] === 'number'; 380 | }) 381 | ) { 382 | throw new Error('tokenlist typeguard error: invalid version'); 383 | } 384 | if ( 385 | !tokens.every((token: any) => { 386 | const tokenKeys = new Set(Object.keys(token)); 387 | return ['chainId', 'address', 'name', 'decimals', 'symbol'].every( 388 | (key) => { 389 | return tokenKeys.has(key); 390 | }, 391 | ); 392 | }) 393 | ) { 394 | throw new Error('tokenlist typeguard error: token missing required key'); 395 | } 396 | }; 397 | 398 | export const sanitizeNameString = (str: string) => 399 | str.replace('₮', 'T').replace(/[^ \w.'+\-%/À-ÖØ-öø-ÿ:&\[\]\(\)]/gi, ''); 400 | 401 | export const sanitizeSymbolString = (str: string) => 402 | str.replace('₮', 'T').replace(/[^\w.'+\-%/À-ÖØ-öø-ÿ:&\[\]\(\)]/gi, ''); 403 | 404 | export function* getChunks(arr: Array, chunkSize = 500) { 405 | for (let i = 0; i < arr.length; i += chunkSize) { 406 | yield arr.slice(i, i + chunkSize); 407 | } 408 | } 409 | 410 | export const promiseRetrier = (createProm: () => Promise): Promise => 411 | promiseErrorMultiplier(createProm(), () => createProm()); 412 | -------------------------------------------------------------------------------- /src/lib/validateTokenList.ts: -------------------------------------------------------------------------------- 1 | import Ajv from 'ajv'; 2 | import betterAjvErrors from 'better-ajv-errors'; 3 | import addFormats from 'ajv-formats'; 4 | import { schema, TokenList } from '@uniswap/token-lists'; 5 | import { ArbTokenList } from './types'; 6 | 7 | export const tokenListIsValid = (tokenList: ArbTokenList | TokenList) => { 8 | const ajv = new Ajv(); 9 | addFormats(ajv); 10 | schema.properties.tokens.minItems = 0; 11 | schema.properties.tokens.maxItems = 15_000; 12 | const validate = ajv.compile(schema); 13 | 14 | const res = validate(tokenList); 15 | if (validate.errors) { 16 | const output = betterAjvErrors(schema, tokenList, validate.errors, { 17 | indent: 2, 18 | }); 19 | console.log(output); 20 | } 21 | 22 | return res; 23 | }; 24 | 25 | export const validateTokenListWithErrorThrowing = ( 26 | tokenList: ArbTokenList | TokenList, 27 | ) => { 28 | try { 29 | const valid = tokenListIsValid(tokenList); 30 | if (valid) return true; 31 | else 32 | throw new Error( 33 | 'Data does not conform to token list schema; not sure why', 34 | ); 35 | } catch (e) { 36 | console.log('Invalid token list:'); 37 | throw e; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { yargsInstance } from './lib/options'; 3 | 4 | import './init'; 5 | import { 6 | command as commandUpdate, 7 | describe as describeUpdate, 8 | handler as handlerUpdate, 9 | } from './commands/update'; 10 | import { 11 | command as commandArbify, 12 | describe as describeArbify, 13 | handler as handlerArbify, 14 | } from './commands/arbify'; 15 | import { 16 | command as commandFull, 17 | describe as describeFull, 18 | handler as handlerFull, 19 | } from './commands/full'; 20 | import { 21 | command as commandAllTokensList, 22 | describe as describeAllTokensList, 23 | handler as handlerAllTokensList, 24 | } from './commands/allTokensList'; 25 | 26 | const update = yargsInstance.command( 27 | commandUpdate, 28 | describeUpdate, 29 | {}, 30 | // @ts-ignore: handler returns list so we can compare the result in test, yargs expect handler to return void 31 | handlerUpdate, 32 | ); 33 | 34 | const arbify = yargsInstance.command( 35 | commandArbify, 36 | describeArbify, 37 | {}, 38 | // @ts-ignore: handler returns list so we can compare the result in test, yargs expect handler to return void 39 | handlerArbify, 40 | ); 41 | const full = yargsInstance.command( 42 | commandFull, 43 | describeFull, 44 | {}, 45 | // @ts-ignore: handler returns list so we can compare the result in test, yargs expect handler to return void 46 | handlerFull, 47 | ); 48 | const alltokenslist = yargsInstance.command( 49 | commandAllTokensList, 50 | describeAllTokensList, 51 | {}, 52 | // @ts-ignore: handler returns list so we can compare the result in test, yargs expect handler to return void 53 | handlerAllTokensList, 54 | ); 55 | 56 | if (process.env.NODE_ENV !== 'test') { 57 | update.parseAsync(); 58 | arbify.parseAsync(); 59 | full.parseAsync(); 60 | alltokenslist.parseAsync(); 61 | } 62 | 63 | export { update, yargsInstance }; 64 | -------------------------------------------------------------------------------- /src/scripts/fetchOrbitChainsData.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { ArbitrumNetwork } from '@arbitrum/sdk'; 3 | 4 | type OrbitChainDataResponse = { 5 | mainnet: ArbitrumNetwork[]; 6 | testnet: ArbitrumNetwork[]; 7 | }; 8 | 9 | const fileName = './src/Assets/orbitChainsData.json'; 10 | 11 | export async function fetchOrbitChainsData() { 12 | const response = await fetch( 13 | 'https://raw.githubusercontent.com/OffchainLabs/arbitrum-token-bridge/refs/heads/master/packages/arb-token-bridge-ui/src/util/orbitChainsData.json', 14 | ); 15 | 16 | const data: OrbitChainDataResponse = await response.json(); 17 | return data.mainnet.concat(data.testnet); 18 | } 19 | 20 | (async () => { 21 | const orbitChains = await fetchOrbitChainsData(); 22 | 23 | fs.writeFileSync( 24 | fileName, 25 | JSON.stringify({ 26 | data: orbitChains, 27 | }), 28 | ); 29 | })(); 30 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import 'cross-fetch/polyfill'; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "experimentalDecorators": true, 6 | "module": "commonjs", 7 | "noEmit": true, 8 | "noFallthroughCasesInSwitch": true, 9 | "noImplicitAny": true, 10 | "noImplicitReturns": true, 11 | "noImplicitThis": true, 12 | "outDir": "./dist", 13 | "resolveJsonModule": true, 14 | "strict": true, 15 | "strictNullChecks": true, 16 | "target": "ES2017" 17 | }, 18 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.js"] 19 | } 20 | -------------------------------------------------------------------------------- /update_all: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ $FORCE_USE_NVM == 'true' ]]; then 4 | echo "(running script using node via nvm)" 5 | 6 | export NVM_DIR="$HOME/.nvm" 7 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm 8 | fi 9 | 10 | # arb1 11 | yarn fullList --l2NetworkID 42161 12 | yarn arbify --l2NetworkID 42161 --tokenList https://gateway.ipfs.io/ipns/tokens.uniswap.org --newArbifiedList ./src/ArbTokenLists/arbed_uniswap_labs.json && cp ./src/ArbTokenLists/arbed_uniswap_labs.json ./src/ArbTokenLists/arbed_uniswap_labs_default.json 13 | yarn arbify --l2NetworkID 42161 --tokenList https://www.gemini.com/uniswap/manifest.json --newArbifiedList ./src/ArbTokenLists/arbed_gemini_token_list.json 14 | yarn arbify --l2NetworkID 42161 --tokenList https://api.coinmarketcap.com/data-api/v3/uniswap/all.json --newArbifiedList ./src/ArbTokenLists/arbed_coinmarketcap.json 15 | 16 | # update whitelist era list (for e.g. changed gateways) 17 | aws s3 cp s3://arb-token-lists/ArbTokenLists/arbed_arb_whitelist_era.json ./src/ArbTokenLists/arbed_arb_whitelist_era.json 18 | yarn update --l2NetworkID 42161 --tokenList ./src/ArbTokenLists/arbed_arb_whitelist_era.json --includeOldDataFields true --newArbifiedList ./src/ArbTokenLists/arbed_arb_whitelist_era.json 19 | 20 | # nova 21 | # yarn fullList --l2NetworkID 42170 22 | yarn arbify --l2NetworkID 42170 --tokenList https://gateway.ipfs.io/ipns/tokens.uniswap.org --newArbifiedList ./src/ArbTokenLists/42170_arbed_uniswap_labs.json && cp ./src/ArbTokenLists/42170_arbed_uniswap_labs.json ./src/ArbTokenLists/42170_arbed_uniswap_labs_default.json 23 | yarn arbify --l2NetworkID 42170 --tokenList https://www.gemini.com/uniswap/manifest.json --newArbifiedList ./src/ArbTokenLists/42170_arbed_gemini_token_list.json 24 | yarn arbify --l2NetworkID 42170 --tokenList https://api.coinmarketcap.com/data-api/v3/uniswap/all.json --newArbifiedList ./src/ArbTokenLists/42170_arbed_coinmarketcap.json 25 | 26 | # goerli rollup testnet 27 | yarn arbify --l2NetworkID 421613 --tokenList https://api.coinmarketcap.com/data-api/v3/uniswap/all.json --newArbifiedList ./src/ArbTokenLists/421613_arbed_coinmarketcap.json 28 | yarn fullList --l2NetworkID 421613 --newArbifiedList ./src/ArbTokenLists/arbed_full.json 29 | --------------------------------------------------------------------------------