Aprenda a construir um Formador de Mercado Automatizado (AMM) na Polkadot usando a linguagem de domínio específico incorporada (eDSL) ink!
Introdução
Neste tutorial, aprenderemos como construir um AMM com as funcionalidades de Fornecimento, Saque e Troca, com taxas de negociação e tolerância a derrapagens. Construiremos o contrato inteligente em ink!, uma Linguagem de Domínio Específico incorporada (eDSL) baseada em Rust e depois veremos como implantá-lo em uma rede de teste pública. Por fim, criaremos um frontend para o contrato inteligente em ReactJS.
Pré-requisitos
- Você deve estar familiarizado com Rust e ReactJS
- Será muito mais fácil seguir este tutorial se você tiver concluído o Guia do iniciante ink!
Requisitos
- Node.js v10.18.0 ou superior
- Extensão Polkadot{.js} no seu navegador
- Ink! - Configuração da versão 3
O que é um AMM?
O Formador de Mercado Automatizado (Automated Market Maker, AMM) é um tipo de exchange descentralizada baseada em uma fórmula matemática de preços de ativos. Ele permite que ativos digitais sejam negociados automaticamente sem nenhuma permissão, usando pools de liquidez em vez de compradores e vendedores tradicionais que utilizam um livro de ordens usado na bolsa tradicional. Aqui os ativos são precificados de acordo com um algoritmo de precificação.
Por exemplo, a Uniswap usa p * q = k, onde p é o valor de um token no pool de liquidez e q é o valor do outro. Aqui “k” é uma constante fixa, o que significa que a liquidez total do pool sempre deve permanecer a mesma. Para mais explicações, vamos dar um exemplo: se um AMM tiver a moeda A e a moeda B, que são dois ativos voláteis, toda vez que A é comprado, o preço de A sobe, pois há menos A no pool do que antes da compra. Por outro lado, o preço de B diminui à medida que há mais B no pool. O pool fica em equilíbrio constante, onde o valor total de A no pool será sempre igual ao valor total de B no pool. O tamanho aumentará apenas quando novos provedores de liquidez ingressarem no pool.
Implementando o contrato inteligente
Vá para o diretório onde deseja criar seu projeto ink!. Execute o seguinte comando no terminal que criará um modelo de projeto ink! para você.
cargo contract new amm
Vá para a pasta amm
e substitua o conteúdo do arquivo lib.rs
pelo seguinte código. Dividimos a implementação em 10 partes.
#![cfg_attr(not(feature = "std"), no_std)]
#![allow(non_snake_case)]
use ink_lang as ink;
const PRECISION: u128 = 1_000_000; // Precisão de 6 dígitos
#[ink::contract]
mod amm {
use ink_storage::collections::HashMap;
// Parte 1. Definir enumeração Error
// Parte 2. Definir struct de armazenamento
// Parte 3. Funções auxiliares
impl Amm {
// Parte 4. Construtor
// Parte 5. Faucet (Torneira)
// Parte 6. Leia o estado atual
// Parte 7. Fornecimento (Provide)
// Parte 8. Saque (Withdraw)
// Part 9. Troca (Swap)
}
// Part 10. Teste Unitário
}
Parte 1. Definir enumeração Error
A enumeração Error
conterá todos os valores de erro que nosso contrato gera. A Ink! requer que os valores retornados tenham certas características. Portanto, estamos derivando-os para nossa enumeração personalizada com o atributo #[derive(...)]
.
#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum Error {
// Liquidez Zero
ZeroLiquidity,
// Valor não pode ser zero!
ZeroAmount,
// Valor Insuficiente
InsufficientAmount,
// Valor de tokens equivalente não fornecido
NonEquivalentValue,
// Valor do ativo menor que o limite para contribuição
ThresholdNotReached,
// A participação deve ser menor que totalShare
InvalidShare,
// Saldo do pool insuficiente
InsufficientLiquidity,
// Tolerância de derrapagem excedida
SlippageExceeded,
}
Parte 2. Definir struct de armazenamento
Em seguida, definimos as variáveis de estado necessárias para operar o AMM. Vamos usar a mesma fórmula matemática usada pela Uniswap para determinar o preço dos ativos (K = totalToken1 * totalToken2). Para simplificar, estamos mantendo nosso próprio mapeamento de saldo interno (token1Balance e token2Balance) em vez de lidar com tokens externos. O HashMap
no Rust funciona de forma semelhante a um mapeamento na linguagem Solidity, armazenando um par chave-valor. Lembre-se também de que, no Rust, os dados devem ter um tipo associado - e é por isso que você verá o tipo Balance
(Saldo) associado ao número de ações.
#[derive(Default)]
#[ink(storage)]
pub struct Amm {
totalShares: Balance, // Armazena a quantidade total de ações emitidas para o pool
totalToken1: Balance, // Armazena a quantidade de Token1 bloqueada no pool
totalToken2: Balance, // Armazena a quantidade de Token2 bloqueada no pool
shares: HashMap<AccountId, Balance>, // Armazena a participação acionária de cada provedor
token1Balance: HashMap<AccountId, Balance>, // Armazena o saldo de token1 de cada usuário
token2Balance: HashMap<AccountId, Balance>, // Armazena o saldo de token2 de cada usuário
fees: Balance, // Porcentagem das taxas de negociação cobradas na negociação
}
Parte 3. Funções auxiliares
Vamos definir as funções privadas em um bloco de implementação separado para manter a estrutura do código limpa, e precisamos adicionar o atributo #[ink(impl)]
para deixar a ink! ciente disso. As funções a seguir serão utilizadas para verificar a validade dos parâmetros passados para as funções e restringir certas atividades quando o pool estiver vazio.
#[ink(impl)]
impl Amm {
// Garante que _qty seja diferente de zero e que o usuário tenha saldo suficiente
fn validAmountCheck(
&self,
_balance: &HashMap<AccountId, Balance>,
_qty: Balance,
) -> Result<(), Error> {
let caller = self.env().caller();
let my_balance = *_balance.get(&caller).unwrap_or(&0);
match _qty {
0 => Err(Error::ZeroAmount),
_ if _qty > my_balance => Err(Error::InsufficientAmount),
_ => Ok(()),
}
}
// Retorna a constante de liquidez do pool
fn getK(&self) -> Balance {
self.totalToken1 * self.totalToken2
}
// Usado para restringir o recurso de retirada e troca até que a liquidez seja adicionada no pool
fn activePool(&self) -> Result<(), Error> {
match self.getK() {
0 => Err(Error::ZeroLiquidity),
_ => Ok(()),
}
}
}
Parte 4. Construtor
Nosso construtor usa _fees
como um parâmetro que determina a porcentagem de taxas cobradas do usuário ao executar uma operação de troca. O valor de _fees
deve estar entre 0 e 1000 (exclusivo) para que qualquer operação de troca seja cobrada _fees/1000 por cento do valor depositado.
// Constrói uma nova instância AMM
// @param _fees: intervalo válido -> [0,1000)
#[ink(constructor)]
pub fn new(_fees: Balance) -> Self {
// Define as taxas como zero se não estiverem no intervalo válido
Self {
fees: if _fees >= 1000 { 0 } else { _fees },
..Default::default()
}
}
Parte 5. Faucet (Torneira)
Não estamos usando nenhum token externo para este tutorial. Em vez disso, estamos mantendo um registro do saldo. Precisamos de uma maneira de alocar tokens para novos usuários para que eles possam interagir com o dApp. Os usuários podem chamar essa função faucet para obter alguns tokens para brincar!
// Envia token(s) grátis para o invocador
#[ink(message)]
pub fn faucet(&mut self, _amountToken1: Balance, _amountToken2: Balance) {
let caller = self.env().caller();
let token1 = *self.token1Balance.get(&caller).unwrap_or(&0);
let token2 = *self.token2Balance.get(&caller).unwrap_or(&0);
self.token1Balance.insert(caller, token1 + _amountToken1);
self.token2Balance.insert(caller, token2 + _amountToken2);
}
Parte 6. Leia o estado atual
As funções a seguir são usadas para obter o estado atual do contrato inteligente.
// Retorna o saldo do usuário
#[ink(message)]
pub fn getMyHoldings(&self) -> (Balance, Balance, Balance) {
let caller = self.env().caller();
let token1 = *self.token1Balance.get(&caller).unwrap_or(&0);
let token2 = *self.token2Balance.get(&caller).unwrap_or(&0);
let myShares = *self.shares.get(&caller).unwrap_or(&0);
(token1, token2, myShares)
}
// Retorna a quantidade de tokens bloqueados no pool, total de ações emitidas e parâmetro de taxa de negociação
#[ink(message)]
pub fn getPoolDetails(&self) -> (Balance, Balance, Balance, Balance) {
(
self.totalToken1,
self.totalToken2,
self.totalShares,
self.fees,
)
}
Parte 7. Fornecimento (Provide)
A função provide
recebe dois parâmetros - a quantidade de token1 e a quantidade de token2 que o usuário deseja bloquear no pool. Se o pool estiver inicialmente vazio, a taxa de equivalência será definida como _amountToken1 : _amountToken2 e o usuário receberá 100 ações para isso. Caso contrário, verifica-se se os dois valores informados pelo usuário possuem valor equivalente ou não. Isso é feito verificando se os dois valores estão em proporção igual ao número total de seus respectivos tokens bloqueados no pool, ou seja, _amountToken1 : totalToken1 : : _amountToken2 : totalToken2.
// Adicionando nova liquidez no pool
// Retorna a quantidade de ações emitidas para bloquear determinados ativos
#[ink(message)]
pub fn provide(
&mut self,
_amountToken1: Balance,
_amountToken2: Balance,
) -> Result<Balance, Error> {
self.validAmountCheck(&self.token1Balance, _amountToken1)?;
self.validAmountCheck(&self.token2Balance, _amountToken2)?;
let share;
if self.totalShares == 0 {
// São emitidas 100 ações para a liquidez Gênesis
share = 100 * super::PRECISION;
} else {
let share1 = self.totalShares * _amountToken1 / self.totalToken1;
let share2 = self.totalShares * _amountToken2 / self.totalToken2;
if share1 != share2 {
return Err(Error::NonEquivalentValue);
}
share = share1;
}
if share == 0 {
return Err(Error::ThresholdNotReached);
}
let caller = self.env().caller();
let token1 = *self.token1Balance.get(&caller).unwrap();
let token2 = *self.token2Balance.get(&caller).unwrap();
self.token1Balance.insert(caller, token1 - _amountToken1);
self.token2Balance.insert(caller, token2 - _amountToken2);
self.totalToken1 += _amountToken1;
self.totalToken2 += _amountToken2;
self.totalShares += share;
self.shares
.entry(caller)
.and_modify(|val| *val += share)
.or_insert(share);
Ok(share)
}
Essas funções de estimativa ajudam o usuário a ter uma ideia da quantidade de um token que ele precisa bloquear para o valor de token fornecido. Aqui, novamente, usamos a proporção _amountToken1 : totalToken1 : : _amountToken2 : totalToken2 para determinar a quantidade de token1 necessária se desejarmos bloquear uma determinada quantidade de token2 e vice-versa.
// Retorna a quantidade de Token1 necessária ao fornecer liquidez com a quantidade de Token2 _amountToken2
#[ink(message)]
pub fn getEquivalentToken1Estimate(
&self,
_amountToken2: Balance,
) -> Result<Balance, Error> {
self.activePool()?;
Ok(self.totalToken1 * _amountToken2 / self.totalToken2)
}
// Retorna a quantidade de Token2 necessária ao fornecer liquidez com a quantidade de Token1 _amountToken1
#[ink(message)]
pub fn getEquivalentToken2Estimate(
&self,
_amountToken1: Balance,
) -> Result<Balance, Error> {
self.activePool()?;
Ok(self.totalToken2 * _amountToken1 / self.totalToken1)
}
Parte 8. Saque (Withdraw)
O saque é usado quando um usuário deseja queimar uma determinada quantidade de ações para recuperar seus tokens. Token1 e Token2 são liberados do pool proporcionalmente à ação queimada em relação ao total de ações emitidas, ou seja, share : totalShare : : amountTokenX : totalTokenX.
// Retorna a estimativa de Token1 e Token2 que serão liberados na queima de determinada ação (_share)
#[ink(message)]
pub fn getWithdrawEstimate(&self, _share: Balance) -> Result<(Balance, Balance), Error> {
self.activePool()?;
if _share > self.totalShares {
return Err(Error::InvalidShare);
}
let amountToken1 = _share * self.totalToken1 / self.totalShares;
let amountToken2 = _share * self.totalToken2 / self.totalShares;
Ok((amountToken1, amountToken2))
}
// Remove a liquidez do pool e libera Token1 e Token2 correspondentes ao sacador
#[ink(message)]
pub fn withdraw(&mut self, _share: Balance) -> Result<(Balance, Balance), Error> {
let caller = self.env().caller();
self.validAmountCheck(&self.shares, _share)?;
let (amountToken1, amountToken2) = self.getWithdrawEstimate(_share)?;
self.shares.entry(caller).and_modify(|val| *val -= _share);
self.totalShares -= _share;
self.totalToken1 -= amountToken1;
self.totalToken2 -= amountToken2;
self.token1Balance
.entry(caller)
.and_modify(|val| *val += amountToken1);
self.token2Balance
.entry(caller)
.and_modify(|val| *val += amountToken2);
Ok((amountToken1, amountToken2))
}
Parte 9. Troca (Swap)
Para trocar do Token1 para o Token2, implementaremos quatro funções - getSwapToken1EstimateGivenToken1
, getSwapToken1EstimateGivenToken2
, swapToken1GivenToken1
e swapToken1GivenToken2
. As duas primeiras funções determinam apenas os valores de troca para fins de estimativa, enquanto as duas últimas fazem a conversão real.
getSwapToken1EstimateGivenToken1
retorna a quantidade de token2 que o usuário receberá ao depositar uma determinada quantidade de token1. A quantidade de token2 é obtida a partir da equação K = totalToken1 * totalToken2 e K = (totalToken1 + delta * amountToken1) * (totalToken2 - amountToken2) onde delta é (1000 - taxas)/1000. Portanto, delta * amountToken1 é o token1Amount ajustado para o qual o amountToken2 resultante é calculado e o resto do token1Amount vai para o pool como taxas de negociação. Obtemos o valor amountToken2 resolvendo a equação acima.
// Retorna a quantidade de Token2 que o usuário receberá ao trocar uma determinada quantidade de Token1 por Token2
#[ink(message)]
pub fn getSwapToken1EstimateGivenToken1(
&self,
_amountToken1: Balance,
) -> Result<Balance, Error> {
self.activePool()?;
let _amountToken1 = (1000 - self.fees) * _amountToken1 / 1000; // Ajustando as taxas cobradas
let token1After = self.totalToken1 + _amountToken1;
let token2After = self.getK() / token1After;
let mut amountToken2 = self.totalToken2 - token2After;
// Para garantir que o pool do Token2 não seja completamente esgotado, levando à proporção inf:0
if amountToken2 == self.totalToken2 {
amountToken2 -= 1;
}
Ok(amountToken2)
}
getSwapToken1EstimateGivenToken2
retorna a quantidade de token1 que o usuário deve depositar para obter uma determinada quantidade de token2. A quantidade de token1 é obtida de forma semelhante, resolvendo a seguinte equação K = (totalToken1 + delta * amountToken1) * (totalToken2 - amountToken2) para amountToken1.
// Retorna a quantidade de Token1 que o usuário deve trocar para obter _amountToken2 em retorno
#[ink(message)]
pub fn getSwapToken1EstimateGivenToken2(
&self,
_amountToken2: Balance,
) -> Result<Balance, Error> {
self.activePool()?;
if _amountToken2 >= self.totalToken2 {
return Err(Error::InsufficientLiquidity);
}
let token2After = self.totalToken2 - _amountToken2;
let token1After = self.getK() / token2After;
let amountToken1 = (token1After - self.totalToken1) * 1000 / (1000 - self.fees);
Ok(amountToken1)
}
swapToken1GivenToken1
pega a quantidade de Token1 que precisa ser trocada por algum Token2. Para lidar com a derrapagem, inserimos o mínimo de Token2 que o usuário deseja para uma negociação bem-sucedida. Se o Token2 esperado for menor que o limite, a transação será revertida.
// Troca determinada quantidade de Token1 para Token2, usando determinação de preço algorítmica
// A troca falha se o valor do Token2 for menor que _minToken2
#[ink(message)]
pub fn swapToken1GivenToken1(
&mut self,
_amountToken1: Balance,
_minToken2: Balance,
) -> Result<Balance, Error> {
let caller = self.env().caller();
self.validAmountCheck(&self.token1Balance, _amountToken1)?;
let amountToken2 = self.getSwapToken1EstimateGivenToken1(_amountToken1)?;
if amountToken2 < _minToken2 {
return Err(Error::SlippageExceeded);
}
self.token1Balance
.entry(caller)
.and_modify(|val| *val -= _amountToken1);
self.totalToken1 += _amountToken1;
self.totalToken2 -= amountToken2;
self.token2Balance
.entry(caller)
.and_modify(|val| *val += amountToken2);
Ok(amountToken2)
}
swapToken1GivenToken2
pega a quantidade de Token2 que o usuário deseja receber e especifica a quantidade máxima de Token1 que está disposto a trocar por ele. Se a quantidade necessária de Token1 exceder o limite, a troca será cancelada.
// Troca determinada quantidade de Token1 para Token2 usando determinação de preço algorítmica
// A troca falha se a quantidade de Token1 necessária para obter _amountToken2 exceder _maxToken1
#[ink(message)]
pub fn swapToken1GivenToken2(
&mut self,
_amountToken2: Balance,
_maxToken1: Balance,
) -> Result<Balance, Error> {
let caller = self.env().caller();
let amountToken1 = self.getSwapToken1EstimateGivenToken2(_amountToken2)?;
if amountToken1 > _maxToken1 {
return Err(Error::SlippageExceeded);
}
self.validAmountCheck(&self.token1Balance, amountToken1)?;
self.token1Balance
.entry(caller)
.and_modify(|val| *val -= amountToken1);
self.totalToken1 += amountToken1;
self.totalToken2 -= _amountToken2;
self.token2Balance
.entry(caller)
.and_modify(|val| *val += _amountToken2);
Ok(amountToken1)
}
Da mesma forma, para trocar Token2 para Token1, precisamos implementar quatro funções - getSwapToken2EstimateGivenToken2
, getSwapToken2EstimateGivenToken1
, swapToken2GivenToken2
& swapToken2GivenToken1
. Isso é deixado como um exercício para você implementar :)
Parabéns! Foi bastante código para concluir a implementação do contrato inteligente. O código completo pode ser encontrado aqui.
Parte 10. Teste Unitário
Agora vamos escrever alguns testes unitários para garantir que nosso programa esteja funcionando conforme o esperado. O(s) módulo(s) marcado(s) com o atributo #[cfg(test)]
diz ao Rust para executar o código a seguir quando o comando cargo test
é executado. As funções de teste são marcadas com o atributo #[ink::test]
quando queremos que a ink! injete variáveis de ambiente, como caller
, durante a invocação do contrato.
#[cfg(test)]
mod tests {
use super::*;
use ink_lang as ink;
#[ink::test]
fn new_works() {
let contract = Amm::new(0);
assert_eq!(contract.getMyHoldings(), (0, 0, 0));
assert_eq!(contract.getPoolDetails(), (0, 0, 0, 0));
}
#[ink::test]
fn faucet_works() {
let mut contract = Amm::new(0);
contract.faucet(100, 200);
assert_eq!(contract.getMyHoldings(), (100, 200, 0));
}
#[ink::test]
fn zero_liquidity_test() {
let contract = Amm::new(0);
let res = contract.getEquivalentToken1Estimate(5);
assert_eq!(res, Err(Error::ZeroLiquidity));
}
#[ink::test]
fn provide_works() {
let mut contract = Amm::new(0);
contract.faucet(100, 200);
let share = contract.provide(10, 20).unwrap();
assert_eq!(share, 100_000_000);
assert_eq!(contract.getPoolDetails(), (10, 20, share, 0));
assert_eq!(contract.getMyHoldings(), (90, 180, share));
}
#[ink::test]
fn withdraw_works() {
let mut contract = Amm::new(0);
contract.faucet(100, 200);
let share = contract.provide(10, 20).unwrap();
assert_eq!(contract.withdraw(share / 5).unwrap(), (2, 4));
assert_eq!(contract.getMyHoldings(), (92, 184, 4 * share / 5));
assert_eq!(contract.getPoolDetails(), (8, 16, 4 * share / 5, 0));
}
#[ink::test]
fn swap_works() {
let mut contract = Amm::new(0);
contract.faucet(100, 200);
let share = contract.provide(50, 100).unwrap();
let amountToken2 = contract.swapToken1GivenToken1(50, 50).unwrap();
assert_eq!(amountToken2, 50);
assert_eq!(contract.getMyHoldings(), (0, 150, share));
assert_eq!(contract.getPoolDetails(), (100, 50, share, 0));
}
#[ink::test]
fn slippage_works() {
let mut contract = Amm::new(0);
contract.faucet(100, 200);
let share = contract.provide(50, 100).unwrap();
let amountToken2 = contract.swapToken1GivenToken1(50, 51);
assert_eq!(amountToken2, Err(Error::SlippageExceeded));
assert_eq!(contract.getMyHoldings(), (50, 100, share));
assert_eq!(contract.getPoolDetails(), (50, 100, share, 0));
}
#[ink::test]
fn trading_fees_works() {
let mut contract = Amm::new(100);
contract.faucet(100, 200);
contract.provide(50, 100).unwrap();
let amountToken2 = contract.getSwapToken1EstimateGivenToken1(50).unwrap();
assert_eq!(amountToken2, 48);
}
}
Do diretório do projeto ink! execute o seguinte comando no terminal para executar o módulo de testes:
cargo +nightly contract test
A seguir, aprenderemos como implantar o contrato em uma rede de teste pública.
Implantando o contrato inteligente
Vamos implantar nosso contrato inteligente ink! na rede de teste Júpiter A1 da Patract (Mais informações sobre a rede de teste Júpiter). Primeiro, precisamos construir nosso projeto ink! para obter os artefatos necessários. Do seu diretório do projeto ink!, execute o seguinte comando no terminal:
cargo +nightly contract build --release
Isso gerará os artefatos em ./target/ink
. Usaremos os arquivos amm.wasm
e metadata.json
para implantar nosso contrato inteligente. O arquivo amm.wasm
é o contrato inteligente compilado, no formato WebAssembly. metadata.json
é a ABI do nosso contrato e será necessária quando nos integrarmos com o frontend do nosso dApp.
Em seguida, precisamos adicionar fundos ao nosso endereço para interagir com a rede. Vá para a torneira para obter alguns tokens da rede de teste.
Agora visite https://polkadot.js.org/apps e mude para a rede de teste Júpiter. Você pode fazer isso clicando no logotipo da rede disponível no canto superior esquerdo da barra de navegação, onde verá uma lista de redes disponíveis. Vá para a seção "TEST NETWORKS" e procure por uma rede chamada Jupiter. Selecione-a, role de volta ao topo e clique em “Switch” (Alternar).
Depois de alternar a rede, clique na opção “Contracts” na guia “Developer” na barra de navegação. Lá, clique em “Upload & Deploy Code” e selecione a conta através da qual você deseja implantar e no campo - "json for ABI or .contract bundle" carregue o arquivo metadata.json
. Em seguida, um novo campo - "compiled contract WASM" aparecerá onde você precisa carregar seu arquivo wasm, ou seja, amm.wasm
em nosso caso. Vai se parecer com algo assim -
Agora clique em “Next”. Como temos apenas um construtor em nosso contrato, ele será escolhido por padrão, caso contrário, um menu suspenso estaria presente para selecionar entre vários construtores. Como nosso construtor new()
aceita um parâmetro chamado fees
, precisamos definir o campo de taxas (fees) com um número positivo.
Observe que a unidade padrão é definida como DOT, que multiplica a entrada por um fator de 10^4. Portanto, se desejarmos passar um valor, digamos 10 (que corresponde a 1% de taxa de negociação, fração 10/1000, em nosso contrato), precisamos escrever 0,0001 DOT.
Defina a dotação para 1 DOT, que transfere 1 DOT para o contrato para aluguel de armazenamento. Por fim, defina o gás máximo permitido (M) para 200.000. Ficará mais ou menos assim -
Clique em “Deploy” (implantar) seguido de “Sign and Submit” (Assinar e Enviar). Aguarde até que a transação seja minerada e, após alguns segundos, você poderá ver a página de contrato atualizada com a lista de seus contratos implantados. Clique no nome do seu contrato para ver o endereço do contrato e anote-o, pois será necessário na integração com o frontend.
Como interagir com polkadot.{js}
Nesta seção, veremos como interagir com nosso contrato inteligente usando polkadot.{js}. Instale os pacotes necessários
npm install @polkadot/api @polkadot/api-contract @polkadot/extension-dapp
Vamos ver o seguinte bloco de código e entender como ele funciona
// Importações
import { ApiPromise, WsProvider } from "@polkadot/api";
import {
web3Accounts,
web3Enable,
web3FromSource,
} from "@polkadot/extension-dapp";
import { ContractPromise } from "@polkadot/api-contract";
// Armazene o ABI do seu contrato
const CONTRACT_ABI = { ... };
// Armazene o Endereço do contrato
const CONTRACT_ADDRESS = "5EyPH...gXA9g5";
// Crie uma nova instância de contrato
const wsProvider = new WsProvider("ws://127.0.0.1:9944");
const api = await ApiPromise.create({ provider: wsProvider });
const contract = new ContractPromise(api, CONTRACT_ABI, CONTRACT_ADDRESS);
// Obtenha contas disponíveis no Polkadot.{js}
const extensions = await web3Enable("local canvas");
const allAccounts = await web3Accounts();
const selectedAccount = allAccounts[0];
// Crie um signatário
const accountSigner = await web3FromSource(selectedAccount.meta.source).then(
(res) => res.signer
);
// Busque ativos da conta e exiba detalhes (faz uma consulta)
const getAccountHoldings = async () => {
let holdings = await contract.query
.getMyHoldings(selectedAccount.address, { value: 0, gasLimit: -1 })
.then((res) => {
if (!res?.result?.toHuman()?.Err) return res.output.toHuman();
});
console.log("Ativos da conta ", holdings);
};
// Financie a conta com determinado valor (faz uma transação)
const faucet = async (amountKAR, amountKOTHI) => {
await contract.tx
.faucet({ value: 0, gasLimit: -1 }, amountKAR, amountKOTHI)
.signAndSend(
selectedAccount.address,
{ signer: accountSigner },
(res) => {
if (res.status.isFinalized) {
getAccountHoldings();
}
}
);
}
No código javascript acima, demonstramos como fazer uma consulta e transação para nosso contrato inteligente AMM. Agora vamos entender cada linha do código acima.
O código acima é apenas uma referência de como interagir com o contrato inteligente.
// Cria um provedor
const wsProvider = new WsProvider("ws://127.0.0.1:9944");
// Cria uma instância de API
const api = await ApiPromise.create({ provider: wsProvider });
// Anexa a um contrato existente com uma ABI e endereço conhecidos.
const contract = new ContractPromise(api, CONTRACT_ABI, CONTRACT_ADDRESS);
Para interagir com o contrato inteligente, precisamos criar uma instância do contrato. Para isso, primeiro precisamos criar uma instância de API e qualquer API requer um provedor e, no trecho acima, criamos um com WsProvider. Em seguida, a criação da API é feita por meio da interface ApiPromise.create. Se um provedor não for passado para o ApiPromise.create, ele construirá uma instância WsProvider padrão para se conectar a ws://127.0.0.1:9944
.
Por fim, interagimos com o contrato implantado criando uma nova instância com a ajuda da interface ContractPromise, que nos permite gerenciar contratos dentro da cadeia, fazer chamadas de leitura e executar transações em contratos.
// Recupera a lista de todas as extensões/provedores injetados
const extensions = await web3Enable("demo");
// Retorna uma lista de todas as contas injetadas, em todas as extensões
const allAccounts = await web3Accounts();
// Selecionamos a primeira conta na lista
const selectedAccount = allAccounts[0];
Agora precisamos selecionar uma conta para trabalhar com ela. Podemos obter a lista de todas as extensões injetadas com web3Enable. Para obter a lista de todas as contas disponíveis, usamos a ajuda de web3Accounts. No trecho acima, armazenamos a primeira conta em uma variável selectedAccount.
// Recupera a interface do signatário desta conta
// web3FromSource retorna um tipo InjectedExtension
const accountSigner = await web3FromSource(selectedAccount.meta.source).then(
(res) => res.signer
);
Para fazer uma transação, precisamos recuperar o signatário da conta. Agora estamos prontos para interagir com o contrato inteligente.
// Buscar ativos da conta e exibir detalhes (faz uma consulta)
const getAccountHoldings = async () => {
let holdings = await contract.query
.getMyHoldings(selectedAccount.address, { value: 0, gasLimit: -1 })
.then((res) => {
if (!res?.result?.toHuman()?.Err) return res.output.toHuman();
});
console.log("Ativos da Conta ", holdings);
};
Criamos uma função getAccountHoldings, que faz uma consulta ao método getMyHoldings do nosso contrato inteligente. Passamos o endereço da conta como primeiro parâmetro, o segundo parâmetro é um objeto com duas chaves, value é útil apenas em mensagens isPayable e gasLimit define o gás máximo que nossa consulta pode levar. Definimos gasLimit como -1, o que indica que o limite é ilimitado e pode usar o máximo disponível.
// Adicione fundos na conta com determinado valor (faz uma transação)
const faucet = async (amountKAR, amountKOTHI) => {
await contract.tx
.faucet({ value: 0, gasLimit: -1 }, amountKAR, amountKOTHI)
.signAndSend(
selectedAccount.address,
{ signer: accountSigner },
(res) => {
if (res.status.isFinalized) {
getAccountHoldings();
}
}
);
}
A função faucet faz uma transação para o método faucet do nosso contrato inteligente. Passamos o objeto com as chaves value e gasLimit como primeiro parâmetro e depois passamos os outros argumentos necessários para o método faucet, o amountKAR e o amountKOTHI. Em seguida, assinamos e enviamos a transação usando o método signAndSend. Para este método, passamos o endereço da conta como primeiro parâmetro, um objeto contendo o signatário como segundo parâmetro e uma função de retorno de chamada que chama a função getAccountHoldings quando a transação é finalizada.
Criando um frontend em React
Agora, vamos criar um aplicativo React e configurar o frontend do aplicativo. No frontend, representamos token1 e token2 como KAR e KOTHI, respectivamente.
Abra um terminal e navegue até o diretório onde criaremos o aplicativo.
cd /path/to/directory
Agora clone o repositório do GitHub, vá para o novo diretório polkadot-amm
e instale todas as dependências.
git clone https://github.com/realnimish/polkadot-amm.git
cd polkadot-amm
npm install
Em nosso aplicativo React, mantemos todos os componentes do React no diretório src/components
.
- BoxTemplate :
Ele renderiza a caixa contendo o campo de entrada, seu cabeçalho e o elemento à direita da caixa, que pode ser um saldo de conta, nome de token, um botão ou vazio.
- FaucetComponent :
Pega a quantidade de token1 (KAR) e token2 (KOTHI) como entrada e financia o endereço do usuário com essa quantia.
- ProvideComponent :
Pega a quantidade de um token (KAR ou KOTHI), preenche o valor estimado do outro token e ajuda a fornecer liquidez ao pool.
- SwapComponent :
Ajuda a trocar um token por outro. Ele pega a quantidade de token no campo de entrada _From _(De) e estima a quantidade de token no campo de entrada _To _(Para) e vice-versa, e também ajuda a definir a tolerância de derrapagem durante a troca.
- WithdrawComponent :
Ajuda a sacar a parte que se tem. Também permite que se saque ao seu limite máximo.
- Account :
Mostra os detalhes do pool e os detalhes da conta. Ele permite alternar entre contas no aplicativo.
- ContainerComponent :
Este componente renderiza o corpo principal do nosso aplicativo, que contém a caixa central, as guias para alternar entre os cinco componentes Swap, Provide, Faucet, Withdraw, Account.
O App.js
renderiza o ContainerComponent
e conecta o aplicativo ao polkadot.{js}
.
O arquivo constants.js
armazena a ABI do contrato e CONTRACT_ADDRESS. Não se esqueça de armazenar seu endereço de contrato e ABI nas respectivas variáveis.
A ABI pode ser obtida de sua pasta do projeto ink! em
/target/ink/metadata.json
Agora é hora de executar nosso aplicativo React. Use o seguinte comando para iniciar o aplicativo React.
npm start
Passo a passo
Conclusão
Parabéns! Desenvolvemos com sucesso um modelo de AMM, em que os usuários podem trocar tokens, fornecer e retirar liquidez. Como próximo passo, você pode brincar com a fórmula de preço, integrar o padrão ERC20 e muito mais...
Solução de problemas
A conta não aparece
Verifique se você adicionou a conta na extensão polkadot{.js} e se a visibilidade da conta está definida como "Allow use on any chain" (Permitir o uso em qualquer rede) ou "Jupiter A1". Você pode encontrar isso abrindo a extensão polkadot{.js} e clicando no menu hambúrguer da conta correspondente.
O projeto Ink! não está compilando
O projeto Ink! está em atual desenvolvimento. Por esse motivo nossa implementação pode se tornar incompatível com versões futuras. Nesse caso, você pode tentar modificar o contrato ou mudar para ink v3.0.0-rc7.
Estrutura fornecida JSON da ABI inválida. Esperava-se uma versão de metadados recente
Tente compilar o contrato ink! usando a versão mais recente. Se o contrato não for compilado na versão mais recente, tente implantar em um nó local.
Erro ExtrinsicFailed ao implantar o contrato
Certifique-se de fornecer uma quantidade de gás suficiente para a transação e de ter criado o contrato com o sinalizador --release
, conforme mencionado na seção de implantação.
Sobre os autores
O tutorial foi criado por Sayan Kar e Nimish Agrawal. Você pode contatá-los no Fórum da Figment para qualquer consulta sobre o tutorial. Traduzido por Paulinho Giovannini.
Referências
Como construir um AMM na Avalanche
Top comments (0)