diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..9dba407 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,89 @@ +name: code coverage + +on: + pull_request: + branches: + - main + +jobs: + comment-forge-coverage: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + - name: Run Forge build + run: | + forge --version + forge build --sizes + id: build + - name: Run forge coverage + id: coverage + run: | + { + echo 'COVERAGE<> "$GITHUB_OUTPUT" + env: + FOUNDRY_RPC_URL: "${{ secrets.RPC_URL }}" + + - name: Check coverage is updated + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const file = "coverage.txt" + if(!fs.existsSync(file)) { + console.log("Nothing to check"); + return + } + const currentCoverage = fs.readFileSync(file, "utf8").trim(); + const newCoverage = (`${{ steps.coverage.outputs.COVERAGE }}`).trim(); + if (newCoverage != currentCoverage) { + core.setFailed(`Code coverage not updated. Run : forge coverage | grep '^|' | grep -v 'test/' > coverage.txt`); + } + + - name: Comment on PR + id: comment + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const {data: comments} = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }) + + const botComment = comments.find(comment => comment.user.id === 41898282) + + const output = `${{ steps.coverage.outputs.COVERAGE }}`; + const commentBody = `Forge code coverage:\n${output}\n`; + + if (botComment) { + github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: commentBody + }) + } else { + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: commentBody + }); + } diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..3220b02 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,24 @@ +name: Lint + +on: + push: + branches: + - main + pull_request: + +jobs: + run-linters: + name: Run linters + runs-on: ubuntu-latest + + steps: + - name: Check out Git repository + uses: actions/checkout@v3 + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Lint + run: forge fmt --check diff --git a/.github/workflows/tests-merge.yml b/.github/workflows/tests-merge.yml new file mode 100644 index 0000000..7c6e5d4 --- /dev/null +++ b/.github/workflows/tests-merge.yml @@ -0,0 +1,41 @@ +name: Tests + +on: + push: + branches: + - main + +jobs: + forge-tests: + name: Forge Tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - uses: actions/setup-node@v3 + with: + node-version: 16 + cache-dependency-path: "./test/js-scripts" + cache: "yarn" + + - run: yarn + working-directory: ./test/js-scripts + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Build + run: forge build + env: + FOUNDRY_PROFILE: ci + + - name: Run tests + run: forge test --isolate -vvv + env: + FOUNDRY_PROFILE: ci + FORGE_SNAPSHOT_CHECK: true diff --git a/.github/workflows/tests-pr.yml b/.github/workflows/tests-pr.yml new file mode 100644 index 0000000..f70fe22 --- /dev/null +++ b/.github/workflows/tests-pr.yml @@ -0,0 +1,42 @@ +name: Tests + +on: + pull_request: + branches: + - main + - dev + +jobs: + forge-tests: + name: Forge Tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - uses: actions/setup-node@v3 + with: + node-version: 16 + cache-dependency-path: "./test/js-scripts" + cache: "yarn" + + - run: yarn + working-directory: ./test/js-scripts + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Build + run: forge build + env: + FOUNDRY_PROFILE: pr + + - name: Run tests + run: forge test --isolate -vvv + env: + FOUNDRY_PROFILE: pr + FORGE_SNAPSHOT_CHECK: true diff --git a/.gitignore b/.gitignore index 14f5af9..7b15833 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Compiler files cache/ out/ +*node_modules/ # Ignores development broadcast logs !/broadcast diff --git a/price_updater/.env.example b/price_updater/.env.example new file mode 100644 index 0000000..e5af586 --- /dev/null +++ b/price_updater/.env.example @@ -0,0 +1,17 @@ +# Sepolia +MAINNET_RPC_URL=https://example.com +ARBITRUM_RPC_URL=https://example.com +PRIVATE_KEY=0x2...... +TARGET_CHAIN=10003 +TARGET_ADDRESS=0x744836a91f5151c6ef730eb7e07c232997debaaa +PRICE_FEED=0x38211b7ed71fa91fd65b69b6bf2c02efe60fc35b +PRICE_FEED_SENDER=0xe572a8631a49ec4c334812bb692beecf934ac4e9 + +# Mainnet +MAINNET_RPC_URL=https://example.com +ARBITRUM_RPC_URL=https://example.com +PRIVATE_KEY=0x2...... +TARGET_CHAIN=23 +TARGET_ADDRESS=0xbd335c16c94be8c4dd073ae376ddf78bec1858df +PRICE_FEED=0xba74737a078c05500dd98c970909e4a3b90c35c6 +PRICE_FEED_SENDER=0xf7d4e7273e5015c96728a6b02f31c505ee184603 diff --git a/price_updater/requirements.txt b/price_updater/requirements.txt new file mode 100644 index 0000000..46f15a1 --- /dev/null +++ b/price_updater/requirements.txt @@ -0,0 +1,39 @@ +aiohttp==3.9.5 +aiosignal==1.3.1 +attrs==23.2.0 +bitarray==2.9.2 +certifi==2024.2.2 +charset-normalizer==3.3.2 +ckzg==1.0.2 +cytoolz==0.12.3 +eth-account==0.11.2 +eth-hash==0.7.0 +eth-keyfile==0.8.1 +eth-keys==0.5.1 +eth-rlp==1.0.1 +eth-typing==4.2.3 +eth-utils==4.1.1 +eth_abi==5.1.0 +frozenlist==1.4.1 +hexbytes==0.3.1 +idna==3.7 +jsonschema==4.22.0 +jsonschema-specifications==2023.12.1 +lru-dict==1.2.0 +multidict==6.0.5 +parsimonious==0.10.0 +protobuf==5.27.0 +pycryptodome==3.20.0 +python-dotenv==1.0.1 +pyunormalize==15.1.0 +referencing==0.35.1 +regex==2024.5.15 +requests==2.32.2 +rlp==4.0.1 +rpds-py==0.18.1 +toolz==0.12.1 +typing_extensions==4.12.0 +urllib3==2.2.1 +web3==6.19.0 +websockets==12.0 +yarl==1.9.4 diff --git a/price_updater/update_price.py b/price_updater/update_price.py new file mode 100644 index 0000000..d2c73c6 --- /dev/null +++ b/price_updater/update_price.py @@ -0,0 +1,63 @@ +import os +import time +from web3 import Web3 +from dotenv import load_dotenv + +load_dotenv() + +TWELVE_HOURS = 12 * 60 * 60 + +MAINNET_PROVIDER = Web3(Web3.HTTPProvider(os.getenv("MAINNET_RPC_URL"))) +ARBITRUM_PROVIDER = Web3(Web3.HTTPProvider(os.getenv("ARBITRUM_RPC_URL"))) +PRIVATE_KEY = os.getenv("PRIVATE_KEY") +ACCOUNT = MAINNET_PROVIDER.eth.account.from_key(PRIVATE_KEY) +ACCOUNT_ADDRESS = ACCOUNT.address + +price_feed_abi = [ + {"constant": True, "inputs": [], "name": "latestTimestamp", "outputs": [{"name": "", "type": "uint256"}], "payable": False, "stateMutability": "view", "type": "function"}, + {"constant": False, "inputs": [{"name": "targetChain", "type": "uint16"}, {"name": "targetAddress", "type": "address"}], "name": "syncRate", "outputs": [], "payable": True, "stateMutability": "payable", "type": "function"}, + {"constant": True, "inputs": [{"name": "targetChain", "type": "uint16"}], "name": "quoteRateSync", "outputs": [{"name": "cost", "type": "uint256"}], "payable": False, "stateMutability": "view", "type": "function"}, +] + +price_feed_address = Web3.to_checksum_address(os.getenv("PRICE_FEED")) # Mainnet +price_feed_sender_address = Web3.to_checksum_address(os.getenv("PRICE_FEED_SENDER")) # Mainnet +target_chain = int(os.getenv("TARGET_CHAIN")) +target_address = Web3.to_checksum_address(os.getenv("TARGET_ADDRESS")) # Arbitrum + +def check_and_sync(): + price_feed = MAINNET_PROVIDER.eth.contract(address=price_feed_address, abi=price_feed_abi) + price_feed_sender = MAINNET_PROVIDER.eth.contract(address=price_feed_sender_address, abi=price_feed_abi) + + # Step 1: Check latest timestamp + latest_timestamp = price_feed.functions.latestTimestamp().call() + current_time = int(time.time()) + + if current_time - latest_timestamp < TWELVE_HOURS: + print("Less than 12 hours since the last update. No action needed.") + return + + # Step 2: Get the cost + current_rate = price_feed_sender.functions.quoteRateSync(target_chain).call() + + # Step 3: Sync the rate + tx = price_feed_sender.functions.syncRate(target_chain, target_address).build_transaction({ + 'chainId': 1, # Mainnet + 'gas': 200000, + 'gasPrice': Web3.to_wei('20', 'gwei'), + 'nonce': MAINNET_PROVIDER.eth.get_transaction_count(ACCOUNT.address), + 'value': current_rate + }) + + signed_tx = ACCOUNT.sign_transaction(tx) + tx_hash = MAINNET_PROVIDER.eth.send_raw_transaction(signed_tx.rawTransaction) + + print(f"Sync transaction sent: {tx_hash.hex()}") + receipt = MAINNET_PROVIDER.eth.wait_for_transaction_receipt(tx_hash) + print("Sync transaction confirmed.") + +if __name__ == "__main__": + try: + check_and_sync() + except Exception as e: + print(f"Error in check_and_sync: {e}") + exit(1)