Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change the branch name to canvasOnchain and also write a readme file in the hardhat folder for description #32

Merged
merged 13 commits into from
Dec 17, 2024
Merged
2 changes: 2 additions & 0 deletions package.json
phipsae marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No changes to this file, pls.

Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@
},
"packageManager": "[email protected]",
"devDependencies": {
"@nomicfoundation/hardhat-network-helpers": "^1.0.12",
phipsae marked this conversation as resolved.
Show resolved Hide resolved
"husky": "^9.1.6",
"lint-staged": "^15.2.10"
},
"engines": {
"node": ">=18.18.0"
},
"dependencies": {
"hardhat": "^2.22.17",
"react-icons": "^5.3.0"
}
}
48 changes: 48 additions & 0 deletions packages/hardhat/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
## PixelCanvas: Decentralized Collaborative Pixel Art Smart Contract

### Contract Overview
A collaborative on-chain pixel art platform implemented as an Ethereum smart contract, enabling users to create and modify a shared 64x64 pixel canvas.

### 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
getCanvasSnapshot(): Returns entire canvas state

3. Initialization
Test the PixelCanvas contract
Includes default Buidlguidl Batch11 logo on contract deployment
Demonstrates initial canvas state

Known Limitations

Fixed 64x64 canvas size
Limited 8-color palette
No pixel modification after placement



122 changes: 122 additions & 0 deletions packages/hardhat/contracts/PixelCanvas.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/access/Ownable.sol";
phipsae marked this conversation as resolved.
Show resolved Hide resolved

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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a mapping here instead. That is more gas-efficient and flexible, storing the whole array from the beginning a is not the ideal solution. The key advantages of a mapping compared to a fixed-size array are:

  • Lower Deployment Cost: There's no need to initialize the entire array, reducing upfront costs.
  • Higher Gas Efficiency: It only stores modified pixels, making it ideal for canvases where most pixels remain unchanged

Consider what additional functions we might not need if we switch to using a mapping.


// 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(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also use in the initialization msg.sender as the author.

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;
}
}
52 changes: 52 additions & 0 deletions packages/hardhat/deploy/PixelCanvas.ts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why make things more complicated than necessary :)
There is already a deploy script named 00_deploy_your_contract.ts file which you can use.
Take the following code, copy it and adapt it for your contract:

await deploy("BatchRegistry", { from: deployer, // Contract constructor arguments args: [deployer, BATCH_NUMBER], log: true, // autoMine: speeds up deployment on local networks by auto-mining the deployment transaction. No effect on live networks. autoMine: true, });

Currently, the contracts are not being copied to the deployedContract.ts file in the Next.js project as they should. (Deploy scripts need to run before 99_generateTsAbis.ts.)

Let me know if you have any questions!

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { ethers, network, run } from "hardhat";
import { ContractFactory } from "ethers";

async function main() {
// Get the account that will deploy the contract
const [deployer] = await ethers.getSigners();

console.log("Deploying PixelCanvas contract with the account:", deployer.address);

// Check deployer's balance
const balance = await ethers.provider.getBalance(deployer.address);
console.log("Account balance:", ethers.formatEther(balance), "ETH");

// Get the contract factory for PixelCanvas
const PixelCanvasFactory: ContractFactory = await ethers.getContractFactory("PixelCanvas");

// Deploy the contract
const pixelCanvas = await PixelCanvasFactory.deploy();

// Wait for the contract to be deployed
await pixelCanvas.waitForDeployment();

// Get the deployed contract address
const contractAddress = await pixelCanvas.getAddress();

console.log("PixelCanvas deployed to:", contractAddress);

// Verify the contract on Etherscan only on supported networks
if (network.name !== "hardhat" && network.name !== "localhost") {
console.log("Waiting for block confirmations...");
await new Promise(resolve => setTimeout(resolve, 45000)); // 45 seconds

try {
await run("verify:verify", {
address: contractAddress,
constructorArguments: [],
});
console.log("Contract verified on Etherscan");
} catch (error) {
console.error("Verification failed:", error);
}
}
}

main()
.then(() => process.exit(0))
.catch(error => {
console.error("Deployment error:", error);
process.exit(1);
});

export default main;
3 changes: 2 additions & 1 deletion packages/hardhat/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import "@typechain/hardhat";
import "hardhat-gas-reporter";
import "solidity-coverage";
import "@nomicfoundation/hardhat-verify";
import "@nomicfoundation/hardhat-network-helpers";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would we need this import here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

import "hardhat-deploy";
import "hardhat-deploy-ethers";

Expand Down Expand Up @@ -54,7 +55,7 @@ const config: HardhatUserConfig = {
accounts: [deployerPrivateKey],
},
sepolia: {
url: `https://rpc2.sepolia.org`,
url: `https://eth-sepolia.g.alchemy.com/v2/FIQ1qwifmra7ZqdkVHnZ2lHQAKG8j4Yd`,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a general rule, never include an API key in the code of a GitHub repository. Instead, use the .env file. Alternatively, you can use public RPCs, such as those available here: https://chainlist.org/chain/11155111.

accounts: [deployerPrivateKey],
},
arbitrum: {
Expand Down
83 changes: 83 additions & 0 deletions packages/hardhat/test/PixelCanvas.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Loading
Loading