diff --git a/packages/hardhat/contracts/FileRegistry.sol b/packages/hardhat/contracts/FileRegistry.sol new file mode 100644 index 00000000..b02bfcc9 --- /dev/null +++ b/packages/hardhat/contracts/FileRegistry.sol @@ -0,0 +1,288 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +/** + * @title FileRegistry + * @notice On-chain source of truth for an x402 "pay-per-use" file marketplace. + * + * Each registered file points at an object stored off-chain in a private, + * S3-compatible bucket (MinIO in this template). The registry never holds the + * file bytes or any funds; it only records who owns a file, how much it costs, + * whether it is public, and where to find it. + * + * Access is enforced off-chain by the resource server: + * - Public files are served to anyone. + * - Private files are gated behind an x402 payment of `priceTinybar` HBAR to + * the owner's `payToAccountId`, settled per-download. There is no allow-list: + * a file is made effectively private by setting an arbitrarily high price. + * - Delisted files are treated as not found (no downloads, no marketplace row). + * The owner may register the same `objectKey` again after delisting. + * + * The bucket is private and all reads flow through the server's presigned URLs, + * so storing `objectKey` on-chain does not leak the file contents. + */ +contract FileRegistry { + /// @notice Asset id used by x402 to denote native HBAR. Prices are quoted in tinybars (1 HBAR = 1e8 tinybars). + string public constant PAYMENT_ASSET = "0.0.0"; + + /// @notice Upper bound on `getFiles` page size to keep view-call gas and return data predictable. + uint256 public constant MAX_PAGE_SIZE = 50; + + /** + * @notice A registered file and its access terms. + * @param owner EVM address that registered the file and controls it. + * @param payToAccountId Hedera account id (e.g. "0.0.1234") that receives x402 payments for this file. + * @param priceTinybar Price per download in tinybars. Ignored when `isPublic` is true. + * @param isPublic When true, anyone may download for free; when false, payment is required per download. + * @param objectKey Key of the object in the private storage bucket. + * @param contentHash SHA-256 of the file contents, for integrity verification. + * @param name Human-readable file name. + * @param mimeType MIME type of the file. + * @param exists Whether this slot holds a registered file (guards against zeroed structs). + */ + struct FileItem { + address owner; + string payToAccountId; + uint256 priceTinybar; + bool isPublic; + string objectKey; + bytes32 contentHash; + string name; + string mimeType; + bool exists; + } + + /// @notice Lookup of file metadata by file id. + mapping(bytes32 fileId => FileItem file) private _files; + + /// @notice All registered file ids, in registration order, for enumeration. + bytes32[] private _fileIds; + + /** + * @notice Emitted when a new file is registered. + * @dev `objectKey` and `payToAccountId` are unindexed so the full values are available to off-chain indexers. + */ + event FileRegistered( + bytes32 indexed fileId, + address indexed owner, + string objectKey, + string payToAccountId, + uint256 priceTinybar, + bool isPublic, + bytes32 contentHash, + string name, + string mimeType + ); + + /// @notice Emitted when a file's price is updated. + event PriceChanged(bytes32 indexed fileId, uint256 oldPriceTinybar, uint256 newPriceTinybar); + + /// @notice Emitted when a file's visibility is updated. + event VisibilityChanged(bytes32 indexed fileId, bool isPublic); + + /// @notice Emitted when a file's payout account is updated. + event PayToChanged(bytes32 indexed fileId, string oldPayToAccountId, string newPayToAccountId); + + /// @notice Emitted when an owner delists a file from the marketplace. + event FileDelisted(bytes32 indexed fileId, address indexed owner); + + /// @notice Thrown when registering a file id that already exists. + error FileAlreadyRegistered(bytes32 fileId); + + /// @notice Thrown when referencing a file id that has not been registered. + error FileNotFound(bytes32 fileId); + + /// @notice Thrown when a non-owner attempts an owner-only action. + error NotFileOwner(bytes32 fileId, address caller); + + /// @notice Thrown when a required string argument is empty. + error EmptyValue(string field); + + /// @notice Thrown when `contentHash` is zero (a SHA-256 digest is required). + error InvalidContentHash(); + + /// @dev Restricts a function to the owner of `fileId`. + modifier onlyFileOwner(bytes32 fileId) { + FileItem storage file = _files[fileId]; + if (!file.exists) revert FileNotFound(fileId); + if (file.owner != msg.sender) revert NotFileOwner(fileId, msg.sender); + _; + } + + /** + * @notice Register a new file and its access terms. + * @dev The file id is deterministic per (owner, objectKey), preventing duplicate registration of the + * same object by the same owner while letting different owners use independent object keys. + * @param objectKey Key of the object in the private storage bucket (must be non-empty). + * @param payToAccountId Hedera account id that receives payments for this file (must be non-empty). + * @param priceTinybar Price per download in tinybars (ignored while the file is public). + * @param isPublic Whether the file is freely downloadable. + * @param contentHash SHA-256 of the file contents (must be non-zero). + * @param name Human-readable file name. + * @param mimeType MIME type of the file (must be non-empty). + * @return fileId The id assigned to the newly registered file. + */ + function registerFile( + string calldata objectKey, + string calldata payToAccountId, + uint256 priceTinybar, + bool isPublic, + bytes32 contentHash, + string calldata name, + string calldata mimeType + ) external returns (bytes32 fileId) { + if (bytes(objectKey).length == 0) revert EmptyValue("objectKey"); + if (bytes(payToAccountId).length == 0) revert EmptyValue("payToAccountId"); + if (contentHash == bytes32(0)) revert InvalidContentHash(); + if (bytes(mimeType).length == 0) revert EmptyValue("mimeType"); + + fileId = computeFileId(msg.sender, objectKey); + if (_files[fileId].exists) revert FileAlreadyRegistered(fileId); + + _files[fileId] = FileItem({ + owner: msg.sender, + payToAccountId: payToAccountId, + priceTinybar: priceTinybar, + isPublic: isPublic, + objectKey: objectKey, + contentHash: contentHash, + name: name, + mimeType: mimeType, + exists: true + }); + _fileIds.push(fileId); + + emit FileRegistered( + fileId, + msg.sender, + objectKey, + payToAccountId, + priceTinybar, + isPublic, + contentHash, + name, + mimeType + ); + } + + /** + * @notice Update the per-download price of a file. + * @param fileId Id of the file to update. + * @param newPriceTinybar New price in tinybars. + */ + function setPrice(bytes32 fileId, uint256 newPriceTinybar) external onlyFileOwner(fileId) { + FileItem storage file = _files[fileId]; + uint256 oldPriceTinybar = file.priceTinybar; + file.priceTinybar = newPriceTinybar; + emit PriceChanged(fileId, oldPriceTinybar, newPriceTinybar); + } + + /** + * @notice Update the visibility of a file. + * @param fileId Id of the file to update. + * @param isPublic New visibility flag. + */ + function setVisibility(bytes32 fileId, bool isPublic) external onlyFileOwner(fileId) { + _files[fileId].isPublic = isPublic; + emit VisibilityChanged(fileId, isPublic); + } + + /** + * @notice Update the Hedera account that receives payments for a file. + * @param fileId Id of the file to update. + * @param newPayToAccountId New payout account id (must be non-empty). + */ + function setPayToAccountId(bytes32 fileId, string calldata newPayToAccountId) external onlyFileOwner(fileId) { + if (bytes(newPayToAccountId).length == 0) revert EmptyValue("payToAccountId"); + FileItem storage file = _files[fileId]; + string memory oldPayToAccountId = file.payToAccountId; + file.payToAccountId = newPayToAccountId; + emit PayToChanged(fileId, oldPayToAccountId, newPayToAccountId); + } + + /** + * @notice Remove a file from the marketplace and block lookups by `fileId`. + * @dev Sets `exists` to false and removes `fileId` from the enumeration array so + * `getFiles` and `getFileCount` stay accurate. Off-chain objects in MinIO are not + * deleted; the owner must manage storage separately. The same `objectKey` may be + * registered again after delisting. + * @param fileId Id of the file to delist. + */ + function delistFile(bytes32 fileId) external onlyFileOwner(fileId) { + _files[fileId].exists = false; + _removeFromFileIds(fileId); + emit FileDelisted(fileId, msg.sender); + } + + /** + * @notice Deterministic file id for an (owner, objectKey) pair. + * @param owner Address registering the file. + * @param objectKey Storage object key. + * @return The file id. + */ + function computeFileId(address owner, string calldata objectKey) public pure returns (bytes32) { + return keccak256(abi.encodePacked(owner, objectKey)); + } + + /** + * @notice Fetch a registered file. + * @param fileId Id of the file. + * @return The file metadata. + */ + function getFile(bytes32 fileId) external view returns (FileItem memory) { + FileItem memory file = _files[fileId]; + if (!file.exists) revert FileNotFound(fileId); + return file; + } + + /// @notice Number of active (non-delist) files in the marketplace. + function getFileCount() external view returns (uint256) { + return _fileIds.length; + } + + /** + * @notice Paginated list of files for marketplace listing. + * @dev Returns at most `limit` files starting at `offset`, capped at {@link MAX_PAGE_SIZE}. If `offset` is + * beyond the end, returns empty arrays. + * @param offset Index to start from. + * @param limit Maximum number of files to return (values above `MAX_PAGE_SIZE` are clamped). + * @return ids The file ids in the returned page. + * @return files The file metadata in the returned page. + */ + function getFiles( + uint256 offset, + uint256 limit + ) external view returns (bytes32[] memory ids, FileItem[] memory files) { + if (limit > MAX_PAGE_SIZE) { + limit = MAX_PAGE_SIZE; + } + + uint256 total = _fileIds.length; + if (offset >= total || limit == 0) { + return (new bytes32[](0), new FileItem[](0)); + } + uint256 end = offset + limit; + if (end > total) end = total; + uint256 size = end - offset; + + ids = new bytes32[](size); + files = new FileItem[](size); + for (uint256 i = 0; i < size; i++) { + bytes32 id = _fileIds[offset + i]; + ids[i] = id; + files[i] = _files[id]; + } + } + + /// @dev Removes `fileId` from `_fileIds` using swap-and-pop (O(n) scan; fine for template scale). + function _removeFromFileIds(bytes32 fileId) private { + uint256 len = _fileIds.length; + for (uint256 i = 0; i < len; i++) { + if (_fileIds[i] == fileId) { + _fileIds[i] = _fileIds[len - 1]; + _fileIds.pop(); + return; + } + } + } +} diff --git a/packages/hardhat/contracts/HederaToken.sol b/packages/hardhat/contracts/HederaToken.sol deleted file mode 100644 index 16ce0872..00000000 --- a/packages/hardhat/contracts/HederaToken.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; - -contract HederaToken is ERC20, Ownable { - constructor(address initialOwner) ERC20("HederaToken", "HTK") Ownable(initialOwner) { - _mint(initialOwner, 10000 * 10 ** decimals()); - } - - function mint(address to, uint256 amount) public onlyOwner { - _mint(to, amount); - } -} diff --git a/packages/hardhat/contracts/HtsTokenCreator.sol b/packages/hardhat/contracts/HtsTokenCreator.sol deleted file mode 100644 index 7b46d421..00000000 --- a/packages/hardhat/contracts/HtsTokenCreator.sol +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import { IHederaTokenService } from "./interfaces/IHederaTokenService.sol"; - -/// Creates and mints fungible HTS tokens via the HTS precompile at 0x167. -contract HtsTokenCreator { - address public constant HTS = 0x0000000000000000000000000000000000000167; - - /// HTS response code for success - int64 public constant SUCCESS = 22; - - event TokenCreated(address indexed tokenAddress, string name, string symbol); - event TokenMinted(address indexed tokenAddress, int64 newTotalSupply); - - /// Creates a fungible HTS token. Treasury and initial supply recipient is msg.sender. - /// Sends msg.value as HBAR to the HTS precompile (required for creation fee). - /// @param initialSupply In smallest units (e.g. 10000 * 10**decimals for 10000 tokens). - function createToken( - string calldata name, - string calldata symbol, - uint256 initialSupply, - uint8 decimals - ) external payable returns (address tokenAddress) { - IHederaTokenService.HederaToken memory token = IHederaTokenService.HederaToken({ - name: name, - symbol: symbol, - treasury: msg.sender, - memo: "", - tokenSupplyType: false, - maxSupply: 0, - freezeDefault: false, - tokenKeys: _defaultTokenKeys(), - expiry: _defaultExpiry() - }); - - (int64 responseCode, address created) = IHederaTokenService(HTS).createFungibleToken{ value: msg.value }( - token, - int64(uint64(initialSupply)), - int32(uint32(decimals)) - ); - - if (responseCode != SUCCESS) { - revert HtsCreateFailed(responseCode); - } - - emit TokenCreated(created, name, symbol); - return created; - } - - /// Mints additional supply to the token's treasury. Caller must hold supply key. - function mintToken(address token, uint256 amount) external returns (int64 newTotalSupply) { - (int64 responseCode, int64 newSupply, ) = IHederaTokenService(HTS).mintToken( - token, - int64(uint64(amount)), - new bytes[](0) - ); - - if (responseCode != SUCCESS) { - revert HtsMintFailed(responseCode); - } - - emit TokenMinted(token, newSupply); - return newSupply; - } - - /// Supply key (bit 4) set to this contract so mintToken() can be called from the contract. - function _defaultTokenKeys() internal view returns (IHederaTokenService.TokenKey[] memory) { - IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](1); - keys[0] = IHederaTokenService.TokenKey({ - keyType: 16, // supply key - key: IHederaTokenService.KeyValue({ - inheritAccountKey: false, - contractId: address(this), - ed25519: "", - ECDSA_secp256k1: "", - delegatableContractId: address(0) - }) - }); - return keys; - } - - function _defaultExpiry() internal view returns (IHederaTokenService.Expiry memory) { - return IHederaTokenService.Expiry({ second: 0, autoRenewAccount: msg.sender, autoRenewPeriod: 7890000 }); - } - - error HtsCreateFailed(int64 responseCode); - error HtsMintFailed(int64 responseCode); -} diff --git a/packages/hardhat/contracts/interfaces/IHederaTokenService.sol b/packages/hardhat/contracts/interfaces/IHederaTokenService.sol deleted file mode 100644 index 63f3122e..00000000 --- a/packages/hardhat/contracts/interfaces/IHederaTokenService.sol +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -/// Minimal interface for the HTS precompile at 0x167 (create fungible token + mint). -/// Struct layout matches the official IHederaTokenService for ABI compatibility. -interface IHederaTokenService { - struct Expiry { - int64 second; - address autoRenewAccount; - int64 autoRenewPeriod; - } - - struct KeyValue { - bool inheritAccountKey; - address contractId; - bytes ed25519; - bytes ECDSA_secp256k1; - address delegatableContractId; - } - - struct TokenKey { - uint256 keyType; - KeyValue key; - } - - struct HederaToken { - string name; - string symbol; - address treasury; - string memo; - bool tokenSupplyType; - int64 maxSupply; - bool freezeDefault; - TokenKey[] tokenKeys; - Expiry expiry; - } - - /// Creates a Fungible Token with the specified properties. - /// @return responseCode SUCCESS is 22. - /// @return tokenAddress The created token's address. - function createFungibleToken( - HederaToken memory token, - int64 initialTotalSupply, - int32 decimals - ) external payable returns (int64 responseCode, address tokenAddress); - - /// Mints an amount of the token to the treasury account. - /// @param metadata For NFTs only; use empty array for fungible. - /// @return responseCode SUCCESS is 22. - function mintToken( - address token, - int64 amount, - bytes[] memory metadata - ) external returns (int64 responseCode, int64 newTotalSupply, int64[] memory serialNumbers); -} diff --git a/packages/hardhat/deploy/01_deploy_hts_token_creator.ts b/packages/hardhat/deploy/00_deploy_file_registry.ts similarity index 51% rename from packages/hardhat/deploy/01_deploy_hts_token_creator.ts rename to packages/hardhat/deploy/00_deploy_file_registry.ts index e2debc4c..d9825663 100644 --- a/packages/hardhat/deploy/01_deploy_hts_token_creator.ts +++ b/packages/hardhat/deploy/00_deploy_file_registry.ts @@ -1,18 +1,19 @@ import type { HardhatRuntimeEnvironment } from "hardhat/types"; import type { DeployFunction } from "hardhat-deploy/types"; -const deployHtsTokenCreator: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { +const deployFileRegistry: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { deployer } = await hre.getNamedAccounts(); const { deploy } = hre.deployments; - await deploy("HtsTokenCreator", { + await deploy("FileRegistry", { from: deployer, args: [], log: true, autoMine: true, + gasLimit: "3000000", + gasPrice: "1100000000000", }); }; -deployHtsTokenCreator.tags = ["HtsTokenCreator"]; -deployHtsTokenCreator.dependencies = ["HederaToken"]; -export default deployHtsTokenCreator; +deployFileRegistry.tags = ["FileRegistry"]; +export default deployFileRegistry; diff --git a/packages/hardhat/deploy/00_deploy_hedera_token.ts b/packages/hardhat/deploy/00_deploy_hedera_token.ts deleted file mode 100644 index 56595e3d..00000000 --- a/packages/hardhat/deploy/00_deploy_hedera_token.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { HardhatRuntimeEnvironment } from "hardhat/types"; -import type { DeployFunction } from "hardhat-deploy/types"; - -const deployHederaToken: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { - const { deployer } = await hre.getNamedAccounts(); - const { deploy } = hre.deployments; - - await deploy("HederaToken", { - from: deployer, - args: [deployer], - log: true, - autoMine: true, - }); -}; - -deployHederaToken.tags = ["HederaToken"]; -export default deployHederaToken; diff --git a/packages/hardhat/deploy/02_create_hts_token.ts b/packages/hardhat/deploy/02_create_hts_token.ts deleted file mode 100644 index 65c3d812..00000000 --- a/packages/hardhat/deploy/02_create_hts_token.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { HardhatRuntimeEnvironment } from "hardhat/types"; -import type { DeployFunction } from "hardhat-deploy/types"; - -const createHtsToken: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { - // Only auto-create an example HTS token on local networks (hardhat/localhost). - // On real networks like hederaTestnet / hederaMainnet we just deploy the contracts. - if (hre.network.name !== "hardhat" && hre.network.name !== "localhost") { - console.log(`Skipping HTS token creation on network ${hre.network.name}`); - return; - } - - const { deployer } = await hre.getNamedAccounts(); - const { deployments } = hre; - const { get } = deployments; - - const creator = await get("HtsTokenCreator"); - if (!creator?.address) return; - - const signer = await hre.ethers.getSigner(deployer); - const contract = await hre.ethers.getContractAt("HtsTokenCreator", creator.address, signer); - - // HTS uses int64 for supply (max 2^63-1). Use 6 decimals so 10000 tokens fits. - const name = "HTS Token"; - const symbol = "HTST"; - const initialSupply = hre.ethers.parseUnits("10000", 6); - const decimals = 6; - const hbarValue = 100_000_000n; // 1 HBAR (10^8 tinybars) for creation fee - - const tokenAddress = await contract.createToken(name, symbol, initialSupply, decimals, { - value: hbarValue, - }); - console.log(`Created HTS token: ${tokenAddress}`); - // On Hedera testnet/mainnet, users must associate their account with the token before receiving transfers. -}; - -createHtsToken.tags = ["HtsToken"]; -createHtsToken.dependencies = ["HtsTokenCreator"]; -export default createHtsToken; diff --git a/packages/hardhat/hardhat.config.ts b/packages/hardhat/hardhat.config.ts index 6ebaabf6..53907725 100644 --- a/packages/hardhat/hardhat.config.ts +++ b/packages/hardhat/hardhat.config.ts @@ -22,6 +22,11 @@ import generateTsAbis from "./scripts/generateTsAbis"; // Hedera JSON-RPC URL (testnet default). Set HEDERA_RPC_URL in .env for mainnet. const hederaRpcUrl = process.env.HEDERA_RPC_URL || "https://testnet.hashio.io/api"; +// Forking the live Hedera network is opt-in (yarn hardhat:chain / yarn hardhat:fork set HEDERA_FORKING=true). +// The FileRegistry contract is pure EVM and does not touch HTS/HSS precompiles, so unit tests and local +// compiles run hermetically without forking. Enable forking only when you need live Hedera system contracts. +const enableForking = process.env.HEDERA_FORKING === "true"; + // Deployer key: run `yarn account:generate` or `yarn account:import`, or set __RUNTIME_DEPLOYER_PRIVATE_KEY at runtime. const deployerPrivateKey = process.env.__RUNTIME_DEPLOYER_PRIVATE_KEY ?? "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; @@ -47,14 +52,16 @@ const config: HardhatUserConfig = { }, }, networks: { - hardhat: { - forking: { - url: hederaRpcUrl, - // @ts-expect-error - custom property for hedera-forking plugin - chainId: 296, - workerPort: 10001, - }, - }, + hardhat: enableForking + ? { + forking: { + url: hederaRpcUrl, + // @ts-expect-error - custom property for hedera-forking plugin + chainId: 296, + workerPort: 10001, + }, + } + : {}, hederaTestnet: { url: "https://testnet.hashio.io/api", accounts: [deployerPrivateKey], diff --git a/packages/hardhat/test/FileRegistry.test.ts b/packages/hardhat/test/FileRegistry.test.ts new file mode 100644 index 00000000..c4550503 --- /dev/null +++ b/packages/hardhat/test/FileRegistry.test.ts @@ -0,0 +1,346 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import type { FileRegistry } from "../typechain-types"; + +describe("FileRegistry", function () { + async function deployFixture() { + const [owner, alice, bob] = await ethers.getSigners(); + const FileRegistry = await ethers.getContractFactory("FileRegistry"); + const registry = await FileRegistry.deploy(); + await registry.waitForDeployment(); + return { registry, owner, alice, bob }; + } + + const sample = { + objectKey: "uploads/2026/report.pdf", + payToAccountId: "0.0.1234", + priceTinybar: 1_000_000_000n, // 10 HBAR + isPublic: false, + contentHash: ethers.id("the file contents"), + name: "Quarterly Report", + mimeType: "application/pdf", + }; + + async function register(registry: FileRegistry, overrides: Partial = {}) { + const f = { ...sample, ...overrides }; + return registry.registerFile( + f.objectKey, + f.payToAccountId, + f.priceTinybar, + f.isPublic, + f.contentHash, + f.name, + f.mimeType, + ); + } + + describe("Deployment", function () { + it("starts with no files", async function () { + const { registry } = await deployFixture(); + expect(await registry.getFileCount()).to.equal(0n); + }); + + it("exposes the HBAR payment asset id", async function () { + const { registry } = await deployFixture(); + expect(await registry.PAYMENT_ASSET()).to.equal("0.0.0"); + }); + }); + + describe("registerFile", function () { + it("registers a file and emits FileRegistered", async function () { + const { registry, owner } = await deployFixture(); + const fileId = await registry.computeFileId(owner.address, sample.objectKey); + + await expect(register(registry)) + .to.emit(registry, "FileRegistered") + .withArgs( + fileId, + owner.address, + sample.objectKey, + sample.payToAccountId, + sample.priceTinybar, + sample.isPublic, + sample.contentHash, + sample.name, + sample.mimeType, + ); + + expect(await registry.getFileCount()).to.equal(1n); + }); + + it("stores the file metadata", async function () { + const { registry, owner } = await deployFixture(); + await register(registry); + const fileId = await registry.computeFileId(owner.address, sample.objectKey); + + const file = await registry.getFile(fileId); + expect(file.owner).to.equal(owner.address); + expect(file.payToAccountId).to.equal(sample.payToAccountId); + expect(file.priceTinybar).to.equal(sample.priceTinybar); + expect(file.isPublic).to.equal(sample.isPublic); + expect(file.objectKey).to.equal(sample.objectKey); + expect(file.contentHash).to.equal(sample.contentHash); + expect(file.name).to.equal(sample.name); + expect(file.mimeType).to.equal(sample.mimeType); + expect(file.exists).to.equal(true); + }); + + it("computes a deterministic file id per (owner, objectKey)", async function () { + const { registry, owner, alice } = await deployFixture(); + const id1 = await registry.computeFileId(owner.address, sample.objectKey); + const id1Again = await registry.computeFileId(owner.address, sample.objectKey); + const idAlice = await registry.computeFileId(alice.address, sample.objectKey); + expect(id1).to.equal(id1Again); + expect(id1).to.not.equal(idAlice); + }); + + it("lets different owners register the same object key", async function () { + const { registry, alice } = await deployFixture(); + await register(registry); + await register(registry.connect(alice)); + expect(await registry.getFileCount()).to.equal(2n); + }); + + it("reverts when the object key is empty", async function () { + const { registry } = await deployFixture(); + await expect(register(registry, { objectKey: "" })) + .to.be.revertedWithCustomError(registry, "EmptyValue") + .withArgs("objectKey"); + }); + + it("reverts when the payTo account id is empty", async function () { + const { registry } = await deployFixture(); + await expect(register(registry, { payToAccountId: "" })) + .to.be.revertedWithCustomError(registry, "EmptyValue") + .withArgs("payToAccountId"); + }); + + it("reverts when the content hash is zero", async function () { + const { registry } = await deployFixture(); + await expect(register(registry, { contentHash: ethers.ZeroHash })).to.be.revertedWithCustomError( + registry, + "InvalidContentHash", + ); + }); + + it("reverts when the mime type is empty", async function () { + const { registry } = await deployFixture(); + await expect(register(registry, { mimeType: "" })) + .to.be.revertedWithCustomError(registry, "EmptyValue") + .withArgs("mimeType"); + }); + + it("reverts when the same owner registers the same object key twice", async function () { + const { registry, owner } = await deployFixture(); + await register(registry); + const fileId = await registry.computeFileId(owner.address, sample.objectKey); + await expect(register(registry)) + .to.be.revertedWithCustomError(registry, "FileAlreadyRegistered") + .withArgs(fileId); + }); + }); + + describe("setPrice", function () { + it("updates the price and emits PriceChanged", async function () { + const { registry, owner } = await deployFixture(); + await register(registry); + const fileId = await registry.computeFileId(owner.address, sample.objectKey); + const newPrice = 5_000_000_000n; + + await expect(registry.setPrice(fileId, newPrice)) + .to.emit(registry, "PriceChanged") + .withArgs(fileId, sample.priceTinybar, newPrice); + + expect((await registry.getFile(fileId)).priceTinybar).to.equal(newPrice); + }); + + it("reverts for a non-owner", async function () { + const { registry, owner, alice } = await deployFixture(); + await register(registry); + const fileId = await registry.computeFileId(owner.address, sample.objectKey); + await expect(registry.connect(alice).setPrice(fileId, 1n)) + .to.be.revertedWithCustomError(registry, "NotFileOwner") + .withArgs(fileId, alice.address); + }); + + it("reverts for an unknown file", async function () { + const { registry } = await deployFixture(); + const fileId = ethers.id("missing"); + await expect(registry.setPrice(fileId, 1n)) + .to.be.revertedWithCustomError(registry, "FileNotFound") + .withArgs(fileId); + }); + }); + + describe("setVisibility", function () { + it("toggles visibility and emits VisibilityChanged", async function () { + const { registry, owner } = await deployFixture(); + await register(registry, { isPublic: false }); + const fileId = await registry.computeFileId(owner.address, sample.objectKey); + + await expect(registry.setVisibility(fileId, true)).to.emit(registry, "VisibilityChanged").withArgs(fileId, true); + expect((await registry.getFile(fileId)).isPublic).to.equal(true); + }); + + it("reverts for a non-owner", async function () { + const { registry, owner, bob } = await deployFixture(); + await register(registry); + const fileId = await registry.computeFileId(owner.address, sample.objectKey); + await expect(registry.connect(bob).setVisibility(fileId, true)) + .to.be.revertedWithCustomError(registry, "NotFileOwner") + .withArgs(fileId, bob.address); + }); + }); + + describe("setPayToAccountId", function () { + it("updates the payout account and emits PayToChanged", async function () { + const { registry, owner } = await deployFixture(); + await register(registry); + const fileId = await registry.computeFileId(owner.address, sample.objectKey); + + await expect(registry.setPayToAccountId(fileId, "0.0.9999")) + .to.emit(registry, "PayToChanged") + .withArgs(fileId, sample.payToAccountId, "0.0.9999"); + expect((await registry.getFile(fileId)).payToAccountId).to.equal("0.0.9999"); + }); + + it("reverts when the new payTo is empty", async function () { + const { registry, owner } = await deployFixture(); + await register(registry); + const fileId = await registry.computeFileId(owner.address, sample.objectKey); + await expect(registry.setPayToAccountId(fileId, "")) + .to.be.revertedWithCustomError(registry, "EmptyValue") + .withArgs("payToAccountId"); + }); + + it("reverts for a non-owner", async function () { + const { registry, owner, alice } = await deployFixture(); + await register(registry); + const fileId = await registry.computeFileId(owner.address, sample.objectKey); + await expect(registry.connect(alice).setPayToAccountId(fileId, "0.0.1")).to.be.revertedWithCustomError( + registry, + "NotFileOwner", + ); + }); + }); + + describe("getFile", function () { + it("reverts for an unknown file", async function () { + const { registry } = await deployFixture(); + const fileId = ethers.id("missing"); + await expect(registry.getFile(fileId)).to.be.revertedWithCustomError(registry, "FileNotFound").withArgs(fileId); + }); + }); + + describe("delistFile", function () { + it("delists a file, emits FileDelisted, and removes it from the marketplace", async function () { + const { registry, owner } = await deployFixture(); + await register(registry); + const fileId = await registry.computeFileId(owner.address, sample.objectKey); + + await expect(registry.delistFile(fileId)).to.emit(registry, "FileDelisted").withArgs(fileId, owner.address); + + expect(await registry.getFileCount()).to.equal(0n); + await expect(registry.getFile(fileId)).to.be.revertedWithCustomError(registry, "FileNotFound").withArgs(fileId); + + const [ids] = await registry.getFiles(0, 10); + expect(ids.length).to.equal(0); + }); + + it("excludes a delisted file from paginated listings", async function () { + const { registry, owner } = await deployFixture(); + await register(registry, { objectKey: "uploads/a.bin", name: "A" }); + await register(registry, { objectKey: "uploads/b.bin", name: "B" }); + await register(registry, { objectKey: "uploads/c.bin", name: "C" }); + const fileIdB = await registry.computeFileId(owner.address, "uploads/b.bin"); + + await registry.delistFile(fileIdB); + expect(await registry.getFileCount()).to.equal(2n); + + const [, files] = await registry.getFiles(0, 10); + expect(files.map(f => f.name)).to.deep.equal(["A", "C"]); + }); + + it("lets the owner re-register the same object key after delisting", async function () { + const { registry, owner } = await deployFixture(); + await register(registry); + const fileId = await registry.computeFileId(owner.address, sample.objectKey); + await registry.delistFile(fileId); + + await expect(register(registry, { name: "Re-listed" })).to.emit(registry, "FileRegistered"); + expect(await registry.getFileCount()).to.equal(1n); + expect((await registry.getFile(fileId)).name).to.equal("Re-listed"); + }); + + it("reverts for a non-owner", async function () { + const { registry, owner, alice } = await deployFixture(); + await register(registry); + const fileId = await registry.computeFileId(owner.address, sample.objectKey); + await expect(registry.connect(alice).delistFile(fileId)) + .to.be.revertedWithCustomError(registry, "NotFileOwner") + .withArgs(fileId, alice.address); + }); + + it("reverts when delisting twice", async function () { + const { registry, owner } = await deployFixture(); + await register(registry); + const fileId = await registry.computeFileId(owner.address, sample.objectKey); + await registry.delistFile(fileId); + await expect(registry.delistFile(fileId)) + .to.be.revertedWithCustomError(registry, "FileNotFound") + .withArgs(fileId); + }); + }); + + describe("getFiles pagination", function () { + async function withThreeFiles() { + const ctx = await deployFixture(); + for (let i = 0; i < 3; i++) { + await register(ctx.registry, { objectKey: `uploads/file-${i}.bin`, name: `File ${i}` }); + } + return ctx; + } + + it("returns a bounded page", async function () { + const { registry } = await withThreeFiles(); + const [ids, files] = await registry.getFiles(0, 2); + expect(ids.length).to.equal(2); + expect(files.length).to.equal(2); + expect(files[0].name).to.equal("File 0"); + expect(files[1].name).to.equal("File 1"); + }); + + it("clamps the page to the number of remaining files", async function () { + const { registry } = await withThreeFiles(); + const [ids, files] = await registry.getFiles(2, 10); + expect(ids.length).to.equal(1); + expect(files[0].name).to.equal("File 2"); + }); + + it("returns empty arrays when the offset is past the end", async function () { + const { registry } = await withThreeFiles(); + const [ids, files] = await registry.getFiles(5, 2); + expect(ids.length).to.equal(0); + expect(files.length).to.equal(0); + }); + + it("returns empty arrays when the limit is zero", async function () { + const { registry } = await withThreeFiles(); + const [ids] = await registry.getFiles(0, 0); + expect(ids.length).to.equal(0); + }); + + it("clamps the page size to MAX_PAGE_SIZE", async function () { + const { registry } = await deployFixture(); + const maxPage = await registry.MAX_PAGE_SIZE(); + const fileCount = Number(maxPage) + 2; + + for (let i = 0; i < fileCount; i++) { + await register(registry, { objectKey: `uploads/cap-${i}.bin`, name: `Cap ${i}` }); + } + + const [ids] = await registry.getFiles(0, fileCount); + expect(ids.length).to.equal(Number(maxPage)); + }); + }); +}); diff --git a/packages/hardhat/test/HederaToken.test.ts b/packages/hardhat/test/HederaToken.test.ts deleted file mode 100644 index ed2bed13..00000000 --- a/packages/hardhat/test/HederaToken.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; - -describe("HederaToken", function () { - async function deployFixture() { - const [owner, alice, bob] = await ethers.getSigners(); - const HederaToken = await ethers.getContractFactory("HederaToken"); - const token = await HederaToken.deploy(owner.address); - await token.waitForDeployment(); - return { token, owner, alice, bob }; - } - - describe("Deployment", function () { - it("should set correct name and symbol", async function () { - const { token } = await deployFixture(); - expect(await token.name()).to.equal("HederaToken"); - expect(await token.symbol()).to.equal("HTK"); - }); - - it("should mint initial supply to owner", async function () { - const { token, owner } = await deployFixture(); - const balance = await token.balanceOf(owner.address); - expect(balance).to.equal(ethers.parseEther("10000")); - }); - }); - - describe("Transfers", function () { - it("should transfer tokens between accounts", async function () { - const { token, alice } = await deployFixture(); - await token.transfer(alice.address, ethers.parseEther("100")); - expect(await token.balanceOf(alice.address)).to.equal(ethers.parseEther("100")); - }); - - it("should fail transfer with insufficient balance", async function () { - const { token, alice, bob } = await deployFixture(); - await expect(token.connect(alice).transfer(bob.address, ethers.parseEther("1"))).to.be.revertedWithCustomError( - token, - "ERC20InsufficientBalance", - ); - }); - }); - - describe("Minting", function () { - it("should allow owner to mint", async function () { - const { token, alice } = await deployFixture(); - await token.mint(alice.address, ethers.parseEther("500")); - expect(await token.balanceOf(alice.address)).to.equal(ethers.parseEther("500")); - }); - - it("should reject minting from non-owner", async function () { - const { token, alice } = await deployFixture(); - await expect(token.connect(alice).mint(alice.address, ethers.parseEther("100"))).to.be.revertedWithCustomError( - token, - "OwnableUnauthorizedAccount", - ); - }); - }); -}); diff --git a/packages/hardhat/test/HtsTokenCreator.test.ts b/packages/hardhat/test/HtsTokenCreator.test.ts deleted file mode 100644 index c19c630a..00000000 --- a/packages/hardhat/test/HtsTokenCreator.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; - -describe("HtsTokenCreator", function () { - async function deployFixture() { - const [owner, alice] = await ethers.getSigners(); - const HtsTokenCreator = await ethers.getContractFactory("HtsTokenCreator"); - const creator = await HtsTokenCreator.deploy(); - await creator.waitForDeployment(); - return { creator, owner, alice }; - } - - // HTS uses int64 for supply; max is 2^63-1. Use 6 decimals so 10000 tokens = 1e10 fits. - const DECIMALS = 6; - const parseHtsUnits = (amount: string, decimals: number = DECIMALS) => ethers.parseUnits(amount, decimals); - // HTS createToken requires HBAR for creation fee; send 1 HBAR (10^8 tinybars/wei) in tests. - const HTS_CREATE_VALUE = 100_000_000n; - - describe("createToken", function () { - it("should create a fungible HTS token and return non-zero address", async function () { - const { creator } = await deployFixture(); - const name = "Test HTS Token"; - const symbol = "THT"; - const initialSupply = parseHtsUnits("10000"); - const decimals = DECIMALS; - - const tokenAddress = await creator.createToken(name, symbol, initialSupply, decimals, { - value: HTS_CREATE_VALUE, - }); - expect(tokenAddress).to.not.equal(ethers.ZeroAddress); - }); - - it("should set caller as treasury", async function () { - const { creator, owner } = await deployFixture(); - const initialSupply = parseHtsUnits("1000"); - const tokenAddress = await creator - .connect(owner) - .createToken("Treasury Token", "TRS", initialSupply, DECIMALS, { value: HTS_CREATE_VALUE }); - expect(tokenAddress).to.not.equal(ethers.ZeroAddress); - }); - }); - - describe("mintToken", function () { - it("should mint additional supply to token treasury", async function () { - const { creator } = await deployFixture(); - const initialSupply = parseHtsUnits("1000"); - const tokenAddress = await creator.createToken.staticCall("Mintable Token", "MNT", initialSupply, DECIMALS, { - value: HTS_CREATE_VALUE, - }); - await creator.createToken("Mintable Token", "MNT", initialSupply, DECIMALS, { value: HTS_CREATE_VALUE }); - - const mintAmount = parseHtsUnits("500"); - const newTotalSupply = await creator.mintToken.staticCall(tokenAddress, mintAmount); - expect(newTotalSupply).to.be.greaterThan(0n); - await creator.mintToken(tokenAddress, mintAmount); - - const secondMintSupply = await creator.mintToken.staticCall(tokenAddress, mintAmount); - await expect(creator.mintToken(tokenAddress, mintAmount)) - .to.emit(creator, "TokenMinted") - .withArgs(tokenAddress, secondMintSupply); - }); - }); -}); diff --git a/packages/nextjs/contracts/deployedContracts.ts b/packages/nextjs/contracts/deployedContracts.ts index b938e35b..c4001459 100644 --- a/packages/nextjs/contracts/deployedContracts.ts +++ b/packages/nextjs/contracts/deployedContracts.ts @@ -6,580 +6,536 @@ import { GenericContractsDeclaration } from "~~/utils/scaffold-hbar/contract"; const deployedContracts = { 296: { - HederaToken: { - address: "0xa510c1b5ebcefb83267f4f2bae2765611606c85a", + FileRegistry: { + address: "0xa92d5f34b4423dAAEA4ACf23d627cc9526C98016", abi: [ { - type: "constructor", inputs: [ { - name: "initialOwner", - type: "address", - internalType: "address", - }, - ], - stateMutability: "nonpayable", - }, - { - type: "function", - name: "allowance", - inputs: [ - { - name: "owner", - type: "address", - internalType: "address", - }, - { - name: "spender", - type: "address", - internalType: "address", - }, - ], - outputs: [ - { - name: "", - type: "uint256", - internalType: "uint256", + internalType: "string", + name: "field", + type: "string", }, ], - stateMutability: "view", + name: "EmptyValue", + type: "error", }, { - type: "function", - name: "approve", inputs: [ { - name: "spender", - type: "address", - internalType: "address", - }, - { - name: "value", - type: "uint256", - internalType: "uint256", + internalType: "bytes32", + name: "fileId", + type: "bytes32", }, ], - outputs: [ - { - name: "", - type: "bool", - internalType: "bool", - }, - ], - stateMutability: "nonpayable", + name: "FileAlreadyRegistered", + type: "error", }, { - type: "function", - name: "balanceOf", inputs: [ { - name: "account", - type: "address", - internalType: "address", + internalType: "bytes32", + name: "fileId", + type: "bytes32", }, ], - outputs: [ - { - name: "", - type: "uint256", - internalType: "uint256", - }, - ], - stateMutability: "view", + name: "FileNotFound", + type: "error", }, { - type: "function", - name: "decimals", inputs: [], - outputs: [ - { - name: "", - type: "uint8", - internalType: "uint8", - }, - ], - stateMutability: "view", + name: "InvalidContentHash", + type: "error", }, { - type: "function", - name: "mint", inputs: [ { - name: "to", - type: "address", - internalType: "address", + internalType: "bytes32", + name: "fileId", + type: "bytes32", }, { - name: "amount", - type: "uint256", - internalType: "uint256", + internalType: "address", + name: "caller", + type: "address", }, ], - outputs: [], - stateMutability: "nonpayable", + name: "NotFileOwner", + type: "error", }, { - type: "function", - name: "name", - inputs: [], - outputs: [ + anonymous: false, + inputs: [ { - name: "", - type: "string", - internalType: "string", + indexed: true, + internalType: "bytes32", + name: "fileId", + type: "bytes32", }, - ], - stateMutability: "view", - }, - { - type: "function", - name: "owner", - inputs: [], - outputs: [ { - name: "", - type: "address", + indexed: true, internalType: "address", + name: "owner", + type: "address", }, ], - stateMutability: "view", - }, - { - type: "function", - name: "renounceOwnership", - inputs: [], - outputs: [], - stateMutability: "nonpayable", - }, - { - type: "function", - name: "symbol", - inputs: [], - outputs: [ - { - name: "", - type: "string", - internalType: "string", - }, - ], - stateMutability: "view", + name: "FileDelisted", + type: "event", }, { - type: "function", - name: "totalSupply", - inputs: [], - outputs: [ + anonymous: false, + inputs: [ { - name: "", - type: "uint256", - internalType: "uint256", + indexed: true, + internalType: "bytes32", + name: "fileId", + type: "bytes32", }, - ], - stateMutability: "view", - }, - { - type: "function", - name: "transfer", - inputs: [ { - name: "to", - type: "address", + indexed: true, internalType: "address", + name: "owner", + type: "address", }, { - name: "value", - type: "uint256", - internalType: "uint256", + indexed: false, + internalType: "string", + name: "objectKey", + type: "string", }, - ], - outputs: [ { - name: "", - type: "bool", - internalType: "bool", + indexed: false, + internalType: "string", + name: "payToAccountId", + type: "string", }, - ], - stateMutability: "nonpayable", - }, - { - type: "function", - name: "transferFrom", - inputs: [ { - name: "from", - type: "address", - internalType: "address", + indexed: false, + internalType: "uint256", + name: "priceTinybar", + type: "uint256", }, { - name: "to", - type: "address", - internalType: "address", + indexed: false, + internalType: "bool", + name: "isPublic", + type: "bool", }, { - name: "value", - type: "uint256", - internalType: "uint256", + indexed: false, + internalType: "bytes32", + name: "contentHash", + type: "bytes32", }, - ], - outputs: [ { - name: "", - type: "bool", - internalType: "bool", + indexed: false, + internalType: "string", + name: "name", + type: "string", }, - ], - stateMutability: "nonpayable", - }, - { - type: "function", - name: "transferOwnership", - inputs: [ { - name: "newOwner", - type: "address", - internalType: "address", + indexed: false, + internalType: "string", + name: "mimeType", + type: "string", }, ], - outputs: [], - stateMutability: "nonpayable", + name: "FileRegistered", + type: "event", }, { - type: "event", - name: "Approval", + anonymous: false, inputs: [ { - name: "owner", - type: "address", indexed: true, - internalType: "address", + internalType: "bytes32", + name: "fileId", + type: "bytes32", }, { - name: "spender", - type: "address", - indexed: true, - internalType: "address", + indexed: false, + internalType: "string", + name: "oldPayToAccountId", + type: "string", }, { - name: "value", - type: "uint256", indexed: false, - internalType: "uint256", + internalType: "string", + name: "newPayToAccountId", + type: "string", }, ], - anonymous: false, - }, - { + name: "PayToChanged", type: "event", - name: "OwnershipTransferred", - inputs: [ - { - name: "previousOwner", - type: "address", - indexed: true, - internalType: "address", - }, - { - name: "newOwner", - type: "address", - indexed: true, - internalType: "address", - }, - ], - anonymous: false, }, { - type: "event", - name: "Transfer", + anonymous: false, inputs: [ { - name: "from", - type: "address", indexed: true, - internalType: "address", + internalType: "bytes32", + name: "fileId", + type: "bytes32", }, { - name: "to", - type: "address", - indexed: true, - internalType: "address", + indexed: false, + internalType: "uint256", + name: "oldPriceTinybar", + type: "uint256", }, { - name: "value", - type: "uint256", indexed: false, internalType: "uint256", + name: "newPriceTinybar", + type: "uint256", }, ], - anonymous: false, + name: "PriceChanged", + type: "event", }, { - type: "error", - name: "ERC20InsufficientAllowance", + anonymous: false, inputs: [ { - name: "spender", - type: "address", - internalType: "address", - }, - { - name: "allowance", - type: "uint256", - internalType: "uint256", + indexed: true, + internalType: "bytes32", + name: "fileId", + type: "bytes32", }, { - name: "needed", - type: "uint256", - internalType: "uint256", + indexed: false, + internalType: "bool", + name: "isPublic", + type: "bool", }, ], + name: "VisibilityChanged", + type: "event", }, { - type: "error", - name: "ERC20InsufficientBalance", - inputs: [ - { - name: "sender", - type: "address", - internalType: "address", - }, + inputs: [], + name: "MAX_PAGE_SIZE", + outputs: [ { - name: "balance", - type: "uint256", internalType: "uint256", - }, - { - name: "needed", + name: "", type: "uint256", - internalType: "uint256", }, ], + stateMutability: "view", + type: "function", }, { - type: "error", - name: "ERC20InvalidApprover", - inputs: [ + inputs: [], + name: "PAYMENT_ASSET", + outputs: [ { - name: "approver", - type: "address", - internalType: "address", + internalType: "string", + name: "", + type: "string", }, ], + stateMutability: "view", + type: "function", }, { - type: "error", - name: "ERC20InvalidReceiver", inputs: [ { - name: "receiver", - type: "address", internalType: "address", + name: "owner", + type: "address", + }, + { + internalType: "string", + name: "objectKey", + type: "string", }, ], - }, - { - type: "error", - name: "ERC20InvalidSender", - inputs: [ + name: "computeFileId", + outputs: [ { - name: "sender", - type: "address", - internalType: "address", + internalType: "bytes32", + name: "", + type: "bytes32", }, ], + stateMutability: "pure", + type: "function", }, { - type: "error", - name: "ERC20InvalidSpender", inputs: [ { - name: "spender", - type: "address", - internalType: "address", + internalType: "bytes32", + name: "fileId", + type: "bytes32", }, ], + name: "delistFile", + outputs: [], + stateMutability: "nonpayable", + type: "function", }, { - type: "error", - name: "OwnableInvalidOwner", inputs: [ { - name: "owner", - type: "address", - internalType: "address", + internalType: "bytes32", + name: "fileId", + type: "bytes32", }, ], - }, - { - type: "error", - name: "OwnableUnauthorizedAccount", - inputs: [ + name: "getFile", + outputs: [ { - name: "account", - type: "address", - internalType: "address", + components: [ + { + internalType: "address", + name: "owner", + type: "address", + }, + { + internalType: "string", + name: "payToAccountId", + type: "string", + }, + { + internalType: "uint256", + name: "priceTinybar", + type: "uint256", + }, + { + internalType: "bool", + name: "isPublic", + type: "bool", + }, + { + internalType: "string", + name: "objectKey", + type: "string", + }, + { + internalType: "bytes32", + name: "contentHash", + type: "bytes32", + }, + { + internalType: "string", + name: "name", + type: "string", + }, + { + internalType: "string", + name: "mimeType", + type: "string", + }, + { + internalType: "bool", + name: "exists", + type: "bool", + }, + ], + internalType: "struct FileRegistry.FileItem", + name: "", + type: "tuple", }, ], + stateMutability: "view", + type: "function", }, - ], - inheritedFunctions: {}, - deployedOnBlock: 33578755, - }, - HtsTokenCreator: { - address: "0x03fcda15d3955b20557028db9fabe6f5847f00ab", - abi: [ { - type: "function", - name: "HTS", inputs: [], + name: "getFileCount", outputs: [ { + internalType: "uint256", name: "", - type: "address", - internalType: "address", + type: "uint256", }, ], stateMutability: "view", + type: "function", }, { - type: "function", - name: "SUCCESS", - inputs: [], + inputs: [ + { + internalType: "uint256", + name: "offset", + type: "uint256", + }, + { + internalType: "uint256", + name: "limit", + type: "uint256", + }, + ], + name: "getFiles", outputs: [ { - name: "", - type: "int64", - internalType: "int64", + internalType: "bytes32[]", + name: "ids", + type: "bytes32[]", + }, + { + components: [ + { + internalType: "address", + name: "owner", + type: "address", + }, + { + internalType: "string", + name: "payToAccountId", + type: "string", + }, + { + internalType: "uint256", + name: "priceTinybar", + type: "uint256", + }, + { + internalType: "bool", + name: "isPublic", + type: "bool", + }, + { + internalType: "string", + name: "objectKey", + type: "string", + }, + { + internalType: "bytes32", + name: "contentHash", + type: "bytes32", + }, + { + internalType: "string", + name: "name", + type: "string", + }, + { + internalType: "string", + name: "mimeType", + type: "string", + }, + { + internalType: "bool", + name: "exists", + type: "bool", + }, + ], + internalType: "struct FileRegistry.FileItem[]", + name: "files", + type: "tuple[]", }, ], stateMutability: "view", + type: "function", }, { - type: "function", - name: "createToken", inputs: [ { - name: "name", - type: "string", internalType: "string", + name: "objectKey", + type: "string", }, { - name: "symbol", - type: "string", internalType: "string", + name: "payToAccountId", + type: "string", }, { - name: "initialSupply", - type: "uint256", internalType: "uint256", + name: "priceTinybar", + type: "uint256", }, { - name: "decimals", - type: "uint8", - internalType: "uint8", + internalType: "bool", + name: "isPublic", + type: "bool", }, - ], - outputs: [ { - name: "tokenAddress", - type: "address", - internalType: "address", + internalType: "bytes32", + name: "contentHash", + type: "bytes32", }, - ], - stateMutability: "payable", - }, - { - type: "function", - name: "mintToken", - inputs: [ { - name: "token", - type: "address", - internalType: "address", + internalType: "string", + name: "name", + type: "string", }, { - name: "amount", - type: "uint256", - internalType: "uint256", + internalType: "string", + name: "mimeType", + type: "string", }, ], + name: "registerFile", outputs: [ { - name: "newTotalSupply", - type: "int64", - internalType: "int64", + internalType: "bytes32", + name: "fileId", + type: "bytes32", }, ], stateMutability: "nonpayable", + type: "function", }, { - type: "event", - name: "TokenCreated", inputs: [ { - name: "tokenAddress", - type: "address", - indexed: true, - internalType: "address", + internalType: "bytes32", + name: "fileId", + type: "bytes32", }, { - name: "name", - type: "string", - indexed: false, internalType: "string", - }, - { - name: "symbol", + name: "newPayToAccountId", type: "string", - indexed: false, - internalType: "string", }, ], - anonymous: false, + name: "setPayToAccountId", + outputs: [], + stateMutability: "nonpayable", + type: "function", }, { - type: "event", - name: "TokenMinted", inputs: [ { - name: "tokenAddress", - type: "address", - indexed: true, - internalType: "address", + internalType: "bytes32", + name: "fileId", + type: "bytes32", }, { - name: "newTotalSupply", - type: "int64", - indexed: false, - internalType: "int64", + internalType: "uint256", + name: "newPriceTinybar", + type: "uint256", }, ], - anonymous: false, + name: "setPrice", + outputs: [], + stateMutability: "nonpayable", + type: "function", }, { - type: "error", - name: "HtsCreateFailed", inputs: [ { - name: "responseCode", - type: "int64", - internalType: "int64", + internalType: "bytes32", + name: "fileId", + type: "bytes32", }, - ], - }, - { - type: "error", - name: "HtsMintFailed", - inputs: [ { - name: "responseCode", - type: "int64", - internalType: "int64", + internalType: "bool", + name: "isPublic", + type: "bool", }, ], + name: "setVisibility", + outputs: [], + stateMutability: "nonpayable", + type: "function", }, ], inheritedFunctions: {}, - deployedOnBlock: 33578759, + deployedOnBlock: 36432979, }, }, } as const;