Introduction to basic knowledge of contract writing

This document introduces the basics of contract writing, including contract initialization, action and permissions. It is suitable for beginners and developers who want to understand the basics of smart contract writing, and help them quickly understand and get started with the writing of EOS smart contracts. As the basics of smart contracts, this article only covers contract initialization, actions and permissions.

01

Smart Contract Introduction

As a distributed and trusted computing platform, blockchain is its most essential feature of decentralization. A record of every transaction is immutably stored on the blockchain. Smart contracts define actions and transaction codes that can be executed on the blockchain. Can be executed on a blockchain and include the contract execution state as part of the immutable history of that blockchain instance.

As a result, developers can rely on the blockchain as a trusted computing environment where the input, execution, and results of smart contracts are independent from outside influence.

02

Introduction to basic knowledge of contract writing

(1) Contract initialization

1. Contract constructor

In EOSIO, smart contracts are written in C++ and deployed on the blockchain network in the form of WASM (WebAssembly) bytecode. When the deployment is complete, the action of the contract can be called to perform the corresponding operation. After the contract is deployed, its initialization function will be automatically executed to initialize the data and state of the contract.

The constructor of the smart contract is called when the contract is deployed to initialize the state and data of the contract. A constructor is a special member function that has no return type and whose function name is the same as the contract name.

The following is an example constructor of a simple EOS smart contract:

#include <eosio/eosio.hpp>

using namespace eosio;

class [[eosio::contract("hello")]] hello : public contract {
public:
   hello(name receiver, name code, datastream<const char*> ds)
      : contract(receiver, code, ds) {
        eosio::print_f("hello is ready. ");
      }
};

In the above code, the parameters of the constructor include:

  • receiver: The receiver of the contract instance, specify the account name of the instance.

  • code: The account name to which the contract belongs.

  • ds: data stream object, used to serialize and deserialize contract data. Usually it is used in the constructor to initialize the state of the contract.

The constructor must inherit from the contract class, and call the constructor of the contract class to initialize the contract state and data.

In the constructor, you can perform various initialization operations, such as allocating initial resources, initializing data structures, loading configuration, and more.

It should be noted that the constructor function will only be executed once when the contract is deployed, and cannot be executed again afterwards. Therefore, the contract constructor is crucial for functions such as initializing the contract state and setting the contract's permissions and authorizations. If you need to modify the data and state of the contract, you need to call other functions through action.

2、ds

The full name of ds is datastream, which is a data stream for reading and writing bytes. The data stream object is an important object in the EOS smart contract, which is used to serialize and deserialize contract data. In EOS, when the contract communicates with the blockchain nodes, the data needs to be serialized into a binary format, and the data in the binary format also needs to be deserialized into the corresponding data type for processing inside the contract. ds can help contract developers to implement these data serialization and deserialization operations conveniently.

(1) Usage scenarios of ds

  • As a parameter of the smart contract constructor

In smart contracts, ds is used to serialize and deserialize contract data. Usually it is used in the constructor to initialize the state of the contract.

  • Read and write parameters in smart contracts

In smart contracts, it is usually necessary to read and write some parameters, for example, some parameters need to be passed in when calling an action of a contract, or some field values ​​need to be specified when adding data to a table. For these parameters, they can be serialized and deserialized using ds for processing in smart contracts.

  • communicate with other contracts

In EOSIO, multiple contracts can call and communicate with each other. When data needs to be passed to other contracts, ds can be used to serialize the data so that binary data streams can be passed between different contracts.

  • Handling complex data types in smart contracts

In smart contracts, you can customize some complex data types, such as structures, objects, and so on. When these complex data types need to be processed in smart contracts, ds can be used to serialize and deserialize them for processing in smart contracts.

(2) Benefits of using ds

  • Easy to serialize and deserialize: Using the ds object, you can easily serialize various data types into binary format, or deserialize binary format into corresponding data types. This is very important for writing contract code and communicating with blockchain nodes.

  • Reduce space overhead: Using ds can reduce the space occupied by data in memory, thereby saving resources. Serialized binary data typically takes up less space than raw data and is also easier to transfer over the network.

  • Improve efficiency: Using ds can improve code execution efficiency. Serialization and deserialization operations usually require a lot of computing resources and time, but ds can provide efficient data stream processing functions, thereby improving code execution efficiency.

