6 个版本 (重大变更)
0.6.1 | 2024年2月19日 |
---|---|
0.6.0 | 2024年2月9日 |
0.5.0 | 2024年2月7日 |
0.4.0 | 2024年1月17日 |
0.1.0 |
|
#180 in #chain
每月下载量 108
用于 2 crates
340KB
7K SLoC
substrate-parser
基于 Substrate 的链数据通用解析器
lib.rs
:
此 crate 是 Substrate 链数据的解析器。它可以用于解码可签署交易、调用、事件、存储项等,并带有链元数据。解码后的数据可以进行模式匹配或以可读的形式表示。
关键特质 AsMetadata
描述了适合解析可签署交易和其他编码链项的元数据,例如链存储中的数据。特质 AsCompleteMetadata
是 AsMetadata
的少数附加属性,它描述了适合解析未检查的外部的元数据。
特质 AsMetadata
和 AsCompleteMetadata
也可以应用于外部内存中可访问的元数据。由于元数据通常为数百 KB,这对于内存容量有限的硬件设备很有用。
AsMetadata
和 AsCompleteMetadata
已针对 RuntimeMetadata
版本 V14
和 V15
实现,这两个版本都内置了类型注册表,允许使用元数据本身跟踪类型,而无需任何附加信息。
假设
链数据是 SCALE 编码。进入解码器的数据块应被完全解码:所有提供的字节必须在解码中使用,没有数据未解析。
解码入口类型(例如特定存储项的类型)或用于在元数据中找到入口类型的内部结构(例如可签名的交易或未检查的外部传输)必须已知。
入口类型在元数据内置类型注册表中解析为构成类型,并从输入数据块中选择适当的字节块进行解码。该过程遵循来自SCALE编解码器的decode
方法,除了在解码过程中动态找到进入解码器的类型。
可签名的交易
可签名的交易由调用部分和扩展部分组成。
调用部分可能或可能不是双重SCALE编码,即SCALE编码的调用数据可能或可能不是由编码的调用长度的前缀compact
开始。
使用双重SCALE编码的调用数据的可签名的交易使用函数parse_transaction
进行处理。调用长度允许将编码的调用数据和扩展分开,并独立解码它们,首先是扩展。如果在尝试解析交易时尝试了多个元数据入口(相同的链,不同的spec_version
),则这种方法更可取,因为扩展必须包含spec_version
元数据,从而允许在调用解码开始之前检查是否使用了正确的解码。
无长度前缀的可签名的交易使用函数parse_transaction_unmarked
进行解析,首先是调用。同样,在扩展链中找到的spec_version
被检查以确保使用了正确的元数据。
调用解析的入口点是call_ty
。这是描述链上所有调用的类型。实际上,call_ty
指向一个枚举,其变体对应于所有小工具,数据的第一u8
是工具索引。每个变体都期望只有一个字段,其类型也是枚举。第二个枚举表示在所选小工具中可用的所有调用,数据中的第二个u8
是枚举变体索引,即事务中确切包含的调用。其他数据只是所选变体的字段集合。
剩余数据是可签名的扩展的SCALE编码集合,如v14::ExtrinsicMetadata
(用于V14
)和v15::ExtrinsicMetadata
(用于V15
)中所声明的。必须在解码的扩展中找到链的创世哈希,并且必须与链上已知的创世哈希匹配,如果为解析器提供了该哈希。必须在解码的扩展中找到spec_version
,并且必须与从提供的元数据中推导出的spec_version
匹配。
存储项
可以通过rpc调用从链中查询存储项,检索到的SCALE编码数据在相应的链元数据StorageEntryType
中声明。
存储项(键和值的组合)使用decode_as_storage_entry
函数进行解析。
未检查的外部事务
可以使用实现AsCompleteMetadata
特质的元数据,通过函数decode_as_unchecked_extrinsic
对未检查的外部事务进行解码。
其他项目
如果已知对应类型,则可以使用函数decode_as_type_at_position
对字节块中的任何部分进行解码。
对于整个块对应单一已知类型的情况,建议使用函数decode_all_as_type
。
解析数据和卡片
使用给定类型解析数据会产生ExtendedData
。将数据解析为调用结果会产生Call
。这两种类型都很复杂,通常包含多层解析数据。在解析过程中,尽可能地保留内部数据结构、标识符和文档,以便更容易在解析项中查找信息或进行模式匹配。
所有解析结果都可以制成卡片。卡片是带有类型相关信息的扁平格式元素,可以打印或以其他方式显示给用户。每个Call
和ExtendedData
都会制成Vec<ExtendedCard>
。
特殊类型
存储在元数据类型注册表中的类型具有相关的Path
信息。用于检测特殊类型的Path
的ident
段。
一些Path
标识符未经进一步检查即可使用,例如知名的基于数组的类型(AccountId32
、散列、公钥、签名等)或具有已知或容易确定的编码大小的其他类型,如Era
、PerThing
项等。
其他Path
标识符首先进行检查,并且仅在发现的类型信息与预期匹配时才使用,这对于Call
和Event
是这种情况。如果不匹配,则数据按原样解析,即不适用于特定项目格式。
枚举和结构体包含一组Field
。字段name
和type_name
也可能提供有关类型特殊信息的线索,尽管不如Path
可靠。这样的提示不会在解析流程中出现错误,并且会被忽略。
字段可能包含与货币相关的数据。在制成卡片并显示时,只有在出现在显示余额的包或扩展中时,货币才会以链上小数和单位显示。
某些类型(如 AccountId32
、Era
、公钥类型、签名类型)在本软件包(模块 additional_types
)中进行了重新定义,与在 sp_core
和 sp_runtime
软件包中的原始类型类似。这样做是为了确保与 no_std
的兼容性并简化依赖关系树。如果需要,这些类型的内部内容可以无缝转移到原始类型中。
特性
在 default-features = false
模式下,软件包支持 no_std
。
示例
# #[cfg(feature = "std")]
# {
use frame_metadata::v14::RuntimeMetadataV14;
use parity_scale_codec::Decode;
use primitive_types::H256;
use scale_info::{IntoPortable, Path, Registry};
use std::str::FromStr;
use substrate_parser::{
parse_transaction,
AddressableBuffer,
AsMetadata,
additional_types::{AccountId32, Era},
cards::{
Call, ExtendedData, FieldData, Info,
PalletSpecificData, ParsedData, VariantData,
},
special_indicators::SpecialtyUnsignedInteger,
};
// A simple signable transaction: Alice sends some cash by `transfer_keep_alive` method
let signable_data = hex::decode("9c0403008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a480284d717d5031504025a62029723000007000000e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e98a8ee9e389043cd8a9954b254d822d34138b9ae97d3b7f50dc6781b13df8d84").unwrap();
// Hexadecimal metadata, such as one fetched through rpc query
let metadata_westend9111_hex = std::fs::read_to_string("for_tests/westend9111").unwrap();
// SCALE-encoded `V14` metadata, first 5 elements cut off here are `META` prefix and
// `V14` enum index
let metadata_westend9111_vec = hex::decode(&metadata_westend9111_hex.trim()).unwrap()[5..].to_vec();
// `RuntimeMetadataV14` decoded and ready to use.
let metadata_westend9111 = RuntimeMetadataV14::decode(&mut &metadata_westend9111_vec[..]).unwrap();
// Chain genesis hash, typically well-known. Could be fetched through a separate rpc query.
let westend_genesis_hash = H256::from_str("e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e").unwrap();
let parsed = parse_transaction(
&signable_data.as_ref(),
&mut (),
&metadata_westend9111,
Some(westend_genesis_hash),
).unwrap();
let call_data = parsed.call_result.unwrap();
// Pallet name.
assert_eq!(call_data.0.pallet_name, "Balances");
// Call name within the pallet.
assert_eq!(call_data.0.variant_name, "transfer_keep_alive");
// Call contents are the associated `Field` data.
let expected_field_data = vec![
FieldData {
field_name: Some(String::from("dest")),
type_name: Some(String::from("<T::Lookup as StaticLookup>::Source")),
field_docs: String::new(),
data: ExtendedData {
data: ParsedData::Variant(VariantData {
variant_name: String::from("Id"),
variant_docs: String::new(),
fields: vec![
FieldData {
field_name: None,
type_name: Some(String::from("AccountId")),
field_docs: String::new(),
data: ExtendedData {
data: ParsedData::Id(AccountId32(hex::decode("8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48").unwrap().try_into().unwrap())),
info: vec![
Info {
docs: String::new(),
path: Path::from_segments(vec![
"sp_core",
"crypto",
"AccountId32"
])
.unwrap()
.into_portable(&mut Registry::new()),
}
]
}
}
]
}),
info: vec![
Info {
docs: String::new(),
path: Path::from_segments(vec![
"sp_runtime",
"multiaddress",
"MultiAddress"
])
.unwrap()
.into_portable(&mut Registry::new()),
}
],
}
},
FieldData {
field_name: Some(String::from("value")),
type_name: Some(String::from("T::Balance")),
field_docs: String::new(),
data: ExtendedData {
data: ParsedData::PrimitiveU128{
value: 100000000,
specialty: SpecialtyUnsignedInteger::Balance,
},
info: Vec::new()
}
}
];
assert_eq!(call_data.0.fields, expected_field_data);
// Parsed extensions. Note that many extensions are empty.
let expected_extensions_data = vec![
ExtendedData {
data: ParsedData::Composite(Vec::new()),
info: vec![
Info {
docs: String::new(),
path: Path::from_segments(vec![
"frame_system",
"extensions",
"check_spec_version",
"CheckSpecVersion",
])
.unwrap()
.into_portable(&mut Registry::new()),
}
]
},
ExtendedData {
data: ParsedData::Composite(Vec::new()),
info: vec![
Info {
docs: String::new(),
path: Path::from_segments(vec![
"frame_system",
"extensions",
"check_tx_version",
"CheckTxVersion",
])
.unwrap()
.into_portable(&mut Registry::new()),
}
]
},
ExtendedData {
data: ParsedData::Composite(Vec::new()),
info: vec![
Info {
docs: String::new(),
path: Path::from_segments(vec![
"frame_system",
"extensions",
"check_genesis",
"CheckGenesis",
])
.unwrap()
.into_portable(&mut Registry::new()),
}
]
},
ExtendedData {
data: ParsedData::Composite(vec![
FieldData {
field_name: None,
type_name: Some(String::from("Era")),
field_docs: String::new(),
data: ExtendedData {
info: vec![
Info {
docs: String::new(),
path: Path::from_segments(vec![
"sp_runtime",
"generic",
"era",
"Era",
])
.unwrap()
.into_portable(&mut Registry::new()),
}
],
data: ParsedData::Era(Era::Mortal(64, 61)),
}
}
]),
info: vec![
Info {
docs: String::new(),
path: Path::from_segments(vec![
"frame_system",
"extensions",
"check_mortality",
"CheckMortality",
])
.unwrap()
.into_portable(&mut Registry::new()),
}
]
},
ExtendedData {
data: ParsedData::Composite(vec![
FieldData {
field_name: None,
type_name: Some(String::from("T::Index")),
field_docs: String::new(),
data: ExtendedData {
data: ParsedData::PrimitiveU32 {
value: 261,
specialty: SpecialtyUnsignedInteger::Nonce,
},
info: Vec::new()
}
}
]),
info: vec![
Info {
docs: String::new(),
path: Path::from_segments(vec![
"frame_system",
"extensions",
"check_nonce",
"CheckNonce",
])
.unwrap()
.into_portable(&mut Registry::new()),
}
]
},
ExtendedData {
data: ParsedData::Composite(Vec::new()),
info: vec![
Info {
docs: String::new(),
path: Path::from_segments(vec![
"frame_system",
"extensions",
"check_weight",
"CheckWeight",
])
.unwrap()
.into_portable(&mut Registry::new()),
}
]
},
ExtendedData {
data: ParsedData::Composite(vec![
FieldData {
field_name: None,
type_name: Some(String::from("BalanceOf<T>")),
field_docs: String::new(),
data: ExtendedData {
data: ParsedData::PrimitiveU128 {
value: 10000000,
specialty: SpecialtyUnsignedInteger::Tip
},
info: Vec::new()
}
}
]),
info: vec![
Info {
docs: String::new(),
path: Path::from_segments(vec![
"pallet_transaction_payment",
"ChargeTransactionPayment",
])
.unwrap()
.into_portable(&mut Registry::new()),
}
]
},
ExtendedData {
data: ParsedData::PrimitiveU32 {
value: 9111,
specialty: SpecialtyUnsignedInteger::SpecVersion
},
info: Vec::new()
},
ExtendedData {
data: ParsedData::PrimitiveU32 {
value: 7,
specialty: SpecialtyUnsignedInteger::TxVersion
},
info: Vec::new()
},
ExtendedData {
data: ParsedData::GenesisHash(H256::from_str("e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e").unwrap()),
info: vec![
Info {
docs: String::new(),
path: Path::from_segments(vec![
"primitive_types",
"H256",
])
.unwrap()
.into_portable(&mut Registry::new()),
}
]
},
ExtendedData {
data: ParsedData::BlockHash(H256::from_str("98a8ee9e389043cd8a9954b254d822d34138b9ae97d3b7f50dc6781b13df8d84").unwrap()),
info: vec![
Info {
docs: String::new(),
path: Path::from_segments(vec![
"primitive_types",
"H256",
])
.unwrap()
.into_portable(&mut Registry::new()),
}
]
},
ExtendedData {
data: ParsedData::Tuple(Vec::new()),
info: Vec::new()
},
ExtendedData {
data: ParsedData::Tuple(Vec::new()),
info: Vec::new()
},
ExtendedData {
data: ParsedData::Tuple(Vec::new()),
info: Vec::new()
}
];
assert_eq!(parsed.extensions, expected_extensions_data);
# }
可以使用 card
方法将解析的数据转换为一系列平坦和格式化的 ExtendedCard
卡。
可以使用 show
或 show_with_docs
方法将卡打印成可读的字符串。
依赖
~8MB
~150K SLoC