Skip to content

Prerequisites & Project Setup

Before we start writing code, let’s ensure you have the necessary tools and set up our project structure.

Make sure you have the following installed on your system:

  • Node.js: A recent version, specifically v20.6.0 or higher.
  • A Text Editor: A code editor like VS Code is highly recommended.
  • MetaMask: The MetaMask browser extension installed in your browser. This is how users will sign transactions.
  • Test ETH: You’ll need some testnet ETH from the Kaolin Faucet. The app will prompt MetaMask to switch to the Kaolin testnet automatically.

We’ll use Vite with a vanilla TypeScript template to scaffold the project quickly.

Open your terminal and run the following commands:

  1. Create and enter the project:

    Terminal window
    npm create vite@latest arkiv-sketch-app -- --template vanilla-ts
    cd arkiv-sketch-app
  2. Install dependencies: We need the Arkiv SDK for blockchain interaction and p5 for the drawing canvas.

    Terminal window
    npm install @arkiv-network/sdk p5 @types/p5
  3. Clean up default files and create our project files:

    Terminal window
    rm -rf public src/counter.ts src/typescript.svg src/vite-env.d.ts
    touch src/wallet.ts src/sketch.ts

Your project structure should now look like this:

  • Directoryarkiv-sketch-app/
    • Directorysrc/
      • main.ts
      • style.css
      • wallet.ts
      • sketch.ts
    • index.html
    • vite.config.ts
    • package.json
    • Directorynode_modules/

The Arkiv SDK uses WASM internally, so we need to tell Vite to exclude it from dependency optimization. Create (or replace) vite.config.ts:

  • Directoryarkiv-sketch-app/
    • Directorysrc/
    • index.html
    • vite.config.ts
    • package.json
vite.config.ts
import { defineConfig } from "vite";
export default defineConfig({
optimizeDeps: {
exclude: ["brotli-wasm", "brotli-wasm/pkg.bundler/brotli_wasm_bg.wasm"],
},
});

Replace the contents of index.html with our app’s skeleton. We need a header with a connect button, a left panel for the gallery, and a right panel for the drawing canvas.

  • Directoryarkiv-sketch-app/
    • Directorysrc/
    • index.html
    • vite.config.ts
    • package.json
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Arkiv Sketch App</title>
</head>
<body>
<div id="app">
<header>
<h1>Arkiv Sketch App</h1>
<button id="connect-btn">Connect MetaMask</button>
<div id="account"></div>
</header>
<div class="container">
<div class="left-panel">
<h2>Recent Sketches</h2>
<div id="sketch-list">
<p>Connect your wallet to see sketches</p>
</div>
</div>
<div class="right-panel">
<h2>Draw Something</h2>
<div id="canvas-container"></div>
<div class="controls">
<button id="reset-btn">Reset</button>
<button id="save-btn">Save</button>
</div>
</div>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

Now for the most important part of this tutorial: connecting to MetaMask. Open src/wallet.ts and let’s build it step by step.

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

First, we import the SDK and create a helper function to switch MetaMask to the Kaolin testnet. If the chain hasn’t been added to MetaMask yet, we add it automatically.

src/wallet.ts
import {
createPublicClient,
createWalletClient,
custom,
http,
} from "@arkiv-network/sdk";
import { kaolin } from "@arkiv-network/sdk/chains";
import "viem/window";
async function switchToKaolinChain() {
if (!window.ethereum) {
throw new Error("MetaMask not installed");
}
const chainIdHex = `0x${kaolin.id.toString(16)}`;
try {
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: chainIdHex }],
});
} catch (error: unknown) {
if (
error &&
typeof error === "object" &&
"code" in error &&
error.code === 4902
) {
// Chain not added yet — add it
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [
{
chainId: chainIdHex,
chainName: kaolin.name,
nativeCurrency: kaolin.nativeCurrency,
rpcUrls: kaolin.rpcUrls.default.http,
blockExplorerUrls: [kaolin.blockExplorers.default.url],
},
],
});
} else {
throw error;
}
}
}

The switchToKaolinChain function first tries wallet_switchEthereumChain. If MetaMask returns error code 4902 (chain not found), it falls back to wallet_addEthereumChain, using the chain details from the SDK’s kaolin object.

Next, add the connectWallet function. This switches to the correct chain and then requests account access from MetaMask.

src/wallet.ts
import {
createPublicClient,
createWalletClient,
custom,
http,
} from "@arkiv-network/sdk";
import { kaolin } from "@arkiv-network/sdk/chains";
import "viem/window";
async function switchToKaolinChain() {
if (!window.ethereum) {
throw new Error("MetaMask not installed");
}
const chainIdHex = `0x${kaolin.id.toString(16)}`;
try {
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: chainIdHex }],
});
} catch (error: unknown) {
if (
error &&
typeof error === "object" &&
"code" in error &&
error.code === 4902
) {
// Chain not added yet — add it
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [
{
chainId: chainIdHex,
chainName: kaolin.name,
nativeCurrency: kaolin.nativeCurrency,
rpcUrls: kaolin.rpcUrls.default.http,
blockExplorerUrls: [kaolin.blockExplorers.default.url],
},
],
});
} else {
throw error;
}
}
}
export async function connectWallet() {
if (!window.ethereum) {
throw new Error("MetaMask not installed");
}
await switchToKaolinChain();
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
return accounts[0];
}

Finally, add the factory function that creates both a public client (for reading data) and a wallet client (for writing data). This is exported so other modules can use it.

src/wallet.ts
import {
createPublicClient,
createWalletClient,
custom,
http,
} from "@arkiv-network/sdk";
import { kaolin } from "@arkiv-network/sdk/chains";
import "viem/window";
async function switchToKaolinChain() {
if (!window.ethereum) {
throw new Error("MetaMask not installed");
}
const chainIdHex = `0x${kaolin.id.toString(16)}`;
try {
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: chainIdHex }],
});
} catch (error: unknown) {
if (
error &&
typeof error === "object" &&
"code" in error &&
error.code === 4902
) {
// Chain not added yet — add it
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [
{
chainId: chainIdHex,
chainName: kaolin.name,
nativeCurrency: kaolin.nativeCurrency,
rpcUrls: kaolin.rpcUrls.default.http,
blockExplorerUrls: [kaolin.blockExplorers.default.url],
},
],
});
} else {
throw error;
}
}
}
export async function connectWallet() {
if (!window.ethereum) {
throw new Error("MetaMask not installed");
}
await switchToKaolinChain();
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
return accounts[0];
}
export function createArkivClients(account?: `0x${string}`) {
if (!window.ethereum) {
throw new Error("MetaMask not installed");
}
const publicClient = createPublicClient({
chain: kaolin,
transport: http(),
});
const walletClient = createWalletClient({
chain: kaolin,
transport: custom(window.ethereum),
account,
});
return { publicClient, walletClient };
}

Your wallet integration is complete! In the next section, we’ll build the data layer for saving and loading sketches.