(3) ignore keyword

The ignore type directive tells the dataflow to ignore a certain type, but let the ABI generator add the correct type information. Currently, non-ignore types cannot follow ignore types in method definitions. For example, void foo(float, ignore) is allowed, while void foo(float, ignore, int) is not allowed.

In EOSIO smart contracts, use the ignore keyword to mark parameters that do not need to be used. Using the ignore keyword in a function parameter can tell the virtual machine not to parse this parameter, but let us manually parse this parameter.

When the fields ignored by ignore need to be used during the execution of the ACTION function, the predefined data stream object _ds can be used for further deserialization operations. The _ds object can help us further deserialize and process the operation data, especially for fields ignored by ignore. For example:

#include <eosio/eosio.hpp>
#include <eosio/ignore.hpp>

using namespace eosio;

struct person
{
  name key;
  std::string first_name;
  std::string last_name;
  uint64_t age;
  std::string street;
  std::string city;
  std::string state;
};

class [[eosio::contract("addressbook")]] addressbook : public eosio::contract
{
public:
  addressbook(name receiver, name code, datastream<const char *> ds)
      : contract(receiver, code, ds) {}

[[eosio::action]] 
  void test( name user, ignore<uint64_t>, ignore<person>) {
     print( "Hello", user );

     // 读取 ignore 数据。
     uint64_t id;
     person p;
     _ds >> id >> p;
     print(id);
     print("##")
     print(p.city,"##",p.street);
   }
};

Invoke the test action with:

cleos push action addressbook test '["alice",5,{"key":"alice","first_name":"alice","last_name":"final","age":32,"street":"Beijin","city":"heping","state":"amsterdam"}]' -p alice@active

The result of the operation is as follows:

executed transaction: c4195834f803c38964b00e0c0baf7ee6fd15b2ca17df7782641c8ec4f81dc70d  160 bytes  260 us
#   addressbook <= addressbook::test            "0000000000855c3405000000000000000000000000855c3405616c6963650566696e616c2000000000000000064265696a6...
>> Helloalice5##heping##Beijin

In this example, a variable id of type uint64_t and a variable p of type person are deserialized from the input data stream through the _ds data stream object. These data are originally marked as ignore, but by deserializing the ignore data, these data can be stored in corresponding variables, and these data can be further processed in the ACTION function.

It should be noted that when we use the ignore keyword, the virtual machine will not skip the parsing of this field. Instead, the VM parses this field as a null value and leaves it in the data stream. If we need to use this field, we can use the _ds object to manually parse this field.

(2) action

A packaged representation of an action, along with metadata information about authorization levels. In EOSIO, when an action is sent, it is packaged into a binary format and transmitted over the network. This binary format contains information such as the operation name, operation data, and authorization level of the action, so that EOSIO network nodes can correctly parse and execute the action. Metadata information includes information about authorization levels, signatures, and other authentication required to perform the action.

1. Action example

#include <eosio/eosio.hpp>
class [[eosio::contract]] hello : public eosio::contract {
  public:
      using eosio::contract::contract;
      // 定义一个名为hi的action,需要传入用户名
      [[eosio::action]] void hi( eosio::name user ) {
         // 打印"Hello,用户名"
         print( "Hello, ", user);
      }
};

Here the class name has nothing to do with the contract account name, but for the convenience of management, the contract file name, class name and contract account should be the same. The contract defines an action named "hi", which accepts a "name", which contains only one sentence to print, any user can call this action, and the corresponding contract will print Hello and the username as a response. Multiple actions can be defined in a contract, and these actions can also call each other.

Notice:

  • An action must be a member method of a C++ contract class

  • Use [[eosio::action]] to identify this is an action, otherwise it is an ordinary class member function

  • Access level must be public

  • The return value must be empty

  • Can accept any number of input parameters

2、action_wrapper

action_wrapper is an action wrapper, which is convenient for itself or other contracts to call the declared action.

(1) How to use action_wrapper

