Skip to content

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!

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

First, import our modules from the previous steps and grab references to all the HTML elements we’ll be interacting with.

src/main.ts
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;

When the user clicks “Connect MetaMask”, we call connectWallet() from Step 1, display the abbreviated address, and load their existing sketches.

src/main.ts
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";
}
});

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.

src/main.ts
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("");
}

Set up a p5.js canvas for mouse-based drawing. The canvas fills its container and draws black lines when the mouse is pressed.

src/main.ts
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);

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.

src/main.ts
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";
}
});

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
src/style.css
* { 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; } }
  1. Start the dev server:

    Terminal window
    npm run dev

    Vite will start a local server, usually at http://localhost:5173.

  2. 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.

  3. 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.

  4. 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.

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-side privateKeyToAccount().
  • You used the query builder to filter entities by owner and attributes, displaying them in a gallery.

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

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 mutateEntities with the deletes option.
  • Add date range filtering to browse sketches by time period using Arkiv’s query builder.