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 GLM: You’ll need some test GLM from the Braga faucet. Braga uses GLM as its native gas token. The app will prompt MetaMask to switch to the Braga 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 Braga 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 { braga } from "@arkiv-network/sdk/chains";
import "viem/window";
async function switchToBragaChain() {
if (!window.ethereum) {
throw new Error("MetaMask not installed");
}
const chainIdHex = `0x${braga.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: braga.name,
nativeCurrency: braga.nativeCurrency,
rpcUrls: braga.rpcUrls.default.http,
blockExplorerUrls: [braga.blockExplorers.default.url],
},
],
});
} else {
throw error;
}
}
}

The switchToBragaChain 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 braga 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 { braga } from "@arkiv-network/sdk/chains";
import "viem/window";
async function switchToBragaChain() {
if (!window.ethereum) {
throw new Error("MetaMask not installed");
}
const chainIdHex = `0x${braga.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: braga.name,
nativeCurrency: braga.nativeCurrency,
rpcUrls: braga.rpcUrls.default.http,
blockExplorerUrls: [braga.blockExplorers.default.url],
},
],
});
} else {
throw error;
}
}
}
export async function connectWallet() {
if (!window.ethereum) {
throw new Error("MetaMask not installed");
}
await switchToBragaChain();
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 { braga } from "@arkiv-network/sdk/chains";
import "viem/window";
async function switchToBragaChain() {
if (!window.ethereum) {
throw new Error("MetaMask not installed");
}
const chainIdHex = `0x${braga.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: braga.name,
nativeCurrency: braga.nativeCurrency,
rpcUrls: braga.rpcUrls.default.http,
blockExplorerUrls: [braga.blockExplorers.default.url],
},
],
});
} else {
throw error;
}
}
}
export async function connectWallet() {
if (!window.ethereum) {
throw new Error("MetaMask not installed");
}
await switchToBragaChain();
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: braga,
transport: http(),
});
const walletClient = createWalletClient({
chain: braga,
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.