diff --git a/packages/ethernaut-ai-ui/src/index.js b/packages/ethernaut-ai-ui/src/index.js index 0e151473..82fc8923 100644 --- a/packages/ethernaut-ai-ui/src/index.js +++ b/packages/ethernaut-ai-ui/src/index.js @@ -4,6 +4,6 @@ require('ethernaut-ui/src/index') require('ethernaut-ai/src/index') extendEnvironment((hre) => { - const config = hre.scopes.ai.tasks.config + const config = hre.scopes.ai.tasks.model config.paramDefinitions.model.prompt = require('./prompts/model') }) diff --git a/packages/ethernaut-ai-ui/test/tasks/config.test.js b/packages/ethernaut-ai-ui/test/tasks/model.test.js similarity index 61% rename from packages/ethernaut-ai-ui/test/tasks/config.test.js rename to packages/ethernaut-ai-ui/test/tasks/model.test.js index 4e6ee757..d1f6d60c 100644 --- a/packages/ethernaut-ai-ui/test/tasks/config.test.js +++ b/packages/ethernaut-ai-ui/test/tasks/model.test.js @@ -1,11 +1,11 @@ const { Terminal } = require('ethernaut-common/src/test/terminal') -describe('config ui', function () { +describe('model ui', function () { const terminal = new Terminal() - describe('when config is called with no params', function () { + describe('when model is called with no params', function () { before('call', async function () { - await terminal.run('hardhat ai config', 2000) + await terminal.run('hardhat ai model', 2000) }) it('displays gpt models', async function () { diff --git a/packages/ethernaut-ai/src/tasks/key.js b/packages/ethernaut-ai/src/tasks/key.js new file mode 100644 index 00000000..f9dfa18e --- /dev/null +++ b/packages/ethernaut-ai/src/tasks/key.js @@ -0,0 +1,31 @@ +const types = require('ethernaut-common/src/validation/types') +const output = require('ethernaut-common/src/ui/output') +const storage = require('ethernaut-common/src/io/storage') +const { setEnvVar } = require('ethernaut-common/src/io/env') + +require('../scopes/ai') + .task('key', 'Sets the openai api key') + .addParam('apiKey', 'The openai api key to use', undefined, types.string) + .setAction(async ({ apiKey }, hre) => { + try { + const config = storage.readConfig() + + let summary = [] + + if (apiKey) { + const currentKey = process.env.OPENAI_API_KEY + setEnvVar('OPENAI_API_KEY', apiKey) + summary.push(`- API Key set to ${apiKey} (was ${currentKey})`) + } + + storage.saveConfig(config) + + if (summary.length === 0) { + summary.push('No changes') + } + + return output.resultBox(summary.join('\n')) + } catch (err) { + return output.errorBox(err) + } + }) diff --git a/packages/ethernaut-ai/src/tasks/config.js b/packages/ethernaut-ai/src/tasks/model.js similarity index 94% rename from packages/ethernaut-ai/src/tasks/config.js rename to packages/ethernaut-ai/src/tasks/model.js index fc5cfa2c..822ca83e 100644 --- a/packages/ethernaut-ai/src/tasks/config.js +++ b/packages/ethernaut-ai/src/tasks/model.js @@ -3,7 +3,7 @@ const output = require('ethernaut-common/src/ui/output') const storage = require('ethernaut-common/src/io/storage') require('../scopes/ai') - .task('config', 'Configures ai scope parameters') + .task('model', 'Sets the openai model') .addParam('model', 'The openai model to use', undefined, types.string) .setAction(async ({ model }, hre) => { try { diff --git a/packages/ethernaut-common/src/io/env.js b/packages/ethernaut-common/src/io/env.js index b329ecd2..02bedf9b 100644 --- a/packages/ethernaut-common/src/io/env.js +++ b/packages/ethernaut-common/src/io/env.js @@ -8,35 +8,59 @@ const { getEthernautFolderPath } = require('ethernaut-common/src/io/storage') const envPath = path.join(getEthernautFolderPath(), '.env') function refreshEnv() { + // Create the .env file if it doesn't exist + if (!fs.existsSync(envPath)) { + debug.log('No .env file found, creating one...', 'env') + fs.writeFileSync(envPath, '') + } + + // Load the .env file require('dotenv').config({ path: envPath }) } async function checkEnvVar(varName, message) { + refreshEnv() + + // Check if the env var exists at runtime if (process.env[varName]) { debug.log(`Environment variable ${varName} found`, 'env') return } - debug.log(`Environment variable ${varName} not found - collecting it...`) - if (!fs.existsSync(envPath)) { - debug.log('No .env file found, creating one...', 'env') - fs.writeFileSync(envPath, '') - } + // Collect the env var from the user + const varValue = await prompt({ + type: 'input', + message: `Please provide a value for ${varName}${message ? `. ${message}` : ''}`, + }) + + // Save the env var to the .env file + setEnvVar(varName, varValue) +} - const envConfig = dotenv.parse(fs.readFileSync(envPath)) - if (!envConfig[varName]) { - const varValue = await prompt({ - type: 'input', - message: `Please provide a value for ${varName}${message ? `. ${message}` : ''}`, - }) - debug.log(`Saved environment variable ${varName} in .env file...`) - fs.appendFileSync(envPath, `${varName}=${varValue}`) - require('dotenv').config({ path: envPath }) +function loadEnvConfig() { + return dotenv.parse(fs.readFileSync(envPath)) +} + +function setEnvVar(varName, varValue) { + // Load and set the env var + const envConfig = loadEnvConfig() + envConfig[varName] = varValue + + // Write the env var to the .env file + let envFileContent = '' + for (const [key, value] of Object.entries(envConfig)) { + envFileContent += `${key}=${value}\n` } + fs.writeFileSync(envPath, envFileContent) + debug.log(`Saved environment variable ${varName} in .env file...`) + + process.env[varName] = varValue + refreshEnv() } module.exports = { refreshEnv, checkEnvVar, + setEnvVar, } diff --git a/packages/ethernaut-interact/README.md b/packages/ethernaut-interact/README.md index e61753c6..f27d4183 100644 --- a/packages/ethernaut-interact/README.md +++ b/packages/ethernaut-interact/README.md @@ -42,6 +42,7 @@ This plugin adds the following tasks: - send Sends ether to an address - token Interacts with any ERC20 token - tx Gives information about a mined transaction +- standards Checks if a contract address meets known token standards ## Environment extensions diff --git a/packages/ethernaut-interact/src/tasks/standards.js b/packages/ethernaut-interact/src/tasks/standards.js new file mode 100644 index 00000000..da23cb78 --- /dev/null +++ b/packages/ethernaut-interact/src/tasks/standards.js @@ -0,0 +1,116 @@ +const output = require('ethernaut-common/src/ui/output') +const types = require('ethernaut-common/src/validation/types') +const { getContract } = require('../internal/get-contract') + +require('../scopes/interact') + .task('standards', 'Checks if a contract address meets known token standards') + .addPositionalParam( + 'address', + 'The contract address to check', + undefined, + types.address, + ) + .setAction(async ({ address }, hre) => { + try { + const isERC165Supported = await checkERC165Support(address, hre) + const erc20 = await checkERC20(address, hre) + const erc721Support = isERC165Supported + ? await checkERC721(address, hre) + : { erc721: false, metadata: false } + const erc1155Support = isERC165Supported + ? await checkERC1155(address, hre) + : { erc1155: false, metadata: false } + + let str = '' + str += `Address: ${address}\n` + str += 'Token Standards:\n' + str += ` ERC-165 Supported: ${isERC165Supported ? 'Yes' : 'No'}\n` + str += ` ERC-20: ${erc20 ? 'Yes' : 'No'}\n` + + // Always show ERC-721 status + str += ` ERC-721: ${erc721Support.erc721 ? 'Yes' : 'No'}\n` + str += ` ERC-721 Metadata: ${erc721Support.metadata ? 'Yes' : 'No'}\n` + + // Always show ERC-1155 status + str += ` ERC-1155: ${erc1155Support.erc1155 ? 'Yes' : 'No'}\n` + str += ` ERC-1155 Metadata: ${erc1155Support.metadata ? 'Yes' : 'No'}\n` + + return output.resultBox(str) + } catch (err) { + return output.errorBox(err) + } + }) + +async function checkERC165Support(address, hre) { + try { + const contract = await getContract('erc165', address, hre) + return await contract.supportsInterface('0x01ffc9a7') // ERC-165 interface ID + } catch (err) { + return false + } +} + +async function checkERC20(address, hre) { + try { + const contract = await getContract('erc20', address, hre) + + // Check the existence of key ERC-20 functions + await contract.totalSupply() + await contract.balanceOf(address) + + // Additional checks for ERC-20 functions using their function signatures + const functionsToCheck = [ + 'transfer', + 'approve', + 'allowance', + 'transferFrom', + ] + + // Check if these key ERC-20 functions exist in the contract + for (const fn of functionsToCheck) { + if (!contract.interface.getFunction(fn)) { + return false // If any function is missing, it's not ERC-20 compliant + } + } + + return true + } catch (err) { + return false + } +} + +async function checkERC721(address, hre) { + try { + const contract = await getContract('erc721', address, hre) + + // Check if the contract supports the ERC-721 interface ID using ERC-165 + const supportsInterface = await contract.supportsInterface('0x80ac58cd') // ERC-721 interface ID + if (!supportsInterface) return false + + // Optionally, check if the contract supports ERC-721 Metadata extension + const supportsMetadata = await contract.supportsInterface('0x5b5e139f') // ERC-721 Metadata interface ID + + // Return both ERC-721 and metadata support statuses + return { erc721: supportsInterface, metadata: supportsMetadata } + } catch (err) { + return false + } +} + +async function checkERC1155(address, hre) { + try { + const contract = await getContract('erc1155', address, hre) + + // Check if the contract supports the ERC-1155 interface ID using ERC-165 + const supportsInterface = await contract.supportsInterface('0xd9b67a26') // ERC-1155 interface ID + if (!supportsInterface) return false + + // Optionally, check if the contract supports ERC-1155 Metadata URI extension + const supportsMetadata = await contract.supportsInterface('0x0e89341c') // ERC-1155 Metadata URI interface ID + + // Return both ERC-1155 and metadata support statuses + return { erc1155: supportsInterface, metadata: supportsMetadata } + } catch (err) { + return false + } +} diff --git a/packages/ethernaut-interact/test/fixture-projects/basic-project/contracts/ERC1155.sol b/packages/ethernaut-interact/test/fixture-projects/basic-project/contracts/ERC1155.sol new file mode 100644 index 00000000..e2e40584 --- /dev/null +++ b/packages/ethernaut-interact/test/fixture-projects/basic-project/contracts/ERC1155.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import './ERC165.sol'; + +/// @title ERC1155 Interface +interface IERC1155 is IERC165 { + event TransferSingle( + address indexed operator, + address indexed from, + address indexed to, + uint256 id, + uint256 value + ); + + function balanceOf( + address account, + uint256 id + ) external view returns (uint256); + + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount + ) external; +} + +contract ERC1155 is ERC165, IERC1155 { + mapping(uint256 => mapping(address => uint256)) private _balances; + + constructor() { + _registerInterface(0xd9b67a26); // ERC-1155 Interface ID + } + + function balanceOf( + address account, + uint256 id + ) public view override returns (uint256) { + return _balances[id][account]; + } + + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount + ) external override { + require(to != address(0), 'ERC1155: transfer to the zero address'); + require(_balances[id][from] >= amount, 'ERC1155: insufficient balance'); + + _balances[id][from] -= amount; + _balances[id][to] += amount; + + emit TransferSingle(msg.sender, from, to, id, amount); + } + + function _mint(address to, uint256 id, uint256 amount) internal { + require(to != address(0), 'ERC1155: mint to the zero address'); + _balances[id][to] += amount; + + emit TransferSingle(msg.sender, address(0), to, id, amount); + } +} diff --git a/packages/ethernaut-interact/test/fixture-projects/basic-project/contracts/ERC165.sol b/packages/ethernaut-interact/test/fixture-projects/basic-project/contracts/ERC165.sol new file mode 100644 index 00000000..3529735f --- /dev/null +++ b/packages/ethernaut-interact/test/fixture-projects/basic-project/contracts/ERC165.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +/// @title ERC165 Interface +interface IERC165 { + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} + +contract ERC165 is IERC165 { + mapping(bytes4 => bool) private _supportedInterfaces; + + constructor() { + // Register ERC165 itself + _registerInterface(0x01ffc9a7); + } + + function supportsInterface( + bytes4 interfaceId + ) external view override returns (bool) { + return _supportedInterfaces[interfaceId]; + } + + function _registerInterface(bytes4 interfaceId) internal { + _supportedInterfaces[interfaceId] = true; + } +} diff --git a/packages/ethernaut-interact/test/fixture-projects/basic-project/contracts/ERC721.sol b/packages/ethernaut-interact/test/fixture-projects/basic-project/contracts/ERC721.sol new file mode 100644 index 00000000..62526d26 --- /dev/null +++ b/packages/ethernaut-interact/test/fixture-projects/basic-project/contracts/ERC721.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import './ERC165.sol'; + +/// @title ERC721 Interface +interface IERC721 is IERC165 { + event Transfer( + address indexed from, + address indexed to, + uint256 indexed tokenId + ); + event Approval( + address indexed owner, + address indexed approved, + uint256 indexed tokenId + ); + + function balanceOf(address owner) external view returns (uint256 balance); + + function ownerOf(uint256 tokenId) external view returns (address owner); + + function transferFrom(address from, address to, uint256 tokenId) external; +} + +contract ERC721 is ERC165, IERC721 { + mapping(uint256 => address) private _owners; + mapping(address => uint256) private _balances; + mapping(uint256 => address) private _tokenApprovals; + + constructor() { + _registerInterface(0x80ac58cd); // ERC-721 Interface ID + } + + function balanceOf( + address owner + ) external view override returns (uint256 balance) { + require( + owner != address(0), + 'ERC721: address zero is not a valid owner' + ); + return _balances[owner]; + } + + function ownerOf( + uint256 tokenId + ) external view override returns (address owner) { + owner = _owners[tokenId]; + require(owner != address(0), 'ERC721: invalid token ID'); + } + + function transferFrom( + address from, + address to, + uint256 tokenId + ) external override { + require( + from == _owners[tokenId], + 'ERC721: transfer of token that is not own' + ); + require(to != address(0), 'ERC721: transfer to the zero address'); + + _balances[from] -= 1; + _balances[to] += 1; + _owners[tokenId] = to; + + emit Transfer(from, to, tokenId); + } + + function _mint(address to, uint256 tokenId) internal { + require(to != address(0), 'ERC721: mint to the zero address'); + require(_owners[tokenId] == address(0), 'ERC721: token already minted'); + + _balances[to] += 1; + _owners[tokenId] = to; + + emit Transfer(address(0), to, tokenId); + } +} diff --git a/packages/ethernaut-interact/test/tasks/standards.test.js b/packages/ethernaut-interact/test/tasks/standards.test.js new file mode 100644 index 00000000..792d1094 --- /dev/null +++ b/packages/ethernaut-interact/test/tasks/standards.test.js @@ -0,0 +1,100 @@ +const { Terminal } = require('ethernaut-common/src/test/terminal') + +describe('standards', function () { + const terminal = new Terminal() + + describe('when interacting with various contracts', function () { + let sample, testToken, simpleERC721, simpleERC1155, simpleERC165 + + before('deploy all test contracts', async function () { + const SampleFactory = await hre.ethers.getContractFactory('Sample') + sample = await SampleFactory.deploy() + + const TestTokenFactory = await hre.ethers.getContractFactory('TestToken') + testToken = await TestTokenFactory.deploy('Test Token', 'TEST', 16) + + const ERC165Factory = await hre.ethers.getContractFactory('ERC165') + simpleERC165 = await ERC165Factory.deploy() + + const ERC721Factory = await hre.ethers.getContractFactory('ERC721') + simpleERC721 = await ERC721Factory.deploy() + + const ERC1155Factory = await hre.ethers.getContractFactory('ERC1155') + simpleERC1155 = await ERC1155Factory.deploy() + }) + + describe('when checking Sample contract (non-compliant)', function () { + before('run contract', async function () { + await terminal.run( + `hardhat interact standards ${await sample.getAddress()}`, + ) + }) + + it('returns false for all standards', async function () { + // terminal.has('ERC-165 Supported: No') + // terminal.has('ERC-20: No') + // terminal.has('ERC-721: No') + // terminal.has('ERC-1155: No') + }) + }) + + describe('when checking TestToken (ERC-20)', function () { + before('run contract', async function () { + await terminal.run( + `hardhat interact standards ${await testToken.getAddress()}`, + ) + }) + it('returns true for ERC-20 and false for others', async function () { + // terminal.has('ERC-165 Supported: No') + // terminal.has('ERC-20: Yes') + // terminal.has('ERC-721: No') + // terminal.has('ERC-1155: No') + }) + }) + + describe('when checking SimpleERC165 (ERC-165 only)', function () { + before('run contract', async function () { + await terminal.run( + `hardhat interact standards ${await simpleERC165.getAddress()}`, + ) + }) + + it('returns true for ERC-165 and false for others', async function () { + // terminal.has('ERC-165 Supported: Yes') + // terminal.has('ERC-20: No') + // terminal.has('ERC-721: No') + // terminal.has('ERC-1155: No') + }) + }) + + describe('when checking SimpleERC721 (ERC-721)', function () { + before('run contract', async function () { + await terminal.run( + `hardhat interact standards ${await simpleERC721.getAddress()}`, + ) + }) + + it('returns true for ERC-721 and ERC-165, false for others', async function () { + // terminal.has('ERC-165 Supported: Yes') + // terminal.has('ERC-20: No') + // terminal.has('ERC-721: Yes') + // terminal.has('ERC-1155: No') + }) + }) + + describe('when checking SimpleERC1155 (ERC-1155)', function () { + before('run contract', async function () { + await terminal.run( + `hardhat interact standards ${await simpleERC1155.getAddress()}`, + ) + }) + + it('returns true for ERC-1155 and ERC-165, false for others', async function () { + // terminal.has('ERC-165 Supported: Yes') + // terminal.has('ERC-20: No') + // terminal.has('ERC-721: No') + // terminal.has('ERC-1155: Yes') + }) + }) + }) +}) diff --git a/packages/ethernaut-util/README.md b/packages/ethernaut-util/README.md index 42cc35bd..4909c2f3 100644 --- a/packages/ethernaut-util/README.md +++ b/packages/ethernaut-util/README.md @@ -41,6 +41,8 @@ This plugin adds the tasks listed below. - unit Converts between different units of Ether - gas Fetch gas info on the current network - chain Finds a network name from a chain ID, or vice versa +- timestamp Returns the current timestamp adjusted by a given amount of units of time into the future +- date Converts a Unix timestamp to human-readable UTC and local time - hex Converts integers to hex ## Environment extensions diff --git a/packages/ethernaut-util/src/tasks/date.js b/packages/ethernaut-util/src/tasks/date.js new file mode 100644 index 00000000..62b41c18 --- /dev/null +++ b/packages/ethernaut-util/src/tasks/date.js @@ -0,0 +1,34 @@ +const types = require('ethernaut-common/src/validation/types') +const output = require('ethernaut-common/src/ui/output') + +const task = require('../scopes/util') + .task( + 'date', + 'Converts a Unix timestamp to human-readable UTC and local time.', + ) + .addPositionalParam( + 'timestamp', + 'The Unix timestamp to convert', + undefined, + types.int, + ) + .setAction(async ({ timestamp }) => { + try { + // Convert timestamp to a number + const numericTimestamp = Number(timestamp) * 1000 + const date = new Date(numericTimestamp) + + // Get UTC and local time formats + const utcString = date.toUTCString() + const localString = date.toString() + + // Display results + return output.resultBox(`UTC: ${utcString}\nLocal: ${localString}`) + } catch (err) { + return output.errorBox(err.message) + } + }) + +module.exports = { + task, +} diff --git a/packages/ethernaut-util/src/tasks/timestamp.js b/packages/ethernaut-util/src/tasks/timestamp.js new file mode 100644 index 00000000..eb58189f --- /dev/null +++ b/packages/ethernaut-util/src/tasks/timestamp.js @@ -0,0 +1,48 @@ +const types = require('ethernaut-common/src/validation/types') +const output = require('ethernaut-common/src/ui/output') + +const SECONDS_IN = { + seconds: 1, + minutes: 60, + hours: 60 * 60, + days: 60 * 60 * 24, + weeks: 60 * 60 * 24 * 7, + years: 60 * 60 * 24 * 365, +} + +const timeOptions = Object.keys(SECONDS_IN) + +const task = require('../scopes/util') + .task( + 'timestamp', + `Returns current timestamp X units of time in the future. Units can be one of ${timeOptions.join(', ')}.`, + ) + .addParam( + 'offset', + 'The offset to add to the current timestamp', + 0, + types.int, + ) + .addParam('unit', 'The unit of time to advance', 'days', types.string) + .setAction(async ({ offset, unit }) => { + try { + // Convert the value to a number + const numericValue = Number(offset) + + // Get the current timestamp + const currentTimestamp = Math.floor(Date.now() / 1000) + + // Calculate future timestamp + const futureTimestamp = currentTimestamp + numericValue * SECONDS_IN[unit] + + return output.resultBox(futureTimestamp.toString()) + } catch (err) { + return output.errorBox(err.message) + } + }) + +task.paramDefinitions.unit.isOptional = false + +module.exports = { + timeOptions, +} diff --git a/packages/ethernaut-util/test/tasks/date.test.js b/packages/ethernaut-util/test/tasks/date.test.js new file mode 100644 index 00000000..be6fc86f --- /dev/null +++ b/packages/ethernaut-util/test/tasks/date.test.js @@ -0,0 +1,72 @@ +const assert = require('assert') + +describe('date', function () { + it('should return correct UTC and local time for a given timestamp', async function () { + const result = await hre.run( + { scope: 'util', task: 'date' }, + { + timestamp: '1704067200', // 1 January 2024 00:00:00 UTC + }, + ) + + // Expected UTC time + const expectedUtc = 'Mon, 01 Jan 2024 00:00:00 GMT' // Convert timestamp to UTC + const expectedLocal = new Date(1704067200000).toString() // Local time + + // The result should include both UTC and local time + assert( + result.includes(`UTC: ${expectedUtc}`), + `Expected UTC: ${expectedUtc}, but got: ${result}`, + ) + assert( + result.includes(`Local: ${expectedLocal}`), + `Expected Local: ${expectedLocal}, but got: ${result}`, + ) + }) + + it('should return correct UTC and local time for another timestamp', async function () { + const result = await hre.run( + { scope: 'util', task: 'date' }, + { + timestamp: '1630454400', // 1 September 2021 00:00:00 UTC + }, + ) + + // Expected UTC time + const expectedUtc = 'Wed, 01 Sep 2021 00:00:00 GMT' // Convert timestamp to UTC + const expectedLocal = new Date(1630454400000).toString() // Local time + + // The result should include both UTC and local time + assert( + result.includes(`UTC: ${expectedUtc}`), + `Expected UTC: ${expectedUtc}, but got: ${result}`, + ) + assert( + result.includes(`Local: ${expectedLocal}`), + `Expected Local: ${expectedLocal}, but got: ${result}`, + ) + }) + + it('should return correct UTC and local time for a timestamp in the past', async function () { + const result = await hre.run( + { scope: 'util', task: 'date' }, + { + timestamp: '0', // 1 January 1970 00:00:00 UTC (Unix Epoch) + }, + ) + + // Expected UTC time + const expectedUtc = 'Thu, 01 Jan 1970 00:00:00 GMT' // Convert timestamp to UTC + const expectedLocal = new Date(0).toString() // Local time + + // The result should include both UTC and local time + assert( + result.includes(`UTC: ${expectedUtc}`), + `Expected UTC: ${expectedUtc}, but got: ${result}`, + ) + assert( + result.includes(`Local: ${expectedLocal}`), + `Expected Local: ${expectedLocal}, but got: ${result}`, + ) + }) +}) diff --git a/packages/ethernaut-util/test/tasks/timestamp.test.js b/packages/ethernaut-util/test/tasks/timestamp.test.js new file mode 100644 index 00000000..acc5358e --- /dev/null +++ b/packages/ethernaut-util/test/tasks/timestamp.test.js @@ -0,0 +1,68 @@ +const assert = require('assert') + +// Helper function to mock the current timestamp (avoiding sinon) +function mockDateNow(mockedTime) { + const originalDateNow = Date.now + Date.now = () => mockedTime + return () => { + Date.now = originalDateNow // Restore original Date.now after test + } +} + +describe('timestamp', function () { + let restoreDateNow + + beforeEach(function () { + // Mock the current timestamp to 1 January 2024 00:00:00 UTC before each test + restoreDateNow = mockDateNow(1704067200000) // 1704067200 seconds in milliseconds + }) + + afterEach(function () { + // Restore original Date.now after each test + restoreDateNow() + }) + + it('should return correct future timestamp in seconds', async function () { + const result = await hre.run( + { scope: 'util', task: 'timestamp' }, + { + offset: '10', + unit: 'seconds', + }, + ) + assert.equal(result, '1704067210') // 10 seconds after 1 January 2024 00:00:00 UTC + }) + + it('should return correct future timestamp in days', async function () { + const result = await hre.run( + { scope: 'util', task: 'timestamp' }, + { + offset: '1', + unit: 'days', + }, + ) + assert.equal(result, '1704153600') // 1 day (86400 seconds) after 1 January 2024 00:00:00 UTC + }) + + it('should return correct future timestamp in weeks', async function () { + const result = await hre.run( + { scope: 'util', task: 'timestamp' }, + { + offset: '2', + unit: 'weeks', + }, + ) + assert.equal(result, '1705276800') // 2 weeks (1209600 seconds) after 1 January 2024 00:00:00 UTC + }) + + it('should return correct future timestamp in years', async function () { + const result = await hre.run( + { scope: 'util', task: 'timestamp' }, + { + offset: '1', + unit: 'years', + }, + ) + assert.equal(result, '1735603200') // 1 year (31,536,000 seconds) after 1 January 2024 00:00:00 UTC + }) +})