diff --git a/.prettierrc b/.prettierrc index 13a0a37..310ac80 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,5 +3,6 @@ "semi": true, "printWidth": 80, "singleQuote": true, - "trailingComma": "all" + "trailingComma": "all", + "proseWrap": "always" } diff --git a/docs/adr/draft-ant-binary.md b/docs/adr/draft-ant-binary.md new file mode 100644 index 0000000..e4c7da4 --- /dev/null +++ b/docs/adr/draft-ant-binary.md @@ -0,0 +1,144 @@ +# Immutable code + Reassign ArNS as a solution for versioning ANTs + +- Status: draft +- Approvers: +- Date: [2024-12-10] +- Authors: Atticus + +## Context and Problem Statement + +For our ANT processes we current us AOS - an implementation of a lua repl in +lua - to manage the methods in user ANT processes thru the `eval` method to +update and modify code. + +When it comes to version this mutable process, we run into the issue of not +being able to trust the version of things in the process - unknown code can be +run on the process, even accidentally, or deliberately with unknown side +effects. + +This means for versioning we need to dryrun the API's to test what the API's +actually do when a non-breaking change happens - this is quite a lot of work +when it comes to understanding where in the versioning timeline an ANT stands +and it increases code complexity and maintenance for clients. + +Ideally we have a version number to operate off of to decide on upgrades. + +## Decision Drivers + +We need a way to upgrade ANT's that not just updates the code but also is easy +to understand what the capabilities are - versioning is the best way to do that +since we can know what each version does. There are other benefits to compiling +a ANT module as well. + +- Updates the ANT source code +- Accurately able to version the APIs + +## Considered Options + +- Continue using `eval` to update code and dry run each api to evaluate of the + inputs were accepted and outputs are valid +- Add a handler that accepts messages to run (dry run only and owner-only) then + validating each input/output + - Similar to the above but would allow one call with results on each api - + essentially this is a batch eval method that would reset ANT state on each + message it runs. +- Compile Custom ANT WASM Binary and spawn a new ANT, port over the current + ANT's state, then call Reassign-Name to transfer the connected arns name(s) to + the new ANT. + - note we can have a tag on the new ANT to indicate which ANT it was + previously. + +## Decision Outcome + + + +The proposed outcome here is to use a custom WASM module we compile specifically +for ANTs instead of using AOS and compiling lua code, with the following +workflow: + +1. Spawn New ANT with an initial ANT state + - This is only concerned with ANT, since no custom code would be run. +2. Reassign the ArNS Names(s) to the new ANT id. + +### Positive Consequences + +- Code is immutable and we do not need to worry about unknown code running on + the ANT. +- Because the code is immutable we can accurately version it - the first version + assigned will be its version forever. +- More control over dependencies (like aos) so we have a proper understanding of + the version of code we do not control. + - Context: AOS is our current binary we use and the code is COMPILED lua, + rather than eval'd lua. Due to how AOS updates (also thru the use of eval) + this further confused the abilities, since we not only have to worry about + our ANT version, but AOS version as well. +- We can leverage the `boot` method to initialize state and have the ANT + register itself on the ANT registry +- When buying names the workflow goes from a 4-5 step process (spawn, load lua, + init state (optional), register the ant, register name) to a 2 step process + (spawn ant, register name) +- Optimize the code further by compiling smaller memory footprints (since we + have literal thousands of these processes, even small memory footprints add + up) + - Note we can already do this if we custom-compile AOS and not use existing + versions, in fact most of these positive consequences could be achieved + without custom ANT code and simple compiling custom AOS first. But if we + were to do that we would need to have something like a git submodule, and + manage both our module ID and Lua code id. + +### Negative Consequences + +- Puts the onus on us for understanding how the full build process and + dependencies work (good and bad depending on perspective) +- Increases complexity of build time. +- Built code is opaque and requires more investigation to match versions with + code (our code is on github and you would need to go look at the code instead + there instead of in-place in the browser) + +## Pros and Cons of the Options + +[Compare the pros and cons of each considered option.] + +### [option 1] + +- `+` [pro 1] +- `+` [pro 2] +- `-` [con 1] +- `-` [con 2] + +### [option 2] + +- `+` [pro 1] +- `+` [pro 2] +- `-` [con 1] +- `-` [con 2] + +### [option 3] + +- `+` [pro 1] +- `+` [pro 2] +- `-` [con 1] +- `-` [con 2] + +## Links + +[Include any relevant links to documents, discussions, or other resources that +provide additional context or background information.] + +- [link 1](url) +- [link 2](url) + +## Related Decisions + +[List any related ADRs or decisions that are connected to this one.] + +- [ADR-1](1-example.md) - [Title of ADR-1] +- [ADR-2](2-example.md) - [Title of ADR-2] + +## Notes + +[Include any additional notes or comments that are relevant to the decision.] + +--- + +[ADR Template]: https://adr.github.io/ diff --git a/docs/adr/template.md b/docs/adr/template.md new file mode 100644 index 0000000..c00b408 --- /dev/null +++ b/docs/adr/template.md @@ -0,0 +1,97 @@ +# [short title of solved problem and solution] + +- Status: [proposed | rejected | accepted | deprecated | superseded by + [ADR-5](5-example.md)] +- Approvers: [list everyone involved in the decision] +- Date: [YYYY-MM-DD] +- Authors: [list of authors] + +## Context and Problem Statement + +[Describe the context and the problem that needs to be solved, capturing the +background and why this decision is necessary.] + +## Decision Drivers + +[Identify key factors that influence the decision, such as +requirements, constraints, and other considerations.] + +- [driver 1] +- [driver 2] +- [driver 3] + +## Considered Options + +[List the options that were considered to address the problem, providing a brief +overview of each.] + +- [option 1] +- [option 2] +- [option 3] + +## Decision Outcome + +[Describe the decision that was made, including why it was chosen over the +other options.] + +### Positive Consequences + +[Highlight the benefits and positive outcomes expected from this decision.] + +- [consequence 1] +- [consequence 2] + +### Negative Consequences + +[Identify any drawbacks or negative outcomes that might result from this decision.] + +- [consequence 1] +- [consequence 2] + +## Pros and Cons of the Options + +[Compare the pros and cons of each considered option.] + +### [option 1] + +- `+` [pro 1] +- `+` [pro 2] +- `-` [con 1] +- `-` [con 2] + +### [option 2] + +- `+` [pro 1] +- `+` [pro 2] +- `-` [con 1] +- `-` [con 2] + +### [option 3] + +- `+` [pro 1] +- `+` [pro 2] +- `-` [con 1] +- `-` [con 2] + +## Links + +[Include any relevant links to documents, discussions, or other resources that +provide additional context or background information.] + +- [link 1](url) +- [link 2](url) + +## Related Decisions + +[List any related ADRs or decisions that are connected to this one.] + +- [ADR-1](1-example.md) - [Title of ADR-1] +- [ADR-2](2-example.md) - [Title of ADR-2] + +## Notes + +[Include any additional notes or comments that are relevant to the decision.] + +--- + +[ADR Template]: https://adr.github.io/ diff --git a/package.json b/package.json index 43d5f1e..7c1f6f8 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "type": "module", "scripts": { - "module:build": "cd src && ao build && cd .. && yarn module:load", - "module:publish": "cd src && ao publish process.wasm -w ../tools/key.mjson --tag=\"Memory-Limit\" --value=\"1-gb\" --tag=\"Compute-Limit\" --value=\"9000000000000\" && cd ..", + "module:build": "cd src/common && ao build && mv process.wasm ../../tools/fixtures/process.wasm && cd ../..", + "module:publish": "ao publish tools/fixtures/process.wasm -w tools/key.json --tag=\"Memory-Limit\" --value=\"2-mb\" --tag=\"Compute-Limit\" --value=\"9000000000000\"", "module:load": "node tools/load-module.mjs", "module:spawn": "node tools/spawn-module.mjs", "lint": "luacheck .", @@ -10,7 +10,7 @@ "aos:publish": "node tools/bundle-aos.mjs && node tools/publish-aos.mjs", "aos:load": "node tools/bundle-aos.mjs && node tools/load-aos.mjs", "aos:spawn": "node tools/spawn-aos.mjs", - "test": "yarn aos:build && node --test --test-concurrency 1 --experimental-wasm-memory64 **/*.test.mjs", + "test": "node --test --test-concurrency 1 --experimental-wasm-memory64 test/balances.test.mjs", "prepare": "husky" }, "devDependencies": { diff --git a/spec/ant_spec.lua b/spec/ant_spec.lua index 8eb7eac..a01ba6b 100644 --- a/spec/ant_spec.lua +++ b/spec/ant_spec.lua @@ -2,7 +2,7 @@ local balances = require("src.common.balances") local controllers = require("src.common.controllers") local initialize = require("src.common.initialize") local records = require("src.common.records") -local json = require("src.common.json") +local json = require("src.common.deps.json") local fake_address = "1111111111111111111111111111111111111111111" diff --git a/src/common/balances.lua b/src/common/balances.lua index ed03bc7..ff8dac6 100644 --- a/src/common/balances.lua +++ b/src/common/balances.lua @@ -1,7 +1,7 @@ --- Module for managing balances and transactions. -- @module balances -local utils = require(".common.utils") +local utils = require(".utils") local balances = {} diff --git a/src/common/config.yml b/src/common/config.yml new file mode 100644 index 0000000..a692ca2 --- /dev/null +++ b/src/common/config.yml @@ -0,0 +1,3 @@ +stack_size: 262144 # 0.25 mb +initial_memory: 524288 # 0.5 mb +maximum_memory: 2097152 # 2 mb diff --git a/src/common/controllers.lua b/src/common/controllers.lua index 8d97e2a..9fb75c9 100644 --- a/src/common/controllers.lua +++ b/src/common/controllers.lua @@ -1,4 +1,4 @@ -local utils = require(".common.utils") +local utils = require(".utils") local controllers = {} diff --git a/src/common/crypto/init.lua b/src/common/crypto/init.lua deleted file mode 100644 index 0fe8134..0000000 --- a/src/common/crypto/init.lua +++ /dev/null @@ -1,10 +0,0 @@ -local util = require(".crypto.util.init") -local digest = require(".crypto.digest.init") - -local crypto = { - _version = "0.0.1", - digest = digest, - utils = util, -} - -return crypto diff --git a/src/common/deps/ao.lua b/src/common/deps/ao.lua new file mode 100644 index 0000000..298eb5d --- /dev/null +++ b/src/common/deps/ao.lua @@ -0,0 +1,406 @@ +--- The AO module provides functionality for managing the AO environment and handling messages. Returns the ao table. +-- @module ao + +local oldao = ao or {} + +--- The AO module +-- @table ao +-- @field _version The version number of the ao module +-- @field _module The module id of the process +-- @field id The id of the process +-- @field authorities A table of authorities of the process +-- @field reference The reference number of the process +-- @field outbox The outbox of the process +-- @field nonExtractableTags The non-extractable tags +-- @field nonForwardableTags The non-forwardable tags +-- @field clone The clone function +-- @field normalize The normalize function +-- @field sanitize The sanitize function +-- @field init The init function +-- @field log The log function +-- @field clearOutbox The clearOutbox function +-- @field send The send function +-- @field spawn The spawn function +-- @field assign The assign function +-- @field isTrusted The isTrusted function +-- @field result The result function +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' + } +} + +--- Checks if a key exists in a list. +-- @lfunction _includes +-- @tparam {table} list The list to check against +-- @treturn {function} A function that takes a key and returns true if the key exists in the list +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 + +--- Checks if a table is an array. +-- @lfunction isArray +-- @tparam {table} table The table to check +-- @treturn {boolean} True if the table is an array, false otherwise +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 + +--- Pads a number with leading zeros to 32 digits. +-- @lfunction padZero32 +-- @tparam {number} num The number to pad +-- @treturn {string} The padded number as a string +local function padZero32(num) return string.format("%032d", num) end + +--- Clones a table recursively. +-- @function clone +-- @tparam {any} obj The object to clone +-- @tparam {table} seen The table of seen objects (default is nil) +-- @treturn {any} The cloned object +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 + +--- Normalizes a message by extracting tags. +-- @function normalize +-- @tparam {table} msg The message to normalize +-- @treturn {table} The normalized message +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 + +--- Sanitizes a message by removing non-forwardable tags. +-- @function sanitize +-- @tparam {table} msg The message to sanitize +-- @treturn {table} The sanitized message +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 + +--- Initializes the AO environment, including ID, module, authorities, outbox, and environment. +-- @function init +-- @tparam {table} env The environment object +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 + +--- Logs a message to the output. +-- @function log +-- @tparam {string} txt The message to log +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 the outbox. +-- @function clearOutbox +function ao.clearOutbox() + ao.outbox = {Output = {}, Messages = {}, Spawns = {}, Assignments = {}} +end + +--- Sends a message. +-- @function send +-- @tparam {table} msg The message to send +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 + +--- Spawns a process. +-- @function spawn +-- @tparam {string} module The module source id +-- @tparam {table} msg The message to send +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 + +--- Assigns a message to a process. +-- @function assign +-- @tparam {table} assignment The assignment to assign +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 + +--- Checks if a message is trusted. +-- The default security model of AOS processes: Trust all and *only* those on the ao.authorities list. +-- @function isTrusted +-- @tparam {table} msg The message to check +-- @treturn {boolean} True if the message is trusted, false otherwise +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 + +--- Returns the result of the process. +-- @function result +-- @tparam {table} result The result of the process +-- @treturn {table} The result of the process, including Output, Messages, Spawns, and Assignments +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 + + +--- Add the MatchSpec to the ao.assignables table. A optional name may be provided. +-- This implies that ao.assignables may have both number and string indices. +-- Added in the assignment module. +-- @function addAssignable +-- @tparam ?string|number|any nameOrMatchSpec The name of the MatchSpec +-- to be added to ao.assignables. if a MatchSpec is provided, then +-- no name is included +-- @tparam ?any matchSpec The MatchSpec to be added to ao.assignables. Only provided +-- if its name is passed as the first parameter +-- @treturn ?string|number name The name of the MatchSpec, either as provided +-- as an argument or as incremented +-- @see assignment + +--- Remove the MatchSpec, either by name or by index +-- If the name is not found, or if the index does not exist, then do nothing. +-- Added in the assignment module. +-- @function removeAssignable +-- @tparam {string|number} name The name or index of the MatchSpec to be removed +-- @see assignment + +--- Return whether the msg is an assignment or not. This can be determined by simply check whether the msg's Target is this process' id +-- Added in the assignment module. +-- @function isAssignment +-- @param msg The msg to be checked +-- @treturn boolean isAssignment +-- @see assignment + +--- Check whether the msg matches any assignable MatchSpec. +-- If not assignables are configured, the msg is deemed not assignable, by default. +-- Added in the assignment module. +-- @function isAssignable +-- @param msg The msg to be checked +-- @treturn boolean isAssignable +-- @see assignment + +return ao diff --git a/src/common/deps/assignment.lua b/src/common/deps/assignment.lua new file mode 100644 index 0000000..66de2e5 --- /dev/null +++ b/src/common/deps/assignment.lua @@ -0,0 +1,186 @@ +--- The Assignment module provides functionality for handling assignments. Returns the Assignment table. +-- @module assignment + +--- The Assignment module +-- @table Assignment +-- @field _version The version number of the assignment module +-- @field init The init function +local Assignment = { _version = "0.1.0" } +--- Given a pattern, a value, and a message, returns whether there is a pattern match. +-- @usage matchesPattern(pattern, value, msg) +-- @param pattern The pattern to match +-- @param value The value to check for in the pattern +-- @param msg The message to check for the pattern +-- @treturn {boolean} Whether there is a pattern match +local function matchesPattern(pattern, value, msg) + -- If the key is not in the message, then it does not match + if not pattern then + return false + end + -- if the patternMatchSpec is a wildcard, then it always matches + if pattern == "_" then + return true + end + -- if the patternMatchSpec is a function, then it is executed on the tag value + if type(pattern) == "function" then + if pattern(value, msg) then + return true + else + return false + end + end + + -- if the patternMatchSpec is a string, check it for special symbols (less `-` alone) + -- and exact string match mode + if type(pattern) == "string" then + if string.match(pattern, "[%^%$%(%)%%%.%[%]%*%+%?]") then + if string.match(value, pattern) then + return true + end + else + if value == pattern then + return true + end + end + end + + -- if the pattern is a table, recursively check if any of its sub-patterns match + if type(pattern) == "table" then + for _, subPattern in pairs(pattern) do + if matchesPattern(subPattern, value, msg) then + return true + end + end + end + + return false +end + +--- Given a message and a spec, returns whether there is a spec match. +-- @usage matchesSpec(msg, spec) +-- @param msg The message to check for the spec +-- @param spec The spec to check for in the message +-- @treturn {boolean} Whether there is a spec match +local function matchesSpec(msg, spec) + if type(spec) == "function" then + return spec(msg) + -- If the spec is a table, step through every key/value pair in the pattern and check if the msg matches + -- Supported pattern types: + -- - Exact string match + -- - Lua gmatch string + -- - '_' (wildcard: Message has tag, but can be any value) + -- - Function execution on the tag, optionally using the msg as the second argument + -- - Table of patterns, where ANY of the sub-patterns matching the tag will result in a match + end + if type(spec) == "table" then + for key, pattern in pairs(spec) do + if not msg[key] then + return false + end + if not matchesPattern(pattern, msg[key], msg) then + return false + end + end + return true + end + if type(spec) == "string" and msg.Action and msg.Action == spec then + return true + end + return false +end + +--- Implement assignable polyfills on ao. +-- Creates addAssignable, removeAssignable, isAssignment, and isAssignable fields on ao. +-- @function init +-- @tparam {table} ao The ao environment object +-- @see ao.addAssignable +-- @see ao.removeAssignable +-- @see ao.isAssignment +-- @see ao.isAssignable +function Assignment.init(ao) + -- Find the index of an object in an array by a given property + -- @lfunction findIndexByProp + -- @tparam {table} array The array to search + -- @tparam {string} prop The property to search by + -- @tparam {any} value The value to search for + -- @treturn {number|nil} The index of the object, or nil if not found + local function findIndexByProp(array, prop, value) + for index, object in ipairs(array) do + if object[prop] == value then + return index + end + end + + return nil + end + + ao.assignables = ao.assignables or {} + + ao.addAssignable = ao.addAssignable + or function(...) + local name = nil + local matchSpec = nil + + local idx = nil + + -- Initialize the parameters based on arguments + if select("#", ...) == 1 then + matchSpec = select(1, ...) + else + name = select(1, ...) + matchSpec = select(2, ...) + assert(type(name) == "string", "MatchSpec name MUST be a string") + end + + if name then + idx = findIndexByProp(ao.assignables, "name", name) + end + + if idx ~= nil and idx > 0 then + -- found update + ao.assignables[idx].pattern = matchSpec + else + -- append the new assignable, including potentially nil name + table.insert(ao.assignables, { pattern = matchSpec, name = name }) + end + end + + ao.removeAssignable = ao.removeAssignable + or function(name) + local idx = nil + + if type(name) == "string" then + idx = findIndexByProp(ao.assignables, "name", name) + else + assert(type(name) == "number", "index MUST be a number") + idx = name + end + + if idx == nil or idx <= 0 or idx > #ao.assignables then + return + end + + table.remove(ao.assignables, idx) + end + + ao.isAssignment = ao.isAssignment or function(msg) + return msg.Target ~= ao.id + end + + ao.isAssignable = ao.isAssignable + or function(msg) + for _, assignable in pairs(ao.assignables) do + if matchesSpec(msg, assignable.pattern) then + return true + end + end + + -- If assignables is empty, the the above loop will noop, + -- and this expression will execute. + -- + -- In other words, all msgs are not assignable, by default. + return false + end +end + +return Assignment diff --git a/src/common/crypto/digest/init.lua b/src/common/deps/crypto/digest/init.lua similarity index 63% rename from src/common/crypto/digest/init.lua rename to src/common/deps/crypto/digest/init.lua index 56f6aa7..70c0b90 100644 --- a/src/common/crypto/digest/init.lua +++ b/src/common/deps/crypto/digest/init.lua @@ -1,4 +1,4 @@ -local SHA3 = require(".crypto.digest.sha3") +local SHA3 = require(".deps.crypto.digest.sha3") local digest = { _version = "0.0.1", diff --git a/src/common/crypto/digest/sha3.lua b/src/common/deps/crypto/digest/sha3.lua similarity index 98% rename from src/common/crypto/digest/sha3.lua rename to src/common/deps/crypto/digest/sha3.lua index bef4656..2534b1d 100644 --- a/src/common/crypto/digest/sha3.lua +++ b/src/common/deps/crypto/digest/sha3.lua @@ -1,4 +1,4 @@ -local Hex = require(".crypto.util.hex"); +local Hex = require(".deps.crypto.util.hex") local ROUNDS = 24 @@ -206,7 +206,7 @@ local function keccakHash(rate, length, data, algorithm) state.permuted = { {}, {}, {}, {}, {}, } state.parities = {0,0,0,0,0} absorb(state, data, algorithm) - local encoded = squeeze(state):sub(1,length/8); + local encoded = squeeze(state):sub(1,length/8) local public = {} diff --git a/src/common/deps/crypto/init.lua b/src/common/deps/crypto/init.lua new file mode 100644 index 0000000..43f0eda --- /dev/null +++ b/src/common/deps/crypto/init.lua @@ -0,0 +1,10 @@ +local util = require(".deps.crypto.util.init") +local digest = require(".deps.crypto.digest.init") + +local crypto = { + _version = "0.0.1", + digest = digest, + utils = util, +} + +return crypto diff --git a/src/common/crypto/util/hex.lua b/src/common/deps/crypto/util/hex.lua similarity index 100% rename from src/common/crypto/util/hex.lua rename to src/common/deps/crypto/util/hex.lua diff --git a/src/common/crypto/util/init.lua b/src/common/deps/crypto/util/init.lua similarity index 58% rename from src/common/crypto/util/init.lua rename to src/common/deps/crypto/util/init.lua index 829195e..bde9d5d 100644 --- a/src/common/crypto/util/init.lua +++ b/src/common/deps/crypto/util/init.lua @@ -1,4 +1,4 @@ -local Hex = require(".crypto.util.hex") +local Hex = require(".deps.crypto.util.hex") local util = { _version = "0.0.1", diff --git a/src/common/deps/handlers-utils.lua b/src/common/deps/handlers-utils.lua new file mode 100644 index 0000000..bb23c45 --- /dev/null +++ b/src/common/deps/handlers-utils.lua @@ -0,0 +1,121 @@ +--- The Handler Utils module is a lightweight Lua utility library designed to provide common functionalities for handling and processing messages within the AOS computer system. It offers a set of functions to check message attributes and send replies, simplifying the development of more complex scripts and modules. This document will guide you through the module's functionalities, installation, and usage. Returns the _utils table. +-- @module handlers-utils + +--- The _utils table +-- @table _utils +-- @field _version The version number of the _utils module +-- @field hasMatchingTag The hasMatchingTag function +-- @field hasMatchingTagOf The hasMatchingTagOf function +-- @field hasMatchingData The hasMatchingData function +-- @field reply The reply function +-- @field continue The continue function +local _utils = { _version = "0.0.2" } + +--- Given a message and a spec, returns whether there is a spec match. +-- @usage matchesSpec(msg, spec) +-- @param msg The message to check for the spec +-- @param spec The spec to check for in the message +-- @treturn {boolean} Whether there is a spec match +local function matchesSpec(msg, spec) + if type(spec) == "function" then + return spec(msg) + -- If the spec is a table, step through every key/value pair in the pattern and check if the msg matches + -- Supported pattern types: + -- - Exact string match + -- - Lua gmatch string + -- - '_' (wildcard: Message has tag, but can be any value) + -- - Function execution on the tag, optionally using the msg as the second argument + -- - Table of patterns, where ANY of the sub-patterns matching the tag will result in a match + end + if type(spec) == "table" then + for key, pattern in pairs(spec) do + if not msg[key] then + return false + end + if not matchesPattern(pattern, msg[key], msg) then + return false + end + end + return true + end + if type(spec) == "string" and msg.Action and msg.Action == spec then + return true + end + return false +end + +--- Checks if a given message has a tag that matches the specified name and value. +-- @function hasMatchingTag +-- @tparam {string} name The tag name to check +-- @tparam {string} value The value to match for in the tag +-- @treturn {function} A function that takes a message and returns whether there is a tag match (-1 if matches, 0 otherwise) +function _utils.hasMatchingTag(name, value) + assert(type(name) == "string" and type(value) == "string", "invalid arguments: (name : string, value : string)") + + return function(msg) + return msg.Tags[name] == value + end +end + +--- Checks if a given message has a tag that matches the specified name and one of the specified values. +-- @function hasMatchingTagOf +-- @tparam {string} name The tag name to check +-- @tparam {string[]} values The list of values of which one should match +-- @treturn {function} A function that takes a message and returns whether there is a tag match (-1 if matches, 0 otherwise) +function _utils.hasMatchingTagOf(name, values) + assert(type(name) == "string" and type(values) == "table", "invalid arguments: (name : string, values : string[])") + return function(msg) + for _, value in ipairs(values) do + local patternResult = Handlers.utils.hasMatchingTag(name, value)(msg) + + if patternResult ~= 0 and patternResult ~= false and patternResult ~= "skip" then + return patternResult + end + end + + return 0 + end +end + +--- Checks if a given message has data that matches the specified value. +-- @function hasMatchingData +-- @tparam {string} value The value to match against the message data +-- @treturn {function} A function that takes a message and returns whether the data matches the value (-1 if matches, 0 otherwise) +function _utils.hasMatchingData(value) + assert(type(value) == "string", "invalid arguments: (value : string)") + return function(msg) + return msg.Data == value + end +end + +--- Given an input, returns a function that takes a message and replies to it. +-- @function reply +-- @tparam {table | string} input The content to send back. If a string, it sends it as data. If a table, it assumes a structure with `Tags`. +-- @treturn {function} A function that takes a message and replies to it +function _utils.reply(input) + assert(type(input) == "table" or type(input) == "string", "invalid arguments: (input : table or string)") + return function(msg) + if type(input) == "string" then + msg.reply({ Data = input }) + return + end + msg.reply(input) + end +end + +--- Inverts the provided pattern's result if it matches, so that it continues execution with the next matching handler. +-- @function continue +-- @tparam {table | function} pattern The pattern to check for in the message +-- @treturn {function} Function that executes the pattern matching function and returns `1` (continue), so that the execution of handlers continues. +function _utils.continue(pattern) + return function(msg) + local match = matchesSpec(msg, pattern) + + if not match or match == 0 or match == "skip" then + return match + end + return 1 + end +end + +return _utils diff --git a/src/common/deps/handlers.lua b/src/common/deps/handlers.lua new file mode 100644 index 0000000..89aceef --- /dev/null +++ b/src/common/deps/handlers.lua @@ -0,0 +1,459 @@ +--- The Handlers library provides a flexible way to manage and execute a series of handlers based on patterns. Each handler consists of a pattern function, a handle function, and a name. This library is suitable for scenarios where different actions need to be taken based on varying input criteria. Returns the handlers table. +-- @module handlers + +--- The handlers table +-- @table handlers +-- @field _version The version number of the handlers module +-- @field list The list of handlers +-- @field coroutines The coroutines of the handlers +-- @field onceNonce The nonce for the once handlers +-- @field utils The handlers-utils module +-- @field generateResolver The generateResolver function +-- @field receive The receive function +-- @field once The once function +-- @field add The add function +-- @field append The append function +-- @field prepend The prepend function +-- @field remove The remove function +-- @field evaluate The evaluate function +local handlers = { _version = "0.0.5" } +local coroutine = require("coroutine") + +--- Given a pattern, a value, and a message, returns whether there is a pattern match. +-- @usage matchesPattern(pattern, value, msg) +-- @param pattern The pattern to match +-- @param value The value to check for in the pattern +-- @param msg The message to check for the pattern +-- @treturn {boolean} Whether there is a pattern match +local function matchesPattern(pattern, value, msg) + -- If the key is not in the message, then it does not match + if not pattern then + return false + end + -- if the patternMatchSpec is a wildcard, then it always matches + if pattern == "_" then + return true + end + -- if the patternMatchSpec is a function, then it is executed on the tag value + if type(pattern) == "function" then + if pattern(value, msg) then + return true + else + return false + end + end + + -- if the patternMatchSpec is a string, check it for special symbols (less `-` alone) + -- and exact string match mode + if type(pattern) == "string" then + if string.match(pattern, "[%^%$%(%)%%%.%[%]%*%+%?]") then + if string.match(value, pattern) then + return true + end + else + if value == pattern then + return true + end + end + end + + -- if the pattern is a table, recursively check if any of its sub-patterns match + if type(pattern) == "table" then + for _, subPattern in pairs(pattern) do + if matchesPattern(subPattern, value, msg) then + return true + end + end + end + + return false +end + +--- Given a message and a spec, returns whether there is a spec match. +-- @usage matchesSpec(msg, spec) +-- @param msg The message to check for the spec +-- @param spec The spec to check for in the message +-- @treturn {boolean} Whether there is a spec match +local function matchesSpec(msg, spec) + if type(spec) == "function" then + return spec(msg) + -- If the spec is a table, step through every key/value pair in the pattern and check if the msg matches + -- Supported pattern types: + -- - Exact string match + -- - Lua gmatch string + -- - '_' (wildcard: Message has tag, but can be any value) + -- - Function execution on the tag, optionally using the msg as the second argument + -- - Table of patterns, where ANY of the sub-patterns matching the tag will result in a match + end + if type(spec) == "table" then + for key, pattern in pairs(spec) do + if not msg[key] then + return false + end + if not matchesPattern(pattern, msg[key], msg) then + return false + end + end + return true + end + if type(spec) == "string" and msg.Action and msg.Action == spec then + return true + end + return false +end + +handlers.utils = require(".deps.handlers-utils") +-- if update we need to keep defined handlers +if Handlers then + handlers.list = Handlers.list or {} + handlers.coroutines = Handlers.coroutines or {} +else + handlers.list = {} + handlers.coroutines = {} +end +handlers.onceNonce = 0 + +--- Given an array, a property name, and a value, returns the index of the object in the array that has the property with the value. +-- @lfunction findIndexByProp +-- @tparam {table[]} array The array to search through +-- @tparam {string} prop The property name to check +-- @tparam {any} value The value to check for in the property +-- @treturn {number | nil} The index of the object in the array that has the property with the value, or nil if no such object is found +local function findIndexByProp(array, prop, value) + for index, object in ipairs(array) do + if object[prop] == value then + return index + end + end + return nil +end + +--- Given a name, a pattern, and a handle, asserts that the arguments are valid. +-- @lfunction assertAddArgs +-- @tparam {string} name The name of the handler +-- @tparam {table | function | string} pattern The pattern to check for in the message +-- @tparam {function} handle The function to call if the pattern matches +-- @tparam {number | string | nil} maxRuns The maximum number of times the handler should run, or nil if there is no limit +local function assertAddArgs(name, pattern, handle, maxRuns) + assert( + type(name) == "string" + and (type(pattern) == "function" or type(pattern) == "table" or type(pattern) == "string"), + "Invalid arguments given. Expected: \n" + .. "\tname : string, " + .. "\tpattern : Action : string | MsgMatch : table,\n" + .. "\t\tfunction(msg: Message) : {-1 = break, 0 = skip, 1 = continue},\n" + .. "\thandle(msg : Message) : void) | Resolver,\n" + .. '\tMaxRuns? : number | "inf" | nil' + ) +end + +--- Given a resolver specification, returns a resolver function. +-- @function generateResolver +-- @tparam {table | function} resolveSpec The resolver specification +-- @treturn {function} A resolver function +function handlers.generateResolver(resolveSpec) + return function(msg) + -- If the resolver is a single function, call it. + -- Else, find the first matching pattern (by its matchSpec), and exec. + if type(resolveSpec) == "function" then + return resolveSpec(msg) + else + for matchSpec, func in pairs(resolveSpec) do + if matchesSpec(msg, matchSpec) then + return func(msg) + end + end + end + end +end + +--- Given a pattern, returns the next message that matches the pattern. +-- This function uses Lua's coroutines under-the-hood to add a handler, pause, +-- and then resume the current coroutine. This allows us to effectively block +-- processing of one message until another is received that matches the pattern. +-- @function receive +-- @tparam {table | function} pattern The pattern to check for in the message +function handlers.receive(pattern) + local self = coroutine.running() + handlers.once(pattern, function(msg) + -- If the result of the resumed coroutine is an error then we should bubble it up to the process + local _, success, errmsg = coroutine.resume(self, msg) + if not success then + error(errmsg) + end + end) + return coroutine.yield(pattern) +end + +--- Given a name, a pattern, and a handle, adds a handler to the list. +-- If name is not provided, "_once_" prefix plus onceNonce will be used as the name. +-- Adds handler with maxRuns of 1 such that it will only be called once then removed from the list. +-- @function once +-- @tparam {string} name The name of the handler +-- @tparam {table | function | string} pattern The pattern to check for in the message +-- @tparam {function} handle The function to call if the pattern matches +function handlers.once(...) + local name, pattern, handle + if select("#", ...) == 3 then + name = select(1, ...) + pattern = select(2, ...) + handle = select(3, ...) + else + name = "_once_" .. tostring(handlers.onceNonce) + handlers.onceNonce = handlers.onceNonce + 1 + pattern = select(1, ...) + handle = select(2, ...) + end + handlers.prepend(name, pattern, handle, 1) +end + +--- Given a name, a pattern, and a handle, adds a handler to the list. +-- @function add +-- @tparam {string} name The name of the handler +-- @tparam {table | function | string} pattern The pattern to check for in the message +-- @tparam {function} handle The function to call if the pattern matches +-- @tparam {number | string | nil} maxRuns The maximum number of times the handler should run, or nil if there is no limit +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) + + -- update existing handler by name + local idx = findIndexByProp(handlers.list, "name", name) + if idx ~= nil and idx > 0 then + -- found update + handlers.list[idx].pattern = pattern + handlers.list[idx].handle = handle + handlers.list[idx].maxRuns = maxRuns + else + -- not found then add + table.insert(handlers.list, { pattern = pattern, handle = handle, name = name, maxRuns = maxRuns }) + end + return #handlers.list +end + +--- Appends a new handler to the end of the handlers list. +-- @function append +-- @tparam {string} name The name of the handler +-- @tparam {table | function | string} pattern The pattern to check for in the message +-- @tparam {function} handle The function to call if the pattern matches +-- @tparam {number | string | nil} maxRuns The maximum number of times the handler should run, or nil if there is no limit +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) + -- update existing handler by name + local idx = findIndexByProp(handlers.list, "name", name) + if idx ~= nil and idx > 0 then + -- found update + handlers.list[idx].pattern = pattern + handlers.list[idx].handle = handle + handlers.list[idx].maxRuns = maxRuns + else + table.insert(handlers.list, { pattern = pattern, handle = handle, name = name, maxRuns = maxRuns }) + end +end + +--- Prepends a new handler to the beginning of the handlers list. +-- @function prepend +-- @tparam {string} name The name of the handler +-- @tparam {table | function | string} pattern The pattern to check for in the message +-- @tparam {function} handle The function to call if the pattern matches +-- @tparam {number | string | nil} maxRuns The maximum number of times the handler should run, or nil if there is no limit +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) + + -- update existing handler by name + local idx = findIndexByProp(handlers.list, "name", name) + if idx ~= nil and idx > 0 then + -- found update + handlers.list[idx].pattern = pattern + handlers.list[idx].handle = handle + handlers.list[idx].maxRuns = maxRuns + else + table.insert(handlers.list, 1, { pattern = pattern, handle = handle, name = name, maxRuns = maxRuns }) + end +end + +--- Returns an object that allows adding a new handler before a specified handler. +-- @function before +-- @tparam {string} handleName The name of the handler before which the new handler will be added +-- @treturn {table} An object with an `add` method to insert the new handler +function handlers.before(handleName) + assert(type(handleName) == "string", "Handler name MUST be a string") + + local idx = findIndexByProp(handlers.list, "name", handleName) + return { + add = function(name, pattern, handle, maxRuns) + assertAddArgs(name, pattern, handle, maxRuns) + + handle = handlers.generateResolver(handle) + + if idx then + table.insert(handlers.list, idx, { pattern = pattern, handle = handle, name = name, maxRuns = maxRuns }) + end + end, + } +end + +--- Returns an object that allows adding a new handler after a specified handler. +-- @function after +-- @tparam {string} handleName The name of the handler after which the new handler will be added +-- @treturn {table} An object with an `add` method to insert the new handler +function handlers.after(handleName) + assert(type(handleName) == "string", "Handler name MUST be a string") + local idx = findIndexByProp(handlers.list, "name", handleName) + return { + add = function(name, pattern, handle, maxRuns) + assertAddArgs(name, pattern, handle, maxRuns) + + handle = handlers.generateResolver(handle) + + if idx then + table.insert( + handlers.list, + idx + 1, + { pattern = pattern, handle = handle, name = name, maxRuns = maxRuns } + ) + end + end, + } +end + +--- Removes a handler from the handlers list by name. +-- @function remove +-- @tparam {string} name The name of the handler to be removed +function handlers.remove(name) + assert(type(name) == "string", "name MUST be string") + if #handlers.list == 1 and handlers.list[1].name == name then + handlers.list = {} + end + + local idx = findIndexByProp(handlers.list, "name", name) + if idx ~= nil and idx > 0 then + table.remove(handlers.list, idx) + end +end + +--- Evaluates each handler against a given message and environment. Handlers are called in the order they appear in the handlers list. +-- Return 0 to not call handler, -1 to break after handler is called, 1 to continue +-- @function evaluate +-- @tparam {table} msg The message to be processed by the handlers. +-- @tparam {table} env The environment in which the handlers are executed. +-- @treturn The response from the handler(s). Returns a default message if no handler matches. +function handlers.evaluate(msg, env) + local handled = false + assert(type(msg) == "table", "msg is not valid") + assert(type(env) == "table", "env is not valid") + + for _, o in ipairs(handlers.list) do + if o.name ~= "_default" then + local match = matchesSpec(msg, o.pattern) + if not (type(match) == "number" or type(match) == "string" or type(match) == "boolean") then + error("Pattern result is not valid, it MUST be string, number, or boolean") + end + + -- handle boolean returns + if type(match) == "boolean" and match == true then + match = -1 + elseif type(match) == "boolean" and match == false then + match = 0 + end + + -- handle string returns + if type(match) == "string" then + if match == "continue" then + match = 1 + elseif match == "break" then + match = -1 + else + match = 0 + end + end + + if match ~= 0 then + if match < 0 then + handled = true + end + -- each handle function can accept, the msg, env + local status, err = pcall(o.handle, msg, env) + if not status then + error(err) + end + -- remove handler if maxRuns is reached. maxRuns can be either a number or "inf" + if o.maxRuns ~= nil and o.maxRuns ~= "inf" then + o.maxRuns = o.maxRuns - 1 + if o.maxRuns == 0 then + handlers.remove(o.name) + end + end + end + if match < 0 then + return handled + end + end + end + -- do default + if not handled then + local idx = findIndexByProp(handlers.list, "name", "_default") + handlers.list[idx].handle(msg, env) + end +end + +return handlers diff --git a/src/common/json.lua b/src/common/deps/json.lua similarity index 100% rename from src/common/json.lua rename to src/common/deps/json.lua diff --git a/src/common/handlers.lua b/src/common/handlers.lua deleted file mode 100644 index d41c9fb..0000000 --- a/src/common/handlers.lua +++ /dev/null @@ -1,190 +0,0 @@ -local handlers = { _version = "0.0.3" } - -handlers.list = {} - -local function findIndexByProp(array, prop, value) - for index, object in ipairs(array) do - if object[prop] == value then - return index - end - end - return nil -end - -function handlers.add(name, pattern, handle) - assert( - type(name) == "string" and type(pattern) == "function" and type(handle) == "function", - "invalid arguments: handler.add(name : string, pattern : function(msg: Message) : {-1 = break, 0 = skip, 1 = continue}, handle(msg : Message) : void)" - ) - assert(type(name) == "string", "name MUST be string") - assert(type(pattern) == "function", "pattern MUST be function") - assert(type(handle) == "function", "handle MUST be function") - - -- update existing handler by name - local idx = findIndexByProp(handlers.list, "name", name) - if idx ~= nil and idx > 0 then - -- found update - handlers.list[idx].pattern = pattern - handlers.list[idx].handle = handle - else - -- not found then add - table.insert(handlers.list, { pattern = pattern, handle = handle, name = name }) - end -end - -function handlers.append(name, pattern, handle) - assert( - type(name) == "string" and type(pattern) == "function" and type(handle) == "function", - "invalid arguments: handler.append(name : string, pattern : function(msg: Message) : {-1 = break, 0 = skip, 1 = continue}, handle(msg : Message) : void)" - ) - assert(type(name) == "string", "name MUST be string") - assert(type(pattern) == "function", "pattern MUST be function") - assert(type(handle) == "function", "handle MUST be function") - - -- update existing handler by name - local idx = findIndexByProp(handlers.list, "name", name) - if idx ~= nil and idx > 0 then - -- found update - handlers.list[idx].pattern = pattern - handlers.list[idx].handle = handle - else - table.insert(handlers.list, { pattern = pattern, handle = handle, name = name }) - end -end - -function handlers.prepend(name, pattern, handle) - assert( - type(name) == "string" and type(pattern) == "function" and type(handle) == "function", - "invalid arguments: handler.prepend(name : string, pattern : function(msg: Message) : {-1 = break, 0 = skip, 1 = continue}, handle(msg : Message) : void)" - ) - assert(type(name) == "string", "name MUST be string") - assert(type(pattern) == "function", "pattern MUST be function") - assert(type(handle) == "function", "handle MUST be function") - - -- update existing handler by name - local idx = findIndexByProp(handlers.list, "name", name) - if idx ~= nil and idx > 0 then - -- found update - handlers.list[idx].pattern = pattern - handlers.list[idx].handle = handle - else - table.insert(handlers.list, 1, { pattern = pattern, handle = handle, name = name }) - end -end - -function handlers.before(handleName) - assert( - handleName ~= nil, - "invalid arguments: handlers.before(name : string) : { add = function(name, pattern, handler)}" - ) - assert(type(handleName) == "string", "name MUST be string") - - local idx = findIndexByProp(handlers.list, "name", handleName) - return { - add = function(name, pattern, handle) - assert( - type(name) == "string" and type(pattern) == "function" and type(handle) == "function", - 'invalid arguments: handler.before("foo").add(name : string, pattern : function(msg: Message) : {-1 = break, 0 = skip, 1 = continue}, handle(msg : Message) : void)' - ) - assert(type(name) == "string", "name MUST be string") - - assert(type(pattern) == "function", "pattern MUST be function") - assert(type(handle) == "function", "handle MUST be function") - - if idx then - table.insert(handlers.list, idx, { pattern = pattern, handle = handle, name = name }) - end - end, - } -end - -function handlers.after(handleName) - assert( - handleName ~= nil, - "invalid arguments: handlers.after(name : string) : { add = function(name, pattern, handler)}" - ) - assert(type(handleName) == "string", "name MUST be string") - local idx = findIndexByProp(handlers.list, "name", handleName) - return { - add = function(name, pattern, handle) - assert( - type(name) == "string" and type(pattern) == "function" and type(handle) == "function", - 'invalid arguments: handler.after("foo").add(name : string, pattern : function(msg: Message) : {-1 = break, 0 = skip, 1 = continue}, handle(msg : Message) : void)' - ) - - assert(type(name) == "string", "name MUST be string") - assert(type(pattern) == "function", "pattern MUST be function") - assert(type(handle) == "function", "handle MUST be function") - - if idx then - table.insert(handlers.list, idx + 1, { pattern = pattern, handle = handle, name = name }) - end - end, - } -end - -function handlers.remove(name) - assert(type(name) == "string", "name MUST be string") - if #handlers.list == 1 and handlers.list[1].name == name then - handlers.list = {} - end - - local idx = findIndexByProp(handlers.list, "name", name) - table.remove(handlers.list, idx) -end - ---- return 0 to not call handler, -1 to break after handler is called, 1 to continue -function handlers.evaluate(msg, env) - local handled = false - assert(type(msg) == "table", "msg is not valid") - assert(type(env) == "table", "env is not valid") - - for _, o in ipairs(handlers.list) do - if o.name ~= "_default" then - local match = o.pattern(msg) - if not (type(match) == "number" or type(match) == "string" or type(match) == "boolean") then - error({ message = "pattern result is not valid, it MUST be string, number, or boolean" }) - end - - -- handle boolean returns - if type(match) == "boolean" and match == true then - match = -1 - elseif type(match) == "boolean" and match == false then - match = 0 - end - - -- handle string returns - if type(match) == "string" then - if match == "continue" then - match = 1 - elseif match == "break" then - match = -1 - else - match = 0 - end - end - - if match ~= 0 then - if match < 0 then - handled = true - end - -- each handle function can accept, the msg, env - local status, err = pcall(o.handle, msg, env) - if not status then - error(err) - ao.outbox.Error = { err = err } - end - end - if match < 0 then - return handled - end - end - end - -- do default - if not handled then - local idx = findIndexByProp(handlers.list, "name", "_default") - handlers.list[idx].handle(msg, env) - end -end - -return handlers diff --git a/src/common/initialize.lua b/src/common/initialize.lua index 855c71e..e332f6c 100644 --- a/src/common/initialize.lua +++ b/src/common/initialize.lua @@ -1,5 +1,5 @@ -local utils = require(".common.utils") -local json = require(".common.json") +local utils = require(".utils") +local json = require(".deps.json") local initialize = {} function initialize.initializeANTState(state) diff --git a/src/common/main.lua b/src/common/main.lua index e3c1812..c31f7cf 100644 --- a/src/common/main.lua +++ b/src/common/main.lua @@ -2,17 +2,16 @@ local ant = {} function ant.init() -- main.lua - -- utils - local json = require(".common.json") - local utils = require(".common.utils") - local notices = require(".common.notices") + local json = require(".deps.json") + local utils = require(".utils") + local notices = require(".notices") local createActionHandler = utils.createActionHandler -- spec modules - local balances = require(".common.balances") - local initialize = require(".common.initialize") - local records = require(".common.records") - local controllers = require(".common.controllers") + local balances = require(".balances") + local initialize = require(".initialize") + local records = require(".records") + local controllers = require(".controllers") ---@alias Owner string ---@description The owner of the ANT diff --git a/src/common/notices.lua b/src/common/notices.lua index 0e32921..cea4892 100644 --- a/src/common/notices.lua +++ b/src/common/notices.lua @@ -1,4 +1,4 @@ -local json = require("json") +local json = require(".deps.json") local notices = {} --- @param oldMsg AoMessage diff --git a/src/common/process.lua b/src/common/process.lua new file mode 100644 index 0000000..c766fb5 --- /dev/null +++ b/src/common/process.lua @@ -0,0 +1,234 @@ +--- The Process library provides an environment for managing and executing processes on the AO network. It includes capabilities for handling messages, spawning processes, and customizing the environment with programmable logic and handlers. Returns the process table. +-- @module process + +-- @dependencies +Handlers = require(".deps.handlers") +ao = require(".deps.ao") +utils = require(".utils") +local coroutine = require("coroutine") +local json = require(".deps.json") +local assignment = require(".deps.assignment") + +local ant = require(".main") + +-- Implement assignable polyfills on _ao +assignment.init(ao) + +--- The process table +-- @table process +-- @field _version The version number of the process +local process = { _version = "0.0.1" } + +-- wrap ao.send for magic table +local aosend = ao.send + +ao.send = function(msg) + if msg.Data and type(msg.Data) == "table" then + msg["Content-Type"] = "application/json" + msg.Data = json.encode(msg.Data) + end + return aosend(msg) +end + +--- Convert a message's tags to a table of key-value pairs +-- @function Tab +-- @tparam {table} msg The message containing tags +-- @treturn {table} A table with tag names as keys and their values +function Tab(msg) + local inputs = {} + for _, o in ipairs(msg.Tags) do + if not inputs[o.name] then + inputs[o.name] = o.value + end + end + return inputs +end + +--- Print a value, formatting tables and converting non-string types +-- @function print +-- @tparam {any} a The value to print +function print(a) + if type(a) == "table" then + a = json.encode(a) + end + + if + type(a) == "boolean" + or type(a) == "nil" + or type(a) == "number" + or type(a) == "function" + or type(a) == "thread" + then + a = tostring(a) + end + + local data = a + if ao.outbox.Output.data then + data = ao.outbox.Output.data .. "\n" .. a + end + ao.outbox.Output = { data = data } + + -- Only supported for newer version of AOS + if HANDLER_PRINT_LOGS then + table.insert(HANDLER_PRINT_LOGS, a) + return nil + end + + return tostring(a) +end + +--- Send a message to a target process +-- @function Send +-- @tparam {table} msg The message to send +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) + return { + output = "Message added to outbox", + receive = result.receive, + onReply = result.onReply, + } +end + +--- Main handler for processing incoming messages. It initializes the state, processes commands, and handles message evaluation and inbox management. +-- @function handle +-- @tparam {table} msg The message to handle +-- @tparam {table} _ The environment to handle the message in +function process.handle(msg, _) + local env = nil + if _.Process then + env = _ + else + env = _.env + end + + ao.init(env) + -- relocate custom tags to root message + msg = ao.normalize(msg) + -- set process id + ao.id = ao.env.Process.Id + + HANDLER_PRINT_LOGS = {} + + -- set os.time to return msg.Timestamp + os.time = function() + return msg.Timestamp + end + + -- tagify msg + msg.TagArray = msg.Tags + msg.Tags = Tab(msg) + -- tagify Process + ao.env.Process.TagArray = ao.env.Process.Tags + ao.env.Process.Tags = Tab(ao.env.Process) + -- magic table - if Content-Type == application/json - decode msg.Data to a Table + if msg.Tags["Content-Type"] and msg.Tags["Content-Type"] == "application/json" then + msg.Data = json.decode(msg.Data or "{}") + end + -- init Errors + Errors = Errors or {} + -- clear Outbox + ao.clearOutbox() + + -- Only trust messages from a signed owner or an Authority + if msg.From ~= msg.Owner and not ao.isTrusted(msg) then + if msg.From ~= ao.id then + Send({ Target = msg.From, Data = "Message is not trusted by this process!" }) + end + print("Message is not trusted! From: " .. msg.From .. " - Owner: " .. msg.Owner) + return ao.result({}) + end + + if ao.isAssignment(msg) and not ao.isAssignable(msg) then + if msg.From ~= ao.id then + Send({ Target = msg.From, Data = "Assignment is not trusted by this process!" }) + end + print("Assignment is not trusted! From: " .. msg.From .. " - Owner: " .. msg.Owner) + return ao.result({}) + end + --- Mount the ANT handlers + ant.init() + + Handlers.append("_default", function() + return true + end, function(m) + m.reply({ + Action = "Default-Notice", + Data = "No Handler found", + Version = process._version, + }) + end) + + -- call evaluate from handlers passing env + msg.reply = function(replyMsg) + replyMsg.Target = msg["Reply-To"] or (replyMsg.Target or msg.From) + replyMsg["X-Reference"] = msg["X-Reference"] or msg.Reference + replyMsg["X-Origin"] = msg["X-Origin"] or nil + + return ao.send(replyMsg) + end + + msg.forward = function(target, forwardMsg) + -- Clone the message and add forwardMsg tags + local newMsg = ao.sanitize(msg) + forwardMsg = forwardMsg or {} + + for k, v in pairs(forwardMsg) do + newMsg[k] = v + end + + -- Set forward-specific tags + newMsg.Target = target + newMsg["Reply-To"] = msg["Reply-To"] or msg.From + newMsg["X-Reference"] = msg["X-Reference"] or msg.Reference + newMsg["X-Origin"] = msg["X-Origin"] or msg.From + -- clear functions + newMsg.reply = nil + newMsg.forward = nil + + ao.send(newMsg) + end + + local co = coroutine.create(function() + return pcall(Handlers.evaluate, msg, env) + end) + local _, status, result = coroutine.resume(co) + + -- Make sure we have a reference to the coroutine if it will wake up. + -- Simultaneously, prune any dead coroutines so that they can be + -- freed by the garbage collector. + table.insert(Handlers.coroutines, co) + for i, x in ipairs(Handlers.coroutines) do + if coroutine.status(x) == "dead" then + table.remove(Handlers.coroutines, i) + end + end + + if not status then + local printData = table.concat(HANDLER_PRINT_LOGS, "\n") + return ao.result({ + Error = printData .. "\n" .. result, + Messages = {}, + Spawns = {}, + Assignments = {}, + }) + end + + if msg.Tags.Type == "Process" and Owner == msg.From then + local response = ao.result({ + Output = { + data = table.concat(HANDLER_PRINT_LOGS, "\n"), + }, + }) + HANDLER_PRINT_LOGS = {} -- clear logs + return response + else + local response = ao.result({ Output = { data = table.concat(HANDLER_PRINT_LOGS, "\n") } }) + HANDLER_PRINT_LOGS = {} -- clear logs + return response + end +end + +return process diff --git a/src/common/records.lua b/src/common/records.lua index 6191794..7af788d 100644 --- a/src/common/records.lua +++ b/src/common/records.lua @@ -1,4 +1,4 @@ -local utils = require(".common.utils") +local utils = require(".utils") local records = {} -- defaults to landing page txid Records = Records or { ["@"] = { transactionId = "-k7t8xMoB8hW482609Z9F4bTFMC3MnuW8bTvTyT8pFI", ttlSeconds = 3600 } } diff --git a/src/common/utils.lua b/src/common/utils.lua index a619844..3a83666 100644 --- a/src/common/utils.lua +++ b/src/common/utils.lua @@ -1,8 +1,8 @@ -- the majority of this file came from https://github.com/permaweb/aos/blob/main/process/utils.lua -local crypto = require(".common.crypto.init") -local constants = require(".common.constants") -local json = require(".common.json") -local notices = require(".common.notices") +local crypto = require(".deps.crypto.init") +local constants = require(".constants") +local json = require(".deps.json") +local notices = require(".notices") local utils = { _version = "0.0.1" } --- @param t table diff --git a/src/process.lua b/src/process.lua deleted file mode 100644 index 63325db..0000000 --- a/src/process.lua +++ /dev/null @@ -1,93 +0,0 @@ --- lib -Handlers = Handlers or require(".common.handlers") -local json = require(".common.json") -local initialize = require(".common.initialize") -local _ao = require("ao") - -local ant = require(".common.main") - -local process = { _version = "0.0.1" } --- wrap ao.send and ao.spawn for magic table -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 = json.encode(msg.Data) - end - return aosend(msg) -end -_ao.spawn = function(module, msg) - if msg.Data and type(msg.Data) == "table" then - msg["Content-Type"] = "application/json" - msg.Data = json.encode(msg.Data) - end - return aospawn(module, msg) -end - -function Send(msg) - _ao.send(msg) - return "message added to outbox" -end -function Tab(msg) - local inputs = {} - for _, o in ipairs(msg.Tags) do - if not inputs[o.name] then - inputs[o.name] = o.value - end - end - return inputs -end - -function process.handle(msg, ao) - ao.id = ao.env.Process.Id - initialize.initializeProcessState(msg, ao.env) - - -- tagify msg - msg.TagArray = msg.Tags - msg.Tags = Tab(msg) - -- tagify Process - ao.env.Process.TagArray = ao.env.Process.Tags - ao.env.Process.Tags = Tab(ao.env.Process) - -- magic table - if Content-Type == application/json - decode msg.Data to a Table - if msg.Tags["Content-Type"] and msg.Tags["Content-Type"] == "application/json" then - msg.Data = json.decode(msg.Data or "{}") - end - -- init Errors - Errors = Errors or {} - -- clear Outbox - ao.clearOutbox() - - -- Only trust messages from a signed owner or an Authority - -- skip this check for test messages in dev - if msg.From ~= msg.Owner and not ao.isTrusted(msg) then - Send({ Target = msg.From, Data = "Message is not trusted by this process!" }) - print("Message is not trusted! From: " .. msg.From .. " - Owner: " .. msg.Owner) - return ao.result({}) - end - - -- initialize the ANT handlers - ant.init() - - local status, result = pcall(Handlers.evaluate, msg, ao.env) - - if not status then - table.insert(Errors, result) - return { Error = result } - -- return { - -- Output = { - -- data = { - -- prompt = Prompt(), - -- json = 'undefined', - -- output = result - -- } - -- }, - -- Messages = {}, - -- Spawns = {} - -- } - end - - return ao.result({}) -end - -return process diff --git a/test/utils.mjs b/test/utils.mjs index 35c0364..6337392 100644 --- a/test/utils.mjs +++ b/test/utils.mjs @@ -19,6 +19,8 @@ export async function createAosLoader(params) { AO_LOADER_HANDLER_ENV, ); + console.dir(evalRes, { depth: null }); + return { handle, memory: evalRes.Memory, diff --git a/tools/constants.mjs b/tools/constants.mjs index 1efc702..8a7e6e1 100644 --- a/tools/constants.mjs +++ b/tools/constants.mjs @@ -12,7 +12,7 @@ export const STUB_ANT_REGISTRY_ID = 'ant-registry-'.padEnd(43, '1'); /* ao READ-ONLY Env Variables */ export const AO_LOADER_HANDLER_ENV = { Process: { - Id: STUB_PROCESS_ID, + Id: STUB_ADDRESS, Owner: STUB_ADDRESS, Tags: [ { name: 'Authority', value: 'XXXXXX' }, @@ -37,7 +37,8 @@ export const AO_LOADER_OPTIONS = { export const AOS_WASM = fs.readFileSync( path.join( __dirname, - 'fixtures/aos-cbn0KKrBZH7hdNkNokuXLtGryrWM--PjSTBqIzw9Kkk.wasm', + // 'fixtures/aos-cbn0KKrBZH7hdNkNokuXLtGryrWM--PjSTBqIzw9Kkk.wasm', + 'fixtures/process.wasm', ), ); @@ -68,7 +69,7 @@ export const DEFAULT_HANDLE_OPTIONS = { // important to set the address so that that `Authority` check passes. Else the `isTrusted` with throw an error. Owner: STUB_ADDRESS, Module: 'ANT', - Target: ''.padEnd(43, '1'), + Target: STUB_ADDRESS, From: STUB_ADDRESS, Timestamp: Date.now(), }; diff --git a/tools/spawn-module.mjs b/tools/spawn-module.mjs index 6e355ae..532b3da 100644 --- a/tools/spawn-module.mjs +++ b/tools/spawn-module.mjs @@ -2,6 +2,10 @@ import { connect, createDataItemSigner } from '@permaweb/aoconnect'; import fs from 'fs'; import path from 'path'; import Arweave from 'arweave'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); const arweave = Arweave.init({ host: 'arweave.net', @@ -12,7 +16,7 @@ const arweave = Arweave.init({ const ao = connect({ GATEWAY_URL: 'https://arweave.net', }); -const moduleId = 'ZUEIijxJlV3UgZS9c7to5cgW5EhyPdAndHqVZxig7vE'; +const moduleId = 'jKPXUTVEjn-CIvTXvoRA9yibc1BjAZbC4tuxQjZBxS0'; const scheduler = '_GQ33BkPtZrqxA84vM8Zk-N2aO0toNNu_C-l-rawrBA'; // with logo process: dcodF0DbVdzvRPE5nDTULn4aagHYruKnW3ulFkPkQC0 @@ -21,14 +25,12 @@ async function main() { const address = await arweave.wallets.jwkToAddress(JSON.parse(wallet)); const signer = createDataItemSigner(JSON.parse(wallet)); - // const processId = await ao.spawn({ - // module: moduleId, - // scheduler, - // signer, - // }); - //const processId = 'AxHXaiKg7c4FAYZ5eo4OPAaEhmB0I0PRxLqzW6ZNHXk'; - // aos process - const processId = 'YD1XXiKJq-R-ruODJk7u_c5dMtZEVsV_Nh687ZmSvDQ'; + const processId = await ao.spawn({ + module: moduleId, + scheduler, + signer, + }); + //--------------- console.log('Process ID:', processId); console.log('Waiting 20 seconds to ensure process is readied.');