Skip to content

Commit

Permalink
feat(PE-6996): allow upgrading leased names (#150)
Browse files Browse the repository at this point in the history
Allows `leased` names to be upgraded to a `permabuy` - can be performed
by anyone and is just the cost of permabuying the name. There is no
discount applied for the original leasing of the name.
  • Loading branch information
dtfiedler authored Oct 30, 2024
2 parents 6da096a + 4730b49 commit 205993e
Show file tree
Hide file tree
Showing 7 changed files with 1,183 additions and 805 deletions.
87 changes: 83 additions & 4 deletions spec/arns_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -403,15 +403,13 @@ describe("arns", function()

describe("calculateLeaseFee [" .. addressType .. "]", function()
it("should return the correct fee for a lease", function()
local name = "test-name" -- 9 character name
local baseFee = demand.getFees()[#name] -- base fee is 500 IO
local baseFee = 500000000 -- base fee is 500 IO
local fee = arns.calculateRegistrationFee("lease", baseFee, 1, 1)
assert.are.equal(600000000, fee)
end)

it("should return the correct fee for a permabuy [" .. addressType .. "]", function()
local name = "test-name" -- 9 character name
local baseFee = demand.getFees()[#name] -- base fee is 500 IO
local baseFee = 500000000 -- base fee is 500 IO
local fee = arns.calculateRegistrationFee("permabuy", baseFee, 1, 1)
local expected = (baseFee * 0.2 * 20) + baseFee
assert.are.equal(expected, fee)
Expand Down Expand Up @@ -846,6 +844,87 @@ describe("arns", function()
end)
end)

describe("upgradeRecord", function()
it("should upgrade a leased record to a permabuy", function()
_G.NameRegistry.records["upgrade-name"] = {
endTimestamp = 1000000,
processId = "test-process-id",
purchasePrice = 1000000,
startTimestamp = 0,
type = "lease",
undernameLimit = 10,
}
_G.Balances[testAddressArweave] = 2500000000
local updatedRecord = arns.upgradeRecord(testAddressArweave, "upgrade-name", 1000000)
assert.are.same({
name = "upgrade-name",
record = {
endTimestamp = nil,
processId = "test-process-id",
purchasePrice = 2500000000,
startTimestamp = 0,
type = "permabuy",
undernameLimit = 10,
},
totalUpgradeFee = 2500000000,
baseRegistrationFee = 500000000,
remainingBalance = 0,
protocolBalance = 2500000000,
df = demand.getDemandFactorInfo(),
}, updatedRecord)
end)

it("should throw an error if the name is not registered", function()
local status, error = pcall(arns.upgradeRecord, testAddressArweave, "upgrade-name", 1000000)
assert.is_false(status)
assert.match("Name is not registered", error)
end)

it("should throw an error if the record is already a permabuy", function()
_G.NameRegistry.records["upgrade-name"] = {
endTimestamp = nil,
processId = "test-process-id",
purchasePrice = 1000000,
startTimestamp = 0,
type = "permabuy",
undernameLimit = 10,
}
local status, error = pcall(arns.upgradeRecord, testAddressArweave, "upgrade-name", 1000000)
assert.is_false(status)
assert.match("Record is already a permabuy", error)
end)

it("should throw an error if the record is expired", function()
local currentTimestamp = 1000000 + constants.gracePeriodMs + 1
_G.NameRegistry.records["upgrade-name"] = {
endTimestamp = 1000000,
processId = "test-process-id",
purchasePrice = 1000000,
startTimestamp = 0,
type = "lease",
undernameLimit = 10,
}
local status, error = pcall(arns.upgradeRecord, testAddressArweave, "upgrade-name", currentTimestamp)
assert.is_false(status)
assert.match("Name is expired", error)
end)

it("should throw an error if the sender does not have enough balance", function()
_G.NameRegistry.records["upgrade-name"] = {
endTimestamp = 1000000,
processId = "test-process-id",
purchasePrice = 1000000,
startTimestamp = 0,
type = "lease",
undernameLimit = 10,
}
_G.Balances[testAddressArweave] = 2500000000 - 1 -- 1 less than the upgrade cost
local status, error = pcall(arns.upgradeRecord, testAddressArweave, "upgrade-name", 1000000)
assert.is_false(status)
assert.match("Insufficient balance", error)
end)
end)

describe("submitAuctionBid", function()
it(
"should accept bid on an existing auction and transfer tokens to the auction initiator and protocol balance, and create the record",
Expand Down
4 changes: 2 additions & 2 deletions spec/setup.lua
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package.path = "./contract/src/?.lua;" .. package.path

_G.ao = {
send = function()
return true
send = function(val)
return val
end,
id = "test",
}
Expand Down
141 changes: 113 additions & 28 deletions src/arns.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function arns.buyRecord(name, purchaseType, years, from, timestamp, processId)
years = 1 -- set to 1 year by default
end

local baseRegistrationFee = demand.getFees()[#name]
local baseRegistrationFee = demand.baseFeeForNameLength(#name)

local totalRegistrationFee =
arns.calculateRegistrationFee(purchaseType, baseRegistrationFee, years, demand.getDemandFactor())
Expand Down Expand Up @@ -95,7 +95,7 @@ function arns.extendLease(from, name, years, currentTimestamp)
local record = arns.getRecord(name)
-- throw error if invalid
arns.assertValidExtendLease(record, currentTimestamp, years)
local baseRegistrationFee = demand.getFees()[#name]
local baseRegistrationFee = demand.baseFeeForNameLength(#name)
local totalExtensionFee = arns.calculateExtensionFee(baseRegistrationFee, years, demand.getDemandFactor())

if balances.getBalance(from) < totalExtensionFee then
Expand Down Expand Up @@ -135,7 +135,7 @@ function arns.increaseundernameLimit(from, name, qty, currentTimestamp)
yearsRemaining = arns.calculateYearsBetweenTimestamps(currentTimestamp, record.endTimestamp)
end

local baseRegistrationFee = demand.getFees()[#name]
local baseRegistrationFee = demand.baseFeeForNameLength(#name)
local additionalUndernameCost =
arns.calculateUndernameCost(baseRegistrationFee, qty, record.type, yearsRemaining, demand.getDemandFactor())

Expand Down Expand Up @@ -304,7 +304,7 @@ function arns.assertValidExtendLease(record, currentTimestamp, years)
error("Name is permabought and cannot be extended")
end

if record.endTimestamp and record.endTimestamp + constants.gracePeriodMs < currentTimestamp then
if arns.recordExpired(record, currentTimestamp) then
error("Name is expired")
end

Expand Down Expand Up @@ -350,22 +350,22 @@ end

function arns.getTokenCost(intendedAction)
local tokenCost = 0
if intendedAction.intent == "Buy-Record" then
local purchaseType = intendedAction.purchaseType
local years = intendedAction.years
local name = intendedAction.name
local baseFee = demand.getFees()[#name]
assert(type(name) == "string", "Name is required and must be a string.")
local purchaseType = intendedAction.purchaseType
local years = tonumber(intendedAction.years)
local name = intendedAction.name
local baseFee = demand.baseFeeForNameLength(#name)
local intent = intendedAction.intent
local qty = tonumber(intendedAction.quantity)

assert(type(intent) == "string", "Intent is required and must be a string.")
assert(type(name) == "string", "Name is required and must be a string.")
if intent == "Buy-Record" then
assert(purchaseType == "lease" or purchaseType == "permabuy", "PurchaseType is invalid.")
if purchaseType == "lease" then
assert(years >= 1 and years <= 5, "Years is invalid. Must be an integer between 1 and 5")
end
tokenCost = arns.calculateRegistrationFee(purchaseType, baseFee, years, demand.getDemandFactor())
elseif intendedAction.intent == "Extend-Lease" then
local name = intendedAction.name
local years = intendedAction.years
local baseFee = demand.getFees()[#intendedAction.name]
assert(type(name) == "string", "Name is required and must be a string.")
elseif intent == "Extend-Lease" then
assert(years >= 1 and years <= 5, "Years is invalid. Must be an integer between 1 and 5")
local record = arns.getRecord(name)
if not record then
Expand All @@ -375,18 +375,14 @@ function arns.getTokenCost(intendedAction)
error("Name is permabought and cannot be extended")
end
tokenCost = arns.calculateExtensionFee(baseFee, years, demand.getDemandFactor())
elseif intendedAction.intent == "Increase-Undername-Limit" then
local name = intendedAction.name
local qty = tonumber(intendedAction.quantity)
local currentTimestamp = intendedAction.currentTimestamp
local baseFee = demand.getFees()[#intendedAction.name]
assert(type(name) == "string", "Name is required and must be a string.")
elseif intent == "Increase-Undername-Limit" then
assert(
qty >= 1 and qty <= 9990 and utils.isInteger(qty),
"Quantity is invalid, must be an integer between 1 and 9990"
)
local currentTimestamp = tonumber(intendedAction.currentTimestamp)
assert(type(currentTimestamp) == "number" and currentTimestamp > 0, "Timestamp is required")
local record = arns.getRecord(intendedAction.name)
local record = arns.getRecord(name)
if not record then
error("Name is not registered")
end
Expand All @@ -395,24 +391,113 @@ function arns.getTokenCost(intendedAction)
yearsRemaining = arns.calculateYearsBetweenTimestamps(currentTimestamp, record.endTimestamp)
end
tokenCost = arns.calculateUndernameCost(baseFee, qty, record.type, yearsRemaining, demand.getDemandFactor())
-- TODO: move action map to constants and use it here
elseif intent == "Upgrade-Name" then
local record = arns.getRecord(name)
if not record then
error("Name is not registered")
end

if record.type == "permabuy" then
error("Name is already a permabuy")
end
tokenCost = arns.calculatePermabuyFee(baseFee, demand.getDemandFactor())
end
return tokenCost
end

function arns.assertValidIncreaseUndername(record, qty, currentTimestamp)
--- Upgrades a leased record to a permabuy
--- @param from string The address of the sender
--- @param name string The name of the record
--- @param currentTimestamp number The current timestamp
--- @return table The upgraded record with name and record fields
function arns.upgradeRecord(from, name, currentTimestamp)
local record = arns.getRecord(name)
if not record then
error("Name is not registered")
end

if
record.endTimestamp
if record.type == "permabuy" then
error("Record is already a permabuy")
end
if arns.recordExpired(record, currentTimestamp) then
error("Name is expired")
end

local baseFee = demand.baseFeeForNameLength(#name)
local demandFactor = demand.getDemandFactor()
local upgradeCost = arns.calculatePermabuyFee(baseFee, demandFactor)

if not utils.walletHasSufficientBalance(from, upgradeCost) then
error("Insufficient balance")
end

record.endTimestamp = nil
record.type = "permabuy"
record.purchasePrice = upgradeCost

balances.transfer(ao.id, from, upgradeCost)
demand.tallyNamePurchase(upgradeCost)

NameRegistry.records[name] = record
return {
name = name,
record = record,
totalUpgradeFee = upgradeCost,
baseRegistrationFee = baseFee,
remainingBalance = balances.getBalance(from),
protocolBalance = balances.getBalance(ao.id),
df = demand.getDemandFactorInfo(),
}
end

--- Checks if a record is in the grace period
--- @param record table The record to check
--- @param currentTimestamp number The current timestamp
--- @return boolean True if the record is in the grace period, false otherwise (active or expired)
function arns.recordInGracePeriod(record, currentTimestamp)
return record.endTimestamp
and record.endTimestamp < currentTimestamp
and record.endTimestamp + constants.gracePeriodMs > currentTimestamp
then
end

--- Checks if a record is expired
--- @param record table The record to check
--- @param currentTimestamp number The current timestamp
--- @return boolean True if the record is expired, false otherwise (active or in grace period)
function arns.recordExpired(record, currentTimestamp)
if record.type == "permabuy" then
return false
end
local isActive = arns.recordIsActive(record, currentTimestamp)
local inGracePeriod = arns.recordInGracePeriod(record, currentTimestamp)
local expired = not isActive and not inGracePeriod
return expired
end

--- Checks if a record is active
--- @param record table The record to check
--- @param currentTimestamp number The current timestamp
--- @return boolean True if the record is active, false otherwise (expired or in grace period)
function arns.recordIsActive(record, currentTimestamp)
if record.type == "permabuy" then
return true
end

return record.endTimestamp and record.endTimestamp >= currentTimestamp
end

function arns.assertValidIncreaseUndername(record, qty, currentTimestamp)
if not record then
error("Name is not registered")
end

-- only records active and not in grace period can increase undernames
if arns.recordInGracePeriod(record, currentTimestamp) then
error("Name must be extended before additional unernames can be purchased")
end

if record.endTimestamp and record.endTimestamp + constants.gracePeriodMs < currentTimestamp then
if arns.recordExpired(record, currentTimestamp) then
error("Name is expired")
end

Expand All @@ -438,7 +523,7 @@ function arns.createAuction(name, timestamp, initiator)
error("Auction already exists for name")
end

local baseFee = demand.getFees()[#name]
local baseFee = demand.baseFeeForNameLength(#name)
local demandFactor = demand.getDemandFactor()
local auction = Auction:new(name, timestamp, demandFactor, baseFee, initiator, arns.calculateRegistrationFee)
NameRegistry.auctions[name] = auction
Expand Down
7 changes: 7 additions & 0 deletions src/demand.lua
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ function demand.tallyNamePurchase(qty)
demand.incrementRevenueThisPeriod(qty)
end

--- Gets the base fee for a given name length
--- @param nameLength number The length of the name
--- @return number The base fee for the name length
function demand.baseFeeForNameLength(nameLength)
return demand.getFees()[nameLength]
end

function demand.mvgAvgTrailingPurchaseCounts()
local sum = 0
local trailingPeriodPurchases = demand.getTrailingPeriodPurchases()
Expand Down
Loading

0 comments on commit 205993e

Please sign in to comment.