WEB3DEV

Cover image for Criar Contrato Inteligente de Crowdfunding usando Solidity e Linguagem Golang
Adriano P. Araujo
Adriano P. Araujo

Posted on

Criar Contrato Inteligente de Crowdfunding usando Solidity e Linguagem Golang

Queremos criar contratos inteligentes para resolver um problema real no mundo real. Imagine um grupo de pessoas que deseja contribuir com dinheiro para um projeto específico. E quando arrecadamos o dinheiro, o proprietário deseja gastar esse dinheiro com um fornecedor específico. Além disso, é necessário votar e selecionar um fornecedor pelos contribuintes. Queremos garantir que tudo seja executado sem trapaça. A melhor solução é usar contratos inteligentes para manter tudo imutável.

Isso também pode ser usado em muitos tópicos diferentes, como arrecadação de impostos, gerenciamento de construção...

https://hackmamba.io/blog/2022/05/create-a-crowdfunding-smart-contract-using-solidity/?source=post_page-----bd5dc7c8f84f--------------------------------

 As tentativas de solicitação de gastos retiram dinheiro do contrato e o enviam para um endereço externo.

Documento Struct em Solidity

https://docs.soliditylang.org/en/v0.8.6/types.html?highlight=struct&source=post_page-----bd5dc7c8f84f--------------------------------#structs

Remoção de Recursos Não Utilizados ou Inseguros

Mapeamentos fora do Armazenamento

  • Se uma struct ou array contiver um mapeamento, ele só pode ser usado no armazenamento. Anteriormente, os membros do mapeamento eram silenciosamente ignorados na memória, o que é confuso e propenso a erros.

  • As atribuições a structs ou arrays no armazenamento não funcionam se contiverem mapeamentos. Anteriormente, os mapeamentos eram silenciosamente ignorados durante a operação de cópia, o que é enganoso e propenso a erros.

https://docs.soliditylang.org/en/v0.7.0/070-breaking-changes.html?source=post_page-----bd5dc7c8f84f--------------------------------#mappings-outside-storage

Armazenamento vs. Memória em Solidity

As palavras-chave Storage e Memory em Solidity são análogas ao disco rígido e à RAM do computador. Assim como a RAM, a Memory em Solidity é um local temporário para armazenar dados, enquanto o Storage mantém dados entre chamadas de função. O Contrato Inteligente em Solidity pode usar qualquer quantidade de memória durante a execução, mas uma vez que a execução para, a Memória é completamente apagada para a próxima execução. Enquanto o Storage, por outro lado, é persistente, cada execução do contrato inteligente tem acesso aos dados previamente armazenados na área de armazenamento.

Cada transação na Máquina Virtual Ethereum nos custa uma certa quantidade de Gás. Quanto menor o consumo de Gás, melhor é o seu código Solidity. O consumo de Gás da Memória não é muito significativo em comparação com o consumo de Gás do Armazenamento. Portanto, é sempre melhor usar a Memória para cálculos intermediários e armazenar o resultado final no Armazenamento.

  1. As variáveis de estado e as variáveis locais de structs e arrays são sempre armazenadas no armazenamento por padrão.

  2. Os argumentos de função estão na memória.

  3. Sempre que uma nova instância de um array é criada usando a palavra-chave 'memory', uma nova cópia dessa variável é criada. Alterar o valor do array da nova instância não afeta o array original.

https://www.geeksforgeeks.org/storage-vs-memory-in-solidity/?source=post_page-----bd5dc7c8f84f--------------------------------

A chave do mapeamento não é armazenada no contrato inteligente, apenas temos um valor no contrato inteligente.

Condição de Corrida em Contrato Inteligente

Todas as transações no Ethereum são executadas serialmente. Uma após a outra. Tudo o que sua transação faz, incluindo chamadas de um contrato para outro, acontece dentro do contexto da sua transação e nada mais é executado até que seu contrato esteja concluído.

Portanto, condições de corrida não são absolutamente uma preocupação. Você pode chamar balanceOf() em outro contrato, colocar o resultado em uma variável local e usá-lo sem se preocupa,que o saldo no outro contrato mudará antes de você terminar.

https://ethereum.stackexchange.com/questions/25870/race-conditions-when-calling-remote-contracts?source=post_page-----bd5dc7c8f84f--------------------------------

