diff --git a/process/ao.lua b/process/ao.lua new file mode 100644 index 00000000..faa61053 --- /dev/null +++ b/process/ao.lua @@ -0,0 +1,296 @@ +local oldao = ao or {} +local ao = { + _version = "0.0.6", + id = oldao.id or "", + _module = oldao._module or "", + authorities = oldao.authorities or {}, + reference = oldao.reference or 0, + outbox = oldao.outbox or + {Output = {}, Messages = {}, Spawns = {}, Assignments = {}}, + nonExtractableTags = { + 'Data-Protocol', 'Variant', 'From-Process', 'From-Module', 'Type', + 'From', 'Owner', 'Anchor', 'Target', 'Data', 'Tags' + }, + nonForwardableTags = { + 'Data-Protocol', 'Variant', 'From-Process', 'From-Module', 'Type', + 'From', 'Owner', 'Anchor', 'Target', 'Tags', 'TagArray', 'Hash-Chain', + 'Timestamp', 'Nonce', 'Epoch', 'Signature', 'Forwarded-By', + 'Pushed-For', 'Read-Only', 'Cron', 'Block-Height', 'Reference', 'Id', + 'Reply-To' + } +} + +local function _includes(list) + return function(key) + local exists = false + for _, listKey in ipairs(list) do + if key == listKey then + exists = true + break + end + end + if not exists then return false end + return true + end +end + +local function isArray(table) + if type(table) == "table" then + local maxIndex = 0 + for k, v in pairs(table) do + if type(k) ~= "number" or k < 1 or math.floor(k) ~= k then + return false -- If there's a non-integer key, it's not an array + end + maxIndex = math.max(maxIndex, k) + end + -- If the highest numeric index is equal to the number of elements, it's an array + return maxIndex == #table + end + return false +end + +local function padZero32(num) return string.format("%032d", num) end + +function ao.clone(obj, seen) + -- Handle non-tables and previously-seen tables. + if type(obj) ~= 'table' then return obj end + if seen and seen[obj] then return seen[obj] end + + -- New table; mark it as seen and copy recursively. + local s = seen or {} + local res = {} + s[obj] = res + for k, v in pairs(obj) do res[ao.clone(k, s)] = ao.clone(v, s) end + return setmetatable(res, getmetatable(obj)) +end + +function ao.normalize(msg) + for _, o in ipairs(msg.Tags) do + if not _includes(ao.nonExtractableTags)(o.name) then + msg[o.name] = o.value + end + end + return msg +end + +function ao.sanitize(msg) + local newMsg = ao.clone(msg) + + for k, _ in pairs(newMsg) do + if _includes(ao.nonForwardableTags)(k) then newMsg[k] = nil end + end + + return newMsg +end + +function ao.init(env) + if ao.id == "" then ao.id = env.Process.Id end + + if ao._module == "" then + for _, o in ipairs(env.Process.Tags) do + if o.name == "Module" then ao._module = o.value end + end + end + + if #ao.authorities < 1 then + for _, o in ipairs(env.Process.Tags) do + if o.name == "Authority" then + table.insert(ao.authorities, o.value) + end + end + end + + ao.outbox = {Output = {}, Messages = {}, Spawns = {}, Assignments = {}} + ao.env = env + +end + +function ao.log(txt) + if type(ao.outbox.Output) == 'string' then + ao.outbox.Output = {ao.outbox.Output} + end + table.insert(ao.outbox.Output, txt) +end + +-- clears outbox +function ao.clearOutbox() + ao.outbox = {Output = {}, Messages = {}, Spawns = {}, Assignments = {}} +end + +function ao.send(msg) + assert(type(msg) == 'table', 'msg should be a table') + ao.reference = ao.reference + 1 + local referenceString = tostring(ao.reference) + + local message = { + Target = msg.Target, + Data = msg.Data, + Anchor = padZero32(ao.reference), + Tags = { + {name = "Data-Protocol", value = "ao"}, + {name = "Variant", value = "ao.TN.1"}, + {name = "Type", value = "Message"}, + {name = "Reference", value = referenceString} + } + } + + -- if custom tags in root move them to tags + for k, v in pairs(msg) do + if not _includes({"Target", "Data", "Anchor", "Tags", "From"})(k) then + table.insert(message.Tags, {name = k, value = v}) + end + end + + if msg.Tags then + if isArray(msg.Tags) then + for _, o in ipairs(msg.Tags) do + table.insert(message.Tags, o) + end + else + for k, v in pairs(msg.Tags) do + table.insert(message.Tags, {name = k, value = v}) + end + end + end + + -- If running in an environment without the AOS Handlers module, do not add + -- the onReply and receive functions to the message. + if not Handlers then return message end + + -- clone message info and add to outbox + local extMessage = {} + for k, v in pairs(message) do extMessage[k] = v end + + -- add message to outbox + table.insert(ao.outbox.Messages, extMessage) + + -- add callback for onReply handler(s) + message.onReply = + function(...) -- Takes either (AddressThatWillReply, handler(s)) or (handler(s)) + local from, resolver + if select("#", ...) == 2 then + from = select(1, ...) + resolver = select(2, ...) + else + from = message.Target + resolver = select(1, ...) + end + + -- Add a one-time callback that runs the user's (matching) resolver on reply + Handlers.once({From = from, ["X-Reference"] = referenceString}, + resolver) + end + + message.receive = function(...) + local from = message.Target + if select("#", ...) == 1 then from = select(1, ...) end + return + Handlers.receive({From = from, ["X-Reference"] = referenceString}) + end + + return message +end + +function ao.spawn(module, msg) + assert(type(module) == "string", "Module source id is required!") + assert(type(msg) == 'table', 'Message must be a table') + -- inc spawn reference + ao.reference = ao.reference + 1 + local spawnRef = tostring(ao.reference) + + local spawn = { + Data = msg.Data or "NODATA", + Anchor = padZero32(ao.reference), + Tags = { + {name = "Data-Protocol", value = "ao"}, + {name = "Variant", value = "ao.TN.1"}, + {name = "Type", value = "Process"}, + {name = "From-Process", value = ao.id}, + {name = "From-Module", value = ao._module}, + {name = "Module", value = module}, + {name = "Reference", value = spawnRef} + } + } + + -- if custom tags in root move them to tags + for k, v in pairs(msg) do + if not _includes({"Target", "Data", "Anchor", "Tags", "From"})(k) then + table.insert(spawn.Tags, {name = k, value = v}) + end + end + + if msg.Tags then + if isArray(msg.Tags) then + for _, o in ipairs(msg.Tags) do + table.insert(spawn.Tags, o) + end + else + for k, v in pairs(msg.Tags) do + table.insert(spawn.Tags, {name = k, value = v}) + end + end + end + + -- If running in an environment without the AOS Handlers module, do not add + -- the after and receive functions to the spawn. + if not Handlers then return spawn end + + -- clone spawn info and add to outbox + local extSpawn = {} + for k, v in pairs(spawn) do extSpawn[k] = v end + + table.insert(ao.outbox.Spawns, extSpawn) + + -- add 'after' callback to returned table + -- local result = {} + spawn.onReply = function(callback) + Handlers.once({ + Action = "Spawned", + From = ao.id, + ["Reference"] = spawnRef + }, callback) + end + + spawn.receive = function() + return Handlers.receive({ + Action = "Spawned", + From = ao.id, + ["Reference"] = spawnRef + }) + + end + + return spawn +end + +function ao.assign(assignment) + assert(type(assignment) == 'table', 'assignment should be a table') + assert(type(assignment.Processes) == 'table', 'Processes should be a table') + assert(type(assignment.Message) == "string", "Message should be a string") + table.insert(ao.outbox.Assignments, assignment) +end + +-- The default security model of AOS processes: Trust all and *only* those +-- on the ao.authorities list. +function ao.isTrusted(msg) + for _, authority in ipairs(ao.authorities) do + if msg.From == authority then return true end + if msg.Owner == authority then return true end + end + return false +end + +function ao.result(result) + -- if error then only send the Error to CU + if ao.outbox.Error or result.Error then + return {Error = result.Error or ao.outbox.Error} + end + return { + Output = result.Output or ao.outbox.Output, + Messages = ao.outbox.Messages, + Spawns = ao.outbox.Spawns, + Assignments = ao.outbox.Assignments + } +end + +return ao diff --git a/process/handlers-utils.lua b/process/handlers-utils.lua index e3a84a51..156fbd61 100644 --- a/process/handlers-utils.lua +++ b/process/handlers-utils.lua @@ -1,7 +1,7 @@ local _utils = { _version = "0.0.1" } local _ = require('.utils') -local ao = require("ao") +local ao = require(".ao") function _utils.hasMatchingTag(name, value) assert(type(name) == 'string' and type(value) == 'string', 'invalid arguments: (name : string, value : string)') diff --git a/process/handlers.lua b/process/handlers.lua index 3642f407..6507a916 100644 --- a/process/handlers.lua +++ b/process/handlers.lua @@ -79,7 +79,25 @@ function handlers.once(...) handlers.add(name, pattern, handle, 1) end -function handlers.add(name, pattern, handle, maxRuns) +function handlers.add(...) + local name, pattern, handle, maxRuns + local args = select("#", ...) + if args == 2 then + name = select(1, ...) + pattern = select(1, ...) + handle = select(2, ...) + maxRuns = nil + elseif args == 3 then + name = select(1, ...) + pattern = select(2, ...) + handle = select(3, ...) + maxRuns = nil + else + name = select(1, ...) + pattern = select(2, ...) + handle = select(3, ...) + maxRuns = select(4, ...) + end assertAddArgs(name, pattern, handle, maxRuns) handle = handlers.generateResolver(handle) @@ -99,7 +117,25 @@ function handlers.add(name, pattern, handle, maxRuns) return #handlers.list end -function handlers.append(name, pattern, handle, maxRuns) +function handlers.append(...) + local name, pattern, handle, maxRuns + local args = select("#", ...) + if args == 2 then + name = select(1, ...) + pattern = select(1, ...) + handle = select(2, ...) + maxRuns = nil + elseif args == 3 then + name = select(1, ...) + pattern = select(2, ...) + handle = select(3, ...) + maxRuns = nil + else + name = select(1, ...) + pattern = select(2, ...) + handle = select(3, ...) + maxRuns = select(4, ...) + end assertAddArgs(name, pattern, handle, maxRuns) handle = handlers.generateResolver(handle) @@ -118,7 +154,25 @@ function handlers.append(name, pattern, handle, maxRuns) end -function handlers.prepend(name, pattern, handle, maxRuns) +function handlers.prepend(...) + local name, pattern, handle, maxRuns + local args = select("#", ...) + if args == 2 then + name = select(1, ...) + pattern = select(1, ...) + handle = select(2, ...) + maxRuns = nil + elseif args == 3 then + name = select(1, ...) + pattern = select(2, ...) + handle = select(3, ...) + maxRuns = nil + else + name = select(1, ...) + pattern = select(2, ...) + handle = select(3, ...) + maxRuns = select(4, ...) + end assertAddArgs(name, pattern, handle, maxRuns) handle = handlers.generateResolver(handle) diff --git a/process/process.lua b/process/process.lua index 8c57fa63..3daa9dca 100644 --- a/process/process.lua +++ b/process/process.lua @@ -20,25 +20,25 @@ Utils = require('.utils') Handlers = require('.handlers') local stringify = require(".stringify") local assignment = require('.assignment') -local _ao = require('ao') +ao = require('.ao') -- Implement assignable polyfills on _ao -assignment.init(_ao) +assignment.init(ao) local process = { _version = "2.0.0.rc1" } local maxInboxCount = 10000 -- wrap ao.send and ao.spawn for magic table -local aosend = _ao.send -local aospawn = _ao.spawn -_ao.send = function (msg) +local aosend = ao.send +local aospawn = ao.spawn +ao.send = function (msg) if msg.Data and type(msg.Data) == 'table' then msg['Content-Type'] = 'application/json' msg.Data = require('json').encode(msg.Data) end return aosend(msg) end -_ao.spawn = function (module, msg) +ao.spawn = function (module, msg) if msg.Data and type(msg.Data) == 'table' then msg['Content-Type'] = 'application/json' msg.Data = require('json').encode(msg.Data) @@ -104,10 +104,10 @@ function print(a) end local data = a - if _ao.outbox.Output.data then - data = _ao.outbox.Output.data .. "\n" .. a + if ao.outbox.Output.data then + data = ao.outbox.Output.data .. "\n" .. a end - _ao.outbox.Output = { data = data, prompt = Prompt(), print = true } + ao.outbox.Output = { data = data, prompt = Prompt(), print = true } -- Only supported for newer version of AOS if HANDLER_PRINT_LOGS then @@ -122,7 +122,7 @@ function Send(msg) if not msg.Target then print("WARN: No target specified for message. Data will be stored, but no process will receive it.") end - local result = _ao.send(msg) + local result = ao.send(msg) return { output = "Message added to outbox", receive = result.receive, @@ -135,7 +135,7 @@ function Spawn(...) if select("#", ...) == 1 then spawnMsg = select(1, ...) - module = _ao._module + module = ao._module else module = select(1, ...) spawnMsg = select(2, ...) @@ -144,7 +144,7 @@ function Spawn(...) if not spawnMsg then spawnMsg = {} end - local result = _ao.spawn(module, spawnMsg) + local result = ao.spawn(module, spawnMsg) return { output = "Spawn process request added to outbox", after = result.after, @@ -157,11 +157,11 @@ function Receive(match) end function Assign(assignment) - if not _ao.assign then + if not ao.assign then print("Assign is not implemented.") return "Assign is not implemented." end - _ao.assign(assignment) + ao.assign(assignment) print("Assignment added to outbox.") return 'Assignment added to outbox.' end @@ -226,7 +226,11 @@ function Version() print("version: " .. process._version) end -function process.handle(msg, ao) +function process.handle(msg, env) + ao.init(env) + -- relocate custom tags to root message + msg = ao.normalize(msg) + -- set process id ao.id = ao.env.Process.Id initializeState(msg, ao.env) HANDLER_PRINT_LOGS = {} diff --git a/process/test/handlers.test.js b/process/test/handlers.test.js index efcbd1a0..d20afa21 100644 --- a/process/test/handlers.test.js +++ b/process/test/handlers.test.js @@ -228,7 +228,6 @@ Handlers.add("one", print("one") end ) - Handlers.add("two", function (Msg) return "continue" @@ -246,7 +245,6 @@ Handlers.add("three", print("three") end ) - ` } // load handler @@ -309,4 +307,50 @@ Handlers.add("timestamp", const result = await handle(Memory, timestamp, env) assert.equal(result.Output.data, currentTimestamp) assert.ok(true) +}) + +test('test pattern, fn handler', async () => { + const handle = await AoLoader(wasm, options) + const env = { + Process: { + Id: 'AOS', + Owner: 'FOOBAR', + Tags: [ + { name: 'Name', value: 'Thomas' } + ] + } + } + const msg = { + Target: 'AOS', + From: 'FOOBAR', + Owner: 'FOOBAR', + ['Block-Height']: "1000", + Id: "1234xyxfoo", + Module: "WOOPAWOOPA", + Tags: [ + { name: 'Action', value: 'Eval' } + ], + Data: ` +Handlers.add("Balance", + function (msg) + msg.reply({Data = "1000"}) + end +) + ` + } + // load handler + const { Memory } = await handle(null, msg, env) + // --- + const currentTimestamp = Date.now(); + const balance = { + Target: 'AOS', + From: 'FRED', + Owner: 'FRED', + Tags: [{ name: 'Action', value: 'Balance' }], + Data: 'timestamp', + Timestamp: currentTimestamp + } + const result = await handle(Memory, balance, env) + assert.equal(result.Messages[0].Data, "1000") + assert.ok(true) }) \ No newline at end of file