Skip to content

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.

Open src/main.ts and build it step by step.

  • Directoryarkiv-sketch-app/
    • Directorysrc/
      • wallet.ts
      • sketch.ts
      • main.ts
      • style.css
    • index.html

First, import the modules from the previous steps. Then get a reference to each HTML element that the app uses.

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”, the app calls connectWallet() from step 1. It then shows the short address and loads the sketches of the user.

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 show them as image thumbnails.

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>
<span class="entity-key">${sketch.id.slice(0, 12)}...</span>
</div>`;
})
.join("");
}

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.

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>
<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);

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.

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>
<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";
}
});

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
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-key { display: inline-block; margin-top: 4px; color: #718096; font-size: 0.85rem; font-family: monospace; }
@media (max-width: 768px) { .container { flex-direction: column; } }
  1. Start the dev server:

    Terminal window
    npm run dev

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

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

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

  4. View your gallery. After the network includes the transaction, your sketch appears in the “Recent Sketches” panel on the left.

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

The complete source code for this tutorial is on GitHub: https://github.com/Arkiv-Network/learn-arkiv/tree/main/tutorial-source-code/metamask-tutorial

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 executeBatch and its deletes option.
  • Add a date-range filter to browse sketches by time period with the query builder of Arkiv.