Compare commits

..

No commits in common. "main" and "pyth-js-v39" have entirely different histories.

479 changed files with 16690 additions and 51083 deletions

View File

@ -15,4 +15,5 @@
.git
!apps/hermes/src/state/cache.rs
hermes/wormhole
!hermes/src/state/cache.rs

View File

@ -21,10 +21,10 @@ jobs:
- uses: actions/checkout@v3
- name: Download CLI
run: wget https://github.com/aptos-labs/aptos-core/releases/download/aptos-cli-v3.1.0/aptos-cli-3.1.0-Ubuntu-22.04-x86_64.zip
run: wget https://github.com/aptos-labs/aptos-core/releases/download/aptos-cli-v1.0.4/aptos-cli-1.0.4-Ubuntu-22.04-x86_64.zip
- name: Unzip CLI
run: unzip aptos-cli-3.1.0-Ubuntu-22.04-x86_64.zip
run: unzip aptos-cli-1.0.4-Ubuntu-22.04-x86_64.zip
- name: Run tests
run: ./aptos move test

View File

@ -2,10 +2,10 @@ name: Check Fortuna
on:
pull_request:
paths: [apps/fortuna/**]
paths: [fortuna/**]
push:
branches: [main]
paths: [apps/fortuna/**]
paths: [fortuna/**]
jobs:
test:
runs-on: ubuntu-latest
@ -17,4 +17,4 @@ jobs:
toolchain: nightly-2023-07-23
override: true
- name: Run executor tests
run: cargo test --manifest-path ./apps/fortuna/Cargo.toml
run: cargo test --manifest-path ./fortuna/Cargo.toml

View File

@ -1,35 +0,0 @@
name: Test Fuel Contract
on:
pull_request:
paths:
- target_chains/fuel/**
push:
branches:
- main
paths:
- target_chains/fuel/**
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: target_chains/fuel/contracts/
steps:
- uses: actions/checkout@v2
- name: Install Fuel toolchain
run: |
curl https://install.fuel.network | sh
echo "$HOME/.fuelup/bin" >> $GITHUB_PATH
- name: Build with Forc
run: forc build --verbose
- name: Run tests with Forc
run: forc test --verbose
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

View File

@ -1,23 +0,0 @@
name: Check Hermes
on:
pull_request:
paths: [apps/hermes/**]
push:
branches: [main]
paths: [apps/hermes/**]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2024-03-26
components: rustfmt, clippy
override: true
- name: Install protoc
uses: arduino/setup-protoc@v3
- name: Run executor tests
run: cargo test --manifest-path ./apps/hermes/Cargo.toml

View File

@ -27,13 +27,6 @@ jobs:
profile: minimal
toolchain: nightly-2023-07-23
components: rustfmt, clippy
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2024-03-26
components: rustfmt, clippy
- name: Install protoc
uses: arduino/setup-protoc@v3
- uses: actions/checkout@v4
- name: Install poetry
run: pipx install poetry

View File

@ -1,37 +0,0 @@
name: Starknet contract
on:
pull_request:
paths:
- target_chains/starknet/contracts/**
push:
branches:
- main
paths:
- target_chains/starknet/contracts/**
jobs:
check:
name: Starknet Foundry tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: target_chains/starknet/contracts/
steps:
- uses: actions/checkout@v3
- name: Install Scarb
uses: software-mansion/setup-scarb@v1
with:
tool-versions: target_chains/starknet/contracts/.tool-versions
- name: Install Starknet Foundry
uses: foundry-rs/setup-snfoundry@v3
with:
tool-versions: target_chains/starknet/contracts/.tool-versions
- name: Install Starkli
run: curl https://get.starkli.sh | sh && . ~/.config/.starkli/env && starkliup -v $(awk '/starkli/{print $2}' .tool-versions)
- name: Install Katana
run: curl -L https://install.dojoengine.org | bash && PATH="$PATH:$HOME/.config/.dojo/bin" dojoup -v $(awk '/dojo/{print $2}' .tool-versions)
- name: Check formatting
run: scarb fmt --check
- name: Run tests
run: snforge test
- name: Test local deployment script
run: bash -c 'PATH="$PATH:$HOME/.config/.dojo/bin" katana & . ~/.config/.starkli/env && deploy/local_deploy'

View File

@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: "18"
node-version: "16"
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npx lerna run build --no-private

View File

@ -11,14 +11,8 @@ jobs:
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
default: true
profile: minimal
- run: cargo +stable-x86_64-unknown-linux-gnu publish --token ${CARGO_REGISTRY_TOKEN}
- run: cargo publish --token ${CARGO_REGISTRY_TOKEN}
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
working-directory: "target_chains/solana/pyth_solana_receiver_sdk"

View File

@ -46,7 +46,7 @@ jobs:
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
with:
context: .
file: "./apps/fortuna/Dockerfile"
file: "./fortuna/Dockerfile"
push: true
tags: ${{ steps.metadata_fortuna.outputs.tags }}
labels: ${{ steps.metadata_fortuna.outputs.labels }}

View File

@ -37,7 +37,7 @@ jobs:
env:
AWS_REGION: us-east-1
- run: |
DOCKER_BUILDKIT=1 docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f apps/hermes/Dockerfile .
DOCKER_BUILDKIT=1 docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f hermes/Dockerfile .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
env:
ECR_REGISTRY: public.ecr.aws

View File

@ -40,7 +40,7 @@ jobs:
id: ecr_login
- run: |
DOCKER_BUILDKIT=1 docker build -t lerna -f Dockerfile.lerna .
DOCKER_BUILDKIT=1 docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f apps/price_pusher/Dockerfile .
DOCKER_BUILDKIT=1 docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f price_pusher/Dockerfile .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
env:
ECR_REGISTRY: public.ecr.aws

View File

@ -6,12 +6,8 @@ on:
permissions:
contents: read
id-token: write
packages: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: pyth-network/xc-admin-frontend
jobs:
xc-admin-frontend-image:
xc-admin-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@ -20,17 +16,23 @@ jobs:
SHORT_HASH=$(echo ${{ github.sha }} | cut -c1-7)
TIMESTAMP=$(date +%s)
echo "IMAGE_TAG=${TIMESTAMP}-${SHORT_HASH}" >> "${GITHUB_ENV}"
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
- uses: aws-actions/configure-aws-credentials@8a84b07f2009032ade05a88a28750d733cc30db1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
role-to-assume: arn:aws:iam::192824654885:role/github-actions-ecr
aws-region: eu-west-2
- uses: aws-actions/amazon-ecr-login@v1
id: ecr_login
- name: Build docker image
run: |
DOCKER_BUILDKIT=1 docker build -t lerna -f Dockerfile.lerna .
DOCKER_BUILDKIT=1 docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} -f governance/xc_admin/packages/xc_admin_frontend/Dockerfile .
DOCKER_BUILDKIT=1 docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f governance/xc_admin/packages/xc_admin_frontend/Dockerfile .
env:
ECR_REGISTRY: ${{ steps.ecr_login.outputs.registry }}
ECR_REPOSITORY: xc-admin-frontend
- name: Push docker image
if: github.ref == 'refs/heads/main'
run: |
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
env:
ECR_REGISTRY: ${{ steps.ecr_login.outputs.registry }}
ECR_REPOSITORY: xc-admin-frontend

View File

@ -6,10 +6,6 @@ on:
permissions:
contents: read
id-token: write
packages: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: pyth-network/xc-admin
jobs:
xc-admin-image:
runs-on: ubuntu-latest
@ -21,16 +17,16 @@ jobs:
PREFIX="refs/tags/xc-admin-"
VERSION="${GITHUB_REF:${#PREFIX}}"
echo "IMAGE_TAG=${VERSION}" >> "${GITHUB_ENV}"
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
- uses: aws-actions/configure-aws-credentials@8a84b07f2009032ade05a88a28750d733cc30db1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build docker image
run: |
role-to-assume: arn:aws:iam::192824654885:role/github-actions-ecr
aws-region: eu-west-2
- uses: aws-actions/amazon-ecr-login@v1
id: ecr_login
- run: |
DOCKER_BUILDKIT=1 docker build -t lerna -f Dockerfile.lerna .
DOCKER_BUILDKIT=1 docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} -f governance/xc_admin/Dockerfile .
- name: Push docker image
run: |
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
DOCKER_BUILDKIT=1 docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f governance/xc_admin/Dockerfile .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
env:
ECR_REGISTRY: ${{ steps.ecr_login.outputs.registry }}
ECR_REPOSITORY: xc-admin

View File

@ -5,24 +5,17 @@ on:
tags:
- "python-v*"
env:
PYTHON_VERSION: "3.11"
jobs:
deploy:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: |
python3 -m pip install --upgrade poetry
poetry install
working-directory: "express_relay/sdk/python/express_relay"
poetry -C express_relay/sdk/python/express_relay build
- name: Build and publish
run: |
poetry build
poetry publish --username __token__ --password ${{ secrets.PYPI_TOKEN }}
working-directory: "express_relay/sdk/python/express_relay"
poetry -C express_relay/sdk/python/express_relay build
poetry -C express_relay/sdk/python/express_relay publish --username __token__ --password ${{ secrets.PYPI_TOKEN }}

1
.npmrc
View File

@ -1 +0,0 @@
engine-strict=true

View File

@ -30,6 +30,21 @@ repos:
entry: cargo +nightly-2023-03-01 clippy --manifest-path ./governance/remote_executor/Cargo.toml --tests --fix --allow-dirty --allow-staged -- -D warnings
pass_filenames: false
files: governance/remote_executor
# Hooks for the attester
- id: cargo-fmt-attester
name: Cargo format for attester
language: "rust"
entry: cargo +nightly-2023-03-01 fmt --manifest-path ./wormhole_attester/Cargo.toml --all -- --config-path rustfmt.toml
pass_filenames: false
files: wormhole_attester
- id: cargo-clippy-attester
name: Cargo clippy for attester
language: "rust"
entry: |
bash -c 'EMITTER_ADDRESS=0 BRIDGE_ADDRESS=0 cargo +nightly-2023-03-01 clippy --manifest-path \
./wormhole_attester/Cargo.toml --tests --fix --allow-dirty --allow-staged -- -D warnings'
pass_filenames: false
files: wormhole_attester
# Hooks for cosmwasm contract
- id: cargo-fmt-cosmwasm
name: Cargo format for cosmwasm contract
@ -45,24 +60,18 @@ repos:
files: target_chains/cosmwasm
# Hooks for Hermes
- id: cargo-fmt-hermes
name: Cargo format for Hermes
name: Cargo format for Pyth Hermes
language: "rust"
entry: cargo +nightly-2024-03-26 fmt --manifest-path ./apps/hermes/Cargo.toml --all -- --config-path rustfmt.toml
entry: cargo +nightly-2023-07-23 fmt --manifest-path ./hermes/Cargo.toml --all -- --config-path rustfmt.toml
pass_filenames: false
files: apps/hermes
- id: cargo-clippy-hermes
name: Cargo clippy for Hermes
language: "rust"
entry: cargo +nightly-2024-03-26 clippy --manifest-path ./apps/hermes/Cargo.toml --tests --fix --allow-dirty --allow-staged -- -D warnings
pass_filenames: false
files: apps/hermes
files: hermes
# Hooks for Fortuna
- id: cargo-fmt-fortuna
name: Cargo format for Fortuna
language: "rust"
entry: cargo +nightly-2023-07-23 fmt --manifest-path ./apps/fortuna/Cargo.toml --all -- --config-path rustfmt.toml
entry: cargo +nightly-2023-07-23 fmt --manifest-path ./fortuna/Cargo.toml --all -- --config-path rustfmt.toml
pass_filenames: false
files: apps/fortuna
files: fortuna
# Hooks for message buffer contract
- id: cargo-fmt-message-buffer
name: Cargo format for message buffer contract
@ -80,13 +89,13 @@ repos:
- id: cargo-fmt-pythnet-sdk
name: Cargo format for pythnet SDK
language: "rust"
entry: cargo +nightly-2024-03-26 fmt --manifest-path ./pythnet/pythnet_sdk/Cargo.toml --all -- --config-path rustfmt.toml
entry: cargo +nightly-2023-07-23 fmt --manifest-path ./pythnet/pythnet_sdk/Cargo.toml --all -- --config-path rustfmt.toml
pass_filenames: false
files: pythnet/pythnet_sdk
- id: cargo-clippy-pythnet-sdk
name: Cargo clippy for pythnet SDK
language: "rust"
entry: cargo +nightly-2024-03-26 clippy --manifest-path ./pythnet/pythnet_sdk/Cargo.toml --tests --fix --allow-dirty --allow-staged -- -D warnings
entry: cargo +nightly-2023-07-23 clippy --manifest-path ./pythnet/pythnet_sdk/Cargo.toml --tests --fix --allow-dirty --allow-staged -- -D warnings
pass_filenames: false
files: pythnet/pythnet_sdk
# Hooks for solana receiver contract
@ -103,24 +112,18 @@ repos:
pass_filenames: false
files: target_chains/solana
# For express relay python files
- id: poetry-install
name: poetry install
entry: poetry -C express_relay/sdk/python/express_relay install
pass_filenames: false
files: express_relay/sdk/python/express_relay
language: "system"
- id: black
name: black
entry: poetry -C express_relay/sdk/python/express_relay run black
files: express_relay/sdk/python/express_relay
entry: poetry -C express_relay/sdk/python/express_relay run black express_relay/sdk/python/express_relay
pass_filenames: false
language: "system"
- id: pyflakes
name: pyflakes
entry: poetry -C express_relay/sdk/python/express_relay run pyflakes
files: express_relay/sdk/python/express_relay
entry: poetry -C express_relay/sdk/python/express_relay run pyflakes express_relay/sdk/python/express_relay
pass_filenames: false
language: "system"
- id: mypy
name: mypy
entry: poetry -C express_relay/sdk/python/express_relay run mypy
files: express_relay/sdk/python/express_relay
entry: poetry -C express_relay/sdk/python/express_relay run mypy express_relay/sdk/python/express_relay
pass_filenames: false
language: "system"

View File

@ -16,7 +16,7 @@ contracts, SDKs, and examples.
## Hermes
> [hermes](./apps/hermes/)
> [hermes](./hermes/)
Hermes is an off-chain service which constantly observes Pythnet and the
Wormhole network watching for price updates emitted from the Pyth contract. It
@ -79,11 +79,10 @@ Lerna has some common failure modes that you may encounter:
1. `npm ci` fails with a typescript compilation error about a missing package.
This error likely means that the failing package has a `prepare` entry compiling the typescript in its `package.json`.
Fix this error by moving that logic to the `prepublishOnly` entry.
2. The software builds locally but fails in CI, or vice-versa.
1. The software builds locally but fails in CI, or vice-versa.
This error likely means that some local build caches need to be cleaned.
The build error may not indicate that this is a caching issue, e.g., it may appear that the packages are being built in the wrong order.
Delete `node_modules/`, `lib/` and `tsconfig.tsbuildinfo` from each package's subdirectory. then try again.
3. `npm ci` fails due to wrong node version. Make sure to be using `v18`. Node version `v21` is not supported and known to cause issues.
## Audit / Feature Status

View File

@ -1,7 +0,0 @@
chains:
lightlink-pegasus:
commitments:
# prettier-ignore
- seed: [219,125,217,197,234,88,208,120,21,181,172,143,239,102,41,233,167,212,237,106,37,255,184,165,238,121,230,155,116,158,173,48]
chain_length: 10000
original_commitment_sequence_number: 104

View File

@ -1 +0,0 @@
nightly-2023-07-23

View File

@ -1,228 +0,0 @@
use {
crate::{
api::{
self,
BlockchainState,
ChainId,
},
chain::ethereum::PythContract,
command::register_provider::CommitmentMetadata,
config::{
Commitment,
Config,
ProviderConfig,
RunOptions,
},
keeper,
state::{
HashChainState,
PebbleHashChain,
},
},
anyhow::{
anyhow,
Error,
Result,
},
axum::Router,
std::{
collections::HashMap,
net::SocketAddr,
sync::Arc,
},
tokio::{
spawn,
sync::watch,
},
tower_http::cors::CorsLayer,
utoipa::OpenApi,
utoipa_swagger_ui::SwaggerUi,
};
pub async fn run_api(
socket_addr: SocketAddr,
chains: HashMap<String, api::BlockchainState>,
mut rx_exit: watch::Receiver<bool>,
) -> Result<()> {
#[derive(OpenApi)]
#[openapi(
paths(
crate::api::revelation,
crate::api::chain_ids,
),
components(
schemas(
crate::api::GetRandomValueResponse,
crate::api::Blob,
crate::api::BinaryEncoding,
)
),
tags(
(name = "fortuna", description = "Random number service for the Pyth Entropy protocol")
)
)]
struct ApiDoc;
let metrics_registry = api::Metrics::new();
let api_state = api::ApiState {
chains: Arc::new(chains),
metrics: Arc::new(metrics_registry),
};
// Initialize Axum Router. Note the type here is a `Router<State>` due to the use of the
// `with_state` method which replaces `Body` with `State` in the type signature.
let app = Router::new();
let app = app
.merge(SwaggerUi::new("/docs").url("/docs/openapi.json", ApiDoc::openapi()))
.merge(api::routes(api_state))
// Permissive CORS layer to allow all origins
.layer(CorsLayer::permissive());
tracing::info!("Starting server on: {:?}", &socket_addr);
// Binds the axum's server to the configured address and port. This is a blocking call and will
// not return until the server is shutdown.
axum::Server::try_bind(&socket_addr)?
.serve(app.into_make_service())
.with_graceful_shutdown(async {
// It can return an error or an Ok(()). In both cases, we would shut down.
// As Ok(()) means, exit signal (ctrl + c) was received.
// And Err(e) means, the sender was dropped which should not be the case.
let _ = rx_exit.changed().await;
tracing::info!("Shutting down RPC server...");
})
.await?;
Ok(())
}
pub async fn run_keeper(
chains: HashMap<String, api::BlockchainState>,
config: Config,
private_key: String,
) -> Result<()> {
let mut handles = Vec::new();
for (chain_id, chain_config) in chains {
let chain_eth_config = config
.chains
.get(&chain_id)
.expect("All chains should be present in the config file")
.clone();
let private_key = private_key.clone();
handles.push(spawn(keeper::run_keeper_threads(
private_key,
chain_eth_config,
chain_config.clone(),
)));
}
Ok(())
}
pub async fn run(opts: &RunOptions) -> Result<()> {
let config = Config::load(&opts.config.config)?;
let provider_config = opts
.provider_config
.provider_config
.as_ref()
.map(|path| ProviderConfig::load(&path).expect("Failed to load provider config"));
let secret = opts.randomness.load_secret()?;
let (tx_exit, rx_exit) = watch::channel(false);
let mut chains: HashMap<ChainId, BlockchainState> = HashMap::new();
for (chain_id, chain_config) in &config.chains {
let contract = Arc::new(PythContract::from_config(&chain_config)?);
let provider_chain_config = provider_config
.as_ref()
.and_then(|c| c.get_chain_config(chain_id));
let mut provider_commitments = provider_chain_config
.as_ref()
.map(|c| c.get_sorted_commitments())
.unwrap_or_else(|| Vec::new());
let provider_info = contract.get_provider_info(opts.provider).call().await?;
let latest_metadata =
bincode::deserialize::<CommitmentMetadata>(&provider_info.commitment_metadata)
.map_err(|e| {
anyhow!(
"Chain: {} - Failed to deserialize commitment metadata: {}",
&chain_id,
e
)
})?;
provider_commitments.push(Commitment {
seed: latest_metadata.seed,
chain_length: latest_metadata.chain_length,
original_commitment_sequence_number: provider_info.original_commitment_sequence_number,
});
// TODO: we may want to load the hash chain in a lazy/fault-tolerant way. If there are many blockchains,
// then it's more likely that some RPC fails. We should tolerate these faults and generate the hash chain
// later when a user request comes in for that chain.
let mut offsets = Vec::<usize>::new();
let mut hash_chains = Vec::<PebbleHashChain>::new();
for commitment in &provider_commitments {
let offset = commitment.original_commitment_sequence_number.try_into()?;
offsets.push(offset);
let pebble_hash_chain = PebbleHashChain::from_config(
&secret,
&chain_id,
&opts.provider,
&chain_config.contract_addr,
&commitment.seed,
commitment.chain_length,
)?;
hash_chains.push(pebble_hash_chain);
}
let chain_state = HashChainState {
offsets,
hash_chains,
};
if chain_state.reveal(provider_info.original_commitment_sequence_number)?
!= provider_info.original_commitment
{
return Err(anyhow!("The root of the generated hash chain for chain id {} does not match the commitment. Are the secret and chain length configured correctly?", &chain_id).into());
} else {
tracing::info!("Root of chain id {} matches commitment", &chain_id);
}
let state = api::BlockchainState {
id: chain_id.clone(),
state: Arc::new(chain_state),
contract,
provider_address: opts.provider,
reveal_delay_blocks: chain_config.reveal_delay_blocks,
confirmed_block_status: chain_config.confirmed_block_status,
};
chains.insert(chain_id.clone(), state);
}
// Listen for Ctrl+C so we can set the exit flag and wait for a graceful shutdown.
spawn(async move {
tracing::info!("Registered shutdown signal handler...");
tokio::signal::ctrl_c().await.unwrap();
tracing::info!("Shut down signal received, waiting for tasks...");
// no need to handle error here, as it will only occur when all the
// receiver has been dropped and that's what we want to do
tx_exit.send(true)?;
Ok::<(), Error>(())
});
if let Some(keeper_private_key) = opts.load_keeper_private_key()? {
spawn(run_keeper(chains.clone(), config, keeper_private_key));
}
run_api(opts.addr.clone(), chains, rx_exit).await?;
Ok(())
}

View File

@ -1,55 +0,0 @@
use {
crate::config::{
ConfigOptions,
ProviderConfigOptions,
RandomnessOptions,
},
anyhow::Result,
clap::Args,
ethers::types::Address,
std::{
fs,
net::SocketAddr,
},
};
/// Run the webservice
#[derive(Args, Clone, Debug)]
pub struct RunOptions {
#[command(flatten)]
pub config: ConfigOptions,
#[command(flatten)]
pub provider_config: ProviderConfigOptions,
#[command(flatten)]
pub randomness: RandomnessOptions,
/// Address and port the HTTP server will bind to.
#[arg(long = "rpc-listen-addr")]
#[arg(default_value = super::DEFAULT_RPC_ADDR)]
#[arg(env = "RPC_ADDR")]
pub addr: SocketAddr,
/// The public key of the provider whose requests the server will respond to.
#[arg(long = "provider")]
#[arg(env = "FORTUNA_PROVIDER")]
pub provider: Address,
/// If provided, the keeper will run alongside the Fortuna API service.
/// It should be a path to a file containing a 20-byte (40 char) hex encoded Ethereum private key.
/// This key is required to submit transactions for entropy callback requests.
/// This key should not be a registered provider.
#[arg(long = "keeper-private-key")]
#[arg(env = "KEEPER_PRIVATE_KEY")]
pub keeper_private_key_file: Option<String>,
}
impl RunOptions {
pub fn load_keeper_private_key(&self) -> Result<Option<String>> {
if let Some(ref keeper_private_key_file) = self.keeper_private_key_file {
return Ok(Some(fs::read_to_string(keeper_private_key_file)?));
}
return Ok(None);
}
}

View File

@ -1,487 +0,0 @@
use {
crate::{
api::{
self,
BlockchainState,
},
chain::{
ethereum::SignablePythContract,
reader::{
BlockNumber,
RequestedWithCallbackEvent,
},
},
config::EthereumConfig,
},
anyhow::{
anyhow,
Result,
},
ethers::{
contract::ContractError,
providers::{
Middleware,
Provider,
Ws,
},
types::U256,
},
futures::StreamExt,
std::sync::Arc,
tokio::{
spawn,
sync::mpsc,
time::{
self,
Duration,
},
},
tracing::{
self,
Instrument,
},
};
#[derive(Debug)]
pub struct BlockRange {
pub from: BlockNumber,
pub to: BlockNumber,
}
/// How much to wait before retrying in case of an RPC error
const RETRY_INTERVAL: Duration = Duration::from_secs(5);
/// How many blocks to look back for events that might be missed when starting the keeper
const BACKLOG_RANGE: u64 = 1000;
/// How many blocks to fetch events for in a single rpc call
const BLOCK_BATCH_SIZE: u64 = 100;
/// How much to wait before polling the next latest block
const POLL_INTERVAL: Duration = Duration::from_secs(5);
/// Get the latest safe block number for the chain. Retry internally if there is an error.
async fn get_latest_safe_block(chain_state: &BlockchainState) -> BlockNumber {
loop {
match chain_state
.contract
.get_block_number(chain_state.confirmed_block_status)
.await
{
Ok(latest_confirmed_block) => {
tracing::info!(
"Fetched latest safe block {}",
latest_confirmed_block - chain_state.reveal_delay_blocks
);
return latest_confirmed_block - chain_state.reveal_delay_blocks;
}
Err(e) => {
tracing::error!("Error while getting block number. error: {:?}", e);
time::sleep(RETRY_INTERVAL).await;
}
}
}
}
/// Run threads to handle events for the last `BACKLOG_RANGE` blocks, watch for new blocks and
/// handle any events for the new blocks.
#[tracing::instrument(name="keeper", skip_all, fields(chain_id=chain_state.id))]
pub async fn run_keeper_threads(
private_key: String,
chain_eth_config: EthereumConfig,
chain_state: BlockchainState,
) {
tracing::info!("starting keeper");
let latest_safe_block = get_latest_safe_block(&chain_state).in_current_span().await;
tracing::info!("latest safe block: {}", &latest_safe_block);
let contract = Arc::new(
SignablePythContract::from_config(&chain_eth_config, &private_key)
.await
.expect("Chain config should be valid"),
);
// Spawn a thread to handle the events from last BACKLOG_RANGE blocks.
spawn(
process_backlog(
BlockRange {
from: latest_safe_block.saturating_sub(BACKLOG_RANGE),
to: latest_safe_block,
},
contract.clone(),
chain_eth_config.gas_limit,
chain_state.clone(),
)
.in_current_span(),
);
let (tx, rx) = mpsc::channel::<BlockRange>(1000);
// Spawn a thread to watch for new blocks and send the range of blocks for which events has not been handled to the `tx` channel.
spawn(
watch_blocks_wrapper(
chain_state.clone(),
latest_safe_block,
tx,
chain_eth_config.geth_rpc_wss.clone(),
)
.in_current_span(),
);
// Spawn a thread that listens for block ranges on the `rx` channel and processes the events for those blocks.
spawn(
process_new_blocks(
chain_state.clone(),
rx,
Arc::clone(&contract),
chain_eth_config.gas_limit,
)
.in_current_span(),
);
}
/// Process an event for a chain. It estimates the gas for the reveal with callback and
/// submits the transaction if the gas estimate is below the gas limit.
/// It will return an Error if the gas estimation failed with a provider error or if the
/// reveal with callback failed with a provider error.
pub async fn process_event(
event: RequestedWithCallbackEvent,
chain_config: &BlockchainState,
contract: &Arc<SignablePythContract>,
gas_limit: U256,
) -> Result<()> {
if chain_config.provider_address != event.provider_address {
return Ok(());
}
let provider_revelation = match chain_config.state.reveal(event.sequence_number) {
Ok(result) => result,
Err(e) => {
tracing::error!(
sequence_number = &event.sequence_number,
"Error while revealing with error: {:?}",
e
);
return Ok(());
}
};
let gas_estimate_res = chain_config
.contract
.estimate_reveal_with_callback_gas(
event.provider_address,
event.sequence_number,
event.user_random_number,
provider_revelation,
)
.in_current_span()
.await;
match gas_estimate_res {
Ok(gas_estimate_option) => match gas_estimate_option {
Some(gas_estimate) => {
// Pad the gas estimate by 33%
let (gas_estimate, _) = gas_estimate
.saturating_mul(U256::from(4))
.div_mod(U256::from(3));
if gas_estimate > gas_limit {
tracing::error!(
sequence_number = &event.sequence_number,
"Gas estimate for reveal with callback is higher than the gas limit"
);
return Ok(());
}
let contract_call = contract
.reveal_with_callback(
event.provider_address,
event.sequence_number,
event.user_random_number,
provider_revelation,
)
.gas(gas_estimate);
let res = contract_call.send().await;
let pending_tx = match res {
Ok(pending_tx) => pending_tx,
Err(e) => match e {
// If there is a provider error, we weren't able to send the transaction.
// We will return an error. So, that the caller can decide what to do (retry).
ContractError::ProviderError { e } => return Err(e.into()),
// For all the other errors, it is likely the case we won't be able to reveal for
// ever. We will return an Ok(()) to signal that we have processed this reveal
// and concluded that its Ok to not reveal.
_ => {
tracing::error!(
sequence_number = &event.sequence_number,
"Error while revealing with error: {:?}",
e
);
return Ok(());
}
},
};
match pending_tx.await {
Ok(res) => {
tracing::info!(
sequence_number = &event.sequence_number,
"Revealed with res: {:?}",
res
);
Ok(())
}
Err(e) => {
tracing::error!(
sequence_number = &event.sequence_number,
"Error while revealing with error: {:?}",
e
);
Err(e.into())
}
}
}
None => {
tracing::info!(
sequence_number = &event.sequence_number,
"Not processing event"
);
Ok(())
}
},
Err(e) => {
tracing::error!(
sequence_number = &event.sequence_number,
"Error while simulating reveal with error: {:?}",
e
);
Err(e)
}
}
}
/// Process a range of blocks in batches. It calls the `process_single_block_batch` method for each batch.
#[tracing::instrument(skip_all, fields(range_from_block=block_range.from, range_to_block=block_range.to))]
pub async fn process_block_range(
block_range: BlockRange,
contract: Arc<SignablePythContract>,
gas_limit: U256,
chain_state: api::BlockchainState,
) {
let BlockRange {
from: first_block,
to: last_block,
} = block_range;
let mut current_block = first_block;
while current_block <= last_block {
let mut to_block = current_block + BLOCK_BATCH_SIZE;
if to_block > last_block {
to_block = last_block;
}
process_single_block_batch(
BlockRange {
from: current_block,
to: to_block,
},
contract.clone(),
gas_limit,
chain_state.clone(),
)
.in_current_span()
.await;
current_block = to_block + 1;
}
}
/// Process a batch of blocks for a chain. It will fetch events for all the blocks in a single call for the provided batch
/// and then try to process them one by one. If the process fails, it will retry indefinitely.
#[tracing::instrument(name="batch", skip_all, fields(batch_from_block=block_range.from, batch_to_block=block_range.to))]
pub async fn process_single_block_batch(
block_range: BlockRange,
contract: Arc<SignablePythContract>,
gas_limit: U256,
chain_state: api::BlockchainState,
) {
loop {
let events_res = chain_state
.contract
.get_request_with_callback_events(block_range.from, block_range.to)
.await;
match events_res {
Ok(events) => {
tracing::info!(num_of_events = &events.len(), "Processing",);
for event in &events {
tracing::info!(sequence_number = &event.sequence_number, "Processing event",);
while let Err(e) =
process_event(event.clone(), &chain_state, &contract, gas_limit)
.in_current_span()
.await
{
tracing::error!(
sequence_number = &event.sequence_number,
"Error while processing event. Waiting for {} seconds before retry. error: {:?}",
RETRY_INTERVAL.as_secs(),
e
);
time::sleep(RETRY_INTERVAL).await;
}
tracing::info!(sequence_number = &event.sequence_number, "Processed event",);
}
tracing::info!(num_of_events = &events.len(), "Processed",);
break;
}
Err(e) => {
tracing::error!(
"Error while getting events. Waiting for {} seconds before retry. error: {:?}",
RETRY_INTERVAL.as_secs(),
e
);
time::sleep(RETRY_INTERVAL).await;
}
}
}
}
/// Wrapper for the `watch_blocks` method. If there was an error while watching, it will retry after a delay.
/// It retries indefinitely.
#[tracing::instrument(name="watch_blocks", skip_all, fields(initial_safe_block=latest_safe_block))]
pub async fn watch_blocks_wrapper(
chain_state: BlockchainState,
latest_safe_block: BlockNumber,
tx: mpsc::Sender<BlockRange>,
geth_rpc_wss: Option<String>,
) {
let mut last_safe_block_processed = latest_safe_block;
loop {
if let Err(e) = watch_blocks(
chain_state.clone(),
&mut last_safe_block_processed,
tx.clone(),
geth_rpc_wss.clone(),
)
.in_current_span()
.await
{
tracing::error!("watching blocks. error: {:?}", e);
time::sleep(RETRY_INTERVAL).await;
}
}
}
/// Watch for new blocks and send the range of blocks for which events have not been handled to the `tx` channel.
/// We are subscribing to new blocks instead of events. If we miss some blocks, it will be fine as we are sending
/// block ranges to the `tx` channel. If we have subscribed to events, we could have missed those and won't even
/// know about it.
pub async fn watch_blocks(
chain_state: BlockchainState,
last_safe_block_processed: &mut BlockNumber,
tx: mpsc::Sender<BlockRange>,
geth_rpc_wss: Option<String>,
) -> Result<()> {
tracing::info!("Watching blocks to handle new events");
let provider_option = match geth_rpc_wss {
Some(wss) => Some(match Provider::<Ws>::connect(wss.clone()).await {
Ok(provider) => provider,
Err(e) => {
tracing::error!("Error while connecting to wss: {}. error: {:?}", wss, e);
return Err(e.into());
}
}),
None => {
tracing::info!("No wss provided");
None
}
};
let mut stream_option = match provider_option {
Some(ref provider) => Some(match provider.subscribe_blocks().await {
Ok(client) => client,
Err(e) => {
tracing::error!("Error while subscribing to blocks. error {:?}", e);
return Err(e.into());
}
}),
None => None,
};
loop {
match stream_option {
Some(ref mut stream) => {
if let None = stream.next().await {
tracing::error!("Error blocks subscription stream ended");
return Err(anyhow!("Error blocks subscription stream ended"));
}
}
None => {
time::sleep(POLL_INTERVAL).await;
}
}
let latest_safe_block = get_latest_safe_block(&chain_state).in_current_span().await;
if latest_safe_block > *last_safe_block_processed {
match tx
.send(BlockRange {
from: *last_safe_block_processed + 1,
to: latest_safe_block,
})
.await
{
Ok(_) => {
tracing::info!(
from_block = *last_safe_block_processed + 1,
to_block = &latest_safe_block,
"Block range sent to handle events",
);
*last_safe_block_processed = latest_safe_block;
}
Err(e) => {
tracing::error!(
"Error while sending block range to handle events. These will be handled in next call. error: {:?}",
e
);
}
};
}
}
}
/// It waits on rx channel to receive block ranges and then calls process_block_range to process them.
#[tracing::instrument(skip_all)]
pub async fn process_new_blocks(
chain_state: BlockchainState,
mut rx: mpsc::Receiver<BlockRange>,
contract: Arc<SignablePythContract>,
gas_limit: U256,
) {
tracing::info!("Waiting for new block ranges to process");
loop {
if let Some(block_range) = rx.recv().await {
process_block_range(
block_range,
Arc::clone(&contract),
gas_limit,
chain_state.clone(),
)
.in_current_span()
.await;
}
}
}
/// Processes the backlog_range for a chain.
#[tracing::instrument(skip_all)]
pub async fn process_backlog(
backlog_range: BlockRange,
contract: Arc<SignablePythContract>,
gas_limit: U256,
chain_state: BlockchainState,
) {
tracing::info!("Processing backlog");
process_block_range(backlog_range, contract, gas_limit, chain_state)
.in_current_span()
.await;
tracing::info!("Backlog processed");
}

View File

@ -1,26 +0,0 @@
# The rust version itself is not so important as we install a fixed
# nightly version. We use the latest stable version to get the latest
# updates and dependencies.
FROM rust:1.77.0 AS build
# Install OS packages
RUN apt-get update && apt-get install --yes \
build-essential curl clang libssl-dev protobuf-compiler
# Set default toolchain
RUN rustup default nightly-2024-03-26
# Build
WORKDIR /src
COPY apps/hermes apps/hermes
COPY pythnet/pythnet_sdk pythnet/pythnet_sdk
WORKDIR /src/apps/hermes
RUN --mount=type=cache,target=/root/.cargo/registry cargo build --release
FROM rust:1.77.0
# Copy artifacts from other images
COPY --from=build /src/apps/hermes/target/release/hermes /usr/local/bin/

View File

@ -1,2 +0,0 @@
[toolchain]
channel = "nightly-2024-03-26"

View File

@ -1,25 +0,0 @@
use {
crate::{
api::ApiState,
state::aggregate::Aggregates,
},
axum::{
extract::State,
http::StatusCode,
response::{
IntoResponse,
Response,
},
},
};
pub async fn ready<S>(State(state): State<ApiState<S>>) -> Response
where
S: Aggregates,
{
let state = &*state.state;
match Aggregates::is_ready(state).await {
true => (StatusCode::OK, "OK").into_response(),
false => (StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable").into_response(),
}
}

View File

@ -1,235 +0,0 @@
use {
crate::{
api::{
rest::{
verify_price_ids_exist,
RestError,
},
types::{
BinaryPriceUpdate,
EncodingType,
ParsedPriceUpdate,
PriceIdInput,
PriceUpdate,
RpcPriceIdentifier,
},
ApiState,
},
state::aggregate::{
Aggregates,
AggregationEvent,
RequestTime,
},
},
anyhow::Result,
axum::{
extract::State,
response::sse::{
Event,
KeepAlive,
Sse,
},
},
futures::Stream,
pyth_sdk::PriceIdentifier,
serde::Deserialize,
serde_qs::axum::QsQuery,
std::convert::Infallible,
tokio::sync::broadcast,
tokio_stream::{
wrappers::BroadcastStream,
StreamExt as _,
},
utoipa::IntoParams,
};
#[derive(Debug, Deserialize, IntoParams)]
#[into_params(parameter_in = Query)]
pub struct StreamPriceUpdatesQueryParams {
/// Get the most recent price update for this set of price feed ids.
///
/// This parameter can be provided multiple times to retrieve multiple price updates,
/// for example see the following query string:
///
/// ```
/// ?ids[]=a12...&ids[]=b4c...
/// ```
#[param(rename = "ids[]")]
#[param(example = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43")]
ids: Vec<PriceIdInput>,
/// If true, include the parsed price update in the `parsed` field of each returned feed. Default is `hex`.
#[serde(default)]
encoding: EncodingType,
/// If true, include the parsed price update in the `parsed` field of each returned feed. Default is `true`.
#[serde(default = "default_true")]
parsed: bool,
/// If true, allows unordered price updates to be included in the stream.
#[serde(default)]
allow_unordered: bool,
/// If true, only include benchmark prices that are the initial price updates at a given timestamp (i.e., prevPubTime != pubTime).
#[serde(default)]
benchmarks_only: bool,
}
fn default_true() -> bool {
true
}
#[utoipa::path(
get,
path = "/v2/updates/price/stream",
responses(
(status = 200, description = "Price updates retrieved successfully", body = PriceUpdate),
(status = 404, description = "Price ids not found", body = String)
),
params(StreamPriceUpdatesQueryParams)
)]
/// SSE route handler for streaming price updates.
pub async fn price_stream_sse_handler<S>(
State(state): State<ApiState<S>>,
QsQuery(params): QsQuery<StreamPriceUpdatesQueryParams>,
) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, RestError>
where
S: Aggregates,
S: Sync,
S: Send,
S: 'static,
{
let price_ids: Vec<PriceIdentifier> = params.ids.into_iter().map(Into::into).collect();
verify_price_ids_exist(&state, &price_ids).await?;
// Clone the update_tx receiver to listen for new price updates
let update_rx: broadcast::Receiver<AggregationEvent> = Aggregates::subscribe(&*state.state);
// Convert the broadcast receiver into a Stream
let stream = BroadcastStream::new(update_rx);
let sse_stream = stream.then(move |message| {
let state_clone = state.clone(); // Clone again to use inside the async block
let price_ids_clone = price_ids.clone(); // Clone again for use inside the async block
async move {
match message {
Ok(event) => {
match handle_aggregation_event(
event,
state_clone,
price_ids_clone,
params.encoding,
params.parsed,
params.benchmarks_only,
params.allow_unordered,
)
.await
{
Ok(Some(update)) => Ok(Event::default()
.json_data(update)
.unwrap_or_else(|e| error_event(e))),
Ok(None) => Ok(Event::default().comment("No update available")),
Err(e) => Ok(error_event(e)),
}
}
Err(e) => Ok(error_event(e)),
}
}
});
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
}
async fn handle_aggregation_event<S>(
event: AggregationEvent,
state: ApiState<S>,
mut price_ids: Vec<PriceIdentifier>,
encoding: EncodingType,
parsed: bool,
benchmarks_only: bool,
allow_unordered: bool,
) -> Result<Option<PriceUpdate>>
where
S: Aggregates,
{
// Handle out-of-order events
if let AggregationEvent::OutOfOrder { .. } = event {
if !allow_unordered {
return Ok(None);
}
}
// We check for available price feed ids to ensure that the price feed ids provided exists since price feeds can be removed.
let available_price_feed_ids = Aggregates::get_price_feed_ids(&*state.state).await;
price_ids.retain(|price_feed_id| available_price_feed_ids.contains(price_feed_id));
let mut price_feeds_with_update_data = Aggregates::get_price_feeds_with_update_data(
&*state.state,
&price_ids,
RequestTime::AtSlot(event.slot()),
)
.await?;
let mut parsed_price_updates: Vec<ParsedPriceUpdate> = price_feeds_with_update_data
.price_feeds
.into_iter()
.map(|price_feed| price_feed.into())
.collect();
if benchmarks_only {
// Remove those with metadata.prev_publish_time != price.publish_time from parsed_price_updates
parsed_price_updates.retain(|price_feed| {
price_feed
.metadata
.prev_publish_time
.map_or(false, |prev_time| {
prev_time != price_feed.price.publish_time
})
});
// Retain price id in price_ids that are in parsed_price_updates
price_ids.retain(|price_id| {
parsed_price_updates
.iter()
.any(|price_feed| price_feed.id == RpcPriceIdentifier::from(*price_id))
});
price_feeds_with_update_data = Aggregates::get_price_feeds_with_update_data(
&*state.state,
&price_ids,
RequestTime::AtSlot(event.slot()),
)
.await?;
}
// Check if price_ids is empty after filtering and return None if it is
if price_ids.is_empty() {
return Ok(None);
}
let price_update_data = price_feeds_with_update_data.update_data;
let encoded_data: Vec<String> = price_update_data
.into_iter()
.map(|data| encoding.encode_str(&data))
.collect();
let binary_price_update = BinaryPriceUpdate {
encoding,
data: encoded_data,
};
Ok(Some(PriceUpdate {
binary: binary_price_update,
parsed: if parsed {
Some(parsed_price_updates)
} else {
None
},
}))
}
fn error_event<E: std::fmt::Debug>(e: E) -> Event {
Event::default()
.event("error")
.data(format!("Error receiving update: {:?}", e))
}

View File

@ -1,96 +0,0 @@
use {
crate::{
api::types::{
AssetType,
PriceFeedMetadata,
},
state::State,
},
anyhow::Result,
tokio::sync::RwLock,
};
pub const DEFAULT_PRICE_FEEDS_CACHE_UPDATE_INTERVAL: u64 = 600;
pub struct PriceFeedMetaState {
pub data: RwLock<Vec<PriceFeedMetadata>>,
}
impl PriceFeedMetaState {
pub fn new() -> Self {
Self {
data: RwLock::new(Vec::new()),
}
}
}
/// Allow downcasting State into CacheState for functions that depend on the `Cache` service.
impl<'a> From<&'a State> for &'a PriceFeedMetaState {
fn from(state: &'a State) -> &'a PriceFeedMetaState {
&state.price_feed_meta
}
}
#[async_trait::async_trait]
pub trait PriceFeedMeta {
async fn retrieve_price_feeds_metadata(&self) -> Result<Vec<PriceFeedMetadata>>;
async fn store_price_feeds_metadata(
&self,
price_feeds_metadata: &[PriceFeedMetadata],
) -> Result<()>;
async fn get_price_feeds_metadata(
&self,
query: Option<String>,
asset_type: Option<AssetType>,
) -> Result<Vec<PriceFeedMetadata>>;
}
#[async_trait::async_trait]
impl<T> PriceFeedMeta for T
where
for<'a> &'a T: Into<&'a PriceFeedMetaState>,
T: Sync,
{
async fn retrieve_price_feeds_metadata(&self) -> Result<Vec<PriceFeedMetadata>> {
let price_feeds_metadata = self.into().data.read().await;
Ok(price_feeds_metadata.clone())
}
async fn store_price_feeds_metadata(
&self,
price_feeds_metadata: &[PriceFeedMetadata],
) -> Result<()> {
let mut price_feeds_metadata_write_guard = self.into().data.write().await;
*price_feeds_metadata_write_guard = price_feeds_metadata.to_vec();
Ok(())
}
async fn get_price_feeds_metadata(
&self,
query: Option<String>,
asset_type: Option<AssetType>,
) -> Result<Vec<PriceFeedMetadata>> {
let mut price_feeds_metadata = self.retrieve_price_feeds_metadata().await?;
// Filter by query if provided
if let Some(query_str) = &query {
price_feeds_metadata.retain(|feed| {
feed.attributes.get("symbol").map_or(false, |symbol| {
symbol.to_lowercase().contains(&query_str.to_lowercase())
})
});
}
// Filter by asset_type if provided
if let Some(asset_type) = &asset_type {
price_feeds_metadata.retain(|feed| {
feed.attributes.get("asset_type").map_or(false, |type_str| {
type_str.to_lowercase() == asset_type.to_string().to_lowercase()
})
});
}
Ok(price_feeds_metadata)
}
}

View File

@ -1,13 +0,0 @@
{
"endpoint": "https://api.mainnet-beta.solana.com",
"keypair-file": "./id.json",
"shard-id": 1,
"jito-endpoint": "mainnet.block-engine.jito.wtf",
"jito-keypair-file": "./jito.json",
"jito-tip-lamports": "100000",
"jito-bundle-size": "5",
"price-config-file": "./price-config.yaml",
"price-service-endpoint": "https://hermes.pyth.network/",
"pyth-contract-address": "pythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsT",
"pushing-frequency": "30"
}

View File

@ -1,9 +0,0 @@
{
"endpoint": "https://api.devnet.solana.com",
"keypair-file": "./id.json",
"shard-id": 1,
"price-config-file": "./price-config.yaml",
"price-service-endpoint": "https://hermes.pyth.network/",
"pyth-contract-address": "pythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsT",
"pushing-frequency": "30"
}

View File

@ -1,177 +0,0 @@
import { Options } from "yargs";
import * as options from "../options";
import { readPriceConfigFile } from "../price-config";
import { PriceServiceConnection } from "@pythnetwork/price-service-client";
import { PythPriceListener } from "../pyth-price-listener";
import {
SolanaPriceListener,
SolanaPricePusher,
SolanaPricePusherJito,
} from "./solana";
import { Controller } from "../controller";
import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver";
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
import { Keypair, Connection } from "@solana/web3.js";
import fs from "fs";
import { PublicKey } from "@solana/web3.js";
import {
SearcherClient,
searcherClient,
} from "jito-ts/dist/sdk/block-engine/searcher";
export default {
command: "solana",
describe: "run price pusher for solana",
builder: {
endpoint: {
description: "Solana RPC API endpoint",
type: "string",
required: true,
} as Options,
"keypair-file": {
description: "Path to a keypair file",
type: "string",
required: true,
} as Options,
"shard-id": {
description: "Shard ID",
type: "number",
required: true,
} as Options,
"compute-unit-price-micro-lamports": {
description: "Priority fee per compute unit",
type: "number",
default: 50000,
} as Options,
"jito-endpoint": {
description: "Jito endpoint",
type: "string",
optional: true,
} as Options,
"jito-keypair-file": {
description:
"Path to the jito keypair file (need for grpc authentication)",
type: "string",
optional: true,
} as Options,
"jito-tip-lamports": {
description: "Lamports to tip the jito builder",
type: "number",
optional: true,
} as Options,
"jito-bundle-size": {
description: "Number of transactions in each bundle",
type: "number",
default: 2,
} as Options,
...options.priceConfigFile,
...options.priceServiceEndpoint,
...options.pythContractAddress,
...options.pollingFrequency,
...options.pushingFrequency,
},
handler: function (argv: any) {
const {
endpoint,
keypairFile,
shardId,
computeUnitPriceMicroLamports,
priceConfigFile,
priceServiceEndpoint,
pythContractAddress,
pushingFrequency,
pollingFrequency,
jitoEndpoint,
jitoKeypairFile,
jitoTipLamports,
jitoBundleSize,
} = argv;
const priceConfigs = readPriceConfigFile(priceConfigFile);
const priceServiceConnection = new PriceServiceConnection(
priceServiceEndpoint,
{
logger: {
// Log only warnings and errors from the price service client
info: () => undefined,
warn: console.warn,
error: console.error,
debug: () => undefined,
trace: () => undefined,
},
}
);
const priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
const pythListener = new PythPriceListener(
priceServiceConnection,
priceItems
);
const wallet = new NodeWallet(
Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(fs.readFileSync(keypairFile, "ascii")))
)
);
const pythSolanaReceiver = new PythSolanaReceiver({
connection: new Connection(endpoint, "processed"),
wallet,
pushOracleProgramId: new PublicKey(pythContractAddress),
});
let solanaPricePusher;
if (jitoTipLamports) {
const jitoKeypair = Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(fs.readFileSync(jitoKeypairFile, "ascii")))
);
const jitoClient = searcherClient(jitoEndpoint, jitoKeypair);
solanaPricePusher = new SolanaPricePusherJito(
pythSolanaReceiver,
priceServiceConnection,
shardId,
jitoTipLamports,
jitoClient,
jitoBundleSize
);
onBundleResult(jitoClient);
} else {
solanaPricePusher = new SolanaPricePusher(
pythSolanaReceiver,
priceServiceConnection,
shardId,
computeUnitPriceMicroLamports
);
}
const solanaPriceListener = new SolanaPriceListener(
pythSolanaReceiver,
shardId,
priceItems,
{ pollingFrequency }
);
const controller = new Controller(
priceConfigs,
pythListener,
solanaPriceListener,
solanaPricePusher,
{ pushingFrequency }
);
controller.start();
},
};
export const onBundleResult = (c: SearcherClient) => {
c.onBundleResult(
() => undefined,
(e) => {
console.log("Error in bundle result: ", e);
}
);
};

View File

@ -1,180 +0,0 @@
import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver";
import {
ChainPriceListener,
IPricePusher,
PriceInfo,
PriceItem,
} from "../interface";
import { DurationInSeconds } from "../utils";
import { PriceServiceConnection } from "@pythnetwork/price-service-client";
import {
sendTransactions,
sendTransactionsJito,
} from "@pythnetwork/solana-utils";
import { SearcherClient } from "jito-ts/dist/sdk/block-engine/searcher";
export class SolanaPriceListener extends ChainPriceListener {
constructor(
private pythSolanaReceiver: PythSolanaReceiver,
private shardId: number,
priceItems: PriceItem[],
config: {
pollingFrequency: DurationInSeconds;
}
) {
super("solana", config.pollingFrequency, priceItems);
}
async getOnChainPriceInfo(priceId: string): Promise<PriceInfo | undefined> {
try {
const priceFeedAccount =
await this.pythSolanaReceiver.fetchPriceFeedAccount(
this.shardId,
Buffer.from(priceId, "hex")
);
console.log(
`Polled a Solana on chain price for feed ${this.priceIdToAlias.get(
priceId
)} (${priceId}).`
);
if (priceFeedAccount) {
return {
conf: priceFeedAccount.priceMessage.conf.toString(),
price: priceFeedAccount.priceMessage.price.toString(),
publishTime: priceFeedAccount.priceMessage.publishTime.toNumber(),
};
} else {
return undefined;
}
} catch (e) {
console.error(`Polling on-chain price for ${priceId} failed. Error:`);
console.error(e);
return undefined;
}
}
}
export class SolanaPricePusher implements IPricePusher {
constructor(
private pythSolanaReceiver: PythSolanaReceiver,
private priceServiceConnection: PriceServiceConnection,
private shardId: number,
private computeUnitPriceMicroLamports: number
) {}
async updatePriceFeed(
priceIds: string[],
pubTimesToPush: number[]
): Promise<void> {
if (priceIds.length === 0) {
return;
}
let priceFeedUpdateData;
try {
priceFeedUpdateData = await this.priceServiceConnection.getLatestVaas(
priceIds
);
} catch (e: any) {
console.error(new Date(), "getPriceFeedsUpdateData failed:", e);
return;
}
const transactionBuilder = this.pythSolanaReceiver.newTransactionBuilder({
closeUpdateAccounts: true,
});
await transactionBuilder.addUpdatePriceFeed(
priceFeedUpdateData,
this.shardId
);
const transactions = await transactionBuilder.buildVersionedTransactions({
computeUnitPriceMicroLamports: this.computeUnitPriceMicroLamports,
tightComputeBudget: true,
});
try {
await sendTransactions(
transactions,
this.pythSolanaReceiver.connection,
this.pythSolanaReceiver.wallet
);
console.log(new Date(), "updatePriceFeed successful");
} catch (e: any) {
console.error(new Date(), "updatePriceFeed failed", e);
return;
}
}
}
export class SolanaPricePusherJito implements IPricePusher {
constructor(
private pythSolanaReceiver: PythSolanaReceiver,
private priceServiceConnection: PriceServiceConnection,
private shardId: number,
private jitoTipLamports: number,
private searcherClient: SearcherClient,
private jitoBundleSize: number
) {}
async updatePriceFeed(
priceIds: string[],
pubTimesToPush: number[]
): Promise<void> {
let priceFeedUpdateData;
try {
priceFeedUpdateData = await this.priceServiceConnection.getLatestVaas(
priceIds
);
} catch (e: any) {
console.error(new Date(), "getPriceFeedsUpdateData failed:", e);
return;
}
const transactionBuilder = this.pythSolanaReceiver.newTransactionBuilder({
closeUpdateAccounts: false,
});
await transactionBuilder.addUpdatePriceFeed(
priceFeedUpdateData,
this.shardId
);
await transactionBuilder.addClosePreviousEncodedVaasInstructions();
const transactions = await transactionBuilder.buildVersionedTransactions({
jitoTipLamports: this.jitoTipLamports,
tightComputeBudget: true,
jitoBundleSize: this.jitoBundleSize,
});
const firstSignature = await sendTransactionsJito(
transactions.slice(0, this.jitoBundleSize),
this.searcherClient,
this.pythSolanaReceiver.wallet
);
const blockhashResult =
await this.pythSolanaReceiver.connection.getLatestBlockhashAndContext({
commitment: "confirmed",
});
await this.pythSolanaReceiver.connection.confirmTransaction(
{
signature: firstSignature,
blockhash: blockhashResult.value.blockhash,
lastValidBlockHeight: blockhashResult.value.lastValidBlockHeight,
},
"confirmed"
);
for (
let i = this.jitoBundleSize;
i < transactions.length;
i += this.jitoBundleSize
) {
await sendTransactionsJito(
transactions.slice(i, i + this.jitoBundleSize),
this.searcherClient,
this.pythSolanaReceiver.wallet
);
}
}
}

View File

@ -1,5 +1,5 @@
{
"name": "@pythnetwork/contract-manager",
"name": "contract_manager",
"version": "1.0.0",
"description": "Set of tools to manage pyth contracts",
"private": true,
@ -21,9 +21,9 @@
"url": "git+https://github.com/pyth-network/pyth-crosschain.git"
},
"dependencies": {
"@certusone/wormhole-sdk": "^0.9.8",
"@coral-xyz/anchor": "^0.29.0",
"@injectivelabs/networks": "^1.14.6",
"@certusone/wormhole-sdk": "^0.9.8",
"@injectivelabs/networks": "1.0.68",
"@mysten/sui.js": "^0.49.1",
"@pythnetwork/cosmwasm-deploy-tools": "*",
"@pythnetwork/entropy-sdk-solidity": "*",
@ -31,7 +31,6 @@
"@pythnetwork/pyth-sui-js": "*",
"@types/yargs": "^17.0.32",
"aptos": "^1.5.0",
"axios": "^0.24.0",
"bs58": "^5.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.3.3"

View File

@ -5,7 +5,6 @@ import { createHash } from "crypto";
import { DefaultStore } from "../src/store";
import {
CosmosUpgradeContract,
EvmExecute,
EvmSetWormholeAddress,
EvmUpgradeContract,
getProposalInstructions,
@ -20,10 +19,8 @@ import {
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
import { AccountMeta, Keypair, PublicKey } from "@solana/web3.js";
import {
EvmEntropyContract,
EvmPriceFeedContract,
getCodeDigestWithoutAddress,
EvmWormholeContract,
WormholeEvmContract,
} from "../src/contracts/evm";
import Web3 from "web3";
@ -73,7 +70,7 @@ async function main() {
instruction.governanceAction.targetChainId
) {
const address = instruction.governanceAction.address;
const contract = new EvmWormholeContract(chain, address);
const contract = new WormholeEvmContract(chain, address);
const currentIndex = await contract.getCurrentGuardianSetIndex();
const guardianSet = await contract.getGuardianSet();
@ -137,70 +134,6 @@ async function main() {
}
}
}
if (instruction.governanceAction instanceof EvmExecute) {
// Note: it only checks for upgrade entropy contracts right now
console.log(
`Verifying EVMExecute Contract on ${instruction.governanceAction.targetChainId}`
);
for (const chain of Object.values(DefaultStore.chains)) {
if (
chain instanceof EvmChain &&
chain.wormholeChainName ===
instruction.governanceAction.targetChainId
) {
const executorAddress =
instruction.governanceAction.executorAddress;
const callAddress = instruction.governanceAction.callAddress;
const calldata = instruction.governanceAction.calldata;
// currently executor is only being used by the entropy contract
const contract = new EvmEntropyContract(chain, callAddress);
const owner = await contract.getOwner();
if (
executorAddress.toUpperCase() !==
owner.replace("0x", "").toUpperCase()
) {
console.log(
`Executor Address: ${executorAddress.toUpperCase()} is not equal to Owner Address: ${owner
.replace("0x", "")
.toUpperCase()}`
);
continue;
}
const calldataHex = calldata.toString("hex");
const web3 = new Web3();
const methodSignature = web3.eth.abi
.encodeFunctionSignature("upgradeTo(address)")
.replace("0x", "");
let newImplementationAddress: string | undefined = undefined;
if (calldataHex.startsWith(methodSignature)) {
newImplementationAddress = web3.eth.abi.decodeParameter(
"address",
calldataHex.replace(methodSignature, "")
) as unknown as string;
}
if (newImplementationAddress === undefined) {
console.log(
`We couldn't parse the instruction for ${chain.getId()}`
);
continue;
}
const newImplementationCode = await getCodeDigestWithoutAddress(
chain.getRpcUrl(),
newImplementationAddress
);
// this should be the same keccak256 of the deployedCode property generated by truffle
console.log(
`${chain.getId()} new implementation address:${newImplementationAddress} digest:${newImplementationCode}`
);
}
}
}
}
}
}

View File

@ -1,18 +1,11 @@
import {
DefaultStore,
EvmChain,
EvmEntropyContract,
EvmWormholeContract,
getDefaultDeploymentConfig,
PrivateKey,
} from "../src";
import { DefaultStore, EvmChain, PrivateKey } from "../src";
import { existsSync, readFileSync, writeFileSync } from "fs";
import { join } from "path";
import Web3 from "web3";
import { Contract } from "web3-eth-contract";
import { InferredOptionType } from "yargs";
export interface BaseDeployConfig {
interface DeployConfig {
gasMultiplier: number;
gasPriceMultiplier: number;
jsonOutputDir: string;
@ -26,7 +19,7 @@ export interface BaseDeployConfig {
export async function deployIfNotCached(
cacheFile: string,
chain: EvmChain,
config: BaseDeployConfig,
config: DeployConfig,
artifactName: string,
deployArgs: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
cacheKey?: string
@ -74,12 +67,12 @@ export const COMMON_DEPLOY_OPTIONS = {
"private-key": {
type: "string",
demandOption: true,
desc: "Private key to sign the transactions with",
desc: "Private key to sign the trnasactions with",
},
chain: {
type: "array",
demandOption: true,
desc: "Chain to upload the contract on. Can be one of the chains available in the store",
desc: "Chain to upload the contract on. Can be one of the evm chains available in the store",
},
"deployment-type": {
type: "string",
@ -188,149 +181,3 @@ export function getSelectedChains(argv: {
}
return selectedChains;
}
/**
* Finds the entropy contract for a given EVM chain.
* @param {EvmChain} chain The EVM chain to find the entropy contract for.
* @returns The entropy contract for the given EVM chain.
* @throws {Error} an error if the entropy contract is not found for the given EVM chain.
*/
export function findEntropyContract(chain: EvmChain): EvmEntropyContract {
for (const contract of Object.values(DefaultStore.entropy_contracts)) {
if (contract.getChain().getId() === chain.getId()) {
return contract;
}
}
throw new Error(`Entropy contract not found for chain ${chain.getId()}`);
}
/**
* Finds an EVM chain by its name.
* @param {string} chainName The name of the chain to find.
* @returns The EVM chain instance.
* @throws {Error} an error if the chain is not found or is not an EVM chain.
*/
export function findEvmChain(chainName: string): EvmChain {
const chain = DefaultStore.chains[chainName];
if (!chain) {
throw new Error(`Chain ${chainName} not found`);
} else if (!(chain instanceof EvmChain)) {
throw new Error(`Chain ${chainName} is not an EVM chain`);
}
return chain;
}
/**
* Finds the wormhole contract for a given EVM chain.
* @param {EvmChain} chain The EVM chain to find the wormhole contract for.
* @returns If found, the wormhole contract for the given EVM chain. Else, undefined
*/
export function findWormholeContract(
chain: EvmChain
): EvmWormholeContract | undefined {
for (const contract of Object.values(DefaultStore.wormhole_contracts)) {
if (
contract instanceof EvmWormholeContract &&
contract.getChain().getId() === chain.getId()
) {
return contract;
}
}
}
export interface DeployWormholeReceiverContractsConfig
extends BaseDeployConfig {
saveContract: boolean;
type: "stable" | "beta";
}
/**
* Deploys the wormhole receiver contract for a given EVM chain.
* @param {EvmChain} chain The EVM chain to find the wormhole receiver contract for.
* @param {DeployWormholeReceiverContractsConfig} config The deployment configuration.
* @param {string} cacheFile The path to the cache file.
* @returns {EvmWormholeContract} The wormhole contract for the given EVM chain.
*/
export async function deployWormholeContract(
chain: EvmChain,
config: DeployWormholeReceiverContractsConfig,
cacheFile: string
): Promise<EvmWormholeContract> {
const receiverSetupAddr = await deployIfNotCached(
cacheFile,
chain,
config,
"ReceiverSetup",
[]
);
const receiverImplAddr = await deployIfNotCached(
cacheFile,
chain,
config,
"ReceiverImplementation",
[]
);
// Craft the init data for the proxy contract
const setupContract = getWeb3Contract(
config.jsonOutputDir,
"ReceiverSetup",
receiverSetupAddr
);
const { wormholeConfig } = getDefaultDeploymentConfig(config.type);
const initData = setupContract.methods
.setup(
receiverImplAddr,
wormholeConfig.initialGuardianSet.map((addr: string) => "0x" + addr),
chain.getWormholeChainId(),
wormholeConfig.governanceChainId,
"0x" + wormholeConfig.governanceContract
)
.encodeABI();
const wormholeReceiverAddr = await deployIfNotCached(
cacheFile,
chain,
config,
"WormholeReceiver",
[receiverSetupAddr, initData]
);
const wormholeContract = new EvmWormholeContract(chain, wormholeReceiverAddr);
if (config.type === "stable") {
console.log(`Syncing mainnet guardian sets for ${chain.getId()}...`);
// TODO: Add a way to pass gas configs to this
await wormholeContract.syncMainnetGuardianSets(config.privateKey);
console.log(`✅ Synced mainnet guardian sets for ${chain.getId()}`);
}
if (config.saveContract) {
DefaultStore.wormhole_contracts[wormholeContract.getId()] =
wormholeContract;
DefaultStore.saveAllContracts();
}
return wormholeContract;
}
/**
* Returns the wormhole contract for a given EVM chain.
* If there was no wormhole contract deployed for the given chain, it will deploy the wormhole contract and save it to the default store.
* @param {EvmChain} chain The EVM chain to find the wormhole contract for.
* @param {DeployWormholeReceiverContractsConfig} config The deployment configuration.
* @param {string} cacheFile The path to the cache file.
* @returns {EvmWormholeContract} The wormhole contract for the given EVM chain.
*/
export async function getOrDeployWormholeContract(
chain: EvmChain,
config: DeployWormholeReceiverContractsConfig,
cacheFile: string
): Promise<EvmWormholeContract> {
return (
findWormholeContract(chain) ??
(await deployWormholeContract(chain, config, cacheFile))
);
}

View File

@ -5,23 +5,29 @@ import { DefaultStore } from "../src/store";
import {
DeploymentType,
EvmEntropyContract,
EvmPriceFeedContract,
getDefaultDeploymentConfig,
PrivateKey,
toDeploymentType,
toPrivateKey,
WormholeEvmContract,
} from "../src";
import {
COMMON_DEPLOY_OPTIONS,
deployIfNotCached,
getWeb3Contract,
getOrDeployWormholeContract,
BaseDeployConfig,
} from "./common";
import Web3 from "web3";
interface DeploymentConfig extends BaseDeployConfig {
type DeploymentConfig = {
type: DeploymentType;
gasMultiplier: number;
gasPriceMultiplier: number;
privateKey: PrivateKey;
jsonOutputDir: string;
wormholeAddr: string;
saveContract: boolean;
}
};
const CACHE_FILE = ".cache-deploy-evm-entropy-contracts";
const ENTROPY_DEFAULT_PROVIDER = {
@ -45,8 +51,7 @@ const parser = yargs(hideBin(process.argv))
async function deployExecutorContracts(
chain: EvmChain,
config: DeploymentConfig,
wormholeAddr: string
config: DeploymentConfig
): Promise<string> {
const executorImplAddr = await deployIfNotCached(
CACHE_FILE,
@ -67,7 +72,7 @@ async function deployExecutorContracts(
const executorInitData = executorImplContract.methods
.initialize(
wormholeAddr,
config.wormholeAddr,
0, // lastExecutedSequence,
chain.getWormholeChainId(),
governanceDataSource.emitterChain,
@ -156,6 +161,19 @@ async function topupProviderIfNecessary(
}
}
async function findWormholeAddress(
chain: EvmChain
): Promise<string | undefined> {
for (const contract of Object.values(DefaultStore.contracts)) {
if (
contract instanceof EvmPriceFeedContract &&
contract.getChain().getId() === chain.getId()
) {
return (await contract.getWormholeContract()).address;
}
}
}
async function main() {
const argv = await parser.argv;
@ -167,6 +185,12 @@ async function main() {
throw new Error(`Chain ${chainName} is not an EVM chain`);
}
const wormholeAddr = await findWormholeAddress(chain);
if (!wormholeAddr) {
// TODO: deploy wormhole if necessary and maintain a wormhole store
throw new Error(`Wormhole contract not found for chain ${chain.getId()}`);
}
const deploymentConfig: DeploymentConfig = {
type: toDeploymentType(argv.deploymentType),
gasMultiplier: argv.gasMultiplier,
@ -174,14 +198,18 @@ async function main() {
privateKey: toPrivateKey(argv.privateKey),
jsonOutputDir: argv.stdOutputDir,
saveContract: argv.saveContract,
wormholeAddr,
};
const wormholeContract = await getOrDeployWormholeContract(
const wormholeContract = new WormholeEvmContract(
chain,
deploymentConfig,
CACHE_FILE
deploymentConfig.wormholeAddr
);
const wormholeChainId = await wormholeContract.getChainId();
if (chain.getWormholeChainId() != wormholeChainId) {
throw new Error(
`Wormhole chain id mismatch. Expected ${chain.getWormholeChainId()} but got ${wormholeChainId}`
);
}
await topupProviderIfNecessary(chain, deploymentConfig);
console.log(
@ -190,11 +218,7 @@ async function main() {
console.log(`Deploying entropy contracts on ${chain.getId()}...`);
const executorAddr = await deployExecutorContracts(
chain,
deploymentConfig,
wormholeContract.address
);
const executorAddr = await deployExecutorContracts(chain, deploymentConfig);
const entropyAddr = await deployEntropyContracts(
chain,
deploymentConfig,

View File

@ -6,23 +6,27 @@ import {
DeploymentType,
EvmPriceFeedContract,
getDefaultDeploymentConfig,
PrivateKey,
toDeploymentType,
toPrivateKey,
WormholeEvmContract,
} from "../src";
import {
COMMON_DEPLOY_OPTIONS,
deployIfNotCached,
getWeb3Contract,
getOrDeployWormholeContract,
BaseDeployConfig,
} from "./common";
interface DeploymentConfig extends BaseDeployConfig {
type DeploymentConfig = {
type: DeploymentType;
validTimePeriodSeconds: number;
singleUpdateFeeInWei: number;
gasMultiplier: number;
gasPriceMultiplier: number;
privateKey: PrivateKey;
jsonOutputDir: string;
saveContract: boolean;
}
};
const CACHE_FILE = ".cache-deploy-evm";
@ -47,6 +51,68 @@ const parser = yargs(hideBin(process.argv))
},
});
async function deployWormholeReceiverContracts(
chain: EvmChain,
config: DeploymentConfig
): Promise<string> {
const receiverSetupAddr = await deployIfNotCached(
CACHE_FILE,
chain,
config,
"ReceiverSetup",
[]
);
const receiverImplAddr = await deployIfNotCached(
CACHE_FILE,
chain,
config,
"ReceiverImplementation",
[]
);
// Craft the init data for the proxy contract
const setupContract = getWeb3Contract(
config.jsonOutputDir,
"ReceiverSetup",
receiverSetupAddr
);
const { wormholeConfig } = getDefaultDeploymentConfig(config.type);
const initData = setupContract.methods
.setup(
receiverImplAddr,
wormholeConfig.initialGuardianSet.map((addr: string) => "0x" + addr),
chain.getWormholeChainId(),
wormholeConfig.governanceChainId,
"0x" + wormholeConfig.governanceContract
)
.encodeABI();
const wormholeReceiverAddr = await deployIfNotCached(
CACHE_FILE,
chain,
config,
"WormholeReceiver",
[receiverSetupAddr, initData]
);
const wormholeEvmContract = new WormholeEvmContract(
chain,
wormholeReceiverAddr
);
if (config.type === "stable") {
console.log(`Syncing mainnet guardian sets for ${chain.getId()}...`);
// TODO: Add a way to pass gas configs to this
await wormholeEvmContract.syncMainnetGuardianSets(config.privateKey);
console.log(`✅ Synced mainnet guardian sets for ${chain.getId()}`);
}
return wormholeReceiverAddr;
}
async function deployPriceFeedContracts(
chain: EvmChain,
config: DeploymentConfig,
@ -120,16 +186,14 @@ async function main() {
console.log(`Deploying price feed contracts on ${chain.getId()}...`);
const wormholeContract = await getOrDeployWormholeContract(
const wormholeAddr = await deployWormholeReceiverContracts(
chain,
deploymentConfig,
CACHE_FILE
deploymentConfig
);
const priceFeedAddr = await deployPriceFeedContracts(
chain,
deploymentConfig,
wormholeContract.address
wormholeAddr
);
if (deploymentConfig.saveContract) {

View File

@ -19,18 +19,6 @@ const parser = yargs(hideBin(process.argv))
async function main() {
const argv = await parser.argv;
const prices: Record<string, number> = {};
for (const token of Object.values(DefaultStore.tokens)) {
const price = await token.getPriceForMinUnit();
// We're going to ignore the value of tokens that aren't configured
// in the store -- these are likely not worth much anyway.
if (price !== undefined) {
prices[token.id] = price;
}
}
let totalFeeUsd = 0;
for (const contract of Object.values(DefaultStore.contracts)) {
if (contract.getChain().isMainnet() === argv.testnet) continue;
if (
@ -39,26 +27,12 @@ async function main() {
contract instanceof CosmWasmPriceFeedContract
) {
try {
const fee = await contract.getTotalFee();
let feeUsd = 0;
if (fee.denom !== undefined && prices[fee.denom] !== undefined) {
feeUsd = Number(fee.amount) * prices[fee.denom];
totalFeeUsd += feeUsd;
console.log(
`${contract.getId()} ${fee.amount} ${fee.denom} ($${feeUsd})`
);
} else {
console.log(
`${contract.getId()} ${fee.amount} ${fee.denom} ($ value unknown)`
);
}
console.log(`${contract.getId()} ${await contract.getTotalFee()}`);
} catch (e) {
console.error(`Error fetching fees for ${contract.getId()}`, e);
}
}
}
console.log(`Total fees in USD: $${totalFeeUsd}`);
}
main();

View File

@ -1,64 +0,0 @@
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { DefaultStore } from "../src";
function deserializeCommitmentMetadata(data: Buffer) {
const seed = Uint8Array.from(data.subarray(0, 32));
const chainLength = data.readBigInt64LE(32);
return {
seed,
chainLength,
};
}
const parser = yargs(hideBin(process.argv))
.usage("Usage: $0")
.options({
testnet: {
type: "boolean",
default: false,
desc: "Fetch the provider registration data for the testnet contracts.",
},
});
async function main() {
const argv = await parser.argv;
for (const contract of Object.values(DefaultStore.entropy_contracts)) {
if (contract.getChain().isMainnet() === argv.testnet) continue;
let provider;
let providerInfo;
try {
provider = await contract.getDefaultProvider();
providerInfo = await contract.getProviderInfo(provider);
} catch (e) {
console.error(`Error fetching info for ${contract.getId()}`, e);
continue;
}
const commitmentMetadata = providerInfo.commitmentMetadata.replace(
"0x",
""
);
// const binaryData = hexToBytes(commitmentMetadata);
const metadata = deserializeCommitmentMetadata(
Buffer.from(commitmentMetadata, "hex")
);
console.log("=".repeat(100));
console.log(`Fetched info for ${contract.getId()}`);
console.log(`chain : ${contract.getChain().getId()}`);
console.log(`contract : ${contract.address}`);
console.log(`provider : ${provider}`);
console.log(`commitment data : ${commitmentMetadata}`);
console.log(`chainLength : ${metadata.chainLength}`);
console.log(`seed : [${metadata.seed}]`);
console.log(
`original seq no : ${providerInfo.originalCommitmentSequenceNumber}`
);
}
}
main();

View File

@ -1,66 +0,0 @@
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { toPrivateKey } from "../src";
import {
COMMON_DEPLOY_OPTIONS,
findEntropyContract,
findEvmChain,
} from "./common";
const parser = yargs(hideBin(process.argv))
.usage(
"Requests and reveals a random number from an entropy contract while measuing the\n" +
"latency between request submission and availablity of the provider revelation from fortuna.\n" +
"Usage: $0 --chain <chain-id> --private-key <private-key>"
)
.options({
chain: {
type: "string",
demandOption: true,
desc: "test latency for the contract on this chain",
},
"private-key": COMMON_DEPLOY_OPTIONS["private-key"],
});
async function main() {
const argv = await parser.argv;
const chain = findEvmChain(argv.chain);
const contract = findEntropyContract(chain);
const provider = await contract.getDefaultProvider();
const providerInfo = await contract.getProviderInfo(provider);
const userRandomNumber = contract.generateUserRandomNumber();
const privateKey = toPrivateKey(argv.privateKey);
const requestResponse = await contract.requestRandomness(
userRandomNumber,
provider,
privateKey
);
console.log("Request tx hash: ", requestResponse.transactionHash);
const startTime = Date.now();
const sequenceNumber = providerInfo.sequenceNumber;
const revealUrl = providerInfo.uri + `/revelations/${sequenceNumber}`;
console.log("Checking this url for revelation:", revealUrl);
// eslint-disable-next-line no-constant-condition
while (true) {
const fortunaResponse = await fetch(revealUrl);
if (fortunaResponse.status === 200) {
const payload = await fortunaResponse.json();
const endTime = Date.now();
console.log(`Fortuna Latency: ${endTime - startTime}ms`);
const providerRevelation = "0x" + payload.value.data;
const revealResponse = await contract.revealRandomness(
userRandomNumber,
providerRevelation,
provider,
sequenceNumber,
privateKey
);
console.log("Reveal tx hash: ", revealResponse.transactionHash);
break;
}
await new Promise((resolve) => setTimeout(resolve, 300));
}
}
main();

View File

@ -1,118 +0,0 @@
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import {
DefaultStore,
EvmEntropyContract,
PrivateKey,
toPrivateKey,
} from "../src";
import {
COMMON_DEPLOY_OPTIONS,
findEntropyContract,
findEvmChain,
} from "./common";
import Web3 from "web3";
const parser = yargs(hideBin(process.argv))
.usage(
"Requests a random number from an entropy contract and measures the\n" +
"latency between request submission and fulfillment by the Fortuna keeper service.\n" +
"Usage: $0 --private-key <private-key> --chain <chain-id> | --all-chains <testnet|mainnet>"
)
.options({
chain: {
type: "string",
desc: "test latency for the contract on this chain",
conflicts: "all-chains",
},
"all-chains": {
type: "string",
conflicts: "chain",
choices: ["testnet", "mainnet"],
desc: "test latency for all entropy contracts deployed either on mainnet or testnet",
},
"private-key": COMMON_DEPLOY_OPTIONS["private-key"],
});
async function testLatency(
contract: EvmEntropyContract,
privateKey: PrivateKey
) {
const provider = await contract.getDefaultProvider();
const userRandomNumber = contract.generateUserRandomNumber();
const requestResponse = await contract.requestRandomness(
userRandomNumber,
provider,
privateKey,
true // with callback
);
console.log(`Request tx hash : ${requestResponse.transactionHash}`);
// Read the sequence number for the request from the transaction events.
const sequenceNumber =
requestResponse.events.RequestedWithCallback.returnValues.sequenceNumber;
console.log(`sequence : ${sequenceNumber}`);
const startTime = Date.now();
let fromBlock = requestResponse.blockNumber;
const web3 = new Web3(contract.chain.getRpcUrl());
const entropyContract = contract.getContract();
// eslint-disable-next-line no-constant-condition
while (true) {
const currentBlock = await web3.eth.getBlockNumber();
if (fromBlock > currentBlock) {
continue;
}
const events = await entropyContract.getPastEvents("RevealedWithCallback", {
fromBlock: fromBlock,
toBlock: currentBlock,
});
fromBlock = currentBlock + 1;
const event = events.find(
(event) => event.returnValues.request[1] == sequenceNumber
);
if (event !== undefined) {
console.log(`Random number : ${event.returnValues.randomNumber}`);
const endTime = Date.now();
console.log(`Fortuna Latency : ${endTime - startTime}ms`);
console.log(
`Revealed after : ${
currentBlock - requestResponse.blockNumber
} blocks`
);
break;
}
await new Promise((resolve) => setTimeout(resolve, 300));
}
}
async function main() {
const argv = await parser.argv;
if (!argv.chain && !argv["all-chains"]) {
throw new Error("Must specify either --chain or --all-chains");
}
const privateKey = toPrivateKey(argv.privateKey);
if (argv["all-chains"]) {
for (const contract of Object.values(DefaultStore.entropy_contracts)) {
if (
contract.getChain().isMainnet() ===
(argv["all-chains"] === "mainnet")
) {
console.log(`Testing latency for ${contract.getId()}...`);
await testLatency(contract, privateKey);
}
}
} else if (argv.chain) {
const chain = findEvmChain(argv.chain);
const contract = findEntropyContract(chain);
await testLatency(contract, privateKey);
}
}
main();

View File

@ -13,22 +13,15 @@ const parser = yargs(hideBin(process.argv))
},
});
const KEEPER_ADDRESS = {
mainnet: "0xBcAb779fCa45290288C35F5E231c37F9fA87b130",
testnet: "0xa5A68ed167431Afe739846A22597786ba2da85df",
};
async function main() {
const argv = await parser.argv;
const entries = [];
const keeperAddress = KEEPER_ADDRESS[argv.testnet ? "testnet" : "mainnet"];
for (const contract of Object.values(DefaultStore.entropy_contracts)) {
if (contract.getChain().isMainnet() === argv.testnet) continue;
try {
const provider = await contract.getDefaultProvider();
const w3 = new Web3(contract.getChain().getRpcUrl());
const balance = await w3.eth.getBalance(provider);
const keeperBalance = await w3.eth.getBalance(keeperAddress);
let version = "unknown";
try {
version = await contract.getVersion();
@ -41,7 +34,6 @@ async function main() {
contract: contract.address,
provider: providerInfo.uri,
balance,
keeperBalance,
seq: providerInfo.sequenceNumber,
version,
});

View File

@ -1,69 +0,0 @@
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import {
CosmWasmPriceFeedContract,
DefaultStore,
EvmPriceFeedContract,
toPrivateKey,
} from "../src";
const parser = yargs(hideBin(process.argv))
.usage("Update the guardian set in stable networks. Usage: $0")
.options({
"private-key": {
type: "string",
demandOption: true,
desc: "Private key to sign the transactions with",
},
chain: {
type: "array",
desc: "Can be one of the chains available in the store",
},
});
async function main() {
const argv = await parser.argv;
const privateKey = toPrivateKey(argv.privateKey);
const chains = argv.chain;
for (const contract of Object.values(DefaultStore.contracts)) {
// We are currently only managing wormhole receiver contracts in EVM and
// CosmWasm and Solana-based networks. The rest of the networks are
// managed by the guardians themselves and they should be the ones updating
// the guardian set.
// TODO: Solana-based receivers have their script in their rust cli. Add
// support for Solana-based networks here once they are added to the
// contract manager.
if (
contract instanceof CosmWasmPriceFeedContract ||
contract instanceof EvmPriceFeedContract
) {
if (chains && !chains.includes(contract.getChain().getId())) {
continue;
}
try {
console.log("------------------------------------");
const wormhole = await contract.getWormholeContract();
// TODO: This is a temporary workaround to skip contracts that are in beta channel
// We should have a better way to handle this
if ((await wormhole.getCurrentGuardianSetIndex()) === 0) {
continue;
}
console.log(
`Current Guardianset for ${contract.getId()}: ${await wormhole.getCurrentGuardianSetIndex()}`
);
await wormhole.syncMainnetGuardianSets(privateKey);
console.log(`Updated Guardianset for ${contract.getId()}`);
} catch (e) {
console.error(`Error updating Guardianset for ${contract.getId()}`, e);
}
}
}
}
main();

View File

@ -9,33 +9,19 @@ import {
makeCacheFunction,
} from "./common";
const EXECUTOR_CACHE_FILE = ".cache-upgrade-evm-executor-contract";
const ENTROPY_CACHE_FILE = ".cache-upgrade-evm-entropy-contract";
const CACHE_FILE = ".cache-upgrade-evm-executor-contract";
const runIfNotCached = makeCacheFunction(CACHE_FILE);
const parser = yargs(hideBin(process.argv))
.usage(
"Deploys a new Upgradeable contract for Executor or Entropy to a set of chains where Entropy is deployed and creates a governance proposal for it.\n" +
`Uses a cache file to avoid deploying contracts twice\n` +
"Deploys a new ExecutorUpgradeable contract to a set of chains where Entropy is deployed and creates a governance proposal for it.\n" +
`Uses a cache file (${CACHE_FILE}) to avoid deploying contracts twice\n` +
"Usage: $0 --chain <chain_1> --chain <chain_2> --private-key <private_key> --ops-key-path <ops_key_path> --std-output <std_output>"
)
.options({
...COMMON_UPGRADE_OPTIONS,
"contract-type": {
type: "string",
choices: ["executor", "entropy"],
demandOption: true,
},
});
.options(COMMON_UPGRADE_OPTIONS);
async function main() {
const argv = await parser.argv;
const cacheFile =
argv["contract-type"] === "executor"
? EXECUTOR_CACHE_FILE
: ENTROPY_CACHE_FILE;
const runIfNotCached = makeCacheFunction(cacheFile);
const selectedChains = getSelectedChains(argv);
const vault =
@ -43,7 +29,7 @@ async function main() {
"mainnet-beta_FVQyHcooAtThJ83XFrNnv74BcinbRH3bRmfFamAHBfuj"
];
console.log("Using cache file", cacheFile);
console.log("Using cache file", CACHE_FILE);
const payloads: Buffer[] = [];
for (const contract of Object.values(DefaultStore.entropy_contracts)) {
@ -65,11 +51,9 @@ async function main() {
console.log(
`Deployed contract at ${address} on ${contract.chain.getId()}`
);
const payload =
argv["contract-type"] === "executor"
? await contract.generateUpgradeExecutorContractsPayload(address)
: await contract.generateUpgradeEntropyContractPayload(address);
const payload = await contract.generateUpgradeExecutorContractsPayload(
address
);
console.log(payload.toString("hex"));
payloads.push(payload);
}

View File

@ -22,13 +22,10 @@ import {
import { Network } from "@injectivelabs/networks";
import { SuiClient } from "@mysten/sui.js/client";
import { Ed25519Keypair } from "@mysten/sui.js/keypairs/ed25519";
import { TransactionObject } from "web3/eth/types";
import { TokenId } from "./token";
export type ChainConfig = Record<string, string> & {
mainnet: boolean;
id: string;
nativeToken: TokenId;
};
export abstract class Chain extends Storable {
public wormholeChainName: ChainName;
@ -39,14 +36,12 @@ export abstract class Chain extends Storable {
* @param mainnet whether this chain is mainnet or testnet/devnet
* @param wormholeChainName the name of the wormhole chain that this chain is associated with.
* Note that pyth has included additional chain names and ids to the wormhole spec.
* @param nativeToken the id of the token used to pay gas on this chain
* @protected
*/
protected constructor(
protected id: string,
protected mainnet: boolean,
wormholeChainName: string,
protected nativeToken: TokenId | undefined
wormholeChainName: string
) {
super();
this.wormholeChainName = wormholeChainName as ChainName;
@ -69,10 +64,6 @@ export abstract class Chain extends Storable {
return this.mainnet;
}
public getNativeToken(): TokenId | undefined {
return this.nativeToken;
}
/**
* Returns the payload for a governance SetFee instruction for contracts deployed on this chain
* @param fee the new fee to set
@ -133,7 +124,7 @@ export abstract class Chain extends Storable {
export class GlobalChain extends Chain {
static type = "GlobalChain";
constructor() {
super("global", true, "unset", undefined);
super("global", true, "unset");
}
generateGovernanceUpgradePayload(): Buffer {
@ -171,13 +162,12 @@ export class CosmWasmChain extends Chain {
id: string,
mainnet: boolean,
wormholeChainName: string,
nativeToken: TokenId | undefined,
public endpoint: string,
public gasPrice: string,
public prefix: string,
public feeDenom: string
) {
super(id, mainnet, wormholeChainName, nativeToken);
super(id, mainnet, wormholeChainName);
}
static fromJson(parsed: ChainConfig): CosmWasmChain {
@ -186,7 +176,6 @@ export class CosmWasmChain extends Chain {
parsed.id,
parsed.mainnet,
parsed.wormholeChainName,
parsed.nativeToken,
parsed.endpoint,
parsed.gasPrice,
parsed.prefix,
@ -258,10 +247,9 @@ export class SuiChain extends Chain {
id: string,
mainnet: boolean,
wormholeChainName: string,
nativeToken: TokenId | undefined,
public rpcUrl: string
) {
super(id, mainnet, wormholeChainName, nativeToken);
super(id, mainnet, wormholeChainName);
}
static fromJson(parsed: ChainConfig): SuiChain {
@ -270,7 +258,6 @@ export class SuiChain extends Chain {
parsed.id,
parsed.mainnet,
parsed.wormholeChainName,
parsed.nativeToken,
parsed.rpcUrl
);
}
@ -326,12 +313,11 @@ export class EvmChain extends Chain {
constructor(
id: string,
mainnet: boolean,
nativeToken: TokenId | undefined,
private rpcUrl: string,
private networkId: number
) {
// On EVM networks we use the chain id as the wormhole chain name
super(id, mainnet, id, nativeToken);
super(id, mainnet, id);
}
static fromJson(parsed: ChainConfig & { networkId: number }): EvmChain {
@ -339,7 +325,6 @@ export class EvmChain extends Chain {
return new EvmChain(
parsed.id,
parsed.mainnet,
parsed.nativeToken,
parsed.rpcUrl,
parsed.networkId
);
@ -401,21 +386,6 @@ export class EvmChain extends Chain {
return gasPrice;
}
async estiamteAndSendTransaction(
transactionObject: TransactionObject<any>,
txParams: { from?: string; value?: string }
) {
const GAS_ESTIMATE_MULTIPLIER = 2;
const gasEstimate = await transactionObject.estimateGas(txParams);
// Some networks like Filecoin do not support the normal transaction type and need a type 2 transaction.
// To send a type 2 transaction, remove the ``gasPrice`` field.
return transactionObject.send({
gas: gasEstimate * GAS_ESTIMATE_MULTIPLIER,
gasPrice: Number(await this.getGasPrice()),
...txParams,
});
}
/**
* Deploys a contract on this chain
* @param privateKey hex string of the 32 byte private key without the 0x prefix
@ -481,10 +451,9 @@ export class AptosChain extends Chain {
id: string,
mainnet: boolean,
wormholeChainName: string,
nativeToken: TokenId | undefined,
public rpcUrl: string
) {
super(id, mainnet, wormholeChainName, nativeToken);
super(id, mainnet, wormholeChainName);
}
getClient(): AptosClient {
@ -522,7 +491,6 @@ export class AptosChain extends Chain {
parsed.id,
parsed.mainnet,
parsed.wormholeChainName,
parsed.nativeToken,
parsed.rpcUrl
);
}

View File

@ -3,7 +3,6 @@ import { ApiError, BCS, CoinClient, TxnBuilderTypes } from "aptos";
import { AptosChain, Chain } from "../chains";
import { DataSource } from "xc_admin_common";
import { WormholeContract } from "./wormhole";
import { TokenQty } from "../token";
type WormholeState = {
chain_id: { number: string };
@ -17,39 +16,7 @@ type GuardianSet = {
index: { number: string };
};
export class AptosWormholeContract extends WormholeContract {
static type = "AptosWormholeContract";
getId(): string {
return `${this.chain.getId()}_${this.address}`;
}
getType(): string {
return AptosWormholeContract.type;
}
toJson() {
return {
chain: this.chain.getId(),
address: this.address,
type: AptosWormholeContract.type,
};
}
static fromJson(
chain: Chain,
parsed: {
type: string;
address: string;
}
): AptosWormholeContract {
if (parsed.type !== AptosWormholeContract.type)
throw new Error("Invalid type");
if (!(chain instanceof AptosChain))
throw new Error(`Wrong chain type ${chain}`);
return new AptosWormholeContract(chain, parsed.address);
}
export class WormholeAptosContract extends WormholeContract {
constructor(public chain: AptosChain, public address: string) {
super();
}
@ -124,11 +91,7 @@ export class AptosPriceFeedContract extends PriceFeedContract {
static fromJson(
chain: Chain,
parsed: {
type: string;
stateId: string;
wormholeStateId: string;
}
parsed: { type: string; stateId: string; wormholeStateId: string }
): AptosPriceFeedContract {
if (parsed.type !== AptosPriceFeedContract.type)
throw new Error("Invalid type");
@ -156,8 +119,8 @@ export class AptosPriceFeedContract extends PriceFeedContract {
return this.chain.sendTransaction(senderPrivateKey, txPayload);
}
public getWormholeContract(): AptosWormholeContract {
return new AptosWormholeContract(this.chain, this.wormholeStateId);
public getWormholeContract(): WormholeAptosContract {
return new WormholeAptosContract(this.chain, this.wormholeStateId);
}
async executeUpdatePriceFeed(
@ -297,13 +260,9 @@ export class AptosPriceFeedContract extends PriceFeedContract {
return AptosPriceFeedContract.type;
}
async getTotalFee(): Promise<TokenQty> {
async getTotalFee(): Promise<bigint> {
const client = new CoinClient(this.chain.getClient());
const amount = await client.checkBalance(this.stateId);
return {
amount,
denom: this.chain.getNativeToken(),
};
return await client.checkBalance(this.stateId);
}
async getValidTimePeriod() {

View File

@ -17,7 +17,6 @@ import {
TxResult,
} from "../base";
import { WormholeContract } from "./wormhole";
import { TokenQty } from "../token";
/**
* Variables here need to be snake case to match the on-chain contract configs
@ -38,36 +37,7 @@ export interface DeploymentConfig {
fee: { amount: string; denom: string };
}
export class CosmWasmWormholeContract extends WormholeContract {
static type = "CosmWasmWormholeContract";
getId(): string {
return `${this.chain.getId()}_${this.address}`;
}
getType(): string {
return CosmWasmWormholeContract.type;
}
toJson() {
return {
chain: this.chain.getId(),
address: this.address,
type: CosmWasmWormholeContract.type,
};
}
static fromJson(
chain: Chain,
parsed: { type: string; address: string }
): CosmWasmWormholeContract {
if (parsed.type !== CosmWasmWormholeContract.type)
throw new Error("Invalid type");
if (!(chain instanceof CosmWasmChain))
throw new Error(`Wrong chain type ${chain}`);
return new CosmWasmWormholeContract(chain, parsed.address);
}
export class WormholeCosmWasmContract extends WormholeContract {
constructor(public chain: CosmWasmChain, public address: string) {
super();
}
@ -240,9 +210,7 @@ export class CosmWasmPriceFeedContract extends PriceFeedContract {
})) as Record<string, string>;
const config = {
config_v1: JSON.parse(allStates["\x00\tconfig_v1"]),
contract_version: allStates["\x00\x10contract_version"]
? JSON.parse(allStates["\x00\x10contract_version"])
: undefined,
contract_version: JSON.parse(allStates["\x00\x10contract_version"]),
};
return config;
}
@ -339,10 +307,10 @@ export class CosmWasmPriceFeedContract extends PriceFeedContract {
return { id: result.txHash, info: result };
}
async getWormholeContract(): Promise<CosmWasmWormholeContract> {
async getWormholeContract(): Promise<WormholeCosmWasmContract> {
const config = await this.getConfig();
const wormholeAddress = config.config_v1.wormhole_contract;
return new CosmWasmWormholeContract(this.chain, wormholeAddress);
return new WormholeCosmWasmContract(this.chain, wormholeAddress);
}
async getUpdateFee(msgs: string[]): Promise<Coin> {
@ -364,16 +332,13 @@ export class CosmWasmPriceFeedContract extends PriceFeedContract {
return this.chain;
}
async getTotalFee(): Promise<TokenQty> {
async getTotalFee(): Promise<bigint> {
const client = await CosmWasmClient.connect(this.chain.endpoint);
const coin = await client.getBalance(
this.address,
this.getChain().feeDenom
);
return {
amount: BigInt(coin.amount),
denom: this.chain.getNativeToken(),
};
return BigInt(coin.amount);
}
async getValidTimePeriod() {

View File

@ -5,9 +5,9 @@ import { PriceFeedContract, PrivateKey, Storable } from "../base";
import { Chain, EvmChain } from "../chains";
import { DataSource, EvmExecute } from "xc_admin_common";
import { WormholeContract } from "./wormhole";
import { TokenQty } from "../token";
// Just to make sure tx gas limit is enough
const GAS_ESTIMATE_MULTIPLIER = 2;
const EXTENDED_ENTROPY_ABI = [
{
inputs: [],
@ -62,19 +62,6 @@ const EXTENDED_ENTROPY_ABI = [
stateMutability: "pure",
type: "function",
},
{
inputs: [
{
internalType: "address",
name: "newImplementation",
type: "address",
},
],
name: "upgradeTo",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
...EntropyAbi,
] as any; // eslint-disable-line @typescript-eslint/no-explicit-any
const EXTENDED_PYTH_ABI = [
@ -367,60 +354,7 @@ const EXECUTOR_ABI = [
type: "function",
},
] as any; // eslint-disable-line @typescript-eslint/no-explicit-any
/**
* Returns the keccak256 digest of the contract bytecode at the given address after replacing
* any occurrences of the contract addr in the bytecode with 0.The bytecode stores the deployment
* address as an immutable variable. This behavior is inherited from OpenZeppelin's implementation
* of UUPSUpgradeable contract. You can read more about verification with immutable variables here:
* https://docs.sourcify.dev/docs/immutables/
* This function can be used to verify that the contract code is the same on all chains and matches
* with the deployedCode property generated by truffle builds
*/
export async function getCodeDigestWithoutAddress(
rpcUrl: string,
address: string
): Promise<string> {
const web3 = new Web3(rpcUrl);
const code = await web3.eth.getCode(address);
const strippedCode = code.replaceAll(
address.toLowerCase().replace("0x", ""),
"0000000000000000000000000000000000000000"
);
return Web3.utils.keccak256(strippedCode);
}
export class EvmWormholeContract extends WormholeContract {
static type = "EvmWormholeContract";
getId(): string {
return `${this.chain.getId()}_${this.address}`;
}
getChain(): EvmChain {
return this.chain;
}
getType(): string {
return EvmWormholeContract.type;
}
async getVersion(): Promise<string> {
const contract = this.getContract();
return contract.methods.version().call();
}
static fromJson(
chain: Chain,
parsed: { type: string; address: string }
): EvmWormholeContract {
if (parsed.type !== EvmWormholeContract.type)
throw new Error("Invalid type");
if (!(chain instanceof EvmChain))
throw new Error(`Wrong chain type ${chain}`);
return new EvmWormholeContract(chain, parsed.address);
}
export class WormholeEvmContract extends WormholeContract {
constructor(public chain: EvmChain, public address: string) {
super();
}
@ -460,20 +394,20 @@ export class EvmWormholeContract extends WormholeContract {
const transactionObject = wormholeContract.methods.submitNewGuardianSet(
"0x" + vaa.toString("hex")
);
const result = await this.chain.estiamteAndSendTransaction(
transactionObject,
{ from: address }
);
const gasEstiamte = await transactionObject.estimateGas({
from: address,
gas: 15000000,
});
// Some networks like Filecoin do not support the normal transaction type and need a type 2 transaction.
// To send a type 2 transaction, remove the ``gasPrice`` field and add the `type` field with the value
// `0x2` to the transaction configuration parameters.
const result = await transactionObject.send({
from: address,
gas: gasEstiamte * GAS_ESTIMATE_MULTIPLIER,
gasPrice: await this.chain.getGasPrice(),
});
return { id: result.transactionHash, info: result };
}
toJson() {
return {
chain: this.chain.getId(),
address: this.address,
type: EvmWormholeContract.type,
};
}
}
interface EntropyProviderInfo {
@ -554,18 +488,6 @@ export class EvmEntropyContract extends Storable {
return this.generateExecutorPayload(newOwner, this.address, data);
}
async generateUpgradeEntropyContractPayload(
newImplementation: string
): Promise<Buffer> {
const contract = this.getContract();
const data = contract.methods.upgradeTo(newImplementation).encodeABI();
return this.generateExecutorPayload(
await this.getOwner(),
this.address,
data
);
}
// Generates a payload to upgrade the executor contract, the owner of entropy contracts
async generateUpgradeExecutorContractsPayload(
newImplementation: string
@ -621,65 +543,6 @@ export class EvmEntropyContract extends Storable {
uri: Web3.utils.toAscii(info.uri),
};
}
generateUserRandomNumber() {
const web3 = new Web3(this.chain.getRpcUrl());
return web3.utils.randomHex(32);
}
async requestRandomness(
userRandomNumber: string,
provider: string,
senderPrivateKey: PrivateKey,
withCallback?: boolean
) {
const web3 = new Web3(this.chain.getRpcUrl());
const userCommitment = web3.utils.keccak256(userRandomNumber);
const contract = new web3.eth.Contract(EXTENDED_ENTROPY_ABI, this.address);
const fee = await contract.methods.getFee(provider).call();
const { address } = web3.eth.accounts.wallet.add(senderPrivateKey);
let transactionObject;
if (withCallback) {
transactionObject = contract.methods.requestWithCallback(
provider,
userCommitment
);
} else {
const useBlockHash = false;
transactionObject = contract.methods.request(
provider,
userCommitment,
useBlockHash
);
}
return this.chain.estiamteAndSendTransaction(transactionObject, {
from: address,
value: fee,
});
}
async revealRandomness(
userRevelation: string,
providerRevelation: string,
provider: string,
sequenceNumber: string,
senderPrivateKey: PrivateKey
) {
const web3 = new Web3(this.chain.getRpcUrl());
const contract = new web3.eth.Contract(EXTENDED_ENTROPY_ABI, this.address);
const { address } = web3.eth.accounts.wallet.add(senderPrivateKey);
const transactionObject = contract.methods.reveal(
provider,
sequenceNumber,
userRevelation,
providerRevelation
);
return this.chain.estiamteAndSendTransaction(transactionObject, {
from: address,
});
}
}
export class EvmExecutorContract {
@ -689,13 +552,13 @@ export class EvmExecutorContract {
return `${this.chain.getId()}_${this.address}`;
}
async getWormholeContract(): Promise<EvmWormholeContract> {
async getWormholeContract(): Promise<WormholeEvmContract> {
const web3 = new Web3(this.chain.getRpcUrl());
//Unfortunately, there is no public method to get the wormhole address
//Found 251 by using `forge build --extra-output storageLayout` and finding the slot for the wormhole variable.
let address = await web3.eth.getStorageAt(this.address, 251);
address = "0x" + address.slice(26);
return new EvmWormholeContract(this.chain, address);
return new WormholeEvmContract(this.chain, address);
}
getContract() {
@ -739,10 +602,15 @@ export class EvmExecutorContract {
const transactionObject = executorContract.methods.execute(
"0x" + vaa.toString("hex")
);
const result = await this.chain.estiamteAndSendTransaction(
transactionObject,
{ from: address }
);
const gasEstimate = await transactionObject.estimateGas({
from: address,
gas: 100000000,
});
const result = await transactionObject.send({
from: address,
gas: gasEstimate * GAS_ESTIMATE_MULTIPLIER,
gasPrice: await this.chain.getGasPrice(),
});
return { id: result.transactionHash, info: result };
}
}
@ -805,19 +673,26 @@ export class EvmPriceFeedContract extends PriceFeedContract {
}
/**
* Returns the keccak256 digest of the contract bytecode
* Returns the keccak256 digest of the contract bytecode after replacing any occurrences of the contract addr in
* the bytecode with 0.The bytecode stores the deployment address as an immutable variable.
* This behavior is inherited from OpenZeppelin's implementation of UUPSUpgradeable contract.
* You can read more about verification with immutable variables here:
* https://docs.sourcify.dev/docs/immutables/
* This function can be used to verify that the contract code is the same on all chains and matches
* with the deployedCode property generated by truffle builds
*/
async getCodeDigestWithoutAddress(): Promise<string> {
return getCodeDigestWithoutAddress(this.chain.getRpcUrl(), this.address);
const code = await this.getCode();
const strippedCode = code.replaceAll(
this.address.toLowerCase().replace("0x", ""),
"0000000000000000000000000000000000000000"
);
return Web3.utils.keccak256(strippedCode);
}
async getTotalFee(): Promise<TokenQty> {
async getTotalFee(): Promise<bigint> {
const web3 = new Web3(this.chain.getRpcUrl());
const amount = BigInt(await web3.eth.getBalance(this.address));
return {
amount,
denom: this.chain.getNativeToken(),
};
return BigInt(await web3.eth.getBalance(this.address));
}
async getLastExecutedGovernanceSequence() {
@ -860,10 +735,10 @@ export class EvmPriceFeedContract extends PriceFeedContract {
/**
* Returns the wormhole contract which is being used for VAA verification
*/
async getWormholeContract(): Promise<EvmWormholeContract> {
async getWormholeContract(): Promise<WormholeEvmContract> {
const pythContract = this.getContract();
const address = await pythContract.methods.wormhole().call();
return new EvmWormholeContract(this.chain, address);
return new WormholeEvmContract(this.chain, address);
}
async getBaseUpdateFee() {
@ -912,10 +787,17 @@ export class EvmPriceFeedContract extends PriceFeedContract {
.call();
const transactionObject =
pythContract.methods.updatePriceFeeds(priceFeedUpdateData);
const result = await this.chain.estiamteAndSendTransaction(
transactionObject,
{ from: address, value: updateFee }
);
const gasEstimate = await transactionObject.estimateGas({
from: address,
gas: 15000000,
value: updateFee,
});
const result = await transactionObject.send({
from: address,
value: updateFee,
gas: gasEstimate * GAS_ESTIMATE_MULTIPLIER,
gasPrice: await this.chain.getGasPrice(),
});
return { id: result.transactionHash, info: result };
}
@ -929,10 +811,15 @@ export class EvmPriceFeedContract extends PriceFeedContract {
const transactionObject = pythContract.methods.executeGovernanceInstruction(
"0x" + vaa.toString("hex")
);
const result = await this.chain.estiamteAndSendTransaction(
transactionObject,
{ from: address }
);
const gasEstiamte = await transactionObject.estimateGas({
from: address,
gas: 15000000,
});
const result = await transactionObject.send({
from: address,
gas: gasEstiamte * GAS_ESTIMATE_MULTIPLIER,
gasPrice: await this.chain.getGasPrice(),
});
return { id: result.transactionHash, info: result };
}

View File

@ -1,6 +1,6 @@
import { PrivateKey, Storable, TxResult } from "../base";
import { PrivateKey, TxResult } from "../base";
export abstract class WormholeContract extends Storable {
export abstract class WormholeContract {
abstract getCurrentGuardianSetIndex(): Promise<number>;
/**
@ -33,7 +33,6 @@ export abstract class WormholeContract extends Storable {
"010000000001007ac31b282c2aeeeb37f3385ee0de5f8e421d30b9e5ae8ba3d4375c1c77a86e77159bb697d9c456d6f8c02d22a94b1279b65b0d6a9957e7d3857423845ac758e300610ac1d2000000030001000000000000000000000000000000000000000000000000000000000000000400000000000005390000000000000000000000000000000000000000000000000000000000436f7265020000000000011358cc3ae5c097b213ce3c81979e1b9f9570746aa5ff6cb952589bde862c25ef4392132fb9d4a42157114de8460193bdf3a2fcf81f86a09765f4762fd1107a0086b32d7a0977926a205131d8731d39cbeb8c82b2fd82faed2711d59af0f2499d16e726f6b211b39756c042441be6d8650b69b54ebe715e234354ce5b4d348fb74b958e8966e2ec3dbd4958a7cdeb5f7389fa26941519f0863349c223b73a6ddee774a3bf913953d695260d88bc1aa25a4eee363ef0000ac0076727b35fbea2dac28fee5ccb0fea768eaf45ced136b9d9e24903464ae889f5c8a723fc14f93124b7c738843cbb89e864c862c38cddcccf95d2cc37a4dc036a8d232b48f62cdd4731412f4890da798f6896a3331f64b48c12d1d57fd9cbe7081171aa1be1d36cafe3867910f99c09e347899c19c38192b6e7387ccd768277c17dab1b7a5027c0b3cf178e21ad2e77ae06711549cfbb1f9c7a9d8096e85e1487f35515d02a92753504a8d75471b9f49edb6fbebc898f403e4773e95feb15e80c9a99c8348d",
"01000000010d0012e6b39c6da90c5dfd3c228edbb78c7a4c97c488ff8a346d161a91db067e51d638c17216f368aa9bdf4836b8645a98018ca67d2fec87d769cabfdf2406bf790a0002ef42b288091a670ef3556596f4f47323717882881eaf38e03345078d07a156f312b785b64dae6e9a87e3d32872f59cb1931f728cecf511762981baf48303668f0103cef2616b84c4e511ff03329e0853f1bd7ee9ac5ba71d70a4d76108bddf94f69c2a8a84e4ee94065e8003c334e899184943634e12043d0dda78d93996da073d190104e76d166b9dac98f602107cc4b44ac82868faf00b63df7d24f177aa391e050902413b71046434e67c770b19aecdf7fce1d1435ea0be7262e3e4c18f50ddc8175c0105d9450e8216d741e0206a50f93b750a47e0a258b80eb8fed1314cc300b3d905092de25cd36d366097b7103ae2d184121329ba3aa2d7c6cc53273f11af14798110010687477c8deec89d36a23e7948feb074df95362fc8dcbd8ae910ac556a1dee1e755c56b9db5d710c940938ed79bc1895a3646523a58bc55f475a23435a373ecfdd0107fb06734864f79def4e192497362513171530daea81f07fbb9f698afe7e66c6d44db21323144f2657d4a5386a954bb94eef9f64148c33aef6e477eafa2c5c984c01088769e82216310d1827d9bd48645ec23e90de4ef8a8de99e2d351d1df318608566248d80cdc83bdcac382b3c30c670352be87f9069aab5037d0b747208eae9c650109e9796497ff9106d0d1c62e184d83716282870cef61a1ee13d6fc485b521adcce255c96f7d1bca8d8e7e7d454b65783a830bddc9d94092091a268d311ecd84c26010c468c9fb6d41026841ff9f8d7368fa309d4dbea3ea4bbd2feccf94a92cc8a20a226338a8e2126cd16f70eaf15b4fc9be2c3fa19def14e071956a605e9d1ac4162010e23fcb6bd445b7c25afb722250c1acbc061ed964ba9de1326609ae012acdfb96942b2a102a2de99ab96327859a34a2b49a767dbdb62e0a1fb26af60fe44fd496a00106bb0bac77ac68b347645f2fb1ad789ea9bd76fb9b2324f25ae06f97e65246f142df717f662e73948317182c62ce87d79c73def0dba12e5242dfc038382812cfe00126da03c5e56cb15aeeceadc1e17a45753ab4dc0ec7bf6a75ca03143ed4a294f6f61bc3f478a457833e43084ecd7c985bf2f55a55f168aac0e030fc49e845e497101626e9d9a5d9e343f00010000000000000000000000000000000000000000000000000000000000000004c1759167c43f501c2000000000000000000000000000000000000000000000000000000000436f7265020000000000021358cc3ae5c097b213ce3c81979e1b9f9570746aa5ff6cb952589bde862c25ef4392132fb9d4a42157114de8460193bdf3a2fcf81f86a09765f4762fd1107a0086b32d7a0977926a205131d8731d39cbeb8c82b2fd82faed2711d59af0f2499d16e726f6b211b39756c042441be6d8650b69b54ebe715e234354ce5b4d348fb74b958e8966e2ec3dbd4958a7cd66b9590e1c41e0b226937bf9217d1d67fd4e91f574a3bf913953d695260d88bc1aa25a4eee363ef0000ac0076727b35fbea2dac28fee5ccb0fea768eaf45ced136b9d9e24903464ae889f5c8a723fc14f93124b7c738843cbb89e864c862c38cddcccf95d2cc37a4dc036a8d232b48f62cdd4731412f4890da798f6896a3331f64b48c12d1d57fd9cbe7081171aa1be1d36cafe3867910f99c09e347899c19c38192b6e7387ccd768277c17dab1b7a5027c0b3cf178e21ad2e77ae06711549cfbb1f9c7a9d8096e85e1487f35515d02a92753504a8d75471b9f49edb6fbebc898f403e4773e95feb15e80c9a99c8348d",
"01000000020d00ce45474d9e1b1e7790a2d210871e195db53a70ffd6f237cfe70e2686a32859ac43c84a332267a8ef66f59719cf91cc8df0101fd7c36aa1878d5139241660edc0010375cc906156ae530786661c0cd9aef444747bc3d8d5aa84cac6a6d2933d4e1a031cffa30383d4af8131e929d9f203f460b07309a647d6cd32ab1cc7724089392c000452305156cfc90343128f97e499311b5cae174f488ff22fbc09591991a0a73d8e6af3afb8a5968441d3ab8437836407481739e9850ad5c95e6acfcc871e951bc30105a7956eefc23e7c945a1966d5ddbe9e4be376c2f54e45e3d5da88c2f8692510c7429b1ea860ae94d929bd97e84923a18187e777aa3db419813a80deb84cc8d22b00061b2a4f3d2666608e0aa96737689e3ba5793810ff3a52ff28ad57d8efb20967735dc5537a2e43ef10f583d144c12a1606542c207f5b79af08c38656d3ac40713301086b62c8e130af3411b3c0d91b5b50dcb01ed5f293963f901fc36e7b0e50114dce203373b32eb45971cef8288e5d928d0ed51cd86e2a3006b0af6a65c396c009080009e93ab4d2c8228901a5f4525934000b2c26d1dc679a05e47fdf0ff3231d98fbc207103159ff4116df2832eea69b38275283434e6cd4a4af04d25fa7a82990b707010aa643f4cf615dfff06ffd65830f7f6cf6512dabc3690d5d9e210fdc712842dc2708b8b2c22e224c99280cd25e5e8bfb40e3d1c55b8c41774e287c1e2c352aecfc010b89c1e85faa20a30601964ccc6a79c0ae53cfd26fb10863db37783428cd91390a163346558239db3cd9d420cfe423a0df84c84399790e2e308011b4b63e6b8015010ca31dcb564ac81a053a268d8090e72097f94f366711d0c5d13815af1ec7d47e662e2d1bde22678113d15963da100b668ba26c0c325970d07114b83c5698f46097010dc9fda39c0d592d9ed92cd22b5425cc6b37430e236f02d0d1f8a2ef45a00bde26223c0a6eb363c8b25fd3bf57234a1d9364976cefb8360e755a267cbbb674b39501108db01e444ab1003dd8b6c96f8eb77958b40ba7a85fefecf32ad00b7a47c0ae7524216262495977e09c0989dd50f280c21453d3756843608eacd17f4fdfe47600001261025228ef5af837cb060bcd986fcfa84ccef75b3fa100468cfd24e7fadf99163938f3b841a33496c2706d0208faab088bd155b2e20fd74c625bb1cc8c43677a0163c53c409e0c5dfa000100000000000000000000000000000000000000000000000000000000000000046c5a054d7833d1e42000000000000000000000000000000000000000000000000000000000436f7265020000000000031358cc3ae5c097b213ce3c81979e1b9f9570746aa5ff6cb952589bde862c25ef4392132fb9d4a42157114de8460193bdf3a2fcf81f86a09765f4762fd1107a0086b32d7a0977926a205131d8731d39cbeb8c82b2fd82faed2711d59af0f2499d16e726f6b211b39756c042441be6d8650b69b54ebe715e234354ce5b4d348fb74b958e8966e2ec3dbd4958a7cd15e7caf07c4e3dc8e7c469f92c8cd88fb8005a2074a3bf913953d695260d88bc1aa25a4eee363ef0000ac0076727b35fbea2dac28fee5ccb0fea768eaf45ced136b9d9e24903464ae889f5c8a723fc14f93124b7c738843cbb89e864c862c38cddcccf95d2cc37a4dc036a8d232b48f62cdd4731412f4890da798f6896a3331f64b48c12d1d57fd9cbe7081171aa1be1d36cafe3867910f99c09e347899c19c38192b6e7387ccd768277c17dab1b7a5027c0b3cf178e21ad2e77ae06711549cfbb1f9c7a9d8096e85e1487f35515d02a92753504a8d75471b9f49edb6fbebc898f403e4773e95feb15e80c9a99c8348d",
"01000000030d03d4a37a6ff4361d91714730831e9d49785f61624c8f348a9c6c1d82bc1d98cadc5e936338204445c6250bb4928f3f3e165ad47ca03a5d63111168a2de4576856301049a5df10464ea4e1961589fd30fc18d1970a7a2ffaad617e56a0f7777f25275253af7d10a0f0f2494dc6e99fc80e444ab9ebbbee252ded2d5dcb50cbf7a54bb5a01055f4603b553b9ba9e224f9c55c7bca3da00abb10abd19e0081aecd3b352be061a70f79f5f388ebe5190838ef3cd13a2f22459c9a94206883b739c90b40d5d74640006a8fade3997f650a36e46bceb1f609edff201ab32362266f166c5c7da713f6a19590c20b68ed3f0119cb24813c727560ede086b3d610c2d7a1efa66f655bad90900080f5e495a75ea52241c59d145c616bfac01e57182ad8d784cbcc9862ed3afb60c0983ccbc690553961ffcf115a0c917367daada8e60be2cbb8b8008bac6341a8c010935ab11e0eea28b87a1edc5ccce3f1fac25f75b5f640fe6b0673a7cd74513c9dc01c544216cf364cc9993b09fda612e0cd1ced9c00fb668b872a16a64ebb55d27010ab2bc39617a2396e7defa24cd7c22f42dc31f3c42ffcd9d1472b02df8468a4d0563911e8fb6a4b5b0ce0bd505daa53779b08ff660967b31f246126ed7f6f29a7e000bdb6d3fd7b33bdc9ac3992916eb4aacb97e7e21d19649e7fa28d2dd6e337937e4274516a96c13ac7a8895da9f91948ea3a09c25f44b982c62ce8842b58e20c8a9000d3d1b19c8bb000856b6610b9d28abde6c35cb7705c6ca5db711f7be96d60eed9d72cfa402a6bfe8bf0496dbc7af35796fc768da51a067b95941b3712dce8ae1e7010ec80085033157fd1a5628fc0c56267469a86f0e5a66d7dede1ad4ce74ecc3dff95b60307a39c3bfbeedc915075070da30d0395def9635130584f709b3885e1bdc0010fc480eb9ee715a2d151b23722b48b42581d7f4001fc1696c75425040bfc1ffc5394fe418adb2b64bd3dc692efda4cc408163677dbe233b16bcdabb853a20843301118ee9e115e1a0c981f19d0772b850e666591322da742a9a12cce9f52a5665bd474abdd59c580016bee8aae67fdf39b315be2528d12eec3a652910e03cc4c6fa3801129d0d1e2e429e969918ec163d16a7a5b2c6729aa44af5dccad07d25d19891556a79b574f42d9adbd9e2a9ae5a6b8750331d2fccb328dd94c3bf8791ee1bfe85aa00661e99781981faea00010000000000000000000000000000000000000000000000000000000000000004fd4c6c55ec8dfd342000000000000000000000000000000000000000000000000000000000436f726502000000000004135893b5a76c3f739645648885bdccc06cd70a3cd3ff6cb952589bde862c25ef4392132fb9d4a42157114de8460193bdf3a2fcf81f86a09765f4762fd1107a0086b32d7a0977926a205131d8731d39cbeb8c82b2fd82faed2711d59af0f2499d16e726f6b211b39756c042441be6d8650b69b54ebe715e234354ce5b4d348fb74b958e8966e2ec3dbd4958a7cd15e7caf07c4e3dc8e7c469f92c8cd88fb8005a2074a3bf913953d695260d88bc1aa25a4eee363ef0000ac0076727b35fbea2dac28fee5ccb0fea768eaf45ced136b9d9e24903464ae889f5c8a723fc14f93124b7c738843cbb89e864c862c38cddcccf95d2cc37a4dc036a8d232b48f62cdd4731412f4890da798f6896a3331f64b48c12d1d57fd9cbe7081171aa1be1d36cafe3867910f99c09e347899c19c38192b6e7387ccd768277c17dab1b7a5027c0b3cf178e21ad2e77ae06711549cfbb1f9c7a9d8096e85e1487f35515d02a92753504a8d75471b9f49edb6fbebc898f403e4773e95feb15e80c9a99c8348d",
];
const currentIndex = await this.getCurrentGuardianSetIndex();
for (let i = currentIndex; i < MAINNET_UPGRADE_VAAS.length; i++) {

View File

@ -33,7 +33,6 @@ import {
deriveWormholeBridgeDataKey,
} from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
import { KeyValueConfig, Storable } from "./base";
import { PriorityFeeConfig } from "@pythnetwork/solana-utils";
class InvalidTransactionError extends Error {
constructor(message: string) {
@ -344,8 +343,7 @@ export class Vault extends Storable {
*/
public async proposeWormholeMessage(
payloads: Buffer[],
proposalAddress?: PublicKey,
priorityFeeConfig: PriorityFeeConfig = {}
proposalAddress?: PublicKey
): Promise<WormholeMultisigProposal> {
const squad = this.getSquadOrThrow();
const multisigVault = new MultisigVault(
@ -358,8 +356,7 @@ export class Vault extends Storable {
await multisigVault.proposeWormholeMultipleMessagesWithPayer(
payloads,
squad.wallet.publicKey,
proposalAddress,
priorityFeeConfig
proposalAddress
);
return new WormholeMultisigProposal(txAccount, squad, this.cluster);
}

View File

@ -8,9 +8,9 @@ repl.evalCode(
"import { loadHotWallet, Vault } from './src/governance';" +
"import { SuiChain, CosmWasmChain, AptosChain, EvmChain } from './src/chains';" +
"import { SuiPriceFeedContract } from './src/contracts/sui';" +
"import { CosmWasmWormholeContract, CosmWasmPriceFeedContract } from './src/contracts/cosmwasm';" +
"import { EvmWormholeContract, EvmPriceFeedContract } from './src/contracts/evm';" +
"import { AptosWormholeContract, AptosPriceFeedContract } from './src/contracts/aptos';" +
"import { WormholeCosmWasmContract, CosmWasmPriceFeedContract } from './src/contracts/cosmwasm';" +
"import { WormholeEvmContract, EvmPriceFeedContract } from './src/contracts/evm';" +
"import { WormholeAptosContract, AptosPriceFeedContract } from './src/contracts/aptos';" +
"import { DefaultStore } from './src/store';" +
"import { toPrivateKey } from './src/base';" +
"DefaultStore"

View File

@ -8,16 +8,11 @@ import {
} from "./chains";
import {
AptosPriceFeedContract,
AptosWormholeContract,
CosmWasmPriceFeedContract,
CosmWasmWormholeContract,
EvmEntropyContract,
EvmPriceFeedContract,
EvmWormholeContract,
SuiPriceFeedContract,
WormholeContract,
} from "./contracts";
import { Token } from "./token";
import { PriceFeedContract, Storable } from "./base";
import { parse, stringify } from "yaml";
import { readdirSync, readFileSync, statSync, writeFileSync } from "fs";
@ -27,14 +22,11 @@ export class Store {
public chains: Record<string, Chain> = { global: new GlobalChain() };
public contracts: Record<string, PriceFeedContract> = {};
public entropy_contracts: Record<string, EvmEntropyContract> = {};
public wormhole_contracts: Record<string, WormholeContract> = {};
public tokens: Record<string, Token> = {};
public vaults: Record<string, Vault> = {};
constructor(public path: string) {
this.loadAllChains();
this.loadAllContracts();
this.loadAllTokens();
this.loadAllVaults();
}
@ -86,7 +78,6 @@ export class Store {
const contractsByType: Record<string, Storable[]> = {};
const contracts: Storable[] = Object.values(this.contracts);
contracts.push(...Object.values(this.entropy_contracts));
contracts.push(...Object.values(this.wormhole_contracts));
for (const contract of contracts) {
if (!contractsByType[contract.getType()]) {
contractsByType[contract.getType()] = [];
@ -120,13 +111,10 @@ export class Store {
loadAllContracts() {
const allContractClasses = {
[CosmWasmPriceFeedContract.type]: CosmWasmPriceFeedContract,
[CosmWasmWormholeContract.type]: CosmWasmWormholeContract,
[SuiPriceFeedContract.type]: SuiPriceFeedContract,
[EvmPriceFeedContract.type]: EvmPriceFeedContract,
[AptosPriceFeedContract.type]: AptosPriceFeedContract,
[AptosWormholeContract.type]: AptosWormholeContract,
[EvmEntropyContract.type]: EvmEntropyContract,
[EvmWormholeContract.type]: EvmWormholeContract,
};
this.getYamlFiles(`${this.path}/contracts/`).forEach((yamlFile) => {
const parsedArray = parse(readFileSync(yamlFile, "utf-8"));
@ -141,16 +129,13 @@ export class Store {
);
if (
this.contracts[chainContract.getId()] ||
this.entropy_contracts[chainContract.getId()] ||
this.wormhole_contracts[chainContract.getId()]
this.entropy_contracts[chainContract.getId()]
)
throw new Error(
`Multiple contracts with id ${chainContract.getId()} found`
);
if (chainContract instanceof EvmEntropyContract) {
this.entropy_contracts[chainContract.getId()] = chainContract;
} else if (chainContract instanceof WormholeContract) {
this.wormhole_contracts[chainContract.getId()] = chainContract;
} else {
this.contracts[chainContract.getId()] = chainContract;
}
@ -158,20 +143,6 @@ export class Store {
});
}
loadAllTokens() {
this.getYamlFiles(`${this.path}/tokens/`).forEach((yamlFile) => {
const parsedArray = parse(readFileSync(yamlFile, "utf-8"));
for (const parsed of parsedArray) {
if (parsed.type !== Token.type) return;
const token = Token.fromJson(parsed);
if (this.tokens[token.getId()])
throw new Error(`Multiple tokens with id ${token.getId()} found`);
this.tokens[token.getId()] = token;
}
});
}
loadAllVaults() {
this.getYamlFiles(`${this.path}/vaults/`).forEach((yamlFile) => {
const parsedArray = parse(readFileSync(yamlFile, "utf-8"));

View File

@ -1,81 +0,0 @@
import axios from "axios";
import { KeyValueConfig, Storable } from "./base";
export type TokenId = string;
/**
* A quantity of a token, represented as an integer number of the minimum denomination of the token.
* This can also represent a quantity of an unknown token (represented by an undefined denom).
*/
export type TokenQty = {
amount: bigint;
denom: TokenId | undefined;
};
/**
* A token represents a cryptocurrency like ETH or BTC.
* The main use of this class is to calculate the dollar value of accrued fees.
*/
export class Token extends Storable {
static type = "token";
public constructor(
public id: TokenId,
// The hexadecimal pyth id of the tokens X/USD price feed
// (get this from hermes or the Pyth docs page)
public pythId: string | undefined,
public decimals: number
) {
super();
}
getId(): TokenId {
return this.id;
}
getType(): string {
return Token.type;
}
/**
* Get the dollar value of 1 token. Returns undefined for tokens that do
* not have a configured pricing method.
*/
async getPrice(): Promise<number | undefined> {
if (this.pythId) {
const url = `https://hermes.pyth.network/v2/updates/price/latest?ids%5B%5D=${this.pythId}&parsed=true`;
const response = await axios.get(url);
const price = response.data.parsed[0].price;
// Note that this conversion can lose some precision.
// We don't really care about that in this application.
return parseInt(price.price) * Math.pow(10, price.expo);
} else {
// We may support other pricing methodologies in the future but whatever.
return undefined;
}
}
/**
* Get the dollar value of the minimum representable quantity of this token.
* E.g., for ETH, this method will return the dollar value of 1 wei.
*/
async getPriceForMinUnit(): Promise<number | undefined> {
const price = await this.getPrice();
return price ? price / Math.pow(10, this.decimals) : undefined;
}
toJson(): KeyValueConfig {
return {
id: this.id,
...(this.pythId !== undefined ? { pythId: this.pythId } : {}),
};
}
static fromJson(parsed: {
id: string;
pythId?: string;
decimals: number;
}): Token {
return new Token(parsed.id, parsed.pythId, parsed.decimals);
}
}

View File

@ -8,7 +8,6 @@
mainnet: true
rpcUrl: https://fullnode.mainnet.aptoslabs.com/v1
type: AptosChain
nativeToken: APT
- id: movement_move_devnet
wormholeChainName: movement_move_devnet
mainnet: false

View File

@ -66,19 +66,3 @@
prefix: juno
feeDenom: ujunox
type: CosmWasmChain
- endpoint: http://18.199.53.161:26657
id: rol_testnet
wormholeChainName: rol_testnet
mainnet: false
gasPrice: "0.025"
prefix: rol
feeDenom: urax
type: CosmWasmChain
- endpoint: https://testnet-burnt-rpc.lavenderfive.com
id: xion_testnet
wormholeChainName: xion_testnet
mainnet: false
gasPrice: "0.025"
prefix: xion
feeDenom: uxion
type: CosmWasmChain

View File

@ -3,11 +3,6 @@
rpcUrl: https://linea-goerli.blastapi.io/$ENV_BLAST_API_KEY
networkId: 59140
type: EvmChain
- id: linea_sepolia
mainnet: false
rpcUrl: https://rpc.sepolia.linea.build
networkId: 59141
type: EvmChain
- id: kava
mainnet: true
rpcUrl: https://kava-evm.publicnode.com
@ -18,7 +13,6 @@
rpcUrl: https://evmos-evm.publicnode.com
networkId: 9001
type: EvmChain
nativeToken: EVMOS
- id: canto
mainnet: true
rpcUrl: https://canto.slingshot.finance
@ -39,6 +33,11 @@
rpcUrl: https://evm-t3.cronos.org
networkId: 338
type: EvmChain
- id: zksync_goerli
mainnet: false
rpcUrl: https://zksync2-testnet.zksync.dev
networkId: 280
type: EvmChain
- id: canto_testnet
mainnet: false
rpcUrl: https://canto-testnet.plexnode.wtf
@ -46,14 +45,9 @@
type: EvmChain
- id: polygon_zkevm_testnet
mainnet: false
rpcUrl: https://rpc.public.zkevm-test.net
rpcUrl: https://rpc.public.zkevm-test.net/
networkId: 1442
type: EvmChain
- id: polygon_blackberry
mainnet: false
rpcUrl: https://rpc.polygon-blackberry.gelato.digital
networkId: 94204209
type: EvmChain
- id: aurora_testnet
mainnet: false
rpcUrl: https://testnet.aurora.dev
@ -69,7 +63,6 @@
rpcUrl: https://rpc.gnosischain.com
networkId: 100
type: EvmChain
nativeToken: DAI
- id: fantom_testnet
mainnet: false
rpcUrl: https://fantom-testnet.blastapi.io/$ENV_BLAST_API_KEY
@ -77,7 +70,7 @@
type: EvmChain
- id: neon
mainnet: true
rpcUrl: https://neon-evm.drpc.org
rpcUrl: https://neon-proxy-mainnet.solana.p2p.org
networkId: 245022934
type: EvmChain
- id: fantom
@ -85,7 +78,6 @@
rpcUrl: https://rpc.ankr.com/fantom
networkId: 250
type: EvmChain
nativeToken: FTM
- id: mumbai
mainnet: false
rpcUrl: https://polygon-testnet.blastapi.io/$ENV_BLAST_API_KEY
@ -111,7 +103,6 @@
rpcUrl: https://rpc.mantle.xyz/
networkId: 5000
type: EvmChain
nativeToken: MNT
- id: kava_testnet
mainnet: false
rpcUrl: https://evm.testnet.kava.io
@ -132,7 +123,6 @@
rpcUrl: https://eth-mainnet.blastapi.io/$ENV_BLAST_API_KEY
networkId: 1
type: EvmChain
nativeToken: ETH
- id: bsc_testnet
mainnet: false
rpcUrl: https://rpc.ankr.com/bsc_testnet_chapel
@ -148,7 +138,6 @@
rpcUrl: https://mainnet.aurora.dev
networkId: 1313161554
type: EvmChain
nativeToken: NEAR
- id: bsc
mainnet: true
rpcUrl: https://rpc.ankr.com/bsc
@ -169,6 +158,11 @@
rpcUrl: https://evm.confluxrpc.org
networkId: 1030
type: EvmChain
- id: optimism_goerli
mainnet: false
rpcUrl: https://rpc.ankr.com/optimism_testnet
networkId: 420
type: EvmChain
- id: celo
mainnet: true
rpcUrl: https://forno.celo.org
@ -179,7 +173,6 @@
rpcUrl: https://polygon-rpc.com
networkId: 137
type: EvmChain
nativeToken: MATIC
- id: wemix_testnet
mainnet: false
rpcUrl: https://api.test.wemix.com
@ -190,13 +183,11 @@
rpcUrl: https://rpc-mainnet.kcc.network
networkId: 321
type: EvmChain
nativeToken: KCS
- id: polygon_zkevm
mainnet: true
rpcUrl: https://zkevm-rpc.com
networkId: 1101
type: EvmChain
nativeToken: ETH
- id: celo_alfajores_testnet
mainnet: false
rpcUrl: https://alfajores-forno.celo-testnet.org
@ -212,25 +203,21 @@
rpcUrl: https://zksync2-mainnet.zksync.io
networkId: 324
type: EvmChain
nativeToken: ETH
- id: base
mainnet: true
rpcUrl: https://developer-access-mainnet.base.org/
networkId: 8453
type: EvmChain
nativeToken: ETH
- id: arbitrum
mainnet: true
rpcUrl: https://arb1.arbitrum.io/rpc
networkId: 42161
type: EvmChain
nativeToken: ETH
- id: optimism
mainnet: true
rpcUrl: https://rpc.ankr.com/optimism
networkId: 10
type: EvmChain
nativeToken: ETH
- id: kcc_testnet
mainnet: false
rpcUrl: https://rpc-testnet.kcc.network
@ -251,7 +238,6 @@
rpcUrl: https://linea.rpc.thirdweb.com
networkId: 59144
type: EvmChain
nativeToken: ETH
- id: shimmer_testnet
mainnet: false
rpcUrl: https://json-rpc.evm.testnet.shimmer.network
@ -279,7 +265,7 @@
type: EvmChain
- id: horizen_eon
mainnet: true
rpcUrl: https://rpc.ankr.com/horizen_eon
rpcUrl: https://eon-rpc.horizenlabs.io/ethv1
networkId: 7332
type: EvmChain
- id: horizen_gobi
@ -302,11 +288,6 @@
rpcUrl: https://sepolia-rollup.arbitrum.io/rpc
networkId: 421614
type: EvmChain
- id: arbitrum_blueberry
mainnet: false
rpcUrl: https://rpc.arb-blueberry.gelato.digital
networkId: 88153591557
type: EvmChain
- id: boba
mainnet: true
rpcUrl: https://replica.boba.network
@ -317,11 +298,6 @@
rpcUrl: https://goerli.boba.network
networkId: 2888
type: EvmChain
- id: boba_sepolia
mainnet: false
rpcUrl: https://sepolia.boba.network
networkId: 28882
type: EvmChain
- id: manta
mainnet: true
rpcUrl: https://pacific-rpc.manta.network/http
@ -329,7 +305,7 @@
type: EvmChain
- id: manta_testnet
mainnet: false
rpcUrl: https://manta-pacific-testnet.drpc.org
rpcUrl: https://pacific-rpc.testnet.manta.network/http
networkId: 3441005
type: EvmChain
- id: manta_sepolia
@ -342,11 +318,6 @@
rpcUrl: https://sepolia.optimism.io
networkId: 11155420
type: EvmChain
- id: optimism_celestia_raspberry
mainnet: false
rpcUrl: https://rpc.opcelestia-raspberry.gelato.digital
networkId: 123420111
type: EvmChain
- id: chiliz_spicy
mainnet: false
rpcUrl: https://spicy-rpc.chiliz.com
@ -372,11 +343,6 @@
rpcUrl: https://rpc.zkatana.gelato.digital
networkId: 1261120
type: EvmChain
- id: astar_zkyoto_testnet
mainnet: false
rpcUrl: https://rpc.startale.com/zkyoto
networkId: 6038361
type: EvmChain
- id: astar_zkevm
mainnet: true
rpcUrl: https://rpc.startale.com/astar-zkevm
@ -392,15 +358,14 @@
rpcUrl: https://rpc.coredao.org
networkId: 1116
type: EvmChain
nativeToken: CORE
- id: viction
- id: tomochain
mainnet: true
rpcUrl: https://viction.blockpi.network/v1/rpc/public
rpcUrl: https://rpc.tomochain.com
networkId: 88
type: EvmChain
- id: viction_testnet
- id: tomochain_testnet
mainnet: false
rpcUrl: https://rpc-testnet.viction.xyz
rpcUrl: https://rpc.testnet.tomochain.com
networkId: 89
type: EvmChain
- id: mode_testnet
@ -450,7 +415,7 @@
type: EvmChain
- id: blast_s2_testnet
mainnet: false
rpcUrl: https://sepolia.blast.io
rpcUrl: https://rpc.s2.testblast.io/$ENV_BLAST_S2_TESTNET_API_KEY
networkId: 168587773
type: EvmChain
- id: hedera_testnet
@ -463,7 +428,6 @@
rpcUrl: https://mainnet.hashio.io/api
networkId: 295
type: EvmChain
nativeToken: HBAR
- id: filecoin_calibration
mainnet: false
rpcUrl: https://rpc.ankr.com/filecoin_testnet
@ -486,7 +450,7 @@
type: EvmChain
- id: sei_evm_devnet
mainnet: false
rpcUrl: https://evm-rpc-arctic-1.sei-apis.com
rpcUrl: https://evm-devnet.seinetwork.io
networkId: 713715
type: EvmChain
- id: fantom_sonic_testnet
@ -502,7 +466,7 @@
- id: idex_xchain_testnet
mainnet: false
rpcUrl: https://xchain-testnet-rpc.idex.io
networkId: 64002
networkId: 671276500
type: EvmChain
- id: injective_inevm_testnet
mainnet: false
@ -534,43 +498,3 @@
rpcUrl: https://rpc.merlinchain.io
networkId: 4200
type: EvmChain
- id: parallel_testnet
mainnet: false
rpcUrl: https://rpc-accused-coffee-koala-b9fn1dik76.t.conduit.xyz
networkId: 9659
type: EvmChain
- id: parallel
mainnet: true
rpcUrl: https://rpc.parallel.fi/
networkId: 1024
type: EvmChain
- id: polynomial_testnet
mainnet: false
rpcUrl: https://rpc-polynomial-network-testnet-x0tryg8u1c.t.conduit.xyz
networkId: 80008
type: EvmChain
- id: morph_testnet
mainnet: false
rpcUrl: https://rpc-testnet.morphl2.io
networkId: 2710
type: EvmChain
- id: iota
mainnet: true
rpcUrl: https://json-rpc.evm.iotaledger.net
networkId: 8822
type: EvmChain
- id: flow_previewnet
mainnet: true
rpcUrl: https://previewnet.evm.nodes.onflow.org
networkId: 646
type: EvmChain
- id: olive_testnet
mainnet: false
rpcUrl: https://olive-network-testnet.rpc.caldera.xyz/http
networkId: 8101902
type: EvmChain
- id: taiko_hekla
mainnet: false
rpcUrl: https://rpc.hekla.taiko.xyz/
networkId: 167009
type: EvmChain

View File

@ -1,9 +0,0 @@
- chain: aptos_mainnet
address: "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625"
type: AptosWormholeContract
- chain: aptos_testnet
address: "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625"
type: AptosWormholeContract
- chain: movement_move_devnet
address: "0x9236893d6444b208b7e0b3e8d4be4ace90b6d17817ab7d1584e46a33ef5c50c9"
type: AptosWormholeContract

View File

@ -40,9 +40,3 @@
- chain: neutron_testnet_pion_1
address: neutron16zwrmx3zgggmxhzau86xfycm42cr4sj888hdvzsxya3qarp6zhhqzhlkvz
type: CosmWasmPriceFeedContract
- chain: rol_testnet
address: rol1pvrwmjuusn9wh34j7y520g8gumuy9xtl3gvprlljfdpwju3x7ucszdyfs8
type: CosmWasmPriceFeedContract
- chain: xion_testnet
address: xion1w39ctwxxhxxc2kxarycjxj9rndn65gf8daek7ggarwh3rq3zl0lqqllnmt
type: CosmWasmPriceFeedContract

View File

@ -1,48 +0,0 @@
- chain: rol_testnet
address: rol17p9rzwnnfxcjp32un9ug7yhhzgtkhvl9jfksztgw5uh69wac2pgss2u902
type: CosmWasmWormholeContract
- chain: osmosis
address: osmo1t7qham5kle36rs28se2xd7cckm9mpwzgt65t40lrdf8fcq3837qqjvw80s
type: CosmWasmWormholeContract
- chain: sei_testnet_atlantic_2
address: sei14utt2wp7hamd2qmuz0e5yj728y4u08cm7etujxkc6qprnrla3uwq95jz86
type: CosmWasmWormholeContract
- chain: juno_testnet
address: juno1h7m0xwgu4qh0nrthahpydxzw7klvyd5w8d7jjl675p944ds7jr4sf3ta4l
type: CosmWasmWormholeContract
- chain: sei_testnet_atlantic_2
address: sei1cn8ygrvqk03p5zce3c6rrst7j97qarm33d23rxgme7rzmasddfusw7cpxw
type: CosmWasmWormholeContract
- chain: neutron_testnet_pion_1
address: neutron1nxs2ajn4ejrggfuvqczfx4txghrendcpy3526avg2tsngjktedtspgla8t
type: CosmWasmWormholeContract
- chain: neutron_testnet_pion_1
address: neutron1wtuuak4yt4vyhtv7gt4xnv0m8zfakad5lnz6r7dx8alyydu0sgns67kmvy
type: CosmWasmWormholeContract
- chain: juno_testnet
address: juno1g9xhl5jzhlm6lqc2earxkzyazwl2cshr5cnemxtjy0le64s4w22skukkxj
type: CosmWasmWormholeContract
- chain: osmosis_testnet_5
address: osmo19ah8ak7rgmds40te22xnz7zsdmx5twjulv3sypqm79skkl2ajm4skuhwmf
type: CosmWasmWormholeContract
- chain: sei_pacific_1
address: sei12qq3cufehhsaprjfjrwpx5ltyr43lcrxvf6eaqf0p4jsjpc7semq8p6ewa
type: CosmWasmWormholeContract
- chain: injective_testnet
address: inj1hglkee95shfsl5xxky26hdqxj0mqp54lh7xm59
type: CosmWasmWormholeContract
- chain: neutron
address: neutron178ruq7gf6gk3uus5n8xztj5tsrt5xwxfelw88mc9egfw5d99ktksnk5rsh
type: CosmWasmWormholeContract
- chain: osmosis_testnet_5
address: osmo1llum0y8zc4h2f0rhcdn63xje4mrkdljrve9l40lun9lpeyu2l7cq4phaw6
type: CosmWasmWormholeContract
- chain: injective_testnet
address: inj17sy3vx5dfeva9wx33d09yqdwruntpccnjyw0hj
type: CosmWasmWormholeContract
- chain: injective
address: inj17p9rzwnnfxcjp32un9ug7yhhzgtkhvl9l2q74d
type: CosmWasmWormholeContract
- chain: xion_testnet
address: xion14ycw3tx0hpz3aawmzm6cufs6hx94d64ht5qawd0ej9ug9j2ffzsqmpecys
type: CosmWasmWormholeContract

View File

@ -19,6 +19,9 @@
- chain: blast_s2_testnet
address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603"
type: EvmEntropyContract
- chain: sei_evm_devnet
address: "0x6E3A2a644eeDCf6007d3c7d85F0094Cc1B25B2AE"
type: EvmEntropyContract
- chain: lightlink_phoenix
address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603"
type: EvmEntropyContract
@ -55,12 +58,3 @@
- chain: zetachain
address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320"
type: EvmEntropyContract
- chain: base
address: "0x6E7D74FA7d5c90FEF9F0512987605a6d546181Bb"
type: EvmEntropyContract
- chain: sei_evm_devnet
address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509"
type: EvmEntropyContract
- chain: taiko_hekla
address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603"
type: EvmEntropyContract

View File

@ -97,7 +97,7 @@
- chain: coredao
address: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729"
type: EvmPriceFeedContract
- chain: viction
- chain: tomochain
address: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729"
type: EvmPriceFeedContract
- chain: arbitrum_sepolia
@ -142,6 +142,9 @@
- chain: meter_testnet
address: "0x5a71C07a0588074443545eE0c08fb0375564c3E4"
type: EvmPriceFeedContract
- chain: optimism_goerli
address: "0xDd24F84d36BF92C65F92307595335bdFab5Bbd21"
type: EvmPriceFeedContract
- chain: shimmer_testnet
address: "0x8D254a21b3C86D32F7179855531CE99164721933"
type: EvmPriceFeedContract
@ -166,6 +169,9 @@
- chain: coredao_testnet
address: "0x8D254a21b3C86D32F7179855531CE99164721933"
type: EvmPriceFeedContract
- chain: tomochain_testnet
address: "0x5D289Ad1CE59fCC25b6892e7A303dfFf3a9f7167"
type: EvmPriceFeedContract
- chain: cronos_testnet
address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320"
type: EvmPriceFeedContract
@ -196,6 +202,9 @@
- chain: neon_devnet
address: "0x0708325268dF9F66270F1401206434524814508b"
type: EvmPriceFeedContract
- chain: zksync_goerli
address: "0x8739d5024B5143278E2b15Bd9e7C26f6CEc658F1"
type: EvmPriceFeedContract
- chain: optimism_sepolia
address: "0x0708325268dF9F66270F1401206434524814508b"
type: EvmPriceFeedContract
@ -239,7 +248,7 @@
address: "0x2880aB155794e7179c9eE2e38200202908C17B43"
type: EvmPriceFeedContract
- chain: sei_evm_devnet
address: "0xe9d69CdD6Fe41e7B621B4A688C5D1a68cB5c8ADc"
address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509"
type: EvmPriceFeedContract
- chain: lightlink_pegasus_testnet
address: "0x5D289Ad1CE59fCC25b6892e7A303dfFf3a9f7167"
@ -254,7 +263,7 @@
address: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729"
type: EvmPriceFeedContract
- chain: idex_xchain_testnet
address: "0x2880aB155794e7179c9eE2e38200202908C17B43"
address: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729"
type: EvmPriceFeedContract
- chain: injective_inevm_testnet
address: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729"
@ -289,36 +298,3 @@
- chain: manta_sepolia
address: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729"
type: EvmPriceFeedContract
- chain: polygon_blackberry
address: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729"
type: EvmPriceFeedContract
- chain: arbitrum_blueberry
address: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729"
type: EvmPriceFeedContract
- chain: optimism_celestia_raspberry
address: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729"
type: EvmPriceFeedContract
- chain: polynomial_testnet
address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509"
type: EvmPriceFeedContract
- chain: parallel_testnet
address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509"
type: EvmPriceFeedContract
- chain: parallel
address: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729"
type: EvmPriceFeedContract
- chain: linea_sepolia
address: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729"
type: EvmPriceFeedContract
- chain: morph_testnet
address: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729"
type: EvmPriceFeedContract
- chain: flow_previewnet
address: "0x2880aB155794e7179c9eE2e38200202908C17B43"
type: EvmPriceFeedContract
- chain: taiko_hekla
address: "0x2880aB155794e7179c9eE2e38200202908C17B43"
type: EvmPriceFeedContract
- chain: olive_testnet
address: "0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c"
type: EvmPriceFeedContract

View File

@ -1,303 +0,0 @@
- chain: polygon
address: "0x35a58BeeE77a2Ad547FcDed7e8CB1c6e19746b13"
type: EvmWormholeContract
- chain: aurora
address: "0x41955476936DdA8d0fA98b8d1778172F7E4fCcA1"
type: EvmWormholeContract
- chain: fantom
address: "0x35a58BeeE77a2Ad547FcDed7e8CB1c6e19746b13"
type: EvmWormholeContract
- chain: optimism
address: "0x87047526937246727E4869C5f76A347160e08672"
type: EvmWormholeContract
- chain: arbitrum
address: "0xEbe57e8045F2F230872523bbff7374986E45C486"
type: EvmWormholeContract
- chain: gnosis
address: "0x26DD80569a8B23768A1d80869Ed7339e07595E85"
type: EvmWormholeContract
- chain: polygon_zkevm
address: "0x41955476936DdA8d0fA98b8d1778172F7E4fCcA1"
type: EvmWormholeContract
- chain: conflux_espace
address: "0xDd24F84d36BF92C65F92307595335bdFab5Bbd21"
type: EvmWormholeContract
- chain: bsc
address: "0x41955476936DdA8d0fA98b8d1778172F7E4fCcA1"
type: EvmWormholeContract
- chain: kava
address: "0x0708325268dF9F66270F1401206434524814508b"
type: EvmWormholeContract
- chain: avalanche
address: "0x41955476936DdA8d0fA98b8d1778172F7E4fCcA1"
type: EvmWormholeContract
- chain: canto
address: "0xf0a1b566B55e0A0CB5BeF52Eb2a57142617Bee67"
type: EvmWormholeContract
- chain: linea
address: "0x0708325268dF9F66270F1401206434524814508b"
type: EvmWormholeContract
- chain: neon
address: "0xCd76c50c3210C5AaA9c39D53A4f95BFd8b1a3a19"
type: EvmWormholeContract
- chain: mantle
address: "0xf0a1b566B55e0A0CB5BeF52Eb2a57142617Bee67"
type: EvmWormholeContract
- chain: meter
address: "0xfA133831D350A2A5997d6db182B6Ca9e8ad4191B"
type: EvmWormholeContract
- chain: kcc
address: "0x41955476936DdA8d0fA98b8d1778172F7E4fCcA1"
type: EvmWormholeContract
- chain: eos
address: "0xEbe57e8045F2F230872523bbff7374986E45C486"
type: EvmWormholeContract
- chain: celo
address: "0x41955476936DdA8d0fA98b8d1778172F7E4fCcA1"
type: EvmWormholeContract
- chain: wemix
address: "0xEbe57e8045F2F230872523bbff7374986E45C486"
type: EvmWormholeContract
- chain: base
address: "0x87047526937246727E4869C5f76A347160e08672"
type: EvmWormholeContract
- chain: zksync
address: "0x53cD6960888cA09361506678adfE267b4CE81A08"
type: EvmWormholeContract
- chain: horizen_eon
address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a"
type: EvmWormholeContract
- chain: shimmer
address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a"
type: EvmWormholeContract
- chain: boba
address: "0x26DD80569a8B23768A1d80869Ed7339e07595E85"
type: EvmWormholeContract
- chain: manta
address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a"
type: EvmWormholeContract
- chain: scroll
address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a"
type: EvmWormholeContract
- chain: chiliz
address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a"
type: EvmWormholeContract
- chain: coredao
address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a"
type: EvmWormholeContract
- chain: viction
address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a"
type: EvmWormholeContract
- chain: arbitrum_sepolia
address: "0xfA25E653b44586dBbe27eE9d252192F0e4956683"
type: EvmWormholeContract
- chain: fuji
address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb"
type: EvmWormholeContract
- chain: canto_testnet
address: "0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c"
type: EvmWormholeContract
- chain: aurora_testnet
address: "0x2880aB155794e7179c9eE2e38200202908C17B43"
type: EvmWormholeContract
- chain: chiado
address: "0x87047526937246727E4869C5f76A347160e08672"
type: EvmWormholeContract
- chain: kava_testnet
address: "0xD458261E832415CFd3BAE5E416FdF3230ce6F134"
type: EvmWormholeContract
- chain: conflux_espace_testnet
address: "0xEbe57e8045F2F230872523bbff7374986E45C486"
type: EvmWormholeContract
- chain: celo_alfajores_testnet
address: "0x2880aB155794e7179c9eE2e38200202908C17B43"
type: EvmWormholeContract
- chain: bsc_testnet
address: "0xe9d69CdD6Fe41e7B621B4A688C5D1a68cB5c8ADc"
type: EvmWormholeContract
- chain: kcc_testnet
address: "0x2880aB155794e7179c9eE2e38200202908C17B43"
type: EvmWormholeContract
- chain: eos_testnet
address: "0x8D254a21b3C86D32F7179855531CE99164721933"
type: EvmWormholeContract
- chain: meter_testnet
address: "0x257c3B61102442C1c3286Efbd24242322d002920"
type: EvmWormholeContract
- chain: shimmer_testnet
address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603"
type: EvmWormholeContract
- chain: scroll_sepolia
address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320"
type: EvmWormholeContract
- chain: boba_goerli
address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603"
type: EvmWormholeContract
- chain: manta_testnet
address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603"
type: EvmWormholeContract
- chain: chiliz_spicy
address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603"
type: EvmWormholeContract
- chain: coredao_testnet
address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603"
type: EvmWormholeContract
- chain: cronos_testnet
address: "0x74f09cb3c7e2A01865f424FD14F6dc9A14E3e94E"
type: EvmWormholeContract
- chain: wemix_testnet
address: "0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c"
type: EvmWormholeContract
- chain: evmos_testnet
address: "0x2880aB155794e7179c9eE2e38200202908C17B43"
type: EvmWormholeContract
- chain: zetachain_testnet
address: "0x8D254a21b3C86D32F7179855531CE99164721933"
type: EvmWormholeContract
- chain: neon_devnet
address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509"
type: EvmWormholeContract
- chain: optimism_sepolia
address: "0x8D254a21b3C86D32F7179855531CE99164721933"
type: EvmWormholeContract
- chain: mode
address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a"
type: EvmWormholeContract
- chain: mode_testnet
address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a"
type: EvmWormholeContract
- chain: bttc_testnet
address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a"
type: EvmWormholeContract
- chain: bttc
address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a"
type: EvmWormholeContract
- chain: zksync_sepolia
address: "0xc10F5BE78E464BB0E1f534D66E5A6ecaB150aEFa"
type: EvmWormholeContract
- chain: base_sepolia
address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a"
type: EvmWormholeContract
- chain: movement_evm_devnet
address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a"
type: EvmWormholeContract
- chain: zkfair_testnet
address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a"
type: EvmWormholeContract
- chain: blast_s2_testnet
address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a"
type: EvmWormholeContract
- chain: zkfair
address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a"
type: EvmWormholeContract
- chain: filecoin_calibration
address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a"
type: EvmWormholeContract
- chain: filecoin
address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a"
type: EvmWormholeContract
- chain: zetachain
address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
type: EvmWormholeContract
- chain: sei_evm_devnet
address: "0x66E9cBa5529824a03B5Bc9931d9c63637101D0F7"
type: EvmWormholeContract
- chain: lightlink_pegasus_testnet
address: "0x5f3c61944CEb01B3eAef861251Fb1E0f14b848fb"
type: EvmWormholeContract
- chain: fantom_sonic_testnet
address: "0x74f09cb3c7e2A01865f424FD14F6dc9A14E3e94E"
type: EvmWormholeContract
- chain: dela_deperp_testnet
address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
type: EvmWormholeContract
- chain: lightlink_phoenix
address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
type: EvmWormholeContract
- chain: injective_inevm_testnet
address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
type: EvmWormholeContract
- chain: injective_inevm
address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
type: EvmWormholeContract
- chain: hedera_testnet
address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
type: EvmWormholeContract
- chain: hedera
address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
type: EvmWormholeContract
- chain: berachain_testnet
address: "0x74f09cb3c7e2A01865f424FD14F6dc9A14E3e94E"
type: EvmWormholeContract
- chain: blast
address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
type: EvmWormholeContract
- chain: astar_zkevm
address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
type: EvmWormholeContract
- chain: merlin_testnet
address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
type: EvmWormholeContract
- chain: mantle_sepolia
address: "0x66E9cBa5529824a03B5Bc9931d9c63637101D0F7"
type: EvmWormholeContract
- chain: merlin
address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
type: EvmWormholeContract
- chain: manta_sepolia
address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
type: EvmWormholeContract
- chain: polygon_blackberry
address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
type: EvmWormholeContract
- chain: arbitrum_blueberry
address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
type: EvmWormholeContract
- chain: optimism_celestia_raspberry
address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
type: EvmWormholeContract
- chain: polynomial_testnet
address: "0x87047526937246727E4869C5f76A347160e08672"
type: EvmWormholeContract
- chain: parallel_testnet
address: "0x87047526937246727E4869C5f76A347160e08672"
type: EvmWormholeContract
- chain: parallel
address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
type: EvmWormholeContract
- chain: linea_sepolia
address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
type: EvmWormholeContract
- chain: morph_testnet
address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
type: EvmWormholeContract
- chain: cronos
address: "0x41955476936DdA8d0fA98b8d1778172F7E4fCcA1"
type: EvmWormholeContract
- chain: ronin
address: "0x41955476936DdA8d0fA98b8d1778172F7E4fCcA1"
type: EvmWormholeContract
- chain: saigon
address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320"
type: EvmWormholeContract
- chain: ethereum
address: "0x74f09cb3c7e2A01865f424FD14F6dc9A14E3e94E"
type: EvmWormholeContract
- chain: mumbai
address: "0x876A4e56A51386aBb1a5ab5d62f77E814372f0C7"
type: EvmWormholeContract
- chain: fantom_testnet
address: "0xe9d69CdD6Fe41e7B621B4A688C5D1a68cB5c8ADc"
type: EvmWormholeContract
- chain: sepolia
address: "0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c"
type: EvmWormholeContract
- chain: linea_goerli
address: "0xfA25E653b44586dBbe27eE9d252192F0e4956683"
type: EvmWormholeContract
- chain: taiko_hekla
address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
type: EvmWormholeContract
- chain: olive_testnet
address: "0x74f09cb3c7e2A01865f424FD14F6dc9A14E3e94E"
type: EvmWormholeContract

View File

@ -1,44 +0,0 @@
- id: ETH
pythId: ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace
decimals: 18
type: token
- id: APT
pythId: 03ae4db29ed4ae33d323568895aa00337e658e348b37509f5372ae51f0af00d5
decimals: 8
type: token
- id: EVMOS
pythId: c19405e4c8bdcbf2a66c37ae05a27d385c8309e9d648ed20dc6ee717e7d30e17
decimals: 18
type: token
- id: MATIC
pythId: 5de33a9112c2b700b8d30b8a3402c103578ccfa2765696471cc672bd5cf6ac52
decimals: 18
type: token
- id: NEAR
pythId: c415de8d2eba7db216527dff4b60e8f3a5311c740dadb233e13e12547e226750
decimals: 18
type: token
- id: FTM
pythId: 5c6c0d2386e3352356c3ab84434fafb5ea067ac2678a38a338c4a69ddc4bdb0c
decimals: 18
type: token
- id: DAI
pythId: b0948a5e5313200c632b51bb5ca32f6de0d36e9950a942d19751e833f70dabfd
decimals: 18
type: token
- id: KCS
pythId: c8acad81438490d4ebcac23b3e93f31cdbcb893fcba746ea1c66b89684faae2f
decimals: 18
type: token
- id: MNT
pythId: 4e3037c822d852d79af3ac80e35eb420ee3b870dca49f9344a38ef4773fb0585
decimals: 18
type: token
- id: HBAR
pythId: 3728e591097635310e6341af53db8b7ee42da9b3a8d918f9463ce9cca886dfbd
decimals: 8
type: token
- id: CORE
pythId: 9b4503710cc8c53f75c30e6e4fda1a7064680ef2e0ee97acd2e3a7c37b3c830c
decimals: 18
type: token

View File

@ -1,6 +0,0 @@
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
};

View File

@ -1,5 +0,0 @@
lib/*
out
cache
tslib
!lib/README.md

View File

@ -1,20 +0,0 @@
# EasyLend Protocol
EasyLend is a simplified lending protocol that uses Express Relay for avoiding value leakage on liquidations.
It uses Pyth price feeds to calculate the asset values and the liquidation thresholds.
This project illustrates how to use the Express Relay SDK for contract integration and publishing opportunities.
## Contracts
The contracts are located in the `contracts` directory. The `EasyLend.sol` file contains the main contract logic.
The protocol can allow creation of undercollateralized vaults that are liquidatable upon creation. This is solely
for ease of testing and demonstration purposes.
## Monitoring script
The script in `src/monitor.ts` is used to monitor the vaults health and publish the liquidation opportunities:
- It subscribes to Pyth price feeds to get the latest prices for the assets used in the protocol.
- It periodically checks for new vaults using the chain rpc.
- Upon finding a vault that is below the liquidation threshold, it publishes a liquidation opportunity using the Express Relay SDK.

View File

@ -1,358 +0,0 @@
// Copyright (C) 2024 Lavra Holdings Limited - All Rights Reserved
pragma solidity ^0.8.13;
import "./EasyLendStructs.sol";
import "./EasyLendErrors.sol";
import "forge-std/StdMath.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
import "@pythnetwork/express-relay-sdk-solidity/IExpressRelayFeeReceiver.sol";
import "@pythnetwork/express-relay-sdk-solidity/IExpressRelay.sol";
contract EasyLend is IExpressRelayFeeReceiver {
using SafeERC20 for IERC20;
event VaultReceivedETH(address sender, uint256 amount, bytes permissionKey);
uint256 _nVaults;
address public immutable expressRelay;
mapping(uint256 => Vault) _vaults;
address _oracle;
bool _allowUndercollateralized;
/**
* @notice EasyLend constructor - Initializes a new token vault contract with given parameters
*
* @param expressRelayAddress: address of the express relay
* @param oracleAddress: address of the oracle contract
* @param allowUndercollateralized: boolean to allow undercollateralized vaults to be created and updated. Can be set to true for testing.
*/
constructor(
address expressRelayAddress,
address oracleAddress,
bool allowUndercollateralized
) {
_nVaults = 0;
expressRelay = expressRelayAddress;
_oracle = oracleAddress;
_allowUndercollateralized = allowUndercollateralized;
}
/**
* @notice getLastVaultId function - getter function to get the id of the next vault to be created
* Ids are sequential and start from 0
*/
function getLastVaultId() public view returns (uint256) {
return _nVaults;
}
/**
* @notice convertToUint function - converts a Pyth price struct to a uint256 representing the price of an asset
*
* @param price: Pyth price struct to be converted
* @param targetDecimals: target number of decimals for the output
*/
function convertToUint(
PythStructs.Price memory price,
uint8 targetDecimals
) private pure returns (uint256) {
if (price.price < 0 || price.expo > 0 || price.expo < -255) {
revert InvalidPriceExponent();
}
uint8 priceDecimals = uint8(uint32(-1 * price.expo));
if (targetDecimals >= priceDecimals) {
return
uint(uint64(price.price)) *
10 ** uint32(targetDecimals - priceDecimals);
} else {
return
uint(uint64(price.price)) /
10 ** uint32(priceDecimals - targetDecimals);
}
}
/**
* @notice getPrice function - retrieves price of a given token from the oracle
*
* @param id: price feed Id of the token
*/
function _getPrice(bytes32 id) internal view returns (uint256) {
IPyth oracle = IPyth(payable(_oracle));
return convertToUint(oracle.getPrice(id), 18);
}
function getAllowUndercollateralized() public view returns (bool) {
return _allowUndercollateralized;
}
function getOracle() public view returns (address) {
return _oracle;
}
/**
* @notice getVaultHealth function - calculates vault collateral/debt ratio
*
* @param vaultId: Id of the vault for which to calculate health
*/
function getVaultHealth(uint256 vaultId) public view returns (uint256) {
Vault memory vault = _vaults[vaultId];
return _getVaultHealth(vault);
}
/**
* @notice _getVaultHealth function - calculates vault collateral/debt ratio using the on-chain price feeds.
* In a real world scenario, caller should ensure that the price feeds are up to date before calling this function.
*
* @param vault: vault struct containing vault parameters
*/
function _getVaultHealth(
Vault memory vault
) internal view returns (uint256) {
uint256 priceCollateral = _getPrice(vault.tokenIdCollateral);
uint256 priceDebt = _getPrice(vault.tokenIdDebt);
if (priceCollateral < 0) {
revert NegativePrice();
}
if (priceDebt < 0) {
revert NegativePrice();
}
uint256 valueCollateral = priceCollateral * vault.amountCollateral;
uint256 valueDebt = priceDebt * vault.amountDebt;
return (valueCollateral * 1_000_000_000_000_000_000) / valueDebt;
}
/**
* @notice createVault function - creates a vault
*
* @param tokenCollateral: address of the collateral token of the vault
* @param tokenDebt: address of the debt token of the vault
* @param amountCollateral: amount of collateral tokens in the vault
* @param amountDebt: amount of debt tokens in the vault
* @param minHealthRatio: minimum health ratio of the vault, 10**18 is 100%
* @param minPermissionlessHealthRatio: minimum health ratio of the vault before permissionless liquidations are allowed. This should be less than minHealthRatio
* @param tokenIdCollateral: price feed Id of the collateral token
* @param tokenIdDebt: price feed Id of the debt token
* @param updateData: data to update price feeds with
*/
function createVault(
address tokenCollateral,
address tokenDebt,
uint256 amountCollateral,
uint256 amountDebt,
uint256 minHealthRatio,
uint256 minPermissionlessHealthRatio,
bytes32 tokenIdCollateral,
bytes32 tokenIdDebt,
bytes[] calldata updateData
) public payable returns (uint256) {
_updatePriceFeeds(updateData);
Vault memory vault = Vault(
tokenCollateral,
tokenDebt,
amountCollateral,
amountDebt,
minHealthRatio,
minPermissionlessHealthRatio,
tokenIdCollateral,
tokenIdDebt
);
if (minPermissionlessHealthRatio > minHealthRatio) {
revert InvalidHealthRatios();
}
if (
!_allowUndercollateralized &&
_getVaultHealth(vault) < vault.minHealthRatio
) {
revert UncollateralizedVaultCreation();
}
IERC20(vault.tokenCollateral).safeTransferFrom(
msg.sender,
address(this),
vault.amountCollateral
);
IERC20(vault.tokenDebt).safeTransfer(msg.sender, vault.amountDebt);
_vaults[_nVaults] = vault;
_nVaults += 1;
return _nVaults;
}
/**
* @notice updateVault function - updates a vault's collateral and debt amounts
*
* @param vaultId: Id of the vault to be updated
* @param deltaCollateral: delta change to collateral amount (+ means adding collateral tokens, - means removing collateral tokens)
* @param deltaDebt: delta change to debt amount (+ means withdrawing debt tokens from protocol, - means resending debt tokens to protocol)
*/
function updateVault(
uint256 vaultId,
int256 deltaCollateral,
int256 deltaDebt
) public {
Vault memory vault = _vaults[vaultId];
uint256 qCollateral = stdMath.abs(deltaCollateral);
uint256 qDebt = stdMath.abs(deltaDebt);
bool withdrawExcessiveCollateral = (deltaCollateral < 0) &&
(qCollateral > vault.amountCollateral);
if (withdrawExcessiveCollateral) {
revert InvalidVaultUpdate();
}
uint256 futureCollateral = (deltaCollateral >= 0)
? (vault.amountCollateral + qCollateral)
: (vault.amountCollateral - qCollateral);
uint256 futureDebt = (deltaDebt >= 0)
? (vault.amountDebt + qDebt)
: (vault.amountDebt - qDebt);
vault.amountCollateral = futureCollateral;
vault.amountDebt = futureDebt;
if (
!_allowUndercollateralized &&
_getVaultHealth(vault) < vault.minHealthRatio
) {
revert InvalidVaultUpdate();
}
// update collateral position
if (deltaCollateral >= 0) {
// sender adds more collateral to their vault
IERC20(vault.tokenCollateral).safeTransferFrom(
msg.sender,
address(this),
qCollateral
);
_vaults[vaultId].amountCollateral += qCollateral;
} else {
// sender takes back collateral from their vault
IERC20(vault.tokenCollateral).safeTransfer(msg.sender, qCollateral);
_vaults[vaultId].amountCollateral -= qCollateral;
}
// update debt position
if (deltaDebt >= 0) {
// sender takes out more debt position
IERC20(vault.tokenDebt).safeTransfer(msg.sender, qDebt);
_vaults[vaultId].amountDebt += qDebt;
} else {
// sender sends back debt tokens
IERC20(vault.tokenDebt).safeTransferFrom(
msg.sender,
address(this),
qDebt
);
_vaults[vaultId].amountDebt -= qDebt;
}
}
/**
* @notice getVault function - getter function to get a vault's parameters
*
* @param vaultId: Id of the vault
*/
function getVault(uint256 vaultId) public view returns (Vault memory) {
return _vaults[vaultId];
}
/**
* @notice _updatePriceFeeds function - updates the specified price feeds with given data
*
* @param updateData: data to update price feeds with
*/
function _updatePriceFeeds(bytes[] calldata updateData) internal {
if (updateData.length == 0) {
return;
}
IPyth oracle = IPyth(payable(_oracle));
oracle.updatePriceFeeds{value: msg.value}(updateData);
}
/**
* @notice liquidate function - liquidates a vault
* This function calculates the health of the vault and based on the vault parameters one of the following actions is taken:
* 1. If health >= minHealthRatio, don't liquidate
* 2. If minHealthRatio > health >= minPermissionlessHealthRatio, only liquidate if the vault is permissioned via express relay
* 3. If minPermissionlessHealthRatio > health, liquidate no matter what
*
* @param vaultId: Id of the vault to be liquidated
*/
function liquidate(uint256 vaultId) public {
Vault memory vault = _vaults[vaultId];
uint256 vaultHealth = _getVaultHealth(vault);
// if vault health is above the minimum health ratio, don't liquidate
if (vaultHealth >= vault.minHealthRatio) {
revert InvalidLiquidation();
}
if (vaultHealth >= vault.minPermissionlessHealthRatio) {
// if vault health is below the minimum health ratio but above the minimum permissionless health ratio,
// only liquidate if permissioned
if (
!IExpressRelay(expressRelay).isPermissioned(
address(this), // protocol fee receiver
abi.encode(vaultId) // vault id uniquely represents the opportunity and can be used as permission id
)
) {
revert InvalidLiquidation();
}
}
IERC20(vault.tokenDebt).transferFrom(
msg.sender,
address(this),
vault.amountDebt
);
IERC20(vault.tokenCollateral).transfer(
msg.sender,
vault.amountCollateral
);
_vaults[vaultId].amountCollateral = 0;
_vaults[vaultId].amountDebt = 0;
}
/**
* @notice liquidateWithPriceUpdate function - liquidates a vault after updating the specified price feeds with given data
*
* @param vaultId: Id of the vault to be liquidated
* @param updateData: data to update price feeds with
*/
function liquidateWithPriceUpdate(
uint256 vaultId,
bytes[] calldata updateData
) external payable {
_updatePriceFeeds(updateData);
liquidate(vaultId);
}
/**
* @notice receiveAuctionProceedings function - receives native token from the express relay
* You can use permission key to distribute the received funds to users who got liquidated, LPs, etc...
*
* @param permissionKey: permission key that was used for the auction
*/
function receiveAuctionProceedings(
bytes calldata permissionKey
) external payable {
emit VaultReceivedETH(msg.sender, msg.value, permissionKey);
}
receive() external payable {}
}

View File

@ -1,20 +0,0 @@
// Copyright (C) 2024 Lavra Holdings Limited - All Rights Reserved
pragma solidity ^0.8.13;
// Signature: 0xe922edfd
error UncollateralizedVaultCreation();
// Signature: 0xdcb430ee
error InvalidVaultUpdate();
// Signature: 0x9cd7b1c6
error InvalidPriceExponent();
// Signature: 0x85914873
error InvalidLiquidation();
// Signature: 0x61ca76d2
error NegativePrice();
// Signature: 0x4a7a3163
error InvalidHealthRatios();

View File

@ -1,13 +0,0 @@
// Copyright (C) 2024 Lavra Holdings Limited - All Rights Reserved
pragma solidity ^0.8.13;
struct Vault {
address tokenCollateral;
address tokenDebt;
uint256 amountCollateral;
uint256 amountDebt;
uint256 minHealthRatio; // 10**18 is 100%
uint256 minPermissionlessHealthRatio;
bytes32 tokenIdCollateral;
bytes32 tokenIdDebt;
}

View File

@ -1,10 +0,0 @@
[profile.default]
src = "contracts"
out = "out"
libs = [
'lib',
'../../../node_modules',
'../../../target_chains/ethereum/sdk/solidity'
]
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

View File

@ -1 +0,0 @@
Forge installs the dependencies in this folder. They are .gitignored

View File

@ -1,40 +0,0 @@
{
"name": "easylend",
"version": "0.1.0",
"description": "Example lending protocol with express relay integration",
"private": true,
"files": [
"tslib/**/*"
],
"scripts": {
"build": "tsc",
"lint": "eslint src/",
"format": "prettier --write \"src/**/*.ts\"",
"monitor": "npm run build && node tslib/monitor.js",
"install-forge-deps": "forge install foundry-rs/forge-std@v1.7.6 --no-git --no-commit"
},
"author": "",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/pyth-network/pyth-crosschain.git"
},
"dependencies": {
"@openzeppelin/contracts": "^4.5.0",
"@pythnetwork/express-relay-evm-js": "*",
"@pythnetwork/express-relay-sdk-solidity": "*",
"@pythnetwork/pyth-evm-js": "*",
"@pythnetwork/pyth-sdk-solidity": "*",
"ts-node": "^10.9.1",
"typescript": "^5.3.3",
"viem": "^2.7.6"
},
"devDependencies": {
"@types/yargs": "^17.0.10",
"eslint": "^8.56.0",
"prettier": "^2.6.2",
"typedoc": "^0.25.7",
"typescript": "^5.1",
"yargs": "^17.4.1"
}
}

View File

@ -1,3 +0,0 @@
forge-std/=lib/forge-std/src/
@openzeppelin/=../../../node_modules/@openzeppelin/
@pythnetwork/=../../../node_modules/@pythnetwork/

View File

@ -1,163 +0,0 @@
// This is only a subset of the generated abi necessary for the monitor script
export const abi = [
{
type: "function",
name: "getLastVaultId",
inputs: [],
outputs: [
{
name: "",
type: "uint256",
internalType: "uint256",
},
],
stateMutability: "view",
},
{
type: "function",
name: "getVault",
inputs: [
{
name: "vaultId",
type: "uint256",
internalType: "uint256",
},
],
outputs: [
{
name: "",
type: "tuple",
internalType: "struct Vault",
components: [
{
name: "tokenCollateral",
type: "address",
internalType: "address",
},
{
name: "tokenDebt",
type: "address",
internalType: "address",
},
{
name: "amountCollateral",
type: "uint256",
internalType: "uint256",
},
{
name: "amountDebt",
type: "uint256",
internalType: "uint256",
},
{
name: "minHealthRatio",
type: "uint256",
internalType: "uint256",
},
{
name: "minPermissionLessHealthRatio",
type: "uint256",
internalType: "uint256",
},
{
name: "tokenIdCollateral",
type: "bytes32",
internalType: "bytes32",
},
{
name: "tokenIdDebt",
type: "bytes32",
internalType: "bytes32",
},
],
},
],
stateMutability: "view",
},
{
type: "function",
name: "liquidate",
inputs: [
{
name: "vaultId",
type: "uint256",
internalType: "uint256",
},
],
outputs: [],
stateMutability: "nonpayable",
},
{
type: "function",
name: "liquidateWithPriceUpdate",
inputs: [
{
name: "vaultId",
type: "uint256",
internalType: "uint256",
},
{
name: "updateData",
type: "bytes[]",
internalType: "bytes[]",
},
],
outputs: [],
stateMutability: "payable",
},
{
type: "event",
name: "VaultReceivedETH",
inputs: [
{
name: "sender",
type: "address",
indexed: false,
internalType: "address",
},
{
name: "amount",
type: "uint256",
indexed: false,
internalType: "uint256",
},
{
name: "permissionKey",
type: "bytes",
indexed: false,
internalType: "bytes",
},
],
anonymous: false,
},
{
type: "error",
name: "InvalidHealthRatios",
inputs: [],
},
{
type: "error",
name: "InvalidLiquidation",
inputs: [],
},
{
type: "error",
name: "InvalidPriceExponent",
inputs: [],
},
{
type: "error",
name: "InvalidVaultUpdate",
inputs: [],
},
{
type: "error",
name: "NegativePrice",
inputs: [],
},
{
type: "error",
name: "UncollateralizedVaultCreation",
inputs: [],
},
] as const;

View File

@ -1,239 +0,0 @@
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import {
checkAddress,
Client,
OpportunityParams,
} from "@pythnetwork/express-relay-evm-js";
import { privateKeyToAccount } from "viem/accounts";
import type { ContractFunctionReturnType } from "viem";
import {
Address,
createPublicClient,
encodeAbiParameters,
encodeFunctionData,
getContract,
Hex,
http,
isHex,
} from "viem";
import { optimismSepolia } from "viem/chains";
import { abi } from "./abi";
import {
PriceFeed,
PriceServiceConnection,
} from "@pythnetwork/price-service-client";
type VaultWithId = ContractFunctionReturnType<
typeof abi,
"view",
"getVault"
> & { id: bigint };
class ProtocolMonitor {
private client: Client;
private subscribedIds: Set<string> = new Set();
private prices: Record<Hex, PriceFeed> = {};
private priceConnection: PriceServiceConnection;
constructor(
expressRelayEndpoint: string,
pythEndpoint: string,
private chainId: string,
private wethContract: Address,
private vaultContract: Address,
private onlyRecent: number | undefined
) {
this.client = new Client({ baseUrl: expressRelayEndpoint });
this.priceConnection = new PriceServiceConnection(pythEndpoint, {
priceFeedRequestConfig: { binary: true },
});
}
updatePrice(feed: PriceFeed) {
this.prices[`0x${feed.id}`] = feed;
}
async subscribeToPriceFeed(tokenId: string) {
if (!this.subscribedIds.has(tokenId)) {
await this.priceConnection.subscribePriceFeedUpdates(
[tokenId],
this.updatePrice.bind(this)
);
this.subscribedIds.add(tokenId);
}
}
async checkVaults() {
const rpcClient = createPublicClient({
chain: optimismSepolia,
transport: http(),
});
const contract = getContract({
address: this.vaultContract,
abi,
client: rpcClient,
});
const lastVaultId = await contract.read.getLastVaultId();
const vaults: VaultWithId[] = [];
let startVaultId = 0n;
if (this.onlyRecent && lastVaultId > BigInt(this.onlyRecent)) {
startVaultId = lastVaultId - BigInt(this.onlyRecent);
}
for (let vaultId = startVaultId; vaultId < lastVaultId; vaultId++) {
const vault = await contract.read.getVault([vaultId]);
// Already liquidated vault
if (vault.amountCollateral == 0n && vault.amountDebt == 0n) {
continue;
}
vaults.push({ id: vaultId, ...vault });
await this.subscribeToPriceFeed(vault.tokenIdCollateral);
await this.subscribeToPriceFeed(vault.tokenIdDebt);
}
for (const vault of vaults) {
if (this.isLiquidatable(vault)) {
const opportunity = this.createOpportunity(vault);
await this.client.submitOpportunity(opportunity);
}
}
}
async start() {
// eslint-disable-next-line no-constant-condition
while (true) {
await this.checkVaults();
await new Promise((resolve) => setTimeout(resolve, 10000));
}
}
private createOpportunity(vault: VaultWithId) {
const priceUpdates = [
this.prices[vault.tokenIdCollateral].getVAA()!,
this.prices[vault.tokenIdDebt].getVAA()!,
];
const vaas: Hex[] = priceUpdates.map(
(vaa): Hex => `0x${Buffer.from(vaa, "base64").toString("hex")}`
);
const calldata = encodeFunctionData({
abi,
functionName: "liquidateWithPriceUpdate",
args: [vault.id, vaas],
});
const permission = this.createPermission(vault.id);
const targetCallValue = BigInt(priceUpdates.length);
let sellTokens;
if (targetCallValue > 0 && vault.tokenDebt == this.wethContract) {
sellTokens = [
{
token: this.wethContract,
amount: targetCallValue + vault.amountDebt,
},
];
} else {
sellTokens = [
{ token: vault.tokenDebt, amount: vault.amountDebt },
{ token: this.wethContract, amount: targetCallValue },
];
}
const opportunity: OpportunityParams = {
chainId: this.chainId,
targetContract: this.vaultContract,
targetCalldata: calldata,
permissionKey: permission,
targetCallValue: targetCallValue,
buyTokens: [
{ token: vault.tokenCollateral, amount: vault.amountCollateral },
],
sellTokens: sellTokens,
};
return opportunity;
}
private isLiquidatable(vault: VaultWithId): boolean {
if (
!this.prices[vault.tokenIdCollateral] ||
!this.prices[vault.tokenIdDebt]
) {
return false;
}
const priceCollateral = BigInt(
this.prices[vault.tokenIdCollateral].getPriceUnchecked().price
);
const priceDebt = BigInt(
this.prices[vault.tokenIdDebt].getPriceUnchecked().price
);
const valueCollateral = priceCollateral * vault.amountCollateral;
const valueDebt = priceDebt * vault.amountDebt;
if (valueDebt * vault.minHealthRatio > valueCollateral * 10n ** 18n) {
const health = Number(valueCollateral) / Number(valueDebt);
console.log(`Vault ${vault.id} is undercollateralized health: ${health}`);
return true;
}
return false;
}
private createPermission(vaultId: bigint) {
const permissionPayload = encodeAbiParameters(
[{ type: "uint256", name: "vaultId" }],
[vaultId]
);
const permission = encodeAbiParameters(
[
{ type: "address", name: "contract" },
{ type: "bytes", name: "vaultId" },
],
[this.vaultContract, permissionPayload]
);
return permission;
}
}
const argv = yargs(hideBin(process.argv))
.option("express-relay-endpoint", {
description:
"Express relay endpoint. e.g: https://per-staging.dourolabs.app/",
type: "string",
default: "https://per-staging.dourolabs.app/",
})
.option("pyth-endpoint", {
description: "Pyth endpoint to use for fetching prices",
type: "string",
default: "https://hermes.pyth.network",
})
.option("chain-id", {
description: "Chain id to send opportunities for. e.g: sepolia",
type: "string",
demandOption: true,
})
.option("weth-contract", {
description: "wrapped eth contract address",
type: "string",
demandOption: true,
})
.option("vault-contract", {
description: "Dummy token vault contract address",
type: "string",
demandOption: true,
})
.option("only-recent", {
description:
"Instead of checking all vaults, only check recent ones. Specify the number of recent vaults to check",
type: "number",
})
.help()
.alias("help", "h")
.parseSync();
async function run() {
const monitor = new ProtocolMonitor(
argv.expressRelayEndpoint,
argv.pythEndpoint,
argv.chainId,
checkAddress(argv.wethContract),
checkAddress(argv.vaultContract),
argv.onlyRecent
);
await monitor.start();
}
run();

View File

@ -1,15 +0,0 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"declaration": true,
"rootDir": "src/",
"outDir": "./tslib",
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true
},
"include": ["src"],
"exclude": ["node_modules", "**/__tests__/*"]
}

View File

@ -1,6 +1,6 @@
{
"name": "@pythnetwork/express-relay-evm-js",
"version": "0.4.1",
"version": "0.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {

View File

@ -1,6 +1,6 @@
{
"name": "@pythnetwork/express-relay-evm-js",
"version": "0.4.1",
"version": "0.2.0",
"description": "Utilities for interacting with the express relay protocol",
"homepage": "https://github.com/pyth-network/pyth-crosschain/tree/main/express_relay/sdk/js",
"author": "Douro Labs",
@ -42,8 +42,6 @@
"ws": "^8.16.0"
},
"devDependencies": {
"@pythnetwork/pyth-evm-js": "*",
"@types/node": "^20.12.7",
"@types/yargs": "^17.0.10",
"@typescript-eslint/eslint-plugin": "^5.21.0",
"@typescript-eslint/parser": "^5.21.0",

View File

@ -23,17 +23,10 @@ class SimpleSearcher {
}
async bidStatusHandler(bidStatus: BidStatusUpdate) {
let resultDetails = "";
if (bidStatus.type == "submitted") {
resultDetails = `, transaction ${bidStatus.result}, index ${bidStatus.index} of multicall`;
} else if (bidStatus.type == "lost") {
resultDetails = `, transaction ${bidStatus.result}`;
}
console.log(
`Bid status for bid ${bidStatus.id}: ${bidStatus.type.replaceAll(
"_",
" "
)}${resultDetails}`
`Bid status for bid ${bidStatus.id}: ${bidStatus.status} ${
bidStatus.status == "submitted" ? bidStatus.result : ""
}`
);
}

View File

@ -2,8 +2,15 @@ import type { components, paths } from "./serverTypes";
import createClient, {
ClientOptions as FetchClientOptions,
} from "openapi-fetch";
import { Address, Hex, isAddress, isHex } from "viem";
import { privateKeyToAccount, signTypedData } from "viem/accounts";
import {
Address,
encodeAbiParameters,
Hex,
isAddress,
isHex,
keccak256,
} from "viem";
import { privateKeyToAccount, sign, signatureToHex } from "viem/accounts";
import WebSocket from "isomorphic-ws";
import {
Bid,
@ -11,7 +18,6 @@ import {
BidParams,
BidStatusUpdate,
Opportunity,
EIP712Domain,
OpportunityBid,
OpportunityParams,
TokenAmount,
@ -130,17 +136,6 @@ export class Client {
});
}
private convertEIP712Domain(
eip712Domain: components["schemas"]["EIP712Domain"]
): EIP712Domain {
return {
name: eip712Domain.name,
version: eip712Domain.version,
verifyingContract: checkAddress(eip712Domain.verifying_contract),
chainId: BigInt(eip712Domain.chain_id),
};
}
/**
* Converts an opportunity from the server to the client format
* Returns undefined if the opportunity version is not supported
@ -164,7 +159,6 @@ export class Client {
targetCallValue: BigInt(opportunity.target_call_value),
sellTokens: opportunity.sell_tokens.map(checkTokenQty),
buyTokens: opportunity.buy_tokens.map(checkTokenQty),
eip712Domain: this.convertEIP712Domain(opportunity.eip_712_domain),
};
}
@ -299,49 +293,62 @@ export class Client {
bidParams: BidParams,
privateKey: Hex
): Promise<OpportunityBid> {
const types = {
ExecutionParams: [
{ name: "sellTokens", type: "TokenAmount[]" },
{ name: "buyTokens", type: "TokenAmount[]" },
{ name: "executor", type: "address" },
{ name: "targetContract", type: "address" },
{ name: "targetCalldata", type: "bytes" },
{ name: "targetCallValue", type: "uint256" },
{ name: "validUntil", type: "uint256" },
{ name: "bidAmount", type: "uint256" },
],
TokenAmount: [
{ name: "token", type: "address" },
{ name: "amount", type: "uint256" },
],
};
const account = privateKeyToAccount(privateKey);
const signature = await signTypedData({
privateKey,
domain: {
...opportunity.eip712Domain,
chainId: Number(opportunity.eip712Domain.chainId),
},
types,
primaryType: "ExecutionParams",
message: {
sellTokens: opportunity.sellTokens,
buyTokens: opportunity.buyTokens,
executor: account.address,
targetContract: opportunity.targetContract,
targetCalldata: opportunity.targetCalldata,
targetCallValue: opportunity.targetCallValue,
validUntil: bidParams.validUntil,
bidAmount: bidParams.amount,
},
});
const convertTokenQty = ({ token, amount }: TokenAmount): [Hex, bigint] => [
token,
amount,
];
const payload = encodeAbiParameters(
[
{
name: "repayTokens",
type: "tuple[]",
components: [
{
type: "address",
},
{
type: "uint256",
},
],
},
{
name: "receiptTokens",
type: "tuple[]",
components: [
{
type: "address",
},
{
type: "uint256",
},
],
},
{ name: "contract", type: "address" },
{ name: "calldata", type: "bytes" },
{ name: "value", type: "uint256" },
{ name: "bid", type: "uint256" },
{ name: "validUntil", type: "uint256" },
],
[
opportunity.sellTokens.map(convertTokenQty),
opportunity.buyTokens.map(convertTokenQty),
opportunity.targetContract,
opportunity.targetCalldata,
opportunity.targetCallValue,
bidParams.amount,
bidParams.validUntil,
]
);
const msgHash = keccak256(payload);
const hash = signatureToHex(await sign({ hash: msgHash, privateKey }));
return {
permissionKey: opportunity.permissionKey,
bid: bidParams,
executor: account.address,
signature,
signature: hash,
opportunityId: opportunity.opportunityId,
};
}

View File

@ -10,7 +10,7 @@ export interface paths {
* @description Bid on a specific permission key for a specific chain.
*
* Your bid will be simulated and verified by the server. Depending on the outcome of the auction, a transaction
* containing the contract call will be sent to the blockchain expecting the bid amount to be paid after the call.
* containing the targetContract call will be sent to the blockchain expecting the bid amount to be paid after the call.
*/
post: operations["bid"];
};
@ -58,7 +58,7 @@ export interface components {
amount: string;
/**
* @description The chain id to bid on.
* @example op_sepolia
* @example sepolia
*/
chain_id: string;
/**
@ -67,12 +67,12 @@ export interface components {
*/
permission_key: string;
/**
* @description Calldata for the contract call.
* @description Calldata for the targetContract call.
* @example 0xdeadbeef
*/
target_calldata: string;
/**
* @description The contract address to call.
* @description The targetContract address to call.
* @example 0xcA11bde05977b3631167028862bE2a173976CA11
*/
target_contract: string;
@ -88,28 +88,20 @@ export interface components {
BidStatus:
| {
/** @enum {string} */
type: "pending";
}
| {
/** @enum {string} */
type: "simulation_failed";
status: "pending";
}
| {
/**
* Format: int32
* @example 1
* @description The bid won the auction and was submitted to the chain in a transaction with the given hash
* @example 0x103d4fbd777a36311b5161f2062490f761f25b67406badb2bace62bb170aa4e3
*/
index: number;
/** @example 0x103d4fbd777a36311b5161f2062490f761f25b67406badb2bace62bb170aa4e3 */
result: string;
/** @enum {string} */
type: "submitted";
status: "submitted";
}
| {
/** @example 0x103d4fbd777a36311b5161f2062490f761f25b67406badb2bace62bb170aa4e3 */
result: string;
/** @enum {string} */
type: "lost";
status: "lost";
};
BidStatusWithId: {
bid_status: components["schemas"]["BidStatus"];
@ -148,28 +140,6 @@ export interface components {
ClientRequest: components["schemas"]["ClientMessage"] & {
id: string;
};
EIP712Domain: {
/**
* @description The network chain id parameter for EIP712 domain.
* @example 31337
*/
chain_id: string;
/**
* @description The name parameter for the EIP712 domain.
* @example OpportunityAdapter
*/
name: string;
/**
* @description The verifying contract address parameter for the EIP712 domain.
* @example 0xcA11bde05977b3631167028862bE2a173976CA11
*/
verifying_contract: string;
/**
* @description The version parameter for the EIP712 domain.
* @example 1
*/
version: string;
};
ErrorBodyResponse: {
error: string;
};
@ -192,7 +162,7 @@ export interface components {
/** @example 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12 */
signature: string;
/**
* @description The latest unix timestamp in seconds until which the bid is valid
* @description How long the bid will be valid for.
* @example 1000000000000000000
*/
valid_until: string;
@ -204,14 +174,14 @@ export interface components {
/**
* @description Opportunity parameters needed for on-chain execution
* If a searcher signs the opportunity and have approved enough tokens to opportunity adapter,
* by calling this target contract with the given target calldata and structures, they will
* by calling this target targetContract with the given target targetCalldata and structures, they will
* send the tokens specified in the sell_tokens field and receive the tokens specified in the buy_tokens field.
*/
OpportunityParamsV1: {
buy_tokens: components["schemas"]["TokenAmount"][];
/**
* @description The chain id where the opportunity will be executed.
* @example op_sepolia
* @example sepolia
*/
chain_id: string;
/**
@ -221,17 +191,17 @@ export interface components {
permission_key: string;
sell_tokens: components["schemas"]["TokenAmount"][];
/**
* @description The value to send with the contract call.
* @description The targetCallValue to send with the targetContract call.
* @example 1
*/
target_call_value: string;
/**
* @description Calldata for the target contract call.
* @description Calldata for the target targetContract call.
* @example 0xdeadbeef
*/
target_calldata: string;
/**
* @description The contract address to call for execution of the opportunity.
* @description The targetContract address to call for execution of the opportunity.
* @example 0xcA11bde05977b3631167028862bE2a173976CA11
*/
target_contract: string;
@ -242,11 +212,11 @@ export interface components {
version: "v1";
}) & {
/**
* @description Creation time of the opportunity (in microseconds since the Unix epoch)
* @example 1700000000000000
* Format: int64
* @description Creation time of the opportunity
* @example 1700000000
*/
creation_time: number;
eip_712_domain: components["schemas"]["EIP712Domain"];
/**
* @description The opportunity unique id
* @example obo3ee3e-58cc-4372-a567-0e02b2c3d479
@ -290,7 +260,7 @@ export interface components {
*/
amount: string;
/**
* @description Token contract address
* @description Token targetContract address
* @example 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
*/
token: string;
@ -325,11 +295,11 @@ export interface components {
version: "v1";
}) & {
/**
* @description Creation time of the opportunity (in microseconds since the Unix epoch)
* @example 1700000000000000
* Format: int64
* @description Creation time of the opportunity
* @example 1700000000
*/
creation_time: number;
eip_712_domain: components["schemas"]["EIP712Domain"];
/**
* @description The opportunity unique id
* @example obo3ee3e-58cc-4372-a567-0e02b2c3d479
@ -355,7 +325,7 @@ export interface operations {
* @description Bid on a specific permission key for a specific chain.
*
* Your bid will be simulated and verified by the server. Depending on the outcome of the auction, a transaction
* containing the contract call will be sent to the blockchain expecting the bid amount to be paid after the call.
* containing the targetContract call will be sent to the blockchain expecting the bid amount to be paid after the call.
*/
bid: {
requestBody: {
@ -413,7 +383,7 @@ export interface operations {
get_opportunities: {
parameters: {
query?: {
/** @example op_sepolia */
/** @example sepolia */
chain_id?: string | null;
};
};

View File

@ -23,27 +23,6 @@ export type BidParams = {
*/
validUntil: bigint;
};
/**
* Represents the configuration for signing an opportunity
*/
export type EIP712Domain = {
/**
* The network chain id for the EIP712 domain.
*/
chainId: bigint;
/**
* The verifying contract address for the EIP712 domain.
*/
verifyingContract: Address;
/**
* The name parameter for the EIP712 domain.
*/
name: string;
/**
* The version parameter for the EIP712 domain.
*/
version: string;
};
/**
* Represents a valid opportunity ready to be executed
*/
@ -81,18 +60,11 @@ export type Opportunity = {
* Tokens to receive after the opportunity is executed
*/
buyTokens: TokenAmount[];
/**
* The data required to sign the opportunity
*/
eip712Domain: EIP712Domain;
};
/**
* All the parameters necessary to represent an opportunity
*/
export type OpportunityParams = Omit<
Opportunity,
"opportunityId" | "eip712Domain"
>;
export type OpportunityParams = Omit<Opportunity, "opportunityId">;
/**
* Represents a bid for an opportunity
*/

View File

@ -6,10 +6,13 @@ from typing import Callable, Any
from collections.abc import Coroutine
from uuid import UUID
import httpx
import web3
import websockets
from websockets.client import WebSocketClientProtocol
from eth_abi import encode
from eth_account.account import Account
from express_relay.express_relay_types import (
from web3.auto import w3
from express_relay.types import (
Opportunity,
BidStatusUpdate,
ClientMessage,
@ -319,14 +322,10 @@ class ExpressRelayClient:
elif msg_json.get("type") == "bid_status_update":
if bid_status_callback is not None:
id = msg_json["status"]["id"]
bid_status = msg_json["status"]["bid_status"]["type"]
bid_status = msg_json["status"]["bid_status"]["status"]
result = msg_json["status"]["bid_status"].get("result")
index = msg_json["status"]["bid_status"].get("index")
bid_status_update = BidStatusUpdate(
id=id,
bid_status=BidStatus(bid_status),
result=result,
index=index,
id=id, bid_status=BidStatus(bid_status), result=result
)
asyncio.create_task(bid_status_callback(bid_status_update))
@ -402,66 +401,42 @@ def sign_bid(
Returns:
A OpportunityBid object, representing the transaction to submit to the server. This object contains the searcher's signature.
"""
sell_tokens = [
(token.token, int(token.amount)) for token in opportunity.sell_tokens
]
buy_tokens = [(token.token, int(token.amount)) for token in opportunity.buy_tokens]
target_calldata = bytes.fromhex(opportunity.target_calldata.replace("0x", ""))
executor = Account.from_key(private_key).address
domain_data = {
"name": opportunity.eip_712_domain.name,
"version": opportunity.eip_712_domain.version,
"chainId": opportunity.eip_712_domain.chain_id,
"verifyingContract": opportunity.eip_712_domain.verifying_contract,
}
message_types = {
"ExecutionParams": [
{"name": "sellTokens", "type": "TokenAmount[]"},
{"name": "buyTokens", "type": "TokenAmount[]"},
{"name": "executor", "type": "address"},
{"name": "targetContract", "type": "address"},
{"name": "targetCalldata", "type": "bytes"},
{"name": "targetCallValue", "type": "uint256"},
{"name": "validUntil", "type": "uint256"},
{"name": "bidAmount", "type": "uint256"},
digest = encode(
[
"(address,uint256)[]",
"(address,uint256)[]",
"address",
"bytes",
"uint256",
"uint256",
"uint256",
],
"TokenAmount": [
{"name": "token", "type": "address"},
{"name": "amount", "type": "uint256"},
[
sell_tokens,
buy_tokens,
opportunity.target_contract,
target_calldata,
opportunity.target_call_value,
bid_amount,
valid_until,
],
}
# the data to be signed
message_data = {
"sellTokens": [
{
"token": token.token,
"amount": int(token.amount),
}
for token in opportunity.sell_tokens
],
"buyTokens": [
{
"token": token.token,
"amount": int(token.amount),
}
for token in opportunity.buy_tokens
],
"executor": executor,
"targetContract": opportunity.target_contract,
"targetCalldata": bytes.fromhex(opportunity.target_calldata.replace("0x", "")),
"targetCallValue": opportunity.target_call_value,
"validUntil": valid_until,
"bidAmount": bid_amount,
}
signed_typed_data = Account.sign_typed_data(
private_key, domain_data, message_types, message_data
)
msg_data = web3.Web3.solidity_keccak(["bytes"], [digest])
signature = w3.eth.account.signHash(msg_data, private_key=private_key)
opportunity_bid = OpportunityBid(
opportunity_id=opportunity.opportunity_id,
permission_key=opportunity.permission_key,
amount=bid_amount,
valid_until=valid_until,
executor=executor,
signature=signed_typed_data,
executor=Account.from_key(private_key).address,
signature=signature,
)
return opportunity_bid

View File

@ -3,7 +3,7 @@ import asyncio
import logging
from eth_account.account import Account
from express_relay.client import ExpressRelayClient, sign_bid
from express_relay.express_relay_types import (
from express_relay.types import (
Opportunity,
OpportunityBid,
Bytes32,
@ -76,16 +76,14 @@ class SimpleSearcher:
bid_status = bid_status_update.bid_status
result = bid_status_update.result
result_details = ""
if bid_status == BidStatus("submitted"):
result_details = (
f", transaction {result}, index {bid_status_update.index} of multicall"
)
logger.info(f"Bid {id} has been submitted in hash {result}")
elif bid_status == BidStatus("lost"):
result_details = f", transaction {result}"
logger.error(
f"Bid status for bid {id}: {bid_status.value.replace('_', ' ')}{result_details}"
)
logger.info(f"Bid {id} was unsuccessful")
elif bid_status == BidStatus("pending"):
logger.info(f"Bid {id} is pending")
else:
logger.error(f"Unrecognized status {bid_status} for bid {id}")
async def main():

View File

@ -105,40 +105,26 @@ class BidStatus(Enum):
SUBMITTED = "submitted"
LOST = "lost"
PENDING = "pending"
SIMULATION_FAILED = "simulation_failed"
class BidStatusUpdate(BaseModel):
"""
Attributes:
id: The ID of the bid.
bid_status: The current status of the bid.
result: The result of the bid: a transaction hash if the status is SUBMITTED or LOST, else None.
index: The index of the bid in the submitted transaction; None if the status is not SUBMITTED.
bid_status: The status enum, either SUBMITTED, LOST, or PENDING.
result: The result of the bid: a transaction hash if the status is SUBMITTED, else None.
"""
id: UUIDString
bid_status: BidStatus
result: Bytes32 | None = Field(default=None)
index: int | None = Field(default=None)
@model_validator(mode="after")
def check_result(self):
if self.bid_status in [
BidStatus("pending"),
BidStatus("simulation_failed"),
]:
assert self.result is None, "result must be None"
else:
assert self.result is not None, "result must be a valid 32-byte hash"
return self
@model_validator(mode="after")
def check_index(self):
if self.bid_status == BidStatus("submitted"):
assert self.index is not None, "index must be a valid integer"
assert self.result is not None, "result must be a valid 32-byte hash"
else:
assert self.index is None, "index must be None"
assert self.result is None, "result must be None"
return self
@ -197,13 +183,6 @@ class OpportunityParams(BaseModel):
params: Union[OpportunityParamsV1] = Field(..., discriminator="version")
class EIP712Domain(BaseModel):
name: str
version: str
chain_id: IntString
verifying_contract: Address
class Opportunity(BaseModel):
"""
Attributes:
@ -217,7 +196,6 @@ class Opportunity(BaseModel):
version: The version of the opportunity.
creation_time: The creation time of the opportunity.
opportunity_id: The ID of the opportunity.
eip_712_domain: The EIP712 domain data needed for signing.
"""
target_calldata: HexString
@ -230,7 +208,6 @@ class Opportunity(BaseModel):
version: str
creation_time: IntString
opportunity_id: UUIDString
eip_712_domain: EIP712Domain
supported_versions: ClassVar[list[str]] = ["v1"]

View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
[[package]]
name = "aiohttp"
@ -316,33 +316,33 @@ files = [
[[package]]
name = "black"
version = "24.3.0"
version = "24.2.0"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.8"
files = [
{file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"},
{file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"},
{file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"},
{file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"},
{file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"},
{file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"},
{file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"},
{file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"},
{file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"},
{file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"},
{file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"},
{file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"},
{file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"},
{file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"},
{file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"},
{file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"},
{file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"},
{file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"},
{file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"},
{file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"},
{file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"},
{file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"},
{file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"},
{file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"},
{file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"},
{file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"},
{file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"},
{file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"},
{file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"},
{file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"},
{file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"},
{file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"},
{file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"},
{file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"},
{file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"},
{file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"},
{file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"},
{file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"},
{file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"},
{file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"},
{file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"},
{file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"},
{file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"},
{file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"},
]
[package.dependencies]
@ -1506,13 +1506,13 @@ files = [
[[package]]
name = "referencing"
version = "0.34.0"
version = "0.33.0"
description = "JSON Referencing + Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "referencing-0.34.0-py3-none-any.whl", hash = "sha256:d53ae300ceddd3169f1ffa9caf2cb7b769e92657e4fafb23d34b93679116dfd4"},
{file = "referencing-0.34.0.tar.gz", hash = "sha256:5773bd84ef41799a5a8ca72dc34590c041eb01bf9aa02632b4a973fb0181a844"},
{file = "referencing-0.33.0-py3-none-any.whl", hash = "sha256:39240f2ecc770258f28b642dd47fd74bc8b02484de54e1882b74b35ebd779bd5"},
{file = "referencing-0.33.0.tar.gz", hash = "sha256:c775fedf74bc0f9189c2a3be1c12fd03e8c23f4d371dce795df44e06c5b412f7"},
]
[package.dependencies]

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "express-relay"
version = "0.4.2"
version = "0.2.0"
description = "Utilities for searchers and protocols to interact with the Express Relay protocol."
authors = ["dourolabs"]
license = "Proprietary"

View File

@ -1,4 +1,4 @@
/target
*config.yaml
config.yaml
*secret*
*private-key*

View File

@ -522,9 +522,9 @@ dependencies = [
[[package]]
name = "cargo_metadata"
version = "0.18.1"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037"
checksum = "e7daec1a2a2129eeba1644b220b4647ec537b0b5d4bfd6876fcc5a540056b592"
dependencies = [
"camino",
"cargo-platform",
@ -1031,9 +1031,9 @@ dependencies = [
[[package]]
name = "enr"
version = "0.10.0"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a3d8dc56e02f954cac8eb489772c552c473346fc34f67412bb6244fd647f7e4"
checksum = "fe81b5c06ecfdbc71dd845216f225f53b62a10cb8a16c946836a3467f701d05b"
dependencies = [
"base64 0.21.4",
"bytes",
@ -1146,9 +1146,9 @@ dependencies = [
[[package]]
name = "ethers"
version = "2.0.14"
version = "2.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "816841ea989f0c69e459af1cf23a6b0033b19a55424a1ea3a30099becdb8dec0"
checksum = "1ad13497f6e0a24292fc7b408e30d22fe9dc262da1f40d7b542c3a44e7fc0476"
dependencies = [
"ethers-addressbook",
"ethers-contract",
@ -1162,9 +1162,9 @@ dependencies = [
[[package]]
name = "ethers-addressbook"
version = "2.0.14"
version = "2.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5495afd16b4faa556c3bba1f21b98b4983e53c1755022377051a975c3b021759"
checksum = "c6e9e8acd0ed348403cc73a670c24daba3226c40b98dc1a41903766b3ab6240a"
dependencies = [
"ethers-core",
"once_cell",
@ -1174,9 +1174,9 @@ dependencies = [
[[package]]
name = "ethers-contract"
version = "2.0.14"
version = "2.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fceafa3578c836eeb874af87abacfb041f92b4da0a78a5edd042564b8ecdaaa"
checksum = "d79269278125006bb0552349c03593ffa9702112ca88bc7046cc669f148fb47c"
dependencies = [
"const-hex",
"ethers-contract-abigen",
@ -1193,9 +1193,9 @@ dependencies = [
[[package]]
name = "ethers-contract-abigen"
version = "2.0.14"
version = "2.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04ba01fbc2331a38c429eb95d4a570166781f14290ef9fdb144278a90b5a739b"
checksum = "ce95a43c939b2e4e2f3191c5ad4a1f279780b8a39139c9905b43a7433531e2ab"
dependencies = [
"Inflector",
"const-hex",
@ -1211,15 +1211,15 @@ dependencies = [
"serde",
"serde_json",
"syn 2.0.38",
"toml 0.8.12",
"toml 0.7.8",
"walkdir",
]
[[package]]
name = "ethers-contract-derive"
version = "2.0.14"
version = "2.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87689dcabc0051cde10caaade298f9e9093d65f6125c14575db3fd8c669a168f"
checksum = "8e9ce44906fc871b3ee8c69a695ca7ec7f70e50cb379c9b9cb5e532269e492f6"
dependencies = [
"Inflector",
"const-hex",
@ -1233,9 +1233,9 @@ dependencies = [
[[package]]
name = "ethers-core"
version = "2.0.14"
version = "2.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82d80cc6ad30b14a48ab786523af33b37f28a8623fc06afd55324816ef18fb1f"
checksum = "c0a17f0708692024db9956b31d7a20163607d2745953f5ae8125ab368ba280ad"
dependencies = [
"arrayvec",
"bytes",
@ -1253,7 +1253,7 @@ dependencies = [
"rlp",
"serde",
"serde_json",
"strum 0.26.2",
"strum 0.25.0",
"syn 2.0.38",
"tempfile",
"thiserror",
@ -1263,11 +1263,10 @@ dependencies = [
[[package]]
name = "ethers-etherscan"
version = "2.0.14"
version = "2.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e79e5973c26d4baf0ce55520bd732314328cabe53193286671b47144145b9649"
checksum = "0e53451ea4a8128fbce33966da71132cf9e1040dcfd2a2084fd7733ada7b2045"
dependencies = [
"chrono",
"ethers-core",
"reqwest",
"semver",
@ -1279,9 +1278,9 @@ dependencies = [
[[package]]
name = "ethers-middleware"
version = "2.0.14"
version = "2.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48f9fdf09aec667c099909d91908d5eaf9be1bd0e2500ba4172c1d28bfaa43de"
checksum = "473f1ccd0c793871bbc248729fa8df7e6d2981d6226e4343e3bbaa9281074d5d"
dependencies = [
"async-trait",
"auto_impl",
@ -1306,9 +1305,9 @@ dependencies = [
[[package]]
name = "ethers-providers"
version = "2.0.14"
version = "2.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6434c9a33891f1effc9c75472e12666db2fa5a0fec4b29af6221680a6fe83ab2"
checksum = "6838fa110e57d572336178b7c79e94ff88ef976306852d8cb87d9e5b1fc7c0b5"
dependencies = [
"async-trait",
"auto_impl",
@ -1317,7 +1316,6 @@ dependencies = [
"const-hex",
"enr",
"ethers-core",
"futures-channel",
"futures-core",
"futures-timer",
"futures-util",
@ -1344,9 +1342,9 @@ dependencies = [
[[package]]
name = "ethers-signers"
version = "2.0.14"
version = "2.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "228875491c782ad851773b652dd8ecac62cda8571d3bc32a5853644dd26766c2"
checksum = "5ea44bec930f12292866166f9ddbea6aa76304850e4d8dcd66dc492b43d00ff1"
dependencies = [
"async-trait",
"coins-bip32",
@ -1363,9 +1361,9 @@ dependencies = [
[[package]]
name = "ethers-solc"
version = "2.0.14"
version = "2.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66244a771d9163282646dbeffe0e6eca4dda4146b6498644e678ac6089b11edd"
checksum = "de34e484e7ae3cab99fbfd013d6c5dc7f9013676a4e0e414d8b12e1213e8b3ba"
dependencies = [
"cfg-if",
"const-hex",
@ -1488,7 +1486,7 @@ dependencies = [
[[package]]
name = "fortuna"
version = "5.2.2"
version = "3.3.4"
dependencies = [
"anyhow",
"axum",
@ -1500,7 +1498,6 @@ dependencies = [
"clap",
"ethabi",
"ethers",
"futures",
"hex",
"lazy_static",
"once_cell",
@ -2761,7 +2758,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
dependencies = [
"once_cell",
"toml_edit 0.19.15",
"toml_edit",
]
[[package]]
@ -2822,7 +2819,7 @@ dependencies = [
[[package]]
name = "pythnet-sdk"
version = "2.1.0"
version = "2.0.0"
dependencies = [
"bincode",
"borsh",
@ -3390,9 +3387,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "0.6.5"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
dependencies = [
"serde",
]
@ -3584,9 +3581,9 @@ dependencies = [
[[package]]
name = "solang-parser"
version = "0.3.3"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c425ce1c59f4b154717592f0bdf4715c3a1d55058883622d3157e1f0908a5b26"
checksum = "7cb9fa2fa2fa6837be8a2495486ff92e3ffe68a99b6eeba288e139efdd842457"
dependencies = [
"itertools 0.11.0",
"lalrpop",
@ -3648,11 +3645,11 @@ dependencies = [
[[package]]
name = "strum"
version = "0.26.2"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
dependencies = [
"strum_macros 0.26.2",
"strum_macros 0.25.2",
]
[[package]]
@ -3670,9 +3667,9 @@ dependencies = [
[[package]]
name = "strum_macros"
version = "0.26.2"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059"
dependencies = [
"heck",
"proc-macro2",
@ -3958,21 +3955,21 @@ dependencies = [
[[package]]
name = "toml"
version = "0.8.12"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3"
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.22.9",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.5"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
dependencies = [
"serde",
]
@ -3982,23 +3979,12 @@ name = "toml_edit"
version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap 2.0.2",
"toml_datetime",
"winnow 0.5.16",
]
[[package]]
name = "toml_edit"
version = "0.22.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4"
dependencies = [
"indexmap 2.0.2",
"serde",
"serde_spanned",
"toml_datetime",
"winnow 0.6.5",
"winnow",
]
[[package]]
@ -4526,15 +4512,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "winnow"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8"
dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.50.0"

View File

@ -1,6 +1,6 @@
[package]
name = "fortuna"
version = "5.2.2"
version = "3.3.4"
edition = "2021"
[dependencies]
@ -12,11 +12,10 @@ bincode = "1.3.3"
byteorder = "1.5.0"
clap = { version = "4.4.6", features = ["derive", "cargo", "env"] }
ethabi = "18.0.0"
ethers = { version = "2.0.14", features = ["ws"] }
futures = { version = "0.3.28" }
hex = "0.4.3"
ethers = "2.0.10"
hex = "0.4.3"
prometheus-client = { version = "0.21.2" }
pythnet-sdk = { path = "../../pythnet/pythnet_sdk", features = ["strum"] }
pythnet-sdk = { path = "../pythnet/pythnet_sdk", features = ["strum"] }
rand = "0.8.5"
reqwest = { version = "0.11.22", features = ["json", "blocking"] }
serde = { version = "1.0.188", features = ["derive"] }
@ -35,6 +34,5 @@ once_cell = "1.18.0"
lazy_static = "1.4.0"
url = "2.5.0"
[dev-dependencies]
axum-test = "13.1.1"

View File

@ -7,15 +7,15 @@ RUN rustup default nightly-2023-07-23
# Build
WORKDIR /src
COPY apps/fortuna apps/fortuna
COPY fortuna fortuna
COPY pythnet pythnet
COPY target_chains/ethereum/entropy_sdk/solidity/abis target_chains/ethereum/entropy_sdk/solidity/abis
WORKDIR /src/apps/fortuna
WORKDIR /src/fortuna
RUN --mount=type=cache,target=/root/.cargo/registry cargo build --release
FROM rust:${RUST_VERSION}
# Copy artifacts from other images
COPY --from=build /src/apps/fortuna/target/release/fortuna /usr/local/bin/
COPY --from=build /src/fortuna/target/release/fortuna /usr/local/bin/

View File

@ -4,4 +4,3 @@ chains:
contract_addr: 0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a
reveal_delay_blocks: 0
legacy_tx: true
gas_limit: 500000

View File

@ -73,8 +73,6 @@ impl ApiState {
/// The state of the randomness service for a single blockchain.
#[derive(Clone)]
pub struct BlockchainState {
/// The chain id for this blockchain, useful for logging
pub id: ChainId,
/// The hash chain(s) required to serve random numbers for this blockchain
pub state: Arc<HashChainState>,
/// The contract that the server is fulfilling requests for.
@ -247,7 +245,6 @@ mod test {
let eth_read = Arc::new(MockEntropyReader::with_requests(10, &[]));
let eth_state = BlockchainState {
id: "ethereum".into(),
state: ETH_CHAIN.clone(),
contract: eth_read.clone(),
provider_address: PROVIDER,
@ -258,7 +255,6 @@ mod test {
let avax_read = Arc::new(MockEntropyReader::with_requests(10, &[]));
let avax_state = BlockchainState {
id: "avalanche".into(),
state: AVAX_CHAIN.clone(),
contract: avax_read.clone(),
provider_address: PROVIDER,

Some files were not shown because too many files have changed in this diff Show More