diff --git a/clients/js/package.json b/clients/js/package.json index 202ccbe9..a33dbfc8 100644 --- a/clients/js/package.json +++ b/clients/js/package.json @@ -25,7 +25,8 @@ "author": "Metaplex Maintainers ", "license": "Apache-2.0", "dependencies": { - "@metaplex-foundation/mpl-toolbox": "^0.7.0" + "@metaplex-foundation/mpl-toolbox": "^0.7.0", + "@noble/hashes": "^1.3.1" }, "peerDependencies": { "@metaplex-foundation/umi": "^0.8.0" diff --git a/clients/js/pnpm-lock.yaml b/clients/js/pnpm-lock.yaml index 9446c43c..28f88fa8 100644 --- a/clients/js/pnpm-lock.yaml +++ b/clients/js/pnpm-lock.yaml @@ -4,6 +4,9 @@ dependencies: '@metaplex-foundation/mpl-toolbox': specifier: ^0.7.0 version: 0.7.0(@metaplex-foundation/umi@0.8.0) + '@noble/hashes': + specifier: ^1.3.1 + version: 1.3.1 devDependencies: '@ava/typescript': @@ -1739,14 +1742,9 @@ packages: resolution: {integrity: sha512-Rk4SkJFaXZiznFyC/t77Q0NKS4FL7TLJJsVG2V2oiEq3kJVeTdxysEe/yRWSpnWMe808XRDJ+VFh5pt/FN5plw==} dev: true - /@noble/hashes@1.1.5: - resolution: {integrity: sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==} - dev: true - /@noble/hashes@1.3.1: resolution: {integrity: sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==} engines: {node: '>= 16'} - dev: true /@noble/secp256k1@1.7.0: resolution: {integrity: sha512-kbacwGSsH/CTout0ZnZWxnW1B+jH/7r/WAAKLBtrRJ/+CUH7lgmQzl3GTrQua3SGKWNSDsS6lmjnDpIJ5Dxyaw==} @@ -1908,7 +1906,7 @@ packages: dependencies: '@babel/runtime': 7.20.7 '@noble/ed25519': 1.7.1 - '@noble/hashes': 1.1.5 + '@noble/hashes': 1.3.1 '@noble/secp256k1': 1.7.0 '@solana/buffer-layout': 4.0.1 agentkeepalive: 4.2.1 diff --git a/clients/js/src/createTree.ts b/clients/js/src/createTree.ts index 7ff890a6..3af8b226 100644 --- a/clients/js/src/createTree.ts +++ b/clients/js/src/createTree.ts @@ -10,6 +10,7 @@ import { SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, createTreeConfig, } from './generated'; +import { getMerkleTreeSize } from './hooked'; export const createTree = async ( context: Parameters[0] & @@ -23,7 +24,8 @@ export const createTree = async ( ): Promise => { const space = input.merkleTreeSize ?? - getMerkleTreeAccountSize( + getMerkleTreeSize( + context, input.maxDepth, input.maxBufferSize, input.canopyDepth @@ -54,18 +56,3 @@ export const createTree = async ( ) ); }; - -export const getMerkleTreeAccountSize = ( - maxDepth: number, - maxBufferSize: number, - canopyDepth?: number -): number => - 1 + // Account discriminant. - 1 + // Header version. - 54 + // Merkle tree header V1. - 8 + // Merkle tree > sequenceNumber. - 8 + // Merkle tree > activeIndex. - 8 + // Merkle tree > bufferSize. - (40 + 32 * maxDepth) * maxBufferSize + // Merkle tree > changeLogs. - (32 * maxDepth + 40) + // Merkle tree > rightMostPath. - (canopyDepth ? Math.max(((1 << (canopyDepth + 1)) - 2) * 32, 0) : 0); diff --git a/clients/js/src/generated/accounts/index.ts b/clients/js/src/generated/accounts/index.ts index e5a51935..13a31fa4 100644 --- a/clients/js/src/generated/accounts/index.ts +++ b/clients/js/src/generated/accounts/index.ts @@ -6,5 +6,6 @@ * @see https://github.com/metaplex-foundation/kinobi */ +export * from './merkleTree'; export * from './treeConfig'; export * from './voucher'; diff --git a/clients/js/src/generated/accounts/merkleTree.ts b/clients/js/src/generated/accounts/merkleTree.ts new file mode 100644 index 00000000..d8d5268d --- /dev/null +++ b/clients/js/src/generated/accounts/merkleTree.ts @@ -0,0 +1,124 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/metaplex-foundation/kinobi + */ + +import { + Account, + Context, + Pda, + PublicKey, + RpcAccount, + RpcGetAccountOptions, + RpcGetAccountsOptions, + assertAccountExists, + deserializeAccount, + gpaBuilder, + publicKey as toPublicKey, +} from '@metaplex-foundation/umi'; +import { + MerkleTreeAccountData, + getMerkleTreeAccountDataSerializer, +} from '../../hooked'; +import { + CompressionAccountTypeArgs, + ConcurrentMerkleTreeHeaderDataArgs, + getCompressionAccountTypeSerializer, + getConcurrentMerkleTreeHeaderDataSerializer, +} from '../types'; + +export type MerkleTree = Account; + +export function deserializeMerkleTree( + context: Pick, + rawAccount: RpcAccount +): MerkleTree { + return deserializeAccount( + rawAccount, + getMerkleTreeAccountDataSerializer(context) + ); +} + +export async function fetchMerkleTree( + context: Pick, + publicKey: PublicKey | Pda, + options?: RpcGetAccountOptions +): Promise { + const maybeAccount = await context.rpc.getAccount( + toPublicKey(publicKey, false), + options + ); + assertAccountExists(maybeAccount, 'MerkleTree'); + return deserializeMerkleTree(context, maybeAccount); +} + +export async function safeFetchMerkleTree( + context: Pick, + publicKey: PublicKey | Pda, + options?: RpcGetAccountOptions +): Promise { + const maybeAccount = await context.rpc.getAccount( + toPublicKey(publicKey, false), + options + ); + return maybeAccount.exists + ? deserializeMerkleTree(context, maybeAccount) + : null; +} + +export async function fetchAllMerkleTree( + context: Pick, + publicKeys: Array, + options?: RpcGetAccountsOptions +): Promise { + const maybeAccounts = await context.rpc.getAccounts( + publicKeys.map((key) => toPublicKey(key, false)), + options + ); + return maybeAccounts.map((maybeAccount) => { + assertAccountExists(maybeAccount, 'MerkleTree'); + return deserializeMerkleTree(context, maybeAccount); + }); +} + +export async function safeFetchAllMerkleTree( + context: Pick, + publicKeys: Array, + options?: RpcGetAccountsOptions +): Promise { + const maybeAccounts = await context.rpc.getAccounts( + publicKeys.map((key) => toPublicKey(key, false)), + options + ); + return maybeAccounts + .filter((maybeAccount) => maybeAccount.exists) + .map((maybeAccount) => + deserializeMerkleTree(context, maybeAccount as RpcAccount) + ); +} + +export function getMerkleTreeGpaBuilder( + context: Pick +) { + const s = context.serializer; + const programId = context.programs.getPublicKey( + 'splAccountCompression', + 'cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK' + ); + return gpaBuilder(context, programId) + .registerFields<{ + discriminator: CompressionAccountTypeArgs; + treeHeader: ConcurrentMerkleTreeHeaderDataArgs; + serializedTree: Uint8Array; + }>({ + discriminator: [0, getCompressionAccountTypeSerializer(context)], + treeHeader: [1, getConcurrentMerkleTreeHeaderDataSerializer(context)], + serializedTree: [56, s.bytes()], + }) + .deserializeUsing((account) => + deserializeMerkleTree(context, account) + ); +} diff --git a/clients/js/src/generated/types/compressionAccountType.ts b/clients/js/src/generated/types/compressionAccountType.ts new file mode 100644 index 00000000..330022e5 --- /dev/null +++ b/clients/js/src/generated/types/compressionAccountType.ts @@ -0,0 +1,25 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/metaplex-foundation/kinobi + */ + +import { Context, Serializer } from '@metaplex-foundation/umi'; + +export enum CompressionAccountType { + Uninitialized, + ConcurrentMerkleTree, +} + +export type CompressionAccountTypeArgs = CompressionAccountType; + +export function getCompressionAccountTypeSerializer( + context: Pick +): Serializer { + const s = context.serializer; + return s.enum(CompressionAccountType, { + description: 'CompressionAccountType', + }) as Serializer; +} diff --git a/clients/js/src/generated/types/concurrentMerkleTreeHeader.ts b/clients/js/src/generated/types/concurrentMerkleTreeHeader.ts new file mode 100644 index 00000000..4de041fe --- /dev/null +++ b/clients/js/src/generated/types/concurrentMerkleTreeHeader.ts @@ -0,0 +1,59 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/metaplex-foundation/kinobi + */ + +import { Context, Serializer } from '@metaplex-foundation/umi'; +import { + CompressionAccountType, + CompressionAccountTypeArgs, + ConcurrentMerkleTreeHeaderData, + ConcurrentMerkleTreeHeaderDataArgs, + getCompressionAccountTypeSerializer, + getConcurrentMerkleTreeHeaderDataSerializer, +} from '.'; + +/** + * Initialization parameters for an SPL ConcurrentMerkleTree. + * + * Only the following permutations are valid: + * + * | max_depth | max_buffer_size | + * | --------- | --------------------- | + * | 14 | (64, 256, 1024, 2048) | + * | 20 | (64, 256, 1024, 2048) | + * | 24 | (64, 256, 512, 1024, 2048) | + * | 26 | (64, 256, 512, 1024, 2048) | + * | 30 | (512, 1024, 2048) | + * + */ + +export type ConcurrentMerkleTreeHeader = { + /** Account type */ + accountType: CompressionAccountType; + /** Versioned header */ + header: ConcurrentMerkleTreeHeaderData; +}; + +export type ConcurrentMerkleTreeHeaderArgs = { + /** Account type */ + accountType: CompressionAccountTypeArgs; + /** Versioned header */ + header: ConcurrentMerkleTreeHeaderDataArgs; +}; + +export function getConcurrentMerkleTreeHeaderSerializer( + context: Pick +): Serializer { + const s = context.serializer; + return s.struct( + [ + ['accountType', getCompressionAccountTypeSerializer(context)], + ['header', getConcurrentMerkleTreeHeaderDataSerializer(context)], + ], + { description: 'ConcurrentMerkleTreeHeader' } + ) as Serializer; +} diff --git a/clients/js/src/generated/types/concurrentMerkleTreeHeaderData.ts b/clients/js/src/generated/types/concurrentMerkleTreeHeaderData.ts new file mode 100644 index 00000000..bdff76da --- /dev/null +++ b/clients/js/src/generated/types/concurrentMerkleTreeHeaderData.ts @@ -0,0 +1,126 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/metaplex-foundation/kinobi + */ + +import { + Context, + GetDataEnumKind, + GetDataEnumKindContent, + PublicKey, + Serializer, +} from '@metaplex-foundation/umi'; + +export type ConcurrentMerkleTreeHeaderData = { + __kind: 'V1'; + /** + * Buffer of changelogs stored on-chain. + * Must be a power of 2; see above table for valid combinations. + */ + maxBufferSize: number; + /** + * Depth of the SPL ConcurrentMerkleTree to store. + * Tree capacity can be calculated as power(2, max_depth). + * See above table for valid options. + */ + maxDepth: number; + /** + * Authority that validates the content of the trees. + * Typically a program, e.g., the Bubblegum contract validates that leaves are valid NFTs. + */ + authority: PublicKey; + /** + * Slot corresponding to when the Merkle tree was created. + * Provides a lower-bound on what slot to start (re-)building a tree from. + */ + creationSlot: bigint; + /** + * Needs padding for the account to be 8-byte aligned + * 8-byte alignment is necessary to zero-copy the SPL ConcurrentMerkleTree + */ + padding: Array; +}; + +export type ConcurrentMerkleTreeHeaderDataArgs = { + __kind: 'V1'; + /** + * Buffer of changelogs stored on-chain. + * Must be a power of 2; see above table for valid combinations. + */ + maxBufferSize: number; + /** + * Depth of the SPL ConcurrentMerkleTree to store. + * Tree capacity can be calculated as power(2, max_depth). + * See above table for valid options. + */ + maxDepth: number; + /** + * Authority that validates the content of the trees. + * Typically a program, e.g., the Bubblegum contract validates that leaves are valid NFTs. + */ + authority: PublicKey; + /** + * Slot corresponding to when the Merkle tree was created. + * Provides a lower-bound on what slot to start (re-)building a tree from. + */ + creationSlot: number | bigint; + /** + * Needs padding for the account to be 8-byte aligned + * 8-byte alignment is necessary to zero-copy the SPL ConcurrentMerkleTree + */ + padding: Array; +}; + +export function getConcurrentMerkleTreeHeaderDataSerializer( + context: Pick +): Serializer< + ConcurrentMerkleTreeHeaderDataArgs, + ConcurrentMerkleTreeHeaderData +> { + const s = context.serializer; + return s.dataEnum( + [ + [ + 'V1', + s.struct>([ + ['maxBufferSize', s.u32()], + ['maxDepth', s.u32()], + ['authority', s.publicKey()], + ['creationSlot', s.u64()], + ['padding', s.array(s.u8(), { size: 6 })], + ]), + ], + ], + { description: 'ConcurrentMerkleTreeHeaderData' } + ) as Serializer< + ConcurrentMerkleTreeHeaderDataArgs, + ConcurrentMerkleTreeHeaderData + >; +} + +// Data Enum Helpers. +export function concurrentMerkleTreeHeaderData( + kind: 'V1', + data: GetDataEnumKindContent +): GetDataEnumKind; +export function concurrentMerkleTreeHeaderData< + K extends ConcurrentMerkleTreeHeaderDataArgs['__kind'] +>( + kind: K, + data?: any +): Extract { + return Array.isArray(data) + ? { __kind: kind, fields: data } + : { __kind: kind, ...(data ?? {}) }; +} +export function isConcurrentMerkleTreeHeaderData< + K extends ConcurrentMerkleTreeHeaderData['__kind'] +>( + kind: K, + value: ConcurrentMerkleTreeHeaderData +): value is ConcurrentMerkleTreeHeaderData & { __kind: K } { + return value.__kind === kind; +} diff --git a/clients/js/src/generated/types/index.ts b/clients/js/src/generated/types/index.ts index 2b8edac9..ad9f05c1 100644 --- a/clients/js/src/generated/types/index.ts +++ b/clients/js/src/generated/types/index.ts @@ -8,6 +8,9 @@ export * from './bubblegumEventType'; export * from './collection'; +export * from './compressionAccountType'; +export * from './concurrentMerkleTreeHeader'; +export * from './concurrentMerkleTreeHeaderData'; export * from './creator'; export * from './instructionName'; export * from './leafSchema'; diff --git a/clients/js/src/hash.ts b/clients/js/src/hash.ts new file mode 100644 index 00000000..6f764e39 --- /dev/null +++ b/clients/js/src/hash.ts @@ -0,0 +1,74 @@ +import { Context, PublicKey, mergeBytes } from '@metaplex-foundation/umi'; +import { keccak_256 } from '@noble/hashes/sha3'; +import { + MetadataArgsArgs, + getCreatorSerializer, + getMetadataArgsSerializer, +} from './generated'; +import { findLeafAssetIdPda } from './leafAssetId'; + +export function hash(input: Uint8Array | Uint8Array[]): Uint8Array { + return keccak_256(Array.isArray(input) ? mergeBytes(input) : input); +} + +export function hashLeaf( + context: Pick, + input: { + merkleTree: PublicKey; + owner: PublicKey; + delegate?: PublicKey; + leafIndex: number | bigint; + metadata: MetadataArgsArgs; + nftVersion?: number; + } +): Uint8Array { + const s = context.serializer; + const delegate = input.delegate ?? input.owner; + const nftVersion = input.nftVersion ?? 1; + const [leafAssetId] = findLeafAssetIdPda(context, { + tree: input.merkleTree, + leafIndex: input.leafIndex, + }); + + return hash([ + s.u8().serialize(nftVersion), + s.publicKey().serialize(leafAssetId), + s.publicKey().serialize(input.owner), + s.publicKey().serialize(delegate), + s.u64().serialize(input.leafIndex), + hashMetadata(context, input.metadata), + ]); +} + +export function hashMetadata( + context: Pick, + metadata: MetadataArgsArgs +): Uint8Array { + return mergeBytes([ + hashMetadataData(context, metadata), + hashMetadataCreators(context, metadata.creators), + ]); +} + +export function hashMetadataData( + context: Pick, + metadata: MetadataArgsArgs +): Uint8Array { + const s = context.serializer; + return hash([ + hash(getMetadataArgsSerializer(context).serialize(metadata)), + s.u16().serialize(metadata.sellerFeeBasisPoints), + ]); +} + +export function hashMetadataCreators( + context: Pick, + creators: MetadataArgsArgs['creators'] +): Uint8Array { + const s = context.serializer; + return hash( + s + .array(getCreatorSerializer(context), { size: 'remainder' }) + .serialize(creators) + ); +} diff --git a/clients/js/src/hooked/changeLog.ts b/clients/js/src/hooked/changeLog.ts new file mode 100644 index 00000000..6d5c82eb --- /dev/null +++ b/clients/js/src/hooked/changeLog.ts @@ -0,0 +1,21 @@ +import { Context, PublicKey, fixSerializer } from '@metaplex-foundation/umi'; + +export type ChangeLog = { + root: PublicKey; + pathNodes: PublicKey[]; + index: number; +}; + +export type ChangeLogArgs = ChangeLog; + +export const getChangeLogSerializer = ( + context: Pick, + maxDepth: number +) => { + const s = context.serializer; + return s.struct([ + ['root', s.publicKey()], + ['pathNodes', s.array(s.publicKey(), { size: maxDepth })], + ['index', fixSerializer(s.u32(), 8)], + ]); +}; diff --git a/clients/js/src/hooked/concurrentMerkleTree.ts b/clients/js/src/hooked/concurrentMerkleTree.ts new file mode 100644 index 00000000..fb9d2574 --- /dev/null +++ b/clients/js/src/hooked/concurrentMerkleTree.ts @@ -0,0 +1,39 @@ +import { Context } from '@metaplex-foundation/umi'; +import { Path, PathArgs, getPathSerializer } from './path'; +import { ChangeLog, ChangeLogArgs, getChangeLogSerializer } from './changeLog'; + +export type ConcurrentMerkleTree = { + sequenceNumber: bigint; + activeIndex: bigint; + bufferSize: bigint; + changeLogs: ChangeLog[]; + rightMostPath: Path; +}; + +export type ConcurrentMerkleTreeArgs = { + sequenceNumber: bigint | number; + activeIndex: bigint | number; + bufferSize: bigint | number; + changeLogs: ChangeLogArgs[]; + rightMostPath: PathArgs; +}; + +export const getConcurrentMerkleTreeSerializer = ( + context: Pick, + maxDepth: number, + maxBufferSize: number +) => { + const s = context.serializer; + return s.struct([ + ['sequenceNumber', s.u64()], + ['activeIndex', s.u64()], + ['bufferSize', s.u64()], + [ + 'changeLogs', + s.array(getChangeLogSerializer(context, maxDepth), { + size: maxBufferSize, + }), + ], + ['rightMostPath', getPathSerializer(context, maxDepth)], + ]); +}; diff --git a/clients/js/src/hooked/index.ts b/clients/js/src/hooked/index.ts new file mode 100644 index 00000000..90c8b0dc --- /dev/null +++ b/clients/js/src/hooked/index.ts @@ -0,0 +1,4 @@ +export * from './changeLog'; +export * from './concurrentMerkleTree'; +export * from './merkleTreeAccountData'; +export * from './path'; diff --git a/clients/js/src/hooked/merkleTreeAccountData.ts b/clients/js/src/hooked/merkleTreeAccountData.ts new file mode 100644 index 00000000..1ac24827 --- /dev/null +++ b/clients/js/src/hooked/merkleTreeAccountData.ts @@ -0,0 +1,118 @@ +import { + Context, + PublicKey, + Serializer, + mapSerializer, +} from '@metaplex-foundation/umi'; +import { + CompressionAccountType, + ConcurrentMerkleTreeHeaderData, + ConcurrentMerkleTreeHeaderDataArgs, + getConcurrentMerkleTreeHeaderDataSerializer, + getConcurrentMerkleTreeHeaderSerializer, +} from '../generated'; +import { + ConcurrentMerkleTree, + ConcurrentMerkleTreeArgs, + getConcurrentMerkleTreeSerializer, +} from './concurrentMerkleTree'; + +export type MerkleTreeAccountData = { + discriminator: CompressionAccountType; + treeHeader: ConcurrentMerkleTreeHeaderData; + tree: ConcurrentMerkleTree; + canopy: PublicKey[]; +}; + +export type MerkleTreeAccountDataArgs = { + treeHeader: ConcurrentMerkleTreeHeaderDataArgs; + tree: ConcurrentMerkleTreeArgs; + canopy: PublicKey[]; +}; + +export const getMerkleTreeAccountDataSerializer = ( + context: Pick +): Serializer => { + const headerSerializer = getConcurrentMerkleTreeHeaderSerializer(context); + return { + description: 'MerkleTreeAccountData', + fixedSize: null, + maxSize: null, + serialize: (value: MerkleTreeAccountDataArgs) => { + switch (value.treeHeader.__kind) { + case 'V1': + return getMerkleTreeAccountDataV1Serializer( + context, + value.treeHeader.maxDepth, + value.treeHeader.maxBufferSize + ).serialize(value); + default: + throw new Error( + `Unknown MerkleTreeAccountData version: ${value.treeHeader.__kind}` + ); + } + }, + deserialize: (bytes: Uint8Array, offset = 0) => { + const { header } = headerSerializer.deserialize(bytes, offset)[0]; + switch (header.__kind) { + case 'V1': + return getMerkleTreeAccountDataV1Serializer( + context, + header.maxDepth, + header.maxBufferSize + ).deserialize(bytes, offset); + default: + throw new Error( + `Unknown MerkleTreeAccountData version: ${header.__kind}` + ); + } + }, + }; +}; + +export const getMerkleTreeAccountDataV1Serializer = ( + context: Pick, + maxDepth: number, + maxBufferSize: number +): Serializer => { + const s = context.serializer; + return mapSerializer( + s.struct< + MerkleTreeAccountDataArgs & { discriminator: number }, + MerkleTreeAccountData + >([ + ['discriminator', s.u8()], + ['treeHeader', getConcurrentMerkleTreeHeaderDataSerializer(context)], + [ + 'tree', + getConcurrentMerkleTreeSerializer(context, maxDepth, maxBufferSize), + ], + ['canopy', s.array(s.publicKey(), { size: 'remainder' })], + ]), + ( + value: MerkleTreeAccountDataArgs + ): MerkleTreeAccountDataArgs & { discriminator: number } => ({ + ...value, + discriminator: CompressionAccountType.ConcurrentMerkleTree, + }) + ); +}; + +export const getMerkleTreeSize = ( + context: Pick, + maxDepth: number, + maxBufferSize: number, + canopyDepth = 0 +): number => { + const discriminatorSize = 1; + const headerSize = getConcurrentMerkleTreeHeaderDataSerializer(context) + .fixedSize as number; + const treeSize = getConcurrentMerkleTreeSerializer( + context, + maxDepth, + maxBufferSize + ).fixedSize as number; + // eslint-disable-next-line no-bitwise + const canopySize = 32 * Math.max((1 << (canopyDepth + 1)) - 2, 0); + return discriminatorSize + headerSize + treeSize + canopySize; +}; diff --git a/clients/js/src/hooked/path.ts b/clients/js/src/hooked/path.ts new file mode 100644 index 00000000..a075fed1 --- /dev/null +++ b/clients/js/src/hooked/path.ts @@ -0,0 +1,37 @@ +import { + Context, + PublicKey, + Serializer, + mapSerializer, +} from '@metaplex-foundation/umi'; + +export type Path = { + proof: PublicKey[]; + leaf: PublicKey; + index: number; +}; + +export type PathArgs = Path; + +export function getPathSerializer( + context: Pick, + maxDepth: number +): Serializer { + const s = context.serializer; + return mapSerializer( + s.struct( + [ + ['proof', s.array(s.publicKey(), { size: maxDepth })], + ['leaf', s.publicKey()], + ['index', s.u32()], + ['padding', s.u32()], + ], + { description: 'Path' } + ), + (path) => ({ ...path, padding: 0 }), + (pathWithPadding) => { + const { padding, ...path } = pathWithPadding; + return path; + } + ) as Serializer; +} diff --git a/clients/js/src/index.ts b/clients/js/src/index.ts index a2147cf8..21b75360 100644 --- a/clients/js/src/index.ts +++ b/clients/js/src/index.ts @@ -1,3 +1,6 @@ -export * from './generated'; export * from './createTree'; +export * from './generated'; +export * from './hash'; +export * from './hooked'; +export * from './leafAssetId'; export * from './plugin'; diff --git a/clients/js/src/leafAssetId.ts b/clients/js/src/leafAssetId.ts new file mode 100644 index 00000000..8e4db458 --- /dev/null +++ b/clients/js/src/leafAssetId.ts @@ -0,0 +1,21 @@ +import { Context, Pda, PublicKey } from '@metaplex-foundation/umi'; +import { MPL_BUBBLEGUM_PROGRAM_ID } from './generated'; + +export function findLeafAssetIdPda( + context: Pick, + seeds: { + tree: PublicKey; + leafIndex: number | bigint; + } +): Pda { + const programId = context.programs.getPublicKey( + 'mplBubblegum', + MPL_BUBBLEGUM_PROGRAM_ID + ); + const s = context.serializer; + return context.eddsa.findPda(programId, [ + s.string({ size: 'variable' }).serialize('asset'), + s.publicKey().serialize(seeds.tree), + s.u64().serialize(seeds.leafIndex), + ]); +} diff --git a/clients/js/test/mintV1.test.ts b/clients/js/test/mintV1.test.ts index 78a936b3..096ba940 100644 --- a/clients/js/test/mintV1.test.ts +++ b/clients/js/test/mintV1.test.ts @@ -1,27 +1,50 @@ -import { generateSigner, none } from '@metaplex-foundation/umi'; +import { generateSigner, none, publicKey } from '@metaplex-foundation/umi'; import test from 'ava'; -import { mintV1 } from '../src'; +import { MetadataArgsArgs, fetchMerkleTree, hashLeaf, mintV1 } from '../src'; import { createTree, createUmi } from './_setup'; test('it can mint an NFT from a Bubblegum tree', async (t) => { - // Given an existing Bubblegum tree. + // Given an empty Bubblegum tree. const umi = await createUmi(); const merkleTree = await createTree(umi); const leafOwner = generateSigner(umi).publicKey; + let merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); + t.is(merkleTreeAccount.tree.sequenceNumber, 0n); + t.is(merkleTreeAccount.tree.activeIndex, 0n); + t.is(merkleTreeAccount.tree.bufferSize, 1n); + t.is(merkleTreeAccount.tree.rightMostPath.index, 0); + t.is( + merkleTreeAccount.tree.rightMostPath.leaf, + publicKey('11111111111111111111111111111111') + ); - // When + // When we mint a new NFT from the tree using the following metadata. + const metadata: MetadataArgsArgs = { + name: 'My NFT', + uri: 'https://example.com/my-nft.json', + sellerFeeBasisPoints: 500, // 5% + collection: none(), + creators: [], + }; await mintV1(umi, { leafOwner, merkleTree, - message: { - name: 'My NFT', - uri: 'https://example.com/my-nft.json', - sellerFeeBasisPoints: 500, // 5% - collection: none(), - creators: [], - }, + message: metadata, }).sendAndConfirm(umi); - // Then - t.pass(); + // Then a new leaf was added to the merkle tree. + merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); + t.is(merkleTreeAccount.tree.sequenceNumber, 1n); + t.is(merkleTreeAccount.tree.activeIndex, 1n); + t.is(merkleTreeAccount.tree.bufferSize, 2n); + t.is(merkleTreeAccount.tree.rightMostPath.index, 1); + + // And the hash of the metadata matches the new leaf. + const leaf = hashLeaf(umi, { + merkleTree, + owner: leafOwner, + leafIndex: 0, + metadata, + }); + t.is(merkleTreeAccount.tree.rightMostPath.leaf, publicKey(leaf)); }); diff --git a/configs/kinobi.cjs b/configs/kinobi.cjs index 2f35b2fb..2e34ed00 100755 --- a/configs/kinobi.cjs +++ b/configs/kinobi.cjs @@ -35,14 +35,10 @@ kinobi.update( // Remove unnecessary spl_account_compression type. ApplicationDataEventV1: { delete: true }, ChangeLogEventV1: { delete: true }, - ConcurrentMerkleTreeHeader: { delete: true }, - ConcurrentMerkleTreeHeaderDataV1: { delete: true }, PathNode: { delete: true }, ApplicationDataEvent: { delete: true }, ChangeLogEvent: { delete: true }, AccountCompressionEvent: { delete: true }, - CompressionAccountType: { delete: true }, - ConcurrentMerkleTreeHeaderData: { delete: true }, }) ); @@ -129,6 +125,55 @@ kinobi.update( }) ); +// Custom tree updates. +kinobi.update( + new k.TransformNodesVisitor([ + { + // Add nodes to the splAccountCompression program. + selector: { kind: "programNode", name: "splAccountCompression" }, + transformer: (node) => { + k.assertProgramNode(node); + return k.programNode({ + ...node, + accounts: [ + ...node.accounts, + k.accountNode({ + name: "merkleTree", + data: k.accountDataNode({ + name: "merkleTreeAccountData", + link: k.linkTypeNode("merkleTreeAccountData", { + importFrom: "hooked", + }), + struct: k.structTypeNode([ + k.structFieldTypeNode({ + name: "discriminator", + child: k.linkTypeNode("compressionAccountType"), + }), + k.structFieldTypeNode({ + name: "treeHeader", + child: k.linkTypeNode("concurrentMerkleTreeHeaderData"), + }), + k.structFieldTypeNode({ + name: "serializedTree", + child: k.bytesTypeNode(k.remainderSize()), + }), + ]), + }), + }), + ], + }); + }, + }, + ]) +); + +// Transform tuple enum variants to structs. +kinobi.update( + new k.UnwrapTupleEnumWithSingleStructVisitor([ + "ConcurrentMerkleTreeHeaderData", + ]) +); + // Render JavaScript. const jsDir = path.join(clientDir, "js", "src", "generated"); const prettier = require(path.join(clientDir, "js", ".prettierrc.json")); diff --git a/package.json b/package.json index 593f24c3..c4287612 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "validator:stop": "amman stop" }, "devDependencies": { - "@metaplex-foundation/kinobi": "^0.10.1", + "@metaplex-foundation/kinobi": "^0.10.2", "@metaplex-foundation/shank-js": "^0.1.0", "@metaplex-foundation/amman": "^0.12.1", "typescript": "^4.9.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 973218ae..ffe10ab2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,8 +5,8 @@ devDependencies: specifier: ^0.12.1 version: 0.12.1(typescript@4.9.5) '@metaplex-foundation/kinobi': - specifier: ^0.10.1 - version: 0.10.1 + specifier: ^0.10.2 + version: 0.10.2 '@metaplex-foundation/shank-js': specifier: ^0.1.0 version: 0.1.0 @@ -85,8 +85,8 @@ packages: resolution: {integrity: sha512-S9RulC2fFCFOQraz61bij+5YCHhSO9llJegK8c8Y6731fSi6snUSQJdCUqYS8AIgR0TKbQvdvgSyIIdbDFZbBA==} dev: true - /@metaplex-foundation/kinobi@0.10.1: - resolution: {integrity: sha512-WIgo/0Dync/bbcJ9NCM4l3kTZC4Cvt2wNkTRMGqrcnd5+9cH6fYBez3N/Z8NMTsZwpeDP/R+QKU2LNGnr6+GOw==} + /@metaplex-foundation/kinobi@0.10.2: + resolution: {integrity: sha512-ytn4X9nsfR1E5DSPNxs6kreXt4jWxeG8HEQsHRNyfw4uKbPN1V4b1nIcC1rL9dtz0TBWfOMUTUxtncappi223A==} dependencies: '@noble/hashes': 1.3.0 chalk: 4.1.2