In a file called hello.hpp, there is an operation called hi.

#include <eosio/eosio.hpp>

class [[eosio::contract("hello")]] hello : public contract {
 public:
  using eosio::contract::contract;
  [[eosio::action]] void hi( eosio::name user ) ;
  [[eosio::action]] void sayhi( eosio::name user ); 
};

To define the Action Wrapper for the hi action, you can use the eosio::action_wrapper template. The first parameter of the template is the action name of type eosio::name, and the second parameter is a reference to the action method.

using hi_action = eosio::action_wrapper<"hi"_n, &hello::hi>;

To use an action wrapper, include the header file that defines the action wrapper. In the example above, the following code could be added to the contract's header file:

#include <hello.hpp>

Then, instantiate the hi_action defined above, and specify the contract to send the action as the first parameter. In this case, assume that the contract is deployed to the hello account, then get its own account by calling the get_self() method, and finally specify the active permission (you can modify these two parameters according to your needs) to define a structure containing two parameters.

hi_action wrapper{"hello"_n,{get_self(), "active"_n}};

Finally, the send method of the action wrapper is called, passing in the parameters of the hi action as positional parameters.

wrapper.send(user);

The complete code is as follows:

hello.hpp​​​​​​​

#include <eosio/eosio.hpp>

class [[eosio::contract("hello")]] hello : public contract {
 public:
  using eosio::contract::contract;
  [[eosio::action]] void hi( eosio::name user ) ;
  [[eosio::action]] void sayhi( eosio::name user ); 
};
using hi_action = eosio::action_wrapper<"hi"_n, &hello::hi>;

hello.cpp​​​​​​​

#include "hello.hpp"

[[eosio::action]] 
void hello::hi(name user){
   require_auth(user);
   print("Hello, ", name{user});
}

[[eosio::action]] 
void hello::sayhi(name user){
  hi_action wrapper{"hello"_n,{get_self(), "active"_n}};
  wrapper.send(user);
  print("action_wrapper");
}

In this example, an action_wrapper object called hi_action is defined, which wraps the hi operation. In the sayhi operation, a hi_action object wrapper is created and sent to the specified user. It should be noted that when calling the send function, the authorization information of the caller needs to be specified, which can be specified through the eosio::permission_level class. In this example, we use the active permission of the contract account for authorization.

(2) What is the difference between the send of action_wrapper and the send of ordinary action

When using action_wrapper, parameters are checked at compile time, which can help avoid potential errors that could arise at runtime. When using the action method to send the operation directly, there is no such compile-time check.

Using the action method does not need to include the header file of the target contract in the contract, but when using action_wrapper to declare the operation, it is necessary to include the header file of the target contract or the function signature of the declaration operation in the contract.

Although action_wrapper is more recommended, if you know exactly what you're doing, you won't have a problem using the action method.

(3) Benefits of using action_wrapper

  • Convenient operation call: use action_wrapper to conveniently call the operation. By encapsulating the operation into an object, the encapsulated object can be called directly, without having to manually specify parameters such as the name of the operation, the contract to which it belongs, etc., thereby improving the convenience of operation invocation.

  • Improve security: Using action_wrapper can improve the security of the contract. When calling an operation, it is necessary to specify parameters such as the name of the operation and the contract it belongs to. If these parameters are incorrect, it may cause the operation to fail or perform an incorrect operation. By using action_wrapper, you can avoid manually specifying these parameters, thereby reducing the possibility of errors.

  • Improve efficiency: Using action_wrapper can improve code execution efficiency. When calling an operation, if it is necessary to repeatedly specify parameters such as the name of the operation and the contract it belongs to, it will increase the execution time of the code. By using action_wrapper, this additional overhead can be avoided, thereby improving the execution efficiency of the code.

(3) Authority

For some specific actions, you don't want others to be able to operate them, so you need to add permission verification.

1. Function description

(1) require_auth function​​​​​​​​

void require_auth(
    capi_name name
)

Verify that the specified account is consistent with the account that invoked the action. If they do not match, an error will be reported directly.

Parameter Description:

  • name - the name of the account to be authenticated

Example :

