Como construir Pallets personalizados com Substrate
Aprenda a criar um pallet personalizado usando o framework de desenvolvimento Substrate
Introdução
Neste tutorial, construiremos um pallet personalizado usando o framework de desenvolvimento Substrate e o FRAME v1. Começaremos com uma visão geral para entender os pallets e a framework FRAME. Em seguida, veremos o modelo de pallet Substrate, usando-o para criar nosso pallet personalizado, gravar casos de teste, publicar usando git e finalmente adicionar o pallet publicado ao nosso runtime.
Pré-requisitos
Este tutorial pressupõe que o leitor esteja familiarizado com a linguagem de programação Rust e tenha um entendimento básico do framework do Substrate.
Pallets
Os pallets são módulos de tempo de execução específicos do domínio, que nos permitem ter um design modular. Podemos adicionar vários pallets ao nosso tempo de execução.
Para referência, alguns dos pallets pré-construídos mais populares podem ser encontrados no site para desenvolvedores do Substrate.
FRAME v1
FRAME é o framework usado para desenvolver pallets, ele vem com um conjunto de bibliotecas e módulos iniciais. Abaixo, o esqueleto de um pallet baseado no FRAME:
1.Importações e dependências
O pallet suporta o uso de qualquer biblioteca Rust que compile com a flag no_std
.
use support::{decl_module, decl_event, decl_storage, ...}
2.Runtime Config Trait
Todos os tipos e constantes de tempo de execução entrarão aqui. Se o pallet depender de outros pallets, suas características de configuração deverão ser adicionadas à lista de características herdadas.
pub trait Config: frame_system::Config { ... }
3.Eventos de tempo de execução
Os eventos são um meio simples de relatar eventos específicos que ocorreram quando usuários, dApps e / ou exploradores de blocos considerarem interessantes e, que de outra forma, seriam difíceis de detectar.
Leia mais sobre Eventos de tempo de execução
decl_event! { ... }
4.Armazenamento em tempo de execução
Isso permite o uso seguro do banco de dados de armazenamento Substrate, para que você possa manter as coisas entre os blocos.
Leia mais sobre Armazenamento em tempo de execução
decl_storage! { ... }
5.Erros de tempo de execução
Esse é um enum que nos permite definir tipos de erros personalizados que podem ser chamados no módulo de tempo de execução.
decl_error! { ... }
6.Módulo de tempo de execução
Isso define o Module
estrutura que é finalmente exportada deste pallet. Ele define as funções exigíveis que este pallet expõe e orquestra as ações que esse pallet realiza durante a execução do bloco.
decl_module!{...}
Vamos agora dar uma olhada em como construir nosso pallet dentro dessa estrutura.
Usando o Modelo Pallet
Podemos começar usando o modelo de pallet do Substrate, que fornece o código inicial usando o Frame v1
Confira o Modelo de pallet Substrate
Agora você pode clonar o repositório gerado na sua máquina local executando o seguinte comando no seu terminal:
git clone [https://github.com/](https://github.com/)<YOUR_GITHUB_USERNAME>/pallet-identity.git
Compreendendo o código do modelo
Primeiro, estamos importando as macros do rust da biblioteca frame_support, necessárias para criar nosso módulo de tempo de execução:
decl_module
, armazenamento: decl_storage
, eventos: decl_event
, erros: decl_error, dispatch
para DispatchResult e o Get
para ‘pegar’ as característica usadas pelo armazenamento e converte-las em tipos. Também devemos importar assegure_signed do frame_system para verificar se as transações são assinadas.
Também importamos ensure_signed
de frame_system
para verificar se as transações são assinadas.
#![cfg_attr(not(feature = "std"), no_std)]
use frame_support::{decl_module, decl_storage, decl_event, decl_error, dispatch, traits::Get};
use frame_system::ensure_signed;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
Configuração
pub trait Config: frame_system::Config {
/// Como esse _pallet_ emite eventos, depende da definição de ///um evento no tempo de execução.
type Event: From<Event<Self>> + Into<<Self as frame_system::Config>::Event>;
Declarando armazenamento
Aqui o nome de armazenamento do pallet TemplateModule
deve ser diferente do nome de armazenamento de outros pallets.
Something
é um item de armazenamento, que pode armazenar um número inteiro opcional de 32 bits não assinado. Este item de armazenamento pode ser acessado no módulo de tempo de execução usando a função something()
. Podemos definir mais itens de armazenamento, se quisermos.
Leia mais sobre declarar Itens de armazenamento
decl_storage! {
trait Store for Module<T: Config> as TemplateModule {
Something get(fn something): Option<u32>;
}
O tempo de execução suporta a emissão de eventos especificados em chamadas de método. Podemos passar argumentos com cada evento. Aqui SomethingStored
emite um número inteiro de 32 bits não assinado e um AccountId
A declaração dos eventos seria feita aqui:
decl_event!(
pub enum Event<T> where AccountId = <T as frame_system::Config>::AccountId {
SomethingStored(u32, AccountId),
}
Podemos declarar eventos com o enum Error, que pode ser chamado nas chamadas do módulo. Isso informa ao usuário que algo deu errado.
decl_error! {
pub enum Error for Module<T: Config> {
NoneValue,
StorageOverflow,
}
Módulo de tempo de execução
É aqui que declaramos chamadas de método ( transações ) que podem ser usadas para modificar / consultar o estado da cadeia, elas também podem emitir eventos e erros. Para cada função despachável, o Peso deve ser mencionado e deve retornar um DispatchResult
decl_module! {
pub struct Module<T: Config> for enum Call where origin: T::Origin {
// Os erros devem ser inicializados se eles forem
// utilizados pelo _pallet_.
type Error = Error<T>;
// Eventos devem ser inicializados se eles forem //utilizados pelo _pallet_.
fn deposit_event() = default;
/// Um exemplo despachável que recebe um valor único como /// parâmetro, registra o valor no armazenamento e emite um evento. /// Esta função deve ser despachada por um extrínseco assinado.
#[weight = 10_000 + T::DbWeight::get().writes(1)]
pub fn do_something(origin, something: u32) -> dispatch::DispatchResult {
// Verifica que o extrínseco foi assinado e obtém o signatário.
// Esta função retornará um erro se o extrínseco não estiver assinado.
// https://substrate.dev/docs/en/knowledgebase/runtime/origin
let who = ensure_signed(origin)?;
//Atualiza o armazenamento.
Something::put(something);
// Emite um evento.
Self::deposit_event(RawEvent::SomethingStored(something, who));
// Retorna o sucesso de DispatchResult
Ok(())
}
/// Um exemplo despachável que pode gerar um erro personalizado.
#[weight = 10_000 + T::DbWeight::get().reads_writes(1,1)]
pub fn cause_error(origin) -> dispatch::DispatchResult {
let _who = ensure_signed(origin)?;
// Leia um valor do armazenamento.
match Something::get() {
// Retorna um erro se o valor não tiver sido definido.
None => Err(Error::<T>::NoneValue)?,
Some(old) => {
//Incrementa o valor lido do armazenamento; retornará erro em caso //de sobrecarga
let new = old.checked_add(1).ok_or(Error::<T>::StorageOverflow)?;
// Atualiza o valor no armazenamento com o resultado incrementado
Something::put(new);
Ok(())
},
}
}
}
}
Projetando um pallet personalizado
Em nosso exemplo, estamos projetando um pallet de identidade, onde:
Os usuários podem gerar uma nova identidade
Adicionar / remover atributos a essa identidade
Excluir a identidade
Itens de armazenamento
Mapa de identidade: Identity = > AccountId
Mapa do atributo: ( Identity, Atribute,_Key ) = > Attribute_Value
Eventos
IdentityCreated ( Identity, AccountId )
AttributeAdded ( Identity, Atribute_Key, Attribute_Value )
AttributeRemoved ( Identity, Atribute_Key ),
Erros
IdentityAlreadyClaimed
IdentityNotFound
NotAuthorized
AttributeNotFound
Chamadas de função
create_identity ( Identity )
add_attribute ( Identity, Atribute_Key, Attribute_Value )
remove_attribute ( Identity, Atribute_Key )
Implementando o pallet
Dê uma olhada nesse código de exemplo
Armazenamento
Para armazenamento, criamos um mapeamento de Identity
para AccountId. Identity
aqui seria uma sequência transformada em um vetor de tamanho u8.
Outro item de armazenamento seria o Attribute
que é um mapeamento de um tuplo de identity vector
e attribute key vector
para attribute value vector
.
decl_storage! {
trait Store for Module<T: Config> as IdentityModule {
pub Identity get(fn get_identity): map hasher(blake2_128_concat) Vec<u8> => Option<T::AccountId>;
// ( identity, attribute_key ) => attribute_value
pub Attribute get(fn get_attribute): map hasher(blake2_128_concat) (Vec<u8>, Vec<u8>) => Vec<u8>;
}
}
Eventos
decl_event!(
pub enum Event<T> where AccountId = <T as frame_system::Config>::AccountId {
// Identity, AccountId
IdentityCreated(Vec<u8>, AccountId),
// Identity, Attribute Key, Attribute Value
AttributeAdded(Vec<u8>, Vec<u8>, Vec<u8>),
// Identity, Attribute Key
AttributeRemoved(Vec<u8>, Vec<u8>),
})
Erros
// Erros informando aos usuários que algo deu errado.
decl_error! {
pub enum Error for Module<T: Config> {
IdentityAlreadyClaimed,
IdentityNotFound,
NotAuthorized,
AttributeNotFound,
}
}
Chamadas de função
1.Criação de identidade
Permite ao usuário escolher uma identidade única. Deve gerar erro quando Identity estiver com already reivindicado.
#[weight = 10_000 + T::DbWeight::get().reads_writes(1, 1)]
pub fn create_identity(
origin,
identity: Vec<u8>
) -> dispatch::DispatchResult {
let who = ensure_signed(origin)?;
match <Identity<T>>::get(&identity) {
// Retorna um erro se o signatário não for o proprietário da identidade
None => {
// Update storage.
<Identity<T>>::insert(&identity, &who);
// Emit an event.
Self::deposit_event(RawEvent::IdentityCreated(identity, who));
// Retorna o sucesso da DispatchResult
Ok(())
},
Some(_) => Err(Error::<T>::IdentityAlreadyClaimed)?
}
}
2.Adicionar atributo
Permite que o proprietário da identidade adicione valores-chave de atributo à sua própria identidade. Isso deve gerar um erro ao tentar adicionar atributos à identidade de outra pessoa ou se a identidade não for encontrada.
// Permite que os proprietários de identidade adicionem atributos à si //(key, value)
#[weight = 10_000 + T::DbWeight::get().reads_writes(1,1)]
pub fn add_attribute(
origin,
identity: Vec<u8>,
attribute_key: Vec<u8>,
attribute_value: Vec<u8>
) -> dispatch::DispatchResult {
let who = ensure_signed(origin)?;
// Lê o valor do armazenamento
match <Identity<T>>::get(&identity) {
// Retorna um erro se o signatário não for o proprietário da identidade
None => Err(Error::<T>::IdentityNotFound)?,
Some(address) => {
if address != who {
return Err(Error::<T>::NotAuthorized)?
} else{
Attribute::insert((&identity, &attribute_key), &attribute_value);
Self::deposit_event(RawEvent::AttributeAdded(identity, attribute_key, attribute_value));
Ok(())
}
},
}
}
3.Remoção de atributo
// Permite que os proprietários de identidade removam a identidade
#[weight = 10_000 + T::DbWeight::get().reads_writes(1,1)]
pub fn remove_attribute(
origin,
identity: Vec<u8>,
attribute_key: Vec<u8>,
) -> dispatch::DispatchResult {
let who = ensure_signed(origin)?;
//Lê o valor do armazenamento.
match <Identity<T>>::get(&identity) {
//Retorna um erro se o signatário não for o proprietário da identidade
None => Err(Error::<T>::IdentityNotFound)?,
Some(address) => {
if address != who {
return Err(Error::<T>::NotAuthorized)?
} else{
Attribute::remove((&identity, &attribute_key));
Self::deposit_event(RawEvent::AttributeRemoved(identity, attribute_key));
Ok(())
}
},
}
}
Escrevendo casos de teste
Confira o exemplo de testes.
Implementamos dois casos de teste:
Criação de identidade,
Adição de atributos e
Remoção atributo
# test
fn should_not_throw_errors() {
new_test_ext().execute_with(|| {
// Despacha um extrínseco assinado
//cria uma identidade "prasad" para accountId 1
let identity = "prasad".as_bytes().to_vec();
assert_ok!(IdentityModule::create_identity(Origin::signed(1), "prasad".as_bytes().to_vec() ));
// Lê o armazenamento do _pallet_ confirma um resultado esperado.
assert_eq!(IdentityModule::get_identity(&identity), Some(1));
let attribute_key = "name".as_bytes().to_vec();
let attribute_value = "prasad kumkar".as_bytes().to_vec();
// adiciona um nome ao atributo => prasad kumkar
assert_ok!(IdentityModule::add_attribute(Origin::signed(1), "prasad".as_bytes().to_vec(), "name".as_bytes().to_vec(), "prasad kumkar".as_bytes().to_vec()));
// checa o valor do atributo
assert_eq!(IdentityModule::get_attribute((&identity, &attribute_key)), attribute_value);
// Remove o atributo
assert_ok!(IdentityModule::remove_attribute(Origin::signed(1), "prasad".as_bytes().to_vec(), "name".as_bytes().to_vec()));
//Depois da remoção, o atributo deve estar vazio
assert_eq!(IdentityModule::get_attribute((identity, attribute_key)), "".as_bytes().to_vec());
});
}
Account
cria identidade 'prasad',Chama
IdentityAlreadyClaimed
quandoAccount 2
cria a mesma identidadeChama
Not Authorized Error
erro ao adicionar atributos aAccount 1
'identidade daAccount 2
# test
fn check_for_errors() {
new_test_ext().execute_with(|| {
// Despacha um extrínseco assinado.
let identity = "prasad".as_bytes().to_vec();
assert_ok!(IdentityModule::create_identity(Origin::signed(1), "prasad".as_bytes().to_vec() ));
// Lê o armazenamento de _pallet_ e confirma o resultado esperado
assert_eq!(IdentityModule::get_identity(&identity), Some(1));
// Deve retornar um erro de que a identidade "prasad" já foi //reivindicada
let identity = "prasad".as_bytes().to_vec();
assert_noop!(
IdentityModule::create_identity(
Origin::signed(2),
"prasad".as_bytes().to_vec()),
Error::<Test>::IdentityAlreadyClaimed);
// add_attribute é assinado por uma identidade diferente (2)
// deve retornar o erro NotAuthorized
assert_noop!(
IdentityModule::add_attribute(
Origin::signed(2),
"prasad".as_bytes().to_vec(),
"name".as_bytes().to_vec(),
"prasad kumkar".as_bytes().to_vec()
),
Error::<Test>::NotAuthorized);
// O valor do atributo deve estar em branco
assert_eq!(IdentityModule::get_attribute((identity, "name".as_bytes().to_vec())), "".as_bytes().to_vec());
});
}
Construindo e Testando
Para construir o pallet completo com cargo, rode cargo build --release.
Para executar os testes em relação ao código compilado, execute cargo test
.
Publicação do pallet
Antes da publicação do nosso pallet, precisamos atualizar os detalhes do pallet. Aqui você precisaria atualizar o nome do pallet para pallet-identity
e alterar o link do repositório.
[package]
authors = ['Prasad-Kumkar <https://github.com/prasad-kumkar>']
description = 'Basic FRAME pallet for managing identities.'
edition = '2018'
homepage = 'https://substrate.dev'
license = 'Unlicense'
name = 'pallet-identity'
repository = 'https://github.com/prasad-kumkar/pallet-identity'
version = '3.0.0'
Depois disso, você pode publicar o código usando git:
git add .
git commit -am "commit message"
git push origin master
Adicionando ao tempo de execução
Para adicionar este pallet ao seu tempo de execução, basta incluir a seguinte seção no arquivo de tempo de execução Cargo.toml
( lembre-se de atualizar o link git ):
[dependencies.pallet-identity]
default_features = false git = '[https://github.com/prasad-kumkar/pallet-identity.git](https://github.com/prasad-kumkar/pallet-identity.git)'
Atualize também o recurso std do seu tempo de execução para incluir este pallet:
std = [
# --snip--
'pallet-identity/std',
]
Tempo de execução
Você deve implementar sua característica da seguinte maneira:
/// Utilizado pelo test_module
impl pallet_identity::Config for Runtime {
type Event = Event;
}
Inclua-o também no seu macro do construct_runtime!
:
IdentityPallet: pallet_identity::{Module, Call, Storage, Event<T>}
Conclusão
Para recapitular o conhecimento adquirido durante o tutorial, nós aprendemos sobre FRAME revisando o modelo de pallet substrate . Em seguida, construímos nosso pallet personalizado ( pallet de identidade ) e vimos como projetar, implementar, testar esse pallet. Em seguida, analisamos a publicação do pallet e a implementação em nosso tempo de execução do substrate. Espero que isso seja útil para a construção de qualquer pallet personalizado.
Autor
Prasad Kumkar é um engenheiro de blockchain com mais de 2 anos de experiência, co-fundador da Chainvote, uma solução de governança corporativa baseada em blockchain. Seu trabalho foi realizado em várias arquiteturas de blockchain, como Substrate, Hyperledger Fabric, cadeias baseadas em EVM, Solana e Near Protocol. Sua equipe foi vencedora no WyoHackathon 2020 e ETH Denver 2021.
Contato - [email protected]
Referências
Este artigo foi escrito por Prasad Kumkar e traduzido por Adriano P. de Araujo. O original em inglês pode ser encontrado aqui.**
Latest comments (0)