#substreams #firehose #ethereum #thegraph #helper #streamingfast #api-bindings

substreams-ethereum-derive

Ethereum 链的 Substreams 开发套件,包含 Firehose Block 模型和辅助工具,以及 Ethereum ABI 编码/解码实用程序。

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 日

#19#streamingfast

Download history 363/week @ 2024-05-03 319/week @ 2024-05-10 355/week @ 2024-05-17 486/week @ 2024-05-24 442/week @ 2024-05-31 324/week @ 2024-06-07 328/week @ 2024-06-14 305/week @ 2024-06-21 126/week @ 2024-06-28 88/week @ 2024-07-05 260/week @ 2024-07-12 297/week @ 2024-07-19 281/week @ 2024-07-26 334/week @ 2024-08-02 293/week @ 2024-08-09 365/week @ 2024-08-16

1,313 每月下载量
2 个 crate 中使用(通过 substreams-ethereum

Apache-2.0

120KB
1.5K 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

注意事项

带有 Tuple 的 ABI

现在支持为 ABI 生成的代码使用 Tuple。它确实为使用它们的 event/function 生成 Rust 无名 Tuple。如果您更喜欢在事件中使用“struct”来生成 Tuple,以下说明可以使用。您可以展开下面的 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)
    }
}

由于在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错误。

您可以使用以下命令在“修改过的”ABI文件上运行以获取正确的有序类型: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 函数中,找到实际进行解码工作的代码片段 pub fn decode(log: <type>),它看起来是这样的:

 ...
 },
 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
~168K SLoC