Reading & Writing Data
This guide targets the Tiramisu testnet. Check Networks for current testnets and faucets.
In this part, you build the data module. This module saves sketches to Arkiv and loads the gallery of the user. It uses two core SDK operations: creating entities, and querying entities by owner.
Open src/sketch.ts and build it step by step.
Get Test GLM
Section titled “Get Test GLM”Before you create entities on Arkiv, your MetaMask wallet needs test GLM to pay for gas fees. Tiramisu uses GLM as its native gas token.
Get test GLM from the Arkiv faucet. If the faucet does not work for you, ask in the Arkiv Discord.
Step 1: Imports and Types
Section titled “Step 1: Imports and Types”First, import the query helpers, the wallet module from step 1, and the SDK utilities. Then define a Sketch interface to type your data.
Directoryarkiv-sketch-app/
Directorysrc/
- wallet.ts
- sketch.ts
- main.ts
- style.css
- index.html
import { eq } from "@arkiv-network/sdk/query";import { u64 } from "@arkiv-network/sdk/attr";import { createArkivClients } from "./wallet";import { jsonToPayload, ExpirationTime } from "@arkiv-network/sdk";
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”, the app captures the canvas as a PNG data URL and stores it as an Arkiv entity. This function creates a wallet client with custom(window.ethereum). MetaMask shows a confirmation dialog so the user can sign the transaction.
- Payload: the sketch image data and a timestamp, stored as JSON.
- Attributes:
type: "sketch"and atimestampvalue. You use these later for querying and sorting. - expires: sketches expire after 365 days.
import { eq } from "@arkiv-network/sdk/query";import { u64 } from "@arkiv-network/sdk/attr";import { createArkivClients } from "./wallet";import { jsonToPayload, ExpirationTime } from "@arkiv-network/sdk";
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: { type: "sketch", // Date.now() is milliseconds since epoch, too large for i32. Use u64 instead. timestamp: u64(Date.now()), }, expires: 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 with the newest sketch first. This function uses a public client, so it needs no wallet signature.
import { eq } from "@arkiv-network/sdk/query";import { u64 } from "@arkiv-network/sdk/attr";import { createArkivClients } from "./wallet";import { jsonToPayload, ExpirationTime } from "@arkiv-network/sdk";
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: { type: "sketch", timestamp: u64(Date.now()), }, expires: ExpirationTime.fromDays(365),});
return entityKey;}
export async function loadSketches(userAddress: string): Promise<Sketch[]> {try { const { publicClient } = createArkivClients();
const result = await publicClient .select({ key: true, payload: true }) .where(eq("type", "sketch")) .ownedBy(userAddress as `0x${string}`) .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 [];}}This is what the query does:
.select({ key: true, payload: true }): names the fields to fetch, the key and the full payload data of the image..where(eq("type", "sketch")): keeps only entities where thetypeattribute equals"sketch"..ownedBy(userAddress): keeps only entities that this wallet created..limit(9): caps the gallery at 9 items.
Each entity is then parsed from JSON. The .filter() call drops any entity without a valid imageData field. Arkiv does not sort results on the server. As a result, the code sorts the gallery by timestamp itself, with the newest sketch first.
The data layer is complete. In the next section, you connect the drawing canvas and the user interface.