diff --git a/spec/arns_spec.lua b/spec/arns_spec.lua index ef28896..62f031c 100644 --- a/spec/arns_spec.lua +++ b/spec/arns_spec.lua @@ -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) @@ -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", diff --git a/spec/setup.lua b/spec/setup.lua index 3a27441..89f13b2 100644 --- a/spec/setup.lua +++ b/spec/setup.lua @@ -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", } diff --git a/src/arns.lua b/src/arns.lua index 5083f33..f3c84d7 100644 --- a/src/arns.lua +++ b/src/arns.lua @@ -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()) @@ -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 @@ -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()) @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/src/demand.lua b/src/demand.lua index c3b102b..4b9bfec 100644 --- a/src/demand.lua +++ b/src/demand.lua @@ -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() diff --git a/src/main.lua b/src/main.lua index 5358ced..39a67a8 100644 --- a/src/main.lua +++ b/src/main.lua @@ -70,7 +70,8 @@ local ActionMap = { VaultedTransfer = "Vaulted-Transfer", ExtendVault = "Extend-Vault", IncreaseVault = "Increase-Vault", - BuyRecord = "Buy-Record", + BuyRecord = "Buy-Record", -- TODO: standardize these as `Buy-Name` or `Upgrade-Record` + UpgradeName = "Upgrade-Name", -- TODO: may be more aligned to `Upgrade-Record` ExtendLease = "Extend-Lease", IncreaseUndernameLimit = "Increase-Undername-Limit", JoinNetwork = "Join-Network", @@ -721,6 +722,64 @@ addEventingHandler(ActionMap.BuyRecord, utils.hasMatchingTag("Action", ActionMap }) end) +addEventingHandler("upgradeName", utils.hasMatchingTag("Action", ActionMap.UpgradeName), function(msg) + local checkAssertions = function() + assert(type(msg.Tags.Name) == "string", "Invalid name") + assert(msg.Timestamp, "Timestamp is required") + end + + local shouldContinue = eventingPcall(msg.ioEvent, function(error) + ao.send({ + Target = msg.From, + Tags = { Action = "Invalid-" .. ActionMap.UpgradeName .. "-Notice", Error = "Bad-Input" }, + Data = tostring(error), + }) + end, checkAssertions) + if not shouldContinue then + return + end + + local name = string.lower(msg.Tags.Name) + local from = utils.formatAddress(msg.From) + local timestamp = tonumber(msg.Timestamp) + + local shouldContinue2, result = eventingPcall(msg.ioEvent, function(error) + ao.send({ + Target = msg.From, + Tags = { + Action = "Invalid-" .. ActionMap.UpgradeName .. "-Notice", + Error = "Invalid-" .. ActionMap.UpgradeName, + }, + Data = tostring(error), + }) + end, arns.upgradeRecord, from, name, timestamp) + if not shouldContinue2 then + return + end + + local record = {} + if result ~= nil then + record = result.record + addRecordResultFields(msg.ioEvent, result) + LastKnownCirculatingSupply = LastKnownCirculatingSupply - record.purchasePrice + addSupplyData(msg.ioEvent) + end + + ao.send({ + Target = from, + Tags = { Action = ActionMap.UpgradeName .. "-Notice", Name = name }, + Data = json.encode({ + name = name, + startTimestamp = record.startTimestamp, + endTimestamp = record.endTimestamp, + undernameLimit = record.undernameLimit, + purchasePrice = record.purchasePrice, + processId = record.processId, + type = record.type, + }), + }) +end) + addEventingHandler(ActionMap.ExtendLease, utils.hasMatchingTag("Action", ActionMap.ExtendLease), function(msg) local checkAssertions = function() assert(type(msg.Tags.Name) == "string", "Invalid name") diff --git a/tests/arns.test.mjs b/tests/arns.test.mjs index 42123c0..8136c2d 100644 --- a/tests/arns.test.mjs +++ b/tests/arns.test.mjs @@ -30,14 +30,6 @@ describe('ArNS', async () => { ); } - it('should get token info', async () => { - const result = await handle({ - Tags: [{ name: 'Action', value: 'Info' }], - }); - const tokenInfo = JSON.parse(result.Messages[0].Data); - assert(tokenInfo); - }); - const runBuyRecord = async ({ sender = STUB_ADDRESS, processId = ''.padEnd(43, 'a'), @@ -154,126 +146,48 @@ describe('ArNS', async () => { }; }; - it('should buy a record with an Arweave address', async () => { - await runBuyRecord({ sender: STUB_ADDRESS }); - }); - - it('should buy a record with an Ethereum address', async () => { - await runBuyRecord({ sender: testEthAddress }); - }); - - it('should fail to buy a permanently registered record', async () => { - const buyRecordResult = await handle({ - Tags: [ - { name: 'Action', value: 'Buy-Record' }, - { name: 'Name', value: 'test-name' }, - { name: 'Purchase-Type', value: 'permabuy' }, - { name: 'Process-Id', value: ''.padEnd(43, 'a') }, - ], + describe('Buy-Record', () => { + it('should buy a record with an Arweave address', async () => { + await runBuyRecord({ sender: STUB_ADDRESS }); }); - const buyRecordData = JSON.parse(buyRecordResult.Messages[0].Data); - - // fetch the record - const realRecord = await handle( - { - Tags: [ - { name: 'Action', value: 'Record' }, - { name: 'Name', value: 'test-name' }, - ], - }, - buyRecordResult.Memory, - ); - const record = JSON.parse(realRecord.Messages[0].Data); - assert.deepEqual(record, { - processId: ''.padEnd(43, 'a'), - purchasePrice: 2500000000, - startTimestamp: buyRecordData.startTimestamp, - type: 'permabuy', - undernameLimit: 10, + it('should buy a record with an Ethereum address', async () => { + await runBuyRecord({ sender: testEthAddress }); }); - const failedBuyRecordResult = await handle( - { + it('should fail to buy a permanently registered record', async () => { + const buyRecordResult = await handle({ Tags: [ { name: 'Action', value: 'Buy-Record' }, { name: 'Name', value: 'test-name' }, - { name: 'Purchase-Type', value: 'lease' }, - { name: 'Years', value: '1' }, + { name: 'Purchase-Type', value: 'permabuy' }, { name: 'Process-Id', value: ''.padEnd(43, 'a') }, ], - }, - buyRecordResult.Memory, - ); - - const failedBuyRecordError = failedBuyRecordResult.Messages[0].Tags.find( - (t) => t.name === 'Error', - ); - - assert.equal(failedBuyRecordError?.value, 'Invalid-Buy-Record'); - const alreadyRegistered = failedBuyRecordResult.Messages[0].Data.includes( - 'Name is already registered', - ); - assert(alreadyRegistered); - }); - - it('should buy a record and default the name to lower case', async () => { - const buyRecordResult = await handle({ - Tags: [ - { name: 'Action', value: 'Buy-Record' }, - { name: 'Name', value: 'Test-NAme' }, - { name: 'Purchase-Type', value: 'lease' }, - { name: 'Years', value: '1' }, - { name: 'Process-Id', value: ''.padEnd(43, 'a') }, - ], - }); - - const buyRecordData = JSON.parse(buyRecordResult.Messages[0].Data); - - // fetch the record - const realRecord = await handle( - { - Tags: [ - { name: 'Action', value: 'Record' }, - { name: 'Name', value: 'test-name' }, - ], - }, - buyRecordResult.Memory, - ); - - const record = JSON.parse(realRecord.Messages[0].Data); - assert.deepEqual(record, { - processId: ''.padEnd(43, 'a'), - purchasePrice: 600000000, - startTimestamp: buyRecordData.startTimestamp, - endTimestamp: buyRecordData.endTimestamp, - type: 'lease', - undernameLimit: 10, - }); - }); - - it('should increase the undernames', async () => { - const assertIncreaseUndername = async (sender) => { - let mem = startMemory; + }); + const buyRecordData = JSON.parse(buyRecordResult.Messages[0].Data); - if (sender != PROCESS_OWNER) { - const transferResult = await handle({ - From: PROCESS_OWNER, - Owner: PROCESS_OWNER, + // fetch the record + const realRecord = await handle( + { Tags: [ - { name: 'Action', value: 'Transfer' }, - { name: 'Recipient', value: sender }, - { name: 'Quantity', value: 6000000000 }, - { name: 'Cast', value: true }, + { name: 'Action', value: 'Record' }, + { name: 'Name', value: 'test-name' }, ], - }); - mem = transferResult.Memory; - } + }, + buyRecordResult.Memory, + ); + + const record = JSON.parse(realRecord.Messages[0].Data); + assert.deepEqual(record, { + processId: ''.padEnd(43, 'a'), + purchasePrice: 2500000000, + startTimestamp: buyRecordData.startTimestamp, + type: 'permabuy', + undernameLimit: 10, + }); - const buyUndernameResult = await handle( + const failedBuyRecordResult = await handle( { - From: sender, - Owner: sender, Tags: [ { name: 'Action', value: 'Buy-Record' }, { name: 'Name', value: 'test-name' }, @@ -282,768 +196,1002 @@ describe('ArNS', async () => { { name: 'Process-Id', value: ''.padEnd(43, 'a') }, ], }, - mem, + buyRecordResult.Memory, ); - const increaseUndernameResult = await handle( - { - From: sender, - Owner: sender, - Tags: [ - { name: 'Action', value: 'Increase-Undername-Limit' }, - { name: 'Name', value: 'test-name' }, - { name: 'Quantity', value: '1' }, - ], - }, - buyUndernameResult.Memory, + const failedBuyRecordError = failedBuyRecordResult.Messages[0].Tags.find( + (t) => t.name === 'Error', ); - const result = await handle( + + assert.equal(failedBuyRecordError?.value, 'Invalid-Buy-Record'); + const alreadyRegistered = failedBuyRecordResult.Messages[0].Data.includes( + 'Name is already registered', + ); + assert(alreadyRegistered); + }); + + it('should buy a record and default the name to lower case', async () => { + const buyRecordResult = await handle({ + Tags: [ + { name: 'Action', value: 'Buy-Record' }, + { name: 'Name', value: 'Test-NAme' }, + { name: 'Purchase-Type', value: 'lease' }, + { name: 'Years', value: '1' }, + { name: 'Process-Id', value: ''.padEnd(43, 'a') }, + ], + }); + + const buyRecordData = JSON.parse(buyRecordResult.Messages[0].Data); + + // fetch the record + const realRecord = await handle( { - From: sender, - Owner: sender, Tags: [ { name: 'Action', value: 'Record' }, { name: 'Name', value: 'test-name' }, ], }, - increaseUndernameResult.Memory, + buyRecordResult.Memory, ); - const record = JSON.parse(result.Messages[0].Data); - assert.equal(record.undernameLimit, 11); - }; - await assertIncreaseUndername(STUB_ADDRESS); - await assertIncreaseUndername(testEthAddress); - }); - //Reference: https://ardriveio.sharepoint.com/:x:/s/AR.IOLaunch/Ec3L8aX0wuZOlG7yRtlQoJgB39wCOoKu02PE_Y4iBMyu7Q?e=ZG750l - it('should get the costs buy record correctly', async () => { - const result = await handle({ - Tags: [ - { name: 'Action', value: 'Token-Cost' }, - { name: 'Intent', value: 'Buy-Record' }, - { name: 'Name', value: 'test-name' }, - { name: 'Purchase-Type', value: 'lease' }, - { name: 'Years', value: '1' }, - { name: 'Process-Id', value: ''.padEnd(43, 'a') }, - ], + const record = JSON.parse(realRecord.Messages[0].Data); + assert.deepEqual(record, { + processId: ''.padEnd(43, 'a'), + purchasePrice: 600000000, + startTimestamp: buyRecordData.startTimestamp, + endTimestamp: buyRecordData.endTimestamp, + type: 'lease', + undernameLimit: 10, + }); }); - const tokenCost = JSON.parse(result.Messages[0].Data); - assert.equal(tokenCost, 600000000); }); - it('should get registration fees', async () => { - const priceListResult = await handle({ - Tags: [{ name: 'Action', value: 'Get-Registration-Fees' }], + describe('Increase-Undername-Limit', () => { + it('should increase the undernames', async () => { + const assertIncreaseUndername = async (sender) => { + let mem = startMemory; + + if (sender != PROCESS_OWNER) { + const transferResult = await handle({ + From: PROCESS_OWNER, + Owner: PROCESS_OWNER, + Tags: [ + { name: 'Action', value: 'Transfer' }, + { name: 'Recipient', value: sender }, + { name: 'Quantity', value: 6000000000 }, + { name: 'Cast', value: true }, + ], + }); + mem = transferResult.Memory; + } + + const buyUndernameResult = await handle( + { + From: sender, + Owner: sender, + Tags: [ + { name: 'Action', value: 'Buy-Record' }, + { name: 'Name', value: 'test-name' }, + { name: 'Purchase-Type', value: 'lease' }, + { name: 'Years', value: '1' }, + { name: 'Process-Id', value: ''.padEnd(43, 'a') }, + ], + }, + mem, + ); + + const increaseUndernameResult = await handle( + { + From: sender, + Owner: sender, + Tags: [ + { name: 'Action', value: 'Increase-Undername-Limit' }, + { name: 'Name', value: 'test-name' }, + { name: 'Quantity', value: '1' }, + ], + }, + buyUndernameResult.Memory, + ); + const result = await handle( + { + From: sender, + Owner: sender, + Tags: [ + { name: 'Action', value: 'Record' }, + { name: 'Name', value: 'test-name' }, + ], + }, + increaseUndernameResult.Memory, + ); + const record = JSON.parse(result.Messages[0].Data); + assert.equal(record.undernameLimit, 11); + }; + await assertIncreaseUndername(STUB_ADDRESS); + await assertIncreaseUndername(testEthAddress); }); + }); + + describe('Get-Registration-Fees', () => { + it('should return the base registration fees for each name length', async () => { + const priceListResult = await handle({ + Tags: [{ name: 'Action', value: 'Get-Registration-Fees' }], + }); - const priceList = JSON.parse(priceListResult.Messages[0].Data); - // check that each key has lease with years and permabuy prices - assert(Object.keys(priceList).length == 51); - Object.keys(priceList).forEach((key) => { - assert(priceList[key].lease); - assert(priceList[key].permabuy); - assert(Object.keys(priceList[key].lease).length == 5); + const priceList = JSON.parse(priceListResult.Messages[0].Data); + // check that each key has lease with years and permabuy prices + assert(Object.keys(priceList).length == 51); + Object.keys(priceList).forEach((key) => { + assert(priceList[key].lease); + assert(priceList[key].permabuy); + assert(Object.keys(priceList[key].lease).length == 5); + }); }); }); - it('should get the costs increase undername correctly', async () => { - const buyUndernameResult = await handle({ - Tags: [ - { name: 'Action', value: 'Buy-Record' }, - { name: 'Name', value: 'test-name' }, - { name: 'Purchase-Type', value: 'lease' }, - { name: 'Years', value: '1' }, - { name: 'Process-Id', value: ''.padEnd(43, 'a') }, - ], - }); - const result = await handle( - { + describe('Token-Cost', () => { + //Reference: https://ardriveio.sharepoint.com/:x:/s/AR.IOLaunch/Ec3L8aX0wuZOlG7yRtlQoJgB39wCOoKu02PE_Y4iBMyu7Q?e=ZG750l + it('should return the correct cost of buying a name as a lease', async () => { + const result = await handle({ Tags: [ { name: 'Action', value: 'Token-Cost' }, - { name: 'Intent', value: 'Increase-Undername-Limit' }, + { name: 'Intent', value: 'Buy-Record' }, { name: 'Name', value: 'test-name' }, - { name: 'Quantity', value: '1' }, + { name: 'Purchase-Type', value: 'lease' }, + { name: 'Years', value: '1' }, + { name: 'Process-Id', value: ''.padEnd(43, 'a') }, ], - }, - buyUndernameResult.Memory, - ); - const tokenCost = JSON.parse(result.Messages[0].Data); - const expectedPrice = 500000000 * 0.001 * 1 * 1; - assert.equal(tokenCost, expectedPrice); - }); - - it('should get the cost of increasing a lease correctly', async () => { - const buyUndernameResult = await handle({ - Tags: [ - { name: 'Action', value: 'Buy-Record' }, - { name: 'Name', value: 'test-name' }, - { name: 'Purchase-Type', value: 'lease' }, - { name: 'Years', value: '1' }, - { name: 'Process-Id', value: ''.padEnd(43, 'a') }, - ], + }); + const tokenCost = JSON.parse(result.Messages[0].Data); + assert.equal(tokenCost, 600000000); }); - const recordResultBefore = await handle( - { - Tags: [ - { name: 'Action', value: 'Record' }, - { name: 'Name', value: 'test-name' }, - ], - }, - buyUndernameResult.Memory, - ); - const recordBefore = JSON.parse(recordResultBefore.Messages[0].Data); - const extendResult = await handle( - { + it('should return the correct cost of increasing an undername limit', async () => { + const buyRecordResult = await handle({ Tags: [ - { name: 'Action', value: 'Extend-Lease' }, + { name: 'Action', value: 'Buy-Record' }, { name: 'Name', value: 'test-name' }, + { name: 'Purchase-Type', value: 'lease' }, { name: 'Years', value: '1' }, + { name: 'Process-Id', value: ''.padEnd(43, 'a') }, ], - }, - buyUndernameResult.Memory, - ); - const recordResult = await handle( - { - Tags: [ - { name: 'Action', value: 'Record' }, - { name: 'Name', value: 'test-name' }, - ], - }, - extendResult.Memory, - ); - const record = JSON.parse(recordResult.Messages[0].Data); - assert.equal( - record.endTimestamp, - recordBefore.endTimestamp + 60 * 1000 * 60 * 24 * 365, - ); - }); + }); + + // assert no error tag + const buyRecordErrorTag = buyRecordResult.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Error', + ); + assert.equal(buyRecordErrorTag, undefined); - it('should create an auction for an existing permabuy record owned by a process id, accept a bid and add the new record to the registry', async () => { - // buy the name first - const processId = ''.padEnd(43, 'a'); - const { mem, record: initialRecord } = await runBuyRecord({ - sender: STUB_ADDRESS, - processId, - type: 'permabuy', + const result = await handle( + { + Tags: [ + { name: 'Action', value: 'Token-Cost' }, + { name: 'Intent', value: 'Increase-Undername-Limit' }, + { name: 'Name', value: 'test-name' }, + { name: 'Quantity', value: '1' }, + ], + }, + buyRecordResult.Memory, + ); + const tokenCost = JSON.parse(result.Messages[0].Data); + const expectedPrice = 500000000 * 0.001 * 1 * 1; + assert.equal(tokenCost, expectedPrice); }); - const releaseNameResult = await handle( - { + it('should return the correct cost of extending an existing leased record', async () => { + const buyRecordResult = await handle({ Tags: [ - { name: 'Action', value: 'Release-Name' }, + { name: 'Action', value: 'Buy-Record' }, { name: 'Name', value: 'test-name' }, - { name: 'Initiator', value: 'test-owner-of-ant' }, // simulate who the owner is of the ANT process when sending the message + { name: 'Purchase-Type', value: 'lease' }, + { name: 'Years', value: '1' }, + { name: 'Process-Id', value: ''.padEnd(43, 'a') }, ], - From: processId, - Owner: processId, - }, - mem, - ); + }); - // assert no error tag - const releaseNameErrorTag = releaseNameResult.Messages?.[0]?.Tags?.find( - (tag) => tag.name === 'Error', - ); - assert.equal(releaseNameErrorTag, undefined); + // assert no error tag + const buyRecordErrorTag = buyRecordResult.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Error', + ); + assert.equal(buyRecordErrorTag, undefined); - // fetch the auction - const auctionResult = await handle( - { + const result = await handle( + { + Tags: [ + { name: 'Action', value: 'Token-Cost' }, + { name: 'Intent', value: 'Extend-Lease' }, + { name: 'Name', value: 'test-name' }, + { name: 'Years', value: '2' }, + ], + }, + buyRecordResult.Memory, + ); + const tokenCost = JSON.parse(result.Messages[0].Data); + assert.equal(tokenCost, 200000000); // known cost for extending a 9 character name by 2 years (500 IO * 0.2 * 2) + }); + + it('should get the cost of upgrading an existing leased record to a permabuy', async () => { + const buyRecordResult = await handle({ Tags: [ - { name: 'Action', value: 'Auction-Info' }, + { name: 'Action', value: 'Buy-Record' }, { name: 'Name', value: 'test-name' }, + { name: 'Purchase-Type', value: 'lease' }, + { name: 'Years', value: '1' }, + { name: 'Process-Id', value: ''.padEnd(43, 'a') }, ], - }, - releaseNameResult.Memory, - ); - // assert no error tag - const auctionErrorTag = auctionResult.Messages?.[0]?.Tags?.find( - (tag) => tag.name === 'Error', - ); + }); - assert.equal(auctionErrorTag, undefined); - const auction = JSON.parse(auctionResult.Messages?.[0]?.Data); - const expectedStartPrice = 125000000000; - const expectedStartTimestamp = STUB_TIMESTAMP; - assert.deepEqual(auction, { - name: 'test-name', - initiator: 'test-owner-of-ant', - startTimestamp: auction.startTimestamp, - endTimestamp: expectedStartTimestamp + 60 * 60 * 1000 * 24 * 14, - baseFee: 500000000, - demandFactor: 1, - settings: { - decayRate: 0.02037911 / (1000 * 60 * 60 * 24 * 14), - scalingExponent: 190, - durationMs: 1209600000, - startPriceMultiplier: 50, - }, + // assert no error tag + const buyRecordErrorTag = buyRecordResult.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Error', + ); + assert.equal(buyRecordErrorTag, undefined); + + const upgradeNameResult = await handle( + { + Tags: [ + { name: 'Action', value: 'Token-Cost' }, + { name: 'Intent', value: 'Upgrade-Name' }, + { name: 'Name', value: 'test-name' }, + ], + }, + buyRecordResult.Memory, + ); + + const tokenCost = JSON.parse(upgradeNameResult.Messages[0].Data); + assert.equal(tokenCost, 2500000000); }); + }); - // // TRANSFER FROM THE OWNER TO A NEW STUB ADDRESS - const bidderAddress = 'auction-bidder-'.padEnd(43, '0'); - const bidTimestamp = auction.startTimestamp + 60 * 1000; // same as the original interval but 1 minute after the auction has started - const decayRate = auction.settings.decayRate; - const expectedPurchasePrice = Math.floor( - expectedStartPrice * - (1 - decayRate * (bidTimestamp - auction.startTimestamp)) ** - auction.settings.scalingExponent, - ); - const transferResult = await handle( - { - From: PROCESS_OWNER, - Owner: PROCESS_OWNER, + describe('Extend-Lease', () => { + it('should properly handle extending a leased record', async () => { + const buyUndernameResult = await handle({ Tags: [ - { name: 'Action', value: 'Transfer' }, - { name: 'Recipient', value: bidderAddress }, - { name: 'Quantity', value: expectedPurchasePrice }, - { name: 'Cast', value: true }, + { name: 'Action', value: 'Buy-Record' }, + { name: 'Name', value: 'test-name' }, + { name: 'Purchase-Type', value: 'lease' }, + { name: 'Years', value: '1' }, + { name: 'Process-Id', value: ''.padEnd(43, 'a') }, ], - }, - releaseNameResult.Memory, - ); - - // assert no error in the transfer - const transferErrorTag = transferResult.Messages?.[0]?.Tags?.find( - (tag) => tag.name === 'Error', - ); + }); + const recordResultBefore = await handle( + { + Tags: [ + { name: 'Action', value: 'Record' }, + { name: 'Name', value: 'test-name' }, + ], + }, + buyUndernameResult.Memory, + ); + const recordBefore = JSON.parse(recordResultBefore.Messages[0].Data); + const extendResult = await handle( + { + Tags: [ + { name: 'Action', value: 'Extend-Lease' }, + { name: 'Name', value: 'test-name' }, + { name: 'Years', value: '1' }, + ], + }, + buyUndernameResult.Memory, + ); + const recordResult = await handle( + { + Tags: [ + { name: 'Action', value: 'Record' }, + { name: 'Name', value: 'test-name' }, + ], + }, + extendResult.Memory, + ); + const record = JSON.parse(recordResult.Messages[0].Data); + assert.equal( + record.endTimestamp, + recordBefore.endTimestamp + 60 * 1000 * 60 * 24 * 365, + ); + }); + }); - assert.equal(transferErrorTag, undefined); - const submitBidResult = await handle( - { - From: bidderAddress, - Owner: bidderAddress, + describe('Upgrade-Name', () => { + it('should properly handle upgrading a name', async () => { + const buyRecordTimestamp = STUB_TIMESTAMP + 1; + const buyRecordResult = await handle({ Tags: [ - { name: 'Action', value: 'Auction-Bid' }, + { name: 'Action', value: 'Buy-Record' }, { name: 'Name', value: 'test-name' }, - { name: 'Process-Id', value: processId }, + { name: 'Purchase-Type', value: 'lease' }, + { name: 'Years', value: '1' }, + { name: 'Process-Id', value: ''.padEnd(43, 'a') }, ], - Timestamp: bidTimestamp, - }, - transferResult.Memory, - ); + Timestamp: buyRecordTimestamp, + }); - // assert no error tag - const submitBidErrorTag = submitBidResult.Messages[0].Tags.find( - (tag) => tag.name === 'Error', - ); - assert.equal(submitBidErrorTag, undefined); + // assert no error tag + const buyRecordErrorTag = buyRecordResult.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Error', + ); + assert.equal(buyRecordErrorTag, undefined); - // should send three messages including a Buy-Record-Notice and a Debit-Notice - assert.equal(submitBidResult.Messages.length, 2); + // now upgrade the name + const upgradeNameResult = await handle( + { + Tags: [ + { name: 'Action', value: 'Upgrade-Name' }, + { name: 'Name', value: 'test-name' }, + ], + Timestamp: buyRecordTimestamp + 1, + }, + buyRecordResult.Memory, + ); - // should send a buy record notice - const buyRecordNoticeTag = submitBidResult.Messages?.[0]?.Tags?.find( - (tag) => tag.name === 'Action' && tag.value === 'Buy-Record-Notice', - ); + // assert no error tag + const upgradeNameErrorTag = upgradeNameResult.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Error', + ); + assert.equal(upgradeNameErrorTag, undefined); - assert.ok(buyRecordNoticeTag); + // assert the message includes the upgrade name notice + const upgradeNameNoticeTag = upgradeNameResult.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Action' && tag.value === 'Upgrade-Name-Notice', + ); - // expect the target tag to be the bidder - assert.equal(submitBidResult.Messages?.[0]?.Target, bidderAddress); + assert.ok(upgradeNameNoticeTag); - const expectedRecord = { - processId, - purchasePrice: expectedPurchasePrice, - startTimestamp: bidTimestamp, - undernameLimit: 10, - type: 'permabuy', - }; - const expectedRewardForInitiator = Math.floor(expectedPurchasePrice * 0.5); - const expectedRewardForProtocol = - expectedPurchasePrice - expectedRewardForInitiator; - - // assert the data response contains the record - const buyRecordNoticeData = JSON.parse(submitBidResult.Messages?.[0]?.Data); - assert.deepEqual(buyRecordNoticeData, { - name: 'test-name', - ...expectedRecord, + const upgradedNameData = JSON.parse( + upgradeNameResult.Messages?.[0]?.Data, + ); + assert.deepStrictEqual(upgradedNameData, { + name: 'test-name', + type: 'permabuy', + startTimestamp: buyRecordTimestamp, + processId: ''.padEnd(43, 'a'), + undernameLimit: 10, + purchasePrice: 2500000000, // expected price for a permabuy of a 9 character name + }); }); + }); - // should send a debit notice - const debitNoticeTag = submitBidResult.Messages?.[1]?.Tags?.find( - (tag) => tag.name === 'Action' && tag.value === 'Debit-Notice', - ); - assert.ok(debitNoticeTag); - - // expect the target to be to the initiator - assert.equal(submitBidResult.Messages?.[1]?.Target, 'test-owner-of-ant'); - - // assert the data response contains the record - const debitNoticeData = JSON.parse(submitBidResult.Messages?.[1]?.Data); - assert.deepEqual(debitNoticeData, { - record: expectedRecord, - bidder: bidderAddress, - bidAmount: expectedPurchasePrice, - rewardForInitiator: expectedRewardForInitiator, - rewardForProtocol: expectedRewardForProtocol, - name: 'test-name', - }); + describe('Release-Name', () => { + it('should create an auction for an existing permabuy record owned by a process id, accept a bid and add the new record to the registry', async () => { + // buy the name first + const processId = ''.padEnd(43, 'a'); + const { mem, record: initialRecord } = await runBuyRecord({ + sender: STUB_ADDRESS, + processId, + type: 'permabuy', + }); - // should add the record to the registry - const recordResult = await handle( - { - Tags: [ - { name: 'Action', value: 'Record' }, - { name: 'Name', value: 'test-name' }, - ], - Timestamp: bidTimestamp, - }, - submitBidResult.Memory, - ); + const releaseNameResult = await handle( + { + Tags: [ + { name: 'Action', value: 'Release-Name' }, + { name: 'Name', value: 'test-name' }, + { name: 'Initiator', value: 'test-owner-of-ant' }, // simulate who the owner is of the ANT process when sending the message + ], + From: processId, + Owner: processId, + }, + mem, + ); - const record = JSON.parse(recordResult.Messages?.[0]?.Data); - assert.deepEqual(record, { - processId, - purchasePrice: expectedPurchasePrice, - startTimestamp: bidTimestamp, - undernameLimit: 10, - type: 'permabuy', - }); + // assert no error tag + const releaseNameErrorTag = releaseNameResult.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Error', + ); + assert.equal(releaseNameErrorTag, undefined); - // assert the balance of the initiator and the protocol where updated correctly - const balancesResult = await handle( - { - Tags: [{ name: 'Action', value: 'Balances' }], - }, - submitBidResult.Memory, - ); + // fetch the auction + const auctionResult = await handle( + { + Tags: [ + { name: 'Action', value: 'Auction-Info' }, + { name: 'Name', value: 'test-name' }, + ], + }, + releaseNameResult.Memory, + ); + // assert no error tag + const auctionErrorTag = auctionResult.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Error', + ); - const expectedProtocolBalance = - INITIAL_PROTOCOL_BALANCE + - initialRecord.purchasePrice + - expectedRewardForProtocol; - const balances = JSON.parse(balancesResult.Messages[0].Data); - assert.equal(balances['test-owner-of-ant'], expectedRewardForInitiator); - assert.equal(balances[PROCESS_ID], expectedProtocolBalance); - assert.equal(balances[bidderAddress], 0); - }); + assert.equal(auctionErrorTag, undefined); + const auction = JSON.parse(auctionResult.Messages?.[0]?.Data); + const expectedStartPrice = 125000000000; + const expectedStartTimestamp = STUB_TIMESTAMP; + assert.deepEqual(auction, { + name: 'test-name', + initiator: 'test-owner-of-ant', + startTimestamp: auction.startTimestamp, + endTimestamp: expectedStartTimestamp + 60 * 60 * 1000 * 24 * 14, + baseFee: 500000000, + demandFactor: 1, + settings: { + decayRate: 0.02037911 / (1000 * 60 * 60 * 24 * 14), + scalingExponent: 190, + durationMs: 1209600000, + startPriceMultiplier: 50, + }, + }); - // TODO: add a test to create a lease expiration intiaited auction and accept a bid - it('should create a lease expiration intiaited auction and accept a bid', async () => { - const { record: initialRecord, mem } = await runBuyRecord({ - sender: STUB_ADDRESS, - processId: ''.padEnd(43, 'a'), - type: 'lease', - years: 1, - }); + // // TRANSFER FROM THE OWNER TO A NEW STUB ADDRESS + const bidderAddress = 'auction-bidder-'.padEnd(43, '0'); + const bidTimestamp = auction.startTimestamp + 60 * 1000; // same as the original interval but 1 minute after the auction has started + const decayRate = auction.settings.decayRate; + const expectedPurchasePrice = Math.floor( + expectedStartPrice * + (1 - decayRate * (bidTimestamp - auction.startTimestamp)) ** + auction.settings.scalingExponent, + ); + const transferResult = await handle( + { + From: PROCESS_OWNER, + Owner: PROCESS_OWNER, + Tags: [ + { name: 'Action', value: 'Transfer' }, + { name: 'Recipient', value: bidderAddress }, + { name: 'Quantity', value: expectedPurchasePrice }, + { name: 'Cast', value: true }, + ], + }, + releaseNameResult.Memory, + ); - // tick the contract after the lease leaves its grace period - const futureTimestamp = - initialRecord.endTimestamp + 60 * 1000 * 60 * 24 * 14 + 1; - const tickResult = await handle( - { - Tags: [{ name: 'Action', value: 'Tick' }], - Timestamp: futureTimestamp, - }, - mem, - ); + // assert no error in the transfer + const transferErrorTag = transferResult.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Error', + ); - // fetch the auction - const auctionResult = await handle( - { - Tags: [ - { name: 'Action', value: 'Auction-Info' }, - { name: 'Name', value: 'test-name' }, - ], - }, - tickResult.Memory, - ); - // assert no error tag - const auctionErrorTag = auctionResult.Messages?.[0]?.Tags?.find( - (tag) => tag.name === 'Error', - ); + assert.equal(transferErrorTag, undefined); + const submitBidResult = await handle( + { + From: bidderAddress, + Owner: bidderAddress, + Tags: [ + { name: 'Action', value: 'Auction-Bid' }, + { name: 'Name', value: 'test-name' }, + { name: 'Process-Id', value: processId }, + ], + Timestamp: bidTimestamp, + }, + transferResult.Memory, + ); - assert.equal(auctionErrorTag, undefined); - const auction = JSON.parse(auctionResult.Messages?.[0]?.Data); - assert.deepEqual(auction, { - name: 'test-name', - initiator: PROCESS_ID, - startTimestamp: futureTimestamp, - endTimestamp: futureTimestamp + 60 * 60 * 1000 * 24 * 14, - baseFee: 500000000, - demandFactor: 1, - settings: { - decayRate: 0.02037911 / (1000 * 60 * 60 * 24 * 14), - scalingExponent: 190, - durationMs: 1209600000, - startPriceMultiplier: 50, - }, - }); + // assert no error tag + const submitBidErrorTag = submitBidResult.Messages[0].Tags.find( + (tag) => tag.name === 'Error', + ); + assert.equal(submitBidErrorTag, undefined); - // // TRANSFER FROM THE OWNER TO A NEW STUB ADDRESS - const bidYears = 3; - const expectedFloorPrice = Math.floor( - auction.baseFee + auction.baseFee * bidYears * 0.2, - ); - const expectedStartPrice = Math.floor( - expectedFloorPrice * auction.settings.startPriceMultiplier, - ); - const bidderAddress = 'auction-bidder-'.padEnd(43, '0'); - const bidTimestamp = futureTimestamp + 60 * 60 * 1000 * 24 * 7; // 7 days into the auction - const expectedPurchasePrice = Math.floor( - expectedStartPrice * - (1 - auction.settings.decayRate * (bidTimestamp - futureTimestamp)) ** - auction.settings.scalingExponent, - ); - const transferResult = await handle( - { - From: PROCESS_OWNER, - Owner: PROCESS_OWNER, - Tags: [ - { name: 'Action', value: 'Transfer' }, - { name: 'Recipient', value: bidderAddress }, - { name: 'Quantity', value: `${expectedPurchasePrice}` }, - { name: 'Cast', value: true }, - ], - Timestamp: bidTimestamp - 1, - }, - tickResult.Memory, - ); + // should send three messages including a Buy-Record-Notice and a Debit-Notice + assert.equal(submitBidResult.Messages.length, 2); - // assert no error in the transfer - const transferErrorTag = transferResult.Messages?.[0]?.Tags?.find( - (tag) => tag.name === 'Error', - ); + // should send a buy record notice + const buyRecordNoticeTag = submitBidResult.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Action' && tag.value === 'Buy-Record-Notice', + ); - assert.equal(transferErrorTag, undefined); - const processId = 'new-name-owner-'.padEnd(43, '1'); - const submitBidResult = await handle( - { - From: bidderAddress, - Owner: bidderAddress, - Tags: [ - { name: 'Action', value: 'Auction-Bid' }, - { name: 'Name', value: 'test-name' }, - { name: 'Process-Id', value: processId }, - { name: 'Purchase-Type', value: 'lease' }, - { name: 'Years', value: bidYears }, - ], - Timestamp: bidTimestamp, - }, - transferResult.Memory, - ); + assert.ok(buyRecordNoticeTag); - // assert no error tag - const submitBidErrorTag = submitBidResult.Messages[0].Tags.find( - (tag) => tag.name === 'Error', - ); - assert.equal(submitBidErrorTag, undefined); + // expect the target tag to be the bidder + assert.equal(submitBidResult.Messages?.[0]?.Target, bidderAddress); - // should send three messages including a Buy-Record-Notice and a Debit-Notice - assert.equal(submitBidResult.Messages.length, 2); + const expectedRecord = { + processId, + purchasePrice: expectedPurchasePrice, + startTimestamp: bidTimestamp, + undernameLimit: 10, + type: 'permabuy', + }; + const expectedRewardForInitiator = Math.floor( + expectedPurchasePrice * 0.5, + ); + const expectedRewardForProtocol = + expectedPurchasePrice - expectedRewardForInitiator; - // should send a buy record notice - const buyRecordNoticeTag = submitBidResult.Messages?.[0]?.Tags?.find( - (tag) => tag.name === 'Action' && tag.value === 'Buy-Record-Notice', - ); + // assert the data response contains the record + const buyRecordNoticeData = JSON.parse( + submitBidResult.Messages?.[0]?.Data, + ); + assert.deepEqual(buyRecordNoticeData, { + name: 'test-name', + ...expectedRecord, + }); - assert.ok(buyRecordNoticeTag); + // should send a debit notice + const debitNoticeTag = submitBidResult.Messages?.[1]?.Tags?.find( + (tag) => tag.name === 'Action' && tag.value === 'Debit-Notice', + ); + assert.ok(debitNoticeTag); + + // expect the target to be to the initiator + assert.equal(submitBidResult.Messages?.[1]?.Target, 'test-owner-of-ant'); + + // assert the data response contains the record + const debitNoticeData = JSON.parse(submitBidResult.Messages?.[1]?.Data); + assert.deepEqual(debitNoticeData, { + record: expectedRecord, + bidder: bidderAddress, + bidAmount: expectedPurchasePrice, + rewardForInitiator: expectedRewardForInitiator, + rewardForProtocol: expectedRewardForProtocol, + name: 'test-name', + }); - // expect the target tag to be the bidder - assert.equal(submitBidResult.Messages?.[0]?.Target, bidderAddress); + // should add the record to the registry + const recordResult = await handle( + { + Tags: [ + { name: 'Action', value: 'Record' }, + { name: 'Name', value: 'test-name' }, + ], + Timestamp: bidTimestamp, + }, + submitBidResult.Memory, + ); - const expectedRecord = { - processId, - purchasePrice: expectedPurchasePrice, - startTimestamp: bidTimestamp, - endTimestamp: bidTimestamp + 60 * 60 * 1000 * 24 * 365 * 3, - undernameLimit: 10, - type: 'lease', - }; - // the protocol gets the entire bid amount - const expectedRewardForProtocol = expectedPurchasePrice; - - // assert the data response contains the record - const buyRecordNoticeData = JSON.parse(submitBidResult.Messages?.[0]?.Data); - assert.deepEqual(buyRecordNoticeData, { - name: 'test-name', - ...expectedRecord, - }); + const record = JSON.parse(recordResult.Messages?.[0]?.Data); + assert.deepEqual(record, { + processId, + purchasePrice: expectedPurchasePrice, + startTimestamp: bidTimestamp, + undernameLimit: 10, + type: 'permabuy', + }); - // should send a debit notice - const debitNoticeTag = submitBidResult.Messages?.[1]?.Tags?.find( - (tag) => tag.name === 'Action' && tag.value === 'Debit-Notice', - ); - assert.ok(debitNoticeTag); - - // expect the target to be to the protocol balance - assert.equal(submitBidResult.Messages?.[1]?.Target, PROCESS_ID); - - // assert the data response contains the record - const debitNoticeData = JSON.parse(submitBidResult.Messages?.[1]?.Data); - assert.deepEqual(debitNoticeData, { - record: expectedRecord, - bidder: bidderAddress, - bidAmount: expectedPurchasePrice, - rewardForInitiator: 0, - rewardForProtocol: expectedRewardForProtocol, - name: 'test-name', + // assert the balance of the initiator and the protocol where updated correctly + const balancesResult = await handle( + { + Tags: [{ name: 'Action', value: 'Balances' }], + }, + submitBidResult.Memory, + ); + + const expectedProtocolBalance = + INITIAL_PROTOCOL_BALANCE + + initialRecord.purchasePrice + + expectedRewardForProtocol; + const balances = JSON.parse(balancesResult.Messages[0].Data); + assert.equal(balances['test-owner-of-ant'], expectedRewardForInitiator); + assert.equal(balances[PROCESS_ID], expectedProtocolBalance); + assert.equal(balances[bidderAddress], 0); }); - // should add the record to the registry - const recordResult = await handle( - { - Tags: [ - { name: 'Action', value: 'Record' }, - { name: 'Name', value: 'test-name' }, - ], - Timestamp: bidTimestamp, - }, - submitBidResult.Memory, - ); + it('should create a lease expiration initiated auction and accept a bid', async () => { + const { record: initialRecord, mem } = await runBuyRecord({ + sender: STUB_ADDRESS, + processId: ''.padEnd(43, 'a'), + type: 'lease', + years: 1, + }); - const record = JSON.parse(recordResult.Messages?.[0]?.Data); - assert.deepEqual(record, expectedRecord); + // tick the contract after the lease leaves its grace period + const futureTimestamp = + initialRecord.endTimestamp + 60 * 1000 * 60 * 24 * 14 + 1; + const tickResult = await handle( + { + Tags: [{ name: 'Action', value: 'Tick' }], + Timestamp: futureTimestamp, + }, + mem, + ); - // assert the balance of the initiator and the protocol where updated correctly - const balancesResult = await handle( - { - Tags: [{ name: 'Action', value: 'Balances' }], - }, - submitBidResult.Memory, - ); + // fetch the auction + const auctionResult = await handle( + { + Tags: [ + { name: 'Action', value: 'Auction-Info' }, + { name: 'Name', value: 'test-name' }, + ], + }, + tickResult.Memory, + ); + // assert no error tag + const auctionErrorTag = auctionResult.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Error', + ); - const expectedProtocolBalance = - INITIAL_PROTOCOL_BALANCE + - initialRecord.purchasePrice + - expectedRewardForProtocol; - const balances = JSON.parse(balancesResult.Messages[0].Data); - assert.equal(balances[PROCESS_ID], expectedProtocolBalance); - assert.equal(balances[bidderAddress], 0); - }); + assert.equal(auctionErrorTag, undefined); + const auction = JSON.parse(auctionResult.Messages?.[0]?.Data); + assert.deepEqual(auction, { + name: 'test-name', + initiator: PROCESS_ID, + startTimestamp: futureTimestamp, + endTimestamp: futureTimestamp + 60 * 60 * 1000 * 24 * 14, + baseFee: 500000000, + demandFactor: 1, + settings: { + decayRate: 0.02037911 / (1000 * 60 * 60 * 24 * 14), + scalingExponent: 190, + durationMs: 1209600000, + startPriceMultiplier: 50, + }, + }); + + // // TRANSFER FROM THE OWNER TO A NEW STUB ADDRESS + const bidYears = 3; + const expectedFloorPrice = Math.floor( + auction.baseFee + auction.baseFee * bidYears * 0.2, + ); + const expectedStartPrice = Math.floor( + expectedFloorPrice * auction.settings.startPriceMultiplier, + ); + const bidderAddress = 'auction-bidder-'.padEnd(43, '0'); + const bidTimestamp = futureTimestamp + 60 * 60 * 1000 * 24 * 7; // 7 days into the auction + const expectedPurchasePrice = Math.floor( + expectedStartPrice * + (1 - auction.settings.decayRate * (bidTimestamp - futureTimestamp)) ** + auction.settings.scalingExponent, + ); + const transferResult = await handle( + { + From: PROCESS_OWNER, + Owner: PROCESS_OWNER, + Tags: [ + { name: 'Action', value: 'Transfer' }, + { name: 'Recipient', value: bidderAddress }, + { name: 'Quantity', value: `${expectedPurchasePrice}` }, + { name: 'Cast', value: true }, + ], + Timestamp: bidTimestamp - 1, + }, + tickResult.Memory, + ); + + // assert no error in the transfer + const transferErrorTag = transferResult.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Error', + ); + + assert.equal(transferErrorTag, undefined); + const processId = 'new-name-owner-'.padEnd(43, '1'); + const submitBidResult = await handle( + { + From: bidderAddress, + Owner: bidderAddress, + Tags: [ + { name: 'Action', value: 'Auction-Bid' }, + { name: 'Name', value: 'test-name' }, + { name: 'Process-Id', value: processId }, + { name: 'Purchase-Type', value: 'lease' }, + { name: 'Years', value: bidYears }, + ], + Timestamp: bidTimestamp, + }, + transferResult.Memory, + ); + + // assert no error tag + const submitBidErrorTag = submitBidResult.Messages[0].Tags.find( + (tag) => tag.name === 'Error', + ); + assert.equal(submitBidErrorTag, undefined); + + // should send three messages including a Buy-Record-Notice and a Debit-Notice + assert.equal(submitBidResult.Messages.length, 2); + + // should send a buy record notice + const buyRecordNoticeTag = submitBidResult.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Action' && tag.value === 'Buy-Record-Notice', + ); - it('should compute the prices of an auction at a specific interval', async () => { - // buy the name first - const processId = ''.padEnd(43, 'a'); - const { mem } = await runBuyRecord({ - sender: STUB_ADDRESS, - processId, - type: 'permabuy', + assert.ok(buyRecordNoticeTag); + + // expect the target tag to be the bidder + assert.equal(submitBidResult.Messages?.[0]?.Target, bidderAddress); + + const expectedRecord = { + processId, + purchasePrice: expectedPurchasePrice, + startTimestamp: bidTimestamp, + endTimestamp: bidTimestamp + 60 * 60 * 1000 * 24 * 365 * 3, + undernameLimit: 10, + type: 'lease', + }; + // the protocol gets the entire bid amount + const expectedRewardForProtocol = expectedPurchasePrice; + + // assert the data response contains the record + const buyRecordNoticeData = JSON.parse( + submitBidResult.Messages?.[0]?.Data, + ); + assert.deepEqual(buyRecordNoticeData, { + name: 'test-name', + ...expectedRecord, + }); + + // should send a debit notice + const debitNoticeTag = submitBidResult.Messages?.[1]?.Tags?.find( + (tag) => tag.name === 'Action' && tag.value === 'Debit-Notice', + ); + assert.ok(debitNoticeTag); + + // expect the target to be to the protocol balance + assert.equal(submitBidResult.Messages?.[1]?.Target, PROCESS_ID); + + // assert the data response contains the record + const debitNoticeData = JSON.parse(submitBidResult.Messages?.[1]?.Data); + assert.deepEqual(debitNoticeData, { + record: expectedRecord, + bidder: bidderAddress, + bidAmount: expectedPurchasePrice, + rewardForInitiator: 0, + rewardForProtocol: expectedRewardForProtocol, + name: 'test-name', + }); + + // should add the record to the registry + const recordResult = await handle( + { + Tags: [ + { name: 'Action', value: 'Record' }, + { name: 'Name', value: 'test-name' }, + ], + Timestamp: bidTimestamp, + }, + submitBidResult.Memory, + ); + + const record = JSON.parse(recordResult.Messages?.[0]?.Data); + assert.deepEqual(record, expectedRecord); + + // assert the balance of the initiator and the protocol where updated correctly + const balancesResult = await handle( + { + Tags: [{ name: 'Action', value: 'Balances' }], + }, + submitBidResult.Memory, + ); + + const expectedProtocolBalance = + INITIAL_PROTOCOL_BALANCE + + initialRecord.purchasePrice + + expectedRewardForProtocol; + const balances = JSON.parse(balancesResult.Messages[0].Data); + assert.equal(balances[PROCESS_ID], expectedProtocolBalance); + assert.equal(balances[bidderAddress], 0); }); + }); - const releaseNameResult = await handle( - { - Tags: [ - { name: 'Action', value: 'Release-Name' }, - { name: 'Name', value: 'test-name' }, - { name: 'Initiator', value: 'test-owner-of-ant' }, // simulate who the owner is of the ANT process when sending the message - ], - From: processId, - Owner: processId, - }, - mem, - ); + describe('Auction-Prices', () => { + it('should compute the prices of an auction at a specific interval', async () => { + // buy the name first + const processId = ''.padEnd(43, 'a'); + const { mem } = await runBuyRecord({ + sender: STUB_ADDRESS, + processId, + type: 'permabuy', + }); - // assert no error tag - const releaseNameErrorTag = releaseNameResult.Messages?.[0]?.Tags?.find( - (tag) => tag.name === 'Error', - ); - assert.equal(releaseNameErrorTag, undefined); - assert.equal(releaseNameResult.Messages?.[0]?.Target, processId); + const releaseNameResult = await handle( + { + Tags: [ + { name: 'Action', value: 'Release-Name' }, + { name: 'Name', value: 'test-name' }, + { name: 'Initiator', value: 'test-owner-of-ant' }, // simulate who the owner is of the ANT process when sending the message + ], + From: processId, + Owner: processId, + }, + mem, + ); - // fetch the auction - const auctionResult = await handle( - { - Tags: [ - { name: 'Action', value: 'Auction-Info' }, - { name: 'Name', value: 'test-name' }, - ], - }, - releaseNameResult.Memory, - ); - // assert no error tag - const auctionErrorTag = auctionResult.Messages?.[0]?.Tags?.find( - (tag) => tag.name === 'Error', - ); + // assert no error tag + const releaseNameErrorTag = releaseNameResult.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Error', + ); + assert.equal(releaseNameErrorTag, undefined); + assert.equal(releaseNameResult.Messages?.[0]?.Target, processId); - assert.equal(auctionErrorTag, undefined); - const auctionPrices = await handle( - { - Tags: [ - { name: 'Action', value: 'Auction-Prices' }, - { name: 'Name', value: 'test-name' }, - { name: 'Purchase-Type', value: 'lease' }, - ], - }, - releaseNameResult.Memory, - ); + // fetch the auction + const auctionResult = await handle( + { + Tags: [ + { name: 'Action', value: 'Auction-Info' }, + { name: 'Name', value: 'test-name' }, + ], + }, + releaseNameResult.Memory, + ); + // assert no error tag + const auctionErrorTag = auctionResult.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Error', + ); - // assert no error tag for auction prices - const auctionPricesErrorTag = auctionPrices.Messages?.[0]?.Tags?.find( - (tag) => tag.name === 'Error', - ); - assert.equal(auctionPricesErrorTag, undefined); - // parse the auction prices data - const auctionPricesData = JSON.parse(auctionPrices.Messages?.[0]?.Data); - - // expectations - const expectedStartPrice = 30000000000; // price for a 1 year lease - const expectedFloorPrice = Math.floor(expectedStartPrice / 50); - - // validate the response structure - assert.ok(auctionPricesData.name, 'Auction prices should include a name'); - assert.ok(auctionPricesData.type, 'Auction prices should include a type'); - assert.ok(auctionPricesData.prices, 'Auction prices should include prices'); - assert.ok( - auctionPricesData.currentPrice, - 'Auction prices should include a current price', - ); + assert.equal(auctionErrorTag, undefined); + const auctionPrices = await handle( + { + Tags: [ + { name: 'Action', value: 'Auction-Prices' }, + { name: 'Name', value: 'test-name' }, + { name: 'Purchase-Type', value: 'lease' }, + ], + }, + releaseNameResult.Memory, + ); - // validate the prices - assert.ok( - Object.keys(auctionPricesData.prices).length > 0, - 'Prices should not be empty', - ); - Object.entries(auctionPricesData.prices).forEach(([timestamp, price]) => { + // assert no error tag for auction prices + const auctionPricesErrorTag = auctionPrices.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Error', + ); + assert.equal(auctionPricesErrorTag, undefined); + // parse the auction prices data + const auctionPricesData = JSON.parse(auctionPrices.Messages?.[0]?.Data); + + // expectations + const expectedStartPrice = 30000000000; // price for a 1 year lease + const expectedFloorPrice = Math.floor(expectedStartPrice / 50); + + // validate the response structure + assert.ok(auctionPricesData.name, 'Auction prices should include a name'); + assert.ok(auctionPricesData.type, 'Auction prices should include a type'); assert.ok( - Number.isInteger(Number(timestamp)), - 'Timestamp should be a number', + auctionPricesData.prices, + 'Auction prices should include prices', + ); + assert.ok( + auctionPricesData.currentPrice, + 'Auction prices should include a current price', ); - assert.ok(Number.isInteger(price), 'Price should be an integer'); - assert.ok(price > 0, 'Price should be positive'); - }); - // assert the first price is the start price - assert.equal(auctionPricesData.prices[STUB_TIMESTAMP], expectedStartPrice); - // assert the last price is the floor price - const lastPriceTimestamp = Math.max( - ...Object.keys(auctionPricesData.prices).map(Number), - ); - assert.equal( - auctionPricesData.prices[lastPriceTimestamp], - expectedFloorPrice, - ); + // validate the prices + assert.ok( + Object.keys(auctionPricesData.prices).length > 0, + 'Prices should not be empty', + ); + Object.entries(auctionPricesData.prices).forEach(([timestamp, price]) => { + assert.ok( + Number.isInteger(Number(timestamp)), + 'Timestamp should be a number', + ); + assert.ok(Number.isInteger(price), 'Price should be an integer'); + assert.ok(price > 0, 'Price should be positive'); + }); + // assert the first price is the start price + assert.equal( + auctionPricesData.prices[STUB_TIMESTAMP], + expectedStartPrice, + ); - // validate the current price - assert.ok( - Number.isInteger(auctionPricesData.currentPrice), - 'Current price should be an integer', - ); - assert.ok( - auctionPricesData.currentPrice > 0, - 'Current price should be positive', - ); - }); + // assert the last price is the floor price + const lastPriceTimestamp = Math.max( + ...Object.keys(auctionPricesData.prices).map(Number), + ); + assert.equal( + auctionPricesData.prices[lastPriceTimestamp], + expectedFloorPrice, + ); - it('should reassign an arns name to a new process id', async () => { - // buy the name first - const processId = ''.padEnd(43, 'a'); - const { mem } = await runBuyRecord({ - sender: STUB_ADDRESS, - processId, - type: 'permabuy', + // validate the current price + assert.ok( + Number.isInteger(auctionPricesData.currentPrice), + 'Current price should be an integer', + ); + assert.ok( + auctionPricesData.currentPrice > 0, + 'Current price should be positive', + ); }); + }); - const reassignNameResult = await handle( - { - Tags: [ - { name: 'Action', value: 'Reassign-Name' }, - { name: 'Name', value: 'test-name' }, - { name: 'Process-Id', value: ''.padEnd(43, 'b') }, - ], - From: processId, - Owner: processId, - }, - mem, - ); + describe('Reassign-Name', () => { + it('should reassign an arns name to a new process id', async () => { + // buy the name first + const processId = ''.padEnd(43, 'a'); + const { mem } = await runBuyRecord({ + sender: STUB_ADDRESS, + processId, + type: 'permabuy', + }); - // assert no error tag - const releaseNameErrorTag = reassignNameResult.Messages?.[0]?.Tags?.find( - (tag) => tag.name === 'Error', - ); - assert.equal(releaseNameErrorTag, undefined); - assert.equal(reassignNameResult.Messages?.[0]?.Target, processId); - }); + const reassignNameResult = await handle( + { + Tags: [ + { name: 'Action', value: 'Reassign-Name' }, + { name: 'Name', value: 'test-name' }, + { name: 'Process-Id', value: ''.padEnd(43, 'b') }, + ], + From: processId, + Owner: processId, + }, + mem, + ); - it('should reassign an arns name to a new process id with initiator', async () => { - // buy the name first - const processId = ''.padEnd(43, 'a'); - const { mem } = await runBuyRecord({ - sender: STUB_ADDRESS, - processId, - type: 'permabuy', + // assert no error tag + const releaseNameErrorTag = reassignNameResult.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Error', + ); + assert.equal(releaseNameErrorTag, undefined); + assert.equal(reassignNameResult.Messages?.[0]?.Target, processId); }); - const reassignNameResult = await handle( - { - Tags: [ - { name: 'Action', value: 'Reassign-Name' }, - { name: 'Name', value: 'test-name' }, - { name: 'Process-Id', value: ''.padEnd(43, 'b') }, - { name: 'Initiator', value: STUB_MESSAGE_ID }, - ], - From: processId, - Owner: processId, - }, - mem, - ); + it('should reassign an arns name to a new process id with initiator', async () => { + // buy the name first + const processId = ''.padEnd(43, 'a'); + const { mem } = await runBuyRecord({ + sender: STUB_ADDRESS, + processId, + type: 'permabuy', + }); - // assert no error tag - const releaseNameErrorTag = reassignNameResult.Messages?.[0]?.Tags?.find( - (tag) => tag.name === 'Error', - ); - assert.equal(releaseNameErrorTag, undefined); - assert.equal(reassignNameResult.Messages?.[0]?.Target, processId); - assert.equal(reassignNameResult.Messages?.[1]?.Target, STUB_MESSAGE_ID); // Check for the message sent to the initiator - }); + const reassignNameResult = await handle( + { + Tags: [ + { name: 'Action', value: 'Reassign-Name' }, + { name: 'Name', value: 'test-name' }, + { name: 'Process-Id', value: ''.padEnd(43, 'b') }, + { name: 'Initiator', value: STUB_MESSAGE_ID }, + ], + From: processId, + Owner: processId, + }, + mem, + ); - it('should not reassign an arns name with invalid ownership', async () => { - // buy the name first - const processId = ''.padEnd(43, 'a'); - const { mem } = await runBuyRecord({ - sender: STUB_ADDRESS, - processId, - type: 'permabuy', + // assert no error tag + const releaseNameErrorTag = reassignNameResult.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Error', + ); + assert.equal(releaseNameErrorTag, undefined); + assert.equal(reassignNameResult.Messages?.[0]?.Target, processId); + assert.equal(reassignNameResult.Messages?.[1]?.Target, STUB_MESSAGE_ID); // Check for the message sent to the initiator }); - const reassignNameResult = await handle( - { - Tags: [ - { name: 'Action', value: 'Reassign-Name' }, - { name: 'Name', value: 'test-name' }, - { name: 'Process-Id', value: ''.padEnd(43, 'b') }, - ], - From: STUB_ADDRESS, - Owner: STUB_ADDRESS, - }, - mem, - ); + it('should not reassign an arns name with invalid ownership', async () => { + // buy the name first + const processId = ''.padEnd(43, 'a'); + const { mem } = await runBuyRecord({ + sender: STUB_ADDRESS, + processId, + type: 'permabuy', + }); - // assert error - const releaseNameErrorTag = reassignNameResult.Messages?.[0]?.Tags?.find( - (tag) => tag.name === 'Error', - ); - assert.equal(releaseNameErrorTag.value, 'Reassign-Name-Error'); - }); + const reassignNameResult = await handle( + { + Tags: [ + { name: 'Action', value: 'Reassign-Name' }, + { name: 'Name', value: 'test-name' }, + { name: 'Process-Id', value: ''.padEnd(43, 'b') }, + ], + From: STUB_ADDRESS, + Owner: STUB_ADDRESS, + }, + mem, + ); - it('should not reassign an arns name with invalid new process id', async () => { - // buy the name first - const processId = ''.padEnd(43, 'a'); - const { mem } = await runBuyRecord({ - sender: STUB_ADDRESS, - processId, - type: 'permabuy', + // assert error + const releaseNameErrorTag = reassignNameResult.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Error', + ); + assert.equal(releaseNameErrorTag.value, 'Reassign-Name-Error'); }); - const reassignNameResult = await handle( - { - Tags: [ - { name: 'Action', value: 'Reassign-Name' }, - { name: 'Name', value: 'test-name' }, - { name: 'Process-Id', value: 'this is an invalid process id' }, - ], - From: processId, - Owner: processId, - }, - mem, - ); + it('should not reassign an arns name with invalid new process id', async () => { + // buy the name first + const processId = ''.padEnd(43, 'a'); + const { mem } = await runBuyRecord({ + sender: STUB_ADDRESS, + processId, + type: 'permabuy', + }); - // assert error - const releaseNameErrorTag = reassignNameResult.Messages?.[0]?.Tags?.find( - (tag) => tag.name === 'Error', - ); - assert.equal(releaseNameErrorTag.value, 'Bad-Input'); + const reassignNameResult = await handle( + { + Tags: [ + { name: 'Action', value: 'Reassign-Name' }, + { name: 'Name', value: 'test-name' }, + { name: 'Process-Id', value: 'this is an invalid process id' }, + ], + From: processId, + Owner: processId, + }, + mem, + ); + + // assert error + const releaseNameErrorTag = reassignNameResult.Messages?.[0]?.Tags?.find( + (tag) => tag.name === 'Error', + ); + assert.equal(releaseNameErrorTag.value, 'Bad-Input'); + }); }); // TODO: add several error scenarios diff --git a/tests/handlers.test.mjs b/tests/handlers.test.mjs index 3ec73d0..a81d104 100644 --- a/tests/handlers.test.mjs +++ b/tests/handlers.test.mjs @@ -38,7 +38,7 @@ describe('handlers', async () => { const evalIndex = handlersList.indexOf('_eval'); const defaultIndex = handlersList.indexOf('_default'); const pruneIndex = handlersList.indexOf('prune'); - const expectedHandlerCount = 58; // TODO: update this if more handlers are added + const expectedHandlerCount = 59; // TODO: update this if more handlers are added assert.ok(evalIndex === 0); assert.ok(defaultIndex === 1); assert.ok(pruneIndex === 2);