All files / src/metadata tree.ts

75% Statements 24/32
57.89% Branches 11/19
100% Functions 5/5
82.75% Lines 24/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 1292x                       2x                                                           16x 16x 16x 16x   16x           16x 116x         116x     16x                         10x     77x                                         10x   10x     10x   10x     10x 77x 77x       10x   77x           10x   10x      
import { MerkleTree, Field } from "o1js";
 
/**
 * Represents a metadata tree using a Merkle tree structure.
 *
 * The `MetadataTree` class is used to manage a set of key-value pairs representing metadata,
 * storing them in a Merkle tree for efficient verification and integrity checks.
 *
 * This class is a utility within the NFT standard on Mina Protocol, enabling secure and verifiable
 * storage of metadata. By leveraging the Merkle tree, clients can prove the inclusion of specific
 * metadata entries without exposing the entire dataset.
 */
export class MetadataTree {
  /**
   * The list of key-value pairs stored in the tree.
   * Each key is a `bigint` representing the index in the Merkle tree,
   * and each value is a `Field` element from o1js.
   */
  readonly values: { key: bigint; value: Field }[];
 
  /**
   * The height of the Merkle tree.
   * Determines the maximum number of elements the tree can hold.
   */
  readonly height: number;
 
  /**
   * The root of the Merkle tree.
   * Used for verifying the integrity of the tree and its contents.
   */
  readonly root: Field;
 
  /**
   * Constructs a new `MetadataTree` with the specified height and key-value pairs.
   *
   * @param height - The height of the Merkle tree (must be between 1 and 254).
   * @param values - An array of key-value pairs to store in the tree.
   *
   * @throws Will throw an error if the number of values exceeds the maximum capacity of the tree.
   * @throws Will throw an error if any key is out of bounds for the tree height.
   */
  constructor(height: number, values: { key: bigint; value: Field }[]) {
    this.values = values;
    this.height = height;
    const tree = new MerkleTree(height);
    const maxElements = tree.leafCount;
 
    Iif (values.length > tree.leafCount) {
      throw new Error(
        `Tree height ${height} can only have ${maxElements} elements`
      );
    }
 
    for (const { key, value } of values) {
      Iif (key >= maxElements) {
        throw new Error(
          `Key ${key} is out of bounds for tree height ${height}`
        );
      }
      tree.setLeaf(key, value);
    }
 
    this.root = tree.getRoot();
  }
 
  /**
   * Serializes the `MetadataTree` to a JSON object.
   *
   * @returns An object containing the tree's height, root, and values.
   */
  public toJSON(): {
    height: number;
    root: string;
    values: { key: string; value: string }[];
  } {
    return {
      height: this.height,
      root: this.root.toJSON(),
      values: this.values.map(({ key, value }) => ({
        key: key.toString(),
        value: value.toJSON(),
      })),
    };
  }
 
  /**
   * Deserializes a JSON object into a `MetadataTree` instance.
   *
   * @param json - The JSON object containing the tree data.
   *
   * @returns A new `MetadataTree` instance constructed from the JSON data.
   *
   * @throws Will throw an error if the JSON data is invalid or inconsistent.
   */
  static fromJSON(json: {
    height: number;
    root: string;
    values: { key: string; value: string }[];
  }): MetadataTree {
    const { height, values, root } = json;
 
    Iif (typeof height !== "number" || height < 1 || height > 254)
      throw new Error(`Invalid tree height`);
 
    Iif (!root || typeof root !== "string") throw new Error(`Invalid tree root`);
 
    Iif (!values || !Array.isArray(values))
      throw new Error(`Tree values are required`);
 
    for (const { key, value } of values) {
      Iif (!key || typeof key !== "string") throw new Error(`Invalid tree key`);
      Iif (!value || typeof value !== "string")
        throw new Error(`Invalid tree value`);
    }
 
    const tree = new MetadataTree(
      height,
      values.map(({ key, value }) => ({
        key: BigInt(key),
        value: Field.fromJSON(value),
      }))
    );
 
    Iif (tree.root.toJSON() !== root) throw new Error("Invalid tree json");
 
    return tree;
  }
}