Existem algumas maneiras diferentes de evitar condições de corrida em contratos inteligentes. Uma abordagem comum é usar bloqueios, conforme descrito acima. Outra abordagem é usar um conceito chamado carimbo de data e hora. O carimbo de data e hora envolve atribuir um timestamping (carimbo de data e hora) a cada transação. As transações são então processadas na ordem de seus carimbos de data e hora, o que ajuda a evitar conflitos.

https://medium.com/coinmonks/race-conditions-and-front-running-vulnerabilities-in-smart-contracts-a-comprehensive-overview-5ea67cfd9d01?source=post_page-----bd5dc7c8f84f--------------------------------

Em geral, é importante estar ciente do potencial de condições de corrida ao escrever contratos inteligentes. Existem várias técnicas que podem ser usadas para evitar condições de corrida, mas é importante escolher a técnica apropriada para a aplicação específica.

_Preste atenção ao usar iteração em contratos inteligentes, isso requer mais gás para ser executado.

Quando lançamos uma nova implantação de contratos inteligentes, eles têm um código de hash diferente e não estão relacionados entre si._

Todo projeto web3 segue estes passos:

  1. Escrever em Solidity e verificar com https://remix.ethereum.org

  2. Compilar o projeto Solidity em JSON e ABI

  3. Implantar o arquivo compilado em uma rede

  4. Escrever testes para verificar a funcionalidade

Vamos fazer isso

Temos um gerente e uma lista de contribuintes, e um valor mínimo de contribuição.


// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity >=0.5.0 <0.9.0;

// Avisos do linter (sublinhado vermelho) sobre a versão da pragma podem ser ignorados!



// O código do contrato irá aqui



contract Campaing {



    address public manager;

    uint256 public minimumContribution;

    mapping (address => bool ) public approvers;



    constructor(uint256 min ) {

        manager = msg.sender;

        minimumContribution = min;

    }

}



Enter fullscreen mode Exit fullscreen mode

Adicionar contribuição com valor mínimo na lista de contribuições


    function contibute() public payable {

        require(

            msg.value >= minimumContribution,

            "minimum contribute is required."

        );



        approvers[msg.sender] = true;

        approversCount ++;



    }



Enter fullscreen mode Exit fullscreen mode

Se o valor for inferior ao mínimo, obtemos um erro e reduzimos o gás.

Podemos verificar se o endereço contribuiu ou não:



 function isPaied() public view returns (bool) {

        return (approvers[msg.sender]);

    }



Enter fullscreen mode Exit fullscreen mode

Obter saldo da conta do contrato inteligente


  function getBalance() public view returns(uint256){

        return (address(this).balance);

    }



Enter fullscreen mode Exit fullscreen mode

Agora, foi adicionada uma lista de solicitações para cada provedor. Os provedores são os destinatários que o gerente deseja gastar dinheiro dos contribuidores.


 struct Request {

        string description;

        uint256 value;

        address payable recipient;

        bool complete;

        uint256 approvalCount;

        mapping(address => bool) approvals;

    }



Enter fullscreen mode Exit fullscreen mode

O gerente só pode chamar essas funções, então precisamos criar um modificador para elas.


 modifier onlyManager() {

        require(

            msg.sender == manager,

            "Only the campaign manager can call this function."

        );

        _;

    }



Enter fullscreen mode Exit fullscreen mode

Add request é uma função que o gerente pode fazer para criar um novo provedor para gastar os contribuidores.


   function addRequest(

        string memory description,

        uint256 value,

        address payable  recipient

    ) public onlyManager {



        Request storage req = requests[numRequests++];

        req.description = description;

        req.value = value;

        req.recipient = recipient;

        req.complete = false;

        req.approvalCount = 0;

    }



Enter fullscreen mode Exit fullscreen mode



   function approveRequest(uint256 index) public{



        Request storage req = requests[index];

        require(

            approvers[msg.sender] ,

            "Only contributors can approve a specific payment request"

        );



        require(

            !req.approvals[msg.sender],

            "You have already voted to approve this request"

        );



        require(index < numRequests,

        "You can vote only in valid index. more than valid value");



        require(index >= 0,

        "You can vote only in valid index. minus value");



        req.approvals[msg.sender] = true;

        req.approvalCount++;

    }



Enter fullscreen mode Exit fullscreen mode

"water",1000,0xdD870fA1b7C4700F2BD7f44238821C26f7392148



