Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Alloy migration #10

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
2,737 changes: 1,038 additions & 1,699 deletions Cargo.lock

Large diffs are not rendered by default.

27 changes: 24 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,35 @@ repository = "https://github.com/keep-starknet-strange/zaun/"
version = "0.1.0"

[workspace.dependencies]
ethers = { git = "https://github.com/gakonst/ethers-rs", rev = "f0e5b194f09c533feb10d1a686ddb9e5946ec107" }
log = "0.4.20"
thiserror = "1.0.51"
num-traits = "0.2.17"
async-trait = "0.1.74"
dirs = "5.0.1"
serde_json = "1.0.108"
hex = "0.4.3"
alloy = { git = "https://github.com/alloy-rs/alloy", rev = "66fa192", features = [
"network",
"providers",
"rpc-types-eth",
"sol-types",
"contract",
"rpc",
"rpc-client",
"signers",
"signer-wallet",
"transport-http",
"node-bindings",
] }
reqwest = { version = "0.12", default-features = false }
url = "2.4.1"
tokio = "1"



[patch.crates-io]
alloy-core = { git = "https://github.com/alloy-rs/core", rev = "525a233" }
alloy-dyn-abi = { git = "https://github.com/alloy-rs/core", rev = "525a233" }
alloy-json-abi = { git = "https://github.com/alloy-rs/core", rev = "525a233" }
alloy-primitives = { git = "https://github.com/alloy-rs/core", rev = "525a233" }
alloy-sol-macro = { git = "https://github.com/alloy-rs/core", rev = "525a233" }
alloy-sol-types = { git = "https://github.com/alloy-rs/core", rev = "525a233" }
syn-solidity = { git = "https://github.com/alloy-rs/core", rev = "525a233" }
4 changes: 3 additions & 1 deletion crates/sandbox/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ authors.workspace = true

[dependencies]
starknet-core-contract-client = { path = "../starknet-core-contract-client" }
ethers = { workspace = true }
dirs = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
hex = { workspace = true }
alloy = { workspace = true }
url = { workspace = true }
reqwest = { workspace = true }

[dev-dependencies]
tokio = { version = "1.29.1", features = ["rt", "macros", "parking_lot"] }
108 changes: 30 additions & 78 deletions crates/sandbox/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
use ethers::abi::Tokenize;
use ethers::contract::ContractError;
use ethers::prelude::SignerMiddleware;
use ethers::prelude::{ContractFactory, ContractInstance};
use ethers::providers::{Http, Provider, ProviderError};
use ethers::signers::{LocalWallet, Signer};
use ethers::types::Bytes;
use ethers::utils::hex::FromHex;
use ethers::utils::{Anvil, AnvilInstance};
use url::Url;
use alloy::{
network::EthereumSigner, node_bindings::{Anvil, AnvilInstance}, providers::ProviderBuilder, rpc::client::RpcClient, signers::{
wallet::{LocalWallet, WalletError},
Signer,
}, transports::TransportError
};

use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;

/// Unsafe proxy is a straightforward implementation of the delegate proxy contract
/// that is used to make Starknet core contract upgradeable.
Expand All @@ -23,7 +21,7 @@ pub mod unsafe_proxy;
pub use starknet_core_contract_client::LocalWalletSignerMiddleware;

