Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Init script and CI #1

Merged
merged 20 commits into from
Jun 3, 2024
Merged
89 changes: 89 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
@@ -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<<EOF'
forge coverage | grep '^|' | grep -v 'test/'
echo EOF
} >> "$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
});
}
42 changes: 42 additions & 0 deletions .github/workflows/docker.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Docker

on: [push]

jobs:
docker:
name: Build Docker Image
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: |
europe-west4-docker.pkg.dev/stakewiselabs/public/multichain
flavor: |
latest=auto
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GAR
uses: docker/login-action@v3
with:
registry: europe-west4-docker.pkg.dev
username: _json_key
password: ${{ secrets.GAR_JSON_KEY }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: price_updater
file: price_updater/Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
24 changes: 24 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Lint

on:
push:
branches:
- main
pull_request:
antares-sw marked this conversation as resolved.
Show resolved Hide resolved

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
39 changes: 39 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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
antares-sw marked this conversation as resolved.
Show resolved Hide resolved
with:
node-version: 18

- run: yarn
antares-sw marked this conversation as resolved.
Show resolved Hide resolved
working-directory: test

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Build
run: forge build
env:
FOUNDRY_PROFILE: ci
antares-sw marked this conversation as resolved.
Show resolved Hide resolved

- name: Run tests
run: forge test --isolate -vvv
env:
FOUNDRY_PROFILE: ci
antares-sw marked this conversation as resolved.
Show resolved Hide resolved
FORGE_SNAPSHOT_CHECK: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Compiler files
cache/
out/
*node_modules/
antares-sw marked this conversation as resolved.
Show resolved Hide resolved

# Ignores development broadcast logs
!/broadcast
Expand Down
17 changes: 17 additions & 0 deletions price_updater/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Sepolia
antares-sw marked this conversation as resolved.
Show resolved Hide resolved
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
antares-sw marked this conversation as resolved.
Show resolved Hide resolved

# Mainnet
antares-sw marked this conversation as resolved.
Show resolved Hide resolved
MAINNET_RPC_URL=https://example.com
antares-sw marked this conversation as resolved.
Show resolved Hide resolved
ARBITRUM_RPC_URL=https://example.com
antares-sw marked this conversation as resolved.
Show resolved Hide resolved
PRIVATE_KEY=0x2......
TARGET_CHAIN=23
TARGET_ADDRESS=0xbd335c16c94be8c4dd073ae376ddf78bec1858df
PRICE_FEED=0xba74737a078c05500dd98c970909e4a3b90c35c6
PRICE_FEED_SENDER=0xf7d4e7273e5015c96728a6b02f31c505ee184603
11 changes: 11 additions & 0 deletions price_updater/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM python:3.11-alpine

WORKDIR /app

COPY requirements.txt .

RUN pip install -r requirements.txt

COPY update_price.py .

ENTRYPOINT ["python", "update_price.py"]
18 changes: 18 additions & 0 deletions price_updater/manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: multichain-price-update
spec:
schedule: "00 */6 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: update
image: europe-west4-docker.pkg.dev/stakewiselabs/public/multichain:latest
imagePullPolicy: Always
envFrom:
- configMapRef:
name: multichain-config
restartPolicy: OnFailure
39 changes: 39 additions & 0 deletions price_updater/requirements.txt
Original file line number Diff line number Diff line change
@@ -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
64 changes: 64 additions & 0 deletions price_updater/update_price.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import os
import time
import logging
from eth_account import Account
from web3 import Web3
from dotenv import load_dotenv
from web3.middleware import construct_sign_and_send_raw_middleware

load_dotenv()

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

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 = Account.from_key(PRIVATE_KEY)
ACCOUNT_ADDRESS = ACCOUNT.address
MAINNET_PROVIDER.middleware_onion.add(construct_sign_and_send_raw_middleware(ACCOUNT))

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
antares-sw marked this conversation as resolved.
Show resolved Hide resolved
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
antares-sw marked this conversation as resolved.
Show resolved Hide resolved

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()
antares-sw marked this conversation as resolved.
Show resolved Hide resolved
current_time = int(time.time())

if current_time - latest_timestamp < TWELVE_HOURS:
logger.info("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).transact({
'from': ACCOUNT_ADDRESS,
'value': current_rate,
})

logger.info(f"Sync transaction sent: {tx.hex()}")
receipt = MAINNET_PROVIDER.eth.wait_for_transaction_receipt(tx)
tsudmi marked this conversation as resolved.
Show resolved Hide resolved
logger.info("Sync transaction confirmed.")

if __name__ == "__main__":
try:
check_and_sync()
except Exception as e:
logger.error(f"Error in check_and_sync: {e}")
exit(1)
antares-sw marked this conversation as resolved.
Show resolved Hide resolved
Loading