Enter fullscreen mode Exit fullscreen mode

O teste desta parte requer três contas diferentes: gerente, contribuidor, provedor

  1. Gerente: implantar o contrato

  2. Contribuidor: contribuir dentro do sistema

  3. Gerente: criar uma solicitação para o provedor

  4. Contribuidor: aprovar a solicitação

O gerente pode finalizar a solicitação e enviar dinheiro ao provedor


    function finalizeRequest(uint256 index) public onlyManager {

        require(index < numRequests,

        "You can vote only in valid index. more than valid");



        require(index >= 0,

        "You can vote only in valid index. minus value");



        Request storage req = requests[index];

        require(!req.complete ,

        "This request has been completed.");



        req.recipient.transfer(req.value);

        req.complete = true;

    }



Enter fullscreen mode Exit fullscreen mode

Também precisamos verificar o número de votos que foram enviados para a solicitação:




        require(

            req.approvalCount > (approversCount / 2),

            "This request needs more approvals before it can be finalized"

        );



Enter fullscreen mode Exit fullscreen mode

O código final está disponível no meu repositório:

[https://github.com/mobintmu/kickstart/?source=post_page-----bd5dc7c8f84f--------------------------------

](https://github.com/mobintmu/kickstart/?source=post_page-----bd5dc7c8f84f--------------------------------)## Implantar código Solidity com Go Ethereum:

Conectar-se à rede

https://github.com/mobintmu/kickstart/blob/main/client/client.go?source=post_page-----bd5dc7c8f84f--------------------------------


type Client struct {

 Client *ethclient.Client

}



func (c *Client) NewClient(address string) {



 client, err := ethclient.Dial(address)

 if err != nil {

  log.Fatal(err)

 }

 fmt.Println("connect to network ...")

 c.Client = client

}

Enter fullscreen mode Exit fullscreen mode

Usando Ganache

https://github.com/trufflesuite/ganache?source=post_page-----bd5dc7c8f84f--------------------------------

ganache-cli

Enter fullscreen mode Exit fullscreen mode

Definição de Contas

Coloque três contas no arquivo de ambiente (env).


ADDRESS = http://localhost:8545

MANAGER_ADDRESS = 0xBad21545CDAc5eA56050a5441E435B2437fE4770

MANAGER_PRIVATE_ADDRESS = 0xbcab03c0fb40ccdd5d75b8024731dd772113b02d01132d81c584027d8a7280b9

PROVIDER_ADDRESS = 0xEc89A6a668206286a875BB906B79BbB756f09E22

PROVIDER_PRIVATE_ADDRESS = 0xbf6e3ba07b0ea7a98e5cbd0f0206bcdb35fd9520571c28cc16c803cae702dc89

CONTRIBUTOR_ADDRESS = 0x6BC477cE822a30331D0D5Ce4B5CeA23b2f2eEaEb

CONTRIBUTOR_PRIVATE_ADDRESS = 0x2b7b6e43aca333d79375eb8cb3d225b795480596d4fff6e0abf70c111628a410

Enter fullscreen mode Exit fullscreen mode

Obtendo o saldo das contas

type Account struct {

 PublicAddress  common.Address

 PrivateAddress string

 Name           string

 client         *ethclient.Client

}



// NewAccount cria uma nova instância de Account

func NewAccount(publicAddress string,

 privateAddress string,

 client *client.Client) *Account {

 return &Account{

  PublicAddress: common.HexToAddress(publicAddress),

  client:        client.Client,

 }

}



func (a *Account) GetBalance() *big.Int {



 balance, err := a.client.BalanceAt(context.Background(), a.PublicAddress, nil)

 if err != nil {

  log.Fatal(err)

 }



 return balance



}

Enter fullscreen mode Exit fullscreen mode

Resultado:

go run *.go



connect to network ...

balance manager : 1000000000000000000000

balance provider : 1000000000000000000000

contributor manager : 1000000000000000000000

Enter fullscreen mode Exit fullscreen mode

Instalação do Abigen:


$ cd $GOPATH/src/github.com/ethereum/go-ethereum

$ go build ./cmd/abigen

Enter fullscreen mode Exit fullscreen mode

build.sol


cd contracts/

solc --abi Campaign.sol -o build

solc --bin Campaign.sol -o Campaign.bin

Enter fullscreen mode Exit fullscreen mode

Então, o abigen pode ser executado novamente, desta vez passando o Campaign.bin:


abigen --abi ./build/Campaign.abi --pkg main --type Campaign --out Campaign.go --bin ./Campaign.bin/Campaign.bin



Enter fullscreen mode Exit fullscreen mode

Se o código não for implantado na rede:

ou se ocorrer o seguinte erro:

vm exception while processing transaction: invalid opcode error

invalid opcode error in ganache 2.7.1



Enter fullscreen mode Exit fullscreen mode
pip3 install solc-select



Enter fullscreen mode Exit fullscreen mode

https://github.com/crytic/solc-select?source=post_page-----bd5dc7c8f84f--------------------------------


solc-select use <version> --always-install

Enter fullscreen mode Exit fullscreen mode



solc --version

solc, the solidity compiler commandline interface

Version: 0.8.7+commit.e28d00a7.Linux.g++

Enter fullscreen mode Exit fullscreen mode

Implantação na rede com a conta de gerenciamento:

Primeiro, precisamos obter a chave privada da conta.


 privateKey, err := crypto.HexToECDSA(strings.TrimPrefix(privateAddress, "0x"))

 if err != nil {

  log.Fatal(err)

 }

Enter fullscreen mode Exit fullscreen mode

// HexToECDSA analisa uma chave privada secp256k1.

Função de implantação


type Campaign struct {

 instance  *contracts.Campaign

 txAddress common.Address

 tx        *types.Transaction

 Min       *big.Int

 client    *ethclient.Client

}



func NewCampaign(min *big.Int, client *ethclient.Client) *Campaign {

 return &Campaign{

  Min:    min,

  client: client,

 }

}



func (c *Campaign) Deploy(manager *account.Account) {

 nonce := manager.GetNonce()



 gasPrice, err := c.client.SuggestGasPrice(context.Background())

 if err != nil {

  log.Fatal(err)

 }



 chainID, err := c.client.ChainID(context.Background())

 if err != nil {

  log.Fatal(err)

 }



 auth, err := bind.NewKeyedTransactorWithChainID(manager.PrivateKey, chainID)

 if err != nil {

  fmt.Println(err)

 }

 auth.Nonce = big.NewInt(int64(nonce))

 auth.Value = big.NewInt(0)     // in wei

 auth.GasLimit = uint64(300000) // in units

 auth.GasPrice = gasPrice



 txAddress, tx, instance, err := contracts.DeployCampaign(auth, c.client, c.Min)

 if err != nil {

  fmt.Println("DeployLottery")

  log.Fatal(err.Error())

 }



 c.instance = instance

 c.txAddress = txAddress

 c.tx = tx



 fmt.Println(txAddress.Hex())

 fmt.Println(tx.Hash().Hex())



}

Enter fullscreen mode Exit fullscreen mode

Execução




go run *.go



connect to network ...

balance manager : 999999400000000000000

balance provider : 1000000000000000000000

contributor manager : 1000000000000000000000

nonce manager :  1

deploying contract  ... _____________________

0x8195b8a7757977AA6ae6Df873dA452a65Ab4791F

0x1e75f1dfa1a7454599acce94e4e1ccfeaa4d9633e0e11ec75cc896c0981e77bb

balance manager : 999998800000000000000

Enter fullscreen mode Exit fullscreen mode

Terminal


  Transaction: 0x1e75f1dfa1a7454599acce94e4e1ccfeaa4d9633e0e11ec75cc896c0981e77bb

  Contract created: 0x8195b8a7757977aa6ae6df873da452a65ab4791f

  Gas usage: 300000

  Block number: 2

  Block time: Wed Dec 27 2023 16:40:48 GMT

  Runtime error: code size to deposit exceeds maximum code size

Enter fullscreen mode Exit fullscreen mode

Agora, implantamos nosso contrato na rede Ganache.

Obter informações do bloco


func (a *Account) GetHeader() *big.Int {



 header, err := a.client.HeaderByNumber(context.Background(), nil)

 if err != nil {

  log.Fatal(err)

 }



 fmt.Println(header.Number.String()) // 5671744



 return header.Number

}



func (a *Account) GetDataFromBlock(blockNumber *big.Int) {

 block, err := a.client.BlockByNumber(context.Background(), blockNumber)

 if err != nil {

  log.Fatal(err)

 }



 fmt.Println("block.Number().Uint64() ", block.Number().Uint64())        // 5671744

 fmt.Println("block.Time()", block.Time())                               // 1527211625

 fmt.Println("block.Difficulty().Uint64()", block.Difficulty().Uint64()) // 3217000136609065

 fmt.Println("block.Hash().Hex()", block.Hash().Hex())                   // 0x9e8751ebb5069389b855bba72d94902cc385042661498a415979b7b6ee9ba4b9

 fmt.Println("len(block.Transactions())", len(block.Transactions()))     // 144

}



Enter fullscreen mode Exit fullscreen mode

Uma vez implantado, use apenas a transação do contrato.

my := common.HexToAddress("0xdd3f3c8afb70786e3bfb7a8cd8d44a3c5049268e")



 var err error

 campaign.Instance, err = contracts.NewCampaign(my, client.Client)

 if err != nil {

  fmt.Println(err)

 }

Enter fullscreen mode Exit fullscreen mode

Contribua na rede.


func (c *Campaign) Contribute(contributor *account.Account) {



 nonce := contributor.GetNonce()



 gasPrice, err := c.client.SuggestGasPrice(context.Background())

 if err != nil {

  log.Fatal(err)

 }



 chainID, err := c.client.ChainID(context.Background())

 if err != nil {

  log.Fatal(err)

 }



 auth, err := bind.NewKeyedTransactorWithChainID(contributor.PrivateKey, chainID)

 if err != nil {

  log.Fatal(err)

 }

 auth.Nonce = big.NewInt(int64(nonce))

 auth.Value = c.Min              // in wei

 auth.GasLimit = uint64(3000000) // in units

 auth.GasPrice = gasPrice



 tx, err := c.instance.Contribute(auth)

 if err != nil {

  log.Fatal(err)

 }



 fmt.Println("transaction hash")

 fmt.Println(tx.Hash())



}

Enter fullscreen mode Exit fullscreen mode



go run *.go




connect to network ...



block.Number().Uint64()  21

block.Time() 1703691148

block.Difficulty().Uint64() 0

block.Hash().Hex() 0x06c93b2954bed6bd1f76340b4b504c985293abbff6b21006dc359daf0d65198c

len(block.Transactions()) 1



>>>>>>>>>>>>>>>>>>>>>>Balance>>>>>>>>>>>>>>>>

balance manager : 999992200000000000000

balance provider : 1000000000000000000000

balance contributor : 999999662975999994800

<<<<<<<<<<<<<<<<<<<<<<Balance<<<<<<<<<<<<<<<<<



deploying contract  ... 

0xaf9087a308a23E335Dd8bB73E5b77AD0bF625Cd0

0xd56b10c190f65cec3be4f437f22da9fce4f3149d770b65fb494ca5f79af70d48




>>>>>>>>>>>>>>>>>>>>>>Balance>>>>>>>>>>>>>>>>

balance manager : 999991600000000000000

balance provider : 1000000000000000000000

balance contributor : 999999662975999994800

<<<<<<<<<<<<<<<<<<<<<<Balance<<<<<<<<<<<<<<<<<



contribute ...

transaction hash ...

0xfc2028dbaa4235e4a19a0b2e437db48b8cdbb6e05f0bebdd93eda4209e0541c6




>>>>>>>>>>>>>>>>>>>>>>Balance>>>>>>>>>>>>>>>>

balance manager : 999991600000000000000

balance provider : 1000000000000000000000

balance contributor : 999999620847999993800

<<<<<<<<<<<<<<<<<<<<<<Balance<<<<<<<<<<<<<<<<<

Enter fullscreen mode Exit fullscreen mode

Resultado no console:

implementação:


Transaction: 0xd56b10c190f65cec3be4f437f22da9fce4f3149d770b65fb494ca5f79af70d48

  Contract created: 0xaf9087a308a23e335dd8bb73e5b77ad0bf625cd0

  Gas usage: 300000

  Block number: 22

Enter fullscreen mode Exit fullscreen mode

contribuição:


  Transaction: 0xfc2028dbaa4235e4a19a0b2e437db48b8cdbb6e05f0bebdd93eda4209e0541c6

  Gas usage: 21064

  Block number: 23

Enter fullscreen mode Exit fullscreen mode

Chame a função IsPaid.


func (c *Campaign) IsPaid(contributor *account.Account) bool {



 result, err := c.Instance.IsPaid(&bind.CallOpts{

  From:    contributor.PublicAddress,

  Context: context.Background(),

 })

 if err != nil {

  fmt.Println("IsPaid")

  fmt.Println(err)

 }



 fmt.Println(result)



 return result

}



Enter fullscreen mode Exit fullscreen mode

Chame a função GetBalance.


func (c *Campaign) GetBalance() *big.Int {

 result, err := c.Instance.GetBalance(nil)

 if err != nil {

  fmt.Println("IsPaid")

  fmt.Println(err)

 }



 fmt.Println(result)



 return result

}

Enter fullscreen mode Exit fullscreen mode

Adicionando a requisição




func (c *Campaign) AddRequest(request entity.Request, manager *account.Account) {

 nonce := manager.GetNonce()



 gasPrice, err := c.client.SuggestGasPrice(context.Background())

 if err != nil {

  log.Fatal(err)

 }



 auth, err := bind.NewKeyedTransactorWithChainID(manager.PrivateKey, c.chainID)

 if err != nil {

  log.Fatal(err)

 }

 auth.Nonce = big.NewInt(int64(nonce))

 auth.Value = big.NewInt(0)      // em wei

 auth.GasLimit = uint64(6721975) // em units 6721975

 auth.GasPrice = gasPrice



 tx, err := c.Instance.AddRequest(auth, request.Description, request.Value, request.ProviderAddress)

 if err != nil {

  fmt.Println("CreateRequest")

  fmt.Println(err.Error())

  log.Fatal(err)

 }



 fmt.Println("transaction hash ...")

 fmt.Println(tx.Hash())



}

Enter fullscreen mode Exit fullscreen mode

Função para obter o número de requisições


 




func (c *Campaign) GetNumberOfRequests() *big.Int {

 result, err := c.Instance.NumRequests(nil)

 if err != nil {

  fmt.Println("GetNumberOfRequests")

  fmt.Println(err)

 }



 fmt.Println(result)



 return result

}

Enter fullscreen mode Exit fullscreen mode

Função para aprovar requisição


func (c *Campaign) ApproveRequest(contributor *account.Account, index *big.Int) {



 nonce := contributor.GetNonce()



 gasPrice, err := c.client.SuggestGasPrice(context.Background())

 if err != nil {

  log.Fatal(err)

 }



 auth, err := bind.NewKeyedTransactorWithChainID(contributor.PrivateKey, c.chainID)

 if err != nil {

  log.Fatal(err)

 }

 auth.Nonce = big.NewInt(int64(nonce))

 auth.Value = big.NewInt(0)      // em wei

 auth.GasLimit = uint64(6721975) // em units 6721975

 auth.GasPrice = gasPrice



 tx, err := c.Instance.ApproveRequest(auth, index)

 if err != nil {

  fmt.Println("ApproveRequest")

  fmt.Println(err.Error())

  log.Fatal(err)

 }



 fmt.Println("transaction hash ...")

 fmt.Println(tx.Hash())

}

Enter fullscreen mode Exit fullscreen mode

Função para Finalizar Requisição


func (c *Campaign) FinalizeRequest(manager *account.Account, index *big.Int) {

 nonce := manager.GetNonce()



 gasPrice, err := c.client.SuggestGasPrice(context.Background())

 if err != nil {

  log.Fatal(err)

 }



 auth, err := bind.NewKeyedTransactorWithChainID(manager.PrivateKey, c.chainID)

 if err != nil {

  log.Fatal(err)

 }

 auth.Nonce = big.NewInt(int64(nonce))

 auth.Value = big.NewInt(0)      // em wei

 auth.GasLimit = uint64(6721975) // em units 6721975

 auth.GasPrice = gasPrice



 tx, err := c.Instance.FinalizeRequest(auth, index)

 if err != nil {

  fmt.Println("FinalizeRequest")

  fmt.Println(err.Error())

  log.Fatal(err)

 }



 fmt.Println("transaction hash ...")

 fmt.Println(tx.Hash())

}

Enter fullscreen mode Exit fullscreen mode

Código Final

https://github.com/mobintmu/kickstart?source=post_page-----bd5dc7c8f84f--------------------------------


Este artigo foi escrito por mobin shaterian e traduzido por Adriano P. de Araujo. O original em inglês pode ser encontrado aqui.

Latest comments (0)