Running the App & Next Steps
In this final part, we’ll build the main application file that ties everything together — wallet connection, the p5.js drawing canvas, saving sketches, and rendering the gallery. Then we’ll add some styles and run it!
1. Main Application Logic
Section titled “1. Main Application Logic”Open src/main.ts and let’s build it step by step.
Directoryarkiv-sketch-app/
Directorysrc/
- wallet.ts
- sketch.ts
- main.ts
- style.css
- index.html
Step 1: Imports and DOM References
Section titled “Step 1: Imports and DOM References”First, import our modules from the previous steps and grab references to all the HTML elements we’ll be interacting with.
import "./style.css";import p5 from "p5";import { connectWallet } from "./wallet";import { loadSketches, saveSketch, type Sketch } from "./sketch";
let userAddress: string | null = null;let sketches: Sketch[] = [];let p5Instance: p5 | null = null;
const connectBtn = document.getElementById("connect-btn") as HTMLButtonElement;const accountDiv = document.getElementById("account") as HTMLDivElement;const sketchList = document.getElementById("sketch-list") as HTMLDivElement;const saveBtn = document.getElementById("save-btn") as HTMLButtonElement;const resetBtn = document.getElementById("reset-btn") as HTMLButtonElement;const canvasContainer = document.getElementById("canvas-container") as HTMLDivElement;Step 2: Wallet Connection Handler
Section titled “Step 2: Wallet Connection Handler”When the user clicks “Connect MetaMask”, we call connectWallet() from Step 1, display the abbreviated address, and load their existing sketches.
import "./style.css";import p5 from "p5";import { connectWallet } from "./wallet";import { loadSketches, saveSketch, type Sketch } from "./sketch";
let userAddress: string | null = null;let sketches: Sketch[] = [];let p5Instance: p5 | null = null;
const connectBtn = document.getElementById("connect-btn") as HTMLButtonElement;const accountDiv = document.getElementById("account") as HTMLDivElement;const sketchList = document.getElementById("sketch-list") as HTMLDivElement;const saveBtn = document.getElementById("save-btn") as HTMLButtonElement;const resetBtn = document.getElementById("reset-btn") as HTMLButtonElement;const canvasContainer = document.getElementById("canvas-container") as HTMLDivElement;
connectBtn.addEventListener("click", async () => {try { connectBtn.disabled = true; connectBtn.textContent = "Connecting..."; userAddress = await connectWallet(); accountDiv.textContent = `Connected: ${userAddress.slice(0, 6)}...${userAddress.slice(-4)}`; connectBtn.style.display = "none"; await refreshSketches();} catch (error) { console.error("Failed to connect:", error); alert(`Failed to connect wallet: ${(error as Error).message}`); connectBtn.disabled = false; connectBtn.textContent = "Connect MetaMask";}});Step 3: Gallery Rendering
Section titled “Step 3: Gallery Rendering”Add the functions that load sketches from Arkiv and render them as image thumbnails with explorer links. Each sketch in the gallery includes a link to verify it on the Arkiv Kaolin Explorer.
import "./style.css";import p5 from "p5";import { connectWallet } from "./wallet";import { loadSketches, saveSketch, type Sketch } from "./sketch";
let userAddress: string | null = null;let sketches: Sketch[] = [];let p5Instance: p5 | null = null;
const connectBtn = document.getElementById("connect-btn") as HTMLButtonElement;const accountDiv = document.getElementById("account") as HTMLDivElement;const sketchList = document.getElementById("sketch-list") as HTMLDivElement;const saveBtn = document.getElementById("save-btn") as HTMLButtonElement;const resetBtn = document.getElementById("reset-btn") as HTMLButtonElement;const canvasContainer = document.getElementById("canvas-container") as HTMLDivElement;
connectBtn.addEventListener("click", async () => {try { connectBtn.disabled = true; connectBtn.textContent = "Connecting..."; userAddress = await connectWallet(); accountDiv.textContent = `Connected: ${userAddress.slice(0, 6)}...${userAddress.slice(-4)}`; connectBtn.style.display = "none"; await refreshSketches();} catch (error) { console.error("Failed to connect:", error); alert(`Failed to connect wallet: ${(error as Error).message}`); connectBtn.disabled = false; connectBtn.textContent = "Connect MetaMask";}});
async function refreshSketches() {if (!userAddress) return;try { sketchList.innerHTML = "<p>Loading sketches...</p>"; sketches = await loadSketches(userAddress); renderSketchList();} catch (error) { console.error("Failed to load sketches:", error); sketchList.innerHTML = "<p>Failed to load sketches</p>";}}
function renderSketchList() {if (sketches.length === 0) { sketchList.innerHTML = "<p>No sketches yet. Draw something!</p>"; return;}
sketchList.innerHTML = sketches .map((sketch) => { const date = new Date(sketch.timestamp).toLocaleString(); return ` <div class="sketch-item"> <img src="${sketch.imageData}" alt="Sketch" /> <div class="sketch-info"><small>${date}</small></div> <a href="https://explorer.kaolin.hoodi.arkiv.network/entity/${sketch.id}" target="_blank" class="entity-link"> ${sketch.id.slice(0, 12)}... </a> </div>`; }) .join("");}Step 4: Drawing Canvas
Section titled “Step 4: Drawing Canvas”Set up a p5.js canvas for mouse-based drawing. The canvas fills its container and draws black lines when the mouse is pressed.
import "./style.css";import p5 from "p5";import { connectWallet } from "./wallet";import { loadSketches, saveSketch, type Sketch } from "./sketch";
let userAddress: string | null = null;let sketches: Sketch[] = [];let p5Instance: p5 | null = null;
const connectBtn = document.getElementById("connect-btn") as HTMLButtonElement;const accountDiv = document.getElementById("account") as HTMLDivElement;const sketchList = document.getElementById("sketch-list") as HTMLDivElement;const saveBtn = document.getElementById("save-btn") as HTMLButtonElement;const resetBtn = document.getElementById("reset-btn") as HTMLButtonElement;const canvasContainer = document.getElementById("canvas-container") as HTMLDivElement;
connectBtn.addEventListener("click", async () => {try { connectBtn.disabled = true; connectBtn.textContent = "Connecting..."; userAddress = await connectWallet(); accountDiv.textContent = `Connected: ${userAddress.slice(0, 6)}...${userAddress.slice(-4)}`; connectBtn.style.display = "none"; await refreshSketches();} catch (error) { console.error("Failed to connect:", error); alert(`Failed to connect wallet: ${(error as Error).message}`); connectBtn.disabled = false; connectBtn.textContent = "Connect MetaMask";}});
async function refreshSketches() {if (!userAddress) return;try { sketchList.innerHTML = "<p>Loading sketches...</p>"; sketches = await loadSketches(userAddress); renderSketchList();} catch (error) { console.error("Failed to load sketches:", error); sketchList.innerHTML = "<p>Failed to load sketches</p>";}}
function renderSketchList() {if (sketches.length === 0) { sketchList.innerHTML = "<p>No sketches yet. Draw something!</p>"; return;}
sketchList.innerHTML = sketches .map((sketch) => { const date = new Date(sketch.timestamp).toLocaleString(); return ` <div class="sketch-item"> <img src="${sketch.imageData}" alt="Sketch" /> <div class="sketch-info"><small>${date}</small></div> <a href="https://explorer.kaolin.hoodi.arkiv.network/entity/${sketch.id}" target="_blank" class="entity-link"> ${sketch.id.slice(0, 12)}... </a> </div>`; }) .join("");}
const sketchFn = (p: p5) => {p.setup = () => { const containerWidth = canvasContainer.offsetWidth; p.createCanvas(containerWidth, containerWidth); p.background(255);};p.draw = () => { if (p.mouseIsPressed) { p.stroke(0); p.strokeWeight(2); p.line(p.mouseX, p.mouseY, p.pmouseX, p.pmouseY); }};};
p5Instance = new p5(sketchFn, canvasContainer);Step 5: Save and Reset Handlers
Section titled “Step 5: Save and Reset Handlers”Finally, add the event handlers for the “Reset” and “Save” buttons. Resetting clears the canvas to white. Saving captures the canvas as an image, stores it on Arkiv via MetaMask, and refreshes the gallery.
import "./style.css";import p5 from "p5";import { connectWallet } from "./wallet";import { loadSketches, saveSketch, type Sketch } from "./sketch";
let userAddress: string | null = null;let sketches: Sketch[] = [];let p5Instance: p5 | null = null;
const connectBtn = document.getElementById("connect-btn") as HTMLButtonElement;const accountDiv = document.getElementById("account") as HTMLDivElement;const sketchList = document.getElementById("sketch-list") as HTMLDivElement;const saveBtn = document.getElementById("save-btn") as HTMLButtonElement;const resetBtn = document.getElementById("reset-btn") as HTMLButtonElement;const canvasContainer = document.getElementById("canvas-container") as HTMLDivElement;
connectBtn.addEventListener("click", async () => {try { connectBtn.disabled = true; connectBtn.textContent = "Connecting..."; userAddress = await connectWallet(); accountDiv.textContent = `Connected: ${userAddress.slice(0, 6)}...${userAddress.slice(-4)}`; connectBtn.style.display = "none"; await refreshSketches();} catch (error) { console.error("Failed to connect:", error); alert(`Failed to connect wallet: ${(error as Error).message}`); connectBtn.disabled = false; connectBtn.textContent = "Connect MetaMask";}});
async function refreshSketches() {if (!userAddress) return;try { sketchList.innerHTML = "<p>Loading sketches...</p>"; sketches = await loadSketches(userAddress); renderSketchList();} catch (error) { console.error("Failed to load sketches:", error); sketchList.innerHTML = "<p>Failed to load sketches</p>";}}
function renderSketchList() {if (sketches.length === 0) { sketchList.innerHTML = "<p>No sketches yet. Draw something!</p>"; return;}
sketchList.innerHTML = sketches .map((sketch) => { const date = new Date(sketch.timestamp).toLocaleString(); return ` <div class="sketch-item"> <img src="${sketch.imageData}" alt="Sketch" /> <div class="sketch-info"><small>${date}</small></div> <a href="https://explorer.kaolin.hoodi.arkiv.network/entity/${sketch.id}" target="_blank" class="entity-link"> ${sketch.id.slice(0, 12)}... </a> </div>`; }) .join("");}
const sketchFn = (p: p5) => {p.setup = () => { const containerWidth = canvasContainer.offsetWidth; p.createCanvas(containerWidth, containerWidth); p.background(255);};p.draw = () => { if (p.mouseIsPressed) { p.stroke(0); p.strokeWeight(2); p.line(p.mouseX, p.mouseY, p.pmouseX, p.pmouseY); }};};
p5Instance = new p5(sketchFn, canvasContainer);
resetBtn.addEventListener("click", () => {if (p5Instance) p5Instance.background(255);});
saveBtn.addEventListener("click", async () => {if (!userAddress || !p5Instance) return;try { saveBtn.disabled = true; saveBtn.textContent = "Saving..."; const canvas = document.querySelector( "#canvas-container canvas" ) as HTMLCanvasElement; const imageData = canvas.toDataURL("image/png"); await saveSketch(imageData, userAddress); p5Instance.background(255); await refreshSketches(); saveBtn.disabled = false; saveBtn.textContent = "Save";} catch (error) { console.error("Failed to save sketch:", error); alert(`Failed to save sketch: ${(error as Error).message}`); saveBtn.disabled = false; saveBtn.textContent = "Save";}});2. CSS Styling
Section titled “2. CSS Styling”Add styles in src/style.css to make the app look clean and usable.
Directoryarkiv-sketch-app/
Directorysrc/
- wallet.ts
- sketch.ts
- main.ts
- style.css
- index.html
* { margin: 0; padding: 0; box-sizing: border-box; }body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f0f2f5; color: #333; padding: 20px; }header { text-align: center; margin-bottom: 30px; }h1 { color: #1a202c; margin-bottom: 15px; }h2 { font-size: 1.2rem; color: #4a5568; margin-bottom: 15px; }#connect-btn { padding: 10px 24px; font-size: 1rem; background: #4299e1; color: white; border: none; border-radius: 8px; cursor: pointer; transition: background 0.2s; }#connect-btn:hover { background: #3182ce; }#connect-btn:disabled { opacity: 0.6; cursor: not-allowed; }#account { margin-top: 8px; font-size: 0.9rem; color: #718096; }.container { display: flex; gap: 30px; max-width: 1200px; margin: auto; }.left-panel { flex: 1; min-width: 300px; }.right-panel { flex: 1; min-width: 300px; }#canvas-container canvas { border: 2px solid #e2e8f0; border-radius: 8px; cursor: crosshair; }.controls { margin-top: 15px; display: flex; gap: 10px; }.controls button { flex: 1; padding: 10px; font-size: 1rem; border: none; border-radius: 8px; cursor: pointer; transition: background 0.2s; }#reset-btn { background: #e2e8f0; color: #4a5568; }#reset-btn:hover { background: #cbd5e0; }#save-btn { background: #48bb78; color: white; }#save-btn:hover { background: #38a169; }#save-btn:disabled { opacity: 0.6; cursor: not-allowed; }.sketch-item { background: white; border-radius: 8px; padding: 12px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.1); }.sketch-item img { width: 100%; border-radius: 4px; }.sketch-info { margin-top: 6px; color: #718096; }.entity-link { display: inline-block; margin-top: 4px; color: #4299e1; font-size: 0.85rem; text-decoration: none; }.entity-link:hover { text-decoration: underline; }@media (max-width: 768px) { .container { flex-direction: column; } }3. Running the App
Section titled “3. Running the App”-
Start the dev server:
Terminal window npm run devVite will start a local server, usually at
http://localhost:5173. -
Connect MetaMask: Click “Connect MetaMask” in the app. MetaMask will prompt you to switch to the Kaolin testnet (or add it if it’s not configured yet), then connect your account.
-
Draw and save: Draw on the canvas with your mouse, then click “Save”. MetaMask will ask you to confirm the transaction to store your sketch on Arkiv.
-
View your gallery: After the transaction confirms, your sketch will appear in the “Recent Sketches” panel on the left. Each sketch links to the Arkiv Kaolin Explorer so you can verify it on-chain.
Summary
Section titled “Summary”Congratulations! You have successfully built a browser-based application with Web3 guarantees.
- You created a MetaMask wallet integration that connects users to Arkiv without requiring them to expose a private key.
- You built a p5.js drawing canvas and stored user-generated artwork as entities on the Arkiv blockchain.
- You learned how to use
custom(window.ethereum)as a transport for browser wallet signing, as opposed to server-sideprivateKeyToAccount(). - You used the query builder to filter entities by owner and attributes, displaying them in a gallery.
Full Source Code
Section titled “Full Source Code”You can find the complete source code for this tutorial on GitHub: https://github.com/Arkiv-Network/learn-arkiv/tree/main/tutorial-source-code/metamask-tutorial
Next Steps
Section titled “Next Steps”This project is a great starting point. Here are some ideas to expand on what you’ve learned:
- Add a color picker and brush size control to make the drawing tools more expressive.
- Implement pagination for loading more sketches beyond the 9-item limit.
- Add the ability to delete your own entities using
mutateEntitieswith thedeletesoption. - Add date range filtering to browse sketches by time period using Arkiv’s query builder.