/// Sandbox is typically used for E2E scenarios so we need to speed things up
const POLLING_INTERVAL_MS: u64 = 10;
// const POLLING_INTERVAL_MS: u64 = 10;
const ANVIL_DEFAULT_ENDPOINT: &str = "http://127.0.0.1:8545";
const ANVIL_DEFAULT_CHAIN_ID: u64 = 31337;
const ANVIL_DEFAULT_PRIVATE_KEY: &str =
Expand All @@ -37,14 +35,14 @@ pub enum Error {
BytecodeObject,
#[error(transparent)]
Hex(#[from] hex::FromHexError),
#[error("Failed to parse URL")]
UrlParser,
#[error(transparent)]
EthersContract(#[from] ContractError<LocalWalletSignerMiddleware>),
#[error("Failed to parse provider URL: {0}")]
ProviderUrlParse(#[source] url::ParseError),
#[error(transparent)]
EthersProvider(#[from] ProviderError),
EthersProvider(#[from] TransportError),
#[error("Invalid contract build artifacts: missing field `{0}`")]
ContractBuildArtifacts(&'static str),
#[error("Failed to parse private key: {0}")]
PrivateKeyParse(#[source] WalletError),
}

/// A convenient wrapper over an already running or spawned Anvil local devnet
Expand All @@ -71,21 +69,15 @@ impl EthereumSandbox {
.unwrap_or_else(|| ANVIL_DEFAULT_ENDPOINT.into())
});

let provider = Provider::<Http>::try_from(anvil_endpoint)
.map_err(|_| Error::UrlParser)?
.interval(Duration::from_millis(POLLING_INTERVAL_MS));

let wallet: LocalWallet = ANVIL_DEFAULT_PRIVATE_KEY
.parse()
.expect("Failed to parse private key");
let client = SignerMiddleware::new(
provider.clone(),
wallet.with_chain_id(ANVIL_DEFAULT_CHAIN_ID),
);

let wallet: LocalWallet = String::from(ANVIL_DEFAULT_PRIVATE_KEY).parse::<LocalWallet>().map_err(Error::PrivateKeyParse)?;
let wallet = wallet.with_chain_id(Some(ANVIL_DEFAULT_CHAIN_ID));
let rpc_client = RpcClient::new_http(Url::parse(&anvil_endpoint).map_err(Error::ProviderUrlParse)?);
let provider_with_signer = ProviderBuilder::new()
.signer(EthereumSigner::from(wallet))
.on_client(rpc_client);
Ok(Self {
_anvil: None,
client: Arc::new(client),
client: Arc::new(provider_with_signer),
})
}

Expand All @@ -94,7 +86,7 @@ impl EthereumSandbox {
/// - `anvil_path` parameter (if specified)
/// - ${ANVIL_PATH} environment variable (if set)
/// - ~/.foundry/bin/anvil (default)
pub fn spawn(anvil_path: Option<PathBuf>) -> Self {
pub fn spawn(anvil_path: Option<PathBuf>) -> Result<Self, Error> {
let anvil_path: PathBuf = anvil_path.unwrap_or_else(|| {
std::env::var("ANVIL_PATH")
.map(Into::into)
Expand All @@ -104,60 +96,20 @@ impl EthereumSandbox {

// Will panic if invalid path
let anvil = Anvil::at(anvil_path).spawn();
let wallet: LocalWallet = anvil.keys()[0].clone().try_into().expect("Failed to parse private key");
let rpc_client = RpcClient::new_http(Url::parse(&anvil.endpoint()).map_err(Error::ProviderUrlParse)?);
let provider_with_signer = ProviderBuilder::new()
.signer(EthereumSigner::from(wallet))
.on_client(rpc_client);

let provider = Provider::<Http>::try_from(anvil.endpoint())
.expect("Failed to connect to Anvil")
.interval(Duration::from_millis(POLLING_INTERVAL_MS));

let wallet: LocalWallet = anvil.keys()[0].clone().into();
let client =
SignerMiddleware::new(provider.clone(), wallet.with_chain_id(anvil.chain_id()));

Self {
Ok(Self {
_anvil: Some(anvil),
client: Arc::new(client),
}
client: Arc::new(provider_with_signer),
})
}

/// Returns local client configured for the running Anvil instance
pub fn client(&self) -> Arc<LocalWalletSignerMiddleware> {
self.client.clone()
}
}

/// Deploys new smart contract using:
/// - Forge build artifacts (JSON file contents)
/// - Constructor args (use () if no args expected)
pub async fn deploy_contract<T: Tokenize>(
client: Arc<LocalWalletSignerMiddleware>,
contract_build_artifacts: &str,
contructor_args: T,
) -> Result<ContractInstance<Arc<LocalWalletSignerMiddleware>, LocalWalletSignerMiddleware>, Error>
{
let (abi, bytecode) = {
let mut artifacts: serde_json::Value = serde_json::from_str(contract_build_artifacts)?;
let abi_value = artifacts
.get_mut("abi")
.ok_or_else(|| Error::ContractBuildArtifacts("abi"))?
.take();
let bytecode_value = artifacts
.get_mut("bytecode")
.ok_or_else(|| Error::ContractBuildArtifacts("bytecode"))?
.get_mut("object")
.ok_or_else(|| Error::ContractBuildArtifacts("bytecode.object"))?
.take();

let abi = serde_json::from_value(abi_value)?;
let bytecode = Bytes::from_hex(bytecode_value.as_str().ok_or(Error::BytecodeObject)?)?;
(abi, bytecode)
};

let factory = ContractFactory::new(abi, bytecode, client.clone());

Ok(factory
.deploy(contructor_args)
.map_err(Into::<ContractError<LocalWalletSignerMiddleware>>::into)?
.send()
.await
.map_err(Into::<ContractError<LocalWalletSignerMiddleware>>::into)?)
}
72 changes: 38 additions & 34 deletions crates/sandbox/src/unsafe_proxy.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,46 @@
use std::sync::Arc;

use starknet_core_contract_client::clients::StarknetSovereignContractClient;
use crate::{Error, LocalWalletSignerMiddleware};
use alloy::{
providers::Provider, sol,
};

use crate::{deploy_contract, Error, LocalWalletSignerMiddleware};
sol! {
#[allow(missing_docs)]
#[sol(rpc)]
StarknetSovereign,
"artifacts/Starknet.json"
}

const STARKNET_SOVEREIGN: &str = include_str!("../artifacts/Starknet.json");
const UNSAFE_PROXY: &str = include_str!("../artifacts/UnsafeProxy.json");
sol! {
#[allow(missing_docs)]
#[sol(rpc)]
UnsafeProxy,
"artifacts/UnsafeProxy.json"
}

/// Deploy Starknet sovereign contract and unsafe proxy for it.
/// Cached forge atrifacts are used for deployment, make sure they are up to date.
pub async fn deploy_starknet_sovereign_behind_unsafe_proxy(
client: Arc<LocalWalletSignerMiddleware>,
) -> Result<StarknetSovereignContractClient, Error> {
// First we deploy the Starknet core contract (no explicit contructor)
let core_contract = deploy_contract(client.clone(), STARKNET_SOVEREIGN, ()).await?;
let base_fee = client.as_ref().get_gas_price().await?;

// First we deploy the Starknet core contract (no explicit contructor)
let core_contract_builder = StarknetSovereign::deploy_builder(&client);
let estimate = core_contract_builder.estimate_gas().await.unwrap();
let core_contract_address = core_contract_builder.gas_price(base_fee).gas(estimate).nonce(0).deploy().await;
// Once we know the Starknet core contract address (implementation address)
// we can deploy and initialize our delegate proxy.
// NOTE that real world proxies typically allow changing the implementation
// address dynamically (this is basically how upgrades work). In our case,
// for simplicity, the proxy is initialized only once during the deployment.
let proxy_contract =
deploy_contract(client.clone(), UNSAFE_PROXY, core_contract.address()).await?;
let proxy_contract_builder = UnsafeProxy::deploy_builder(&client, core_contract_address.unwrap());
let estimate = proxy_contract_builder.estimate_gas().await.unwrap();
let proxy_contract_address = proxy_contract_builder.gas_price(base_fee).gas(estimate).nonce(1).deploy().await;

Ok(StarknetSovereignContractClient::new(
proxy_contract.address(),
proxy_contract_address.unwrap(),
client.clone(),
))
}
Expand All @@ -33,47 +49,35 @@ pub async fn deploy_starknet_sovereign_behind_unsafe_proxy(
mod tests {
use super::deploy_starknet_sovereign_behind_unsafe_proxy;
use crate::EthereumSandbox;
use alloy::{contract::Error, primitives::U256, providers::Provider, rpc::types::eth::TransactionReceipt, transports::{RpcError, TransportErrorKind}};
use starknet_core_contract_client::{
interfaces::{
CoreContractInitData, OperatorTrait, ProxyInitializeData, ProxySupportTrait,
StarknetSovereignContractTrait,
},
StarknetCoreContractClient,
CoreContractInitData, OperatorTrait, ProxyInitializeData, ProxySupportTrait, StarknetSovereignContractTrait
}, StarknetCoreContractClient
};

#[tokio::test]
async fn test_starknet_sovereign_contract_initialized_in_anvil() {
let sandbox = EthereumSandbox::spawn(None);
let starknet = deploy_starknet_sovereign_behind_unsafe_proxy(sandbox.client())
let sandbox_ref = sandbox.as_ref().clone();
let starknet = deploy_starknet_sovereign_behind_unsafe_proxy(sandbox_ref.unwrap().client())
.await
.expect("Failed to deploy");

let data = ProxyInitializeData::<0> {
sub_contract_addresses: [],
eic_address: Default::default(),
init_data: CoreContractInitData {
program_hash: 1u64.into(), // zero program hash would be deemed invalid
program_hash: U256::from(1_u64), // zero program hash would be deemed invalid
..Default::default()
},
};

// Initialize state & governance
starknet
.initialize_with(data)
.await
.expect("Failed to initialize");

// Register as operator
starknet
.register_operator(starknet.client().address())
.await
.expect("Failed to register as operator");

// Check that contract is initialized
let program_hash = starknet
.program_hash()
.await
.expect("Failed to query program hash");
assert_eq!(program_hash, 1u64.into());
let _init: Result<TransactionReceipt, RpcError<TransportErrorKind>> = starknet.initialize(data.into()).await;

let _register: Result<TransactionReceipt, RpcError<TransportErrorKind>> = starknet.register_operator(starknet.client().get_accounts().await.unwrap()[0]).await;

let program_hash: Result<U256, Error> = starknet.program_hash().await;

assert_eq!(program_hash.unwrap(), U256::from(1_u64));
}
}
6 changes: 5 additions & 1 deletion crates/starknet-core-contract-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ version.workspace = true
authors.workspace = true

[dependencies]
ethers = { workspace = true }
log = { workspace = true }
thiserror = { workspace = true }
num-traits = { workspace = true }
async-trait = { workspace = true }
alloy = { workspace = true }
reqwest = { workspace = true, features = ["blocking", "json"] }

[dev-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
Loading