Add call builder and eth_call state overrides (#1340)
* Add call builder and eth_call state overrides * Fix <Caller as Future>::poll impl * Move call overrides to ethers_provider::call_raw * Add example for call_raw overrides * Add support for contract call overrides * Documentation and convenience impls for call_raw types * Test for eth_call state overrides * ci: install geth Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
This commit is contained in:
parent
3b54f18e4e
commit
a532eb4d29
|
@ -177,6 +177,16 @@ jobs:
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install geth (for state overrides example)
|
||||||
|
run: |
|
||||||
|
mkdir -p "$HOME/bin"
|
||||||
|
wget -q https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.23-8c2f2715.tar.gz
|
||||||
|
tar -xvf geth-linux-amd64-1.9.23-8c2f2715.tar.gz
|
||||||
|
mv geth-linux-amd64-1.9.23-8c2f2715/geth $HOME/bin/geth
|
||||||
|
chmod u+x "$HOME/bin/geth"
|
||||||
|
export PATH=$HOME/bin:$PATH
|
||||||
|
geth version
|
||||||
|
|
||||||
- name: Install Anvil
|
- name: Install Anvil
|
||||||
uses: foundry-rs/foundry-toolchain@v1
|
uses: foundry-rs/foundry-toolchain@v1
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -9,9 +9,12 @@ use ethers_core::{
|
||||||
},
|
},
|
||||||
utils::id,
|
utils::id,
|
||||||
};
|
};
|
||||||
use ethers_providers::{Middleware, PendingTransaction, ProviderError};
|
use ethers_providers::{
|
||||||
|
call_raw::{CallBuilder, RawCall},
|
||||||
|
Middleware, PendingTransaction, ProviderError,
|
||||||
|
};
|
||||||
|
|
||||||
use std::{borrow::Cow, fmt::Debug, marker::PhantomData, sync::Arc};
|
use std::{borrow::Cow, fmt::Debug, future::Future, marker::PhantomData, sync::Arc};
|
||||||
|
|
||||||
use thiserror::Error as ThisError;
|
use thiserror::Error as ThisError;
|
||||||
|
|
||||||
|
@ -169,6 +172,37 @@ where
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns an implementer of [`RawCall`] which can be `.await`d to query the blockchain via
|
||||||
|
/// `eth_call`, returning the deoded return data.
|
||||||
|
///
|
||||||
|
/// The returned call can also be used to override the input parameters to `eth_call`.
|
||||||
|
///
|
||||||
|
/// Note: this function _does not_ send a transaction from your account
|
||||||
|
pub fn call_raw(
|
||||||
|
&self,
|
||||||
|
) -> impl RawCall<'_> + Future<Output = Result<D, ContractError<M>>> + Debug {
|
||||||
|
let call = self.call_raw_bytes();
|
||||||
|
call.map(move |res: Result<Bytes, ProviderError>| {
|
||||||
|
let bytes = res.map_err(ContractError::ProviderError)?;
|
||||||
|
decode_function_data(&self.function, &bytes, false).map_err(From::from)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a [`CallBuilder`] which can be `.await`d to query the blochcain via `eth_call`,
|
||||||
|
/// returning the raw bytes from the transaction.
|
||||||
|
///
|
||||||
|
/// The returned call can also be used to override the input parameters to `eth_call`.
|
||||||
|
///
|
||||||
|
/// Note: this function _does not_ send a transaction from your account
|
||||||
|
pub fn call_raw_bytes(&self) -> CallBuilder<'_, M::Provider> {
|
||||||
|
let call = self.client.provider().call_raw(&self.tx);
|
||||||
|
if let Some(block) = self.block {
|
||||||
|
call.block(block)
|
||||||
|
} else {
|
||||||
|
call
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Signs and broadcasts the provided transaction
|
/// Signs and broadcasts the provided transaction
|
||||||
pub async fn send(&self) -> Result<PendingTransaction<'_, M::Provider>, ContractError<M>> {
|
pub async fn send(&self) -> Result<PendingTransaction<'_, M::Provider>, ContractError<M>> {
|
||||||
self.client
|
self.client
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::{Contract, ContractError};
|
use crate::{Contract, ContractError};
|
||||||
use std::marker::PhantomData;
|
|
||||||
|
|
||||||
use ethers_core::{
|
use ethers_core::{
|
||||||
abi::{Abi, Token, Tokenize},
|
abi::{Abi, Token, Tokenize},
|
||||||
|
@ -8,12 +7,15 @@ use ethers_core::{
|
||||||
TransactionReceipt, TransactionRequest, U256, U64,
|
TransactionReceipt, TransactionRequest, U256, U64,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use ethers_providers::Middleware;
|
use ethers_providers::{
|
||||||
|
call_raw::{CallBuilder, RawCall},
|
||||||
|
Middleware,
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(not(feature = "legacy"))]
|
#[cfg(not(feature = "legacy"))]
|
||||||
use ethers_core::types::Eip1559TransactionRequest;
|
use ethers_core::types::Eip1559TransactionRequest;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::{marker::PhantomData, sync::Arc};
|
||||||
|
|
||||||
/// Helper which manages the deployment transaction of a smart contract.
|
/// Helper which manages the deployment transaction of a smart contract.
|
||||||
///
|
///
|
||||||
|
@ -114,6 +116,15 @@ impl<M: Middleware, C: From<Contract<M>>> ContractDeployer<M, C> {
|
||||||
self.deployer.call().await
|
self.deployer.call().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a CallBuilder, which when awaited executes the deployment of this contract via
|
||||||
|
/// `eth_call`. This call resolves to the returned data which would have been stored at the
|
||||||
|
/// destination address had the deploy transaction been executed via `send()`.
|
||||||
|
///
|
||||||
|
/// Note: this function _does not_ send a transaction from your account
|
||||||
|
pub fn call_raw(&self) -> CallBuilder<'_, M::Provider> {
|
||||||
|
self.deployer.call_raw()
|
||||||
|
}
|
||||||
|
|
||||||
/// Broadcasts the contract deployment transaction and after waiting for it to
|
/// Broadcasts the contract deployment transaction and after waiting for it to
|
||||||
/// be sufficiently confirmed (default: 1), it returns a new instance of the contract type at
|
/// be sufficiently confirmed (default: 1), it returns a new instance of the contract type at
|
||||||
/// the deployed contract's address.
|
/// the deployed contract's address.
|
||||||
|
@ -203,6 +214,15 @@ impl<M: Middleware> Deployer<M> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a CallBuilder, which when awaited executes the deployment of this contract via
|
||||||
|
/// `eth_call`. This call resolves to the returned data which would have been stored at the
|
||||||
|
/// destination address had the deploy transaction been executed via `send()`.
|
||||||
|
///
|
||||||
|
/// Note: this function _does not_ send a transaction from your account
|
||||||
|
pub fn call_raw(&self) -> CallBuilder<'_, M::Provider> {
|
||||||
|
self.client.provider().call_raw(&self.tx).block(self.block.into())
|
||||||
|
}
|
||||||
|
|
||||||
/// Broadcasts the contract deployment transaction and after waiting for it to
|
/// Broadcasts the contract deployment transaction and after waiting for it to
|
||||||
/// be sufficiently confirmed (default: 1), it returns a [`Contract`](crate::Contract)
|
/// be sufficiently confirmed (default: 1), it returns a [`Contract`](crate::Contract)
|
||||||
/// struct at the deployed contract's address.
|
/// struct at the deployed contract's address.
|
||||||
|
|
|
@ -0,0 +1,620 @@
|
||||||
|
//! Overrides for the `eth_call` rpc method
|
||||||
|
|
||||||
|
use crate::{JsonRpcClient, PinBoxFut, Provider, ProviderError};
|
||||||
|
use ethers_core::{
|
||||||
|
types::{
|
||||||
|
transaction::eip2718::TypedTransaction, Address, BlockId, BlockNumber, Bytes, H256, U256,
|
||||||
|
U64,
|
||||||
|
},
|
||||||
|
utils,
|
||||||
|
};
|
||||||
|
use pin_project::pin_project;
|
||||||
|
use serde::{ser::SerializeTuple, Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
fmt,
|
||||||
|
future::Future,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use spoof::{balance, code, nonce, state, storage};
|
||||||
|
|
||||||
|
/// Provides methods for overriding parameters to the `eth_call` rpc method
|
||||||
|
pub trait RawCall<'a> {
|
||||||
|
/// Sets the block number to execute against
|
||||||
|
fn block(self, id: BlockId) -> Self;
|
||||||
|
/// Sets the [state override set](https://geth.ethereum.org/docs/rpc/ns-eth#3-object---state-override-set).
|
||||||
|
/// Note that not all client implementations will support this as a parameter.
|
||||||
|
fn state(self, state: &'a spoof::State) -> Self;
|
||||||
|
|
||||||
|
/// Maps a closure `f` over the result of `.await`ing this call
|
||||||
|
fn map<F>(self, f: F) -> Map<Self, F>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
Map::new(self, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A builder which implements [`RawCall`] methods for overriding `eth_call` parameters.
|
||||||
|
///
|
||||||
|
/// `CallBuilder` also implements [`std::future::Future`], so `.await`ing a `CallBuilder` will
|
||||||
|
/// resolve to the result of executing the `eth_call`.
|
||||||
|
#[must_use = "call_raw::CallBuilder does nothing unless you `.await` or poll it"]
|
||||||
|
pub enum CallBuilder<'a, P> {
|
||||||
|
/// The primary builder which exposes [`RawCall`] methods.
|
||||||
|
Build(Caller<'a, P>),
|
||||||
|
/// Used by the [`std::future::Future`] implementation. You are unlikely to encounter this
|
||||||
|
/// variant unless you are constructing your own [`RawCall`] wrapper type.
|
||||||
|
Wait(PinBoxFut<'a, Bytes>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: fmt::Debug> fmt::Debug for CallBuilder<'_, P> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Build(call) => f.debug_tuple("Build").field(call).finish(),
|
||||||
|
Self::Wait(_) => f.debug_tuple("Wait").field(&"< Future >").finish(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, P> CallBuilder<'a, P> {
|
||||||
|
pub fn new(provider: &'a Provider<P>, tx: &'a TypedTransaction) -> Self {
|
||||||
|
Self::Build(Caller::new(provider, tx))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies a closure `f` to a `CallBuilder::Build`. Does nothing for `CallBuilder::Wait`.
|
||||||
|
pub fn map_input<F>(self, f: F) -> Self
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Caller<'a, P>),
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Self::Build(mut call) => {
|
||||||
|
f(&mut call);
|
||||||
|
Self::Build(call)
|
||||||
|
}
|
||||||
|
wait => wait,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the inner `Caller` from a `CallBuilder::Build`. Panics if the `CallBuilder` future
|
||||||
|
/// has already been polled.
|
||||||
|
pub fn unwrap(self) -> Caller<'a, P> {
|
||||||
|
match self {
|
||||||
|
Self::Build(b) => b,
|
||||||
|
_ => panic!("CallBuilder::unwrap on a Wait value"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, P> RawCall<'a> for CallBuilder<'a, P> {
|
||||||
|
/// Sets the block number to execute against
|
||||||
|
fn block(self, id: BlockId) -> Self {
|
||||||
|
self.map_input(|mut call| call.input.block = Some(id))
|
||||||
|
}
|
||||||
|
/// Sets the [state override set](https://geth.ethereum.org/docs/rpc/ns-eth#3-object---state-override-set).
|
||||||
|
/// Note that not all client implementations will support this as a parameter.
|
||||||
|
fn state(self, state: &'a spoof::State) -> Self {
|
||||||
|
self.map_input(|mut call| call.input.state = Some(state))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, P: JsonRpcClient> Future for CallBuilder<'a, P> {
|
||||||
|
type Output = Result<Bytes, ProviderError>;
|
||||||
|
|
||||||
|
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||||
|
let pin = self.get_mut();
|
||||||
|
loop {
|
||||||
|
match pin {
|
||||||
|
CallBuilder::Build(ref call) => {
|
||||||
|
let fut = Box::pin(call.execute());
|
||||||
|
*pin = CallBuilder::Wait(fut);
|
||||||
|
}
|
||||||
|
CallBuilder::Wait(ref mut fut) => return fut.as_mut().poll(cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Holds the inputs to the `eth_call` rpc method along with the rpc provider.
|
||||||
|
/// This type is constructed by [`CallBuilder::new`].
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Caller<'a, P> {
|
||||||
|
provider: &'a Provider<P>,
|
||||||
|
input: CallInput<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, P> Caller<'a, P> {
|
||||||
|
pub fn new(provider: &'a Provider<P>, tx: &'a TypedTransaction) -> Self {
|
||||||
|
Self { provider, input: CallInput::new(tx) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a, P: JsonRpcClient> Caller<'a, P> {
|
||||||
|
/// Executes an `eth_call` rpc request with the overriden parameters. Returns a future that
|
||||||
|
/// resolves to the result of the request.
|
||||||
|
fn execute(&self) -> impl Future<Output = Result<Bytes, ProviderError>> + 'a {
|
||||||
|
self.provider.request("eth_call", utils::serialize(&self.input))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The input parameters to the `eth_call` rpc method
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
struct CallInput<'a> {
|
||||||
|
tx: &'a TypedTransaction,
|
||||||
|
block: Option<BlockId>,
|
||||||
|
state: Option<&'a spoof::State>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> CallInput<'a> {
|
||||||
|
fn new(tx: &'a TypedTransaction) -> Self {
|
||||||
|
Self { tx, block: None, state: None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Serialize for CallInput<'a> {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::ser::Serializer,
|
||||||
|
{
|
||||||
|
let len = 2 + self.state.is_some() as usize;
|
||||||
|
|
||||||
|
let mut tup = serializer.serialize_tuple(len)?;
|
||||||
|
tup.serialize_element(self.tx)?;
|
||||||
|
|
||||||
|
let block = self.block.unwrap_or_else(|| BlockNumber::Latest.into());
|
||||||
|
tup.serialize_element(&block)?;
|
||||||
|
|
||||||
|
if let Some(state) = self.state {
|
||||||
|
tup.serialize_element(state)?;
|
||||||
|
}
|
||||||
|
tup.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An implementer of [`RawCall`] that maps a function `f` over the output of the inner future.
|
||||||
|
///
|
||||||
|
/// This struct is created by the [`map`] method on [`RawCall`].
|
||||||
|
///
|
||||||
|
/// [`map`]: RawCall::map
|
||||||
|
#[must_use = "call_raw::Map does nothing unless you `.await` or poll it"]
|
||||||
|
#[derive(Clone)]
|
||||||
|
#[pin_project]
|
||||||
|
pub struct Map<T, F> {
|
||||||
|
#[pin]
|
||||||
|
inner: T,
|
||||||
|
f: F,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: fmt::Debug, F> fmt::Debug for Map<T, F> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("Map").field("inner", &self.inner).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, F> Map<T, F> {
|
||||||
|
pub fn new(inner: T, f: F) -> Self {
|
||||||
|
Self { inner, f }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T, F> RawCall<'a> for Map<T, F>
|
||||||
|
where
|
||||||
|
T: RawCall<'a>,
|
||||||
|
{
|
||||||
|
/// Sets the block number to execute against
|
||||||
|
fn block(self, id: BlockId) -> Self {
|
||||||
|
Self { inner: self.inner.block(id), f: self.f }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [state override set](https://geth.ethereum.org/docs/rpc/ns-eth#3-object---state-override-set).
|
||||||
|
/// Note that not all client implementations will support this as a parameter.
|
||||||
|
fn state(self, state: &'a spoof::State) -> Self {
|
||||||
|
Self { inner: self.inner.state(state), f: self.f }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, F, Y> Future for Map<T, F>
|
||||||
|
where
|
||||||
|
T: Future,
|
||||||
|
F: FnMut(T::Output) -> Y,
|
||||||
|
{
|
||||||
|
type Output = Y;
|
||||||
|
|
||||||
|
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
|
let pin = self.project();
|
||||||
|
let x = futures_util::ready!(pin.inner.poll(cx));
|
||||||
|
Poll::Ready((pin.f)(x))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provides types and methods for constructing an `eth_call`
|
||||||
|
/// [state override set](https://geth.ethereum.org/docs/rpc/ns-eth#3-object---state-override-set)
|
||||||
|
pub mod spoof {
|
||||||
|
use super::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// The state elements to override for a particular account.
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Account {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub nonce: Option<U64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub balance: Option<U256>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub code: Option<Bytes>,
|
||||||
|
#[serde(flatten, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub storage: Option<Storage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Account {
|
||||||
|
/// Override the account nonce
|
||||||
|
pub fn nonce(&mut self, nonce: U64) -> &mut Self {
|
||||||
|
self.nonce = Some(nonce);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Override the account balance
|
||||||
|
pub fn balance(&mut self, bal: U256) -> &mut Self {
|
||||||
|
self.balance = Some(bal);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Override the code at the account
|
||||||
|
pub fn code(&mut self, code: Bytes) -> &mut Self {
|
||||||
|
self.code = Some(code);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Override the value of the account storage at the given storage `key`
|
||||||
|
pub fn store(&mut self, key: H256, val: H256) -> &mut Self {
|
||||||
|
self.storage.get_or_insert_with(Default::default).insert(key, val);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps a map from storage slot to the overriden value.
|
||||||
|
///
|
||||||
|
/// Storage overrides can either replace the existing state of an account or they can be treated
|
||||||
|
/// as a diff on the existing state.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum Storage {
|
||||||
|
#[serde(rename = "stateDiff")]
|
||||||
|
Diff(HashMap<H256, H256>),
|
||||||
|
#[serde(rename = "state")]
|
||||||
|
Replace(HashMap<H256, H256>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default storage override is a diff on the existing state of the account.
|
||||||
|
impl Default for Storage {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Diff(Default::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::Deref for Storage {
|
||||||
|
type Target = HashMap<H256, H256>;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
match self {
|
||||||
|
Self::Diff(map) => map,
|
||||||
|
Self::Replace(map) => map,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::DerefMut for Storage {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
match self {
|
||||||
|
Self::Diff(map) => map,
|
||||||
|
Self::Replace(map) => map,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper type that holds a complete state override set.
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct State(#[serde(skip_serializing_if = "HashMap::is_empty")] HashMap<Address, Account>);
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
/// Returns a mutable reference to the [`Account`] in the map.
|
||||||
|
pub fn account(&mut self, adr: Address) -> &mut Account {
|
||||||
|
self.0.entry(adr).or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an empty state override set.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # use ethers_core::{
|
||||||
|
/// # types::{Address, TransactionRequest, H256},
|
||||||
|
/// # utils::{parse_ether, Geth},
|
||||||
|
/// # };
|
||||||
|
/// # use ethers_providers::{Provider, Http, Middleware, call_raw::{spoof, RawCall}};
|
||||||
|
/// # use std::convert::TryFrom;
|
||||||
|
/// #
|
||||||
|
/// # #[tokio::main(flavor = "current_thread")]
|
||||||
|
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// let geth = Geth::new().spawn();
|
||||||
|
/// let provider = Provider::<Http>::try_from(geth.endpoint()).unwrap();
|
||||||
|
///
|
||||||
|
/// let adr1: Address = "0x6fC21092DA55B392b045eD78F4732bff3C580e2c".parse().unwrap();
|
||||||
|
/// let adr2: Address = "0x295a70b2de5e3953354a6a8344e616ed314d7251".parse().unwrap();
|
||||||
|
/// let key = H256::from_low_u64_be(1);
|
||||||
|
/// let val = H256::from_low_u64_be(17);
|
||||||
|
///
|
||||||
|
/// let tx = TransactionRequest::default().to(adr2).from(adr1).into();
|
||||||
|
///
|
||||||
|
/// // override the storage at `adr2`
|
||||||
|
/// let mut state = spoof::state();
|
||||||
|
/// state.account(adr2).store(key, val);
|
||||||
|
///
|
||||||
|
/// // override the nonce at `adr1`
|
||||||
|
/// state.account(adr1).nonce(2.into());
|
||||||
|
///
|
||||||
|
/// provider.call_raw(&tx).state(&state).await.unwrap();
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn state() -> State {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a state override set with a single element setting the balance of the address.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # use ethers_core::{
|
||||||
|
/// # types::{Address, TransactionRequest, H256},
|
||||||
|
/// # utils::{parse_ether, Geth},
|
||||||
|
/// # };
|
||||||
|
/// # use ethers_providers::{Provider, Http, Middleware, call_raw::{RawCall, spoof}};
|
||||||
|
/// # use std::convert::TryFrom;
|
||||||
|
/// #
|
||||||
|
/// # #[tokio::main(flavor = "current_thread")]
|
||||||
|
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// let geth = Geth::new().spawn();
|
||||||
|
/// let provider = Provider::<Http>::try_from(geth.endpoint()).unwrap();
|
||||||
|
///
|
||||||
|
/// let adr1: Address = "0x6fC21092DA55B392b045eD78F4732bff3C580e2c".parse()?;
|
||||||
|
/// let adr2: Address = "0x295a70b2de5e3953354a6a8344e616ed314d7251".parse()?;
|
||||||
|
/// let pay_amt = parse_ether(1u64)?;
|
||||||
|
///
|
||||||
|
/// // Not enough ether to pay for the transaction
|
||||||
|
/// let tx = TransactionRequest::pay(adr2, pay_amt).from(adr1).into();
|
||||||
|
///
|
||||||
|
/// // override the sender's balance for the call
|
||||||
|
/// let state = spoof::balance(adr1, pay_amt * 2);
|
||||||
|
/// provider.call_raw(&tx).state(&state).await?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn balance(adr: Address, bal: U256) -> State {
|
||||||
|
let mut state = State::default();
|
||||||
|
state.account(adr).balance(bal);
|
||||||
|
state
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a state override set with a single element setting the nonce of the address.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # use ethers_core::{
|
||||||
|
/// # types::{Address, TransactionRequest, H256},
|
||||||
|
/// # utils::{parse_ether, Geth},
|
||||||
|
/// # };
|
||||||
|
/// # use ethers_providers::{Provider, Http, Middleware, call_raw::{RawCall, spoof}};
|
||||||
|
/// # use std::convert::TryFrom;
|
||||||
|
/// #
|
||||||
|
/// # #[tokio::main(flavor = "current_thread")]
|
||||||
|
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// let geth = Geth::new().spawn();
|
||||||
|
/// let provider = Provider::<Http>::try_from(geth.endpoint()).unwrap();
|
||||||
|
///
|
||||||
|
/// let adr: Address = "0x6fC21092DA55B392b045eD78F4732bff3C580e2c".parse()?;
|
||||||
|
/// let pay_amt = parse_ether(1u64)?;
|
||||||
|
///
|
||||||
|
/// let tx = TransactionRequest::default().from(adr).into();
|
||||||
|
///
|
||||||
|
/// // override the sender's nonce for the call
|
||||||
|
/// let state = spoof::nonce(adr, 72.into());
|
||||||
|
/// provider.call_raw(&tx).state(&state).await?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn nonce(adr: Address, nonce: U64) -> State {
|
||||||
|
let mut state = State::default();
|
||||||
|
state.account(adr).nonce(nonce);
|
||||||
|
state
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a state override set with a single element setting the code at the address.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # use ethers_core::{
|
||||||
|
/// # types::{Address, TransactionRequest, H256},
|
||||||
|
/// # utils::{parse_ether, Geth},
|
||||||
|
/// # };
|
||||||
|
/// # use ethers_providers::{Provider, Http, Middleware, call_raw::{RawCall, spoof}};
|
||||||
|
/// # use std::convert::TryFrom;
|
||||||
|
/// #
|
||||||
|
/// # #[tokio::main(flavor = "current_thread")]
|
||||||
|
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// let geth = Geth::new().spawn();
|
||||||
|
/// let provider = Provider::<Http>::try_from(geth.endpoint()).unwrap();
|
||||||
|
///
|
||||||
|
/// let adr: Address = "0x6fC21092DA55B392b045eD78F4732bff3C580e2c".parse()?;
|
||||||
|
/// let pay_amt = parse_ether(1u64)?;
|
||||||
|
///
|
||||||
|
/// let tx = TransactionRequest::default().to(adr).into();
|
||||||
|
///
|
||||||
|
/// // override the code at the target address
|
||||||
|
/// let state = spoof::code(adr, "0x00".parse()?);
|
||||||
|
/// provider.call_raw(&tx).state(&state).await?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn code(adr: Address, code: Bytes) -> State {
|
||||||
|
let mut state = State::default();
|
||||||
|
state.account(adr).code(code);
|
||||||
|
state
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a state override set with a single element setting the storage at the given address
|
||||||
|
/// and key.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # use ethers_core::{
|
||||||
|
/// # types::{Address, TransactionRequest, H256},
|
||||||
|
/// # utils::{parse_ether, Geth},
|
||||||
|
/// # };
|
||||||
|
/// # use ethers_providers::{Provider, Http, Middleware, call_raw::{RawCall, spoof}};
|
||||||
|
/// # use std::convert::TryFrom;
|
||||||
|
/// #
|
||||||
|
/// # #[tokio::main(flavor = "current_thread")]
|
||||||
|
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// let geth = Geth::new().spawn();
|
||||||
|
/// let provider = Provider::<Http>::try_from(geth.endpoint()).unwrap();
|
||||||
|
///
|
||||||
|
/// let adr: Address = "0x6fC21092DA55B392b045eD78F4732bff3C580e2c".parse()?;
|
||||||
|
/// let key = H256::from_low_u64_be(1);
|
||||||
|
/// let val = H256::from_low_u64_be(17);
|
||||||
|
///
|
||||||
|
/// let tx = TransactionRequest::default().to(adr).into();
|
||||||
|
///
|
||||||
|
/// // override the storage slot `key` at `adr`
|
||||||
|
/// let state = spoof::storage(adr, key, val);
|
||||||
|
/// provider.call_raw(&tx).state(&state).await?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn storage(adr: Address, key: H256, val: H256) -> State {
|
||||||
|
let mut state = State::default();
|
||||||
|
state.account(adr).store(key, val);
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::{Http, Middleware, Provider};
|
||||||
|
use ethers_core::{
|
||||||
|
types::TransactionRequest,
|
||||||
|
utils::{get_contract_address, keccak256, parse_ether, Anvil, Geth},
|
||||||
|
};
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
// Deserializes eth_call parameters as owned data for testing serialization
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct CallInputOwned(
|
||||||
|
TypedTransaction,
|
||||||
|
Option<BlockId>,
|
||||||
|
#[serde(default)] Option<spoof::State>,
|
||||||
|
);
|
||||||
|
impl<'a> From<&'a CallInputOwned> for CallInput<'a> {
|
||||||
|
fn from(src: &'a CallInputOwned) -> Self {
|
||||||
|
Self { tx: &src.0, block: src.1, state: src.2.as_ref() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests "roundtrip" serialization of calls: deserialize(serialize(call)) == call
|
||||||
|
fn test_encode<'a, P>(call: CallBuilder<'a, P>) {
|
||||||
|
let input = call.unwrap().input;
|
||||||
|
let ser = utils::serialize(&input).to_string();
|
||||||
|
let de: CallInputOwned = serde_json::from_str(&ser).unwrap();
|
||||||
|
let de = CallInput::from(&de);
|
||||||
|
|
||||||
|
assert_eq!(input.tx, de.tx);
|
||||||
|
assert_eq!(input.state, de.state);
|
||||||
|
|
||||||
|
let block = input.block.or_else(|| Some(BlockNumber::Latest.into()));
|
||||||
|
assert_eq!(block, de.block);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize() {
|
||||||
|
let adr1: Address = "0x6fC21092DA55B392b045eD78F4732bff3C580e2c".parse().unwrap();
|
||||||
|
let adr2: Address = "0x295a70b2de5e3953354a6a8344e616ed314d7251".parse().unwrap();
|
||||||
|
let k1 = utils::keccak256("foo").into();
|
||||||
|
let v1 = H256::from_low_u64_be(534);
|
||||||
|
let k2 = utils::keccak256("bar").into();
|
||||||
|
let v2 = H256::from_low_u64_be(8675309);
|
||||||
|
|
||||||
|
let tx = TypedTransaction::default();
|
||||||
|
let (provider, _) = Provider::mocked();
|
||||||
|
|
||||||
|
let call = provider.call_raw(&tx);
|
||||||
|
test_encode(call);
|
||||||
|
|
||||||
|
let mut state = spoof::state();
|
||||||
|
state.account(adr1).nonce(1.into()).balance(2.into()).store(k1, v1).store(k2, v2);
|
||||||
|
let call = provider.call_raw(&tx).block(100.into()).state(&state);
|
||||||
|
test_encode(call);
|
||||||
|
|
||||||
|
let mut state = spoof::state();
|
||||||
|
state.account(adr1).nonce(1.into());
|
||||||
|
state.account(adr2).nonce(7.into());
|
||||||
|
let call = provider.call_raw(&tx).state(&state);
|
||||||
|
test_encode(call);
|
||||||
|
|
||||||
|
// State override with an empty acccount should be encoded as "0xab..": {}
|
||||||
|
let mut state = spoof::state();
|
||||||
|
state.account(adr1);
|
||||||
|
let call = provider.call_raw(&tx).state(&state);
|
||||||
|
test_encode(call);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_state_overrides() {
|
||||||
|
let geth = Geth::new().spawn();
|
||||||
|
let provider = Provider::<Http>::try_from(geth.endpoint()).unwrap();
|
||||||
|
|
||||||
|
let adr1: Address = "0x6fC21092DA55B392b045eD78F4732bff3C580e2c".parse().unwrap();
|
||||||
|
let adr2: Address = "0x295a70b2de5e3953354a6a8344e616ed314d7251".parse().unwrap();
|
||||||
|
let pay_amt = parse_ether(1u64).unwrap();
|
||||||
|
|
||||||
|
// Not enough ether to pay for the transaction
|
||||||
|
let tx = TransactionRequest::pay(adr2, pay_amt).from(adr1).into();
|
||||||
|
|
||||||
|
// assert that overriding the sender's balance works
|
||||||
|
let state = spoof::balance(adr1, pay_amt * 2);
|
||||||
|
provider.call_raw(&tx).state(&state).await.expect("eth_call success");
|
||||||
|
|
||||||
|
// bytecode that returns the result of the SELFBALANCE opcode
|
||||||
|
const RETURN_BALANCE: &str = "0x4760005260206000f3";
|
||||||
|
let bytecode = RETURN_BALANCE.parse().unwrap();
|
||||||
|
let balance = 100.into();
|
||||||
|
|
||||||
|
let tx = TransactionRequest::default().to(adr2).into();
|
||||||
|
let mut state = spoof::state();
|
||||||
|
state.account(adr2).code(bytecode).balance(balance);
|
||||||
|
|
||||||
|
// assert that overriding the code and balance at adr2 works
|
||||||
|
let bytes = provider.call_raw(&tx).state(&state).await.unwrap();
|
||||||
|
assert_eq!(U256::from_big_endian(bytes.as_ref()), balance);
|
||||||
|
|
||||||
|
// bytecode that deploys a contract and returns the deployed address
|
||||||
|
const DEPLOY_CONTRACT: &str = "0x6000600052602060006000f060005260206000f3";
|
||||||
|
let bytecode = DEPLOY_CONTRACT.parse().unwrap();
|
||||||
|
let nonce = 17.into();
|
||||||
|
|
||||||
|
let mut state = spoof::state();
|
||||||
|
state.account(adr2).code(bytecode).nonce(nonce);
|
||||||
|
|
||||||
|
// assert that overriding nonce works (contract is deployed to expected address)
|
||||||
|
let bytes = provider.call_raw(&tx).state(&state).await.unwrap();
|
||||||
|
let deployed = Address::from_slice(&bytes.as_ref()[12..]);
|
||||||
|
assert_eq!(deployed, get_contract_address(adr2, nonce.as_u64()));
|
||||||
|
|
||||||
|
// bytecode that returns the value of storage slot 1
|
||||||
|
const RETURN_STORAGE: &str = "0x60015460005260206000f3";
|
||||||
|
let bytecode = RETURN_STORAGE.parse().unwrap();
|
||||||
|
let slot = H256::from_low_u64_be(1);
|
||||||
|
let val = keccak256("foo").into();
|
||||||
|
|
||||||
|
let mut state = spoof::state();
|
||||||
|
state.account(adr2).code(bytecode).store(slot, val);
|
||||||
|
|
||||||
|
// assert that overriding storage works
|
||||||
|
let bytes = provider.call_raw(&tx).state(&state).await.unwrap();
|
||||||
|
assert_eq!(H256::from_slice(bytes.as_ref()), val);
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ pub use stream::{interval, FilterWatcher, TransactionStream, DEFAULT_POLL_INTERV
|
||||||
mod pubsub;
|
mod pubsub;
|
||||||
pub use pubsub::{PubsubClient, SubscriptionStream};
|
pub use pubsub::{PubsubClient, SubscriptionStream};
|
||||||
|
|
||||||
|
pub mod call_raw;
|
||||||
pub mod erc;
|
pub mod erc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
|
call_raw::CallBuilder,
|
||||||
ens, erc, maybe,
|
ens, erc, maybe,
|
||||||
pubsub::{PubsubClient, SubscriptionStream},
|
pubsub::{PubsubClient, SubscriptionStream},
|
||||||
stream::{FilterWatcher, DEFAULT_POLL_INTERVAL},
|
stream::{FilterWatcher, DEFAULT_POLL_INTERVAL},
|
||||||
|
@ -230,6 +231,46 @@ impl<P: JsonRpcClient> Provider<P> {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Analogous to [`Middleware::call`], but returns a [`CallBuilder`] that can either be
|
||||||
|
/// `.await`d or used to override the parameters sent to `eth_call`.
|
||||||
|
///
|
||||||
|
/// See the [`call_raw::spoof`] for functions to construct state override parameters.
|
||||||
|
///
|
||||||
|
/// Note: this method _does not_ send a transaction from your account
|
||||||
|
///
|
||||||
|
/// [`call_raw::spoof`]: crate::call_raw::spoof
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```no_run
|
||||||
|
/// # use ethers_core::{
|
||||||
|
/// # types::{Address, TransactionRequest, H256},
|
||||||
|
/// # utils::{parse_ether, Geth},
|
||||||
|
/// # };
|
||||||
|
/// # use ethers_providers::{Provider, Http, Middleware, call_raw::{RawCall, spoof}};
|
||||||
|
/// # use std::convert::TryFrom;
|
||||||
|
/// #
|
||||||
|
/// # #[tokio::main(flavor = "current_thread")]
|
||||||
|
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// let geth = Geth::new().spawn();
|
||||||
|
/// let provider = Provider::<Http>::try_from(geth.endpoint()).unwrap();
|
||||||
|
///
|
||||||
|
/// let adr1: Address = "0x6fC21092DA55B392b045eD78F4732bff3C580e2c".parse()?;
|
||||||
|
/// let adr2: Address = "0x295a70b2de5e3953354a6a8344e616ed314d7251".parse()?;
|
||||||
|
/// let pay_amt = parse_ether(1u64)?;
|
||||||
|
///
|
||||||
|
/// // Not enough ether to pay for the transaction
|
||||||
|
/// let tx = TransactionRequest::pay(adr2, pay_amt).from(adr1).into();
|
||||||
|
///
|
||||||
|
/// // override the sender's balance for the call
|
||||||
|
/// let mut state = spoof::balance(adr1, pay_amt * 2);
|
||||||
|
/// provider.call_raw(&tx).state(&state).await?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn call_raw<'a>(&'a self, tx: &'a TypedTransaction) -> CallBuilder<'a, P> {
|
||||||
|
CallBuilder::new(self, tx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "celo")]
|
#[cfg(feature = "celo")]
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
use ethers::{
|
||||||
|
prelude::*,
|
||||||
|
providers::call_raw::RawCall,
|
||||||
|
utils::{parse_ether, Geth},
|
||||||
|
};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
abigen!(Greeter, "ethers-contract/tests/solidity-contracts/greeter.json",);
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> eyre::Result<()> {
|
||||||
|
let geth = Geth::new().spawn();
|
||||||
|
let provider = Provider::<Http>::try_from(geth.endpoint()).unwrap();
|
||||||
|
let client = Arc::new(provider);
|
||||||
|
|
||||||
|
// Both empty accounts
|
||||||
|
let target: Address = "0x6fC21092DA55B392b045eD78F4732bff3C580e2c".parse()?;
|
||||||
|
let from: Address = "0x295a70b2de5e3953354a6a8344e616ed314d7251".parse()?;
|
||||||
|
|
||||||
|
// Override the sender's balance for the call
|
||||||
|
let pay_amt = parse_ether(1u64)?;
|
||||||
|
let tx = TransactionRequest::pay(target, pay_amt).from(from);
|
||||||
|
let state = call_raw::balance(from, pay_amt * 2);
|
||||||
|
|
||||||
|
// The call succeeds as if the sender had sufficient balance
|
||||||
|
client.call_raw(&tx.into()).state(&state).await.expect("balance override");
|
||||||
|
|
||||||
|
// Get the runtime bytecode for the Greeter contract using eth_call to
|
||||||
|
// simulate the deploy transaction
|
||||||
|
let deploy = Greeter::deploy(client.clone(), "Hi".to_string())?;
|
||||||
|
let runtime_bytecode = deploy.call_raw().await?;
|
||||||
|
|
||||||
|
// Instantiate a Greeter, though no bytecode exists at the target address
|
||||||
|
let greeter = Greeter::new(target, client.clone());
|
||||||
|
|
||||||
|
// Override the target account's code, simulating a call to Greeter.greet()
|
||||||
|
// as if the Greeter contract was deployed at the target address
|
||||||
|
let state = call_raw::code(target, runtime_bytecode.clone());
|
||||||
|
let res = greeter.greet().call_raw().state(&state).await?;
|
||||||
|
|
||||||
|
// greet() returns the empty string, because the target account's storage is empty
|
||||||
|
assert_eq!(&res, "");
|
||||||
|
|
||||||
|
// Encode the greeting string as solidity expects it to be stored
|
||||||
|
let greeting = "Hello, world";
|
||||||
|
let greet_slot = H256::zero();
|
||||||
|
let greet_val = encode_string_for_storage(greeting);
|
||||||
|
|
||||||
|
// Override the target account's code and storage
|
||||||
|
let mut state = call_raw::state();
|
||||||
|
state.account(target).code(runtime_bytecode.clone()).store(greet_slot, greet_val);
|
||||||
|
let res = greeter.greet().call_raw().state(&state).await?;
|
||||||
|
|
||||||
|
// The call returns "Hello, world"
|
||||||
|
assert_eq!(&res, greeting);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solidity stores strings shorter than 32 bytes in a single storage slot,
|
||||||
|
// with the lowest order byte storing 2 * length and the higher order
|
||||||
|
// bytes storing the string data (left aligned)
|
||||||
|
fn encode_string_for_storage(s: &str) -> H256 {
|
||||||
|
let mut bytes = s.as_bytes().to_vec();
|
||||||
|
let len = bytes.len();
|
||||||
|
assert!(len < 32, "longer strings aren't stored in a single slot");
|
||||||
|
bytes.resize(31, 0);
|
||||||
|
bytes.push(len as u8 * 2);
|
||||||
|
H256::from_slice(&bytes)
|
||||||
|
}
|
Loading…
Reference in New Issue