Skip to content

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.

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.

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
src/sketch.ts
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;
}

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 a timestamp value, which we’ll use for querying and sorting later.
  • ExpiresIn: Sketches are set to expire after 365 days.
src/sketch.ts
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;
}

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.

src/sketch.ts
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 a type attribute 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.