@silvana-one/mina-utils
Version:
Silvana Mina Utils
171 lines (160 loc) • 5.14 kB
text/typescript
import { Experimental, Field } from "o1js";
import { bigintToBase64, bigintFromBase64 } from "./base64.js";
import { sleep } from "./utils.js";
import { pinJSON } from "../storage/pinata.js";
const { IndexedMerkleMap } = Experimental;
type IndexedMerkleMap = Experimental.IndexedMerkleMap;
export interface IndexedMapSerialized {
height: number;
root: string;
length: string;
nodes: string;
sortedLeaves: string;
}
export async function loadIndexedMerkleMap(params: {
url: string;
type: ReturnType<typeof IndexedMerkleMap>;
timeout?: number;
attempts?: number;
}) {
const { url, type, timeout = 60000, attempts = 5 } = params;
let attempt = 0;
const start = Date.now();
let response = await fetch(url);
while (!response.ok && attempt < attempts && Date.now() - start < timeout) {
attempt++;
await sleep(5000 * attempt); // handle rate limiting
response = await fetch(url);
}
if (!response.ok) {
throw new Error("Failed to fetch IndexedMerkleMap");
}
const json = await response.json();
const serializedIndexedMap = (
json as unknown as { map: IndexedMapSerialized }
).map;
if (!serializedIndexedMap)
throw new Error("wrong IndexedMerkleMap json format");
const map = deserializeIndexedMerkleMapInternal({
serializedIndexedMap,
type,
});
if (!map) {
throw new Error("Failed to deserialize whitelist");
}
return map;
}
export async function saveIndexedMerkleMap(params: {
map: IndexedMerkleMap;
name?: string;
keyvalues?: { key: string; value: string }[];
auth: string;
}): Promise<string | undefined> {
const { map, name = "indexed-map", keyvalues, auth } = params;
const serialized = serializeIndexedMap(map);
const ipfsHash = await pinJSON({
data: { map: serialized },
name,
keyvalues,
auth,
});
return ipfsHash;
}
export function serializeIndexedMap(
map: IndexedMerkleMap
): IndexedMapSerialized {
return {
height: map.height,
root: map.root.toJSON(),
length: map.length.toJSON(),
nodes: JSON.stringify(map.data.get().nodes, (_, v) =>
typeof v === "bigint" ? "n" + bigintToBase64(v) : v
),
sortedLeaves: JSON.stringify(
map.data
.get()
.sortedLeaves.map((v) => [
bigintToBase64(v.key),
bigintToBase64(v.nextKey),
bigintToBase64(v.value),
bigintToBase64(BigInt(v.index)),
])
),
};
}
export function deserializeIndexedMerkleMap(params: {
serializedIndexedMap: IndexedMapSerialized;
type?: ReturnType<typeof IndexedMerkleMap>;
}): InstanceType<ReturnType<typeof IndexedMerkleMap>> | undefined {
try {
const { serializedIndexedMap, type } = params;
return deserializeIndexedMerkleMapInternal({
serializedIndexedMap,
type: type ?? IndexedMerkleMap(serializedIndexedMap.height),
});
} catch (error: any) {
console.error("Error deserializing map:", error?.message ?? error);
return undefined;
}
}
export function parseIndexedMapSerialized(
serializedMap: string
): IndexedMapSerialized {
const json = JSON.parse(serializedMap);
if (
json.height === undefined ||
json.root === undefined ||
json.length === undefined ||
json.nodes === undefined ||
json.sortedLeaves === undefined
)
throw new Error("wrong IndexedMerkleMap json format");
if (typeof json.height !== "number")
throw new Error("wrong IndexedMerkleMap height format");
if (typeof json.root !== "string")
throw new Error("wrong IndexedMerkleMap root format");
if (typeof json.length !== "string")
throw new Error("wrong IndexedMerkleMap length format");
if (typeof json.nodes !== "string")
throw new Error("wrong IndexedMerkleMap nodes format");
if (typeof json.sortedLeaves !== "string")
throw new Error("wrong IndexedMerkleMap sortedLeaves format");
return json;
}
function deserializeIndexedMerkleMapInternal(params: {
serializedIndexedMap: IndexedMapSerialized;
type: ReturnType<typeof IndexedMerkleMap>;
}): InstanceType<ReturnType<typeof IndexedMerkleMap>> {
const { serializedIndexedMap, type } = params;
const map = new type();
if (serializedIndexedMap.height !== map.height) {
throw new Error("wrong IndexedMap height");
}
const nodes = JSON.parse(serializedIndexedMap.nodes, (_, v) => {
// Check if the value is a string that represents a BigInt
if (typeof v === "string" && v[0] === "n") {
// Remove the first 'n' and convert the string to a BigInt
return bigintFromBase64(v.slice(1));
}
return v;
});
const sortedLeaves = JSON.parse(serializedIndexedMap.sortedLeaves).map(
(row: any) => {
return {
key: bigintFromBase64(row[0]),
nextKey: bigintFromBase64(row[1]),
value: bigintFromBase64(row[2]),
index: Number(bigintFromBase64(row[3])),
};
}
);
map.root = Field.fromJSON(serializedIndexedMap.root);
map.length = Field.fromJSON(serializedIndexedMap.length);
map.data.updateAsProver(() => {
return {
nodes: nodes.map((row: any) => [...row]),
sortedLeaves: [...sortedLeaves],
};
});
return map;
}