diff --git a/blueprints/subledger_code.lua b/blueprints/subledger_code.lua new file mode 100644 index 00000000..05556e89 --- /dev/null +++ b/blueprints/subledger_code.lua @@ -0,0 +1,316 @@ +local subledgerCode = [===[ + +local bint = require('.bint')(256) +local ao = require('ao') +--[[ + This module implements the ao Standard Token Specification. + + Terms: + Sender: the wallet or Process that sent the Message + + It will first initialize the internal state, and then attach handlers, + according to the ao Standard Token Spec API: + + - Info(): return the token parameters, like Name, Ticker, Logo, and Denomination + + - Balance(Target?: string): return the token balance of the Target. If Target is not provided, the Sender + is assumed to be the Target + + - Balances(): return the token balance of all participants + + - Transfer(Target: string, Quantity: number): if the Sender has a sufficient balance, send the specified Quantity + to the Target. It will also issue a Credit-Notice to the Target and a Debit-Notice to the Sender + + - Mint(Quantity: number): if the Sender matches the Process Owner, then mint the desired Quantity of tokens, adding + them the Processes' balance +]] +-- +local json = require('json') + +--[[ + utils helper functions to remove the bint complexity. +]] +-- + + +local utils = { + add = function(a, b) + return tostring(bint(a) + bint(b)) + end, + subtract = function(a, b) + return tostring(bint(a) - bint(b)) + end, + toBalanceValue = function(a) + return tostring(bint(a)) + end, + toNumber = function(a) + return tonumber(a) + end +} + +--[[ + Initialize State + + ao.id is equal to the Process.Id + ]] +-- +Variant = "0.0.3" + +-- token state should be assigned from the spawning message +-- token should be idempotent and not change previous state updates +Name = Name or ao.env.Process.Tags['Token-Name'] +Ticker = Ticker or ao.env.Process.Tags['Token-Ticker'] +Denomination = Denomination or tonumber(ao.env.Process.Tags['Token-Denomination']) or 12 +Logo = Logo or ao.env.Process.Tags['Token-Logo'] + +if not ao.env.Process.Tags['Parent-Token'] then + local initialSupply = tonumber(ao.env.Process.Tags['Token-Supply']) or 10000 + local initialBalance = utils.toBalanceValue(initialSupply * 10 ^ Denomination) + + Balances = Balances or { [ao.id] = initialBalance } + TotalSupply = TotalSupply or initialBalance +else + Balances = Balances or {} +end + +SourceToken = ao.env.Process.Tags['Source-Token'] or ao.id +ParentToken = ao.env.Process.Tags['Parent-Token'] + +Subledgers = Subledgers or {} +SubledgersPendingInit = SubledgersPendingInit or {} + +--[[ + Add handlers for each incoming Action defined by the ao Standard Token Specification + ]] +-- + +--[[ + Info + ]] +-- +Handlers.add('info', Handlers.utils.hasMatchingTag('Action', 'Info'), function(msg) + ao.send({ + Target = msg.From, + Name = Name, + Ticker = Ticker, + Logo = Logo, + Denomination = tostring(Denomination) + }) +end) + +--[[ + Balance + ]] +-- +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 msg.Tags.Target and Balances[msg.Tags.Target] then + bal = Balances[msg.Tags.Target] + elseif Balances[msg.From] then + bal = Balances[msg.From] + end + + ao.send({ + Target = msg.From, + Balance = bal, + Ticker = Ticker, + Account = msg.Tags.Recipient or msg.From, + Data = bal + }) +end) + +--[[ + Balances + ]] +-- +Handlers.add('balances', Handlers.utils.hasMatchingTag('Action', 'Balances'), + function(msg) ao.send({ Target = msg.From, Data = json.encode(Balances) }) end) + +--[[ + Transfer + ]] +-- +Handlers.add('transfer', Handlers.utils.hasMatchingTag('Action', 'Transfer'), function(msg) + assert(type(msg.Recipient) == 'string', 'Recipient is required!') + assert(type(msg.Quantity) == 'string', 'Quantity is required!') + assert(bint.__lt(0, bint(msg.Quantity)), 'Quantity must be greater than 0') + + if not Balances[msg.From] then Balances[msg.From] = "0" end + if not Balances[msg.Recipient] then Balances[msg.Recipient] = "0" end + + if bint(msg.Quantity) <= bint(Balances[msg.From]) then + Balances[msg.From] = utils.subtract(Balances[msg.From], msg.Quantity) + Balances[msg.Recipient] = utils.add(Balances[msg.Recipient], msg.Quantity) + + local SubledgerTags = {} + if Subledgers and Subledgers[msg.Recipient] then + SubledgerTags = { + ['Source-Token'] = SourceToken or ao.id, + ['Parent-Token'] = ParentToken, + } + end + + --[[ + Only send the notifications to the Sender and Recipient + if the Cast tag is not set on the Transfer message + ]] + -- + if not msg.Cast then + -- Debit-Notice message template, that is sent to the Sender of the transfer + local debitNotice = { + Target = msg.From, + Action = 'Debit-Notice', + Recipient = msg.Recipient, + Quantity = msg.Quantity, + Data = Colors.gray .. + "You transferred " .. + Colors.blue .. msg.Quantity .. Colors.gray .. " to " .. Colors.green .. msg.Recipient .. Colors.reset + } + -- Credit-Notice message template, that is sent to the Recipient of the transfer + local creditNotice = { + Target = msg.Recipient, + Action = 'Credit-Notice', + Sender = msg.From, + Quantity = msg.Quantity, + Data = Colors.gray .. + "You received " .. + Colors.blue .. msg.Quantity .. Colors.gray .. " from " .. Colors.green .. msg.From .. Colors.reset + } + + -- Add forwarded tags to the credit and debit notice messages + for tagName, tagValue in pairs(msg) do + -- Tags beginning with "X-" are forwarded + if string.sub(tagName, 1, 2) == "X-" then + debitNotice[tagName] = tagValue + creditNotice[tagName] = tagValue + end + end + + -- Add SubledgerTags to the credit notice if applicable + for tagName, tagValue in pairs(SubledgerTags) do + creditNotice[tagName] = tagValue + end + + -- Send Debit-Notice and Credit-Notice + ao.send(debitNotice) + ao.send(creditNotice) + end + else + ao.send({ + Target = msg.From, + Action = 'Transfer-Error', + ['Message-Id'] = msg.Id, + Error = 'Insufficient Balance!' + }) + end +end) + +--[[ + Mint + ]] +-- +Handlers.add('mint', Handlers.utils.hasMatchingTag('Action', 'Mint'), function(msg) + assert(type(msg.Quantity) == 'string', 'Quantity is required!') + assert(bint(0) < bint(msg.Quantity), 'Quantity must be greater than zero!') + + if not Balances[ao.id] then Balances[ao.id] = "0" end + + if msg.From == ao.id then + -- Add tokens to the token pool, according to Quantity + Balances[msg.From] = utils.add(Balances[msg.From], msg.Quantity) + TotalSupply = utils.add(TotalSupply, msg.Quantity) + ao.send({ + Target = msg.From, + Data = Colors.gray .. "Successfully minted " .. Colors.blue .. msg.Quantity .. Colors.reset + }) + else + ao.send({ + Target = msg.From, + Action = 'Mint-Error', + ['Message-Id'] = msg.Id, + Error = 'Only the Process Id can mint new ' .. Ticker .. ' tokens!' + }) + end +end) + +--[[ + Total Supply + ]] +-- +Handlers.add('totalSupply', Handlers.utils.hasMatchingTag('Action', 'Total-Supply'), function(msg) + assert(msg.From ~= ao.id, 'Cannot call Total-Supply from the same process!') + + ao.send({ + Target = msg.From, + Action = 'Total-Supply', + Data = TotalSupply, + Ticker = Ticker + }) +end) + +--[[ + Burn +]] -- +Handlers.add('burn', Handlers.utils.hasMatchingTag('Action', 'Burn'), function(msg) + assert(type(msg.Quantity) == 'string', 'Quantity is required!') + assert(bint(msg.Quantity) <= bint(Balances[msg.From]), 'Quantity must be less than or equal to the current balance!') + + Balances[msg.From] = utils.subtract(Balances[msg.From], msg.Quantity) + TotalSupply = utils.subtract(TotalSupply, msg.Quantity) + + ao.send({ + Target = msg.From, + Data = Colors.gray .. "Successfully burned " .. Colors.blue .. msg.Quantity .. Colors.reset + }) +end) + +Handlers.add('creditNotice', Handlers.utils.hasMatchingTag('Action', 'Credit-Notice'), function(msg) + assert(type(msg.Quantity) == 'string', 'Quantity is required!') + assert(type(msg.Sender) == 'string', 'Sender is required!') + + if msg.From == ParentToken then + if not Balances[msg.Sender] then Balances[msg.Sender] = "0" end + Balances[msg.Sender] = utils.add(Balances[msg.Sender], msg.Quantity) + ao.send({ + Target = msg.Sender, + Data = Colors.gray .. "Successfully credited " .. Colors.blue .. msg.Quantity .. Colors.gray .. " to subledger " .. Colors.green .. ao.id .. Colors.reset + }) + else + ao.send({ + Target = msg.Sender, + Action = 'Credit-Error', + ['Message-Id'] = msg.Id, + Error = 'Invalid Parent-Token!' + }) + end +end) + +Handlers.add('withdraw', Handlers.utils.hasMatchingTag('Action', 'Withdraw'), function(msg) + assert(type(ParentToken) == 'string', 'This process has no Parent') + assert(type(msg.Quantity) == 'string', 'Quantity is required!') + assert(bint(msg.Quantity) <= bint(Balances[msg.From]), 'Quantity must be less than or equal to the current balance!') + + Balances[msg.From] = utils.subtract(Balances[msg.From], msg.Quantity) + + local recipient = msg.From + if type(msg.Recipient) == 'string' then + recipient = msg.Recipient + end + + ao.send({ + Target = ParentToken, + Action = 'Transfer', + Recipient = recipient, + Quantity = msg.Quantity + }) +end) + +]===] + +return subledgerCode diff --git a/blueprints/token.lua b/blueprints/token.lua index ff4cde32..66f26c8b 100644 --- a/blueprints/token.lua +++ b/blueprints/token.lua @@ -1,5 +1,8 @@ local bint = require('.bint')(256) local ao = require('ao') + +local subledgerCode = require('subledger_code') + --[[ This module implements the ao Standard Token Specification. @@ -55,13 +58,28 @@ local utils = { -- Variant = "0.0.3" +-- token state should be assigned from the spawning message -- token should be idempotent and not change previous state updates -Denomination = Denomination or 12 -Balances = Balances or { [ao.id] = utils.toBalanceValue(10000 * 10 ^ Denomination) } -TotalSupply = TotalSupply or utils.toBalanceValue(10000 * 10 ^ Denomination) -Name = Name or 'Points Coin' -Ticker = Ticker or 'PNTS' -Logo = Logo or 'SBCCXwwecBlDqRLUjb8dYABExTJXLieawf7m2aBJ-KY' +Name = Name or ao.env.Process.Tags['Token-Name'] +Ticker = Ticker or ao.env.Process.Tags['Token-Ticker'] +Denomination = Denomination or tonumber(ao.env.Process.Tags['Token-Denomination']) or 12 +Logo = Logo or ao.env.Process.Tags['Token-Logo'] + +if not ao.env.Process.Tags['Parent-Token'] then + local initialSupply = tonumber(ao.env.Process.Tags['Token-Supply']) or 10000 + local initialBalance = utils.toBalanceValue(initialSupply * 10 ^ Denomination) + + Balances = Balances or { [ao.id] = initialBalance } + TotalSupply = TotalSupply or initialBalance +else + Balances = Balances or {} +end + +SourceToken = ao.env.Process.Tags['Source-Token'] or ao.id +ParentToken = ao.env.Process.Tags['Parent-Token'] + +Subledgers = Subledgers or {} +SubledgersPendingInit = SubledgersPendingInit or {} --[[ Add handlers for each incoming Action defined by the ao Standard Token Specification @@ -132,6 +150,14 @@ Handlers.add('transfer', Handlers.utils.hasMatchingTag('Action', 'Transfer'), fu Balances[msg.From] = utils.subtract(Balances[msg.From], msg.Quantity) Balances[msg.Recipient] = utils.add(Balances[msg.Recipient], msg.Quantity) + local SubledgerTags = {} + if Subledgers and Subledgers[msg.Recipient] then + SubledgerTags = { + ['Source-Token'] = SourceToken or ao.id, + ['Parent-Token'] = ParentToken, + } + end + --[[ Only send the notifications to the Sender and Recipient if the Cast tag is not set on the Transfer message @@ -168,6 +194,11 @@ Handlers.add('transfer', Handlers.utils.hasMatchingTag('Action', 'Transfer'), fu end end + -- Add SubledgerTags to the credit notice if applicable + for tagName, tagValue in pairs(SubledgerTags) do + creditNotice[tagName] = tagValue + end + -- Send Debit-Notice and Credit-Notice ao.send(debitNotice) ao.send(creditNotice) @@ -240,3 +271,113 @@ Handlers.add('burn', Handlers.utils.hasMatchingTag('Action', 'Burn'), function(m Data = Colors.gray .. "Successfully burned " .. Colors.blue .. msg.Quantity .. Colors.reset }) end) + +Handlers.add('creditNotice', Handlers.utils.hasMatchingTag('Action', 'Credit-Notice'), function(msg) + assert(type(msg.Quantity) == 'string', 'Quantity is required!') + assert(type(msg.Sender) == 'string', 'Sender is required!') + + if msg.From == ParentToken then + if not Balances[msg.Sender] then Balances[msg.Sender] = "0" end + Balances[msg.Sender] = utils.add(Balances[msg.Sender], msg.Quantity) + ao.send({ + Target = msg.Sender, + Data = Colors.gray .. + "Successfully credited " .. + Colors.blue .. msg.Quantity .. Colors.gray .. " to subledger " .. Colors.green .. ao.id .. Colors.reset + }) + else + ao.send({ + Target = msg.Sender, + Action = 'Credit-Error', + ['Message-Id'] = msg.Id, + Error = 'Invalid Parent-Token!' + }) + end +end) + +Handlers.add('withdraw', Handlers.utils.hasMatchingTag('Action', 'Withdraw'), function(msg) + assert(type(ParentToken) == 'string', 'This process has no Parent') + assert(type(msg.Quantity) == 'string', 'Quantity is required!') + assert(bint(msg.Quantity) <= bint(Balances[msg.From]), 'Quantity must be less than or equal to the current balance!') + + Balances[msg.From] = utils.subtract(Balances[msg.From], msg.Quantity) + + local recipient = msg.From + if type(msg.Recipient) == 'string' then + recipient = msg.Recipient + end + + ao.send({ + Target = ParentToken, + Action = 'Transfer', + Recipient = recipient, + Quantity = msg.Quantity + }) +end) + +-- Subledger specific handlers + +-- Spawning subledger process +Handlers.add('SpawnSubledger', Handlers.utils.hasMatchingTag('Action', 'Spawn-Subledger'), function(msg) + print("Spawning Subledger...") + + ao.spawn(ao.env.Module.Id, { + Data = "", + Tags = { + ['Source-Token'] = SourceToken or ao.id, + ['Parent-Token'] = ao.id, + ['Token-Name'] = Name, + ['Token-Ticker'] = Ticker, + ['Token-Logo'] = Logo, + ['Token-Denomination'] = tostring(Denomination), + ['Deployer'] = msg.From, + ['Original-Message-Id'] = msg.Id, + } + }) + + ao.send({ Target = msg.From, Action = "Spawning-Subledger", ['Original-Message-Id'] = msg.Id }) +end) + +Handlers.add("NotifySpawn", Handlers.utils.hasMatchingTag("Action", "Spawned"), function(msg) + local processId = msg.Tags['AO-Spawn-Success'] + local originalMessageId = msg.Tags['Original-Message-Id'] + local deployer = msg.Tags['Deployer'] + + table.insert(SubledgersPendingInit, { + processId = processId, + originalMessageId = originalMessageId, + deployer = deployer + }) + print("Spawned.") +end) + +Handlers.add("InitSubledgers", Handlers.utils.hasMatchingTag("Action", "Init-Subledgers"), function(msg) + local processIds = {} + for i, subledger in ipairs(SubledgersPendingInit) do + table.insert(processIds, subledger.processId) + end + assert(#processIds > 0, "No subledger to init") + ao.send({ + Target = ao.id, + Action = "Eval", + Data = subledgerCode, + Assignments = processIds + }) + + for i, subledger in ipairs(SubledgersPendingInit) do + ao.send({ + Target = subledger.deployer, + Action = "Subledger-Initialized", + ['Subledger-Id'] = subledger.processId, + ['Original-Message-Id'] = subledger.originalMessageId + }) + table.insert(Subledgers, { + processId = subledger.processId, + originalMessageId = subledger.originalMessageId, + deployer = subledger.deployer + }) + print("Initialized subledger " .. subledger.processId .. " for deployer " .. subledger.deployer) + end + SubledgersPendingInit = {} +end +)