diff --git a/package.json b/package.json index a569f19..2436d72 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ }, "packageManager": "yarn@3.2.3", "devDependencies": { + "@nomicfoundation/hardhat-network-helpers": "^1.0.12", "husky": "^9.1.6", "lint-staged": "^15.2.10" }, diff --git a/packages/hardhat/contracts/CanvasOnchain.sol b/packages/hardhat/contracts/CanvasOnchain.sol new file mode 100644 index 0000000..9679698 --- /dev/null +++ b/packages/hardhat/contracts/CanvasOnchain.sol @@ -0,0 +1,122 @@ +// 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; + } + + // 2D Mapping to store pixels + Pixel[CANVAS_WIDTH][CANVAS_HEIGHT] public canvas; + + // Events + event PixelPlaced(address indexed author, uint256 x, uint256 y, Color color); + + // 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 { + // B letter representation + for (uint256 x = 10; x < 20; x++) { + for (uint256 y = 10; y < 50; y++) { + canvas[x][y] = Pixel({ + author: owner(), + 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: owner(), + color: Color.GREEN, + timestamp: block.timestamp + }); + } + } + + // Add some distinctive pixels to represent Buidlguidl spirit + canvas[32][25] = Pixel({ + author: owner(), + 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 { + // Validate pixel coordinates + require(x < CANVAS_WIDTH, "X coordinate out of bounds"); + require(y < CANVAS_HEIGHT, "Y coordinate out of bounds"); + + // Update pixel + canvas[x][y] = Pixel({ + author: msg.sender, + color: color, + timestamp: block.timestamp + }); + + // Emit event + 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]; + } + + + + + /** + * @dev Get canvas snapshot (useful for viewing entire canvas) + * @return Array of pixels + */ + function getCanvasSnapshot() external view returns (Pixel[CANVAS_WIDTH][CANVAS_HEIGHT] memory) { + return canvas; + } +} diff --git a/packages/hardhat/hardhat.config.ts b/packages/hardhat/hardhat.config.ts index e82e01b..878af2f 100644 --- a/packages/hardhat/hardhat.config.ts +++ b/packages/hardhat/hardhat.config.ts @@ -7,6 +7,7 @@ import "@typechain/hardhat"; import "hardhat-gas-reporter"; import "solidity-coverage"; import "@nomicfoundation/hardhat-verify"; +import "@nomicfoundation/hardhat-network-helpers"; import "hardhat-deploy"; import "hardhat-deploy-ethers"; diff --git a/packages/hardhat/test/CanvasOnchain.ts b/packages/hardhat/test/CanvasOnchain.ts new file mode 100644 index 0000000..2ced047 --- /dev/null +++ b/packages/hardhat/test/CanvasOnchain.ts @@ -0,0 +1,83 @@ +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); + }); + }); + + describe("Canvas Snapshot", function () { + it("Should return full canvas snapshot", async function () { + const { Pixel } = await loadFixture(deployPixelCanvas); + const snapshot = await Pixel.getCanvasSnapshot(); + expect(snapshot.length).to.equal(64); + expect(snapshot[0].length).to.equal(64); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index e0de057..0e96696 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1739,6 +1739,17 @@ __metadata: languageName: node linkType: hard +"@nomicfoundation/hardhat-network-helpers@npm:^1.0.12": + version: 1.0.12 + resolution: "@nomicfoundation/hardhat-network-helpers@npm:1.0.12" + dependencies: + ethereumjs-util: ^7.1.4 + peerDependencies: + hardhat: ^2.9.5 + checksum: 7e1b91789dd4e73464b4eec919b1e67c6d482dd7534f4f7cae73fb5bdddd69f2a47143754b34385b098a1df0f4875cd4d2e1109fc3d847db76f4b0a9a44bd959 + languageName: node + linkType: hard + "@nomicfoundation/hardhat-verify@npm:^2.0.10": version: 2.0.10 resolution: "@nomicfoundation/hardhat-verify@npm:2.0.10" @@ -11941,6 +11952,7 @@ __metadata: version: 0.0.0-use.local resolution: "se-2@workspace:." dependencies: + "@nomicfoundation/hardhat-network-helpers": ^1.0.12 husky: ^9.1.6 lint-staged: ^15.2.10 react-icons: ^5.3.0