[[eosio::action]] void hi(eosio::name user ) {
   require_auth( user );
   print( "Hello, ", name{user} );
}

This example implements an action named hi, which requires only the parameter user passed in to execute the action. In this action function, use the require_auth function to check whether the account currently executing the action has user authorization. If there is no authorization, a default authorization error message will be triggered to prevent the execution of the action. If the authorization is successful, a message starting with Hello, and ending with user will be output.

(2) require_auth2 function

void eosio::require_auth2(capi_name name, capi_name permission)

Verify that the specified account and permissions are consistent with the account and permissions that invoked the action. If they do not match, an error will be reported directly.

Parameter Description:

  • name - the name of the account to be authenticated

  • permission - the permission level that needs to be authenticated

Example :

#include <capi/eosio/action.h>

[[eosio::action]]void hi( eosio::name user ) {
   require_auth2(user.value, "active"_n.value);
   print( "Hello, ", name{user} );
}

This code is an action function of an EOSIO smart contract, which is used to send a greeting message to a specified user. In this action function, the require_auth2 function is used to ensure that only specified users with active permissions can call the action function.

If the provided user permissions do not meet the requirements when calling this action function, an exception will be thrown and the execution of the action function will fail. Because no custom error message is provided in this function, when the authorization fails, the error message will be the default authorization error message.

(3) has_auth function

bool eosio::has_auth(
    name n
)

Verify that the specified account matches the account that invoked the action

Parameter Description:

  • n - the name of the account that needs to be verified

Example :

[[eosio::action]] void hi( eosio::name user ) {
   if(has_auth( user )){
      print("Hello, ", eosio::name{user} );
      }else{
      print("This is not ",eosio::name{user} );
   }
}

This example defines a hi action that receives a parameter user of type eosio::name. When executing this action, it will check whether the current executor has the authority of the incoming user account. If yes, it will output "Hello," followed by the name of the user account, otherwise it will output "This is not " followed by The name of the above user account. This operation ensures that only the incoming user account can execute this action, regardless of the permissions (such as owner, active, code) used by the executor. In addition, the error message of this action is customized to provide a better user experience.

(4) check function

void eosio::check(
    bool pred,
    const char * msg
)

Assert, if pred is false, feedback with the provided message.

Example :

#include <capi/eosio/action.h>

void hi( name user ) {
   check(has_auth(user), "User is not authorized to perform this action.");
   print( "Hello, ", name{user} );
}

This example defines a hi action, which receives a parameter user of type eosio::name, uses the has_auth function to check whether the account is authorized to perform this action, and if not, uses the check function to throw a custom error message "User is not authorized to perform this action.". After the authorization check is passed, the program will output the account name received after "Hello, ".

Notice:

Only the account specified as a parameter can perform this action, no matter which authority the account uses to sign the transaction.

(5) is_account function

bool eosio::is_account(
    name n
)

Check if the account exists.

Parameter Description:

  • n - the name of the account that needs to be verified

Example :

#include <capi/eosio/action.h>

void hi( name user ) {
   check(is_account(user), "The provided name is not an existing account");
   print( "Hello, ", name{user} );

This example uses the is_account function to check whether an account exists. If the account name passed in does not exist, an exception will be thrown and the corresponding prompt will be included in the error message.

2. The difference between require_auth, require_auth2 and has_auth

has_auth, require_auth, and require_auth2 are all functions used for permission checking in EOSIO.

  • has_auth: has_auth is used to verify whether the specified account matches the account that called the action. Returns true if the specified account matches the account that called the action, otherwise returns false, and this function does not throw an exception.

  • require_auth: The specified account must match the account that invoked the action, otherwise the function will throw an exception and will not execute further.

  • require_auth2: require_auth2 adds restrictions on account permissions on the basis of require_auth.

In short, the require_auth function requires that the caller must be a specified account, otherwise the contract execution fails; the require_auth2 function requires that the caller must have the specified authority of the specified account, otherwise the contract execution fails; the has_auth function returns whether the caller is a specified account, and no exception will be thrown .

-END-

Guess you like

Origin blog.csdn.net/BSN_yanxishe/article/details/131637957