Prerequisites & Project Setup
Before we start writing code, let’s ensure you have the necessary tools and set up our project structure.
1. Prerequisites
Section titled “1. Prerequisites”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.
2. Project Setup
Section titled “2. Project Setup”We’ll use Vite with a vanilla TypeScript template to scaffold the project quickly.
Open your terminal and run the following commands:
-
Create and enter the project:
Terminal window npm create vite@latest arkiv-sketch-app -- --template vanilla-tscd arkiv-sketch-app -
Install dependencies: We need the Arkiv SDK for blockchain interaction and
p5for the drawing canvas.Terminal window npm install @arkiv-network/sdk p5 @types/p5 -
Clean up default files and create our project files:
Terminal window rm -rf public src/counter.ts src/typescript.svg src/vite-env.d.tstouch src/wallet.ts src/sketch.ts
Open PowerShell and run the following commands:
-
Create and enter the project:
Terminal window npm create vite@latest arkiv-sketch-app -- --template vanilla-tscd arkiv-sketch-app -
Install dependencies: We need the Arkiv SDK for blockchain interaction and
p5for the drawing canvas.Terminal window npm install @arkiv-network/sdk p5 @types/p5 -
Clean up default files and create our project files:
Terminal window Remove-Item -Recurse public, src\counter.ts, src\typescript.svg, src\vite-env.d.tsNew-Item 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/
- …
3. Configure Vite for WASM
Section titled “3. Configure Vite for WASM”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
import { defineConfig } from "vite";
export default defineConfig({optimizeDeps: { exclude: ["brotli-wasm", "brotli-wasm/pkg.bundler/brotli_wasm_bg.wasm"],},});4. HTML Structure
Section titled “4. HTML Structure”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
<!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>5. MetaMask Wallet Integration
Section titled “5. MetaMask Wallet Integration”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
Step 1: Imports and Chain Switching
Section titled “Step 1: Imports and Chain Switching”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.
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.
Step 2: Connect Wallet
Section titled “Step 2: Connect Wallet”Next, add the connectWallet function. This switches to the correct chain and then requests account access from MetaMask.
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];}Step 3: Create Arkiv Clients
Section titled “Step 3: Create Arkiv Clients”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.
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.