Writing and editing program logic
Start writing code for smart contracts
Last updated
Start writing code for smart contracts
Last updated
Once SmartX is up and running, this what the main window looks like.
Since we have selected the OEP-4 template, the code is already present in the editor area. You may choose to edit this code as you please based on the logic that you're trying to implement. But for the sake of simplicity and staying within the scope of this tutorial we will use the code as it is to ensure uniformity.
It is worth taking some time to take a look at the code and the overall structure of the program that we'll be working with.
The variables declared in this section of the code define the protocol itself and the specifics that will govern its functionality.
Variables such as NAME
and SYMBOL
serve as identifiers for the token.
FACTOR
is the base 10 value that dictates the precision of amounts that can be transferred. For example, if the value is set to 100, transfer amounts with values up to two levels of precision are supported by the system. DECIMALS
stores the multiplier value for access convenience.
OWNER
stores the Base58 address of the entity or account that holds the authority over the totality of tokens and can choose to distribute them as and when needed.
TOTAL_AMOUNT
stores the total number of tokens that exist. Always a fixed number.
BALANCE_PREFIX
is an access modifier that is used with account addresses for authentication purposes. APPROVE_PREFIX
serves the same purpose, but for the approve operation wherein the owner can authenticate another account to use tokens.
SUPPLY_KEY
correlates directly to the total amount of tokens and is used for any operations that may be carried with the total tokens figure, since the value isn't directly accessible.
This line immediately stands out in the upper section of the code.
GetContext()
is a function that acts as the bridge between the smart contract and the blockchain. It is used when fetching and transmitting data from and to the chain by calling the GET
and PUT
functions which are a part of the Storage API.
We will go through the relevant APIs as we come across the respective functions, and the full set of available APIs in a later section of the tutorial.
Since we're working with the online SmartX IDE, all the APIs and functions are available for use and can be accessed directly by importing at the start of the program. Here, the functions that we import are GetContext()
, Get()
, Put()
, and Delete()
from the Storage API, Notify()
, CheckWitness()
and Base58ToAddress()
from the Runtime API and the built-in function concat()
.
In later versions, built-in functions don't need to be imported and can be called directly.
Let us take a look at the main()
function.
The Main() function takes two arguments, operation
and args
. The operation argument is based on the operation to be performed and dictates the function to the executed. The args
argument helps passing the important information that a function needs to carry out further execution, for example account addresses or input data.
Here, clearly there are 11 different functions that can be called depending upon the argument that is passed in Main(). SmartX passes these arguments using the "Options" pane on the bottom right.
init() : The init() function serves as the starting point of the program logic. It initializes the definition variables declared at the top based on the values provided. Thus, this is the first function that needs to be executed post deployment.
name() : This function returns the name assigned to the token, "My Token" in this case.
symbol() : This function returns the symbol assigned to the token, "MYT" in this case.
decimals() : Returns the number of decimal places that designate the precision of valid token values, 8 in this case.
totalSupply() : Returns the total number of tokens assigned while initializing. Denotes the fixed number of tokens allocated for circulation. (Uses SUPPLY_KEY
to fetch the value from the chain, stored earlier during initialization)
balanceOf(acct) : Fetches the corresponding token balance of the account that identifies with the Base58 address passed as argument to the function.
transfer(from_acc, to_acc, amount) : Transfers the equivalent token value passed as the amount argument to the to_acc
address from the from_acc
address.
transferMulti(args) : The parameter here is an array that contains the same information, i.e., the sender's address, receiver's address, and the amount to be sent, in that sequence at the respective indices. It can be iterated for multiples transactions by passing the respective account addresses and the corresponding amount.
transferFrom(spender, from_acc, to_acc, amount) : The spender here takes a certain amount of tokens from the from_acc
address, and transfers them to the to_acc
address.
approve(owner, spender, amount) : The owner authorizes the spender to use a certain amount of tokens from their own account. Both the owner and spender arguments here are Base58 addresses and the amount specifies the amount that the spender is authorized to spend.
allowance(owner, spender) : This function can be used to the amount that the owner account has authorized spender to use. Both arguments in this case are Base58 addresses.
The functions above can be classified into two different types- access functions and utilities. Let us consider the flow of control as these functions are called.
Access functions are primarily used to fetch data post contract deployment. Functions such as name()
, symbol()
, totalSupply()
and balanceOf(acc)
allow us to achieve this by using get()
function from the Storage API which fetches relevant data from the chain. Let us look at how it is implemented in program logic.
name() function definition
This is how a simple access function can be defined. The name()
function takes no arguments. Even though functions such as name()
, symbol()
and decimals()
do not explicitly call the get()
function, after the contract is deployed, all the data is fetched from the chain.
balanceOf(acc) function definition
The balanceOf()
function takes one argument, a Base58
address which denotes an account. There is a validity check in place that verifies the length of the address and raises an exception if the address is invalid. If the address is valid the get()
function is called with two arguments, the account address prefixed with BALANCE_PREFIX
, and the context.
The context allows for data reference on the chain to fetch the account balance value, while the prefixed account address ensures authenticated access. Next, get()
returns this data to Main()
where it is output to the log window using the notify()
function. The totalSupply()
function works in a similar fashion.
The balance and approve prefixes are hexadecimal values in ASCII
format and can be modified to support your own program logic.
Let us look at the utilities in the sample code.
Utilities are methods that have richer functionality and help modifying the on-chain data, which is the basis for transactions and tasks that may be carried out. The OEP-4 token logic that the code template is using illustrates various use cases and scenarios in the form of functionality. This is to exhibit just how versatile smart contracts are in nature and the different kinds of business logic that they can be used to generate.
transfer(from_acc, to_acc, amount)
The transfer()
function implements the most fundamental transaction feature, transferring tokens from one account to another. It takes three arguments, the sender's address, the receiver's address, and the amount to be transferred.
The function carries out verification by a simple length check, but a more complex logic can be developed based on individual needs.
Next, the BALANCE_PREFIX
is concatenated to the sender's account address, and balance is retrieved by making a get()
call using this address. A quick comparison is made in the next step where the sender account's balance is compared with the amount to be transferred. All three scenarios have been defined clearly.
If the balance is less than the transfer amount, the transaction fails, and the control returns to Main()
directly.
If the amount equates to the balance amount exactly, the balance of sender account is set to 0 by calling the delete()
method using the sender's prefixed address. This is practically equivalent to using the put()
method to manually assign the value 0 to sender's account but using put()
method in this case might give rise to security vulnerabilities.
If the balance is higher than the transfer amount, the amount is deducted from the balance by making a put()
call and updating the sender accounts balance with the deducted value.
Next, the receiver's address is prefixed with the BALANCE_PREFIX
, and the prefixed address is used to add the transfer amount to the receiver's account.
Finally, this transaction event is sent to the chain using the RegisterAction()
method for recording in a ledger.
The transfer()
function implements the most fundamental transaction feature, transferring tokens from one account to another. It takes three arguments, the sender's address, the receiver's address, and the amount to be transferred.
The function carries out verification by a simple length check, but a more complex logic can be developed based on individual needs.
Next, the BALANCE_PREFIX
is concatenated to the sender's account address, and balance is retrieved by making a get()
call using this address. A quick comparison is made in the next step where the sender account's balance is compared with the amount to be transferred. All three scenarios have been defined clearly.
If the balance is less than the transfer amount, the transaction fails, and the control returns to Main()
directly.
If the amount equates to the balance amount exactly, the balance of sender account is set to 0 by calling the delete()
method using the sender's prefixed address. This is practically equivalent to using the put()
method to manually assign the value 0 to sender's account but using put()
method in this case might give rise to security vulnerabilities.
If the balance is higher than the transfer amount, the amount is deducted from the balance by making a put()
call and updating the sender accounts balance with the deducted value.
Next, the receiver's address is prefixed with the BALANCE_PREFIX
, and the prefixed address is used to add the transfer amount to the receiver's account.
Finally, this transaction event is sent to the chain using the RegisterAction()
method for recording in a ledger.
TransferEvent is the alias that RegisterAction()
uses here. RegisterAction()
is a method of the Action API and it takes four arguments that are transferred to the chain in order to record transaction details. The transaction hash and certain other details are output in the logs section.
The transferMulti() is works in a similar manner. The logic remains the same, since basically all that transferMulti()
does is call transfer()
, but it allows for multiple transfers to take place simultaneously. It takes one argument which is a nested array. is the alias that RegisterAction()
uses here. RegisterAction()
is a method of the Action API and it takes four arguments that are transferred to the chain in order to record transaction details. The transaction hash and certain other details are output in the logs section.
The transferMulti() is works in a similar manner. The logic remains the same, since basically all that transferMulti()
does is call transfer()
, but it allows for multiple transfers to take place simultaneously. It takes one argument which is a nested array.
The sub-array elements are processed in sets of three such that the first and second elements still represent the sender's and receiver's addresses, and the third element represents the transfer amount. The sub arrays are iterated till there are no more elements left in the args[]
array.
Exception is thrown in case the sub-array does not contain exactly three elements, or the previous transaction fails for some reason, and the control comes out of the loop and goes back to Main()
approve(owner, spender, amount)
The approve function implements another complex logic wherein an account, namely the spender
is given the permission to utilize a certain amount in tokens from another account, namely the owner
.
The function first carries out address validation in terms of length, and user authentication for the owner
who is about to perform the approval.
Next, the amount selected for approval is compared to the available balance in the owner
account.
If the account does not have enough balance the process is terminated, and the control returns to Main()
.
If the account has enough balance, a key is generated by concatenating the owner
address prefixed with APPROVAL_PREFIX
and the spender
address. It is then added to the ledger using the put()
method by passing the context, the above generated key, and the approval amount.
The transaction event is then recorded using the ApprovalEvent()
method, and the result containing the transaction hash returned by the NeoVM engine is displayed in the logs section.
The transferFrom()
function implements a more complex logic and carries out a task that may prove to be useful for certain applications.
This function allows a third party, namely the spender, to utilize a certain amount in tokens that are provided from an account that does not designate to their own credentials, basically implementing the same logic as that of the approve()
function. It takes four arguments, which are three Byte58 addresses and one transfer amount.
First, the function carries out the conventional address validation. Then it verifies whether the spender has the authorization to carry out this transaction using the CheckWitness()
function which is a part of the Runtime API.
Next, the balance of the from
account is fetched and cross-checked with the transaction amount to ensure the account has enough balance. The process to fetch the balance remains the same.
The spender's address is then prefixed with the APPROVE_PREFIX
and the approved amount from is fetched using the prefixed address.
The transaction comes next. If the transaction amount is higher than the approved amount the transaction is aborted, and control returns to Main()
.
If the amount is exactly equal to the approved amount, the transaction amount is deducted from the from
accounts balance.
If the approved amount exceeds the transaction amount, the difference is calculated and stored in the ledger for future reference, and the transaction amount is deducted from the from
account.
The transaction amount is then transferred to the to
account using the put()
function. The event is then recorded and the result with the transaction hash is displayed in the logs section of the IDE.
Another function that implements a similar logic has be defined as allowance(owner, spender) which facilitates querying the amount of allowance that has been allocated to the spender
account from the owner
account.
Practically speaking, this function cam be classified as an access function too in the sense that it returns the allowance value. But it also performs a get()
query to fetch this result from the chain.
A key generated by concatenating the prefixed owner address and the spender address is passed to the get()
method along with the context to fetch the required allowance value, which is then returned to Main()
. The value can then be displayed or used to perform other tasks.