diff --git a/Cargo.lock b/Cargo.lock
index 98a8720b..25ebf3f9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4452,6 +4452,7 @@ dependencies = [
"serde_json",
"strum 0.26.1",
"thiserror",
+ "tokio",
]
[[package]]
diff --git a/bin/rundler/src/cli/mod.rs b/bin/rundler/src/cli/mod.rs
index 1564f84c..005f9dcf 100644
--- a/bin/rundler/src/cli/mod.rs
+++ b/bin/rundler/src/cli/mod.rs
@@ -32,7 +32,7 @@ use rundler_rpc::{EthApiSettings, RundlerApiSettings};
use rundler_sim::{
EstimationSettings, PrecheckSettings, PriorityFeeMode, SimulationSettings, MIN_CALL_GAS_LIMIT,
};
-use rundler_types::hybrid_compute;
+use rundler_types::{contracts::v0_7::hc_helper::HCHelper, hybrid_compute};
/// Main entry point for the CLI
///
@@ -53,19 +53,26 @@ pub async fn run() -> anyhow::Result<()> {
let cs = chain_spec::resolve_chain_spec(&opt.common.network, &opt.common.chain_spec);
tracing::info!("Chain spec: {:#?}", cs);
+ let node_http = opt
+ .common
+ .node_http
+ .clone()
+ .expect("must provide node_http");
+ let p2 = rundler_provider::new_provider(&node_http, None)?;
+ let hx = HCHelper::new(opt.common.hc_helper_addr, p2);
+ let slot_idx = hx
+ .response_slot()
+ .await
+ .expect("Failed to get ResponseSlot");
hybrid_compute::init(
opt.common.hc_helper_addr,
opt.common.hc_sys_account,
opt.common.hc_sys_owner,
opt.common.hc_sys_privkey,
- //opt.common.entry_points[0].parse::
().expect("Must provide an entry_point"),
- cs.entry_point_address_v0_6,
- cs.id,
- opt.common
- .node_http
- .clone()
- .expect("Must provide node_http"),
+ cs.clone(),
+ node_http,
+ slot_idx,
);
match opt.command {
diff --git a/crates/builder/src/bundle_proposer.rs b/crates/builder/src/bundle_proposer.rs
index 25519364..12a8c07d 100644
--- a/crates/builder/src/bundle_proposer.rs
+++ b/crates/builder/src/bundle_proposer.rs
@@ -530,6 +530,7 @@ where
let mut gas_spent = self.settings.chain_spec.transaction_intrinsic_gas;
let mut cleanup_keys: Vec = Vec::new();
let mut constructed_bundle_size = BUNDLE_BYTE_OVERHEAD;
+
for (po, simulation) in ops_with_simulations {
let op = po.clone().uo;
let simulation = match simulation {
@@ -637,7 +638,11 @@ where
if hc_ent.is_some() {
gas_spent += hc_ent.clone().unwrap().oc_gas;
//println!("HC insert, hc_ent {:?}", hc_ent);
- let u_op2: UserOperationVariant = hc_ent.clone().unwrap().user_op.into();
+ let u_op2: UserOperationVariant = hc_ent
+ .clone()
+ .unwrap()
+ .user_op
+ .into_variant(&self.settings.chain_spec);
let sim_result = self
.simulator
@@ -679,16 +684,23 @@ where
.get_nonce(cfg.sys_account, U256::zero())
.await
.unwrap();
- let cleanup_op: UserOperationVariant =
- hybrid_compute::rr_op(&cfg, c_nonce, cleanup_keys)
- .await
- .into();
+ let is_v7 =
+ self.entry_point.address() == self.settings.chain_spec.entry_point_address_v0_7;
+ let cleanup_op: UserOperationVariant = hybrid_compute::rr_op(
+ &cfg,
+ self.entry_point.address(),
+ c_nonce,
+ cleanup_keys,
+ is_v7,
+ )
+ .await
+ .into_variant(&self.settings.chain_spec);
let cleanup_sim = self
.simulator
.simulate_validation(cleanup_op.clone().into(), None, None)
.await
- .expect("Failed to unwrap sim_result"); // FIXME
+ .expect("Failed to unwrap sim_result");
context
.groups_by_aggregator
diff --git a/crates/provider/src/ethers/provider.rs b/crates/provider/src/ethers/provider.rs
index 7242feb0..b17f24ce 100644
--- a/crates/provider/src/ethers/provider.rs
+++ b/crates/provider/src/ethers/provider.rs
@@ -141,10 +141,10 @@ impl Provider for EthersProvider {
block_id: Option,
trace_options: GethDebugTracingCallOptions,
) -> ProviderResult {
- println!(
- "HC debug_trace_call overrides {:?} tx {:?}",
- trace_options.state_overrides, tx
- );
+ //println!(
+ // "HC debug_trace_call overrides {:?}",
+ // trace_options.state_overrides
+ //);
println!("HC will use BlockNumber::Latest instead of {:?}", block_id);
let ret = Middleware::debug_trace_call(
self,
@@ -153,7 +153,7 @@ impl Provider for EthersProvider {
trace_options,
)
.await;
- println!("HC debug_trace_call ret {:?}", ret);
+ //println!("HC debug_trace_call ret {:?}", ret);
Ok(ret?)
}
diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs
index 12466dcc..c38c25b3 100644
--- a/crates/rpc/src/eth/api.rs
+++ b/crates/rpc/src/eth/api.rs
@@ -24,7 +24,7 @@ use jsonrpsee::{
};
use rundler_types::{
chain::ChainSpec,
- contracts::v0_6::{hc_helper::HCHelper as HH2, simple_account::SimpleAccount},
+ contracts::{v0_6::simple_account::SimpleAccount, v0_7::hc_helper::HCHelper},
hybrid_compute,
pool::Pool,
UserOperation, UserOperationOptionalGas, UserOperationVariant,
@@ -156,22 +156,19 @@ where
// max_simulate_handle_ops_gas: 0,
// validation_estimation_gas_fee: 0,
//};
- let op6: rundler_types::v0_6::UserOperationOptionalGas = op.clone().into();
- let hh = op6
- .clone()
- .into_user_operation(U256::from(0), U256::from(0))
- .hc_hash();
+ let hh = op.hc_hash();
println!("HC api.rs hh {:?}", hh);
+ let op_t: UserOperationVariant = op.into_variant(&self.chain_spec);
- let ep_addr = hybrid_compute::hc_ep_addr(revert_data);
+ let ep_addr = hybrid_compute::hc_ha_addr(revert_data);
- let n_key: U256 = op6.nonce >> 64;
- let at_price = op6.max_priority_fee_per_gas;
- //let hc_nonce = context.gas_estimator.entry_point.get_nonce(op6.sender, n_key).await.unwrap();
+ let n_key: U256 = op_t.nonce() >> 64;
+ let at_price = Some(op_t.max_priority_fee_per_gas());
+ //let hc_nonce = context.gas_estimator.entry_point.get_nonce(op_t.sender(), n_key).await.unwrap();
let hc_nonce = self
.router
- .get_nonce(&entry_point, op6.sender, n_key)
+ .get_nonce(&entry_point, op_t.sender(), n_key)
.await
.unwrap();
@@ -182,11 +179,14 @@ where
.unwrap();
println!(
"HC hc_nonce {:?} err_nonce {:?} op_nonce {:?} n_key {:?}",
- hc_nonce, err_nonce, op6.nonce, n_key
+ hc_nonce,
+ err_nonce,
+ op_t.nonce(),
+ n_key
);
let p2 = rundler_provider::new_provider(&self.hc.node_http, None)?;
- let hx = HH2::new(self.hc.helper_addr, p2.clone());
+ let hx = HCHelper::new(self.hc.helper_addr, p2.clone());
let url = hx.registered_callers(ep_addr).await.expect("url_decode").1;
println!("HC registered_caller url {:?}", url);
@@ -206,9 +206,10 @@ where
let payload = hex::encode(hybrid_compute::hc_req_payload(revert_data));
let n_bytes: [u8; 32] = (hc_nonce).into();
let src_n = hex::encode(n_bytes);
- let src_addr = hex::encode(op6.sender);
+ let src_addr = hex::encode(op_t.sender());
+
+ let oo_n_key: U256 = U256::from_big_endian(op_t.sender().as_fixed_bytes());
- let oo_n_key: U256 = U256::from_big_endian(op6.sender.as_fixed_bytes());
let oo_nonce = self
.router
.get_nonce(&entry_point, ep_addr, oo_n_key)
@@ -222,10 +223,31 @@ where
"Failed to look up HybridAccount owner"
)));
}
- const REQ_VERSION: &str = "0.2";
+ // This version parameter tells the offchain RPC which version of the
+ // AA contracts are being used. This affects the hashing algorithm needed
+ // to generate an offchain signature.
+ const REQ_VERSION_V6: &str = "0.2";
+ const REQ_VERSION_V7: &str = "0.3";
+
+ let is_v7 = match self.router.get_ep_version(&entry_point)? {
+ rundler_types::EntryPointVersion::V0_7 => true,
+ rundler_types::EntryPointVersion::V0_6 => false,
+ rundler_types::EntryPointVersion::Unspecified => {
+ return Err(EthRpcError::Internal(anyhow::anyhow!(
+ "HC04: Unknown EntryPoint version"
+ )))
+ }
+ };
let mut params = ObjectParams::new();
- let _ = params.insert("ver", REQ_VERSION);
+ let _ = params.insert(
+ "ver",
+ if is_v7 {
+ REQ_VERSION_V7
+ } else {
+ REQ_VERSION_V6
+ },
+ );
let _ = params.insert("sk", sk_hex);
let _ = params.insert("src_addr", src_addr);
let _ = params.insert("src_nonce", src_n);
@@ -250,11 +272,11 @@ where
let resp_hex = resp["response"].as_str().unwrap();
let sig_hex: String = resp["signature"].as_str().unwrap().into();
let hc_res: Bytes = hex::decode(resp_hex).unwrap().into();
- //println!("HC api.rs do_op result sk {:?} success {:?} res {:?}", sub_key, op_success, hc_res);
err_hc = hybrid_compute::external_op(
+ entry_point,
hh,
- op6.sender,
+ op_t.sender(),
hc_nonce,
op_success,
&hc_res,
@@ -266,6 +288,7 @@ where
&self.hc,
ha_owner.unwrap(),
err_nonce,
+ is_v7,
)
.await;
} else {
@@ -329,11 +352,12 @@ where
entry_point,
err_hc.clone(),
sub_key,
- op6.sender,
+ op_t.sender(),
hc_nonce,
err_nonce,
map_key,
&self.hc,
+ is_v7,
)
.await;
}
@@ -349,31 +373,19 @@ where
println!("HC api.rs Ok gas result2 = {:?}", result2);
let r3a = result2.unwrap();
match r3a {
- RpcGasEstimate::V0_6(abc) => {
- r3 = abc;
+ RpcGasEstimate::V0_6(est) => {
+ r3 = est;
}
- _ => {
- return Err(EthRpcError::Internal(anyhow::anyhow!(
- "HC04 user_op gas estimation failed"
- )));
+ RpcGasEstimate::V0_7(est) => {
+ r3 = RpcGasEstimateV0_6 {
+ pre_verification_gas: est.pre_verification_gas,
+ call_gas_limit: est.call_gas_limit,
+ verification_gas_limit: est.verification_gas_limit,
+ };
}
}
- let op_tmp = hybrid_compute::get_hc_ent(hh).unwrap().user_op;
- let op_tmp_2: rundler_types::v0_6::UserOperationOptionalGas =
- rundler_types::v0_6::UserOperationOptionalGas {
- sender: op_tmp.sender,
- nonce: op_tmp.nonce,
- init_code: op_tmp.init_code,
- call_data: op_tmp.call_data,
- call_gas_limit: Some(op_tmp.call_gas_limit),
- verification_gas_limit: Some(op_tmp.verification_gas_limit),
- pre_verification_gas: Some(op_tmp.pre_verification_gas),
- max_fee_per_gas: Some(op_tmp.max_fee_per_gas),
- max_priority_fee_per_gas: Some(op_tmp.max_priority_fee_per_gas),
- paymaster_and_data: op_tmp.paymaster_and_data,
- signature: op_tmp.signature,
- };
+ let op_tmp_2 = hybrid_compute::get_hc_ent(hh).unwrap().user_op;
// The op_tmp_2 below specifies a 0 gas price, but we need to estimate the L1 fee at the
// price offered by real userOperation which will be paying for it.
@@ -382,7 +394,7 @@ where
.router
.estimate_gas(
&entry_point,
- rundler_types::UserOperationOptionalGas::V0_6(op_tmp_2.clone()),
+ op_tmp_2.clone(),
Some(spoof::State::default()),
at_price,
)
@@ -396,17 +408,17 @@ where
};
let r2: RpcGasEstimateV0_6 = match r2a? {
- RpcGasEstimate::V0_6(estimate) => estimate,
- _ => {
- return Err(EthRpcError::Internal(anyhow::anyhow!(
- "HC04 offchain_op gas estimation failed"
- )));
- }
+ RpcGasEstimate::V0_6(est) => est,
+ RpcGasEstimate::V0_7(est) => RpcGasEstimateV0_6 {
+ pre_verification_gas: est.pre_verification_gas,
+ call_gas_limit: est.call_gas_limit,
+ verification_gas_limit: est.verification_gas_limit,
+ },
};
// The current formula used to estimate gas usage in the offchain_rpc service
// sometimes underestimates the true cost. For now all we can do is error here.
- if r2.call_gas_limit > op_tmp_2.call_gas_limit.unwrap() {
+ if r2.call_gas_limit > op_tmp_2.into_variant(&self.chain_spec).call_gas_limit() {
println!("HC op_tmp_2 failed, call_gas_limit too low");
let msg = "HC04: Offchain call_gas_limit too low".to_string();
return Err(EthRpcError::Internal(anyhow::anyhow!(msg)));
@@ -421,38 +433,28 @@ where
.get_nonce(&entry_point, self.hc.sys_account, U256::zero())
.await
.unwrap();
- let cleanup_op = hybrid_compute::rr_op(&self.hc, c_nonce, cleanup_keys.clone()).await;
- let op_tmp_4: rundler_types::v0_6::UserOperationOptionalGas =
- rundler_types::v0_6::UserOperationOptionalGas {
- sender: cleanup_op.sender,
- nonce: cleanup_op.nonce,
- init_code: cleanup_op.init_code,
- call_data: cleanup_op.call_data,
- call_gas_limit: Some(cleanup_op.call_gas_limit),
- verification_gas_limit: Some(cleanup_op.verification_gas_limit),
- pre_verification_gas: Some(cleanup_op.pre_verification_gas),
- max_fee_per_gas: Some(cleanup_op.max_fee_per_gas),
- max_priority_fee_per_gas: Some(cleanup_op.max_priority_fee_per_gas),
- paymaster_and_data: cleanup_op.paymaster_and_data,
- signature: cleanup_op.signature,
- };
- println!("HC op_tmp_4 {:?} {:?}", op_tmp_4, cleanup_keys);
+ let cleanup_op =
+ hybrid_compute::rr_op(&self.hc, entry_point, c_nonce, cleanup_keys.clone(), is_v7)
+ .await;
+
+ println!("HC cleanup_op {:?} {:?}", cleanup_op, cleanup_keys);
let r4a = self
.router
.estimate_gas(
&entry_point,
- rundler_types::UserOperationOptionalGas::V0_6(op_tmp_4),
+ // rundler_types::UserOperationOptionalGas::V0_6(op_tmp_4),
+ cleanup_op,
Some(spoof::State::default()),
at_price,
)
.await;
let r4: RpcGasEstimateV0_6 = match r4a? {
- RpcGasEstimate::V0_6(estimate) => estimate,
- _ => {
- return Err(EthRpcError::Internal(anyhow::anyhow!(
- "HC04 cleanup_op gas estimation failed"
- )));
- }
+ RpcGasEstimate::V0_6(est) => est,
+ RpcGasEstimate::V0_7(est) => RpcGasEstimateV0_6 {
+ pre_verification_gas: est.pre_verification_gas,
+ call_gas_limit: est.call_gas_limit,
+ verification_gas_limit: est.verification_gas_limit,
+ },
};
let cleanup_gas =
diff --git a/crates/rpc/src/eth/router.rs b/crates/rpc/src/eth/router.rs
index d2a7146e..cf0dbf2d 100644
--- a/crates/rpc/src/eth/router.rs
+++ b/crates/rpc/src/eth/router.rs
@@ -198,7 +198,6 @@ impl EntryPointRouter {
.get_nonce(addr, key)
.await
.map_err(Into::into)
- //Ok(U256::from(0))
}
pub(crate) async fn check_signature(
@@ -213,7 +212,7 @@ impl EntryPointRouter {
.map_err(Into::into)
}
- fn get_ep_version(&self, entry_point: &Address) -> EthResult {
+ pub(crate) fn get_ep_version(&self, entry_point: &Address) -> EthResult {
if let Some((addr, _)) = self.v0_6 {
if addr == *entry_point {
return Ok(EntryPointVersion::V0_6);
diff --git a/crates/rpc/src/types/v0_7.rs b/crates/rpc/src/types/v0_7.rs
index ccd9ced6..7808c1bb 100644
--- a/crates/rpc/src/types/v0_7.rs
+++ b/crates/rpc/src/types/v0_7.rs
@@ -176,10 +176,10 @@ impl From for UserOperationOptionalGas {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct RpcGasEstimate {
- pre_verification_gas: U256,
- call_gas_limit: U256,
- verification_gas_limit: U256,
- paymaster_verification_gas_limit: Option,
+ pub(crate) pre_verification_gas: U256,
+ pub(crate) call_gas_limit: U256,
+ pub(crate) verification_gas_limit: U256,
+ pub(crate) paymaster_verification_gas_limit: Option,
}
impl From for RpcGasEstimate {
diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml
index a9904f9e..d0929b4b 100644
--- a/crates/types/Cargo.toml
+++ b/crates/types/Cargo.toml
@@ -32,6 +32,7 @@ ethers.workspace = true
[dev-dependencies]
rundler-types = { path = ".", features = ["test-utils"] }
+tokio.workspace = true # For hybrid compute testing.
[features]
test-utils = [ "mockall" ]
diff --git a/crates/types/build.rs b/crates/types/build.rs
index 3a854f83..56fc658f 100644
--- a/crates/types/build.rs
+++ b/crates/types/build.rs
@@ -38,7 +38,7 @@ fn generate_v0_6_bindings() -> Result<(), Box> {
// hybrid compute
run_command(
- forge_build("../lib/account-abstraction-versions/v0_6/contracts")
+ forge_build("hc0_6")
.arg("--remappings")
.arg("@openzeppelin/=lib/openzeppelin-contracts-versions/v4_9")
.arg("--remappings")
@@ -57,8 +57,9 @@ fn generate_v0_6_bindings() -> Result<(), Box> {
abigen_of("v0_6", "VerifyingPaymaster")?,
abigen_of("v0_6", "CallGasEstimationProxy")?,
// hybrid compute
- abigen_of("v0_6", "INonceManager")?,
- abigen_of("v0_6", "HCHelper")?,
+ abigen_of("hc0_6", "INonceManager")?,
+ abigen_of("hc0_6", "HCHelper")?,
+ abigen_of("hc0_6", "HybridAccount")?,
])
.build()?
.write_to_module("src/contracts/v0_6", false)?;
@@ -75,6 +76,16 @@ fn generate_v0_7_bindings() -> Result<(), Box> {
"generate ABIs",
)?;
+ run_command(
+ forge_build("hc0_7")
+ .arg("--remappings")
+ .arg("@openzeppelin/=lib/openzeppelin-contracts-versions/v5_0")
+ .arg("--remappings")
+ .arg("@gnosis.pm/safe-contracts=lib/safe-smart-account"),
+ "https://getfoundry.sh/",
+ "generate ABIs",
+ )?;
+
MultiAbigen::from_abigens([
abigen_of("v0_7", "IEntryPoint")?,
abigen_of("v0_7", "IAccount")?,
@@ -85,6 +96,10 @@ fn generate_v0_7_bindings() -> Result<(), Box> {
abigen_of("v0_7", "EntryPointSimulations")?,
abigen_of("v0_7", "CallGasEstimationProxy")?,
abigen_of("v0_7", "SenderCreator")?,
+ // hybrid compute
+ abigen_of("hc0_7", "INonceManager")?,
+ abigen_of("hc0_7", "HCHelper")?,
+ abigen_of("hc0_7", "HybridAccount")?,
])
.build()?
.write_to_module("src/contracts/v0_7", false)?;
diff --git a/crates/types/contracts/hc_scripts/ExampleDeploy.s.sol b/crates/types/contracts/hc_scripts/ExampleDeploy_v6.s.sol
similarity index 100%
rename from crates/types/contracts/hc_scripts/ExampleDeploy.s.sol
rename to crates/types/contracts/hc_scripts/ExampleDeploy_v6.s.sol
diff --git a/crates/types/contracts/hc_scripts/ExampleDeploy_v7.s.sol b/crates/types/contracts/hc_scripts/ExampleDeploy_v7.s.sol
new file mode 100644
index 00000000..95ec612f
--- /dev/null
+++ b/crates/types/contracts/hc_scripts/ExampleDeploy_v7.s.sol
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.23;
+
+import "forge-std/Script.sol";
+import "src/hc0_7/HybridAccount.sol";
+import "src/hc0_7/TestAuctionSystem.sol";
+import "src/hc0_7/TestCaptcha.sol";
+import "src/hc0_7/TestHybrid.sol";
+import "src/hc0_7/TestRainfallInsurance.sol";
+import "src/hc0_7/TestSportsBetting.sol";
+import "src/hc0_7/TestKyc.sol";
+import "src/hc0_7/TestTokenPrice.sol";
+
+contract LocalDeploy is Script {
+ function run() external
+ returns (address[7] memory) {
+ uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
+
+ address payable ha1Addr = payable(vm.envAddress("OC_HYBRID_ACCOUNT"));
+ HybridAccount ha1;
+
+ address[7] memory ret;
+
+ vm.startBroadcast(deployerPrivateKey);
+
+ ret[0] = address(new AuctionFactory(ha1Addr));
+ ret[1] = address(new TestCaptcha(ha1Addr));
+ ret[2] = address(new TestHybrid(ha1Addr));
+ ret[3] = address(new RainfallInsurance(ha1Addr));
+ ret[4] = address(new SportsBetting(ha1Addr));
+ ret[5] = address(new TestKyc(ha1Addr));
+ ret[6] = address(new TestTokenPrice(ha1Addr));
+
+ vm.stopBroadcast();
+ return ret;
+ }
+}
diff --git a/crates/types/contracts/hc_scripts/LocalDeploy.s.sol b/crates/types/contracts/hc_scripts/LocalDeploy_v6.s.sol
similarity index 100%
rename from crates/types/contracts/hc_scripts/LocalDeploy.s.sol
rename to crates/types/contracts/hc_scripts/LocalDeploy_v6.s.sol
diff --git a/crates/types/contracts/hc_scripts/LocalDeploy_v7.s.sol b/crates/types/contracts/hc_scripts/LocalDeploy_v7.s.sol
new file mode 100644
index 00000000..6364214b
--- /dev/null
+++ b/crates/types/contracts/hc_scripts/LocalDeploy_v7.s.sol
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.23;
+
+import "forge-std/Script.sol";
+import "lib/account-abstraction-versions/v0_7/contracts/core/EntryPoint.sol";
+import "src/hc0_7/HCHelper.sol";
+import "src/hc0_7/HybridAccountFactory.sol";
+import "lib/account-abstraction-versions/v0_7/contracts/samples/SimpleAccountFactory.sol";
+
+contract LocalDeploy is Script {
+ function run() external
+ returns (address[5] memory) {
+ address deployAddr = vm.envAddress("DEPLOY_ADDR");
+ uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
+ address hcSysOwner = vm.envAddress("HC_SYS_OWNER");
+ require (hcSysOwner != address(0), "HC_SYS_OWNER not set");
+ uint256 deploySalt = vm.envOr("DEPLOY_SALT",uint256(0)); // Change this to force redeployment of contracts
+
+ address bobaAddr = vm.envOr("BOBA_TOKEN", 0x4200000000000000000000000000000000000023);
+
+ EntryPoint ept;
+ HCHelper helper;
+ SimpleAccountFactory saf;
+ HybridAccountFactory haf;
+ HybridAccount ha0;
+
+ bytes32 salt_val = bytes32(deploySalt);
+ uint112 min_deposit = 0.001 ether;
+
+ vm.startBroadcast(deployerPrivateKey);
+
+ // EntryPointAddr is hard-coded for the v0.7 implementation
+ ept = EntryPoint(payable(0x0000000071727De22E5E9d8BAf0edAc6f37da032));
+
+ {
+ address helperAddr = vm.envOr("HC_HELPER_ADDR", 0x0000000000000000000000000000000000000000);
+ if (helperAddr != address(0) && helperAddr.code.length > 0) {
+ helper = HCHelper(helperAddr);
+ } else {
+ helper = new HCHelper{salt: salt_val}(address(ept), bobaAddr, deployAddr);
+ }
+ }
+ {
+ address safAddr = vm.envOr("SA_FACTORY_ADDR", 0x0000000000000000000000000000000000000000);
+ if (safAddr != address(0) && safAddr.code.length > 0) {
+ saf = SimpleAccountFactory(safAddr);
+ } else {
+ saf = new SimpleAccountFactory(ept);
+ }
+ }
+ {
+ address hafAddr = vm.envOr("HA_FACTORY_ADDR", 0x0000000000000000000000000000000000000000);
+ if (hafAddr != address(0) && hafAddr.code.length > 0) {
+ haf = HybridAccountFactory(hafAddr);
+ } else {
+ haf = new HybridAccountFactory(ept, address(helper));
+ }
+ }
+ {
+ address ha0Addr = vm.envOr("HC_SYS_ACCOUNT", 0x0000000000000000000000000000000000000000);
+ if (ha0Addr != address(0) && ha0Addr.code.length > 0) {
+ ha0 = HybridAccount(payable(ha0Addr));
+ } else {
+ ha0 = haf.createAccount(hcSysOwner,0);
+ }
+ }
+ if (helper.systemAccount() != address(ha0)) {
+ helper.SetSystemAccount(address(ha0));
+ }
+
+ // Previous version deposited to EntryPoint, here we fund the acct directly
+ if (address(ha0).balance < min_deposit) {
+ payable(address(ha0)).transfer(min_deposit - address(ha0).balance);
+ }
+
+ vm.stopBroadcast();
+ return [address(ept),address(helper), address(saf), address(haf), address(ha0)];
+ }
+}
diff --git a/crates/types/contracts/src/hc0_7/HCHelper.sol b/crates/types/contracts/src/hc0_7/HCHelper.sol
new file mode 100644
index 00000000..ca74ebe1
--- /dev/null
+++ b/crates/types/contracts/src/hc0_7/HCHelper.sol
@@ -0,0 +1,200 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.12;
+
+import "account-abstraction/v0_7/interfaces/INonceManager.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "@openzeppelin/contracts/access/Ownable.sol";
+
+contract HCHelper is ReentrancyGuard, Ownable {
+ using SafeERC20 for IERC20;
+
+ event SystemAccountSet(address oldAccount, address newAccount);
+ event RegisteredUrl(address contract_addr, string url);
+ event TokenWithdrawal(address withdrawTo, uint256 amount);
+
+ // Response data is stored here by PutResponse() and then consumed by TryCallOffchain().
+ mapping(bytes32=>bytes) ResponseCache;
+
+ // BOBA token address
+ address public tokenAddr;
+
+ // Token amount required to purchase each prepaid credit (may be 0 for testing)
+ uint256 public pricePerCall;
+
+ // Account which is used to insert system error responses. Currently a single
+ // address but could be extended to a list of authorized accounts if needed.
+ address public systemAccount;
+
+ // Data stored per RegisteredCaller
+ struct callerInfo {
+ address owner;
+ string url;
+ uint256 credits;
+ }
+
+ // Contracts which are allowed to use Hybrid Compute.
+ mapping(address=>callerInfo) public RegisteredCallers;
+
+ // AA EntryPoint
+ address immutable entryPoint;
+
+ // Constructor
+ constructor(address _entryPoint, address _tokenAddr, address _owner) Ownable(_owner) {
+ entryPoint = _entryPoint;
+ tokenAddr = _tokenAddr;
+ }
+
+ // Change the SystemAccount address (used for error responses)
+ function SetSystemAccount(address _systemAccount) public onlyOwner {
+ emit SystemAccountSet(systemAccount, _systemAccount);
+ systemAccount = _systemAccount;
+ }
+
+ // Temporary method, until an auto-registration protocol is developed.
+ function RegisterUrl(address contract_addr, string calldata url) public onlyOwner {
+ require(bytes(url).length > 0, "URL cannot be empty");
+ RegisteredCallers[contract_addr].owner = msg.sender;
+ RegisteredCallers[contract_addr].url = url;
+ emit RegisteredUrl(contract_addr, url);
+ }
+
+ // Set or change the per-call token price (0 is allowed). Does not affect
+ // existing credit balances, only applies to new AddCredit() calls.
+ function SetPrice(uint256 _pricePerCall) public onlyOwner {
+ pricePerCall = _pricePerCall;
+ }
+
+ // Purchase credits allowing the specified contract to perform HC calls.
+ // The token cost is (pricePerCall() * numCredits) and is non-refundable
+ function AddCredit(address contract_addr, uint256 numCredits) public nonReentrant {
+ uint256 tokenPrice = numCredits * pricePerCall;
+ RegisteredCallers[contract_addr].credits += numCredits;
+ IERC20(tokenAddr).safeTransferFrom(msg.sender, address(this), tokenPrice);
+ }
+
+ // Allow the owner to withdraw tokens
+ function WithdrawTokens(uint256 amount, address withdrawTo) public onlyOwner nonReentrant {
+ emit TokenWithdrawal(withdrawTo, amount);
+ IERC20(tokenAddr).safeTransfer(withdrawTo, amount);
+ }
+
+ // Called from a HybridAccount contract, to populate the response which it will
+ // subsequently request in TryCallOffchain()
+ function PutResponse(bytes32 subKey, bytes calldata response) public {
+ require(RegisteredCallers[msg.sender].owner != address(0), "Unregistered caller");
+ require(response.length % 32 == 0, "Response not properly encoded");
+ require(response.length >= 32*4, "Response too short");
+
+ (,, uint32 errCode,) = abi.decode(response,(address, uint256, uint32, bytes));
+ require(errCode < 2, "invalid errCode for PutResponse()");
+
+ bytes32 mapKey = keccak256(abi.encodePacked(msg.sender, subKey));
+ ResponseCache[mapKey] = response;
+ }
+
+ // Allow the system to supply an error response for unsuccessful requests.
+ // Any such response will only be retrieved if there was nothing supplied
+ // by PutResponse()
+ function PutSysResponse(bytes32 subKey, bytes calldata response) public {
+ require(msg.sender == systemAccount, "Only systemAccount may call PutSysResponse");
+ require(response.length >= 32*4, "Response too short");
+
+ (,, uint32 errCode,) = abi.decode(response,(address, uint256, uint32, bytes));
+ require(errCode >= 2, "PutSysResponse() may only be used for error responses");
+
+ bytes32 mapKey = keccak256(abi.encodePacked(address(this), subKey));
+ ResponseCache[mapKey] = response;
+ }
+
+ // Remove one or more map entries (only needed if response was not retrieved normally).
+ function RemoveResponses(bytes32[] calldata mapKeys) public {
+ require(msg.sender == systemAccount, "Only systemAccount may call RemoveResponses");
+ for (uint32 i = 0; i < mapKeys.length; i++) {
+ delete(ResponseCache[mapKeys[i]]);
+ }
+ }
+
+ // Try to retrieve an entry, also removing it from the mapping. This
+ // function will check for stale entries by checking the nonce of the srcAccount.
+ // Stale entries will return a "not found" condition.
+ function getEntry(bytes32 mapKey) internal returns (bool, uint32, bytes memory) {
+ bytes memory entry;
+ bool found;
+ uint32 errCode;
+ bytes memory response;
+ address srcAddr;
+ uint256 srcNonce;
+
+ entry = ResponseCache[mapKey];
+ delete(ResponseCache[mapKey]);
+
+ if (entry.length == 1) {
+ // Used during state simulation to verify that a trigger request actually came from this helper contract
+ revert("_HC_VRFY");
+ } else if (entry.length != 0) {
+ found = true;
+ (srcAddr, srcNonce, errCode, response) = abi.decode(entry,(address, uint256, uint32, bytes));
+ uint192 nonceKey = uint192(srcNonce >> 64);
+
+ INonceManager NM = INonceManager(entryPoint);
+ uint256 actualNonce = NM.getNonce(srcAddr, nonceKey);
+
+ if (srcNonce + 1 != actualNonce) {
+ // stale entry
+ found = false;
+ errCode = 0;
+ response = "0x";
+ }
+ }
+ return (found, errCode, response);
+ }
+
+ // Make an offchain call to a pre-registered endpoint.
+ function TryCallOffchain(bytes32 userKey, bytes memory req) public returns (uint32, bytes memory) {
+ bool found;
+ uint32 errCode;
+ bytes memory ret;
+
+ require(RegisteredCallers[msg.sender].owner != address(0), "Calling contract not registered");
+ require(req.length % 32 == 4, "Request must be ABI-encoded with selector");
+
+ if (RegisteredCallers[msg.sender].credits == 0) {
+ return (5, "Insufficient credit");
+ }
+ RegisteredCallers[msg.sender].credits -= 1;
+
+ bytes32 subKey = keccak256(abi.encodePacked(userKey, req));
+ bytes32 mapKey = keccak256(abi.encodePacked(msg.sender, subKey));
+
+ (found, errCode, ret) = getEntry(mapKey);
+
+ if (found) {
+ return (errCode, ret);
+ } else {
+ // If no off-chain response, check for a system error response.
+ bytes32 errKey = keccak256(abi.encodePacked(address(this), subKey));
+
+ (found, errCode, ret) = getEntry(errKey);
+ if (found) {
+ require(errCode >= 2, "invalid errCode");
+ return (errCode, ret);
+ } else {
+ // Nothing found, so trigger a new request.
+ bytes memory prefix = "_HC_TRIG";
+ bytes memory r2 = bytes.concat(prefix, abi.encodePacked(msg.sender, userKey, req));
+ assembly {
+ revert(add(r2, 32), mload(r2))
+ }
+ }
+ }
+ }
+
+ // Returns the slot index of ResponseCache, needed by the bundler.
+ // This index is affected by the OpenZeppelin libraries like Ownable.
+ function ResponseSlot() public pure returns (uint256 ret) {
+ assembly {
+ ret := ResponseCache.slot
+ }
+ }
+}
diff --git a/crates/types/contracts/src/hc0_7/HybridAccount.sol b/crates/types/contracts/src/hc0_7/HybridAccount.sol
new file mode 100644
index 00000000..0faaf5cc
--- /dev/null
+++ b/crates/types/contracts/src/hc0_7/HybridAccount.sol
@@ -0,0 +1,173 @@
+// SPDX-License-Identifier: GPL-3.0
+pragma solidity ^0.8.23;
+
+/* solhint-disable avoid-low-level-calls */
+/* solhint-disable no-inline-assembly */
+/* solhint-disable reason-string */
+
+import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
+import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
+import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
+import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
+import "account-abstraction/v0_7/core/BaseAccount.sol";
+import "account-abstraction/v0_7/core/Helpers.sol";
+import "account-abstraction/v0_7/samples/callback/TokenCallbackHandler.sol";
+
+interface IHCHelper {
+ function TryCallOffchain(bytes32, bytes memory) external returns (uint32, bytes memory);
+}
+
+/**
+ * minimal account.
+ * this is sample minimal account.
+ * has execute, eth handling methods
+ * has a single signer that can send requests through the entryPoint.
+ */
+contract HybridAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, Initializable {
+ mapping(address=>bool) public PermittedCallers;
+
+ address public owner;
+
+ IEntryPoint private immutable _entryPoint;
+ address public immutable _helperAddr;
+
+ event HybridAccountInitialized(IEntryPoint indexed entryPoint, address indexed owner);
+
+ modifier onlyOwner() {
+ _onlyOwner();
+ _;
+ }
+
+ /// @inheritdoc BaseAccount
+ function entryPoint() public view virtual override returns (IEntryPoint) {
+ return _entryPoint;
+ }
+
+ // solhint-disable-next-line no-empty-blocks
+ receive() external payable {}
+
+ constructor(IEntryPoint anEntryPoint, address helperAddr) {
+ _entryPoint = anEntryPoint;
+ _helperAddr = helperAddr;
+ _disableInitializers();
+ }
+
+ function _onlyOwner() internal view {
+ //directly from EOA owner, or through the account itself (which gets redirected through execute())
+ require(msg.sender == owner || msg.sender == address(this), "only owner");
+ }
+
+ /**
+ * execute a transaction (called directly from owner, or by entryPoint)
+ * @param dest destination address to call
+ * @param value the value to pass in this call
+ * @param func the calldata to pass in this call
+ */
+ function execute(address dest, uint256 value, bytes calldata func) external {
+ _requireFromEntryPointOrOwner();
+ _call(dest, value, func);
+ }
+
+ /**
+ * execute a sequence of transactions
+ * @dev to reduce gas consumption for trivial case (no value), use a zero-length array to mean zero value
+ * @param dest an array of destination addresses
+ * @param value an array of values to pass to each call. can be zero-length for no-value calls
+ * @param func an array of calldata to pass to each call
+ */
+ function executeBatch(address[] calldata dest, uint256[] calldata value, bytes[] calldata func) external {
+ _requireFromEntryPointOrOwner();
+ require(dest.length == func.length && (value.length == 0 || value.length == func.length), "wrong array lengths");
+ if (value.length == 0) {
+ for (uint256 i = 0; i < dest.length; i++) {
+ _call(dest[i], 0, func[i]);
+ }
+ } else {
+ for (uint256 i = 0; i < dest.length; i++) {
+ _call(dest[i], value[i], func[i]);
+ }
+ }
+ }
+
+ /**
+ * @dev The _entryPoint member is immutable, to reduce gas consumption. To upgrade EntryPoint,
+ * a new implementation of HybridAccount must be deployed with the new EntryPoint address, then upgrading
+ * the implementation by calling `upgradeTo()`
+ * @param anOwner the owner (signer) of this account
+ */
+ function initialize(address anOwner) public virtual initializer {
+ _initialize(anOwner);
+ }
+
+ function _initialize(address anOwner) internal virtual {
+ owner = anOwner;
+ emit HybridAccountInitialized(_entryPoint, owner);
+ }
+
+ // Require the function call went through EntryPoint or owner
+ function _requireFromEntryPointOrOwner() internal view {
+ require(msg.sender == address(entryPoint()) || msg.sender == owner, "account: not Owner or EntryPoint");
+ }
+
+ /// implement template method of BaseAccount
+ function _validateSignature(PackedUserOperation calldata userOp, bytes32 userOpHash)
+ internal override virtual returns (uint256 validationData) {
+ bytes32 hash = MessageHashUtils.toEthSignedMessageHash(userOpHash);
+ if (owner != ECDSA.recover(hash, userOp.signature))
+ return SIG_VALIDATION_FAILED;
+ return SIG_VALIDATION_SUCCESS;
+ }
+
+ function _call(address target, uint256 value, bytes memory data) internal {
+ (bool success, bytes memory result) = target.call{value: value}(data);
+ if (!success) {
+ assembly {
+ revert(add(result, 32), mload(result))
+ }
+ }
+ }
+
+ /**
+ * check current account deposit in the entryPoint
+ */
+ function getDeposit() public view returns (uint256) {
+ return entryPoint().balanceOf(address(this));
+ }
+
+ /**
+ * deposit more funds for this account in the entryPoint
+ */
+ function addDeposit() public payable {
+ entryPoint().depositTo{value: msg.value}(address(this));
+ }
+
+ /**
+ * withdraw value from the account's deposit
+ * @param withdrawAddress target to send to
+ * @param amount to withdraw
+ */
+ function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public onlyOwner {
+ entryPoint().withdrawTo(withdrawAddress, amount);
+ }
+
+ function _authorizeUpgrade(address newImplementation) internal view override {
+ (newImplementation);
+ _onlyOwner();
+ }
+
+ function PermitCaller(address caller, bool allowed) public {
+ _requireFromEntryPointOrOwner();
+ PermittedCallers[caller] = allowed;
+ }
+
+ function CallOffchain(bytes32 userKey, bytes memory req) public returns (uint32, bytes memory) {
+ /* By default a simple whitelist is used. Endpoint implementations may choose to allow
+ unrestricted access, to use a custom permission model, to charge fees, etc. */
+ require(_helperAddr != address(0), "Helper address not set");
+ require(PermittedCallers[msg.sender], "Permission denied");
+ IHCHelper HC = IHCHelper(_helperAddr);
+
+ userKey = keccak256(abi.encodePacked(userKey, msg.sender));
+ return HC.TryCallOffchain(userKey, req);
+ }
+}
diff --git a/crates/types/contracts/src/hc0_7/HybridAccountFactory.sol b/crates/types/contracts/src/hc0_7/HybridAccountFactory.sol
new file mode 100644
index 00000000..3c0cf280
--- /dev/null
+++ b/crates/types/contracts/src/hc0_7/HybridAccountFactory.sol
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: GPL-3.0
+pragma solidity ^0.8.12;
+
+import "@openzeppelin/contracts/utils/Create2.sol";
+import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
+
+import "./HybridAccount.sol";
+
+/**
+ * A sample factory contract for HybridAccount
+ * A UserOperations "initCode" holds the address of the factory, and a method call (to createAccount, in this sample factory).
+ * The factory's createAccount returns the target account address even if it is already installed.
+ * This way, the entryPoint.getSenderAddress() can be called either before or after the account is created.
+ */
+contract HybridAccountFactory {
+ HybridAccount public immutable accountImplementation;
+ address public Helper;
+
+ constructor(IEntryPoint _entryPoint, address _helper) {
+ accountImplementation = new HybridAccount(_entryPoint, _helper);
+ Helper = _helper;
+ }
+
+ /**
+ * create an account, and return its address.
+ * returns the address even if the account is already deployed.
+ * Note that during UserOperation execution, this method is called only if the account is not deployed.
+ * This method returns an existing account address so that entryPoint.getSenderAddress() would work even after account creation
+ */
+ function createAccount(address owner,uint256 salt) public returns (HybridAccount ret) {
+ address addr = getAddress(owner, salt);
+ uint codeSize = addr.code.length;
+ if (codeSize > 0) {
+ return HybridAccount(payable(addr));
+ }
+ ret = HybridAccount(payable(new ERC1967Proxy{salt : bytes32(salt)}(
+ address(accountImplementation),
+ abi.encodeCall(HybridAccount.initialize, (owner))
+ )));
+ }
+
+ /**
+ * calculate the counterfactual address of this account as it would be returned by createAccount()
+ */
+ function getAddress(address owner,uint256 salt) public view returns (address) {
+ return Create2.computeAddress(bytes32(salt), keccak256(abi.encodePacked(
+ type(ERC1967Proxy).creationCode,
+ abi.encode(
+ address(accountImplementation),
+ abi.encodeCall(HybridAccount.initialize, (owner))
+ )
+ )));
+ }
+}
diff --git a/crates/types/contracts/src/hc0_7/TestAuctionSystem.sol b/crates/types/contracts/src/hc0_7/TestAuctionSystem.sol
new file mode 100644
index 00000000..15a446f3
--- /dev/null
+++ b/crates/types/contracts/src/hc0_7/TestAuctionSystem.sol
@@ -0,0 +1,103 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+import "./HybridAccount.sol";
+
+contract AuctionFactory {
+ uint256 public auctionCount = 0;
+ mapping(uint256 => Auction) public auctions;
+
+ event AuctionCreated(uint256 auctionId, address auctionAddress);
+ event AuctionEnded(uint256 auctionId, address winner, uint256 amount);
+
+ struct Auction {
+ address highestBidder;
+ uint256 highestBid;
+ uint256 endTime;
+ address payable beneficiary;
+ bool ended;
+ }
+
+ address payable immutable helperAddr;
+
+ constructor(address payable _helperAddr) {
+ helperAddr = _helperAddr;
+ }
+
+ modifier auctionExists(uint256 auctionId) {
+ require(auctions[auctionId].beneficiary != address(0), "Auction does not exist");
+ _;
+ }
+
+ function createAuction(uint256 _biddingTime, address payable _beneficiary) public {
+ auctionCount++;
+ auctions[auctionCount] = Auction({
+ highestBidder: address(0),
+ highestBid: 0,
+ endTime: block.timestamp + _biddingTime,
+ beneficiary: _beneficiary,
+ ended: false
+ });
+ emit AuctionCreated(auctionCount, address(this));
+ }
+
+ function bid(uint256 auctionId) public payable auctionExists(auctionId) {
+ Auction storage auction = auctions[auctionId];
+ require(block.timestamp < auction.endTime, "Auction already ended.");
+ require(msg.value > auction.highestBid, "There already is a higher bid.");
+ require(verifyBidder(), "Bidder not verified.");
+
+ if (auction.highestBidder != address(0)) {
+ payable(auction.highestBidder).transfer(auction.highestBid);
+ }
+
+ auction.highestBidder = msg.sender;
+ auction.highestBid = msg.value;
+ }
+
+ function verifyBidder() private returns (bool) {
+ HybridAccount ha = HybridAccount(helperAddr);
+
+ bytes memory req = abi.encodeWithSignature(
+ "verifyBidder(address)",
+ msg.sender
+ );
+ bytes32 userKey = bytes32(abi.encode(msg.sender));
+ (uint32 error, bytes memory ret) = ha.CallOffchain(userKey, req);
+
+ if (error != 0) {
+ revert(string(ret));
+ }
+
+ bool isVerified;
+ (isVerified) = abi.decode(ret, (bool));
+ return isVerified;
+ }
+
+ function endAuction(uint256 auctionId) public auctionExists(auctionId) {
+ Auction storage auction = auctions[auctionId];
+ require(block.timestamp >= auction.endTime, "Auction not yet ended.");
+ require(!auction.ended, "Auction end already called.");
+
+ auction.ended = true;
+ emit AuctionEnded(auctionId, auction.highestBidder, auction.highestBid);
+
+ auction.beneficiary.transfer(auction.highestBid);
+ }
+
+ function getHighestBid(uint256 auctionId) public view auctionExists(auctionId) returns (uint256) {
+ return auctions[auctionId].highestBid;
+ }
+
+ function getHighestBidder(uint256 auctionId) public view auctionExists(auctionId) returns (address) {
+ return auctions[auctionId].highestBidder;
+ }
+
+ function getAuctionEndTime(uint256 auctionId) public view auctionExists(auctionId) returns (uint256) {
+ return auctions[auctionId].endTime;
+ }
+
+ function isAuctionEnded(uint256 auctionId) public view auctionExists(auctionId) returns (bool) {
+ return auctions[auctionId].ended;
+ }
+}
diff --git a/crates/types/contracts/src/hc0_7/TestCaptcha.sol b/crates/types/contracts/src/hc0_7/TestCaptcha.sol
new file mode 100644
index 00000000..69868406
--- /dev/null
+++ b/crates/types/contracts/src/hc0_7/TestCaptcha.sol
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: GPL-3.0
+pragma solidity ^0.8.12;
+
+import "@openzeppelin/contracts/access/Ownable.sol";
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "./HybridAccount.sol";
+
+contract TestCaptcha is Ownable {
+ address payable immutable helperAddr;
+ uint256 constant public nativeFaucetAmount = 0.01 ether;
+ uint256 constant public waitingPeriod = 1 days;
+ IERC20 public token;
+
+ mapping(address => uint256) public claimRecords;
+
+ uint256 private constant SAFE_GAS_STIPEND = 6000;
+
+ constructor(address payable _helperAddr) Ownable(msg.sender) {
+ helperAddr = _helperAddr;
+ }
+
+ event Withdraw(address receiver, uint256 nativeAmount);
+
+ receive() external payable {}
+
+ function withdraw(uint256 _nativeAmount) public onlyOwner {
+ (bool sent, ) = msg.sender.call{
+ gas: SAFE_GAS_STIPEND,
+ value: _nativeAmount
+ }("");
+ require(sent, "Failed to send native Ether");
+
+ emit Withdraw(msg.sender, _nativeAmount);
+ }
+
+ function verifyCaptcha(
+ address _to,
+ bytes32 _uuid,
+ string memory _key
+ ) private returns (bool) {
+ HybridAccount ha = HybridAccount(helperAddr);
+
+ bytes memory req = abi.encodeWithSignature(
+ "verifyCaptcha(string,string,string)",
+ _to,
+ _uuid,
+ _key
+ );
+ bytes32 userKey = bytes32(abi.encode(msg.sender));
+ (uint32 error, bytes memory ret) = ha.CallOffchain(userKey, req);
+
+ if (error != 0) {
+ revert(string(ret));
+ }
+
+ bool isVerified;
+ (isVerified) = abi.decode(ret, (bool));
+ return isVerified;
+ }
+
+ function getTestnetETH(
+ bytes32 _uuid,
+ string memory _key,
+ address _to) external {
+ require(claimRecords[_to] + waitingPeriod <= block.timestamp, 'Invalid request');
+ require(verifyCaptcha(_to, _uuid, _key), "Invalid captcha");
+ claimRecords[_to] = block.timestamp;
+
+ (bool sent,) = (_to).call{gas: SAFE_GAS_STIPEND, value: nativeFaucetAmount}("");
+ require(sent, "Failed to send native");
+ }
+}
diff --git a/crates/types/contracts/src/hc0_7/TestHybrid.sol b/crates/types/contracts/src/hc0_7/TestHybrid.sol
new file mode 100644
index 00000000..85ecc36a
--- /dev/null
+++ b/crates/types/contracts/src/hc0_7/TestHybrid.sol
@@ -0,0 +1,108 @@
+// SPDX-License-Identifier: GPL-3.0
+pragma solidity ^0.8.23;
+
+import "./HybridAccount.sol";
+
+contract TestHybrid {
+ mapping(address => uint256) public counters;
+
+ address payable immutable hcAccount;
+
+ event CalledFrom(address sender);
+
+ constructor(address payable _hcAccount) {
+ hcAccount = _hcAccount;
+ }
+
+ // helper method to waste gas
+ // repeat - waste gas on writing storage in a loop
+ // junk - dynamic buffer to stress the function size.
+ mapping(uint256 => uint256) public xxx;
+ uint256 public offset;
+
+ function gasWaster(uint256 repeat, string calldata /*junk*/) external {
+ for (uint256 i = 1; i <= repeat; i++) {
+ offset++;
+ xxx[offset] = i;
+ }
+ }
+
+ function count(uint32 a, uint32 b) public {
+ HybridAccount HA = HybridAccount(hcAccount);
+ uint256 x;
+ uint256 y;
+ if (b == 0) {
+ counters[msg.sender] = counters[msg.sender] + a;
+ return;
+ }
+ bytes memory req = abi.encodeWithSignature("addsub2(uint32,uint32)", a, b);
+ bytes32 userKey = bytes32(abi.encode(msg.sender));
+ (uint32 error, bytes memory ret) = HA.CallOffchain(userKey, req);
+
+ if (error == 0) {
+ (x,y) = abi.decode(ret,(uint256,uint256)); // x=(a+b), y=(a-b)
+
+ this.gasWaster(x, "abcd1234");
+ counters[msg.sender] = counters[msg.sender] + y;
+ } else if (b >= 10) {
+ revert(string(ret));
+ } else if (error == 1) {
+ counters[msg.sender] = counters[msg.sender] + 100;
+ } else {
+ //revert(string(ret));
+ counters[msg.sender] = counters[msg.sender] + 1000;
+ }
+ }
+
+ function countFail() public pure {
+ revert("count failed");
+ }
+
+ function justemit() public {
+ emit CalledFrom(msg.sender);
+ }
+
+ /* This example is a word-guessing game. The user picks a four-letter word as their guess,
+ and pays for the number of entries they wish to purchase. This wager is added to a pool.
+ The offchain provider generates a random array of words and returns it as a string[]. If
+ the user's guess appears in the list returned from the server then they win the entire pool.
+
+ A boolean flag allows the user to cheat by guaranteeing that the word "frog" will appear
+ in the list.
+ */
+ event GameResult(address indexed caller,uint256 indexed win, uint256 indexed Pool);
+ uint256 public constant EntryCost = 2 gwei;
+ uint256 public Pool = 0;
+
+ function wordGuess(string calldata myGuess, bool cheat) public payable {
+ HybridAccount HA = HybridAccount(payable(hcAccount));
+ uint256 entries = msg.value / EntryCost;
+ require(entries > 0, "No entries purchased");
+ require(entries <= 100, "Excess payment");
+ Pool += msg.value;
+ require(bytes(myGuess).length == 4, "Game uses 4-letter words");
+
+ bytes memory req = abi.encodeWithSignature("ramble(uint256,bool)", entries, cheat);
+ bytes32 userKey = bytes32(abi.encode(msg.sender));
+ (uint32 error, bytes memory ret) = HA.CallOffchain(userKey, req);
+ if (error != 0) {
+ revert(string(ret));
+ }
+
+ uint256 win = 0;
+ string[] memory words = abi.decode(ret,(string[]));
+
+ for (uint256 i=0; i Policy) public policies;
+ mapping(string => Rainfall) public currentRainfall;
+ uint256 public constant MULTIPLIER = 3;
+ address payable immutable helperAddr;
+
+ uint256 private nonce;
+
+ event PolicyCreated(uint256 indexed policyId, address indexed policyholder, string city, uint256 premium, uint256 payoutAmount);
+
+
+ constructor(address payable _helperAddr) {
+ helperAddr = _helperAddr;
+ }
+
+ function generatePolicyId(address policyHolder, string memory city) internal returns (uint256) {
+ nonce++;
+ return uint256(keccak256(abi.encodePacked(policyHolder, city, block.timestamp, nonce)));
+ }
+
+ function buyInsurance(
+ uint256 triggerRainfall,
+ string memory city
+ ) public payable returns (uint256){
+ require(msg.value > 0, "Premium must be greater than zero");
+ uint256 payoutAmount = msg.value * MULTIPLIER;
+ uint256 policyId = generatePolicyId(msg.sender, city);
+
+ policies[policyId] = Policy(
+ msg.sender,
+ msg.value,
+ payoutAmount,
+ triggerRainfall,
+ city,
+ block.timestamp,
+ PolicyState.Active
+ );
+
+ emit PolicyCreated(policyId, msg.sender, city, msg.value, payoutAmount);
+ }
+
+ function updateRainfall(
+ string memory city
+ ) internal returns (Rainfall storage) {
+ HybridAccount ha = HybridAccount(helperAddr);
+
+ bytes memory req = abi.encodeWithSignature(
+ "get_rainfall(string)",
+ city
+ );
+ bytes32 userKey = bytes32(abi.encode(msg.sender));
+ (uint32 error, bytes memory ret) = ha.CallOffchain(userKey, req);
+
+ if (error != 0) {
+ revert(string(ret));
+ }
+
+ uint256 rainfallInMm;
+ (rainfallInMm) = abi.decode(ret, (uint256));
+ currentRainfall[city] = Rainfall(rainfallInMm, block.timestamp);
+
+ return currentRainfall[city];
+ }
+
+ function checkAndPayout(uint256 policyId) public {
+ Policy storage policy = policies[policyId];
+ require(policy.state == PolicyState.Active, "Policy is not active");
+
+ if (policy.timestamp + 365 days < block.timestamp) {
+ policy.state = PolicyState.Expired;
+ revert("Policy expired");
+ }
+
+ Rainfall storage rainfall = currentRainfall[policy.city];
+
+ if (
+ rainfall.updatedAt == 0 ||
+ rainfall.updatedAt + 24 hours < block.timestamp
+ ) {
+ rainfall = updateRainfall(policy.city);
+ }
+
+
+ require(
+ rainfall.rainfallInMm <= policy.triggerRainfall,
+ "Trigger condition not met"
+ );
+
+ policy.state = PolicyState.Claimed;
+ payable(policy.policyholder).transfer(policy.payoutAmount);
+ }
+}
diff --git a/crates/types/contracts/src/hc0_7/TestSportsBetting.sol b/crates/types/contracts/src/hc0_7/TestSportsBetting.sol
new file mode 100644
index 00000000..a515e6c9
--- /dev/null
+++ b/crates/types/contracts/src/hc0_7/TestSportsBetting.sol
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+import "./HybridAccount.sol";
+
+contract SportsBetting {
+ address payable immutable helperAddr;
+
+ constructor(address payable _helperAddr) {
+ helperAddr = _helperAddr;
+ }
+
+ struct Bet {
+ address bettor;
+ uint256 amount;
+ uint256 outcome; // 1 for Team A win, 2 for Team B win, 3 for Draw
+ bool settled;
+ }
+
+ struct Game {
+ uint256 gameId;
+ bool exists;
+ }
+
+ mapping(uint256 => Game) public games;
+ mapping(uint256 => Bet[]) public bets;
+ mapping(uint256 => uint256) public gameScores; // 1 for Team A win, 2 for Team B win, 3 for Draw
+
+ event GameCreated(uint256 indexed gameId);
+ event BetPlaced(
+ address indexed bettor,
+ uint256 indexed gameId,
+ uint256 amount,
+ uint256 outcome
+ );
+ event BetSettled(
+ address indexed bettor,
+ uint256 indexed gameId,
+ uint256 outcome,
+ uint256 winnings
+ );
+ event GameScoreUpdated(uint256 indexed gameId, uint256 score);
+
+ function createGame(uint256 gameId) external returns (uint256) {
+ games[gameId] = Game({gameId: gameId, exists: true});
+
+ emit GameCreated(gameId);
+ return gameId;
+ }
+
+ function placeBet(uint256 _gameId, uint256 _outcome) external payable {
+ require(msg.value > 0, "Bet amount must be greater than zero");
+ require(_outcome >= 1 && _outcome <= 3, "Invalid outcome");
+ require(games[_gameId].exists, "Game does not exist");
+
+ bets[_gameId].push(
+ Bet({
+ bettor: msg.sender,
+ amount: msg.value,
+ outcome: _outcome,
+ settled: false
+ })
+ );
+
+ emit BetPlaced(msg.sender, _gameId, msg.value, _outcome);
+ }
+
+ function settleBet(uint256 _gameId) external {
+ require(games[_gameId].exists, "Game does not exist");
+
+ uint256 actualOutcome = updateGameScore(_gameId);
+ //uint256 actualOutcome = gameScores[_gameId];
+
+ for (uint256 i = 0; i < bets[_gameId].length; i++) {
+ Bet storage bet = bets[_gameId][i];
+ if (!bet.settled) {
+ if (bet.outcome == actualOutcome) {
+ uint256 winnings = bet.amount * 2; // Here you could fetch the winning ratio from offchain to calculate the user's win.
+ payable(bet.bettor).transfer(winnings);
+ emit BetSettled(
+ bet.bettor,
+ _gameId,
+ actualOutcome,
+ winnings
+ );
+ }
+ bet.settled = true;
+ }
+ }
+ }
+
+ function updateGameScore(uint256 _gameId) internal returns (uint256) {
+ require(games[_gameId].exists, "Game does not exist");
+
+ HybridAccount ha = HybridAccount(helperAddr);
+
+ bytes memory req = abi.encodeWithSignature(
+ "get_score(uint256)",
+ _gameId
+ );
+ bytes32 userKey = bytes32(abi.encode(msg.sender));
+ (uint32 error, bytes memory ret) = ha.CallOffchain(userKey, req);
+
+ if (error != 0) {
+ revert(string(ret));
+ }
+
+ uint256 result;
+ (result) = abi.decode(ret, (uint256));
+ gameScores[_gameId] = result;
+ emit GameScoreUpdated(_gameId, result);
+ return result;
+ }
+}
diff --git a/crates/types/contracts/src/hc0_7/TestTokenPrice.sol b/crates/types/contracts/src/hc0_7/TestTokenPrice.sol
new file mode 100644
index 00000000..9dcbf397
--- /dev/null
+++ b/crates/types/contracts/src/hc0_7/TestTokenPrice.sol
@@ -0,0 +1,35 @@
+// SPDX-License-Identifier: GPL-3.0
+pragma solidity ^0.8.12;
+
+import "./HybridAccount.sol";
+
+contract TestTokenPrice {
+ mapping(uint256 => uint256) public counters;
+ address payable immutable helperAddr;
+
+ event PriceQuote(string, string);
+
+ constructor(address payable _helperAddr) {
+ helperAddr = _helperAddr;
+ counters[0] = 100;
+ }
+
+ function fetchPrice(
+ string calldata token
+ ) public returns (string memory) {
+ HybridAccount ha = HybridAccount(payable(helperAddr));
+ string memory price;
+
+ bytes memory req = abi.encodeWithSignature("getprice(string)", token);
+ bytes32 userKey = bytes32(abi.encode(msg.sender));
+ (uint32 error, bytes memory ret) = ha.CallOffchain(userKey, req);
+
+ if (error != 0) {
+ revert(string(ret));
+ }
+
+ (price) = abi.decode(ret, (string));
+ emit PriceQuote(token, price);
+ return price;
+ }
+}
diff --git a/crates/types/contracts/src/v0_7/imports.sol b/crates/types/contracts/src/v0_7/imports.sol
index 8b094c63..d6c6f5af 100644
--- a/crates/types/contracts/src/v0_7/imports.sol
+++ b/crates/types/contracts/src/v0_7/imports.sol
@@ -10,3 +10,4 @@ import "account-abstraction/v0_7/interfaces/IAggregator.sol";
import "account-abstraction/v0_7/interfaces/IStakeManager.sol";
import "account-abstraction/v0_7/core/EntryPointSimulations.sol";
import "account-abstraction/v0_7/core/SenderCreator.sol";
+import "src/hc0_7/HCHelper.sol";
diff --git a/crates/types/src/hybrid_compute.rs b/crates/types/src/hybrid_compute.rs
index 6129ff6a..07c473af 100644
--- a/crates/types/src/hybrid_compute.rs
+++ b/crates/types/src/hybrid_compute.rs
@@ -21,12 +21,19 @@ use std::{
use ethers::{
abi::{AbiDecode, AbiEncode},
signers::{LocalWallet, Signer},
- types::{Address, BigEndianHash, Bytes, RecoveryMessage::Data, H256, U256},
+ types::{Address, BigEndianHash, Bytes, RecoveryMessage::Data, H256, U128, U256},
utils::keccak256,
};
use once_cell::sync::Lazy;
-use crate::{user_operation::UserOperation, v0_6::UserOperation as UserOperationV0_6};
+use crate::{
+ chain::ChainSpec,
+ user_operation::{
+ v0_6::UserOperationOptionalGas as UserOperationOptionalGasV0_6,
+ v0_7::UserOperationOptionalGas as UserOperationOptionalGasV0_7, UserOperation,
+ UserOperationOptionalGas,
+ },
+};
#[derive(Clone, Debug)]
/// Error code
@@ -44,16 +51,18 @@ pub struct HcEntry {
pub sub_key: H256,
/// Merged key, used for end-of-bundle cleanup
pub map_key: H256,
- /// Extracted calldata
- //pub call_data: Bytes,
+ /// Extracted calldata (also in user_op; duplicated for easier access)
+ pub call_data: Bytes,
/// Full operation
- pub user_op: UserOperationV0_6,
+ pub user_op: UserOperationOptionalGas,
/// Creation timestamp, used to prune expired entries
pub ts: SystemTime,
/// The total computed offchain gas (all 3 phases)
pub oc_gas: U256,
/// The required preVerificationGas incl. HC overhead (set during successful gas estimation)
pub needed_pvg: U256,
+ /// Version flag
+ pub is_v7: bool,
}
const EXPIRE_SECS: std::time::Duration = Duration::new(120, 0);
@@ -63,11 +72,12 @@ impl Clone for HcEntry {
HcEntry {
sub_key: self.sub_key,
map_key: self.map_key,
- //call_data: self.call_data.clone(),
+ call_data: self.call_data.clone(),
user_op: self.user_op.clone(),
ts: self.ts,
oc_gas: self.oc_gas,
needed_pvg: self.needed_pvg,
+ is_v7: self.is_v7,
}
}
}
@@ -77,7 +87,7 @@ static HC_MAP: Lazy>> = Lazy::new(||
Mutex::new(m)
});
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug)]
/// Parameters needed for Hybrid Compute, accessed from various modules.
pub struct HcCfg {
/// Helper contract address
@@ -88,14 +98,14 @@ pub struct HcCfg {
pub sys_owner: Address,
/// Private key for sys_account
pub sys_privkey: H256,
- /// EntryPoint contract address (currently only 1 EP is supported)
- pub entry_point: Address,
- /// Chain ID
- pub chain_id: u64,
+ /// Chain spec
+ pub chain_spec: ChainSpec,
/// Temporary workaround; would be better to use an existing Provider.
pub node_http: String,
/// Temporary workaround
pub from_addr: Address,
+ /// Index of ResponseCache slot in HCHelper
+ pub slot_idx: U256,
}
//pub static mut HC_CONFIG: HcCfg = HcCfg { helper_addr:Address::zero(), sys_account:Address::zero(), sys_owner:Address::zero(), sys_privkey:H256::zero(), entry_point: Address::zero(), chain_id: 0, node_http:String::new(), from_addr: Address::zero()};
@@ -107,10 +117,10 @@ pub static HC_CONFIG: Lazy> = Lazy::new(|| {
sys_account: Address::zero(),
sys_owner: Address::zero(),
sys_privkey: H256::zero(),
- entry_point: Address::zero(),
- chain_id: 0,
+ chain_spec: ChainSpec::default(),
node_http: String::new(),
from_addr: Address::zero(),
+ slot_idx: U256::from(0),
};
Mutex::new(c)
});
@@ -121,9 +131,9 @@ pub fn init(
sys_account: Address,
sys_owner: Address,
sys_privkey: H256,
- entry_point: Address,
- chain_id: u64,
+ chain_spec: ChainSpec,
node_http: String,
+ slot_idx: U256,
) {
let mut cfg = HC_CONFIG.lock().unwrap();
@@ -131,9 +141,9 @@ pub fn init(
cfg.sys_account = sys_account;
cfg.sys_owner = sys_owner;
cfg.sys_privkey = sys_privkey;
- cfg.entry_point = entry_point;
- cfg.chain_id = chain_id;
+ cfg.chain_spec = chain_spec;
cfg.node_http.clone_from(&node_http);
+ cfg.slot_idx = slot_idx;
}
/// Set the EOA address which the bundler is using. Erigon, but not geth, needs this for tx simulation
@@ -205,11 +215,11 @@ pub fn hc_map_key(revert_data: &Bytes) -> H256 {
/// Calculates the HCHelper storage slot key for a ResponseCache entry
pub fn hc_storage_key(map_key: H256) -> H256 {
- let slot_idx = "0x0000000000000000000000000000000000000000000000000000000000000000"
- .parse::()
- .unwrap();
+ let cfg = HC_CONFIG.lock().unwrap();
+ let slot_idx_bytes: Bytes = cfg.slot_idx.encode().into();
+
let storage_key: H256 =
- keccak256([Bytes::from(map_key.to_fixed_bytes()), slot_idx].concat()).into();
+ keccak256([Bytes::from(map_key.to_fixed_bytes()), slot_idx_bytes].concat()).into();
storage_key
}
@@ -220,7 +230,7 @@ pub fn hc_sub_key(revert_data: &Bytes) -> H256 {
}
/// Endpoint address (address of HybridAccount which called HCHelper)
-pub fn hc_ep_addr(revert_data: &Bytes) -> Address {
+pub fn hc_ha_addr(revert_data: &Bytes) -> Address {
Address::from_slice(&revert_data[8..28])
}
@@ -243,11 +253,12 @@ fn make_external_op(
op_success: bool,
response_payload: &Bytes,
sub_key: H256,
- ep_addr: Address,
+ ha_addr: Address,
sig_hex: String,
oo_nonce: U256,
cfg: &HcCfg,
-) -> UserOperationV0_6 {
+ is_v7: bool,
+) -> (UserOperationOptionalGas, Bytes) {
let tmp_bytes: Bytes = Bytes::from(response_payload.to_vec());
let err_code: u32 = if op_success { 0 } else { 1 };
@@ -267,56 +278,83 @@ fn make_external_op(
call_gas,
call_data
);
+ if is_v7 {
+ let mut new_op = UserOperationOptionalGasV0_7 {
+ sender: ha_addr,
+ nonce: oo_nonce,
+ call_data: call_data.clone(),
+ call_gas_limit: Some(U128::from(call_gas)),
+ verification_gas_limit: Some(U128::from(0x10000)),
+ pre_verification_gas: Some(U256::from(0x10000)),
+ max_fee_per_gas: Some(U128::zero()),
+ max_priority_fee_per_gas: Some(U128::zero()),
+ paymaster_data: Bytes::new(),
+ signature: Bytes::new(),
+ paymaster: None,
+ factory: None,
+ factory_data: Bytes::new(),
+ paymaster_verification_gas_limit: None,
+ paymaster_post_op_gas_limit: None,
+ };
- let mut new_op: UserOperationV0_6 = UserOperationV0_6 {
- sender: ep_addr,
- nonce: oo_nonce,
- init_code: Bytes::new(),
- call_data: call_data.clone(),
- call_gas_limit: U256::from(call_gas),
- verification_gas_limit: U256::from(0x10000),
- pre_verification_gas: U256::from(0x10000),
- max_fee_per_gas: U256::zero(),
- max_priority_fee_per_gas: U256::zero(),
- paymaster_and_data: Bytes::new(),
- signature: Bytes::new(),
- };
+ new_op.signature = sig_hex.parse::().unwrap();
+ (UserOperationOptionalGas::V0_7(new_op), call_data)
+ } else {
+ let mut new_op = UserOperationOptionalGasV0_6 {
+ sender: ha_addr,
+ nonce: oo_nonce,
+ init_code: Bytes::new(),
+ call_data: call_data.clone(),
+ call_gas_limit: Some(U256::from(call_gas)),
+ verification_gas_limit: Some(U256::from(0x10000)),
+ pre_verification_gas: Some(U256::from(0x10000)),
+ max_fee_per_gas: Some(U256::zero()),
+ max_priority_fee_per_gas: Some(U256::zero()),
+ paymaster_and_data: Bytes::new(),
+ signature: Bytes::new(),
+ };
- new_op.signature = sig_hex.parse::().unwrap();
+ new_op.signature = sig_hex.parse::().unwrap();
- new_op
+ (UserOperationOptionalGas::V0_6(new_op), call_data)
+ }
}
/// Processes an external hybrid compute op.
#[allow(clippy::too_many_arguments)] // FIXME later
pub async fn external_op(
+ entry_point: Address,
op_key: H256,
src_addr: Address,
nonce: U256,
op_success: bool,
response_payload: &Bytes,
sub_key: H256,
- ep_addr: Address,
+ ha_addr: Address,
sig_hex: String,
oo_nonce: U256,
map_key: H256,
cfg: &HcCfg,
ha_owner: Address,
nn: U256,
+ is_v7: bool,
) -> HcErr {
- let mut new_op = make_external_op(
+ let (mut new_op, mut new_cd) = make_external_op(
src_addr,
nonce,
op_success,
response_payload,
sub_key,
- ep_addr,
+ ha_addr,
sig_hex.clone(),
oo_nonce,
cfg,
+ is_v7,
);
- let check_hash = new_op.hash(cfg.entry_point, cfg.chain_id);
+ let check_hash = new_op
+ .into_variant(&cfg.chain_spec)
+ .hash(entry_point, cfg.chain_spec.id);
let check_sig: ethers::types::Signature =
ethers::types::Signature::from_str(&sig_hex).expect("Signature decode");
let check_msg: ethers::types::RecoveryMessage = Data(check_hash.to_fixed_bytes().to_vec());
@@ -325,37 +363,56 @@ pub async fn external_op(
code: 0,
message: "".to_string(),
};
-
if check_sig.verify(check_msg, ha_owner).is_err() {
println!("HC Bad offchain signature");
hc_err = HcErr {
code: 3,
message: "HC03: Bad offchain signature".to_string(),
};
- new_op = make_err_op(hc_err.clone(), sub_key, src_addr, nn, oo_nonce, cfg);
+ let key_bytes: Bytes = cfg.sys_privkey.as_fixed_bytes().into();
+ let wallet = LocalWallet::from_bytes(&key_bytes).unwrap();
+
+ (new_op, new_cd) = make_err_op(
+ hc_err.clone(),
+ sub_key,
+ src_addr,
+ nn,
+ oo_nonce,
+ cfg,
+ entry_point,
+ wallet,
+ is_v7,
+ )
+ .await;
}
let ent: HcEntry = HcEntry {
sub_key,
map_key,
+ call_data: new_cd.clone(),
user_op: new_op.clone(),
ts: SystemTime::now(),
oc_gas: U256::zero(),
needed_pvg: U256::zero(),
+ is_v7,
};
HC_MAP.lock().unwrap().insert(op_key, ent);
hc_err
}
-fn make_err_op(
+#[allow(clippy::too_many_arguments)] // FIXME later
+async fn make_err_op(
err_hc: HcErr,
sub_key: H256,
src_addr: Address,
nn: U256,
oo_nonce: U256,
cfg: &HcCfg,
-) -> UserOperationV0_6 {
+ entry_point: Address,
+ wallet: LocalWallet,
+ is_v7: bool,
+) -> (UserOperationOptionalGas, Bytes) {
let response_payload: Bytes =
AbiEncode::encode((src_addr, nn, err_hc.code, err_hc.message)).into();
@@ -364,23 +421,58 @@ fn make_err_op(
sub_key,
Bytes::from(response_payload.to_vec()),
);
- println!("HC err_op call_data {:?}", call_data);
-
- let new_op: UserOperationV0_6 = UserOperationV0_6 {
- sender: cfg.sys_account,
- nonce: oo_nonce,
- init_code: Bytes::new(),
- call_data: call_data.clone(),
- call_gas_limit: U256::from(0x40000),
- verification_gas_limit: U256::from(0x10000),
- pre_verification_gas: U256::from(0x10000),
- max_fee_per_gas: U256::zero(),
- max_priority_fee_per_gas: U256::zero(),
- paymaster_and_data: Bytes::new(),
- signature: Bytes::new(),
- };
- new_op
+ if is_v7 {
+ let mut new_op = UserOperationOptionalGasV0_7 {
+ sender: cfg.sys_account,
+ nonce: oo_nonce,
+ call_data: call_data.clone(),
+ call_gas_limit: Some(U128::from(0x40000)),
+ verification_gas_limit: Some(U128::from(0x10000)),
+ pre_verification_gas: Some(U256::from(0x10000)),
+ max_fee_per_gas: Some(U128::zero()),
+ max_priority_fee_per_gas: Some(U128::zero()),
+ paymaster_data: Bytes::new(),
+ signature: Bytes::new(),
+ paymaster: None,
+ factory: None,
+ factory_data: Bytes::new(),
+ paymaster_verification_gas_limit: None,
+ paymaster_post_op_gas_limit: None,
+ };
+ let hh = UserOperationOptionalGas::V0_7(new_op.clone())
+ .into_variant(&cfg.chain_spec)
+ .hash(entry_point, cfg.chain_spec.id);
+ let signature = wallet.sign_message(hh).await;
+ let sig_bytes: Bytes = signature.as_ref().unwrap().to_vec().into();
+ println!("HC err_op signed {:?} {:?}", signature, sig_bytes);
+ new_op.signature = sig_bytes;
+
+ (UserOperationOptionalGas::V0_7(new_op), call_data)
+ } else {
+ let mut new_op = UserOperationOptionalGasV0_6 {
+ sender: cfg.sys_account,
+ nonce: oo_nonce,
+ init_code: Bytes::new(),
+ call_data: call_data.clone(),
+ call_gas_limit: Some(U256::from(0x40000)),
+ verification_gas_limit: Some(U256::from(0x10000)),
+ pre_verification_gas: Some(U256::from(0x10000)),
+ max_fee_per_gas: Some(U256::zero()),
+ max_priority_fee_per_gas: Some(U256::zero()),
+ paymaster_and_data: Bytes::new(),
+ signature: Bytes::new(),
+ };
+ let hh = UserOperationOptionalGas::V0_6(new_op.clone())
+ .into_variant(&cfg.chain_spec)
+ .hash(entry_point, cfg.chain_spec.id);
+ let signature = wallet.sign_message(hh).await;
+ let sig_bytes: Bytes = signature.as_ref().unwrap().to_vec().into();
+ println!("HC err_op signed {:?} {:?}", signature, sig_bytes);
+ new_op.signature = sig_bytes;
+
+ (UserOperationOptionalGas::V0_6(new_op), call_data)
+ }
}
/// Encapsulate an error code into a UserOperation
@@ -395,63 +487,113 @@ pub async fn err_op(
oo_nonce: U256,
map_key: H256,
cfg: &HcCfg,
+ is_v7: bool,
) {
println!(
"HC hybrid_compute err_op op_key {:?} err_str {:?}",
op_key, err_hc.message
);
assert!(err_hc.code >= 2);
- let mut new_op = make_err_op(err_hc, sub_key, src_addr, nn, oo_nonce, cfg);
let key_bytes: Bytes = cfg.sys_privkey.as_fixed_bytes().into();
let wallet = LocalWallet::from_bytes(&key_bytes).unwrap();
- let hh = new_op.hash(entry_point, cfg.chain_id);
-
- let signature = wallet.sign_message(hh).await;
- new_op.signature = signature.as_ref().unwrap().to_vec().into();
- println!("HC err_op signed {:?} {:?}", signature, new_op.signature);
+ let (new_op, new_cd) = make_err_op(
+ err_hc,
+ sub_key,
+ src_addr,
+ nn,
+ oo_nonce,
+ cfg,
+ entry_point,
+ wallet,
+ is_v7,
+ )
+ .await;
let ent: HcEntry = HcEntry {
sub_key,
map_key,
+ call_data: new_cd.clone(),
user_op: new_op.clone(),
ts: SystemTime::now(),
oc_gas: U256::zero(),
needed_pvg: U256::zero(),
+ is_v7,
};
HC_MAP.lock().unwrap().insert(op_key, ent);
}
/// Encapsulate a RemoveResponses into a UserOperation
-pub async fn rr_op(cfg: &HcCfg, oo_nonce: U256, keys: Vec) -> UserOperationV0_6 {
+pub async fn rr_op(
+ cfg: &HcCfg,
+ entry_point: Address,
+ oo_nonce: U256,
+ keys: Vec,
+ is_v7: bool,
+) -> UserOperationOptionalGas {
let call_data = make_rr_calldata(keys);
println!("HC rr_op call_data {:?}", call_data);
- let mut new_op: UserOperationV0_6 = UserOperationV0_6 {
- sender: cfg.sys_account,
- nonce: oo_nonce,
- init_code: Bytes::new(),
- call_data: call_data.clone(),
- call_gas_limit: U256::from(0x6000),
- verification_gas_limit: U256::from(0x10000),
- pre_verification_gas: U256::from(0x10000),
- max_fee_per_gas: U256::zero(),
- max_priority_fee_per_gas: U256::zero(),
- paymaster_and_data: Bytes::new(),
- signature: Bytes::new(),
- };
+ if is_v7 {
+ let mut new_op = UserOperationOptionalGasV0_7 {
+ sender: cfg.sys_account,
+ nonce: oo_nonce,
+ call_data: call_data.clone(),
+ call_gas_limit: Some(U128::from(0x6000)),
+ verification_gas_limit: Some(U128::from(0x10000)),
+ pre_verification_gas: Some(U256::from(0x10000)),
+ max_fee_per_gas: Some(U128::zero()),
+ max_priority_fee_per_gas: Some(U128::zero()),
+ paymaster_data: Bytes::new(),
+ signature: Bytes::new(),
+ paymaster: None,
+ factory: None,
+ factory_data: Bytes::new(),
+ paymaster_verification_gas_limit: None,
+ paymaster_post_op_gas_limit: None,
+ };
- let key_bytes: Bytes = cfg.sys_privkey.as_fixed_bytes().into();
- let wallet = LocalWallet::from_bytes(&key_bytes).unwrap();
+ let key_bytes: Bytes = cfg.sys_privkey.as_fixed_bytes().into();
+ let wallet = LocalWallet::from_bytes(&key_bytes).unwrap();
+
+ let hh = UserOperationOptionalGas::V0_7(new_op.clone())
+ .into_variant(&cfg.chain_spec)
+ .hash(entry_point, cfg.chain_spec.id);
- let hh = new_op.hash(cfg.entry_point, cfg.chain_id);
- println!("HC pre_sign hash {:?}", hh);
+ let signature = wallet.sign_message(hh).await;
+ new_op.signature = signature.as_ref().unwrap().to_vec().into();
+ println!("HC rr_op signed {:?} {:?}", signature, new_op.signature);
- let signature = wallet.sign_message(hh).await;
- new_op.signature = signature.as_ref().unwrap().to_vec().into();
- println!("HC rr_op signed {:?} {:?}", signature, new_op.signature);
+ UserOperationOptionalGas::V0_7(new_op)
+ } else {
+ let mut new_op = UserOperationOptionalGasV0_6 {
+ sender: cfg.sys_account,
+ nonce: oo_nonce,
+ init_code: Bytes::new(),
+ call_data: call_data.clone(),
+ call_gas_limit: Some(U256::from(0x6000)),
+ verification_gas_limit: Some(U256::from(0x10000)),
+ pre_verification_gas: Some(U256::from(0x10000)),
+ max_fee_per_gas: Some(U256::zero()),
+ max_priority_fee_per_gas: Some(U256::zero()),
+ paymaster_and_data: Bytes::new(),
+ signature: Bytes::new(),
+ };
- new_op
+ let key_bytes: Bytes = cfg.sys_privkey.as_fixed_bytes().into();
+ let wallet = LocalWallet::from_bytes(&key_bytes).unwrap();
+
+ let hh = UserOperationOptionalGas::V0_6(new_op.clone())
+ .into_variant(&cfg.chain_spec)
+ .hash(entry_point, cfg.chain_spec.id);
+ println!("HC pre_sign hash {:?}", hh);
+
+ let signature = wallet.sign_message(hh).await;
+ new_op.signature = signature.as_ref().unwrap().to_vec().into();
+ println!("HC rr_op signed {:?} {:?}", signature, new_op.signature);
+
+ UserOperationOptionalGas::V0_6(new_op)
+ }
}
/// Retrieve a cached HC operation
@@ -467,7 +609,7 @@ pub fn del_hc_ent(key: H256) {
/// Retrieve the PutResponse() payload from a cached HC operation
pub fn get_hc_op_payload(key: H256) -> Bytes {
let op = HC_MAP.lock().unwrap().get(&key).cloned().unwrap();
- let cd1 = &op.user_op.call_data[4..];
+ let cd1 = &op.call_data[4..];
let dec1 = <(Address, U256, Bytes) as AbiDecode>::decode(cd1).unwrap();
let cd2 = &dec1.2[4..];
let dec2 = <(H256, Bytes) as AbiDecode>::decode(cd2).unwrap();
@@ -520,10 +662,12 @@ pub fn hc_set_pvg(key: H256, needed_pvg: U256, oc_gas: U256) {
let new_ent = HcEntry {
sub_key: ent.sub_key,
map_key: ent.map_key,
+ call_data: ent.call_data.clone(),
user_op: ent.user_op.clone(),
ts: ent.ts,
needed_pvg,
oc_gas,
+ is_v7: ent.is_v7,
};
map.remove(&key);
map.insert(key, new_ent);
@@ -563,11 +707,9 @@ mod test {
"0x1111111111111111111111111111111111111111111111111111111111111111"
.parse::()
.unwrap(),
- "0x0000000000000000000000000000000000000004"
- .parse::()
- .unwrap(),
- 123,
+ ChainSpec::default(),
"http://test.local/rpc".to_string(),
+ U256::from(2),
);
set_signer(
"0x0000000000000000000000000000000000000005"
@@ -588,17 +730,22 @@ mod test {
sys_privkey: "0x1111111111111111111111111111111111111111111111111111111111111111"
.parse::()
.unwrap(),
- entry_point: "0x0000000000000000000000000000000000000004"
- .parse::()
- .unwrap(),
- chain_id: 123,
+ chain_spec: ChainSpec::default(),
node_http: "http://test.local/rpc".to_string(),
from_addr: "0x0000000000000000000000000000000000000005"
.parse::()
.unwrap(),
+ slot_idx: U256::from(2),
};
let cfg: HcCfg = HC_CONFIG.lock().unwrap().clone();
- assert_eq!(expected, cfg);
+ // chain_spec doesn't support PartialEq
+ assert_eq!(expected.helper_addr, cfg.helper_addr);
+ assert_eq!(expected.sys_account, cfg.sys_account);
+ assert_eq!(expected.sys_owner, cfg.sys_owner);
+ assert_eq!(expected.sys_privkey, cfg.sys_privkey);
+ assert_eq!(expected.chain_spec.id, cfg.chain_spec.id);
+ assert_eq!(expected.node_http, cfg.node_http);
+ assert_eq!(expected.from_addr, cfg.from_addr);
}
#[test]
@@ -620,7 +767,7 @@ mod test {
let e_sub_key = "0x16d7f606293dca5dbbe97735b2913e6dade6e3f216310b12148cb67a6fd86947"
.parse::()
.unwrap();
- let e_ep_addr = "0x9c6df0d4c9d8f527221b59c66ad5279c16a1dbc2"
+ let e_ha_addr = "0x9c6df0d4c9d8f527221b59c66ad5279c16a1dbc2"
.parse::()
.unwrap();
let e_sel = [151, 224, 215, 186];
@@ -630,8 +777,8 @@ mod test {
assert_eq!(e_map_key, map_key);
let sub_key = hc_sub_key(&rev_data);
assert_eq!(e_sub_key, sub_key);
- let ep_addr = hc_ep_addr(&rev_data);
- assert_eq!(e_ep_addr, ep_addr);
+ let ha_addr = hc_ha_addr(&rev_data);
+ assert_eq!(e_ha_addr, ha_addr);
let sel = hc_selector(&rev_data);
assert_eq!(e_sel, sel);
let payload = hc_req_payload(&rev_data);
@@ -645,7 +792,7 @@ mod test {
let payload = "0x0000000000000000000000000000000000000000000000000000000000000002"
.parse::()
.unwrap();
- let op = make_external_op(
+ let (op, _) = make_external_op(
"0x1000000000000000000000000000000000000001".parse::().unwrap(),
U256::from(100),
true,
@@ -655,27 +802,36 @@ mod test {
"0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c".to_string(),
U256::from(222),
&cfg,
+ true,
);
let e_calldata = "0xb61d27f60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000124dfc98ae82222222222222222222222222222222222222222222222222222222222222222000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000010000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000".parse::().unwrap();
- let expected:UserOperationV0_6 = UserOperationV0_6{
+ let expected = UserOperationOptionalGasV0_7 {
sender: "0x2000000000000000000000000000000000000002".parse::().unwrap(),
nonce: U256::from(222),
- init_code: Bytes::new(),
call_data: e_calldata,
- call_gas_limit: U256::from(192560),
- verification_gas_limit: U256::from(65536),
- pre_verification_gas: U256::from(65536),
- max_fee_per_gas: U256::from(0),
- max_priority_fee_per_gas: U256::from(0),
- paymaster_and_data: Bytes::new(),
+ call_gas_limit: Some(U128::from(192560)),
+ verification_gas_limit: Some(U128::from(65536)),
+ pre_verification_gas: Some(U256::from(65536)),
+ max_fee_per_gas: Some(U128::from(0)),
+ max_priority_fee_per_gas: Some(U128::from(0)),
+ paymaster_data: Bytes::new(),
signature: "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c".parse::().unwrap(),
+ paymaster: None,
+ factory: None,
+ factory_data: Bytes::new(),
+ paymaster_verification_gas_limit: None,
+ paymaster_post_op_gas_limit: None,
};
- assert_eq!(expected, op);
+ if let UserOperationOptionalGas::V0_7(op7) = op {
+ assert_eq!(expected, op7);
+ } else {
+ panic!("Invalud UserOperation variant");
+ }
}
- #[test]
- fn test_op_gen_error() {
+ #[tokio::test]
+ async fn test_op_gen_error() {
let cfg = HcCfg {
helper_addr: "0x0000000000000000000000000000000000000001"
.parse::()
@@ -689,17 +845,16 @@ mod test {
sys_privkey: "0x1111111111111111111111111111111111111111111111111111111111111111"
.parse::()
.unwrap(),
- entry_point: "0x0000000000000000000000000000000000000004"
- .parse::()
- .unwrap(),
- chain_id: 123,
+ chain_spec: ChainSpec::default(),
node_http: "http://test.local/rpc".to_string(),
from_addr: "0x0000000000000000000000000000000000000005"
.parse::()
.unwrap(),
+ slot_idx: U256::from(0),
};
- let op = make_err_op(
+ let wallet = LocalWallet::from_bytes(&cfg.sys_privkey.to_fixed_bytes()).unwrap();
+ let op_future = make_err_op(
HcErr {
code: 4,
message: "unit test".to_string(),
@@ -713,24 +868,40 @@ mod test {
U256::from(100),
U256::from(222),
&cfg,
+ "0x0000000000000000000000000000000000000004"
+ .parse::()
+ .unwrap(),
+ wallet,
+ true,
);
let e_calldata = "0xb61d27f60000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000124fde89b642222222222222222222222222222222222222222222222222222222222222222000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000020000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000009756e69742074657374000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".parse::().unwrap();
- let expected: UserOperationV0_6 = UserOperationV0_6 {
+ let expected = UserOperationOptionalGasV0_7 {
sender: "0x0000000000000000000000000000000000000002"
.parse::()
.unwrap(),
nonce: U256::from(222),
- init_code: Bytes::new(),
call_data: e_calldata,
- call_gas_limit: U256::from(262144),
- verification_gas_limit: U256::from(65536),
- pre_verification_gas: U256::from(65536),
- max_fee_per_gas: U256::from(0),
- max_priority_fee_per_gas: U256::from(0),
- paymaster_and_data: Bytes::new(),
- signature: Bytes::new(),
+ call_gas_limit: Some(U128::from(262144)),
+ verification_gas_limit: Some(U128::from(65536)),
+ pre_verification_gas: Some(U256::from(65536)),
+ max_fee_per_gas: Some(U128::from(0)),
+ max_priority_fee_per_gas: Some(U128::from(0)),
+ paymaster_data: Bytes::new(),
+ signature:"0xfe10d7b74cc08f195b44caa8ce3dbd084941c5b26231f456d54cd101efe4e1ea37793d70c6732bbea5f10e9214ea2596fd5dfaf3f7c162b0c4d41a5d5eca64af1b"
+ .parse::()
+ .unwrap(),
+ paymaster: None,
+ factory: None,
+ factory_data: Bytes::new(),
+ paymaster_verification_gas_limit: None,
+ paymaster_post_op_gas_limit: None,
};
- assert_eq!(expected, op);
+ let (op, _) = op_future.await;
+ if let UserOperationOptionalGas::V0_7(op7) = op {
+ assert_eq!(expected, op7);
+ } else {
+ panic!("Invalud UserOperation variant");
+ }
}
}
diff --git a/crates/types/src/user_operation/mod.rs b/crates/types/src/user_operation/mod.rs
index 5e1c0ea4..d46d46af 100644
--- a/crates/types/src/user_operation/mod.rs
+++ b/crates/types/src/user_operation/mod.rs
@@ -15,7 +15,7 @@ use std::{fmt::Debug, time::Duration};
use ethers::{
abi::AbiEncode,
- types::{Address, Bytes, H256, U256},
+ types::{Address, Bytes, H256, U128, U256},
};
/// User Operation types for Entry Point v0.6
@@ -375,6 +375,28 @@ impl UserOperationOptionalGas {
};
abi_size + BUNDLE_BYTE_OVERHEAD + USER_OP_OFFSET_WORD_SIZE
}
+
+ /// Hash fields relevant to Hybrid Compute
+ pub fn hc_hash(&self) -> H256 {
+ match self {
+ UserOperationOptionalGas::V0_6(op) => op.hc_hash(),
+ UserOperationOptionalGas::V0_7(op) => op.hc_hash(),
+ }
+ }
+
+ /// Convert into UserOperationVariant type - needed for Hybrid Compute
+ pub fn into_variant(&self, cs: &ChainSpec) -> UserOperationVariant {
+ match self {
+ UserOperationOptionalGas::V0_6(op) => UserOperationVariant::V0_6(
+ op.clone().into_user_operation(U256::from(0), U256::from(0)),
+ ),
+ UserOperationOptionalGas::V0_7(op) => UserOperationVariant::V0_7(
+ op.clone()
+ .into_user_operation_builder(cs, U128::from(0), U128::from(0), U128::from(0))
+ .build(),
+ ),
+ }
+ }
}
/// Gas estimate
diff --git a/crates/types/src/user_operation/v0_6.rs b/crates/types/src/user_operation/v0_6.rs
index 8a502118..408d11c6 100644
--- a/crates/types/src/user_operation/v0_6.rs
+++ b/crates/types/src/user_operation/v0_6.rs
@@ -284,7 +284,7 @@ impl AsMut for super::UserOperationVariant {
}
/// User operation with optional gas fields for gas estimation
-#[derive(Serialize, Deserialize, Clone, Debug)]
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] // PartialEq for hybrid_compute
#[serde(rename_all = "camelCase")]
pub struct UserOperationOptionalGas {
/// Sender (required)
@@ -401,6 +401,13 @@ impl UserOperationOptionalGas {
rand::thread_rng().fill_bytes(&mut bytes);
bytes.into()
}
+
+ /// Hash fields relevant to Hybrid Compute
+ pub fn hc_hash(&self) -> H256 {
+ self.clone()
+ .into_user_operation(U256::from(0), U256::from(0))
+ .hc_hash()
+ }
}
impl From for UserOperationOptionalGas {
diff --git a/crates/types/src/user_operation/v0_7.rs b/crates/types/src/user_operation/v0_7.rs
index d9bf8b86..b77f1250 100644
--- a/crates/types/src/user_operation/v0_7.rs
+++ b/crates/types/src/user_operation/v0_7.rs
@@ -111,7 +111,10 @@ impl UserOperationTrait for UserOperation {
}
fn hc_hash(&self) -> H256 {
- H256::zero() // Not yet implemented
+ keccak256(encode(&[Token::FixedBytes(
+ keccak256(self.pack_for_hc_hash()).to_vec(),
+ )]))
+ .into()
}
fn id(&self) -> UserOperationId {
@@ -254,6 +257,22 @@ impl UserOperation {
pub fn packed(&self) -> &PackedUserOperation {
&self.packed
}
+
+ /// Gets the byte array representation of the user operation to be used as HC key
+ pub fn pack_for_hc_hash(&self) -> Bytes {
+ let hash_init_code = keccak256(self.packed.init_code.clone());
+ let hash_call_data = keccak256(self.call_data.clone());
+ let hash_paymaster_and_data = keccak256(self.packed.paymaster_and_data.clone());
+
+ encode(&[
+ Token::Address(self.sender),
+ Token::Uint(self.nonce),
+ Token::FixedBytes(hash_init_code.to_vec()),
+ Token::FixedBytes(hash_call_data.to_vec()),
+ Token::FixedBytes(hash_paymaster_and_data.to_vec()), // ???
+ ])
+ .into()
+ }
}
impl From for UserOperation {
@@ -504,6 +523,15 @@ impl UserOperationOptionalGas {
rand::thread_rng().fill_bytes(&mut bytes);
bytes.into()
}
+
+ /// Hash fields relevant to Hybrid Compute
+ pub fn hc_hash(&self) -> H256 {
+ let cs = ChainSpec::default();
+ self.clone()
+ .into_user_operation_builder(&cs, U128::from(0), U128::from(0), U128::from(0))
+ .build()
+ .hc_hash()
+ }
}
impl From for UserOperationOptionalGas {
diff --git a/hybrid-compute/aa-client.py b/hybrid-compute/aa-client.py
index 13edb195..fd571079 100644
--- a/hybrid-compute/aa-client.py
+++ b/hybrid-compute/aa-client.py
@@ -50,7 +50,7 @@ def build_op(to_contract, value_in_wei, initcode_hex, calldata_hex):
'preVerificationGas': "0x0",
'maxFeePerGas': Web3.to_hex(w3.eth.gas_price),
'maxPriorityFeePerGas': Web3.to_hex(w3.eth.max_priority_fee),
- 'paymasterAndData':"0x",
+# 'paymasterAndData':"0x",
'signature': '0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c'
}
return p
@@ -187,7 +187,7 @@ def ParseReceipt(opReceipt):
EP_addr = response.json()['result'][0]
vprint("Detected EntryPoint address", EP_addr)
-aa = aa_utils(EP_addr, w3.eth.chain_id)
+aa = aa_rpc(EP_addr, w3, args.bundler_rpc)
vprint("gasPrices", w3.eth.gas_price, w3.eth.max_priority_fee)
diff --git a/hybrid-compute/aa_utils/__init__.py b/hybrid-compute/aa_utils/__init__.py
index 3f68561f..31fc3ffe 100644
--- a/hybrid-compute/aa_utils/__init__.py
+++ b/hybrid-compute/aa_utils/__init__.py
@@ -35,6 +35,46 @@ def sign_op(self, op, signer_key):
op['signature'] = Web3.to_hex(sig.signature)
return op
+ def sign_v7_op(self, user_op, signer_key):
+ """Signs a UserOperation, returning a modified op containing a 'signature' field."""
+ op = dict(user_op) # Derived fields are added to 'op' prior to hashing
+
+ assert 'paymaster' not in op # not yet implemented
+
+ # The deploy-local script supplies the packed values prior to signature, as it bypasses the bundler.
+ # For normal UserOperations the fields are derived here
+ if 'accountGasLimits' not in op:
+ account_gas_limits = ethabi.encode(['uint128'],[Web3.to_int(hexstr=op['verificationGasLimit'])])[16:32] \
+ + ethabi.encode(['uint128'],[Web3.to_int(hexstr=op['callGasLimit'])])[16:32]
+ else:
+ account_gas_limits = Web3.to_bytes(hexstr=op['accountGasLimits'])
+
+ if 'gasFees' not in op:
+ gas_fees = ethabi.encode(['uint128'],[Web3.to_int(hexstr=op['maxPriorityFeePerGas'])])[16:32] \
+ + ethabi.encode(['uint128'],[Web3.to_int(hexstr=op['maxFeePerGas'])])[16:32]
+ else:
+ gas_fees = Web3.to_bytes(hexstr=op['gasFees'])
+
+ if 'paymasterAndData' not in op:
+ op['paymasterAndData'] = "0x"
+
+ pack1 = ethabi.encode(['address','uint256','bytes32','bytes32','bytes32','uint256','bytes32','bytes32'], \
+ [op['sender'],
+ Web3.to_int(hexstr=op['nonce']),
+ Web3.keccak(hexstr="0x"), # initcode
+ Web3.keccak(hexstr=op['callData']),
+ account_gas_limits,
+ Web3.to_int(hexstr=op['preVerificationGas']),
+ gas_fees,
+ Web3.keccak(hexstr=op['paymasterAndData']),
+ ])
+ pack2 = ethabi.encode(['bytes32','address','uint256'], [Web3.keccak(pack1), self.EP_addr, self.chain_id])
+ e_msg = eth_account.messages.encode_defunct(Web3.keccak(pack2))
+ signer_acct = eth_account.account.Account.from_key(signer_key)
+ sig = signer_acct.sign_message(e_msg)
+ user_op['signature'] = Web3.to_hex(sig.signature)
+ return user_op
+
class aa_rpc(aa_utils):
"""Provides AA helper methods which talk to an ETH node and/or a Bundler"""
def __init__(self, _EP_addr, _eth_rpc, _bundler_url):
@@ -69,22 +109,24 @@ def build_op(self, sender, target, value, calldata, nonce_key=0):
op = {
'sender': sender,
'nonce': self.aa_nonce(sender,nonce_key),
- 'initCode': '0x',
+ #factory - none
+ #factoryData - none
'callData': Web3.to_hex(ex_calldata),
'callGasLimit': "0x0",
'verificationGasLimit': Web3.to_hex(0),
'preVerificationGas': "0x0",
'maxFeePerGas': Web3.to_hex(fee),
'maxPriorityFeePerGas': Web3.to_hex(tip),
- 'paymasterAndData': '0x',
+ #paymaster - none
+ #paymasterVerificationGasLimit - none
+ #paymasterPostOpGasLimit - none
+ #paymasterData - none
# Dummy signature, per Alchemy AA documentation
- # A future update may require a valid signature on gas estimation ops. This should be safe because the gas
- # limits in the signed request are set to zero, therefore it would be rejected if a third party attempted to
- # submit it as a real transaction.
'signature': '0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c'
}
print("Built userOperation", op)
return op
+
def estimate_op_gas(self, op, extra_pvg=0, extra_vg=0, extra_cg=0):
""" Wrapper to call eth_estimateUserOperationGas() and update the op.
Allows limits to be increased in cases where a bundler is
@@ -100,21 +142,31 @@ def estimate_op_gas(self, op, extra_pvg=0, extra_vg=0, extra_cg=0):
print("*** eth_estimateUserOperationGas failed")
time.sleep(2)
return False, op
- else:
- est_result = response.json()['result']
-
- op['preVerificationGas'] = Web3.to_hex(Web3.to_int(
- hexstr=est_result['preVerificationGas']) + extra_pvg)
- op['verificationGasLimit'] = Web3.to_hex(Web3.to_int(
- hexstr=est_result['verificationGasLimit']) + extra_vg)
- op['callGasLimit'] = Web3.to_hex(Web3.to_int(
- hexstr=est_result['callGasLimit']) + extra_cg)
+
+ est_result = response.json()['result']
+
+ op['preVerificationGas'] = Web3.to_hex(Web3.to_int(
+ hexstr=est_result['preVerificationGas']) + extra_pvg)
+ op['verificationGasLimit'] = Web3.to_hex(Web3.to_int(
+ hexstr=est_result['verificationGasLimit']) + extra_vg)
+ op['callGasLimit'] = Web3.to_hex(Web3.to_int(
+ hexstr=est_result['callGasLimit']) + extra_cg)
return True, op
def sign_submit_op(self, op, owner_key):
"""Sign and submit a UserOperation to the Bundler"""
- op = self.sign_op(op, owner_key)
+ is_v7 = False
+ if self.EP_addr == "0x0000000071727De22E5E9d8BAf0edAc6f37da032":
+ is_v7 = True
+ else:
+ assert self.EP_addr == "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"
+
+ if is_v7:
+ op = self.sign_v7_op(op, owner_key)
+ else:
+ op = self.sign_op(op, owner_key)
+
while True:
response = requests.post(self.bundler_url, json=request(
"eth_sendUserOperation", params=[op, self.EP_addr]))
@@ -184,13 +236,13 @@ def sign_and_submit(self, tx, key):
def approve_token(self, token, spender, deploy_addr, deploy_key):
"""Perform an unlimited ERC20 token approval"""
- approveCD = selector("approve(address,uint256)") + ethabi.encode(
+ approve_calldata = selector("approve(address,uint256)") + ethabi.encode(
['address','uint256'],
[spender, Web3.to_int(hexstr="0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")])
tx = {
'from': deploy_addr,
- 'data': approveCD,
+ 'data': approve_calldata,
'to': token,
}
print("ERC20 approval of", token, "for", spender)
diff --git a/hybrid-compute/deploy-local.py b/hybrid-compute/deploy-local.py
index 26b53e16..3a1bcbe2 100644
--- a/hybrid-compute/deploy-local.py
+++ b/hybrid-compute/deploy-local.py
@@ -18,9 +18,17 @@
parser = argparse.ArgumentParser()
parser.add_argument("--boba-path", required=True, help="Path to your local Boba/Optimism repository")
parser.add_argument("--deploy-salt", required=False, help="Salt value for contract deployment", default="0")
+parser.add_argument("--ep-version", required=False, help="EntryPoint contract version (0.6|0.7)", default="0.7")
cli_args = parser.parse_args()
+ep7 = False
+
+if cli_args.ep_version == "0.7":
+ ep7 = True
+elif cli_args.ep_version != "0.6":
+ assert "Invalid EntryPoint version (0.6 or 0.7 are supported)" == False
+
with open(cli_args.boba_path + "/.devnet/addresses.json", "r", encoding="ascii") as f:
jj = json.load(f)
boba_l1_addr = Web3.to_checksum_address(jj['BOBA'])
@@ -66,7 +74,10 @@
l2_util = eth_utils(w3)
contract_info = {}
-OUT_PREFIX = "../crates/types/contracts/out/"
+if ep7:
+ OUT_PREFIX = "../crates/types/contracts/out/hc0_7/"
+else:
+ OUT_PREFIX = "../crates/types/contracts/out/hc0_6/"
def load_contract(w, name, path, address):
"""Loads a contract's JSON ABI"""
@@ -84,7 +95,7 @@ def load_contract(w, name, path, address):
return w.eth.contract(abi=contract_info[name]['abi'], address=address)
-def submit_as_op(addr, calldata, signer_key):
+def submit_as_v6_op(addr, calldata, signer_key):
"""Wrapper to build and submit a UserOperation directly to the int. We don't
have a Bundler to run gas estimation so the values are hard-coded. It might be
necessary to change these values e.g. if simulating different L1 prices on the local devnet"""
@@ -125,6 +136,47 @@ def submit_as_op(addr, calldata, signer_key):
return l2_util.sign_and_submit(ho, deploy_key)
+def submit_as_v7_op(addr, calldata, signer_key):
+ """Wrapper to build and submit a UserOperation directly to the EntryPoint. We don't
+ have a Bundler to run gas estimation so the values are hard-coded. It might be
+ necessary to change these values e.g. if simulating different L1 prices on the local devnet"""
+
+ gasLimits = "0x00000000000000000000000000016ed900000000000000000000000000053652"
+ gasFees = "0x00000000000000000000000039d106800000000000000000000000025b9c274c"
+
+ op = {
+ 'sender':addr,
+ 'nonce': aa.aa_nonce(addr, 1235),
+ 'initCode':"0x",
+ 'callData': Web3.to_hex(calldata),
+ 'accountGasLimits': gasLimits,
+ 'preVerificationGas': "0xF0000",
+ 'gasFees': gasFees,
+ 'paymasterAndData':"0x",
+ 'signature': '0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c'
+ }
+
+ op = aa.sign_v7_op(op, signer_key)
+
+ # Because the bundler is not running yet we must call the EntryPoint directly.
+ ho = EP.functions.handleOps([(
+ op['sender'],
+ Web3.to_int(hexstr=op['nonce']),
+ op['initCode'],
+ op['callData'],
+ Web3.to_bytes(hexstr=op['accountGasLimits']),
+ Web3.to_int(hexstr=op['preVerificationGas']),
+ Web3.to_bytes(hexstr=op['gasFees']),
+ op['paymasterAndData'],
+ op['signature'],
+ )], deploy_addr).build_transaction({
+ 'from': deploy_addr,
+ 'value': 0,
+ })
+ ho['gas'] = int(w3.eth.estimate_gas(ho) * 1.2)
+
+ return l2_util.sign_and_submit(ho, deploy_key)
+
def permit_caller(acct, caller):
"""Whitelist a contract to call a HybridAccount. Now implemented as
a UserOperation rather than requiring the Owner to be an EOA."""
@@ -134,7 +186,10 @@ def permit_caller(acct, caller):
calldata = selector("PermitCaller(address,bool)") + \
ethabi.encode(['address','bool'], [caller, True])
- submit_as_op(acct.address, calldata, env_vars['OC_PRIVKEY'])
+ if ep7:
+ submit_as_v7_op(acct.address, calldata, env_vars['OC_PRIVKEY'])
+ else:
+ submit_as_v6_op(acct.address, calldata, env_vars['OC_PRIVKEY'])
def register_url(caller, url):
"""Associates a URL with the address of a HybridAccount contract"""
@@ -168,18 +223,6 @@ def fund_addr(addr):
tx['gasPrice'] = Web3.to_wei(1, 'gwei')
l2_util.sign_and_submit(tx, deploy_key)
-def fund_addr_ep(EP, addr):
- """Deposit funds for an address into the EntryPoint"""
- if EP.functions.deposits(addr).call()[0] < Web3.to_wei(0.005, 'ether'):
- print("Funding acct (depositTo)", addr)
- tx = EP.functions.depositTo(addr).build_transaction({
- 'from': deploy_addr,
- 'value': Web3.to_wei(0.01, "ether")
- })
- l2_util.sign_and_submit(tx, deploy_key)
- print("Balances for", addr, Web3.from_wei(w3.eth.get_balance(addr), 'ether'),
- Web3.from_wei(EP.functions.deposits(addr).call()[0], 'ether'))
-
def deploy_account(factory, owner):
"""Deploy an account using a Factory contract"""
calldata = selector("createAccount(address,uint256)") + ethabi.encode(['address','uint256'],[owner,0])
@@ -199,9 +242,15 @@ def deploy_forge(script, cmd_env):
args = ["/home/enya/.foundry/bin/forge", "script", "--silent", "--json", "--broadcast"]
args.append("--rpc-url=http://127.0.0.1:9545")
args.append("--contracts")
- args.append("src/hc0_6")
- args.append("--remappings")
- args.append("@openzeppelin/=lib/openzeppelin-contracts-versions/v4_9")
+ if ep7:
+ args.append("src/hc0_7")
+ args.append("--remappings")
+ args.append("@openzeppelin/=lib/openzeppelin-contracts-versions/v5_0")
+ else:
+ args.append("src/hc0_6")
+ args.append("--remappings")
+ args.append("@openzeppelin/=lib/openzeppelin-contracts-versions/v4_9")
+
args.append(script)
sys_env = os.environ.copy()
@@ -209,7 +258,14 @@ def deploy_forge(script, cmd_env):
cmd_env['PRIVATE_KEY'] = deploy_key
cmd_env['DEPLOY_ADDR'] = deploy_addr
cmd_env['DEPLOY_SALT'] = cli_args.deploy_salt # Update to force redeployment
- cmd_env['ENTRY_POINTS'] = env_vars['ENTRY_POINTS']
+
+ if 'ENTRY_POINTS' in env_vars:
+ cmd_env['ENTRY_POINTS'] = env_vars['ENTRY_POINTS']
+ elif ep7:
+ cmd_env['ENTRY_POINTS'] = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"
+ else:
+ cmd_env['ENTRY_POINTS'] = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"
+ print("Using EntryPoint address:", cmd_env['ENTRY_POINTS'])
out = subprocess.run(args, cwd="../crates/types/contracts", env=cmd_env,
capture_output=True, check=True)
@@ -232,14 +288,22 @@ def deploy_base():
cmd_env = {}
cmd_env['HC_SYS_OWNER'] = env_vars['HC_SYS_OWNER']
cmd_env['BOBA_TOKEN'] = boba_token
- addrs = deploy_forge("hc_scripts/LocalDeploy.s.sol", cmd_env)
+ if ep7:
+ addrs = deploy_forge("hc_scripts/LocalDeploy_v7.s.sol", cmd_env)
+ else:
+ addrs = deploy_forge("hc_scripts/LocalDeploy_v6.s.sol", cmd_env)
+
print("Deployed base contracts:", addrs)
return addrs.split(',')
def deploy_examples(hybrid_acct_addr):
cmd_env = {}
cmd_env['OC_HYBRID_ACCOUNT'] = hybrid_acct_addr
- addrs = deploy_forge("hc_scripts/ExampleDeploy.s.sol", cmd_env)
+ if ep7:
+ addrs = deploy_forge("hc_scripts/ExampleDeploy_v7.s.sol", cmd_env)
+ else:
+ addrs = deploy_forge("hc_scripts/ExampleDeploy_v6.s.sol", cmd_env)
+
print("Deployed example contracts:", addrs)
return addrs.split(',')
@@ -259,7 +323,10 @@ def boba_balance(addr):
bal = w3.eth.call({'to':boba_token, 'data':bal_calldata})
return Web3.to_int(bal)
-EP = load_contract(w3, "EntryPoint", "../crates/types/contracts/lib/account-abstraction-versions/v0_6/deployments/optimism/EntryPoint.json", "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789")
+if ep7:
+ EP = load_contract(w3, "EntryPoint", "../crates/types/contracts/out/v0_7/EntryPoint.sol/EntryPoint.json", "0x0000000071727De22E5E9d8BAf0edAc6f37da032")
+else:
+ EP = load_contract(w3, "EntryPoint", "../crates/types/contracts/lib/account-abstraction-versions/v0_6/deployments/optimism/EntryPoint.json", "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789")
assert l1.eth.get_balance(deploy_addr) > Web3.to_wei(1000, 'ether')
@@ -325,10 +392,10 @@ def boba_balance(addr):
fund_addr(client_addr)
ha1_addr = deploy_account(haf_addr, env_vars['OC_OWNER'])
-fund_addr_ep(EP, ha1_addr)
+fund_addr(ha1_addr)
HA = load_contract(w3, 'HybridAccount', OUT_PREFIX + "HybridAccount.sol/HybridAccount.json", ha1_addr)
-SA = load_contract(w3, 'SimpleAccount', OUT_PREFIX + "SimpleAccount.sol/SimpleAccount.json", client_addr)
+#SA = load_contract(w3, 'SimpleAccount', OUT_PREFIX + "SimpleAccount.sol/SimpleAccount.json", client_addr)
example_addrs = deploy_examples(ha1_addr)
diff --git a/hybrid-compute/local.env b/hybrid-compute/local.env
index e57e1e32..6dd0c11e 100644
--- a/hybrid-compute/local.env
+++ b/hybrid-compute/local.env
@@ -12,4 +12,3 @@ OC_OWNER=0xE073fC0ff8122389F6e693DD94CcDc5AF637448e
OC_PRIVKEY=0x7c0c629efc797f8c5f658919b7efbae01275470d59d03fdeb0fca1e6bd11d7fa
CLIENT_OWNER=0x77Fe14A710E33De68855b0eA93Ed8128025328a9
CLIENT_PRIVKEY=0x541b3e3b20b8bb0e5bae310b2d4db4c8b7912ba09750e6ff161b7e67a26a9bf7
-ENTRY_POINTS=0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789
diff --git a/hybrid-compute/offchain/add_sub_2/add_sub_2_offchain.py b/hybrid-compute/offchain/add_sub_2/add_sub_2_offchain.py
index c427b71e..431154b1 100644
--- a/hybrid-compute/offchain/add_sub_2/add_sub_2_offchain.py
+++ b/hybrid-compute/offchain/add_sub_2/add_sub_2_offchain.py
@@ -1,13 +1,13 @@
from web3 import Web3
from eth_abi import abi as ethabi
-from offchain_utils import gen_response, parse_req
+from offchain_utils import gen_response_v7, parse_req
def offchain_addsub2(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args):
print(" -> offchain_addsub2 handler called with ver={} subkey={} src_addr={} src_nonce={} oo_nonce={} payload={} extra_args={}".format(
ver, sk, src_addr, src_nonce, oo_nonce, payload, args))
err_code = 1
resp = Web3.to_bytes(text="unknown error")
- assert ver == "0.2"
+ assert ver == "0.3"
try:
req = parse_req(sk, src_addr, src_nonce, oo_nonce, payload)
@@ -24,4 +24,4 @@ def offchain_addsub2(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args):
except Exception as e:
print("DECODE FAILED", e)
- return gen_response(req, err_code, resp)
+ return gen_response_v7(req, err_code, resp)
diff --git a/hybrid-compute/offchain/add_sub_2/add_sub_2_test.py b/hybrid-compute/offchain/add_sub_2/add_sub_2_test.py
index ee2d1e48..128dde2d 100644
--- a/hybrid-compute/offchain/add_sub_2/add_sub_2_test.py
+++ b/hybrid-compute/offchain/add_sub_2/add_sub_2_test.py
@@ -3,12 +3,12 @@
def TestAddSub2(aa, a, b):
print(f"\n - - - - TestAddSub2({a},{b}) - - - -")
- print("TestCount(begin)=", TC.functions.counters(SA.address).call())
+ print("TestCount(begin)=", TC.functions.counters(u_account).call())
count_call = selector("count(uint32,uint32)") + \
ethabi.encode(['uint32', 'uint32'], [a, b])
- op = aa.build_op(SA.address, TC.address, 0, count_call, nKey)
+ op = aa.build_op(u_account, TC.address, 0, count_call, nKey)
(success, op) = estimateOp(aa, op)
if not success:
@@ -17,4 +17,4 @@ def TestAddSub2(aa, a, b):
rcpt = aa.sign_submit_op(op, u_key)
ParseReceipt(rcpt)
- print("TestCount(end)=", TC.functions.counters(SA.address).call())
+ print("TestCount(end)=", TC.functions.counters(u_account).call())
diff --git a/hybrid-compute/offchain/auction_system/auction_system_offchain.py b/hybrid-compute/offchain/auction_system/auction_system_offchain.py
index 046f3157..ba3781a9 100644
--- a/hybrid-compute/offchain/auction_system/auction_system_offchain.py
+++ b/hybrid-compute/offchain/auction_system/auction_system_offchain.py
@@ -1,6 +1,6 @@
from web3 import Web3
from eth_abi import abi as ethabi
-from offchain_utils import gen_response, parse_req
+from offchain_utils import gen_response_v7, parse_req
blacklist = ["0x123"]
@@ -9,7 +9,7 @@ def offchain_auction(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args):
src_addr, src_nonce, oo_nonce, payload, args))
err_code = 0
resp = Web3.to_bytes(text="unknown error")
- assert(ver == "0.2")
+ assert(ver == "0.3")
try:
req = parse_req(sk, src_addr, src_nonce, oo_nonce, payload)
@@ -26,4 +26,4 @@ def offchain_auction(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args):
err_code = 1
print("DECODE FAILED", e)
- return gen_response(req, err_code, resp)
+ return gen_response_v7(req, err_code, resp)
diff --git a/hybrid-compute/offchain/auction_system/auction_system_test.py b/hybrid-compute/offchain/auction_system/auction_system_test.py
index ffb4aa66..b79abc65 100644
--- a/hybrid-compute/offchain/auction_system/auction_system_test.py
+++ b/hybrid-compute/offchain/auction_system/auction_system_test.py
@@ -4,9 +4,9 @@
def TestAuction(aa):
print("\n - - - - TestAuction() - - - -")
- start_auction_call = selector("createAuction(uint256,address)") + ethabi.encode(['uint256', 'address'], [300, SA.address])
+ start_auction_call = selector("createAuction(uint256,address)") + ethabi.encode(['uint256', 'address'], [300, u_account])
- op = aa.build_op(SA.address, TEST_AUCTION.address, 0, start_auction_call, nKey)
+ op = aa.build_op(u_account, TEST_AUCTION.address, 0, start_auction_call, nKey)
(success, op) = estimateOp(aa, op)
assert success
@@ -22,7 +22,7 @@ def bid(aa, auctionId):
print("\n - - - - bid() - - - -")
bid_call = selector("bid(uint256)") + ethabi.encode(['uint256'], [auctionId])
- op = aa.build_op(SA.address, TEST_AUCTION.address, 6, bid_call, nKey)
+ op = aa.build_op(u_account, TEST_AUCTION.address, 6, bid_call, nKey)
(success, op) = estimateOp(aa, op)
assert success
diff --git a/hybrid-compute/offchain/check_kyc/check_kyc_offchain.py b/hybrid-compute/offchain/check_kyc/check_kyc_offchain.py
index 222c5e45..f9a7b910 100644
--- a/hybrid-compute/offchain/check_kyc/check_kyc_offchain.py
+++ b/hybrid-compute/offchain/check_kyc/check_kyc_offchain.py
@@ -1,6 +1,6 @@
from web3 import Web3
from eth_abi import abi as ethabi
-from offchain_utils import gen_response, parse_req
+from offchain_utils import gen_response_v7, parse_req
validWallets = ["0x123"]
@@ -9,7 +9,7 @@ def offchain_checkkyc(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args):
src_addr, src_nonce, oo_nonce, payload, args))
err_code = 0
resp = Web3.to_bytes(text="unknown error")
- assert(ver == "0.2")
+ assert(ver == "0.3")
try:
req = parse_req(sk, src_addr, src_nonce, oo_nonce, payload)
@@ -26,4 +26,4 @@ def offchain_checkkyc(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args):
err_code = 1
print("DECODE FAILED", e)
- return gen_response(req, err_code, resp)
+ return gen_response_v7(req, err_code, resp)
diff --git a/hybrid-compute/offchain/check_kyc/check_kyc_test.py b/hybrid-compute/offchain/check_kyc/check_kyc_test.py
index c8fa129a..0a919bc3 100644
--- a/hybrid-compute/offchain/check_kyc/check_kyc_test.py
+++ b/hybrid-compute/offchain/check_kyc/check_kyc_test.py
@@ -14,7 +14,7 @@
def TestKyc(aa, isValid: bool):
print("\n - - - - TestKyc({}) - - - -".format(isValid))
- print("SA ADDRESS {}".format(SA.address))
+ print("SA ADDRESS {}".format(u_account))
print("TestKyc begin")
kycCall = None
@@ -24,7 +24,7 @@ def TestKyc(aa, isValid: bool):
else:
kycCall = selector("openForKyced(string)") + ethabi.encode(['string'], [""])
- op = aa.build_op(SA.address, KYC.address, 0, kycCall, nKey)
+ op = aa.build_op(u_account, KYC.address, 0, kycCall, nKey)
(success, op) = estimateOp(aa, op)
assert success == isValid
diff --git a/hybrid-compute/offchain/get_token_price/get_token_price_offchain.py b/hybrid-compute/offchain/get_token_price/get_token_price_offchain.py
index 5c3f8d2e..714feb1b 100644
--- a/hybrid-compute/offchain/get_token_price/get_token_price_offchain.py
+++ b/hybrid-compute/offchain/get_token_price/get_token_price_offchain.py
@@ -1,7 +1,7 @@
from web3 import Web3
import requests
from eth_abi import abi as ethabi
-from offchain_utils import gen_response, parse_req
+from offchain_utils import gen_response_v7, parse_req
def offchain_getprice(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args):
@@ -9,7 +9,7 @@ def offchain_getprice(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args):
src_addr, src_nonce, oo_nonce, payload, args))
err_code = 0
resp = Web3.to_bytes(text="unknown error")
- assert(ver == "0.2")
+ assert(ver == "0.3")
try:
req = parse_req(sk, src_addr, src_nonce, oo_nonce, payload)
@@ -66,4 +66,4 @@ def offchain_getprice(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args):
except Exception as e:
print("DECODE FAILED", e)
- return gen_response(req, err_code, resp)
+ return gen_response_v7(req, err_code, resp)
diff --git a/hybrid-compute/offchain/get_token_price/get_token_price_test.py b/hybrid-compute/offchain/get_token_price/get_token_price_test.py
index e7f73977..97aa227a 100644
--- a/hybrid-compute/offchain/get_token_price/get_token_price_test.py
+++ b/hybrid-compute/offchain/get_token_price/get_token_price_test.py
@@ -18,7 +18,7 @@ def TestTokenPrice(aa, tokenSymbol):
calldata = selector("fetchPrice(string)") + \
ethabi.encode(['string'], [tokenSymbol])
- op = aa.build_op(SA.address, TFP.address, 0, calldata, nKey)
+ op = aa.build_op(u_account, TFP.address, 0, calldata, nKey)
(success, op) = estimateOp(aa, op)
assert success
diff --git a/hybrid-compute/offchain/offchain_utils.py b/hybrid-compute/offchain/offchain_utils.py
index a2a4780e..c1b5283f 100644
--- a/hybrid-compute/offchain/offchain_utils.py
+++ b/hybrid-compute/offchain/offchain_utils.py
@@ -81,7 +81,72 @@ def gen_response(req, err_code, resp_payload):
e_msg = eth_account.messages.encode_defunct(oo_hash)
sig = signer_acct.sign_message(e_msg)
- success = (err_code == 0)
+ success = err_code == 0
+ print("Method returning success={} response={} signature={}".format(
+ success, Web3.to_hex(resp_payload), Web3.to_hex(sig.signature)))
+ return ({
+ "success": success,
+ "response": Web3.to_hex(resp_payload),
+ "signature": Web3.to_hex(sig.signature)
+ })
+
+def gen_response_v7(req, err_code, resp_payload):
+ resp2 = ethabi.encode(['address', 'uint256', 'uint32', 'bytes'], [
+ req['srcAddr'], req['srcNonce'], err_code, resp_payload])
+ p_enc1 = selector("PutResponse(bytes32,bytes)") + \
+ ethabi.encode(['bytes32', 'bytes'], [req['skey'], resp2]) # dfc98ae8
+
+ p_enc2 = selector("execute(address,uint256,bytes)") + \
+ ethabi.encode(['address', 'uint256', 'bytes'], [
+ Web3.to_checksum_address(HelperAddr), 0, p_enc1]) # b61d27f6
+
+ limits = {
+ 'verificationGasLimit': "0x10000",
+ 'preVerificationGas': "0x10000",
+ }
+
+ # This call_gas formula is a "close enough" estimate for the initial implementation.
+ # A more accurate model, or a protocol enhancement to run an actual simulation, may
+ # be required in the future.
+ call_gas = 705*len(resp_payload) + 170000
+
+ print("call_gas calculation", len(resp_payload), 4+len(p_enc2), call_gas)
+
+ account_gas_limits = \
+ ethabi.encode(['uint128'],[Web3.to_int(hexstr=limits['verificationGasLimit'])])[16:32] + \
+ ethabi.encode(['uint128'],[call_gas])[16:32]
+
+ gas_fees = Web3.to_bytes(
+ hexstr="0x0000000000000000000000000000000000000000000000000000000000000000"
+ )
+
+ packed = ethabi.encode([
+ 'address',
+ 'uint256',
+ 'bytes32',
+ 'bytes32',
+ 'bytes32',
+ 'uint256',
+ 'bytes32',
+ 'bytes32',
+ ], [
+ HybridAcctAddr,
+ req['opNonce'],
+ Web3.keccak(Web3.to_bytes(hexstr='0x')), # initCode
+ Web3.keccak(p_enc2),
+ account_gas_limits,
+ Web3.to_int(hexstr=limits['preVerificationGas']),
+ gas_fees,
+ Web3.keccak(Web3.to_bytes(hexstr='0x')), # paymasterAndData
+ ])
+ oo_hash = Web3.keccak(ethabi.encode(['bytes32', 'address', 'uint256'], [
+ Web3.keccak(packed), EntryPointAddr, HC_CHAIN]))
+
+ signer_acct = eth_account.account.Account.from_key(hc1_key)
+ e_msg = eth_account.messages.encode_defunct(oo_hash)
+ sig = signer_acct.sign_message(e_msg)
+
+ success = err_code == 0
print("Method returning success={} response={} signature={}".format(
success, Web3.to_hex(resp_payload), Web3.to_hex(sig.signature)))
return ({
diff --git a/hybrid-compute/offchain/rainfall_insurance/rainfall_insurance_offchain.py b/hybrid-compute/offchain/rainfall_insurance/rainfall_insurance_offchain.py
index ff329b2b..2c586a47 100644
--- a/hybrid-compute/offchain/rainfall_insurance/rainfall_insurance_offchain.py
+++ b/hybrid-compute/offchain/rainfall_insurance/rainfall_insurance_offchain.py
@@ -3,7 +3,7 @@
from web3 import Web3
import requests
from eth_abi import abi as ethabi
-from offchain_utils import gen_response, parse_req
+from offchain_utils import gen_response_v7, parse_req
load_dotenv()
@@ -16,7 +16,7 @@ def offchain_getrainfall(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args)
src_addr, src_nonce, oo_nonce, payload, args))
err_code = 0
resp = Web3.to_bytes(text="unknown error")
- assert(ver == "0.2")
+ assert(ver == "0.3")
try:
req = parse_req(sk, src_addr, src_nonce, oo_nonce, payload)
@@ -53,6 +53,6 @@ def offchain_getrainfall(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args)
except Exception as e:
print("DECODE FAILED", e)
- return gen_response(req, err_code, resp)
+ return gen_response_v7(req, err_code, resp)
diff --git a/hybrid-compute/offchain/rainfall_insurance/rainfall_insurance_test.py b/hybrid-compute/offchain/rainfall_insurance/rainfall_insurance_test.py
index efc4da5b..854f86cd 100644
--- a/hybrid-compute/offchain/rainfall_insurance/rainfall_insurance_test.py
+++ b/hybrid-compute/offchain/rainfall_insurance/rainfall_insurance_test.py
@@ -15,7 +15,7 @@ def test_rainfall_insurance_purchase(aa):
calldata = selector("buyInsurance(uint256,string)") + \
ethabi.encode(['uint256','string'],[trigger_rainfall, city])
- op = aa.build_op(SA.address, TEST_RAINFALL_INSURANCE.address, premium, calldata, nKey)
+ op = aa.build_op(u_account, TEST_RAINFALL_INSURANCE.address, premium, calldata, nKey)
(success, op) = estimateOp(aa, op)
assert success
@@ -38,7 +38,7 @@ def test_rainfall_insurance_payout(aa, policy_id):
ethabi.encode(['address', 'uint256', 'bytes'], [
TEST_RAINFALL_INSURANCE.address, 0, payout_call])
- op = aa.build_op(SA.address, TEST_RAINFALL_INSURANCE.address, 0, payout_call, nKey)
+ op = aa.build_op(u_account, TEST_RAINFALL_INSURANCE.address, 0, payout_call, nKey)
(success, op) = estimateOp(aa, op)
assert success
diff --git a/hybrid-compute/offchain/ramble/ramble_offchain.py b/hybrid-compute/offchain/ramble/ramble_offchain.py
index cee138ac..a1c8b6c4 100644
--- a/hybrid-compute/offchain/ramble/ramble_offchain.py
+++ b/hybrid-compute/offchain/ramble/ramble_offchain.py
@@ -2,7 +2,7 @@
import random
from web3 import Web3
from eth_abi import abi as ethabi
-from offchain_utils import gen_response, parse_req
+from offchain_utils import gen_response_v7, parse_req
def load_words():
"""Loads a list of dictionary words, assumes a standard file path"""
@@ -24,7 +24,7 @@ def offchain_ramble(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args):
src_addr, src_nonce, oo_nonce, payload, args))
err_code = 1
resp = Web3.to_bytes(text="unknown error")
- assert ver == "0.2"
+ assert ver == "0.3"
try:
req = parse_req(sk, src_addr, src_nonce, oo_nonce, payload)
@@ -51,4 +51,4 @@ def offchain_ramble(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args):
except Exception as e:
print("DECODE FAILED", e)
- return gen_response(req, err_code, resp)
+ return gen_response_v7(req, err_code, resp)
diff --git a/hybrid-compute/offchain/ramble/ramble_test.py b/hybrid-compute/offchain/ramble/ramble_test.py
index 609d5eb0..937a7285 100644
--- a/hybrid-compute/offchain/ramble/ramble_test.py
+++ b/hybrid-compute/offchain/ramble/ramble_test.py
@@ -11,7 +11,7 @@ def TestWordGuess(aa, n, cheat):
print("Pool balance before playing =", Web3.from_wei(
TC.functions.Pool().call(), 'gwei'))
- op = aa.build_op(SA.address, TC.address, n * per_entry, game_call, nKey)
+ op = aa.build_op(u_account, TC.address, n * per_entry, game_call, nKey)
(success, op) = estimateOp(aa, op)
assert success
diff --git a/hybrid-compute/offchain/sports_betting/sports_betting_offchain.py b/hybrid-compute/offchain/sports_betting/sports_betting_offchain.py
index 56b15742..6be3f42c 100644
--- a/hybrid-compute/offchain/sports_betting/sports_betting_offchain.py
+++ b/hybrid-compute/offchain/sports_betting/sports_betting_offchain.py
@@ -1,12 +1,13 @@
from web3 import Web3
from eth_abi import abi as ethabi
-from offchain_utils import gen_response, parse_req
+from offchain_utils import gen_response_v7, parse_req
def offchain_sports_betting(ver, sk, src_addr, src_nonce, oo_nonce, payload, *args):
print(" -> offchain_sport_betting handler called with subkey={} src_addr={} src_nonce={} oo_nonce={} payload={} extra_args={}".format(sk,
src_addr, src_nonce, oo_nonce, payload, args))
err_code = 0
resp = Web3.to_bytes(text="unknown error")
+ assert ver == "0.3"
try:
req = parse_req(sk, src_addr, src_nonce, oo_nonce, payload)
@@ -30,7 +31,7 @@ def offchain_sports_betting(ver, sk, src_addr, src_nonce, oo_nonce, payload, *ar
err_code = 1
print("DECODE FAILED", e)
- return gen_response(req, err_code, resp)
+ return gen_response_v7(req, err_code, resp)
def get_game_score(game_id):
# This is a dummy function to simulate the offchain data retrieval
diff --git a/hybrid-compute/offchain/sports_betting/sports_betting_test.py b/hybrid-compute/offchain/sports_betting/sports_betting_test.py
index 0b133478..a1fc1645 100644
--- a/hybrid-compute/offchain/sports_betting/sports_betting_test.py
+++ b/hybrid-compute/offchain/sports_betting/sports_betting_test.py
@@ -3,7 +3,7 @@
import time
def TestSportsBetting(aa):
print("\n - - - - SportBetting() - - - -")
- print("SA ADDRESS {}".format(SA.address))
+ print("SA ADDRESS {}".format(u_account))
game_id = 456
create_bet(aa, game_id)
@@ -21,7 +21,7 @@ def create_bet(aa, game_id):
print("--------------------Create Bet--------------------")
create_call = selector("createGame(uint256)") + ethabi.encode(['uint256'], [game_id])
- op = aa.build_op(SA.address, TEST_SPORTS_BETTING.address, 0, create_call, nKey)
+ op = aa.build_op(u_account, TEST_SPORTS_BETTING.address, 0, create_call, nKey)
(success, op) = estimateOp(aa, op)
assert success
@@ -38,7 +38,7 @@ def place_bet(aa, game_id):
[game_id, outcome])
amount_to_bet = 2
- op = aa.build_op(SA.address, TEST_SPORTS_BETTING.address, amount_to_bet, place_bet, nKey)
+ op = aa.build_op(u_account, TEST_SPORTS_BETTING.address, amount_to_bet, place_bet, nKey)
(success, op) = estimateOp(aa, op)
assert success
@@ -52,7 +52,7 @@ def settle_bet(aa, game_id):
print("--------------------Settle Bet--------------------")
settle_bet = selector("settleBet(uint256)") + ethabi.encode(['uint256'], [game_id])
- op = aa.build_op(SA.address, TEST_SPORTS_BETTING.address, 0, settle_bet, nKey)
+ op = aa.build_op(u_account, TEST_SPORTS_BETTING.address, 0, settle_bet, nKey)
time.sleep(5)
(success, op) = estimateOp(aa, op)
assert success
diff --git a/hybrid-compute/offchain/userop.py b/hybrid-compute/offchain/userop.py
index ee6fae8f..ad3f9a6d 100644
--- a/hybrid-compute/offchain/userop.py
+++ b/hybrid-compute/offchain/userop.py
@@ -18,9 +18,9 @@
print("Starting Balances:")
showBalances()
balStart_bnd = w3.eth.get_balance(bundler_addr)
-balStart_sa = EP.functions.getDepositInfo(SA.address).call()[0] + w3.eth.get_balance(SA.address)
+balStart_sa = EP.functions.getDepositInfo(u_account).call()[0] + w3.eth.get_balance(u_account)
-print("TestCount(start)=", TC.functions.counters(SA.address).call())
+print("TestCount(start)=", TC.functions.counters(u_account).call())
#print("TestFetchPrice(start)=", TFP.functions.counters(0).call())
# ===============================================
@@ -28,6 +28,7 @@
aa = aa_rpc(EP.address, w3, bundler_rpc)
TestAddSub2(aa, 2, 1) # Success
+
TestAddSub2(aa, 2, 10) # Underflow error, asserted
TestAddSub2(aa, 2, 3) # Underflow error, handled internally
TestAddSub2(aa, 7, 0) # Not HC
@@ -54,13 +55,13 @@
# ===============================================
-print("TestCount(final)=", TC.functions.counters(SA.address).call())
+print("TestCount(final)=", TC.functions.counters(u_account).call())
#print("TestFetchPrice(final)=", TFP.functions.counters(0).call())
print("\nFinal Balances:")
showBalances()
balFinal_bnd = w3.eth.get_balance(bundler_addr)
-balFinal_sa = EP.functions.getDepositInfo(SA.address).call()[0] + w3.eth.get_balance(SA.address)
+balFinal_sa = EP.functions.getDepositInfo(u_account).call()[0] + w3.eth.get_balance(u_account)
print("Net balance changes", balFinal_bnd - balStart_bnd, balFinal_sa - balStart_sa,
(balFinal_bnd + balFinal_sa) - (balStart_bnd + balStart_sa), (gasFees['l1Fees'] + gasFees['l2Fees']))
diff --git a/hybrid-compute/offchain/userop_utils.py b/hybrid-compute/offchain/userop_utils.py
index 47f16856..5bc15427 100644
--- a/hybrid-compute/offchain/userop_utils.py
+++ b/hybrid-compute/offchain/userop_utils.py
@@ -62,8 +62,9 @@
HH = w3.eth.contract(
address=deployed['HCHelper']['address'], abi=deployed['HCHelper']['abi'])
# This address is unique for each user, who deploys their own wallet account
-SA = w3.eth.contract(
- address=u_account, abi=deployed['SimpleAccount']['abi'])
+#SA = w3.eth.contract(
+# address=u_account, abi=deployed['SimpleAccount']['abi'])
+
HA = w3.eth.contract(address=deployed['HybridAccount']
['address'], abi=deployed['HybridAccount']['abi'])
TC = w3.eth.contract(
@@ -90,7 +91,7 @@ def showBalances():
print("bnd", EP.functions.getDepositInfo(
bundler_addr).call(), w3.eth.get_balance(bundler_addr))
print("SA ", EP.functions.getDepositInfo(
- SA.address).call(), w3.eth.get_balance(SA.address))
+ u_account).call(), w3.eth.get_balance(u_account))
print("HA ", EP.functions.getDepositInfo(
HA.address).call(), w3.eth.get_balance(HA.address))
print("TC ", EP.functions.getDepositInfo(
diff --git a/hybrid-compute/offchain/verify_captcha/captcha_offchain.py b/hybrid-compute/offchain/verify_captcha/captcha_offchain.py
index da65a04e..d9f050af 100644
--- a/hybrid-compute/offchain/verify_captcha/captcha_offchain.py
+++ b/hybrid-compute/offchain/verify_captcha/captcha_offchain.py
@@ -1,12 +1,13 @@
from web3 import Web3
import redis
from eth_abi import abi as ethabi
-from offchain_utils import gen_response, parse_req
+from offchain_utils import gen_response_v7, parse_req
def offchain_verifycaptcha(sk, src_addr, src_nonce, oo_nonce, payload, *args):
print(" -> offchain_verifycaptcha handler called with subkey={} src_addr={} src_nonce={} oo_nonce={} payload={} extra_args={}".format(sk,
src_addr, src_nonce, oo_nonce, payload, args))
+ assert ver == "0.3"
try:
req = parse_req(sk, src_addr, src_nonce, oo_nonce, payload)
@@ -24,9 +25,9 @@ def offchain_verifycaptcha(sk, src_addr, src_nonce, oo_nonce, payload, *args):
print("ismatch ", is_match)
print('captcha input ', captcha_input)
print("key decoded ", key_in_redis.decode('utf-8'))
- return gen_response(req, 0, ethabi.encode(["bool"], [is_match]))
+ return gen_response_v7(req, 0, ethabi.encode(["bool"], [is_match]))
else:
- return gen_response(req, 1, Web3.to_bytes(text="Error: uuid or to not found"))
+ return gen_response_v7(req, 1, Web3.to_bytes(text="Error: uuid or to not found"))
except Exception as e:
print("Error:", e)
diff --git a/hybrid-compute/offchain/verify_captcha/captcha_test.py b/hybrid-compute/offchain/verify_captcha/captcha_test.py
index 8bfb1e91..308b25fe 100644
--- a/hybrid-compute/offchain/verify_captcha/captcha_test.py
+++ b/hybrid-compute/offchain/verify_captcha/captcha_test.py
@@ -20,7 +20,7 @@
def TestCaptcha(user_addr):
global estGas
print("\n - - - - TestCaptcha({}) - - - -".format(user_addr))
- print("SA ADDRESS {}".format(SA.address))
+ print("SA ADDRESS {}".format(u_account))
print("TestCaptcha begin")
captcha = get_captcha(user_addr)
diff --git a/hybrid-compute/runit.sh b/hybrid-compute/runit.sh
index ffd1d596..f1d8cdde 100755
--- a/hybrid-compute/runit.sh
+++ b/hybrid-compute/runit.sh
@@ -6,6 +6,6 @@ RUST_BACKTRACE=1 ETH_POLL_INTERVAL_MILLIS=5000 \
--rpc.port 3300 \
--metrics.port 8380 \
--builder.private_keys $BUILDER_PRIVKEY \
- --disable_entry_point_v0_7 \
+ --disable_entry_point_v0_6 \
--builder.dropped_status_unsupported \
$@ 2>&1