Esta é uma tradução de Bernardo Perelló. O artigo original de Garrin McGoldrick pode ser lido aqui.
Este tutorial aborda o desenvolvimento de um aplicativo Stateful da Algorand (contrato inteligente) em um ambiente python. O código-fonte do tutorial mostrado pode ser encontrado em . O tutorial foi desenvolvido junto com o pacote python algo-app-dev e funciona como uma introdução à sua funcionalidade.
O tutorial não apenas mostra as etapas necessárias para criar o contrato inteligente, mas também explica os conceitos necessários para entender essas etapas, vinculando à documentação py-algorand-sdk e pyteal quando possível.
Requisitos
O código-fonte do tutorial pode ser executado em um ambiente Ubuntu (> 18.04) (incluindo WSL2).
Obtenha o código-fonte do tutorial
Clone o projeto e atualize seu local no diretório do projeto.
git clone https://github.com/gmcgoldr/algorand-tut.git
cd algorand-tut
Instale o software do nó Algorand
Você usa o Algorand Sandbox ou instala um nó Algorand:
sudo apt-get update
sudo apt-get install -y gnupg2 curl software-properties-common
curl -O https://releases.algorand.com/key.pub
sudo apt-key add key.pub
rm -f key.pub
sudo add-apt-repository "deb [arch=amd64] https://releases.algorand.com/deb/ stable main"
Instale os pacotes Python
pip install -U py-algorand-sdk pyteal algo-app-dev[dev]
Contexto
Uma boa maneira de pensar em contratos inteligentes é em termos de transações. Um contrato inteligente define algum estado que é registrado no livro-razão e também algumas transações (alterações de estado) que são permitidas de acordo com as regras do contrato.
Todo o potencial desta tecnologia é realizado quando se pensa em transações de forma mais ampla do que apenas em um sentido financeiro.
Considere a questão da identidade pessoal. Atualmente, a identidade é amplamente estabelecida por credenciais emitidas por alguma forma de governo. Mas uma pessoa pode ter suas credenciais negadas, suas credenciais podem ser invalidadas e suas credenciais podem ser usadas para discriminar com base em status arbitrário.
Isso pode, até certo ponto, ser resolvido com transações de credibilidade em uma blockchain. Alguém pode criar uma transação na qual atesta as credenciais de outra pessoa. O gráfico de tais atestados pode ser usado para avaliar a credibilidade de uma pessoa, de maneira sem permissão, transparente e consistente.
No exemplo acima, Alice confia diretamente em Bob e Charlie. Alice provavelmente também pode confiar em Dave, já que ela conhece duas pessoas que atestam por Dave. Mas Alice pode suspeitar de Grace, porque ela conhece apenas uma pessoa que atesta Grace. O que é muito bom, pois Grace, Erin e Frank são bots conspirando para dar a impressão de credibilidade. E enganar Charlie não é suficiente para estabelecer credibilidade aos olhos de Alice.
Este tutorial cobrirá as etapas necessárias para criar um contrato inteligente.
Passos
- Preliminares
- Construa o Estado
- Construa a Lógica
- Crie o aplicativo na rede
- Faça chamadas ao aplicativo
- Testando o aplicativo
A seguir, explica como criar e chamar o aplicativo. O código associado pode ser encontrado em demo-app.py
. Os trechos de código assumem as seguintes importações: from pyteal import *
e algoappdev import *
. O resultado das funções pyteal
é tipicamente uma expressão que pode ser compilada no código-fonte TEAL.
Preliminares
A funcionalidade em aplicativos stateful (apps) é executada com uma transação de chamada de aplicativo. Esta é uma transação com o campo TxType definido para a variante app. Outros tipos de transação não são discutidos aqui.
As transações de chamada de aplicativo podem ser construídas com py-algorand-sdk
da seguinte forma:
future.transaction.Transaction(txn_type=constants.appcall_txn)
Há também uma variedade de classes derivadas que implementam comportamentos mais específicos.
Um aplicativo é composto por algum estado e algumas regras que especificam como o estado pode ser afetado por transações de chamada de aplicativo. É útil pensar no aplicativo como uma função e nas transações como chamando essa função, onde a transação está fornecendo argumentos para a chamada.
Existem muitos argumentos (campos) que podem ser acessados por um aplicativo.
A chamada do aplicativo como uma função pode ser descrita da seguinte forma (alguns argumentos são omitidos):
call_app(
# escopo: global
creator_address: Bytes,
current_application_id: Int,
latest_timestamp: Int,
...
# escopo: transação i
sender_i: Bytes,
on_completion_i: Int,
application_args_i_idx_j: Bytes,
applications_i_idx_j: Int,
assets_i_idx_j: Int,
...
# escopo: ativo i
asset_i_creator: Bytes,
asset_i_total: Int,
...
# escopo: ativo i, conta j
asset_i_account_j_balance: Int,
asset_i_account_j_frozen: Int,
# escopo: aplicativo i (armazenamento global)
app_i_key_k: Union[Bytes, Int],
# escopo: aplicativo i (armazenamento local)
app_i_account_j_key_k: Union[Bytes, Int],
)
Os argumentos de escopo global podem ser acessados com expressões: Global.field_name()
.
Os argumentos da transação podem ser acessados com expressões: Gtxn[i].field_name()
. E para aqueles campos que estão em um array (sufixo _idx acima), eles são acessados pela indexação em um array: Gtxn[i].array_name[j]
.
O estado do aplicativo pode ser acessado com os métodos:
App.globalGet(key)
App.localGet(address, key)
App.globalGetEx(id, key)
App.localGetEx(address, id, key)
Onde key
é a chave para o valor de estado a ser recuperado (o estado do aplicativo é um armazenamento de valor de chave); id
é o id do aplicativo no qual pesquisar a chave ou um índice na matriz de aplicativos; address
é o endereço do armazenamento local no qual pesquisar a chave ou um índice na matriz de contas.
As duas primeiras expressões retornam o valor do estado e funcionam apenas para o aplicativo atual. As duas últimas expressões retornam um objeto MaybeValue
que é uma expressão. Quando executado, ele constrói dois valores: se a chave foi encontrada ou não e seu valor (ou valor padrão se não for encontrado). Então, esses valores podem ser acessados com expressões retornadas pelos métodos value()
e hasValue()
.
No exemplo anterior, app_i_account_j_key_k
seria acessado por: App.localGetEx(address_j, app_id_i, key_k)
.
Observe as seguintes equivalências (tendo em mente que as chamadas GetEx
devem ser avaliadas primeiro e que as chamadas GetEx
com ID de aplicativo 0 usarão o aplicativo atual que está no índice 0 no array de aplicativos):
Txn
↔ Gtxn[0]
Txn.sender()
↔ Txn.accounts[0]
Global.current_application_id()
↔ Txn.applications[0]
App.globalGet(key)
↔ App.globalGetEx(0, key).value()
App.localGet(addr, key)
↔ App.localGetEx(addr, 0, key).value()
Um programa aplicativo é uma expressão pyteal
, que retorna zero ou um valor diferente de zero. Um valor diferente de zero indica que a transação foi bem-sucedida: as alterações feitas no estado do aplicativo durante a execução do programa são confirmadas. Um valor zero indica que a transação foi rejeitada: o estado permanece inalterado.
Um aplicativo stateful na cadeia Algorand consiste em dois programas: o programa de aprovação e o programa clear state.
O programa Clear state é executado quando uma transação de chamada de aplicativo é enviada com o código OnComplete
: ClearState
. Essa transação sempre removerá o estado do aplicativo local da conta do chamador, independentemente do valor de retorno.
Existem duas classes utilitárias em algo-app-dev
que ajudam na criação de aplicativos: A classe State
e a classe AppBuilder
. As seções a seguir abordam como usá-los para: definir o estado de um aplicativo e definir a lógica do aplicativo.
Construa o Estado
Um aplicativo pode manter o estado global e localmente (por conta). Até 64 valores podem ser armazenados no estado global e até 16 valores podem ser armazenados no estado local. Cada conta que opta pelo aplicativo pode armazenar sua própria instância do estado local.
O objeto base State
em algo-app-dev
é usado para descrever os pares de valores-chave que compõem o estado de um contrato. Ele é inicializado com uma lista de objetos State.KeyInfo
, cada um especificando a chave, o tipo de seu valor associado e possivelmente um valor padrão.
Um objeto State
é usado para:
construir expressões para definir e obter um valor de estado
construir uma expressão para definir valores padrão (construtor)
construir o esquema do aplicativo que define quanto espaço o aplicativo pode usar
A subclasse StateGlobalExternal
de State
é usada para descrever o estado global de um aplicativo externo (ou seja, qualquer aplicativo cujo id esteja no array Txn.applications
). Ele pode obter valores, mas não pode defini-los, pois os aplicativos externos são somente leitura.
A subclasse StateGlobal
de StateGlobalExternal
é usada para descrever o estado global do aplicativo atual. Ele adiciona a capacidade de definir valores. E adiciona um método get que retorna diretamente um valor, em vez de retornar um MaybeValue
(em um aplicativo externo, a existência de um valor não pode ser garantida).
As classes locais equivalentes são: StateLocalExternal
e StateLocal
.
Neste aplicativo, cada conta tem um nome associado a ela (a credencial), e até 8 contas podem atestar essa credencial. O armazenamento local é composto pelo nome e 8 endereços de voucher.
Os valores TEAL são do tipo Bytes
ou Int
. O tipo Bytes representa uma fatia de bytes e pode ser usado para representar dados binários arbitrários. Strings e endereços são codificados como fatias de bytes. O tipo Int
representa um inteiro de 64 bits sem sinal.
# o state consiste em 8 índices, cada um para um endereço de voucher
MAX_VOUCHERS = 8
state = apps.StateLocal(
[apps.State.KeyInfo(key="name", type=Bytes)]
+ [
apps.State.KeyInfo(key=f"voucher_{i}", type=Bytes)
for i in range(MAX_VOUCHERS)
]
)
Construa a Lógica
A classe AppBuilder
em algo-app-dev
cria a lógica de um programa de aprovação com as seguintes ramificações:
Txn.application_id() == Int(0)
→ Inicializa o estado
OnComplete == DeleteApplication
→ Deleta o estado e programas
OnComplete == UpdateApplication"=
→ Atualiza os programas
OnComplete == OptIn
→ Inicializa o estado local
OnComplete == CloseOut
→ Deleta o estado local
OnComplete == NoOp
and Txn.application_args[0] == Bytes(name)
→ Chame a invocação com o nome
OnComplete == NoOp
Chame a invocação padrão
Exatamente uma ramificação será executada quando uma chamada de aplicativo for feita. As ramificações podem ser desabilitadas fazendo com que retornem zero.
A ramificação de inicialização é invocada quando o ID do aplicativo é zero, o que acontece apenas quando uma chamada é feita para um aplicativo que ainda não está na cadeia.
O código OnComplete
indica qual alteração de estado a transação está solicitando. O código NoOp
solicita que a lógica do aplicativo seja executada sem nenhuma operação a seguir. Todos os outros códigos completos solicitam que algumas operações adicionais sejam realizadas após a execução da lógica do aplicativo. Por exemplo, o código DeleteApplication
solicita que os programas do aplicativo sejam excluídos juntamente com seu estado. Se a lógica do aplicativo aceitar a transação (retornar diferente de zero), a rede realizará as operações solicitadas.
Neste aplicativo de demonstração, o comportamento padrão do construtor de aplicativos é usado: a adesão é permitida, mas a exclusão, atualização e encerramento não são permitidos. Observe que o programa Clear State está sempre disponível e irá desativar uma conta e excluir seu estado local, independentemente do valor de retorno.
Além disso, as três ramificações a seguir são adicionadas: definir o nome (set_name
), garantir uma conta (vouch_for
) e receber uma garantia (vouch_from
).
O voucher e o vouchee devem concordar para que um voucher seja bem-sucedido. Não deve ser possível que um voucher aleatório ocupe pontos de voucher na conta vouchee. E não deve ser possível para um voucher reivindicar um voucher sem sua permissão.
A solução é condicionar a lógica de escrever um novo comprovante a duas transações em um grupo.
# a txn anterior no grupo é o enviada pelo voucher
voucher_txn = Gtxn[Txn.group_index() - Int(1)]
# o 3º argumento do vouchee txn é a chave para escrever no address to
vouch_key = Txn.application_args[2]
# Chaves vouch válidas (limiatdo a MAX_VOUCHERS)
vouch_keys = [Bytes(f"voucher_{i}") for i in range(MAX_VOUCHERS)]
builder = apps.AppBuilder(
invocations={
# definir o nome altera as credenciais e, portanto, deve limpar os
# vouchers (ou seja, os vouchers atestaram um nome, portanto, um novo nome
# requer novos comprovantes)
"set_name": Seq(
# deixe as antigas garantias
Seq(*[state.drop(f"voucher_{i}") for i in range(MAX_VOUCHERS)]),
# definir o novo nome
state.set("name", Txn.application_args[1]),
Return(Int(1)),
),
# sempre permitir que o voucher envie esta invocação
"vouch_for": Return(Int(1)),
# vouchee envia esta invocação para escrever o voucher para o estado local
"vouch_from": Seq(
# garantir que o voucher esteja usando este contrato
Assert(voucher_txn.application_id() == Global.current_application_id()),
# certifique-se de que o voucher está atestando
Assert(voucher_txn.application_args[0] == Bytes("vouch_for")),
# garantir que o voucher esteja confirmando o vouchee
Assert(voucher_txn.application_args[1] == Txn.sender()),
# garantir que o vouchee esteja recebendo o comprovante do voucher
Assert(Txn.application_args[1] == voucher_txn.sender()),
# garantir a configuração de uma chave de garantia válida
Assert(Or(*[vouch_key == k for k in vouch_keys])),
# armazene o endereço do voucher no índice de voucher fornecido
App.localPut(Txn.sender(), vouch_key, voucher_txn.sender()),
Return(Int(1)),
),
},
local_state=state,
)
Crie o Aplicativo na Rede
O método create_txn
combina todas as ramificações nos programas de aprovação e limpeza de estado e cria a transação necessária para publicar o aplicativo na cadeia.
txn = app_builder.create_txn(
algod_client, address, algod_client.suggested_params()
)
Aqui está a aparência da transação de criação do aplicativo:
def compile_expr(expr: Expr) -> str:
return compileTeal(
expr,
mode=Mode.Application,
version=MAX_TEAL_VERSION,
)
def compile_source(client: AlgodClient, source: str) -> bytes:
result = client.compile(source)
result = result["result"]
return base64.b64decode(result)
future.transaction.ApplicationCreateTxn(
# this will be the app creator
sender=address,
sp=params,
# no state change requested in this transaction beyond app creation
on_complete=OnComplete.NoOpOC.real,
approval_program=compile_source(client, compile_expr(self.approval_expr())),
clear_program=compile_source(client, compile_expr(self.clear_expr())),
global_schema=self.global_schema(),
local_schema=self.local_schema(),
)
O ID e o endereço do aplicativo podem ser recuperados do resultado da transação, usando a classe AppMeta
:
app_meta = utils.AppMeta.from_result(
transactions.get_confirmed_transaction(algod_client, txid, WAIT_ROUNDS)
)
Faça chamadas ao Aplicativo
Bob quer que a rede saiba que seu nome é Bob. Ele primeiro optará pelo aplicativo:
txn = future.transaction.ApplicationOptInTxn(
address_bob,
algod_client.suggested_params(),
app_meta.app_id,
)
Em seguida, ele vinculará seu nome à sua conta:
txn = future.transaction.ApplicationNoOpTxn(
address_bob,
algod_client.suggested_params(),
app_meta.app_id,
["set_name", "Bob"],
)
Agora ele pode pedir a Alice para atestar por ele:
txns = transactions.group_txns(
future.transaction.ApplicationNoOpTxn(
address_alice,
algod_client.suggested_params(),
app_meta.app_id,
# o endereço deve ser decodificado em bytes a partir de sua forma base64
["vouch_for", decode_address(address_bob)],
),
future.transaction.ApplicationNoOpTxn(
address_bob,
algod_client.suggested_params(),
app_meta.app_id,
[
"vouch_from",
decode_address(address_alice),
# Bob tem 8 índices de garantia para escolher, este é o primeiro, então ele o coloca no índice 0
"voucher_0",
],
),
)
Finalmente, ele enviará a transação de Alice para ela e fará com que ela assine. Então ele pode enviar as transações para a rede.
O front-end seria responsável por percorrer o gráfico para estabelecer a credibilidade de uma conta. Isso provavelmente seria feito na máquina de um usuário com chamadas feitas ao indexador em execução em um nó.
Testando o Aplicativo
O módulo algoappdev.dryruns
ajuda a configurar simulações para chamadas de aplicativos. As simulações permitem testes rápidos e retornam informações úteis de depuração.
Aqui está um exemplo simples de como usar o módulo dryruns
para testar a invocação set_name
:
jsdef test_can_set_name(algod_client: AlgodClient):
# O `AlgodClient` conectado ao nó com dados em `NODE_DIR`
# será construído e transmitido pelo `pytest`. É necessário compilar
# a fonte TEAL em bytes de programa e executar a simulação.
app_builder = app_vouch.build_app()
# construir um endereço fictício (não precisará assinar nada com ele)
address_1 = dryruns.idx_to_address(1)
result = algod_client.dryrun(
# construa um objeto que especificará completamente o contexto no qual
# a chamada do aplicativo é executada (ou seja, defina todos os argumentos)
dryruns.AppCallCtx()
# adicione um aplicativo ao contexto, use os programas do `app_builder` e
# defina o ID do aplicativo para 1
.with_app(app_builder.build_application(algod_client, 1))
# adicionar uma conta incluída no último aplicativo
.with_account_opted_in(address=address_1)
# criar uma chamada no-op com a última conta
.with_txn_call(args=["set_name", "abc"])
# construir a solicitação de dryrun
.build_request()
)
# levantar quaisquer erros no resultado do dryrun
dryruns.check_err(result)
# garantir que o programa retornou diferente de zero
assert dryruns.get_messages(result) == ["ApprovalProgram", "PASS"]
# garantir que o programa alterou o estado local da conta
assert dryruns.get_local_deltas(result) == {
address_1: [dryruns.KeyDelta(b"name", b"abc")]
}
A função dryruns.get_trace
pode ser usada para iterar sobre linhas de rastreamento de pilha, para quando as coisas derem errado.
Os testes de integração ainda devem envolver o envio de transações adequadas, embora fazê-lo com um nó no modo dev possa ajudar a acelerar significativamente as coisas. Em última análise, alguns testes devem ser executados na rede de teste real.
O módulo algoappdev.testing
inclui alguns acessórios úteis para testar aplicativos com pytest.
Defina a variável de ambiente AAD_NODE_DIR
para o diretório de dados do nó (por exemplo, /var/lib/algorand/nets/private_dev/Primary
). Em seguida, os fixtures podem ser usados para acessar rapidamente o algod_client
, kmd_client
e um funded_account
.
from algoappdev.testing import
*
O valor de testing.WAIT_ROUNDS
é carregado da variável de ambiente AAD_WAIT_ROUNDS
. Ao testar com um nó não desenvolvedor, isso deve ser definido como um valor de 5 ou maior, para dar tempo à rede para confirmar as transações.
Oldest comments (1)