diff --git a/.luarc.json b/.luarc.json index 3814fec..4d44823 100644 --- a/.luarc.json +++ b/.luarc.json @@ -5,18 +5,14 @@ "path": ["?.lua", "?/init.lua"] }, "workspace": { - "library": ["src", "spec"] + "library": ["src", "spec", + "${addons}/busted/module/library", + "${3rd}/luassert/library"] }, "diagnostics": { "enable": true, - "globals": [ - "describe", - "it", - "before_each", - "after_each", - "after_all", - "before_all" - ] + "globals": [] } - } + }, + "workspace.checkThirdParty": false } diff --git a/spec/arns_spec.lua b/spec/arns_spec.lua index bc0aabc..522341b 100644 --- a/spec/arns_spec.lua +++ b/spec/arns_spec.lua @@ -36,10 +36,8 @@ describe("arns", function() function() local demandBefore = demand.getCurrentPeriodRevenue() local purchasesBefore = demand.getCurrentPeriodPurchases() - local status, result = - pcall(arns.buyRecord, "test-name", "lease", 1, testAddress, timestamp, testProcessId) + local result = arns.buyRecord("test-name", "lease", 1, testAddress, timestamp, testProcessId) - assert.is_true(status) assert.are.same({ purchasePrice = 600000000, type = "lease", @@ -448,10 +446,7 @@ describe("arns", function() -- Reassign the name local newProcessId = "test-this-is-valid-arweave-wallet-address-2" - local status, result = pcall(arns.reassignName, "test-name", testProcessId, timestamp, newProcessId) - - -- Assertions - assert.is_true(status) + local result = arns.reassignName("test-name", testProcessId, timestamp, newProcessId) assert.are.same(newProcessId, result.processId) end) @@ -819,6 +814,7 @@ describe("arns", function() it("should create an auction and remove any existing record", function() local auction = arns.createAuction("test-name", 1000000, "test-initiator") local twoWeeksMs = 1000 * 60 * 60 * 24 * 14 + assert(auction, "Auction should be created") assert.are.equal(auction.name, "test-name") assert.are.equal(auction.startTimestamp, 1000000) assert.are.equal(auction.endTimestamp, twoWeeksMs + 1000000) -- 14 days late @@ -868,6 +864,7 @@ describe("arns", function() it("should return the correct price for an auction at a given timestamp permanently", function() local startTimestamp = 1000000 local auction = arns.createAuction("test-name", startTimestamp, "test-initiator") + assert(auction, "Auction should be created") local currentTimestamp = startTimestamp + 1000 * 60 * 60 * 24 * 7 -- 1 week into the auction local decayRate = 0.02037911 / (1000 * 60 * 60 * 24 * 14) local scalingExponent = 190 @@ -890,6 +887,7 @@ describe("arns", function() it("should return the correct prices for an auction with for a lease", function() local startTimestamp = 1729524023521 local auction = arns.createAuction("test-name", startTimestamp, "test-initiator") + assert(auction, "Auction should be created") local intervalMs = 1000 * 60 * 15 -- 15 min (how granular we want to compute the prices) local prices = auction:computePricesForAuction("lease", 1, intervalMs) local baseFee = 500000000 @@ -1019,6 +1017,7 @@ describe("arns", function() local floorPrice = baseFee + permabuyAnnualFee local startPrice = floorPrice * 50 local auction = arns.createAuction("test-name", startTimestamp, "test-initiator") + assert(auction, "Auction should be created") local result = arns.submitAuctionBid( "test-name", startPrice, @@ -1026,7 +1025,7 @@ describe("arns", function() bidTimestamp, "test-process-id", "permabuy", - nil + 0 ) local totalDecay = auction.settings.decayRate * (bidTimestamp - startTimestamp) local expectedPrice = math.floor(startPrice * ((1 - totalDecay) ^ auction.settings.scalingExponent)) @@ -1068,6 +1067,7 @@ describe("arns", function() it("should throw an error if the bid is not high enough", function() local startTimestamp = 1000000 local auction = arns.createAuction("test-name", startTimestamp, "test-initiator") + assert(auction, "Auction should be created") local startPrice = auction:getPriceForAuctionAtTimestamp(startTimestamp, "permabuy", nil) local status, error = pcall( arns.submitAuctionBid, @@ -1086,6 +1086,7 @@ describe("arns", function() it("should throw an error if the bidder does not have enough balance", function() local startTimestamp = 1000000 local auction = arns.createAuction("test-name", startTimestamp, "test-initiator") + assert(auction, "Auction should be created") local requiredBid = auction:getPriceForAuctionAtTimestamp(startTimestamp, "permabuy", nil) _G.Balances[testAddressArweave] = requiredBid - 1 local status, error = pcall( diff --git a/spec/vaults_spec.lua b/spec/vaults_spec.lua index 96db86a..b0c76d1 100644 --- a/spec/vaults_spec.lua +++ b/spec/vaults_spec.lua @@ -96,25 +96,25 @@ describe("vaults", function() _G.Balances["test-this-is-valid-arweave-wallet-address-1"] = 200 local vaultOwner = "test-this-is-valid-arweave-wallet-address-1" local lockLengthMs = constants.MIN_TOKEN_LOCK_TIME_MS - local msgId = "msgId" - local vault = vaults.createVault(vaultOwner, 100, lockLengthMs, startTimestamp, msgId) + local vaultId = "msgId" + local vault = vaults.createVault(vaultOwner, 100, lockLengthMs, startTimestamp, vaultId) local increaseAmount = 100 local currentTimestamp = vault.startTimestamp + 1000 - local increasedVault = vaults.increaseVault(vaultOwner, increaseAmount, msgId, currentTimestamp) - assert.are.same(vault.balance + increaseAmount, increasedVault.balance) - assert.are.same(vault.startTimestamp, increasedVault.startTimestamp) - assert.are.same(vault.endTimestamp, increasedVault.endTimestamp) + local increasedVault = vaults.increaseVault(vaultOwner, increaseAmount, vaultId, currentTimestamp) + assert.are.same(100 + increaseAmount, increasedVault.balance) + assert.are.same(startTimestamp, increasedVault.startTimestamp) + assert.are.same(startTimestamp + lockLengthMs, increasedVault.endTimestamp) end) it("should throw an error if insufficient balance", function() _G.Balances["test-this-is-valid-arweave-wallet-address-1"] = 100 local vaultOwner = "test-this-is-valid-arweave-wallet-address-1" local lockLengthMs = constants.MIN_TOKEN_LOCK_TIME_MS - local msgId = "msgId" - local vault = vaults.createVault(vaultOwner, 100, lockLengthMs, startTimestamp, msgId) + local vaultId = "msgId" + local vault = vaults.createVault(vaultOwner, 100, lockLengthMs, startTimestamp, vaultId) local increaseAmount = 101 local currentTimestamp = vault.startTimestamp + 1000 - local status, result = pcall(vaults.increaseVault, vaultOwner, increaseAmount, msgId, currentTimestamp) + local status, result = pcall(vaults.increaseVault, vaultOwner, increaseAmount, vaultId, currentTimestamp) assert.is_false(status) assert.match("Insufficient balance", result) end) @@ -123,13 +123,13 @@ describe("vaults", function() _G.Balances["test-this-is-valid-arweave-wallet-address-1"] = 200 local vaultOwner = "test-this-is-valid-arweave-wallet-address-1" local lockLengthMs = constants.MIN_TOKEN_LOCK_TIME_MS - local msgId = "msgId" - local vault = vaults.createVault(vaultOwner, 100, lockLengthMs, startTimestamp, msgId) + local vaultId = "msgId" + local vault = vaults.createVault(vaultOwner, 100, lockLengthMs, startTimestamp, vaultId) local increaseAmount = 100 local currentTimestamp = vault.endTimestamp + 1 - local status, result = pcall(vaults.increaseVault, vaultOwner, increaseAmount, msgId, currentTimestamp) + local status, result = pcall(vaults.increaseVault, vaultOwner, increaseAmount, vaultId, currentTimestamp) assert.is_false(status) - assert.match("This vault has ended.", result) + assert.match("Vault has ended.", result) end) it("should throw an error if the vault not found", function() @@ -146,38 +146,38 @@ describe("vaults", function() _G.Balances["test-this-is-valid-arweave-wallet-address-1"] = 100 local vaultOwner = "test-this-is-valid-arweave-wallet-address-1" local lockLengthMs = constants.MIN_TOKEN_LOCK_TIME_MS - local msgId = "msgId" - local vault = vaults.createVault(vaultOwner, 100, lockLengthMs, startTimestamp, msgId) + local vaultId = "msgId" + local vault = vaults.createVault(vaultOwner, 100, lockLengthMs, startTimestamp, vaultId) local extendLengthMs = constants.MIN_TOKEN_LOCK_TIME_MS local currentTimestamp = vault.startTimestamp + 1000 - local extendedVault = vaults.extendVault(vaultOwner, extendLengthMs, currentTimestamp, msgId) - assert.are.same(vault.balance, extendedVault.balance) - assert.are.same(vault.startTimestamp, extendedVault.startTimestamp) - assert.are.same(vault.endTimestamp + extendLengthMs, extendedVault.endTimestamp) + local extendedVault = vaults.extendVault(vaultOwner, extendLengthMs, currentTimestamp, vaultId) + assert.are.same(100, extendedVault.balance) + assert.are.same(startTimestamp, extendedVault.startTimestamp) + assert.are.same(startTimestamp + lockLengthMs + extendLengthMs, extendedVault.endTimestamp) end) it("should throw an error if the vault is expired", function() _G.Balances["test-this-is-valid-arweave-wallet-address-1"] = 100 local vaultOwner = "test-this-is-valid-arweave-wallet-address-1" local lockLengthMs = constants.MIN_TOKEN_LOCK_TIME_MS - local msgId = "msgId" - local vault = vaults.createVault(vaultOwner, 100, lockLengthMs, startTimestamp, msgId) + local vaultId = "msgId" + local vault = vaults.createVault(vaultOwner, 100, lockLengthMs, startTimestamp, vaultId) local extendLengthMs = constants.MIN_TOKEN_LOCK_TIME_MS local currentTimestamp = vault.endTimestamp + 1000 - local status, result = pcall(vaults.extendVault, vaultOwner, extendLengthMs, currentTimestamp, msgId) + local status, result = pcall(vaults.extendVault, vaultOwner, extendLengthMs, currentTimestamp, vaultId) assert.is_false(status) - assert.match("This vault has ended.", result) + assert.match("Vault has ended.", result) end) it("should throw an error if the lock length would be larger than the maximum", function() _G.Balances["test-this-is-valid-arweave-wallet-address-1"] = 100 local vaultOwner = "test-this-is-valid-arweave-wallet-address-1" local lockLengthMs = constants.MAX_TOKEN_LOCK_TIME_MS - local msgId = "msgId" - local vault = vaults.createVault(vaultOwner, 100, lockLengthMs, startTimestamp, msgId) + local vaultId = "msgId" + local vault = vaults.createVault(vaultOwner, 100, lockLengthMs, startTimestamp, vaultId) local extendLengthMs = constants.MAX_TOKEN_LOCK_TIME_MS + 1 local currentTimestamp = vault.startTimestamp + 1000 - local status, result = pcall(vaults.extendVault, vaultOwner, extendLengthMs, currentTimestamp, msgId) + local status, result = pcall(vaults.extendVault, vaultOwner, extendLengthMs, currentTimestamp, vaultId) assert.is_false(status) assert.match( "Invalid vault extension. Total lock time cannot be greater than " @@ -199,11 +199,11 @@ describe("vaults", function() _G.Balances["test-this-is-valid-arweave-wallet-address-1"] = 100 local vaultOwner = "test-this-is-valid-arweave-wallet-address-1" local lockLengthMs = constants.MIN_TOKEN_LOCK_TIME_MS - local msgId = "msgId" - local vault = vaults.createVault(vaultOwner, 100, lockLengthMs, startTimestamp, msgId) + local vaultId = "msgId" + local vault = vaults.createVault(vaultOwner, 100, lockLengthMs, startTimestamp, vaultId) local extendLengthMs = 0 local currentTimestamp = vault.startTimestamp + 1000 - local status, result = pcall(vaults.extendVault, vaultOwner, extendLengthMs, currentTimestamp, msgId) + local status, result = pcall(vaults.extendVault, vaultOwner, extendLengthMs, currentTimestamp, vaultId) assert.is_false(status) assert.match("Invalid extend length. Must be a positive number.", result) end) @@ -218,11 +218,8 @@ describe("vaults", function() local quantity = 50 local lockLengthMs = constants.MIN_TOKEN_LOCK_TIME_MS local timestamp = 1000000 - local msgId = "msgId" - local result, vault = - pcall(vaults.vaultedTransfer, from, recipient, quantity, lockLengthMs, timestamp, msgId) - assert.is_true(result) - assert.is_not_nil(vault) + local vaultId = "msgId" + local vault = vaults.vaultedTransfer(from, recipient, quantity, lockLengthMs, timestamp, vaultId) assert.are.equal(50, _G.Balances[from]) assert.are.same({ balance = 50, @@ -239,9 +236,9 @@ describe("vaults", function() local quantity = 150 local lockLengthMs = constants.MIN_TOKEN_LOCK_TIME_MS local timestamp = 1000000 - local msgId = "msgId" + local vaultId = "msgId" local status, result = - pcall(vaults.vaultedTransfer, from, recipient, quantity, lockLengthMs, timestamp, msgId) + pcall(vaults.vaultedTransfer, from, recipient, quantity, lockLengthMs, timestamp, vaultId) assert.is_false(status) assert.match("Insufficient balance", result) end) @@ -254,9 +251,10 @@ describe("vaults", function() local quantity = 50 local lockLengthMs = constants.MIN_TOKEN_LOCK_TIME_MS - 1 local timestamp = 1000000 - local msgId = "msgId" - local status = pcall(vaults.vaultedTransfer, from, recipient, quantity, lockLengthMs, timestamp, msgId) + local vaultId = "msgId" + local status = pcall(vaults.vaultedTransfer, from, recipient, quantity, lockLengthMs, timestamp, vaultId) assert.is_false(status) + -- the string fails to match because the error message is not exactly as expected end) end) diff --git a/src/arns.lua b/src/arns.lua index 8d61294..3910570 100644 --- a/src/arns.lua +++ b/src/arns.lua @@ -12,41 +12,40 @@ NameRegistry = NameRegistry or { auctions = {}, } +--- Buys a record +--- @param name string The name of the record +--- @param purchaseType string The purchase type (lease/permabuy) +--- @param years number|nil The number of years +--- @param from string The address of the sender +--- @param timestamp number The current timestamp +--- @param processId string The process id +--- @return table The updated record function arns.buyRecord(name, purchaseType, years, from, timestamp, processId) - -- don't catch, let the caller handle the error arns.assertValidBuyRecord(name, years, purchaseType, processId) if purchaseType == nil then purchaseType = "lease" -- set to lease by default end - if years == nil and purchaseType == "lease" then + if not years and purchaseType == "lease" then years = 1 -- set to 1 year by default end + local numYears = purchaseType == "lease" and (years or 1) or 0 local baseRegistrationFee = demand.baseFeeForNameLength(#name) local totalRegistrationFee = - arns.calculateRegistrationFee(purchaseType, baseRegistrationFee, years, demand.getDemandFactor()) + arns.calculateRegistrationFee(purchaseType, baseRegistrationFee, numYears, demand.getDemandFactor()) - if balances.getBalance(from) < totalRegistrationFee then - error("Insufficient balance") - end + assert(balances.getBalance(from) >= totalRegistrationFee, "Insufficient balance") local record = arns.getRecord(name) local isPermabuy = record ~= nil and record.type == "permabuy" local isActiveLease = record ~= nil and (record.endTimestamp or 0) + constants.gracePeriodMs > timestamp - if isPermabuy or isActiveLease then - error("Name is already registered") - end + assert(not isPermabuy and not isActiveLease, "Name is already registered") - if arns.getReservedName(name) and arns.getReservedName(name).target ~= from then - error("Name is reserved") - end - - if arns.getAuction(name) then - error("Name is in auction") - end + assert(not arns.getReservedName(name) or arns.getReservedName(name).target == from, "Name is reserved") + assert(not arns.getAuction(name), "Name is in auction") local newRecord = { processId = processId, @@ -97,6 +96,7 @@ end function arns.extendLease(from, name, years, currentTimestamp) local record = arns.getRecord(name) + assert(record, "Name is not registered") -- throw error if invalid arns.assertValidExtendLease(record, currentTimestamp, years) local baseRegistrationFee = demand.baseFeeForNameLength(#name) @@ -173,10 +173,17 @@ function arns.increaseundernameLimit(from, name, qty, currentTimestamp) } end +--- Gets a record +--- @param name string The name of the record +--- @return table|nil The a deep copy of the record or nil if it does not exist function arns.getRecord(name) return utils.deepCopy(NameRegistry.records[name]) end +--- Gets the active ARNS names between two timestamps +--- @param startTimestamp number The start timestamp +--- @param endTimestamp number The end timestamp +--- @return table The active ARNS names between the two timestamps function arns.getActiveArNSNamesBetweenTimestamps(startTimestamp, endTimestamp) local records = arns.getRecords() local activeNames = {} @@ -197,84 +204,124 @@ function arns.getActiveArNSNamesBetweenTimestamps(startTimestamp, endTimestamp) return activeNames end +--- Gets all records +--- @return table The a deep copy of the records table function arns.getRecords() local records = utils.deepCopy(NameRegistry.records) return records or {} end +--- Gets all reserved names +--- @return table The a deep copy of the reserved names table function arns.getReservedNames() local reserved = utils.deepCopy(NameRegistry.reserved) return reserved or {} end +--- Gets a reserved name +--- @param name string The name of the reserved record +--- @return table|nil The a deep copy of the reserved name or nil if it does not exist function arns.getReservedName(name) return utils.deepCopy(NameRegistry.reserved[name]) end +--- Modifies the undername limit for a record +--- @param name string The name of the record +--- @param qty number The quantity to increase the undername limit by +--- @return table The updated record function arns.modifyRecordundernameLimit(name, qty) local record = arns.getRecord(name) - if not record then - error("Name is not registered") - end - + assert(record, "Name is not registered") NameRegistry.records[name].undernameLimit = record.undernameLimit + qty return arns.getRecord(name) end -function arns.modifyRecordEndTimestamp(name, newEndTimestamp) +--- Modifies the process id for a record +--- @param name string The name of the record +--- @param processId string The new process id +--- @return table The updated record +function arns.modifyProcessId(name, processId) local record = arns.getRecord(name) - if not record then - error("Name is not registered") - end - - -- if new end timestamp + existing timetamp is > 5 years throw error - if newEndTimestamp > record.startTimestamp + constants.maxLeaseLengthYears * constants.oneYearMs then - error("Cannot extend lease beyond 5 years") - end + assert(record, "Name is not registered") + NameRegistry.records[name].processId = processId + return arns.getRecord(name) +end +--- Modifies the end timestamp for a record +--- @param name string The name of the record +--- @param newEndTimestamp number The new end timestamp +--- @return table The updated record +function arns.modifyRecordEndTimestamp(name, newEndTimestamp) + local record = arns.getRecord(name) + assert(record, "Name is not registered") + local maxLeaseLength = constants.maxLeaseLengthYears * constants.oneYearMs + local maxEndTimestamp = record.startTimestamp + maxLeaseLength + assert(newEndTimestamp <= maxEndTimestamp, "Cannot extend lease beyond 5 years") NameRegistry.records[name].endTimestamp = newEndTimestamp + return arns.getRecord(name) end --- internal functions +---Calculates the lease fee for a given base fee, years, and demand factor +--- @param baseFee number The base fee for the name +--- @param years number The number of years +--- @param demandFactor number The demand factor +--- @return number The lease fee function arns.calculateLeaseFee(baseFee, years, demandFactor) local annualRegistrationFee = arns.calculateAnnualRenewalFee(baseFee, years) local totalLeaseCost = baseFee + annualRegistrationFee return math.floor(demandFactor * totalLeaseCost) end +---Calculates the annual renewal fee for a given base fee and years +--- @param baseFee number The base fee for the name +--- @param years number The number of years +--- @return number The annual renewal fee function arns.calculateAnnualRenewalFee(baseFee, years) local totalAnnualRenewalCost = baseFee * constants.ANNUAL_PERCENTAGE_FEE * years return math.floor(totalAnnualRenewalCost) end +---Calculates the permabuy fee for a given base fee and demand factor +--- @param baseFee number The base fee for the name +--- @param demandFactor number The demand factor +--- @return number The permabuy fee function arns.calculatePermabuyFee(baseFee, demandFactor) local permabuyPrice = baseFee + arns.calculateAnnualRenewalFee(baseFee, constants.PERMABUY_LEASE_FEE_LENGTH) return math.floor(demandFactor * permabuyPrice) end +---Calculates the registration fee for a given purchase type, base fee, years, and demand factor +--- @param purchaseType string The purchase type (lease/permabuy) +--- @param baseFee number The base fee for the name +--- @param years number The number of years, may be empty for permabuy +--- @param demandFactor number The demand factor +--- @return number The registration fee function arns.calculateRegistrationFee(purchaseType, baseFee, years, demandFactor) - if purchaseType == "lease" then - return arns.calculateLeaseFee(baseFee, years, demandFactor) - elseif purchaseType == "permabuy" then - return arns.calculatePermabuyFee(baseFee, demandFactor) - end -end - + assert(purchaseType == "lease" or purchaseType == "permabuy", "Invalid purchase type") + local registrationFee = purchaseType == "lease" and arns.calculateLeaseFee(baseFee, years, demandFactor) + or arns.calculatePermabuyFee(baseFee, demandFactor) + return registrationFee +end + +---Calculates the undername cost for a given base fee, increase quantity, registration type, years, and demand factor +--- @param baseFee number The base fee for the name +--- @param increaseQty number The increase quantity +--- @param registrationType string The registration type (lease/permabuy) +--- @param years number The number of years +--- @param demandFactor number The demand factor +--- @return number The undername cost function arns.calculateUndernameCost(baseFee, increaseQty, registrationType, years, demandFactor) - local undernamePercentageFee = 0 - if registrationType == "lease" then - undernamePercentageFee = constants.UNDERNAME_LEASE_FEE_PERCENTAGE - elseif registrationType == "permabuy" then - undernamePercentageFee = constants.UNDERNAME_PERMABUY_FEE_PERCENTAGE - else - error("Invalid registration type") - end - + assert(registrationType == "lease" or registrationType == "permabuy", "Invalid registration type") + local undernamePercentageFee = registrationType == "lease" and constants.UNDERNAME_LEASE_FEE_PERCENTAGE + or constants.UNDERNAME_PERMABUY_FEE_PERCENTAGE local totalFeeForQtyAndYears = baseFee * undernamePercentageFee * increaseQty * years return math.floor(demandFactor * totalFeeForQtyAndYears) end --- this is intended to be a float point number - TODO: protect against large decimals +--- Calculates the number of years between two timestamps +--- @param startTimestamp number The start timestamp +--- @param endTimestamp number The end timestamp +--- @return number The number of years between the two timestamps function arns.calculateYearsBetweenTimestamps(startTimestamp, endTimestamp) local yearsRemainingFloat = (endTimestamp - startTimestamp) / constants.oneYearMs return yearsRemainingFloat @@ -286,7 +333,6 @@ end --- @param purchaseType string|nil The purchase type to check --- @param processId string|nil The processId of the record function arns.assertValidBuyRecord(name, years, purchaseType, processId) - -- assert name is valid pattern assert(type(name) == "string", "Name is required and must be a string.") assert(#name >= 1 and #name <= 51, "Name pattern is invalid.") assert(name:match("^%w") and name:match("%w$") and name:match("^[%w-]+$"), "Name pattern is invalid.") @@ -310,14 +356,10 @@ function arns.assertValidBuyRecord(name, years, purchaseType, processId) end --- Asserts that a record is valid for extending the lease ---- @param record table|nil The record to check ---- @param currentTimestamp number|nil The current timestamp ---- @param years number|nil The number of years to check +--- @param record table The record to check +--- @param currentTimestamp number The current timestamp +--- @param years number The number of years to check function arns.assertValidExtendLease(record, currentTimestamp, years) - assert(record, "Name is not registered") - assert(currentTimestamp, "Timestamp is required") - assert(years, "Years is required") - assert(record.type ~= "permabuy", "Name is permanently owned and cannot be extended") assert(not arns.recordExpired(record, currentTimestamp), "Name is expired") @@ -325,6 +367,10 @@ function arns.assertValidExtendLease(record, currentTimestamp, years) assert(years <= maxAllowedYears, "Cannot extend lease beyond 5 years") end +--- Calculates the maximum allowed years extension for a record +--- @param record table The record to check +--- @param currentTimestamp number The current timestamp +--- @return number The maximum allowed years extension for the record function arns.getMaxAllowedYearsExtensionForRecord(record, currentTimestamp) if not record.endTimestamp then return 0 @@ -341,6 +387,16 @@ function arns.getMaxAllowedYearsExtensionForRecord(record, currentTimestamp) return constants.maxLeaseLengthYears - yearsRemainingOnLease end +--- Gets the registration fees for all name lengths and years +--- @return table A table containing registration fees for each name length, with the following structure: +--- - [nameLength]: table The fees for names of this length +--- - lease: table Lease fees by year +--- - ["1"]: number Cost for 1 year lease +--- - ["2"]: number Cost for 2 year lease +--- - ["3"]: number Cost for 3 year lease +--- - ["4"]: number Cost for 4 year lease +--- - ["5"]: number Cost for 5 year lease +--- - permabuy: number Cost for permanent purchase function arns.getRegistrationFees() local fees = {} local demandFactor = demand.getDemandFactor() @@ -359,8 +415,22 @@ function arns.getRegistrationFees() return fees end +---@class IntendedAction +---@field purchaseType string|nil The type of purchase (lease/permabuy) +---@field years number|nil The number of years for lease +---@field quantity number|nil The quantity for increasing undername limit +---@field name string The name of the record +---@field intent string The intended action type (Buy-Record/Extend-Lease/Increase-Undername-Limit/Upgrade-Name) +---@field currentTimestamp number The current timestamp + --- Gets the token cost for an intended action ---- @param intendedAction table The intended action +--- @param intendedAction IntendedAction The intended action with fields: +--- - purchaseType string|nil The type of purchase (lease/permabuy) +--- - years number|nil The number of years for lease +--- - quantity number|nil The quantity for increasing undername limit +--- - name string The name of the record +--- - intent string The intended action type (Buy-Record/Extend-Lease/Increase-Undername-Limit/Upgrade-Name) +--- - currentTimestamp number The current timestamp --- @return number The token cost in mIO of the intended action function arns.getTokenCost(intendedAction) local tokenCost = 0 @@ -381,17 +451,24 @@ function arns.getTokenCost(intendedAction) arns.assertValidBuyRecord(name, years, purchaseType, processId) tokenCost = arns.calculateRegistrationFee(purchaseType, baseFee, years, demand.getDemandFactor()) elseif intent == "Extend-Lease" then + assert(record, "Name is not registered") + assert(currentTimestamp, "Timestamp is required") + assert(years, "Years is required") arns.assertValidExtendLease(record, currentTimestamp, years) tokenCost = arns.calculateExtensionFee(baseFee, years, demand.getDemandFactor()) elseif intent == "Increase-Undername-Limit" then - arns.assertValidIncreaseUndername(record, qty, currentTimestamp) assert(record, "Name is not registered") + assert(currentTimestamp, "Timestamp is required") + assert(qty, "Quantity is required for increasing undername limit") + arns.assertValidIncreaseUndername(record, qty, currentTimestamp) local yearsRemaining = constants.PERMABUY_LEASE_FEE_LENGTH if record.type == "lease" then yearsRemaining = arns.calculateYearsBetweenTimestamps(currentTimestamp, record.endTimestamp) end tokenCost = arns.calculateUndernameCost(baseFee, qty, record.type, yearsRemaining, demand.getDemandFactor()) elseif intent == "Upgrade-Name" then + assert(record, "Name is not registered") + assert(currentTimestamp, "Timestamp is required") arns.assertValidUpgradeName(record, currentTimestamp) tokenCost = arns.calculatePermabuyFee(baseFee, demand.getDemandFactor()) end @@ -403,11 +480,9 @@ function arns.getTokenCost(intendedAction) end --- Asserts that a name is valid for upgrading ---- @param record table|nil The record to check ---- @param currentTimestamp number|nil The current timestamp +--- @param record table The record to check +--- @param currentTimestamp number The current timestamp function arns.assertValidUpgradeName(record, currentTimestamp) - assert(record, "Name is not registered") - assert(currentTimestamp, "Timestamp is required") assert(record.type ~= "permabuy", "Name is permanently owned") assert( arns.recordIsActive(record, currentTimestamp) or arns.recordInGracePeriod(record, currentTimestamp), @@ -422,15 +497,15 @@ end --- @return table The upgraded record with name and record fields function arns.upgradeRecord(from, name, currentTimestamp) local record = arns.getRecord(name) + assert(record, "Name is not registered") + assert(currentTimestamp, "Timestamp is required") arns.assertValidUpgradeName(record, currentTimestamp) local baseFee = demand.baseFeeForNameLength(#name) local demandFactor = demand.getDemandFactor() local upgradeCost = arns.calculatePermabuyFee(baseFee, demandFactor) - if not balances.walletHasSufficientBalance(from, upgradeCost) then - error("Insufficient balance") - end + assert(balances.walletHasSufficientBalance(from, upgradeCost), "Insufficient balance") record.endTimestamp = nil record.type = "permabuy" @@ -488,31 +563,22 @@ function arns.recordIsActive(record, currentTimestamp) end --- Asserts that a record is valid for increasing the undername limit ---- @param record table|nil The record to check ---- @param qty number|nil The quantity to check ---- @param currentTimestamp number|nil The current timestamp +--- @param record table The record to check +--- @param qty number The quantity to check +--- @param currentTimestamp number The current timestamp function arns.assertValidIncreaseUndername(record, qty, currentTimestamp) - assert(record, "Name is not registered") - assert(currentTimestamp, "Timestamp is required") assert(arns.recordIsActive(record, currentTimestamp), "Name must be active to increase undername limit") assert(qty > 0 and utils.isInteger(qty), "Qty is invalid") end --- AUCTIONS - --- Creates an auction for a given name --- @param name string The name of the auction --- @param timestamp number The timestamp to start the auction --- @param initiator string The address of the initiator of the auction --- @return Auction|nil The auction instance function arns.createAuction(name, timestamp, initiator) - if not arns.getRecord(name) then - error("Name is not registered. Auctions must be created for registered names.") - end - if arns.getAuction(name) then - error("Auction already exists for name") - end - + assert(arns.getRecord(name), "Name is not registered. Auctions can only be created for registered names.") + assert(not arns.getAuction(name), "Auction already exists for name") local baseFee = demand.baseFeeForNameLength(#name) local demandFactor = demand.getDemandFactor() local auction = Auction:new(name, timestamp, demandFactor, baseFee, initiator, arns.calculateRegistrationFee) @@ -546,29 +612,21 @@ end --- @return table The result of the bid including the auction, bidder, bid amount, reward for initiator, reward for protocol, and record function arns.submitAuctionBid(name, bidAmount, bidder, timestamp, processId, type, years) local auction = arns.getAuction(name) - if not auction then - error("Auction not found") - end - - -- assert the bid is between auction start and end timestamps - if timestamp < auction.startTimestamp or timestamp > auction.endTimestamp then - -- TODO: we should likely clean up the auction if it is outside of the time range - error("Bid timestamp is outside of auction start and end timestamps") - end + assert(auction, "Auction not found") + assert( + timestamp >= auction.startTimestamp and timestamp <= auction.endTimestamp, + "Bid timestamp is outside of auction start and end timestamps" + ) local requiredBid = auction:getPriceForAuctionAtTimestamp(timestamp, type, years) local floorPrice = auction:floorPrice(type, years) -- useful for analytics, used by getPriceForAuctionAtTimestamp local startPrice = auction:startPrice(type, years) -- useful for analytics, used by getPriceForAuctionAtTimestamp local requiredOrBidAmount = bidAmount or requiredBid - if requiredOrBidAmount < requiredBid then - error("Bid amount is less than the required bid of " .. requiredBid) - end + assert(requiredOrBidAmount >= requiredBid, "Bid amount is less than the required bid of " .. requiredBid) local finalBidAmount = math.min(requiredOrBidAmount, requiredBid) -- check the balance of the bidder - if not balances.walletHasSufficientBalance(bidder, finalBidAmount) then - error("Insufficient balance") - end + assert(balances.walletHasSufficientBalance(bidder, finalBidAmount), "Insufficient balance") local record = { processId = processId, @@ -604,25 +662,36 @@ function arns.submitAuctionBid(name, bidAmount, bidder, timestamp, processId, ty } end +--- Removes an auction by name +--- @param name string The name of the auction +--- @return Auction|nil The auction instance function arns.removeAuction(name) local auction = arns.getAuction(name) NameRegistry.auctions[name] = nil return auction end +--- Removes a record by name +--- @param name string The name of the record +--- @return table|nil The record instance function arns.removeRecord(name) local record = NameRegistry.records[name] NameRegistry.records[name] = nil return record end +--- Removes a reserved name by name +--- @param name string The name of the reserved name +--- @return table|nil The reserved name instance function arns.removeReservedName(name) local reserved = NameRegistry.reserved[name] NameRegistry.reserved[name] = nil return reserved end --- prune records that have expired +--- Prunes records that have expired +--- @param currentTimestamp number The current timestamp +--- @return table The pruned records function arns.pruneRecords(currentTimestamp) local prunedRecords = {} -- identify any records that are leases and that have expired, account for a one week grace period in seconds @@ -636,7 +705,9 @@ function arns.pruneRecords(currentTimestamp) return prunedRecords end --- prune auctions that have expired +--- Prunes auctions that have expired +--- @param currentTimestamp number The current timestamp +--- @return table The pruned auctions function arns.pruneAuctions(currentTimestamp) local prunedAuctions = {} for name, auction in pairs(arns.getAuctions()) do @@ -647,7 +718,9 @@ function arns.pruneAuctions(currentTimestamp) return prunedAuctions end --- identify any reserved names that have expired, account for a one week grace period in seconds +--- Prunes reserved names that have expired +--- @param currentTimestamp number The current timestamp +--- @return table The pruned reserved names function arns.pruneReservedNames(currentTimestamp) local prunedReserved = {} for name, details in pairs(arns.getReservedNames()) do @@ -658,40 +731,40 @@ function arns.pruneReservedNames(currentTimestamp) return prunedReserved end +--- Asserts that a name can be reassigned +--- @param record table The record to check +--- @param currentTimestamp number The current timestamp +--- @param from string The address of the sender +--- @param newProcessId string The new process id function arns.assertValidReassignName(record, currentTimestamp, from, newProcessId) - if not record then - error("Name is not registered") - end - + assert(record, "Name is not registered") + assert(currentTimestamp, "Timestamp is required") assert(utils.isValidAOAddress(newProcessId), "Invalid Process-Id") - - if record.processId ~= from then - error("Not authorized to reassign this name") - end + assert(record.processId == from, "Not authorized to reassign this name") if record.endTimestamp then local isWithinGracePeriod = record.endTimestamp < currentTimestamp and record.endTimestamp + constants.gracePeriodMs > currentTimestamp local isExpired = record.endTimestamp + constants.gracePeriodMs < currentTimestamp - - if isWithinGracePeriod then - error("Name must be extended before it can be reassigned") - elseif isExpired then - error("Name is expired") - end + assert(not isWithinGracePeriod, "Name must be extended before it can be reassigned") + assert(not isExpired, "Name is expired") end return true end +--- Reassigns a name +--- @param name string The name of the record +--- @param from string The address of the sender +--- @param currentTimestamp number The current timestamp +--- @param newProcessId string The new process id +--- @return table The updated record function arns.reassignName(name, from, currentTimestamp, newProcessId) local record = arns.getRecord(name) - + assert(record, "Name is not registered") arns.assertValidReassignName(record, currentTimestamp, from, newProcessId) - - NameRegistry.records[name].processId = newProcessId - - return arns.getRecord(name) + local updatedRecord = arns.modifyProcessId(name, newProcessId) + return updatedRecord end return arns diff --git a/src/balances.lua b/src/balances.lua index 8513c87..b89db63 100644 --- a/src/balances.lua +++ b/src/balances.lua @@ -52,8 +52,8 @@ function balances.reduceBalance(target, qty) end --- Increases the balance of an address ----@param target string The address to increase balance for ----@param qty number The amount to increase by (must be integer) +--- @param target string The address to increase balance for +--- @param qty number The amount to increase by (must be integer) function balances.increaseBalance(target, qty) assert(utils.isInteger(qty), debug.traceback("Quantity must be an integer: " .. qty)) local prevBalance = balances.getBalance(target) or 0 @@ -61,11 +61,11 @@ function balances.increaseBalance(target, qty) end --- Gets paginated list of all balances ----@param cursor string|nil The address to start from ----@param limit number|nil Max number of results to return ----@param sortBy string|nil Field to sort by ----@param sortOrder string|nil "asc" or "desc" sort direction ----@return table Array of {address, balance} objects +--- @param cursor string|nil The address to start from +--- @param limit number|nil Max number of results to return +--- @param sortBy string|nil Field to sort by +--- @param sortOrder string|nil "asc" or "desc" sort direction +--- @return table Array of {address, balance} objects function balances.getPaginatedBalances(cursor, limit, sortBy, sortOrder) local allBalances = balances.getBalances() local balancesArray = {} @@ -80,6 +80,10 @@ function balances.getPaginatedBalances(cursor, limit, sortBy, sortOrder) return utils.paginateTableWithCursor(balancesArray, cursor, cursorField, limit, sortBy, sortOrder) end +--- Checks if a wallet has a sufficient balance +--- @param wallet string The address of the wallet +--- @param quantity number The amount to check against the balance +--- @return boolean True if the wallet has a sufficient balance, false otherwise function balances.walletHasSufficientBalance(wallet, quantity) return Balances[wallet] ~= nil and Balances[wallet] >= quantity end diff --git a/src/demand.lua b/src/demand.lua index 4b9bfec..c16a533 100644 --- a/src/demand.lua +++ b/src/demand.lua @@ -27,6 +27,8 @@ DemandFactorSettings = DemandFactorSettings criteria = "revenue", } +--- Tally a name purchase +--- @param qty number The quantity of the purchase function demand.tallyNamePurchase(qty) demand.incrementPurchasesThisPeriodRevenue(1) demand.incrementRevenueThisPeriod(qty) @@ -39,6 +41,8 @@ function demand.baseFeeForNameLength(nameLength) return demand.getFees()[nameLength] end +--- Gets the moving average of trailing purchase counts +--- @return number The moving average of trailing purchase counts function demand.mvgAvgTrailingPurchaseCounts() local sum = 0 local trailingPeriodPurchases = demand.getTrailingPeriodPurchases() @@ -48,6 +52,8 @@ function demand.mvgAvgTrailingPurchaseCounts() return sum / #trailingPeriodPurchases end +--- Gets the moving average of trailing revenues +--- @return number The moving average of trailing revenues function demand.mvgAvgTrailingRevenues() local sum = 0 local trailingPeriodRevenues = demand.getTrailingPeriodRevenues() @@ -57,6 +63,8 @@ function demand.mvgAvgTrailingRevenues() return sum / #trailingPeriodRevenues end +--- Checks if the demand is increasing +--- @return boolean True if the demand is increasing, false otherwise function demand.isDemandIncreasing() local settings = demand.getSettings() @@ -78,7 +86,9 @@ function demand.isDemandIncreasing() end end --- update at the end of the demand if the current timestamp results in a period greater than our current state +--- Checks if the demand should update the demand factor +--- @param currentTimestamp number The current timestamp +--- @return boolean True if the demand should update the demand factor, false otherwise function demand.shouldUpdateDemandFactor(currentTimestamp) local settings = demand.getSettings() @@ -92,22 +102,27 @@ function demand.shouldUpdateDemandFactor(currentTimestamp) return calculatedPeriod > demand.getCurrentPeriod() end +--- Gets the demand factor info +--- @return table The demand factor info function demand.getDemandFactorInfo() return utils.deepCopy(DemandFactor) end +--- Updates the demand factor +--- @param timestamp number The current timestamp +--- @return number | nil The updated demand factor or nil if it should not be updated function demand.updateDemandFactor(timestamp) if not demand.shouldUpdateDemandFactor(timestamp) then print("Not updating demand factor") - return -- silently return + return demand.getDemandFactor() end local settings = demand.getSettings() -- check that we have settings - if not settings then + if not demand.shouldUpdateDemandFactor(timestamp) or not settings then print("No settings found") - return + return demand.getDemandFactor() end if demand.isDemandIncreasing() then @@ -143,6 +158,9 @@ function demand.updateDemandFactor(timestamp) return demand.getDemandFactor() end +--- Updates the fees +--- @param multiplier number The multiplier for the fees +--- @return table The updated fees function demand.updateFees(multiplier) local currentFees = demand.getFees() -- update all fees multiply them by the demand factor minimim @@ -150,52 +168,73 @@ function demand.updateFees(multiplier) local updatedFee = fee * multiplier DemandFactor.fees[nameLength] = updatedFee end + return demand.getFees() end +--- Gets the demand factor +--- @return number The demand factor function demand.getDemandFactor() local demandFactor = utils.deepCopy(DemandFactor) return demandFactor and demandFactor.currentDemandFactor or 1 end +--- Gets the current period revenue +--- @return number The current period revenue function demand.getCurrentPeriodRevenue() local demandFactor = utils.deepCopy(DemandFactor) return demandFactor and demandFactor.revenueThisPeriod or 0 end +--- Gets the current period purchases +--- @return number The current period purchases function demand.getCurrentPeriodPurchases() local demandFactor = utils.deepCopy(DemandFactor) return demandFactor and demandFactor.purchasesThisPeriod or 0 end +--- Gets the trailing period purchases +--- @return table The trailing period purchases function demand.getTrailingPeriodPurchases() local demandFactor = utils.deepCopy(DemandFactor) return demandFactor and demandFactor.trailingPeriodPurchases or { 0, 0, 0, 0, 0, 0, 0 } end +--- Gets the trailing period revenues +--- @return table The trailing period revenues function demand.getTrailingPeriodRevenues() local demandFactor = utils.deepCopy(DemandFactor) return demandFactor and demandFactor.trailingPeriodRevenues or { 0, 0, 0, 0, 0, 0, 0 } end +--- Gets the fees +--- @return table The fees function demand.getFees() local demandFactor = utils.deepCopy(DemandFactor) return demandFactor and demandFactor.fees or {} end +--- Gets the settings +--- @return table The settings function demand.getSettings() return utils.deepCopy(DemandFactorSettings) end +--- Gets the consecutive periods with minimum demand factor +--- @return number The consecutive periods with minimum demand factor function demand.getConsecutivePeriodsWithMinDemandFactor() local demandFactor = utils.deepCopy(DemandFactor) return demandFactor and demandFactor.consecutivePeriodsWithMinDemandFactor or 0 end +--- Gets the current period +--- @return number The current period function demand.getCurrentPeriod() local demandFactor = utils.deepCopy(DemandFactor) return demandFactor and demandFactor.currentPeriod or 1 end +--- Updates the settings +--- @param settings table The settings function demand.updateSettings(settings) if not settings then return @@ -203,18 +242,26 @@ function demand.updateSettings(settings) DemandFactorSettings = settings end +--- Updates the start timestamp +--- @param timestamp number The timestamp function demand.updateStartTimestamp(timestamp) DemandFactorSettings.periodZeroStartTimestamp = timestamp end +--- Updates the current period +--- @param period number The period function demand.updateCurrentPeriod(period) DemandFactor.currentPeriod = period end +--- Sets the demand factor +--- @param demandFactor number The demand factor function demand.setDemandFactor(demandFactor) DemandFactor.currentDemandFactor = demandFactor end +--- Gets the period index +--- @return number The period index function demand.getPeriodIndex() local currentPeriod = demand.getCurrentPeriod() local settings = demand.getSettings() @@ -225,44 +272,59 @@ function demand.getPeriodIndex() return (currentPeriod % settings.movingAvgPeriodCount) + 1 -- has to be + 1 to avoid zero index end +--- Updates the trailing period purchases function demand.updateTrailingPeriodPurchases() local periodIndex = demand.getPeriodIndex() DemandFactor.trailingPeriodPurchases[periodIndex] = demand.getCurrentPeriodPurchases() end +--- Updates the trailing period revenues function demand.updateTrailingPeriodRevenues() local periodIndex = demand.getPeriodIndex() DemandFactor.trailingPeriodRevenues[periodIndex] = demand.getCurrentPeriodRevenue() end +--- Resets the purchases this period function demand.resetPurchasesThisPeriod() DemandFactor.purchasesThisPeriod = 0 end +--- Resets the revenue this period function demand.resetRevenueThisPeriod() DemandFactor.revenueThisPeriod = 0 end +--- Increments the purchases this period +--- @param count number The count to increment function demand.incrementPurchasesThisPeriodRevenue(count) DemandFactor.purchasesThisPeriod = DemandFactor.purchasesThisPeriod + count end +--- Increments the revenue this period +--- @param revenue number The revenue to increment function demand.incrementRevenueThisPeriod(revenue) DemandFactor.revenueThisPeriod = DemandFactor.revenueThisPeriod + revenue end +--- Updates the revenue this period +--- @param revenue number The revenue to update function demand.updateRevenueThisPeriod(revenue) DemandFactor.revenueThisPeriod = revenue end +--- Increments the current period +--- @param count number The count to increment function demand.incrementCurrentPeriod(count) DemandFactor.currentPeriod = DemandFactor.currentPeriod + count end +--- Resets the consecutive periods with minimum demand factor function demand.resetConsecutivePeriodsWithMinimumDemandFactor() DemandFactor.consecutivePeriodsWithMinDemandFactor = 0 end +--- Increments the consecutive periods with minimum demand factor +--- @param count number The count to increment function demand.incrementConsecutivePeriodsWithMinDemandFactor(count) DemandFactor.consecutivePeriodsWithMinDemandFactor = DemandFactor.consecutivePeriodsWithMinDemandFactor + count end diff --git a/src/epochs.lua b/src/epochs.lua index 2f1d48d..31a3365 100644 --- a/src/epochs.lua +++ b/src/epochs.lua @@ -5,6 +5,58 @@ local balances = require("balances") local arns = require("arns") local epochs = {} +--- @class Epoch +--- @field epochIndex number The index of the epoch +--- @field startTimestamp number The start timestamp of the epoch +--- @field endTimestamp number The end timestamp of the epoch +--- @field startHeight number The start height of the epoch +--- @field distributionTimestamp number The distribution timestamp of the epoch +--- @field prescribedObservers table The prescribed observers of the epoch +--- @field prescribedNames table The prescribed names of the epoch +--- @field observations Observations The observations of the epoch +--- @field distributions Distribution The distributions of the epoch + +--- @class EpochSettings +--- @field pruneEpochsCount number The number of epochs to prune +--- @field prescribedNameCount number The number of prescribed names +--- @field rewardPercentage number The reward percentage +--- @field maxObservers number The maximum number of observers +--- @field epochZeroStartTimestamp number The start timestamp of epoch zero +--- @field durationMs number The duration of an epoch in milliseconds +--- @field distributionDelayMs number The distribution delay in milliseconds + +--- @class WeightedGateway +--- @field gatewayAddress string The gateway address +--- @field stakeWeight number The stake weight +--- @field tenureWeight number The tenure weight +--- @field gatewayRewardRatioWeight number The gateway reward ratio weight +--- @field observerRewardRatioWeight number The observer reward ratio weight +--- @field compositeWeight number The composite weight +--- @field normalizedCompositeWeight number The normalized composite weight + +--- @class Observations +--- @field failureSummaries table The failure summaries +--- @field reports Reports The reports for the epoch (indexed by observer address) + +--- @class Reports: table + +--- @class GatewayRewards +--- @field operatorReward number The total operator reward eligible +--- @field delegateRewards table The delegate rewards eligible, indexed by delegate address + +--- @class Rewards +--- @field eligible table A table representing the eligible operator and delegate rewards for a gateway +--- @field distributed table A table representing the distributed rewards, only set if rewards have been distributed + +--- @class Distribution +--- @field totalEligibleGateways number The total eligible gateways +--- @field totalEligibleRewards number The total eligible rewards +--- @field totalEligibleGatewayReward number The total eligible gateway reward +--- @field totalEligibleObserverReward number The total eligible observer reward +--- @field distributedTimestamp number|nil The distributed timestamp, only set if rewards have been distributed +--- @field totalDistributedRewards number|nil The total distributed rewards, only set if rewards have been distributed +--- @field rewards Rewards The rewards + Epochs = Epochs or {} EpochSettings = EpochSettings or { @@ -17,39 +69,60 @@ EpochSettings = EpochSettings distributionDelayMs = 60 * 1000 * 40, -- 40 minutes (~ 20 arweave blocks) } +--- Gets all the epochs +--- @return table The epochs indexed by their epoch index function epochs.getEpochs() - return utils.deepCopy(epochs) or {} + return utils.deepCopy(Epochs) or {} end +--- Gets an epoch by index +--- @param epochIndex number The epoch index +--- @return Epoch The epoch function epochs.getEpoch(epochIndex) local epoch = utils.deepCopy(Epochs[epochIndex]) or {} return epoch end -function epochs.getObservers() - return epochs.getCurrentEpoch().prescribedObservers or {} +--- Gets the current epoch +--- @return Epoch The current epoch +function epochs.getCurrentEpoch() + return epochs.getEpoch(epochs.getEpochIndexForTimestamp(os.time())) end +--- Gets the epoch settings +--- @return EpochSettings|nil The epoch settings function epochs.getSettings() return utils.deepCopy(EpochSettings) end +--- Gets the observations for the current epoch +--- @return Observations The observations for the current epoch function epochs.getObservations() return epochs.getCurrentEpoch().observations or {} end +--- Gets the reports for the current epoch +--- @return Reports The reports for the current epoch function epochs.getReports() return epochs.getObservations().reports or {} end +--- Gets the current distribution +--- @return Distribution The current distribution function epochs.getDistribution() return epochs.getCurrentEpoch().distributions or {} end +--- Gets the prescribed observers for an epoch +--- @param epochIndex number The epoch index +--- @return WeightedGateway[] The prescribed observers for the epoch function epochs.getPrescribedObserversForEpoch(epochIndex) return epochs.getEpoch(epochIndex).prescribedObservers or {} end +--- Gets the eligible rewards for an epoch +--- @param epochIndex number The epoch index +--- @return Rewards The eligible rewards for the epoch function epochs.getEligibleRewardsForEpoch(epochIndex) local epoch = epochs.getEpoch(epochIndex) local eligible = epoch @@ -60,6 +133,9 @@ function epochs.getEligibleRewardsForEpoch(epochIndex) return eligible end +--- Gets the distributed rewards for an epoch +--- @param epochIndex number The epoch index +--- @return Rewards The distributed rewards for the epoch function epochs.getDistributedRewardsForEpoch(epochIndex) local epoch = epochs.getEpoch(epochIndex) local distributed = epoch @@ -70,30 +146,52 @@ function epochs.getDistributedRewardsForEpoch(epochIndex) return distributed end +--- Gets the observations for an epoch +--- @param epochIndex number The epoch index +--- @return Observations The observations for the epoch function epochs.getObservationsForEpoch(epochIndex) return epochs.getEpoch(epochIndex).observations or {} end +--- Gets the distributions for an epoch +--- @param epochIndex number The epoch index +--- @return Distribution The distributions for the epoch function epochs.getDistributionsForEpoch(epochIndex) return epochs.getEpoch(epochIndex).distributions or {} end +--- Gets the prescribed names for an epoch +--- @param epochIndex number The epoch index +--- @return string[] The prescribed names for the epoch function epochs.getPrescribedNamesForEpoch(epochIndex) return epochs.getEpoch(epochIndex).prescribedNames or {} end +--- Gets the reports for an epoch +--- @param epochIndex number The epoch index +--- @return table The reports for the epoch function epochs.getReportsForEpoch(epochIndex) return epochs.getEpoch(epochIndex).observations.reports or {} end +--- Gets the distribution for an epoch +--- @param epochIndex number The epoch index +--- @return Distribution The distribution for the epoch function epochs.getDistributionForEpoch(epochIndex) return epochs.getEpoch(epochIndex).distributions or {} end +--- Gets the epoch from a timestamp +--- @param timestamp number The timestamp +--- @return Epoch The epoch function epochs.getEpochFromTimestamp(timestamp) local epochIndex = epochs.getEpochIndexForTimestamp(timestamp) return epochs.getEpoch(epochIndex) end + +--- Sets the prescribed observers for an epoch +--- @param epochIndex number The epoch index +--- @param hashchain string The hashchain function epochs.setPrescribedObserversForEpoch(epochIndex, hashchain) local prescribedObservers = epochs.computePrescribedObserversForEpoch(epochIndex, hashchain) local epoch = epochs.getEpoch(epochIndex) @@ -102,6 +200,9 @@ function epochs.setPrescribedObserversForEpoch(epochIndex, hashchain) Epochs[epochIndex] = epoch end +--- Sets the prescribed names for an epoch +--- @param epochIndex number The epoch index +--- @param hashchain string The hashchain function epochs.setPrescribedNamesForEpoch(epochIndex, hashchain) local prescribedNames = epochs.computePrescribedNamesForEpoch(epochIndex, hashchain) local epoch = epochs.getEpoch(epochIndex) @@ -110,6 +211,10 @@ function epochs.setPrescribedNamesForEpoch(epochIndex, hashchain) Epochs[epochIndex] = epoch end +--- Computes the prescribed names for an epoch +--- @param epochIndex number The epoch index +--- @param hashchain string The hashchain +--- @return string[] The prescribed names for the epoch function epochs.computePrescribedNamesForEpoch(epochIndex, hashchain) local epochStartTimestamp, epochEndTimestamp = epochs.getEpochTimestampsForIndex(epochIndex) local activeArNSNames = arns.getActiveArNSNamesBetweenTimestamps(epochStartTimestamp, epochEndTimestamp) @@ -157,6 +262,10 @@ function epochs.computePrescribedNamesForEpoch(epochIndex, hashchain) return prescribedNames end +--- Computes the prescribed observers for an epoch +--- @param epochIndex number The epoch index +--- @param hashchain string The hashchain +--- @return WeightedGateway[], WeightedGateway[] The prescribed observers for the epoch, and all the gateways with weights function epochs.computePrescribedObserversForEpoch(epochIndex, hashchain) assert(epochIndex >= 0, "Epoch index must be greater than or equal to 0") assert(type(hashchain) == "string", "Hashchain must be a string") @@ -239,6 +348,9 @@ function epochs.computePrescribedObserversForEpoch(epochIndex, hashchain) return prescribedObservers, weightedGateways end +--- Gets the epoch timestamps for an epoch index +--- @param epochIndex number The epoch index +--- @return number, number, number The epoch start timestamp, epoch end timestamp, and epoch distribution timestamp function epochs.getEpochTimestampsForIndex(epochIndex) local epochStartTimestamp = epochs.getSettings().epochZeroStartTimestamp + epochs.getSettings().durationMs * epochIndex @@ -247,6 +359,9 @@ function epochs.getEpochTimestampsForIndex(epochIndex) return epochStartTimestamp, epochEndTimestamp, epochDistributionTimestamp end +--- Gets the epoch index for a given timestamp +--- @param timestamp number The timestamp +--- @return number The epoch index function epochs.getEpochIndexForTimestamp(timestamp) local timestampInMS = utils.checkAndConvertTimestamptoMs(timestamp) local epochZeroStartTimestamp = epochs.getSettings().epochZeroStartTimestamp @@ -255,6 +370,11 @@ function epochs.getEpochIndexForTimestamp(timestamp) return epochIndex end +--- Creates a new epoch and updates the gateway weights +--- @param timestamp number The timestamp +--- @param blockHeight number The block height +--- @param hashchain string The hashchain +--- @return Epoch|nil The created epoch, or nil if an epoch already exists for the index function epochs.createEpoch(timestamp, blockHeight, hashchain) assert(type(timestamp) == "number", "Timestamp must be a number") assert(type(blockHeight) == "number", "Block height must be a number") @@ -325,6 +445,12 @@ function epochs.createEpoch(timestamp, blockHeight, hashchain) return epoch end +--- Saves the observations for an epoch +--- @param observerAddress string The observer address +--- @param reportTxId string The report transaction ID +--- @param failedGatewayAddresses string[] The failed gateway addresses +--- @param timestamp number The timestamp +--- @return Observations The updated observations for the epoch function epochs.saveObservations(observerAddress, reportTxId, failedGatewayAddresses, timestamp) -- assert report tx id is valid arweave address assert(utils.isValidArweaveAddress(reportTxId), "Report transaction ID is not a valid Arweave address") @@ -414,11 +540,22 @@ function epochs.saveObservations(observerAddress, reportTxId, failedGatewayAddre return epoch.observations end --- for testing purposes +--- Updates the epoch settings +--- @param newSettings EpochSettings The new settings function epochs.updateEpochSettings(newSettings) EpochSettings = newSettings end +--- @class ComputedRewards +--- @field totalEligibleRewards number The total eligible rewards +--- @field perGatewayReward number The per gateway reward +--- @field perObserverReward number The per observer reward +--- @field potentialRewards table The potential rewards for each gateway + +--- Computes the total eligible rewards for an epoch based on the protocol balance and the reward percentage and prescribed observers +--- @param epochIndex number The epoch index +--- @param prescribedObservers Observer[] The prescribed observers +--- @return ComputedRewards The total eligible rewards function epochs.computeTotalEligibleRewardsForEpoch(epochIndex, prescribedObservers) local epochStartTimestamp = epochs.getEpochTimestampsForIndex(epochIndex) local activeGatewayAddresses = gar.getActiveGatewaysBeforeTimestamp(epochStartTimestamp) @@ -470,13 +607,15 @@ function epochs.computeTotalEligibleRewardsForEpoch(epochIndex, prescribedObserv potentialRewards = potentialRewards, } end --- Steps --- 1. Get gateways participated in full epoch based on start and end timestamp --- 2. Get the prescribed observers for the relevant epoch --- 3. Calcualte the rewards for the epoch based on protocol balance --- 4. Allocate 95% of the rewards for passed gateways, 5% for observers - based on total gateways during the epoch and # of prescribed observers --- 5. Distribute the rewards to the gateways and observers --- 6. Increment the epoch stats for the gateways +--- Distributes the rewards for an epoch +--- 1. Get gateways participated in full epoch based on start and end timestamp +--- 2. Get the prescribed observers for the relevant epoch +--- 3. Calcualte the rewards for the epoch based on protocol balance +--- 4. Allocate 95% of the rewards for passed gateways, 5% for observers - based on total gateways during the epoch and # of prescribed observers +--- 5. Distribute the rewards to the gateways and observers +--- 6. Increment the epoch stats for the gateways +--- @param currentTimestamp number The current timestamp +--- @return Epoch|nil The updated epoch with the distributed rewards, or nil if no rewards were distributed function epochs.distributeRewardsForEpoch(currentTimestamp) local epochIndex = epochs.getEpochIndexForTimestamp(currentTimestamp - epochs.getSettings().durationMs) -- go back to previous epoch local epoch = epochs.getEpoch(epochIndex) @@ -636,7 +775,9 @@ function epochs.distributeRewardsForEpoch(currentTimestamp) return epochs.getEpoch(epochIndex) end --- prune epochs older than 14 days +--- Prunes epochs older than the cutoff epoch index +--- @param timestamp number The timestamp to prune epochs older than +--- @return Epoch[] The pruned epochs function epochs.pruneEpochs(timestamp) local prunedEpochs = {} local currentEpochIndex = epochs.getEpochIndexForTimestamp(timestamp) diff --git a/src/tick.lua b/src/tick.lua index c742947..9e4d03e 100644 --- a/src/tick.lua +++ b/src/tick.lua @@ -3,6 +3,11 @@ local arns = require("arns") local gar = require("gar") local vaults = require("vaults") local epochs = require("epochs") + +--- Prunes the state +--- @param timestamp number The timestamp +--- @param msgId string The message ID +--- @return table The pruned records, auctions, reserved names, vaults, gateways, and epochs function tick.pruneState(timestamp, msgId) local prunedRecords = arns.pruneRecords(timestamp) local prunedAuctions = arns.pruneAuctions(timestamp) diff --git a/src/utils.lua b/src/utils.lua index 34c15af..20f8b47 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -25,6 +25,9 @@ function utils.roundToPrecision(number, precision) return math.floor(number * (10 ^ precision) + 0.5) / (10 ^ precision) end +--- Sums the values of a table +--- @param tbl table The table to sum +--- @return number The sum of the table values function utils.sumTableValues(tbl) local sum = 0 for _, value in pairs(tbl) do @@ -33,6 +36,12 @@ function utils.sumTableValues(tbl) return sum end +--- Slices a table +--- @param tbl table The table to slice +--- @param first number The first index to slice from +--- @param last number The last index to slice to +--- @param step number The step to slice by +--- @return table The sliced table function utils.slice(tbl, first, last, step) local sliced = {} @@ -43,6 +52,9 @@ function utils.slice(tbl, first, last, step) return sliced end +--- Parses the pagination tags from a message +--- @param msg table The message provided to a handler (see ao docs for more info) +--- @return table The pagination tags function utils.parsePaginationTags(msg) local cursor = msg.Tags.Cursor local limit = tonumber(msg.Tags["Limit"]) or 100 @@ -56,8 +68,13 @@ function utils.parsePaginationTags(msg) } end +--- Sorts a table by a given field +--- @param prevTable table The table to sort +--- @param field string The field to sort by +--- @param order string The order to sort by ("asc" or "desc") +--- @return table The sorted table function utils.sortTableByField(prevTable, field, order) - local tableCopy = utils.deepCopy(prevTable) + local tableCopy = utils.deepCopy(prevTable) or {} if order ~= "asc" and order ~= "desc" then error("Invalid sort order") @@ -89,6 +106,14 @@ function utils.sortTableByField(prevTable, field, order) return tableCopy end +--- Paginate a table with a cursor +--- @param tableArray table The table to paginate +--- @param cursor string The cursor to paginate from +--- @param cursorField string The field to use as the cursor +--- @param limit number The limit of items to return +--- @param sortBy string The field to sort by +--- @param sortOrder string The order to sort by ("asc" or "desc") +--- @return table The paginated table function utils.paginateTableWithCursor(tableArray, cursor, cursorField, limit, sortBy, sortOrder) local sortedArray = utils.sortTableByField(tableArray, sortBy, sortOrder) @@ -138,24 +163,32 @@ function utils.paginateTableWithCursor(tableArray, cursor, cursorField, limit, s } end +--- Checks if an address is a valid Arweave address +--- @param address string The address to check +--- @return boolean Whether the address is a valid Arweave address function utils.isValidArweaveAddress(address) return type(address) == "string" and #address == 43 and string.match(address, "^[%w-_]+$") ~= nil end +--- Checks if an address is a valid Ethereum address +--- @param address string The address to check +--- @return boolean Whether the address is a valid Ethereum address function utils.isValidEthAddress(address) return type(address) == "string" and #address == 42 and string.match(address, "^0x[%x]+$") ~= nil end ---- Checks if an address is a valid base64url +--- Checks if an address is a valid AO address --- @param url string|nil The address to check ---- @return boolean Whether the address is a valid base64url +--- @return boolean Whether the address is a valid AO address function utils.isValidAOAddress(url) return url and (utils.isValidArweaveAddress(url) or utils.isValidEthAddress(url)) or false end --- Convert address to EIP-55 checksum format --- assumes address has been validated as a valid Ethereum address (see utils.isValidEthAddress) --- Reference: https://eips.ethereum.org/EIPS/eip-55 +--- Converts an address to EIP-55 checksum format +--- Assumes address has been validated as a valid Ethereum address (see utils.isValidEthAddress) +--- Reference: https://eips.ethereum.org/EIPS/eip-55 +--- @param address string The address to convert +--- @return string The EIP-55 checksum formatted address function utils.formatEIP55Address(address) local hex = string.lower(string.sub(address, 3)) @@ -177,6 +210,9 @@ function utils.formatEIP55Address(address) return checksumAddress end +--- Formats an address to EIP-55 checksum format if it is a valid Ethereum address +--- @param address string The address to format +--- @return string The EIP-55 checksum formatted address function utils.formatAddress(address) if utils.isValidEthAddress(address) then return utils.formatEIP55Address(address) @@ -184,6 +220,9 @@ function utils.formatAddress(address) return address end +--- Safely decodes a JSON string +--- @param jsonString string The JSON string to decode +--- @return table|nil The decoded JSON or nil if the string is nil or the decoding fails function utils.safeDecodeJson(jsonString) if not jsonString then return nil @@ -196,6 +235,10 @@ function utils.safeDecodeJson(jsonString) return result end +--- Finds an element in an array that matches a predicate +--- @param array table The array to search +--- @param predicate function The predicate to match +--- @return number|nil The index of the found element or nil if the element is not found function utils.findInArray(array, predicate) for i = 1, #array do if predicate(array[i]) then @@ -206,8 +249,8 @@ function utils.findInArray(array, predicate) end --- Deep copies a table ----@param original table The table to copy ----@return table|nil The deep copy of the table or nil if the original is nil +--- @param original table The table to copy +--- @return table|nil The deep copy of the table or nil if the original is nil function utils.deepCopy(original) if not original then return nil @@ -228,6 +271,9 @@ function utils.deepCopy(original) return copy end +--- Gets the length of a table +--- @param table table The table to get the length of +--- @return number The length of the table function utils.lengthOfTable(table) local count = 0 for _, val in pairs(table) do @@ -237,12 +283,20 @@ function utils.lengthOfTable(table) end return count end + +--- Gets a hash from a base64 URL encoded string +--- @param str string The base64 URL encoded string +--- @return table The hash function utils.getHashFromBase64URL(str) local decodedHash = base64.decode(str, base64.URL_DECODER) local hashStream = crypto.utils.stream.fromString(decodedHash) return crypto.digest.sha2_256(hashStream).asBytes() end +--- Splits a string by a delimiter +--- @param input string The string to split +--- @param delimiter string The delimiter to split by +--- @return table The split string function utils.splitString(input, delimiter) delimiter = delimiter or "," local result = {} @@ -252,10 +306,17 @@ function utils.splitString(input, delimiter) return result end +--- Trims a string +--- @param input string The string to trim +--- @return string The trimmed string function utils.trimString(input) return input:match("^%s*(.-)%s*$") end +--- Splits a string by a delimiter and trims each token +--- @param input string The string to split +--- @param delimiter string The delimiter to split by +--- @return table The split and trimmed string function utils.splitAndTrimString(input, delimiter) local tokens = {} for _, token in ipairs(utils.splitString(input, delimiter)) do @@ -267,11 +328,13 @@ function utils.splitAndTrimString(input, delimiter) return tokens end +--- Checks if a timestamp is an integer and converts it to milliseconds if it is in seconds +--- @param timestamp number The timestamp to check and convert +--- @return number The timestamp in milliseconds function utils.checkAndConvertTimestamptoMs(timestamp) -- Check if the timestamp is an integer - if type(timestamp) ~= "number" or timestamp % 1 ~= 0 then - return error("Timestamp must be an integer") - end + assert(type(timestamp) == "number", "Timestamp must be a number") + assert(utils.isInteger(timestamp), "Timestamp must be an integer") -- Define the plausible range for Unix timestamps in seconds local min_timestamp = 0 @@ -290,7 +353,7 @@ function utils.checkAndConvertTimestamptoMs(timestamp) return timestamp end - return error("Timestamp is out of range") + error("Timestamp is out of range") end function utils.reduce(tbl, fn, init) diff --git a/src/vaults.lua b/src/vaults.lua index 2395f87..93fa1d8 100644 --- a/src/vaults.lua +++ b/src/vaults.lua @@ -6,126 +6,132 @@ local balances = require("balances") local utils = require("utils") local constants = require("constants") -function vaults.createVault(from, qty, lockLengthMs, currentTimestamp, msgId) - if vaults.getVault(from, msgId) then - error("Vault with id " .. msgId .. " already exists") - end - - if lockLengthMs < constants.MIN_TOKEN_LOCK_TIME_MS or lockLengthMs > constants.MAX_TOKEN_LOCK_TIME_MS then - error( - "Invalid lock length. Must be between " - .. constants.MIN_TOKEN_LOCK_TIME_MS - .. " - " - .. constants.MAX_TOKEN_LOCK_TIME_MS - .. " ms" - ) - end +--- @class Vault +--- @field balance number The balance of the vault +--- @field startTimestamp number The start timestamp of the vault +--- @field endTimestamp number The end timestamp of the vault + +--- @class Vaults: table A table of vaults indexed by owner address + +--- Creates a vault +--- @param from string The address of the owner +--- @param qty number The quantity of tokens to vault +--- @param lockLengthMs number The lock length in milliseconds +--- @param currentTimestamp number The current timestamp +--- @param vaultId string The vault id +--- @return Vault The created vault +function vaults.createVault(from, qty, lockLengthMs, currentTimestamp, vaultId) + assert(not vaults.getVault(from, vaultId), "Vault with id " .. vaultId .. " already exists") + assert(balances.walletHasSufficientBalance(from, qty), "Insufficient balance") + assert( + lockLengthMs >= constants.MIN_TOKEN_LOCK_TIME_MS and lockLengthMs <= constants.MAX_TOKEN_LOCK_TIME_MS, + "Invalid lock length. Must be between " + .. constants.MIN_TOKEN_LOCK_TIME_MS + .. " - " + .. constants.MAX_TOKEN_LOCK_TIME_MS + .. " ms" + ) balances.reduceBalance(from, qty) - vaults.setVault(from, msgId, { + local newVault = vaults.setVault(from, vaultId, { balance = qty, startTimestamp = currentTimestamp, endTimestamp = currentTimestamp + lockLengthMs, }) - return vaults.getVault(from, msgId) + return newVault end -function vaults.vaultedTransfer(from, recipient, qty, lockLengthMs, currentTimestamp, msgId) - if balances.getBalance(from) < qty then - error("Insufficient balance") - end - - local vault = vaults.getVault(from, msgId) - - if vault then - error("Vault with id " .. msgId .. " already exists") - end - - if lockLengthMs < constants.MIN_TOKEN_LOCK_TIME_MS or lockLengthMs > constants.MAX_TOKEN_LOCK_TIME_MS then - error( - "Invalid lock length. Must be between " - .. constants.MIN_TOKEN_LOCK_TIME_MS - .. " - " - .. constants.MAX_TOKEN_LOCK_TIME_MS - .. " ms" - ) - end +--- Vaults a transfer +--- @param from string The address of the owner +--- @param recipient string The address of the recipient +--- @param qty number The quantity of tokens to vault +--- @param lockLengthMs number The lock length in milliseconds +--- @param currentTimestamp number The current timestamp +--- @param vaultId string The vault id +--- @return Vault The created vault +function vaults.vaultedTransfer(from, recipient, qty, lockLengthMs, currentTimestamp, vaultId) + assert(balances.walletHasSufficientBalance(from, qty), "Insufficient balance") + assert(not vaults.getVault(recipient, vaultId), "Vault with id " .. vaultId .. " already exists") + assert( + lockLengthMs >= constants.MIN_TOKEN_LOCK_TIME_MS and lockLengthMs <= constants.MAX_TOKEN_LOCK_TIME_MS, + "Invalid lock length. Must be between " + .. constants.MIN_TOKEN_LOCK_TIME_MS + .. " - " + .. constants.MAX_TOKEN_LOCK_TIME_MS + .. " ms" + ) balances.reduceBalance(from, qty) - vaults.setVault(recipient, msgId, { + local newVault = vaults.setVault(recipient, vaultId, { balance = qty, startTimestamp = currentTimestamp, endTimestamp = currentTimestamp + lockLengthMs, }) - return vaults.getVault(recipient, msgId) + return newVault end +--- Extends a vault +--- @param from string The address of the owner +--- @param extendLengthMs number The extension length in milliseconds +--- @param currentTimestamp number The current timestamp +--- @param vaultId string The vault id +--- @return Vault The extended vault function vaults.extendVault(from, extendLengthMs, currentTimestamp, vaultId) local vault = vaults.getVault(from, vaultId) - - if not vault then - error("Vault not found.") - end - - if currentTimestamp >= vault.endTimestamp then - error("This vault has ended.") - end - - if extendLengthMs <= 0 then - error("Invalid extend length. Must be a positive number.") - end + assert(vault, "Vault not found.") + assert(currentTimestamp <= vault.endTimestamp, "Vault has ended.") + assert(extendLengthMs > 0, "Invalid extend length. Must be a positive number.") local totalTimeRemaining = vault.endTimestamp - currentTimestamp local totalTimeRemainingWithExtension = totalTimeRemaining + extendLengthMs - if totalTimeRemainingWithExtension > constants.MAX_TOKEN_LOCK_TIME_MS then - error( - "Invalid vault extension. Total lock time cannot be greater than " - .. constants.MAX_TOKEN_LOCK_TIME_MS - .. " ms" - ) - end + assert( + totalTimeRemainingWithExtension <= constants.MAX_TOKEN_LOCK_TIME_MS, + "Invalid vault extension. Total lock time cannot be greater than " .. constants.MAX_TOKEN_LOCK_TIME_MS .. " ms" + ) vault.endTimestamp = vault.endTimestamp + extendLengthMs - -- update the vault Vaults[from][vaultId] = vault - return vaults.getVault(from, vaultId) + return vault end +--- Increases a vault +--- @param from string The address of the owner +--- @param qty number The quantity of tokens to increase the vault by +--- @param vaultId string The vault id +--- @param currentTimestamp number The current timestamp +--- @return Vault The increased vault function vaults.increaseVault(from, qty, vaultId, currentTimestamp) - if balances.getBalance(from) < qty then - error("Insufficient balance") - end + assert(balances.walletHasSufficientBalance(from, qty), "Insufficient balance") local vault = vaults.getVault(from, vaultId) - - if not vault then - error("Vault not found.") - end - - if currentTimestamp >= vault.endTimestamp then - error("This vault has ended.") - end + assert(vault, "Vault not found.") + assert(currentTimestamp <= vault.endTimestamp, "Vault has ended.") balances.reduceBalance(from, qty) vault.balance = vault.balance + qty - -- update the vault Vaults[from][vaultId] = vault - return vaults.getVault(from, vaultId) + return vault end +--- Gets all vaults +--- @return Vaults The vaults function vaults.getVaults() - local _vaults = utils.deepCopy(Vaults) - return _vaults or {} + return utils.deepCopy(Vaults) or {} end +--- Gets a vault +--- @param target string The address of the owner +--- @param id string The vault id +--- @return Vault| nil The vault function vaults.getVault(target, id) - local _vaults = vaults.getVaults() - if not _vaults[target] then - return nil - end - return _vaults[target][id] + return Vaults[target] and Vaults[target][id] end +--- Sets a vault +--- @param target string The address of the owner +--- @param id string The vault id +--- @param vault Vault The vault +--- @return Vault The vault function vaults.setVault(target, id, vault) -- create the top key first if not exists if not Vaults[target] then @@ -136,7 +142,9 @@ function vaults.setVault(target, id, vault) return vault end --- return any vaults to owners that have expired +--- Prunes expired vaults +--- @param currentTimestamp number The current timestamp +--- @return Vault[] The pruned vaults function vaults.pruneVaults(currentTimestamp) local allVaults = vaults.getVaults() local prunedVaults = {}