Process Design
In this section, we will discuss the design principles and best practices for writing AO processes. AO processes are written in Lua, a lightweight, high-level, multi-paradigm programming language. Lua is designed to be embedded in other applications, making it an ideal choice for writing AO processes.
An over simplified AO token process could look like this, however, try and follow along with the code below. It's not really easy to comprehend and follow, but we will break it down and explain how things can be improved in the following sections.
local ao = require('ao')
Version = "1.0.0"
-- token should be idempotent and not change previous state updates
Balances = Balances or { [ao.id] = 1000 }
TotalSupply = TotalSupply or 1000
Name = "test token"
Handlers.add('info', Handlers.utils.hasMatchingTag('Action', 'Info'), function(msg)
ao.send({
Target = msg.From,
Tags = [
Name = Name,
TotalSuppy = TotalSupply
]
})
end)
Handlers.add('balance', Handlers.utils.hasMatchingTag('Action', 'Balance'), function(msg)
local bal = 0
-- If not Recipient is provided, then return the Senders balance
if (msg.Tags.Recipient) then
if (Balances[msg.Tags.Recipient]) then
bal = Balances[msg.Tags.Recipient]
end
elseif Balances[msg.From] then
bal = Balances[msg.From]
end
ao.send({
Target = msg.From,
Tags = [
Balance = bal,
Account = msg.Tags.Recipient or msg.From,
]
Data = bal
})
end)
Handlers.add('mint', Handlers.utils.hasMatchingTag('Action', 'Mint'), function(msg)
assert(type(msg.Quantity) == 'string', 'Quantity is required!')
assert(0 < tonumber(msg.Quantity), 'Quantity must be greater than zero!')
if not Balances[ao.id] then Balances[ao.id] = 0 end
-- Add tokens to the token pool, according to Quantity
Balances[msg.From] = Balances[msg.From] + tonumber(msg.Quantity)
TotalSupply = TotalSupply + tonumber(msg.Quantity)
ao.send({
Target = msg.From,
Data = Colors.gray .. "Successfully minted " .. Colors.blue .. msg.Quantity .. Colors.reset
})
end)
Handlers.add('burn', Handlers.utils.hasMatchingTag('Action', 'Burn'), function(msg)
assert(type(msg.Quantity) == 'string', 'Quantity is required!')
assert(tonumber(msg.Quantity) <= tonumber(Balances[msg.From]), 'Quantity must be less than or equal to the current balance!')
Balances[msg.From] = Balances[msg.From] - tonumber(msg.Quantity)
TotalSupply = TotalSupply - tonumber(msg.Quantity)
ao.send({
Target = msg.From,
Data = Colors.gray .. "Successfully burned " .. Colors.blue .. msg.Quantity .. Colors.reset
})
end)
Process Design Principles
When designing AO processes, it is essential to follow certain design principles to ensure that the processes are efficient, maintainable, and scalable. Here are some key design principles to keep in mind:
1. Modularity
Modularity is the practice of breaking down a system into smaller, independent modules that can be developed, tested, and maintained separately. In the context of AO processes, it is essential to break down the process logic into smaller, reusable components that can be easily combined to create complex processes.
What we can do is to break down the process into smaller modules, such as balance
, transfer
, mint
, burn
, etc. This will make the code more organized and easier to maintain and test.
For displaying porposes we will only break things into process_lib.lua
and process.lua
.
local process_lib = require('process_lib')
Version = "1.0.0"
-- token should be idempotent and not change previous state updates
Balances = Balances or { [ao.id] = 1000 }
TotalSupply = TotalSupply or 1000
Name = "test token"
Handlers.add('info',
Handlers.utils.hasMatchingTag('Action', 'Info'),
process_lib.getInfo
)
Handlers.add('balance',
Handlers.utils.hasMatchingTag('Action', 'Balance'),
process_lib.getBalance
)
Handlers.add('mint',
Handlers.utils.hasMatchingTag('Action', 'Mint'),
process_lib.mint
)
Handlers.add('burn',
Handlers.utils.hasMatchingTag('Action', 'Burn'),
process_lib.burn
)
2. Separation of Concerns
Separation of concerns is the practice of dividing a system into distinct sections, each responsible for a specific aspect of the system's functionality. In the context of AO processes, it is essential to separate the process logic from other concerns, such as input/output handling, error handling, and logging. This makes the code more organized and easier to maintain, test, and debug.
Continuing with the previous example, we can separate the process logic from the input/output handling by creating separate function to handle replying things to the user.
local ao = require('ao')
local process_lib = {}
function process_lib.getInfo(msg)
process_lib.sendReply(msg, {
Name = Name,
TotalSuppy = TotalSupply
}, "")
return {
Name = Name,
TotalSuppy = TotalSupply
}
end
function process_lib.getBalance(msg)
local bal = 0
-- If not Recipient is provided, then return the Senders balance
if (msg.Tags.Recipient) then
if (Balances[msg.Tags.Recipient]) then
bal = Balances[msg.Tags.Recipient]
end
elseif Balances[msg.From] then
bal = Balances[msg.From]
end
process_lib.sendReply(msg, {
Balance = bal,
Account = msg.Tags.Recipient or msg.From,
}, bal)
return {
Balance = bal,
Account = msg.Tags.Recipient or msg.From
}
end
function process_lib.mint(msg)
assert(type(msg.Quantity) == 'string', 'Quantity is required!')
assert(0 < tonumber(msg.Quantity), 'Quantity must be greater than zero!')
if not Balances[ao.id] then Balances[ao.id] = 0 end
-- Add tokens to the token pool, according to Quantity
Balances[msg.From] = Balances[msg.From] + tonumber(msg.Quantity)
TotalSupply = TotalSupply + tonumber(msg.Quantity)
process_lib.sendReply(msg, {}, Colors.gray .. "Successfully minted " .. Colors.blue .. msg.Quantity .. Colors.reset)
return {
Balance = Balances[msg.From],
}
end
function process_lib.burn(msg)
assert(type(msg.Quantity) == 'string', 'Quantity is required!')
assert(tonumber(msg.Quantity) <= tonumber(Balances[msg.From]), 'Quantity must be less than or equal to the current balance!')
Balances[msg.From] = Balances[msg.From] - tonumber(msg.Quantity)
TotalSupply = TotalSupply - tonumber(msg.Quantity)
process_lib.sendReply(msg, {}, Colors.gray .. "Successfully burned " .. Colors.blue .. msg.Quantity .. Colors.reset)
return {
Balance = Balances[msg.From],
}
end
function process_lib.sendReply(msg, tags, data)
ao.send({
Target = msg.From,
["Response-For"] = msg.Action,
Tags = tags,
Data = data
})
end
return process_lib
sendReply
is basically just a wrapper around the ao.send
function, but it makes the code more readable and easier to maintain. It also allows us to easily test the process logic without having to worry about the input/output handling.
In the next section, we will introduce Ao Process Testing and discuss how to write unit tests for AO processes to ensure that they are working as expected.