Reading & Writing Data
In this part, we’ll build the data module that handles saving sketches to Arkiv and loading the user’s gallery. It demonstrates the two core SDK operations: creating entities and querying entities by owner.
Open src/sketch.ts and let’s build it step by step.
Get Testnet ETH
Section titled “Get Testnet ETH”Before we can create entities on Arkiv, your MetaMask wallet needs some testnet ETH to pay for gas fees. Visit the Kaolin testnet faucet:
👉 https://kaolin.hoodi.arkiv.network/faucet/
Enter your MetaMask wallet address and request testnet ETH. This is completely free and only takes a moment.
Step 1: Imports and Types
Section titled “Step 1: Imports and Types”First, we import the query helpers, our wallet module from Step 1, and the SDK utilities. We also define a Sketch interface to type our data.
Directoryarkiv-sketch-app/
Directorysrc/
- wallet.ts
- sketch.ts
- main.ts
- style.css
- index.html
import { desc, eq } from "@arkiv-network/sdk/query";import { createArkivClients } from "./wallet";import { jsonToPayload, ExpirationTime } from "@arkiv-network/sdk/utils";
export interface Sketch {id: string;timestamp: number;imageData: string;}Step 2: Saving a Sketch
Section titled “Step 2: Saving a Sketch”When the user clicks “Save”, we capture the canvas as a PNG data URL and store it as an Arkiv entity. This function creates a wallet client using custom(window.ethereum) — meaning MetaMask will pop up a confirmation dialog for the user to sign the transaction.
- Payload: The sketch image data and a timestamp, stored as JSON.
- Attributes:
type: "sketch"and atimestampvalue, which we’ll use for querying and sorting later. - ExpiresIn: Sketches are set to expire after 365 days.
import { desc, eq } from "@arkiv-network/sdk/query";import { createArkivClients } from "./wallet";import { jsonToPayload, ExpirationTime } from "@arkiv-network/sdk/utils";
export interface Sketch {id: string;timestamp: number;imageData: string;}
export async function saveSketch(imageData: string,userAddress: string,): Promise<string> {const { walletClient } = createArkivClients(userAddress as `0x${string}`);
const { entityKey } = await walletClient.createEntity({ payload: jsonToPayload({ imageData, timestamp: Date.now(), }), contentType: "application/json", attributes: [ { key: "type", value: "sketch" }, { key: "timestamp", value: Date.now() }, ], expiresIn: ExpirationTime.fromDays(365),});
return entityKey;}Step 3: Loading the Gallery
Section titled “Step 3: Loading the Gallery”Now add the query function that fetches all sketches owned by the connected wallet, sorted by newest first. This uses a public client — no wallet signature is needed for read-only queries.
import { desc, eq } from "@arkiv-network/sdk/query";import { createArkivClients } from "./wallet";import { jsonToPayload, ExpirationTime } from "@arkiv-network/sdk/utils";
export interface Sketch {id: string;timestamp: number;imageData: string;}
export async function saveSketch(imageData: string,userAddress: string,): Promise<string> {const { walletClient } = createArkivClients(userAddress as `0x${string}`);
const { entityKey } = await walletClient.createEntity({ payload: jsonToPayload({ imageData, timestamp: Date.now(), }), contentType: "application/json", attributes: [ { key: "type", value: "sketch" }, { key: "timestamp", value: Date.now() }, ], expiresIn: ExpirationTime.fromDays(365),});
return entityKey;}
export async function loadSketches(userAddress: string): Promise<Sketch[]> {try { const { publicClient } = createArkivClients();
const result = await publicClient .buildQuery() .where(eq("type", "sketch")) .ownedBy(userAddress as `0x${string}`) .orderBy(desc("timestamp", "number")) .withPayload(true) .limit(9) .fetch();
const sketches = result.entities .map((entity) => { try { const payload = entity.toJson(); if (payload?.imageData) { return { id: entity.key, timestamp: payload.timestamp || 0, imageData: payload.imageData, } as Sketch; } return null; } catch { return null; } }) .filter((s): s is Sketch => s !== null) .sort((a, b) => b.timestamp - a.timestamp);
return sketches;} catch (error) { console.error("Failed to load sketches:", error); return [];}}Let’s break down the query:
.where(eq("type", "sketch"))— filters entities to only those with atypeattribute equal to"sketch".ownedBy(userAddress)— restricts results to entities created by this wallet.orderBy(desc("timestamp", "number"))— sorts newest first.withPayload(true)— includes the full payload data (our image) in the response.limit(9)— caps the gallery at 9 items
Each entity is then parsed from JSON. We use .filter() to safely discard any entities that don’t have a valid imageData field.
Your data layer is complete! In the next section, we’ll wire everything together with the drawing canvas and UI.