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

substreams-ethereum-abigen

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日

#23 in 魔法豆

Download history 335/week @ 2024-05-01 346/week @ 2024-05-08 332/week @ 2024-05-15 442/week @ 2024-05-22 495/week @ 2024-05-29 421/week @ 2024-06-05 301/week @ 2024-06-12 294/week @ 2024-06-19 220/week @ 2024-06-26 31/week @ 2024-07-03 233/week @ 2024-07-10 310/week @ 2024-07-17 271/week @ 2024-07-24 336/week @ 2024-07-31 307/week @ 2024-08-07 226/week @ 2024-08-14

1,215 每月下载量
用于 3 个crate(2个直接使用)

Apache-2.0

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