35个版本
新 0.9.13 | 2024年8月20日 |
---|---|
0.9.11 | 2024年4月22日 |
0.9.10 | 2024年3月25日 |
0.9.9 | 2023年12月19日 |
0.1.4 | 2022年6月22日 |
#23 in 魔法豆
1,215 每月下载量
用于 3 个crate(2个直接使用)
200KB
3K SLoC
Substreams Ethereum
Ethereum链的Substreams开发套件,包含Rust Firehose Block模型和助手,以及Ethereum ABI编码/解码工具。
用法
[package]
name = "substreams-acme"
version = 0.1.2
[lib]
crate-type = ["cdylib"]
[dependencies]
substreams-ethereum = "0.6.0"
开发
我们手动将渲染的Rust Firehose Block模型与sf-ethereum中找到的实际Protocol Buffer定义文件保持同步,并将它们提交到Git。
这意味着对Protobuf文件所做的更改必须手动重新生成和提交,下面将说明如何操作。
从Protobuf重新生成Rust Firehose Block
./gen.sh
注意事项
带元组的ABI
现在支持为ABI生成的代码使用元组。它会为使用它们的函数/事件生成Rust未命名的元组。以下说明可用于如果您希望在事件中为元组生成"结构体"。您可以展开下面的Instructions
(说明)部分,以获取有关如何“手动”生成代码的详细信息。
说明
第一步将是稍微修改ABI,以便正确生成解码代码。然后,将复制并修改初始生成的代码,以正确解码元组。
想法是将ABI内的tuple
“分解”成它自己的“事件”,这将生成表示元组的struct
的代码以及该struct
本身的解码代码。然后我们将调整生成的代码,将一切连接起来。
从
[
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "bytes32",
"name": "orderHash",
"type": "bytes32"
},
{
"components": [
{
"internalType": "enum ItemType",
"name": "itemType",
"type": "uint8"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"indexed": false,
"internalType": "struct SpentItem[]",
"name": "offer",
"type": "tuple[]"
}
],
"name": "OrderFulfilled",
"type": "event"
}
]
我们将SpentItem
分解成它自己的类型,并将offer
类型tuple[]
替换为address[]
,以使其正确编译
[
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "bytes32",
"name": "orderHash",
"type": "bytes32"
},
{
"components": [
{
"internalType": "enum ItemType",
"name": "itemType",
"type": "uint8"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"indexed": false,
"internalType": "struct SpentItem[]",
"name": "offer",
"type": "address[]"
}
],
"name": "OrderFulfilled",
"type": "event"
},
{
"anonymous": false,
"name": "SpentItem",
"type": "event",
"inputs": [
{
"internalType": "enum ItemType",
"name": "itemType",
"type": "uint8"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
]
}
]
注意无需删除
components
或更改internalType
值,它们将被忽略。
使用此修改后的ABI执行cargo build
,以便在src/abi/<file>.rs
中生成代码,它现在是不正确的,但我们将其复制到其他地方并使其工作。
在src/abi/<file>.rs
中找到OrderFulfilled
事件的生成代码,并将其复制到新文件src/events.rs
中。您应该复制以下内容:pub struct OrderFulfilled
块、impl OrderFulfilled
块、impl substreams_ethereum::Event for OrderFulfilled
块、SpentItem
结构和impl SpentItem
块。
#[derive(Debug, Clone, PartialEq)]
pub struct OrderFulfilled {
pub order_hash: [u8; 32usize],
pub offer: Vec<Vec<u8>>,
}
impl OrderFulfilled {
const TOPIC_ID: [u8; 32] = [
227u8,
56u8,
222u8,
32u8,
39u8,
120u8,
0u8,
226u8,
120u8,
84u8,
168u8,
160u8,
171u8,
38u8,
80u8,
66u8,
198u8,
237u8,
193u8,
186u8,
154u8,
14u8,
209u8,
73u8,
102u8,
185u8,
47u8,
163u8,
179u8,
98u8,
194u8,
244u8,
];
pub fn match_log(log: &substreams_ethereum::pb::eth::v2::Log) -> bool {
if log.topics.len() != 1usize {
return false;
}
if log.data.len() < 96usize {
return false;
}
return log.topics.get(0).expect("bounds already checked").as_ref()
== Self::TOPIC_ID;
}
pub fn decode(
log: &substreams_ethereum::pb::eth::v2::Log,
) -> Result<Self, String> {
let mut values = ethabi::decode(
&[
ethabi::ParamType::FixedBytes(32usize),
ethabi::ParamType::Array(
Box::new(ethabi::ParamType::Address),
),
],
log.data.as_ref(),
)
.map_err(|e| format!("unable to decode log.data: {:?}", e))?;
values.reverse();
Ok(Self {
order_hash: {
let mut result = [0u8; 32];
let v = values
.pop()
.expect(INTERNAL_ERR)
.into_fixed_bytes()
.expect(INTERNAL_ERR);
result.copy_from_slice(&v);
result
},
offer: values
.pop()
.expect(INTERNAL_ERR)
.into_array()
.expect(INTERNAL_ERR)
.into_iter()
.map(|inner| {
inner.into_address().expect(INTERNAL_ERR).as_bytes().to_vec()
})
.collect(),
})
}
}
impl substreams_ethereum::Event for OrderFulfilled {
const NAME: &'static str = "OrderFulfilled";
fn match_log(log: &substreams_ethereum::pb::eth::v2::Log) -> bool {
Self::match_log(log)
}
fn decode(
log: &substreams_ethereum::pb::eth::v2::Log,
) -> Result<Self, String> {
Self::decode(log)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SpentItem {
pub item_type: substreams::scalar::BigInt,
pub amount: substreams::scalar::BigInt,
}
impl SpentItem {
const TOPIC_ID: [u8; 32] = [
18u8,
7u8,
103u8,
62u8,
30u8,
101u8,
94u8,
85u8,
209u8,
209u8,
166u8,
82u8,
139u8,
137u8,
197u8,
45u8,
11u8,
224u8,
230u8,
74u8,
27u8,
234u8,
238u8,
52u8,
150u8,
245u8,
214u8,
202u8,
230u8,
104u8,
138u8,
22u8,
];
pub fn match_log(log: &substreams_ethereum::pb::eth::v2::Log) -> bool {
if log.topics.len() != 1usize {
return false;
}
if log.data.len() != 64usize {
return false;
}
return log.topics.get(0).expect("bounds already checked").as_ref()
== Self::TOPIC_ID;
}
pub fn decode(
log: &substreams_ethereum::pb::eth::v2::Log,
) -> Result<Self, String> {
let mut values = ethabi::decode(
&[
ethabi::ParamType::Uint(8usize),
ethabi::ParamType::Uint(256usize),
],
log.data.as_ref(),
)
.map_err(|e| format!("unable to decode log.data: {:?}", e))?;
values.reverse();
Ok(Self {
item_type: {
let mut v = [0 as u8; 32];
values
.pop()
.expect(INTERNAL_ERR)
.into_uint()
.expect(INTERNAL_ERR)
.to_big_endian(v.as_mut_slice());
substreams::scalar::BigInt::from_unsigned_bytes_be(&v)
},
amount: {
let mut v = [0 as u8; 32];
values
.pop()
.expect(INTERNAL_ERR)
.into_uint()
.expect(INTERNAL_ERR)
.to_big_endian(v.as_mut_slice());
substreams::scalar::BigInt::from_unsigned_bytes_be(&v)
},
})
}
}
impl substreams_ethereum::Event for SpentItem {
const NAME: &'static str = "SpentItem";
fn match_log(log: &substreams_ethereum::pb::eth::v2::Log) -> bool {
Self::match_log(log)
}
fn decode(
log: &substreams_ethereum::pb::eth::v2::Log,
) -> Result<Self, String> {
Self::decode(log)
}
}
目前,此src/events.rs
文件未编译/包含在项目中,因为在Rust中,只有当模块“定义”在某处时才会包含模块,所以让我们定义模块。在src/lib.rs
文件的顶部,添加以下内容:
mod events
让我们开始修改我们的错误生成的代码,使其正确。首先定义INTERNAL_ERR
常量,因为它通常被使用,将其放在src/events.rs
文件顶部
const INTERNAL_ERR: &str = "decode event internal error";
现在一个棘手的部分,我们需要更新TOPIC_ID
常量,因为它现在是不正确的。如果您已经知道事件ID(它是topics #0
),那就太好了。如果您不知道,可以通过对事件定义进行keccak256
哈希来计算它。您需要事件名称及其类型才能进行计算,在我们的例子中是OrderFulfilled(bytes32,uint8,uint256[])
警告事件定义必须没有空格或额外标点符号,任何一个错误的字符都会使整个事件ID错误。
您可以使用 jq -'.[] | select(.name == "OrderFulfilled") | .inputs[].type' | tr "\n" ","
和 jq -'.[] | select(.name == "SpentItem") | .inputs[].type' | tr "\n" ","
来获取正确的有序类型。然后正确地组装它,将 OrderFulfilled
的第二个字段更改为元组定义 (uint8,uint256)[]
(由于我们的修改,它被返回为 address[]
)。
现在我们有了事件定义,我们可以计算 keccak256 哈希。我们使用 CLI 工具 keccak-256sum
来完成这个任务(安装说明)
$ printf 'OrderFulfilled(bytes32,(uint8,uint256)[])' | keccak-256sum
e86f4727db138d4b9cb776888b1d2239562eafaa38dd110b7d5def7698ccfd41 -
因此,我们的事件主题 0 是 e86f4727db138d4b9cb776888b1d2239562eafaa38dd110b7d5def7698ccfd41
。现在我们只需要将 TOPIC_ID
常量定义在 impl OrderFulfilled
定义中更改为
const TOPIC_ID: [u8; 32] = hex_literal::hex!("e86f4727db138d4b9cb776888b1d2239562eafaa38dd110b7d5def7698ccfd41");
现在,在 pub struct OrderFulfilled
中,将 offer
字段(目前定义为 offer: Vec<Vec<u8>>
)更改为正确的值 offer: Vec<SpentItem>
pub struct OrderFulfilled {
pub order_hash: [u8; 32usize],
pub offer: Vec<SpentItem>,
}
然后在 pub fn decode(log: <type>)
中找到事件 "token" 定义,它现在是这样的
let mut values = ethabi::decode(
&[
ethabi::ParamType::FixedBytes(32usize),
ethabi::ParamType::Array(Box::new(ethabi::ParamType::Address)),
],
log.data.as_ref(),
)
确定字段是元组,在我们的例子中是第二个字段,并将其更改为定义为 ethabi::ParamType::Tuple
类型。
let mut values = ethabi::decode(
&[
ethabi::ParamType::FixedBytes(32usize),
ethabi::ParamType::Array(Box::new(ethabi::ParamType::Tuple(vec![
ethabi::ParamType::Uint(8usize),
ethabi::ParamType::Uint(256usize),
]))),
],
log.data.as_ref(),
信息
SpentItem::decode
函数在函数开头就定义了一个let mut values
变量,列出了为元组放置的正确元素,无需手动定义列表,只需简单复制即可。
现在仍然在 OrderFulfilled
函数中,找到执行实际解码工作的 offer:
代码块,它现在看起来像
...
},
offer: values
.pop()
.expect(INTERNAL_ERR)
.into_array()
.expect(INTERNAL_ERR)
.into_iter()
.map(|inner| {
inner
.into_address()
.expect(INTERNAL_ERR)
.as_bytes()
.to_vec()
})
.collect(),
将其修改为将解码工作转发给 SpentItem
结构体
...
},
offer: values
.pop()
.expect(INTERNAL_ERR)
.into_array()
.expect(INTERNAL_ERR)
.into_iter()
.map(|inner| {
let fields = inner.into_tuple().expect(INTERNAL_ERR);
SpentItem::decode(fields).expect(INTERNAL_ERR);
})
.collect(),
在 impl OrderFulfilled
结构体中的最终修改是略微修改 pub fn match_log(log: &substreams_ethereum::pb::eth::v2::Log) -> bool
定义。它包含一段代码,确保 log.data
有一定数量的字节,我们修改的ABI将生成错误的验证代码用于 log.data
,所以让我们移除它
pub fn match_log(log: &substreams_ethereum::pb::eth::v2::Log) -> bool {
if log.topics.len() != 1usize {
return false;
}
if log.data.len() < 96usize {
return false;
}
return log.topics.get(0).expect("bounds already checked").as_ref() == Self::TOPIC_ID;
}
应该变成
pub fn match_log(log: &substreams_ethereum::pb::eth::v2::Log) -> bool {
if log.topics.len() != 1usize {
return false;
}
return log.topics.get(0).expect("bounds already checked").as_ref() == Self::TOPIC_ID;
}
注意 计算过于繁琐,无法手动执行,主题计数和验证主题0的值就足够匹配所需的唯一事件。
现在,让我们将重点放在 SpentItem
实现上。在 impl SpentItem
块中,删除
TOPIC_ID
常量match_log
函数
并完全删除 impl substreams_ethereum::Event for SpentItem
块。最后要做的是通过更改当前签名来调整 SpentItem::decode
函数
pub fn decode(log: &substreams_ethereum::pb::eth::v2::Log) -> Result<Self, String>
使其接受一个 Vec<ethabi::Token>
并将变量重命名为 values
,同时使其可变
pub fn decode(mut values: Vec<ethabi::Token>) -> Result<Self, String>
并最终,移除函数体中我们现在的上一个 let mut values
定义,它看起来像
let mut values = ethabi::decode(
&[
ethabi::ParamType::Uint(8usize),
ethabi::ParamType::Uint(256usize),
],
log.data.as_ref(),
)
.map_err(|e| format!("unable to decode log.data: {:?}",
注意 您必须保留下面的
values.reverse()
部分,这对于解码代码的正确运行很重要。
现在一切就绪,您可以使用 OrderFulfilled
来解码包含 tuple
的事件。最终的代码看起来像这样
const INTERNAL_ERR: &str = "decode event internal error";
#[derive(Debug, Clone, PartialEq)]
pub struct OrderFulfilled {
pub order_hash: [u8; 32usize],
pub offer: Vec<SpentItem>,
}
impl OrderFulfilled {
const TOPIC_ID: [u8; 32] =
hex_literal::hex!("e86f4727db138d4b9cb776888b1d2239562eafaa38dd110b7d5def7698ccfd41");
pub fn match_log(log: &substreams_ethereum::pb::eth::v2::Log) -> bool {
if log.topics.len() != 1usize {
return false;
}
return log.topics.get(0).expect("bounds already checked").as_ref() == Self::TOPIC_ID;
}
pub fn decode(log: &substreams_ethereum::pb::eth::v2::Log) -> Result<Self, String> {
let mut values = ethabi::decode(
&[
ethabi::ParamType::FixedBytes(32usize),
ethabi::ParamType::Array(Box::new(ethabi::ParamType::Tuple(vec![
ethabi::ParamType::Uint(8usize),
ethabi::ParamType::Uint(256usize),
]))),
],
log.data.as_ref(),
)
.map_err(|e| format!("unable to decode log.data: {:?}", e))?;
values.reverse();
Ok(Self {
order_hash: {
let mut result = [0u8; 32];
let v = values
.pop()
.expect(INTERNAL_ERR)
.into_fixed_bytes()
.expect(INTERNAL_ERR);
result.copy_from_slice(&v);
result
},
offer: values
.pop()
.expect(INTERNAL_ERR)
.into_array()
.expect(INTERNAL_ERR)
.into_iter()
.map(|inner| {
let fields = inner.into_tuple().expect(INTERNAL_ERR);
SpentItem::decode(fields).unwrap()
})
.collect(),
})
}
}
impl substreams_ethereum::Event for OrderFulfilled {
const NAME: &'static str = "OrderFulfilled";
fn match_log(log: &substreams_ethereum::pb::eth::v2::Log) -> bool {
Self::match_log(log)
}
fn decode(log: &substreams_ethereum::pb::eth::v2::Log) -> Result<Self, String> {
Self::decode(log)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SpentItem {
pub item_type: substreams::scalar::BigInt,
pub amount: substreams::scalar::BigInt,
}
impl SpentItem {
pub fn decode(mut values: Vec<ethabi::Token>) -> Result<Self, String> {
values.reverse();
Ok(Self {
item_type: {
let mut v = [0 as u8; 32];
values
.pop()
.expect(INTERNAL_ERR)
.into_uint()
.expect(INTERNAL_ERR)
.to_big_endian(v.as_mut_slice());
substreams::scalar::BigInt::from_unsigned_bytes_be(&v)
},
amount: {
let mut v = [0 as u8; 32];
values
.pop()
.expect(INTERNAL_ERR)
.into_uint()
.expect(INTERNAL_ERR)
.to_big_endian(v.as_mut_slice());
substreams::scalar::BigInt::from_unsigned_bytes_be(&v)
},
})
}
}
如果您遇到任何困难,请在 Discord 上联系我们,我们将帮助您解决问题。
依赖项
~7–10MB
~167K SLoC