diff --git a/packages/hardhat/Readme.md b/packages/hardhat/Readme.md new file mode 100644 index 0000000..da8bb4b --- /dev/null +++ b/packages/hardhat/Readme.md @@ -0,0 +1,51 @@ +## PixelCanvas: Decentralized Collaborative Pixel Art Smart Contract + +### Contract Overview +PixelCanvas is an Ethereum smart contract that enables collaborative pixel art creation on a fixed 64x64 canvas with a predefined color palette. + +### Technical Specifications +- **Blockchain**: Ethereum +- **Solidity Version**: ^0.8.20 +- **Dependencies**: OpenZeppelin Ownable +- **Canvas Dimensions**: 64x64 pixels +- **Color Palette**: 8 predefined colors + +### Key Components + +#### Pixel Structure +```solidity +struct Pixel { + address author; // Address of pixel creator + Color color; // Chosen color from enum + uint256 timestamp;// Block timestamp of pixel placement +} + +Core Functionalities +1. Pixel Placement + +Function: placePixel(uint256 x, uint256 y, Color color) +Validates coordinate boundaries +Allows user to place a single pixel +Emits PixelPlaced event for tracking + +2. Canvas Retrieval +getPixel(x, y): Retrieves individual pixel information +initializeBuidlGuidlLogo(): Sets initial canvas state with Batch11 logo + +3. Contract Management +withdraw(): Allows owner to withdraw contract balance +Accepts Ether via fallback() and receive() functions + +4. Initialization +Test the PixelCanvas contract +Includes default Buidlguidl Batch11 logo on contract deployment +Demonstrates initial canvas state + +Current Limitations +Fixed canvas size (64x64) +No pixel modification after placement +Limited to 8 colors +Only contract owner can withdraw funds + + + diff --git a/packages/hardhat/contracts/PixelCanvas.sol b/packages/hardhat/contracts/PixelCanvas.sol new file mode 100644 index 0000000..e4b02d6 --- /dev/null +++ b/packages/hardhat/contracts/PixelCanvas.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract PixelCanvas is Ownable { + // Canvas Dimensions + uint256 public constant CANVAS_WIDTH = 64; + uint256 public constant CANVAS_HEIGHT = 64; + + // Color Palette (Limited Options) + enum Color { + WHITE, + BLACK, + RED, + GREEN, + BLUE, + YELLOW, + PURPLE, + ORANGE + } + + // Pixel Structure + struct Pixel { + address author; + Color color; + uint256 timestamp; + } + + // Mapping to store modified pixels +mapping(uint256 => mapping(uint256 => Pixel)) public canvas; + + + // Events + event PixelPlaced(address indexed author, uint256 x, uint256 y, Color color); + event Withdrawal(address indexed owner, uint256 amount); + + // Constructor to initialize default Buidlguidl Batch11 drawing + constructor() Ownable(msg.sender){ + + // Initial drawing representing Buidlguidl Batch11 logo + initializeBuidlGuidlLogo(); + } + + /** + * @dev Initialize a default Buidlguidl Batch11 inspired pixel art + * This is a simplified representation and can be customized + */ + function initializeBuidlGuidlLogo() private { + for (uint256 x = 10; x < 20; x++) { + for (uint256 y = 10; y < 50; y++) { + canvas[x][y] = Pixel({ + author: msg.sender, + color: Color.BLUE, + timestamp: block.timestamp + }); + } + } + + // 11 representation with some pixels + for (uint256 x = 30; x < 40; x++) { + for (uint256 y = 20; y < 30; y++) { + canvas[x][y] = Pixel({ + author: msg.sender, + color: Color.GREEN, + timestamp: block.timestamp + }); + } + } + + // Add some distinctive pixels to represent Buidlguidl spirit + canvas[32][25] = Pixel({ + author: msg.sender, + color: Color.RED, + timestamp: block.timestamp + }); + } + + /** + * @dev Place a pixel on the canvas + * @param x X-coordinate of the pixel + * @param y Y-coordinate of the pixel + * @param color Color of the pixel + */ + function placePixel(uint256 x, uint256 y, Color color) external { + require(x < CANVAS_WIDTH, "X coordinate out of bounds"); + require(y < CANVAS_HEIGHT, "Y coordinate out of bounds"); + + canvas[x][y] = Pixel({ + author: msg.sender, + color: color, + timestamp: block.timestamp + }); + + emit PixelPlaced(msg.sender, x, y, color); +} + + + /** + * @dev Get pixel information + * @param x X-coordinate of the pixel + * @param y Y-coordinate of the pixel + * @return Pixel details + */ + function getPixel(uint256 x, uint256 y) external view returns (Pixel memory) { + require(x < CANVAS_WIDTH, "X coordinate out of bounds"); + require(y < CANVAS_HEIGHT, "Y coordinate out of bounds"); + return canvas[x][y]; +} + + + + function withdraw() external onlyOwner { + uint256 balance = address(this).balance; + require(balance > 0, "No funds to withdraw"); + + + (bool success, ) = payable(owner()).call{value: balance}(""); + require(success, "Transfer failed"); + + + emit Withdrawal(owner(), balance); +} + + // Fallback and receive functions to accept Ether + fallback() external payable {} + + receive() external payable {} +} diff --git a/packages/hardhat/deploy/00_deploy_your_contract.ts b/packages/hardhat/deploy/00_deploy_your_contract.ts index 1fc13e0..d9f8943 100644 --- a/packages/hardhat/deploy/00_deploy_your_contract.ts +++ b/packages/hardhat/deploy/00_deploy_your_contract.ts @@ -30,6 +30,11 @@ const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEn // Contract constructor arguments args: [deployer, BATCH_NUMBER], log: true, + }); + + await deploy("PixelCanvas", { + from: deployer, + log: true, // autoMine: can be passed to the deploy function to make the deployment process faster on local networks by // automatically mining the contract deployment transaction. There is no effect on live networks. autoMine: true, @@ -40,13 +45,21 @@ const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEn console.log("\nBatchRegistry deployed to:", await batchRegistry.getAddress()); console.log("Remember to update the allow list!\n"); + const pixelCanvas = await hre.ethers.getContract("PixelCanvas", deployer); + console.log("\nPixelCanvas deployed to:", await pixelCanvas.getAddress()); + // The GraduationNFT contract is deployed on the BatchRegistry constructor. const batchGraduationNFTAddress = await batchRegistry.batchGraduationNFT(); console.log("BatchGraduation NFT deployed to:", batchGraduationNFTAddress, "\n"); + + // Verify initial canvas state + const canvasWidth = await pixelCanvas.CANVAS_WIDTH(); + const canvasHeight = await pixelCanvas.CANVAS_HEIGHT(); + console.log(`Canvas dimensions: ${canvasWidth}x${canvasHeight}`); }; export default deployYourContract; // Tags are useful if you have multiple deploy files and only want to run one of them. // e.g. yarn deploy --tags YourContract -deployYourContract.tags = ["BatchRegistry"]; +deployYourContract.tags = ["BatchRegistry", "PixelCanvas"]; diff --git a/packages/hardhat/hardhat.config.ts b/packages/hardhat/hardhat.config.ts index e82e01b..ba28b3b 100644 --- a/packages/hardhat/hardhat.config.ts +++ b/packages/hardhat/hardhat.config.ts @@ -54,7 +54,7 @@ const config: HardhatUserConfig = { accounts: [deployerPrivateKey], }, sepolia: { - url: `https://rpc2.sepolia.org`, + url: `https://ethereum-sepolia.blockpi.network/v1/rpc/public`, accounts: [deployerPrivateKey], }, arbitrum: { diff --git a/packages/hardhat/test/PixelCanvas.ts b/packages/hardhat/test/PixelCanvas.ts new file mode 100644 index 0000000..b78bfca --- /dev/null +++ b/packages/hardhat/test/PixelCanvas.ts @@ -0,0 +1,74 @@ +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import hre from "hardhat"; + +describe("PixelCanvas", function () { + async function deployPixelCanvas() { + const [owner, user1, user2] = await hre.ethers.getSigners(); + const pixel = await hre.ethers.getContractFactory("PixelCanvas"); + const Pixel = await pixel.deploy(); + return { owner, user1, user2, Pixel }; + } + + describe("Deployment", function () { + it("Should set the correct owner", async function () { + const { owner, Pixel } = await loadFixture(deployPixelCanvas); + expect(await Pixel.owner()).to.equal(owner.address); + }); + + it("Should initialize canvas with predefined pixels", async function () { + // Check a few predefined pixels from initializeBuidlGuidlLogo() + const { owner, Pixel } = await loadFixture(deployPixelCanvas); + const bluePixel = await Pixel.canvas(15, 25); + expect(bluePixel.color).to.equal(4); // Blue is enum index 1 + expect(bluePixel.author).to.equal(owner.address); + }); + }); + + describe("Pixel Placement", function () { + it("Should allow placing a pixel", async function () { + const x = 10; + const y = 20; + const color = 2; // Red from enum + const { user1, Pixel } = await loadFixture(deployPixelCanvas); + // Place pixel from user1 + await Pixel.connect(user1).placePixel(x, y, color); + + const pixel = await Pixel.canvas(x, y); + expect(pixel.author).to.equal(user1.address); + expect(pixel.color).to.equal(color); + }); + + it("Should reject out-of-bounds pixel placement", async function () { + const { Pixel } = await loadFixture(deployPixelCanvas); + await expect(Pixel.placePixel(64, 10, 0)).to.be.revertedWith("X coordinate out of bounds"); + + await expect(Pixel.placePixel(10, 64, 0)).to.be.revertedWith("Y coordinate out of bounds"); + }); + + it("Should emit PixelPlaced event", async function () { + const x = 30; + const y = 40; + + const color = 3; // Green from enum + const { user1, Pixel } = await loadFixture(deployPixelCanvas); + await expect(Pixel.connect(user1).placePixel(x, y, color)) + .to.emit(Pixel, "PixelPlaced") + .withArgs(user1.address, x, y, color); + }); + }); + + describe("Pixel Retrieval", function () { + it("Should retrieve pixel information", async function () { + const x = 12; + const y = 15; + const color = 4; // Blue from enum + const { user1, Pixel } = await loadFixture(deployPixelCanvas); + await Pixel.connect(user1).placePixel(x, y, color); + + const pixel = await Pixel.getPixel(x, y); + expect(pixel.author).to.equal(user1.address); + expect(pixel.color).to.equal(color); + }); + }); +}); diff --git a/packages/nextjs/contracts/externalContracts.ts b/packages/nextjs/contracts/externalContracts.ts index 1af01ae..edf87fd 100644 --- a/packages/nextjs/contracts/externalContracts.ts +++ b/packages/nextjs/contracts/externalContracts.ts @@ -347,6 +347,742 @@ const externalContracts = { transferOwnership: "@openzeppelin/contracts/access/Ownable.sol", }, }, + BatchGraduationNFT: { + address: "0x72A22AE8dDabA9D316Ffed3d5C0A46F7Ea8Aa809", + abi: [ + { + inputs: [ + { + internalType: "address", + name: "_batchRegistry", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "NoMetadataSet", + type: "error", + }, + { + inputs: [], + name: "UnauthorizedCaller", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "approved", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "Approval", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "operator", + type: "address", + }, + { + indexed: false, + internalType: "bool", + name: "approved", + type: "bool", + }, + ], + name: "ApprovalForAll", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "builder", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "metadataContract", + type: "address", + }, + ], + name: "MetadataSet", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "from", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "to", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "Transfer", + type: "event", + }, + { + inputs: [ + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "approve", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "owner", + type: "address", + }, + ], + name: "balanceOf", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "batchRegistry", + outputs: [ + { + internalType: "contract IBatchRegistry", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "getApproved", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "owner", + type: "address", + }, + { + internalType: "address", + name: "operator", + type: "address", + }, + ], + name: "isApprovedForAll", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "builder", + type: "address", + }, + ], + name: "mint", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "name", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "ownerOf", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "from", + type: "address", + }, + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "safeTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "from", + type: "address", + }, + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + name: "safeTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "operator", + type: "address", + }, + { + internalType: "bool", + name: "approved", + type: "bool", + }, + ], + name: "setApprovalForAll", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "metadataContract", + type: "address", + }, + ], + name: "setMetadataContract", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes4", + name: "interfaceId", + type: "bytes4", + }, + ], + name: "supportsInterface", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "symbol", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + name: "tokenToMetadataContract", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "tokenURI", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "from", + type: "address", + }, + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "transferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + name: "yourGraduationContractAddress", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + ], + }, + PixelCanvas: { + address: "0x54c92BB9f6c3d3416c22070bcD5678cfF57784B4", + abi: [ + { + inputs: [], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [ + { + internalType: "address", + name: "owner", + type: "address", + }, + ], + name: "OwnableInvalidOwner", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "OwnableUnauthorizedAccount", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "previousOwner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "OwnershipTransferred", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "author", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "x", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "y", + type: "uint256", + }, + { + indexed: false, + internalType: "enum PixelCanvas.Color", + name: "color", + type: "uint8", + }, + ], + name: "PixelPlaced", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "Withdrawal", + type: "event", + }, + { + stateMutability: "payable", + type: "fallback", + }, + { + inputs: [], + name: "CANVAS_HEIGHT", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "CANVAS_WIDTH", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + name: "canvas", + outputs: [ + { + internalType: "address", + name: "author", + type: "address", + }, + { + internalType: "enum PixelCanvas.Color", + name: "color", + type: "uint8", + }, + { + internalType: "uint256", + name: "timestamp", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "x", + type: "uint256", + }, + { + internalType: "uint256", + name: "y", + type: "uint256", + }, + ], + name: "getPixel", + outputs: [ + { + components: [ + { + internalType: "address", + name: "author", + type: "address", + }, + { + internalType: "enum PixelCanvas.Color", + name: "color", + type: "uint8", + }, + { + internalType: "uint256", + name: "timestamp", + type: "uint256", + }, + ], + internalType: "struct PixelCanvas.Pixel", + name: "", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "x", + type: "uint256", + }, + { + internalType: "uint256", + name: "y", + type: "uint256", + }, + { + internalType: "enum PixelCanvas.Color", + name: "color", + type: "uint8", + }, + ], + name: "placePixel", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "renounceOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "transferOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "withdraw", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + stateMutability: "payable", + type: "receive", + }, + ], + inheritedFunctions: { + owner: "@openzeppelin/contracts/access/Ownable.sol", + renounceOwnership: "@openzeppelin/contracts/access/Ownable.sol", + transferOwnership: "@openzeppelin/contracts/access/Ownable.sol", + }, + }, }, } as const;