Running the App & Next Steps
This guide targets the Tiramisu testnet. Check Networks for current testnets and faucets.
In this final part, you build the main application file. This file connects the wallet, the p5.js drawing canvas, the sketch-saving code, and the gallery. Then you add styles and run the app.
1. Main Application Logic
Section titled “1. Main Application Logic”Open src/main.ts and 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 the modules from the previous steps. Then get a reference to each HTML element that the app uses.
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”, the app calls connectWallet() from step 1. It then shows the short address and loads the sketches of the user.
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 show them as image thumbnails.
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> <span class="entity-key">${sketch.id.slice(0, 12)}...</span> </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. It draws a black line while the user holds the mouse button down.
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> <span class="entity-key">${sketch.id.slice(0, 12)}...</span> </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. The reset handler clears the canvas to white. The save handler captures the canvas as an image, stores it on Arkiv through 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> <span class="entity-key">${sketch.id.slice(0, 12)}...</span> </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 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-key { display: inline-block; margin-top: 4px; color: #718096; font-size: 0.85rem; font-family: monospace; }@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 starts a local server, usually at
http://localhost:5173. -
Connect MetaMask. Click “Connect MetaMask” in the app. MetaMask prompts you to switch to Tiramisu, or to add it if needed, then connects your account.
-
Draw and save. Draw on the canvas with your mouse, then click “Save”. MetaMask asks you to approve the transaction that stores your sketch on Arkiv.
-
View your gallery. After the network includes the transaction, your sketch appears in the “Recent Sketches” panel on the left.
Summary
Section titled “Summary”You built a browser-based application with Web3 guarantees.
- You created a MetaMask wallet integration that connects users to Arkiv without exposing a private key.
- You built a p5.js drawing canvas and stored user-generated artwork as entities on Arkiv.
- You used
custom(window.ethereum)as a transport for browser wallet signing. Server-side code usesprivateKeyToAccount()instead. - You used the query builder to filter entities by owner and attributes, and to show them in a gallery.
Full Source Code
Section titled “Full Source Code”The complete source code for this tutorial is 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 good starting point. Here are some ideas to extend it:
- Add a color picker and a brush-size control to make the drawing tools more expressive.
- Add pagination to load more sketches beyond the 9-item limit.
- Add a way to delete your own entities with
executeBatchand itsdeletesoption. - Add a date-range filter to browse sketches by time period with the query builder of Arkiv.