diff --git a/.github/workflows/pull-request-account-compression.yml b/.github/workflows/pull-request-account-compression.yml index 9504a1bcc46..a3e936f8c8a 100644 --- a/.github/workflows/pull-request-account-compression.yml +++ b/.github/workflows/pull-request-account-compression.yml @@ -79,7 +79,7 @@ jobs: js-test-account-compression: runs-on: ubuntu-latest env: - NODE_VERSION: 16.x + NODE_VERSION: 20.5 steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ env.NODE_VERSION }} diff --git a/.github/workflows/pull-request-js.yml b/.github/workflows/pull-request-js.yml index 88d018875cc..29ce10a05bf 100644 --- a/.github/workflows/pull-request-js.yml +++ b/.github/workflows/pull-request-js.yml @@ -40,29 +40,32 @@ jobs: js-test: strategy: matrix: + node-version: [16.x, 18.x, 20.x] package: [ account-compression, libraries, memo, name-service, - single-pool, stake-pool, token, token-group, - token-lending, token-metadata, token-swap, ] + include: + # Restrict single-pool and token-lending to supported Node.js versions. + - package: single-pool + node-version: 20.5 + - package: token-lending + node-version: 18.5 runs-on: ubuntu-latest - env: - NODE_VERSION: 20.5 steps: - uses: actions/checkout@v4 - - name: Use Node.js ${{ env.NODE_VERSION }} + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: - node-version: ${{ env.NODE_VERSION }} + node-version: ${{ matrix.node-version }} - uses: pnpm/action-setup@v4 - uses: actions/cache@v4 with: diff --git a/.github/workflows/pull-request-libraries.yml b/.github/workflows/pull-request-libraries.yml index 18ca39623a9..2b4eeef1e33 100644 --- a/.github/workflows/pull-request-libraries.yml +++ b/.github/workflows/pull-request-libraries.yml @@ -66,7 +66,7 @@ jobs: js-test: runs-on: ubuntu-latest env: - NODE_VERSION: 16.x + NODE_VERSION: 20.5 steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ env.NODE_VERSION }} diff --git a/Cargo.lock b/Cargo.lock index 4f3a8ca3820..4a88221431a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8839,9 +8839,9 @@ checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" [[package]] name = "uint" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" dependencies = [ "byteorder", "crunchy", diff --git a/account-compression/Cargo.lock b/account-compression/Cargo.lock index c5764a640be..4ad012b4ab3 100644 --- a/account-compression/Cargo.lock +++ b/account-compression/Cargo.lock @@ -544,9 +544,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.16.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" +checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" dependencies = [ "bytemuck_derive", ] @@ -1563,7 +1563,7 @@ dependencies = [ [[package]] name = "spl-account-compression" -version = "0.3.0" +version = "0.4.0" dependencies = [ "anchor-lang", "bytemuck", @@ -1574,7 +1574,7 @@ dependencies = [ [[package]] name = "spl-concurrent-merkle-tree" -version = "0.3.0" +version = "0.4.0" dependencies = [ "bytemuck", "solana-program", diff --git a/account-compression/programs/account-compression/Cargo.toml b/account-compression/programs/account-compression/Cargo.toml index a57df385abe..6aac624cf76 100644 --- a/account-compression/programs/account-compression/Cargo.toml +++ b/account-compression/programs/account-compression/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spl-account-compression" -version = "0.3.1" +version = "0.4.0" description = "Solana Program Library Account Compression Program" authors = ["Solana Labs Maintainers "] repository = "https://github.com/solana-labs/solana-program-library" @@ -21,7 +21,7 @@ default = [] anchor-lang = "0.29.0" bytemuck = "1.13" solana-program = ">=1.18.11,<=2" -spl-concurrent-merkle-tree = { version = "0.3.0", path = "../../../libraries/concurrent-merkle-tree" } +spl-concurrent-merkle-tree = { version = "0.4.0", path = "../../../libraries/concurrent-merkle-tree" } spl-noop = { version = "0.2.0", path = "../noop", features = ["no-entrypoint"] } [profile.release] diff --git a/account-compression/programs/account-compression/src/canopy.rs b/account-compression/programs/account-compression/src/canopy.rs index bdf85d7d5d0..284b2fbbe73 100644 --- a/account-compression/programs/account-compression/src/canopy.rs +++ b/account-compression/programs/account-compression/src/canopy.rs @@ -20,9 +20,13 @@ use crate::error::AccountCompressionError; use crate::events::ChangeLogEvent; use anchor_lang::prelude::*; use bytemuck::{cast_slice, cast_slice_mut}; -use spl_concurrent_merkle_tree::node::{empty_node_cached, Node, EMPTY}; +use solana_program::keccak::hashv; +use spl_concurrent_merkle_tree::node::{empty_node_cached, empty_node_cached_mut, Node, EMPTY}; use std::mem::size_of; +/// Maximum depth of the tree, supported by the SPL Compression +const MAX_SUPPORTED_DEPTH: usize = 30; + #[inline(always)] pub fn check_canopy_bytes(canopy_bytes: &[u8]) -> Result<()> { if canopy_bytes.len() % size_of::() != 0 { @@ -94,8 +98,7 @@ pub fn fill_in_proof_from_canopy( index: u32, proof: &mut Vec, ) -> Result<()> { - // 30 is hard coded as it is the current max depth that SPL Compression supports - let mut empty_node_cache = Box::new([EMPTY; 30]); + let mut empty_node_cache = Box::new([EMPTY; MAX_SUPPORTED_DEPTH]); check_canopy_bytes(canopy_bytes)?; let canopy = cast_slice::(canopy_bytes); let path_len = get_cached_path_length(canopy, max_depth)?; @@ -114,7 +117,7 @@ pub fn fill_in_proof_from_canopy( }; if canopy[cached_idx] == EMPTY { let level = max_depth - (31 - node_idx.leading_zeros()); - let empty_node = empty_node_cached::<30>(level, &mut empty_node_cache); + let empty_node = empty_node_cached::(level, &mut empty_node_cache); inferred_nodes.push(empty_node); } else { inferred_nodes.push(canopy[cached_idx]); @@ -128,3 +131,431 @@ pub fn fill_in_proof_from_canopy( proof.extend(inferred_nodes.iter().skip(overlap)); Ok(()) } + +/// Sets the leaf nodes of the canopy. The leaf nodes are the lowest level of the canopy, +/// representing the leaves of the canopy-tree. The method will update the parent nodes of all the +/// modified subtrees up to the uppermost level of the canopy. The leaf nodes indexing for the +/// start_index is 0-based without regards to the full tree indexes or the node indexes. The +/// start_index is the index of the first leaf node to be updated. +pub fn set_canopy_leaf_nodes( + canopy_bytes: &mut [u8], + max_depth: u32, + start_index: u32, + nodes: &[Node], +) -> Result<()> { + check_canopy_bytes(canopy_bytes)?; + let canopy = cast_slice_mut::(canopy_bytes); + let path_len = get_cached_path_length(canopy, max_depth)?; + if path_len == 0 { + return err!(AccountCompressionError::CanopyNotAllocated); + } + let start_canopy_node = leaf_node_index_to_canopy_index(path_len, start_index)?; + let start_canopy_idx = start_canopy_node - 2; + // set the "leaf" nodes of the canopy first - that's the lowest level of the canopy + for (i, node) in nodes.iter().enumerate() { + canopy[start_canopy_idx + i] = *node; + } + let mut start_canopy_node = start_canopy_node; + let mut end_canopy_node = start_canopy_node + nodes.len() - 1; + let mut empty_node_cache = Box::new([EMPTY; MAX_SUPPORTED_DEPTH]); + let leaf_node_level = max_depth - path_len; + // traverse up the tree and update the parent nodes in the modified subtree + for level in leaf_node_level + 1..max_depth { + start_canopy_node >>= 1; + end_canopy_node >>= 1; + for node in start_canopy_node..end_canopy_node + 1 { + let left_child = get_value_for_node::( + node << 1, + level - 1, + canopy, + &mut empty_node_cache, + ); + let right_child = get_value_for_node::( + (node << 1) + 1, + level - 1, + canopy, + &mut empty_node_cache, + ); + canopy[node - 2].copy_from_slice(hashv(&[&left_child, &right_child]).as_ref()); + } + } + Ok(()) +} + +/// Checks the root of the canopy against the expected root. +pub fn check_canopy_root(canopy_bytes: &[u8], expected_root: &Node, max_depth: u32) -> Result<()> { + check_canopy_bytes(canopy_bytes)?; + let canopy = cast_slice::(canopy_bytes); + if canopy.is_empty() { + return Ok(()); // Canopy is empty + } + let mut empty_node_cache = Box::new([EMPTY; MAX_SUPPORTED_DEPTH]); + // first two nodes are the children of the root, they have index 2 and 3 respectively + let left_root_child = + get_value_for_node::(2, max_depth - 1, canopy, &mut empty_node_cache); + let right_root_child = + get_value_for_node::(3, max_depth - 1, canopy, &mut empty_node_cache); + let actual_root = hashv(&[&left_root_child, &right_root_child]).to_bytes(); + if actual_root != *expected_root { + msg!( + "Canopy root mismatch. Expected: {:?}, Actual: {:?}", + expected_root, + actual_root + ); + err!(AccountCompressionError::CanopyRootMismatch) + } else { + Ok(()) + } +} + +/// Checks the canopy doesn't have any nodes to the right of the provided index in the full tree. +/// This is done by iterating through the canopy nodes to the right of the provided index and +/// finding the top-most node that has the node as its left child. The node should be empty. The +/// iteration contains following the previous checked node on the same level until the last node on +/// the level is reached. +pub fn check_canopy_no_nodes_to_right_of_index( + canopy_bytes: &[u8], + max_depth: u32, + index: u32, +) -> Result<()> { + check_canopy_bytes(canopy_bytes)?; + check_index(index, max_depth)?; + let canopy = cast_slice::(canopy_bytes); + let path_len = get_cached_path_length(canopy, max_depth)?; + + let mut node_idx = ((1 << max_depth) + index) >> (max_depth - path_len); + // no need to check the node_idx as it's the leaf continaing the index underneath it + while node_idx & (node_idx + 1) != 0 { + // check the next node to the right + node_idx += 1; + // find the top-most node that has the node as its left-most child + node_idx >>= node_idx.trailing_zeros(); + + let shifted_index = node_idx as usize - 2; + if canopy[shifted_index] != EMPTY { + msg!("Canopy node at index {} is not empty", shifted_index); + return err!(AccountCompressionError::CanopyRightmostLeafMismatch); + } + } + Ok(()) +} + +#[inline(always)] +fn check_index(index: u32, at_depth: u32) -> Result<()> { + if at_depth > MAX_SUPPORTED_DEPTH as u32 { + return err!(AccountCompressionError::ConcurrentMerkleTreeConstantsError); + } + if at_depth == 0 { + return err!(AccountCompressionError::ConcurrentMerkleTreeConstantsError); + } + if index >= (1 << at_depth) { + return err!(AccountCompressionError::LeafIndexOutOfBounds); + } + Ok(()) +} + +#[inline(always)] +fn get_value_for_node( + node_idx: usize, + level: u32, + canopy: &[Node], + empty_node_cache: &mut [Node; N], +) -> Node { + if canopy[node_idx - 2] != EMPTY { + return canopy[node_idx - 2]; + } + empty_node_cached_mut::(level, empty_node_cache) +} + +#[inline(always)] +fn leaf_node_index_to_canopy_index(path_len: u32, index: u32) -> Result { + check_index(index, path_len)?; + Ok((1 << path_len) + index as usize) +} + +#[cfg(test)] +mod tests { + use {super::*, spl_concurrent_merkle_tree::node::empty_node}; + + fn success_leaf_node_index_to_canopy_index(path_len: u32, index: u32, expected: usize) { + assert_eq!( + leaf_node_index_to_canopy_index(path_len, index).unwrap(), + expected + ); + } + + #[test] + fn test_zero_length_tree() { + assert_eq!( + leaf_node_index_to_canopy_index(0, 0).unwrap_err(), + AccountCompressionError::ConcurrentMerkleTreeConstantsError.into() + ); + } + + #[test] + fn test_1_level_0_index() { + success_leaf_node_index_to_canopy_index(1, 0, 2); + } + + #[test] + fn test_1_level_1_index() { + success_leaf_node_index_to_canopy_index(1, 1, 3); + } + + #[test] + fn test_2_level_0_index() { + success_leaf_node_index_to_canopy_index(2, 0, 4); + } + #[test] + fn test_2_level_3_index() { + success_leaf_node_index_to_canopy_index(2, 3, 7); + } + + #[test] + fn test_10_level_0_index() { + success_leaf_node_index_to_canopy_index(10, 0, 1024); + } + + #[test] + fn test_10_level_1023_index() { + success_leaf_node_index_to_canopy_index(10, 1023, 2047); + } + + #[test] + fn test_simple_single_level_canopy_set_canopy_leaf_nodes_with_empty_nodes() { + let mut canopy_bytes = vec![0_u8; 2 * size_of::()]; + let nodes = vec![EMPTY; 2]; + set_canopy_leaf_nodes(&mut canopy_bytes, 1, 0, &nodes).unwrap(); + let canopy = cast_slice::(&canopy_bytes); + + assert_eq!(canopy[0], EMPTY); + assert_eq!(canopy[1], EMPTY); + } + + #[test] + fn test_simple_single_level_canopy_set_canopy_leaf_nodes_non_empty_nodes() { + let mut canopy_bytes = vec![0_u8; 2 * size_of::()]; + let nodes = vec![[1_u8; 32], [2_u8; 32]]; + set_canopy_leaf_nodes(&mut canopy_bytes, 1, 0, &nodes).unwrap(); + let canopy = cast_slice::(&canopy_bytes); + + assert_eq!(canopy[0], [1_u8; 32]); + assert_eq!(canopy[1], [2_u8; 32]); + } + + #[test] + fn test_2levels_canopy_set_canopy_leaf_nodes_first_2_elements_provided() { + let mut canopy_bytes = vec![0_u8; 6 * size_of::()]; + let nodes = vec![[1_u8; 32], [2_u8; 32]]; + set_canopy_leaf_nodes(&mut canopy_bytes, 2, 0, &nodes).unwrap(); + let canopy = cast_slice::(&canopy_bytes); + + assert_eq!(canopy[0], hashv(&[&[1_u8; 32], &[2_u8; 32]]).to_bytes()); + assert_eq!(canopy[1], EMPTY); // is not updated + assert_eq!(canopy[2], [1_u8; 32]); + assert_eq!(canopy[3], [2_u8; 32]); + assert_eq!(canopy[4], EMPTY); + assert_eq!(canopy[5], EMPTY); + } + + #[test] + fn test_2levels_canopy_set_canopy_leaf_nodes_last_2_elements_provided() { + let mut canopy_bytes = vec![0_u8; 6 * size_of::()]; + let nodes = vec![[1_u8; 32], [2_u8; 32]]; + set_canopy_leaf_nodes(&mut canopy_bytes, 2, 2, &nodes).unwrap(); + let canopy = cast_slice::(&canopy_bytes); + + assert_eq!(canopy[0], EMPTY); // is not updated + assert_eq!(canopy[1], hashv(&[&[1_u8; 32], &[2_u8; 32]]).to_bytes()); + assert_eq!(canopy[2], EMPTY); + assert_eq!(canopy[3], EMPTY); + assert_eq!(canopy[4], [1_u8; 32]); + assert_eq!(canopy[5], [2_u8; 32]); + } + + #[test] + fn test_2levels_canopy_set_canopy_leaf_nodes_middle_2_elements_provided() { + let mut canopy_bytes = vec![0_u8; 6 * size_of::()]; + let nodes = vec![[1_u8; 32], [2_u8; 32]]; + set_canopy_leaf_nodes(&mut canopy_bytes, 2, 1, &nodes).unwrap(); + let canopy = cast_slice::(&canopy_bytes); + + assert_eq!(canopy[2], EMPTY); + assert_eq!(canopy[3], [1_u8; 32]); + assert_eq!(canopy[4], [2_u8; 32]); + assert_eq!(canopy[5], EMPTY); + assert_eq!(canopy[0], hashv(&[&EMPTY, &[1_u8; 32]]).to_bytes()); + assert_eq!(canopy[1], hashv(&[&[2_u8; 32], &EMPTY]).to_bytes()); + } + + #[test] + fn test_3level_canopy_in_10_level_tree_set_canopy_leaf_nodes_first_2_elements_provided() { + let mut canopy_bytes = vec![0_u8; 14 * size_of::()]; + let nodes = vec![[1_u8; 32], [2_u8; 32]]; + set_canopy_leaf_nodes(&mut canopy_bytes, 10, 0, &nodes).unwrap(); + let canopy = cast_slice::(&canopy_bytes); + + let expected_hash12 = hashv(&[&[1_u8; 32], &[2_u8; 32]]).to_bytes(); + assert_eq!( + canopy[0], + hashv(&[&expected_hash12, &empty_node(8)]).to_bytes() + ); + assert_eq!(canopy[1], EMPTY); // is not updated + assert_eq!(canopy[2], expected_hash12); + assert_eq!(canopy[3], EMPTY); // is not updated + assert_eq!(canopy[4], EMPTY); // is not updated + assert_eq!(canopy[5], EMPTY); // is not updated + assert_eq!(canopy[6], [1_u8; 32]); + assert_eq!(canopy[7], [2_u8; 32]); + } + + #[test] + fn test_3level_canopy_in_10_level_tree_set_canopy_leaf_nodes_middle_2_elements_provided() { + let mut canopy_bytes = vec![0_u8; 14 * size_of::()]; + let nodes = vec![[1_u8; 32], [2_u8; 32]]; + set_canopy_leaf_nodes(&mut canopy_bytes, 10, 3, &nodes).unwrap(); + let canopy = cast_slice::(&canopy_bytes); + + let expected_hash_empty_1 = hashv(&[&empty_node(7), &[1_u8; 32]]).to_bytes(); + let expected_hash_2_empty = hashv(&[&[2_u8; 32], &empty_node(7)]).to_bytes(); + + assert_eq!( + canopy[0], + hashv(&[&empty_node(8), &expected_hash_empty_1]).to_bytes() + ); + assert_eq!( + canopy[1], + hashv(&[&expected_hash_2_empty, &empty_node(8)]).to_bytes() + ); + assert_eq!(canopy[2], EMPTY); // is not updated + assert_eq!(canopy[3], expected_hash_empty_1); + assert_eq!(canopy[4], expected_hash_2_empty); + assert_eq!(canopy[5], EMPTY); // is not updated + assert_eq!(canopy[9], [1_u8; 32]); + assert_eq!(canopy[10], [2_u8; 32]); + } + + #[test] + fn test_3level_canopy_empty_set_canopy_leaf_nodes_no_impact() { + let mut canopy_bytes = vec![0_u8; 14 * size_of::()]; + let nodes = vec![]; + set_canopy_leaf_nodes(&mut canopy_bytes, 10, 0, &nodes).unwrap(); + assert_eq!(canopy_bytes, vec![0_u8; 14 * size_of::()]); + } + + #[test] + fn test_success_check_canopy_root() { + let mut canopy_bytes = vec![0_u8; 2 * size_of::()]; + let expected_root = hashv(&[&[1_u8; 32], &[2_u8; 32]]).to_bytes(); + let nodes = vec![[1_u8; 32], [2_u8; 32]]; + set_canopy_leaf_nodes(&mut canopy_bytes, 1, 0, &nodes).unwrap(); + check_canopy_root(&canopy_bytes, &expected_root, 30).unwrap(); + } + + #[test] + fn test_success_check_canopy_root_with_empty_right_branch() { + let mut canopy_bytes = vec![0_u8; 2 * size_of::()]; + let mut empty_node_cache = Box::new([EMPTY; MAX_SUPPORTED_DEPTH]); + let top_level = (MAX_SUPPORTED_DEPTH - 1) as u32; + let right_branch = + empty_node_cached_mut::(top_level, &mut empty_node_cache); + let expected_root = hashv(&[&[1_u8; 32], &right_branch]).to_bytes(); + let nodes = vec![[1_u8; 32], EMPTY]; + set_canopy_leaf_nodes(&mut canopy_bytes, MAX_SUPPORTED_DEPTH as u32, 0, &nodes).unwrap(); + check_canopy_root(&canopy_bytes, &expected_root, 30).unwrap(); + } + + #[test] + fn test_failure_check_canopy_root() { + let mut canopy_bytes = vec![0_u8; 2 * size_of::()]; + let expected_root = hashv(&[&[1_u8; 32], &[2_u8; 32]]).to_bytes(); + let nodes = vec![[1_u8; 32], [2_u8; 32]]; + set_canopy_leaf_nodes(&mut canopy_bytes, 1, 0, &nodes).unwrap(); + let mut expected_root = expected_root; + expected_root[0] = 0; + assert_eq!( + check_canopy_root(&canopy_bytes, &expected_root, 30).unwrap_err(), + AccountCompressionError::CanopyRootMismatch.into() + ); + } + + #[test] + fn test_success_check_canopy_no_nodes_to_right_of_index_empty_tree_first_index() { + let canopy_bytes = vec![0_u8; 6 * size_of::()]; + check_canopy_no_nodes_to_right_of_index(&canopy_bytes, 20, 0).unwrap(); + } + + #[test] + fn test_success_check_canopy_no_nodes_to_right_of_index_empty_tree_last_index() { + let canopy_bytes = vec![0_u8; 6 * size_of::()]; + check_canopy_no_nodes_to_right_of_index(&canopy_bytes, 20, (1 << 20) - 1).unwrap(); + } + + #[test] + fn test_success_check_canopy_no_nodes_to_right_of_index_empty_canopy_only_tree_first_index() { + let canopy_bytes = vec![0_u8; 6 * size_of::()]; + check_canopy_no_nodes_to_right_of_index(&canopy_bytes, 2, 0).unwrap(); + } + + #[test] + fn test_success_check_canopy_no_nodes_to_right_of_index_empty_canopy_only_tree_last_index() { + let canopy_bytes = vec![0_u8; 6 * size_of::()]; + check_canopy_no_nodes_to_right_of_index(&canopy_bytes, 2, (1 << 2) - 1).unwrap(); + } + + #[test] + fn test_failure_check_canopy_no_nodes_to_right_of_index_empty_tree_index_out_of_range() { + let canopy_bytes = vec![0_u8; 6 * size_of::()]; + assert_eq!( + check_canopy_no_nodes_to_right_of_index(&canopy_bytes, 2, 1 << 20).unwrap_err(), + AccountCompressionError::LeafIndexOutOfBounds.into() + ); + } + + #[test] + fn test_failure_check_canopy_no_nodes_to_right_of_index_full_tree_index_out_of_range() { + let canopy_bytes = vec![1_u8; 6 * size_of::()]; + assert_eq!( + check_canopy_no_nodes_to_right_of_index(&canopy_bytes, 2, 1 << 21).unwrap_err(), + AccountCompressionError::LeafIndexOutOfBounds.into() + ); + } + + #[test] + fn test_success_check_canopy_no_nodes_to_right_of_index_full_tree_last_index() { + let canopy_bytes = vec![1_u8; 6 * size_of::()]; + check_canopy_no_nodes_to_right_of_index(&canopy_bytes, 20, (1 << 20) - 1).unwrap(); + } + + #[test] + fn test_success_check_canopy_no_nodes_to_right_of_index_full_tree_first_child_of_last_canopy_node_leaf( + ) { + let canopy_bytes = vec![1_u8; 6 * size_of::()]; + check_canopy_no_nodes_to_right_of_index(&canopy_bytes, 20, 3 << (20 - 2)).unwrap(); + } + + #[test] + fn test_failure_check_canopy_no_nodes_to_right_of_index_full_tree_last_child_of_second_to_last_canopy_node_leaf( + ) { + let canopy_bytes = vec![1_u8; 6 * size_of::()]; + assert_eq!( + check_canopy_no_nodes_to_right_of_index(&canopy_bytes, 20, (3 << (20 - 2)) - 1) + .unwrap_err(), + AccountCompressionError::CanopyRightmostLeafMismatch.into() + ); + } + + #[test] + fn test_success_check_canopy_no_nodes_to_right_of_index_last_child_of_second_to_last_canopy_node_leaf( + ) { + let mut canopy_bytes = vec![1_u8; 6 * size_of::()]; + canopy_bytes[5 * size_of::()..].fill(0); + check_canopy_no_nodes_to_right_of_index(&canopy_bytes, 20, (3 << (20 - 2)) - 1).unwrap(); + } + + #[test] + fn test_succes_check_canopy_no_nodes_to_right_of_index_no_canopy() { + let canopy_bytes = vec![]; + check_canopy_no_nodes_to_right_of_index(&canopy_bytes, 20, 0).unwrap(); + } +} diff --git a/account-compression/programs/account-compression/src/concurrent_tree_wrapper.rs b/account-compression/programs/account-compression/src/concurrent_tree_wrapper.rs index 23c518c731f..e954fea5c57 100644 --- a/account-compression/programs/account-compression/src/concurrent_tree_wrapper.rs +++ b/account-compression/programs/account-compression/src/concurrent_tree_wrapper.rs @@ -86,3 +86,30 @@ pub fn merkle_tree_append_leaf( ) -> Result> { merkle_tree_apply_fn_mut!(header, tree_id, tree_bytes, append, *args) } + +/// Checks whether the tree in not initialized yet without doing the deserialization. A rought +/// equivalent to deserializing the tree and calling is_initialized() on it without the heavy +/// lifting with macros. An empty account is a zero'd account. The tree is considered empty if the +/// tree_bytes are all 0. A regular non-batch initialized tree is initialized early on when the +/// init_empty_merkle_tree is called. A batch initialized tree stays uninitialized until the +/// init_prepared_tree_with_root is called. +pub fn tree_bytes_uninitialized(tree_bytes: &[u8]) -> bool { + tree_bytes.iter().all(|&x| x == 0) +} + +#[inline(never)] +pub fn assert_tree_is_empty( + header: &ConcurrentMerkleTreeHeader, + tree_id: Pubkey, + tree_bytes: &mut [u8], +) -> Result<()> { + // If the tree is batch initialized and not finalized yet, we can treat it as empty. + // Before the tree is finalized, the tree_bytes will be all 0 as only the header will be + // initialized at that point, so we may skip the deserialization. + if header.get_is_batch_initialized() && tree_bytes_uninitialized(tree_bytes) { + return Ok(()); + } + // check the tree is empty + merkle_tree_apply_fn_mut!(header, tree_id, tree_bytes, prove_tree_is_empty,)?; + Ok(()) +} diff --git a/account-compression/programs/account-compression/src/error.rs b/account-compression/programs/account-compression/src/error.rs index fc7efdb215a..ef682300ebc 100644 --- a/account-compression/programs/account-compression/src/error.rs +++ b/account-compression/programs/account-compression/src/error.rs @@ -47,6 +47,26 @@ pub enum AccountCompressionError { /// is out of bounds of tree's maximum leaf capacity #[msg("Leaf index of concurrent merkle tree is out of bounds")] LeafIndexOutOfBounds, + + /// When initializing a canopy of the tree, the underlying tree was allocated without space for the canopy + #[msg("Tree was initialized without allocating space for the canopy")] + CanopyNotAllocated, + + /// The tree was already initialized + #[msg("Tree was already initialized")] + TreeAlreadyInitialized, + + /// The tree header was not initialized for batch processing + #[msg("Tree header was not initialized for batch processing")] + BatchNotInitialized, + + /// The canopy root doesn't match the root of the tree + #[msg("Canopy root does not match the root of the tree")] + CanopyRootMismatch, + + /// The canopy contains nodes to the right of the rightmost leaf of the tree + #[msg("Canopy contains nodes to the right of the rightmost leaf of the tree")] + CanopyRightmostLeafMismatch, } impl From<&ConcurrentMerkleTreeError> for AccountCompressionError { diff --git a/account-compression/programs/account-compression/src/lib.rs b/account-compression/programs/account-compression/src/lib.rs index 152cd5f745b..93d8411b3c4 100644 --- a/account-compression/programs/account-compression/src/lib.rs +++ b/account-compression/programs/account-compression/src/lib.rs @@ -40,7 +40,10 @@ pub mod zero_copy; pub use crate::noop::{wrap_application_data_v1, Noop}; -use crate::canopy::{fill_in_proof_from_canopy, update_canopy}; +use crate::canopy::{ + check_canopy_bytes, check_canopy_no_nodes_to_right_of_index, check_canopy_root, + fill_in_proof_from_canopy, set_canopy_leaf_nodes, update_canopy, +}; use crate::concurrent_tree_wrapper::*; pub use crate::error::AccountCompressionError; pub use crate::events::{AccountCompressionEvent, ChangeLogEvent}; @@ -48,13 +51,13 @@ use crate::noop::wrap_event; use crate::state::{ merkle_tree_get_size, ConcurrentMerkleTreeHeader, CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1, }; -use crate::zero_copy::ZeroCopy; /// Exported for Anchor / Solita pub use spl_concurrent_merkle_tree::{ concurrent_merkle_tree::{ConcurrentMerkleTree, FillEmptyOrAppendArgs}, error::ConcurrentMerkleTreeError, node::Node, + node::EMPTY, }; declare_id!("cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK"); @@ -74,7 +77,8 @@ pub struct Initialize<'info> { pub noop: Program<'info, Noop>, } -/// Context for inserting, appending, or replacing a leaf in the tree +/// Context for modifying a tree: inserting, appending, or replacing a leaf in +/// the existing tree and setting the canopy or finalizing a prepared tree. /// /// Modification instructions also require the proof to the leaf to be provided /// as 32-byte nodes via "remaining accounts". @@ -180,69 +184,166 @@ pub mod spl_account_compression { update_canopy(canopy_bytes, header.get_max_depth(), None) } - /// Note: - /// Supporting this instruction open a security vulnerability for indexers. - /// This instruction has been deemed unusable for publicly indexed compressed NFTs. - /// Indexing batched data in this way requires indexers to read in the `uri`s onto physical storage - /// and then into their database. This opens up a DOS attack vector, whereby this instruction is - /// repeatedly invoked, causing indexers to fail. - /// - /// Because this instruction was deemed insecure, this instruction has been removed - /// until secure usage is available on-chain. - // pub fn init_merkle_tree_with_root( - // ctx: Context, - // max_depth: u32, - // max_buffer_size: u32, - // root: [u8; 32], - // leaf: [u8; 32], - // index: u32, - // _changelog_db_uri: String, - // _metadata_db_uri: String, - // ) -> Result<()> { - // require_eq!( - // *ctx.accounts.merkle_tree.owner, - // crate::id(), - // AccountCompressionError::IncorrectAccountOwner - // ); - // let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?; - - // let (mut header_bytes, rest) = - // merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1); - - // let mut header = ConcurrentMerkleTreeHeader::try_from_slice(&header_bytes)?; - // header.initialize( - // max_depth, - // max_buffer_size, - // &ctx.accounts.authority.key(), - // Clock::get()?.slot, - // ); - // header.serialize(&mut header_bytes)?; - // let merkle_tree_size = merkle_tree_get_size(&header)?; - // let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size); - - // // Get rightmost proof from accounts - // let mut proof = vec![]; - // for node in ctx.remaining_accounts.iter() { - // proof.push(node.key().to_bytes()); - // } - // fill_in_proof_from_canopy(canopy_bytes, header.max_depth, index, &mut proof)?; - // assert_eq!(proof.len(), max_depth as usize); - - // let id = ctx.accounts.merkle_tree.key(); - // // A call is made to ConcurrentMerkleTree::initialize_with_root(root, leaf, proof, index) - // let change_log = merkle_tree_apply_fn!( - // header, - // id, - // tree_bytes, - // initialize_with_root, - // root, - // leaf, - // &proof, - // index - // )?; - // wrap_event(change_log.try_to_vec()?, &ctx.accounts.log_wrapper)?; - // update_canopy(canopy_bytes, header.max_depth, Some(change_log)) - // } + /// (Devnet only) In order to initialize a tree with a root, we need to create the tree on-chain first with + /// the proper authority. The tree might contain a canopy, which is a cache of the uppermost + /// nodes. The canopy is used to decrease the size of the proof required to update the tree. + /// If the tree is expected to have a canopy, it needs to be prefilled with the necessary nodes. + /// There are 2 ways to initialize a merkle tree: + /// 1. Initialize an empty tree + /// 2. Initialize a tree with a root and leaf + /// For the former case, the canopy will be empty which is expected for an empty tree. The + /// expected flow is `init_empty_merkle_tree`. For the latter case, the canopy should be + /// filled with the necessary nodes to render the tree usable. Thus we need to prefill the + /// canopy with the necessary nodes. The expected flow for a tree without canopy is + /// `prepare_batch_merkle_tree` -> `init_prepared_tree_with_root`. The expected flow for a tree + /// with canopy is `prepare_batch_merkle_tree` -> `append_canopy_nodes` (multiple times + /// until all of the canopy is filled) -> `init_prepared_tree_with_root`. This instruction + /// initializes the tree header while leaving the tree itself uninitialized. This allows + /// distinguishing between an empty tree and a tree prepare to be initialized with a root. + pub fn prepare_batch_merkle_tree( + ctx: Context, + max_depth: u32, + max_buffer_size: u32, + ) -> Result<()> { + require_eq!( + *ctx.accounts.merkle_tree.owner, + crate::id(), + AccountCompressionError::IncorrectAccountOwner + ); + let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?; + + let (mut header_bytes, rest) = + merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1); + + let mut header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?; + header.initialize_batched( + max_depth, + max_buffer_size, + &ctx.accounts.authority.key(), + Clock::get()?.slot, + ); + header.serialize(&mut header_bytes)?; + let merkle_tree_size = merkle_tree_get_size(&header)?; + let (_tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size); + check_canopy_bytes(canopy_bytes) + } + + /// (Devnet only) This instruction pre-initializes the canopy with the specified leaf nodes of the canopy. + /// This is intended to be used after `prepare_batch_merkle_tree` and in conjunction with the + /// `init_prepared_tree_with_root` instruction that'll finalize the tree initialization. + /// The canopy is used to cache the uppermost nodes of the tree, which allows for a smaller + /// proof size when updating the tree. The canopy should be filled with the necessary nodes + /// before calling `init_prepared_tree_with_root`. You may call this instruction multiple + /// times to fill the canopy with the necessary nodes. The canopy may be filled with the + /// nodes in any order. The already filled nodes may be replaced with new nodes before calling + /// `init_prepared_tree_with_root` if the step was done in error. + /// The canopy should be filled with all the nodes that are to the left of the rightmost + /// leaf of the tree before calling `init_prepared_tree_with_root`. The canopy should not + /// contain any nodes to the right of the rightmost leaf of the tree. + /// This instruction calculates and filles in all the canopy nodes "above" the provided ones. + /// The validation of the canopy is done in the `init_prepared_tree_with_root` instruction. + pub fn append_canopy_nodes( + ctx: Context, + start_index: u32, + canopy_nodes: Vec<[u8; 32]>, + ) -> Result<()> { + require_eq!( + *ctx.accounts.merkle_tree.owner, + crate::id(), + AccountCompressionError::IncorrectAccountOwner + ); + let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?; + + let (header_bytes, rest) = + merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1); + + let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?; + header.assert_valid_authority(&ctx.accounts.authority.key())?; + header.assert_is_batch_initialized()?; + // assert the tree is not initialized yet, we don't want to overwrite the canopy of an + // initialized tree + let merkle_tree_size = merkle_tree_get_size(&header)?; + let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size); + // ensure the tree is not initialized, the hacky way + require!( + tree_bytes_uninitialized(tree_bytes), + AccountCompressionError::TreeAlreadyInitialized + ); + set_canopy_leaf_nodes( + canopy_bytes, + header.get_max_depth(), + start_index, + &canopy_nodes, + ) + } + + /// (Devnet only) Initializes a prepared tree with a root and a rightmost leaf. The rightmost leaf is used to + /// verify the canopy if the tree has it. Before calling this instruction, the tree should be + /// prepared with `prepare_batch_merkle_tree` and the canopy should be filled with the necessary + /// nodes with `append_canopy_nodes` (if the canopy is used). This method should be used for + /// batch creation of trees. The indexing of such batches should be done off-chain. The + /// programs calling this instruction should take care of ensuring the indexing is possible. + /// For example, staking may be required to ensure the tree creator has some responsibility + /// for what is being indexed. If indexing is not possible, there should be a mechanism to + /// penalize the tree creator. + pub fn init_prepared_tree_with_root( + ctx: Context, + root: [u8; 32], + rightmost_leaf: [u8; 32], + rightmost_index: u32, + ) -> Result<()> { + require_eq!( + *ctx.accounts.merkle_tree.owner, + crate::id(), + AccountCompressionError::IncorrectAccountOwner + ); + let mut merkle_tree_bytes = ctx.accounts.merkle_tree.try_borrow_mut_data()?; + + let (header_bytes, rest) = + merkle_tree_bytes.split_at_mut(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1); + // the header should already be initialized with prepare_batch_merkle_tree + let header = ConcurrentMerkleTreeHeader::try_from_slice(header_bytes)?; + header.assert_valid_authority(&ctx.accounts.authority.key())?; + header.assert_is_batch_initialized()?; + let merkle_tree_size = merkle_tree_get_size(&header)?; + let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size); + // check the canopy root matches the tree root + check_canopy_root(canopy_bytes, &root, header.get_max_depth())?; + // verify the canopy does not conain any nodes to the right of the rightmost leaf + check_canopy_no_nodes_to_right_of_index( + canopy_bytes, + header.get_max_depth(), + rightmost_index, + )?; + + // Get rightmost proof from accounts + let mut proof = vec![]; + for node in ctx.remaining_accounts.iter() { + proof.push(node.key().to_bytes()); + } + fill_in_proof_from_canopy( + canopy_bytes, + header.get_max_depth(), + rightmost_index, + &mut proof, + )?; + assert_eq!(proof.len(), header.get_max_depth() as usize); + + let id = ctx.accounts.merkle_tree.key(); + // A call is made to ConcurrentMerkleTree::initialize_with_root + let args = &InitializeWithRootArgs { + root, + rightmost_leaf, + proof_vec: proof, + index: rightmost_index, + }; + let change_log = merkle_tree_initialize_with_root(&header, id, tree_bytes, args)?; + update_canopy(canopy_bytes, header.get_max_depth(), Some(&change_log))?; + wrap_event( + &AccountCompressionEvent::ChangeLog(*change_log), + &ctx.accounts.noop, + ) + } /// Executes an instruction that overwrites a leaf node. /// Composing programs should check that the data hashed into previous_leaf @@ -466,7 +567,7 @@ pub mod spl_account_compression { let (tree_bytes, canopy_bytes) = rest.split_at_mut(merkle_tree_size); let id = ctx.accounts.merkle_tree.key(); - merkle_tree_apply_fn_mut!(header, id, tree_bytes, prove_tree_is_empty,)?; + assert_tree_is_empty(&header, id, tree_bytes)?; // Close merkle tree account // 1. Move lamports diff --git a/account-compression/programs/account-compression/src/state/concurrent_merkle_tree_header.rs b/account-compression/programs/account-compression/src/state/concurrent_merkle_tree_header.rs index 6d326b76142..29c0a38f71c 100644 --- a/account-compression/programs/account-compression/src/state/concurrent_merkle_tree_header.rs +++ b/account-compression/programs/account-compression/src/state/concurrent_merkle_tree_header.rs @@ -65,9 +65,14 @@ pub struct ConcurrentMerkleTreeHeaderDataV1 { /// Provides a lower-bound on what slot to start (re-)building a tree from. creation_slot: u64, + /// A flag indicating whether the tree has been initialized with a root. + /// This field was added together with the `finalize_tree_with_root` instruction. + /// It takes 1 byte of space taken from the previous padding for existing accounts. + is_batch_initialized: bool, + /// Needs padding for the account to be 8-byte aligned /// 8-byte alignment is necessary to zero-copy the SPL ConcurrentMerkleTree - _padding: [u8; 6], + _padding: [u8; 5], } #[repr(C)] @@ -95,6 +100,24 @@ impl ConcurrentMerkleTreeHeader { header.max_depth = max_depth; header.authority = *authority; header.creation_slot = creation_slot; + // is_batch_initialized is left false by default + } + } + } + + /// Initializes the header with the given parameters and sets the `is_batch_initialized` flag to + /// true. + pub fn initialize_batched( + &mut self, + max_depth: u32, + max_buffer_size: u32, + authority: &Pubkey, + creation_slot: u64, + ) { + self.initialize(max_depth, max_buffer_size, authority, creation_slot); + match self.header { + ConcurrentMerkleTreeHeaderData::V1(ref mut header) => { + header.is_batch_initialized = true; } } } @@ -117,6 +140,12 @@ impl ConcurrentMerkleTreeHeader { } } + pub fn get_is_batch_initialized(&self) -> bool { + match &self.header { + ConcurrentMerkleTreeHeaderData::V1(header) => header.is_batch_initialized, + } + } + pub fn set_new_authority(&mut self, new_authority: &Pubkey) { match self.header { ConcurrentMerkleTreeHeaderData::V1(ref mut header) => { @@ -155,6 +184,18 @@ impl ConcurrentMerkleTreeHeader { } Ok(()) } + + pub fn assert_is_batch_initialized(&self) -> Result<()> { + match &self.header { + ConcurrentMerkleTreeHeaderData::V1(header) => { + require!( + header.is_batch_initialized, + AccountCompressionError::BatchNotInitialized + ); + } + } + Ok(()) + } } pub fn merkle_tree_get_size(header: &ConcurrentMerkleTreeHeader) -> Result { @@ -166,10 +207,10 @@ pub fn merkle_tree_get_size(header: &ConcurrentMerkleTreeHeader) -> Result Ok(size_of::>()), (8, 16) => Ok(size_of::>()), (9, 16) => Ok(size_of::>()), - (10, 32)=> Ok(size_of::>()), - (11, 32)=> Ok(size_of::>()), - (12, 32)=> Ok(size_of::>()), - (13, 32)=> Ok(size_of::>()), + (10, 32) => Ok(size_of::>()), + (11, 32) => Ok(size_of::>()), + (12, 32) => Ok(size_of::>()), + (13, 32) => Ok(size_of::>()), (14, 64) => Ok(size_of::>()), (14, 256) => Ok(size_of::>()), (14, 1024) => Ok(size_of::>()), diff --git a/account-compression/sdk/idl/spl_account_compression.json b/account-compression/sdk/idl/spl_account_compression.json index 1a5de6f83d9..a9477f25eca 100644 --- a/account-compression/sdk/idl/spl_account_compression.json +++ b/account-compression/sdk/idl/spl_account_compression.json @@ -1,5 +1,5 @@ { - "version": "0.2.0", + "version": "0.3.1", "name": "spl_account_compression", "instructions": [ { @@ -51,18 +51,171 @@ } ] }, + { + "name": "prepareBatchMerkleTree", + "docs": [ + "In order to initialize a tree with a root, we need to create the tree on-chain first with", + "the proper authority. The tree might contain a canopy, which is a cache of the uppermost", + "nodes. The canopy is used to decrease the size of the proof required to update the tree.", + "If the tree is expected to have a canopy, it needs to be prefilled with the necessary nodes.", + "There are 2 ways to initialize a merkle tree:", + "1. Initialize an empty tree", + "2. Initialize a tree with a root and leaf", + "For the former case, the canopy will be empty which is expected for an empty tree. The", + "expected flow is `init_empty_merkle_tree`. For the latter case, the canopy should be", + "filled with the necessary nodes to render the tree usable. Thus we need to prefill the", + "canopy with the necessary nodes. The expected flow for a tree without canopy is", + "`prepare_batch_merkle_tree` -> `init_prepared_tree_with_root`. The expected flow for a tree", + "with canopy is `prepare_batch_merkle_tree` -> `append_canopy_nodes` (multiple times", + "until all of the canopy is filled) -> `init_prepared_tree_with_root`. This instruction", + "initializes the tree header while leaving the tree itself uninitialized. This allows", + "distinguishing between an empty tree and a tree prepare to be initialized with a root." + ], + "accounts": [ + { + "name": "merkleTree", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true, + "docs": [ + "Authority that controls write-access to the tree", + "Typically a program, e.g., the Bubblegum contract validates that leaves are valid NFTs." + ] + }, + { + "name": "noop", + "isMut": false, + "isSigner": false, + "docs": ["Program used to emit changelogs as cpi instruction data."] + } + ], + "args": [ + { + "name": "maxDepth", + "type": "u32" + }, + { + "name": "maxBufferSize", + "type": "u32" + } + ] + }, + { + "name": "appendCanopyNodes", + "docs": [ + "This instruction pre-initializes the canopy with the specified leaf nodes of the canopy.", + "This is intended to be used after `prepare_batch_merkle_tree` and in conjunction with the", + "`init_prepared_tree_with_root` instruction that'll finalize the tree initialization.", + "The canopy is used to cache the uppermost nodes of the tree, which allows for a smaller", + "proof size when updating the tree. The canopy should be filled with the necessary nodes", + "before calling `init_prepared_tree_with_root`. You may call this instruction multiple", + "times to fill the canopy with the necessary nodes. The canopy may be filled with the", + "nodes in any order. The already filled nodes may be replaced with new nodes before calling", + "`init_prepared_tree_with_root` if the step was done in error.", + "The canopy should be filled with all the nodes that are to the left of the rightmost", + "leaf of the tree before calling `init_prepared_tree_with_root`. The canopy should not", + "contain any nodes to the right of the rightmost leaf of the tree.", + "This instruction calculates and filles in all the canopy nodes \"above\" the provided ones.", + "The validation of the canopy is done in the `init_prepared_tree_with_root` instruction." + ], + "accounts": [ + { + "name": "merkleTree", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true, + "docs": [ + "Authority that controls write-access to the tree", + "Typically a program, e.g., the Bubblegum contract validates that leaves are valid NFTs." + ] + }, + { + "name": "noop", + "isMut": false, + "isSigner": false, + "docs": ["Program used to emit changelogs as cpi instruction data."] + } + ], + "args": [ + { + "name": "startIndex", + "type": "u32" + }, + { + "name": "canopyNodes", + "type": { + "vec": { + "array": ["u8", 32] + } + } + } + ] + }, + { + "name": "initPreparedTreeWithRoot", + "docs": [ + "Initializes a prepared tree with a root and a rightmost leaf. The rightmost leaf is used to", + "verify the canopy if the tree has it. Before calling this instruction, the tree should be", + "prepared with `prepare_batch_merkle_tree` and the canopy should be filled with the necessary", + "nodes with `append_canopy_nodes` (if the canopy is used). This method should be used for", + "batch creation of trees. The indexing of such batches should be done off-chain. The", + "programs calling this instruction should take care of ensuring the indexing is possible.", + "For example, staking may be required to ensure the tree creator has some responsibility", + "for what is being indexed. If indexing is not possible, there should be a mechanism to", + "penalize the tree creator." + ], + "accounts": [ + { + "name": "merkleTree", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true, + "docs": [ + "Authority that controls write-access to the tree", + "Typically a program, e.g., the Bubblegum contract validates that leaves are valid NFTs." + ] + }, + { + "name": "noop", + "isMut": false, + "isSigner": false, + "docs": ["Program used to emit changelogs as cpi instruction data."] + } + ], + "args": [ + { + "name": "root", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "rightmostLeaf", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "rightmostIndex", + "type": "u32" + } + ] + }, { "name": "replaceLeaf", "docs": [ - "Note:", - "Supporting this instruction open a security vulnerability for indexers.", - "This instruction has been deemed unusable for publicly indexed compressed NFTs.", - "Indexing batched data in this way requires indexers to read in the `uri`s onto physical storage", - "and then into their database. This opens up a DOS attack vector, whereby this instruction is", - "repeatedly invoked, causing indexers to fail.", - "", - "Because this instruction was deemed insecure, this instruction has been removed", - "until secure usage is available on-chain.", "Executes an instruction that overwrites a leaf node.", "Composing programs should check that the data hashed into previous_leaf", "matches the authority information necessary to execute this instruction." @@ -407,6 +560,15 @@ ], "type": "u64" }, + { + "name": "isBatchInitialized", + "docs": [ + "A flag indicating whether the tree has been initialized with a root.", + "This field was added together with the `finalize_tree_with_root` instruction.", + "It takes 1 byte of space taken from the previous padding for existing accounts." + ], + "type": "bool" + }, { "name": "padding", "docs": [ @@ -414,7 +576,7 @@ "8-byte alignment is necessary to zero-copy the SPL ConcurrentMerkleTree" ], "type": { - "array": ["u8", 6] + "array": ["u8", 5] } } ] @@ -570,12 +732,37 @@ "code": 6008, "name": "LeafIndexOutOfBounds", "msg": "Leaf index of concurrent merkle tree is out of bounds" + }, + { + "code": 6009, + "name": "CanopyNotAllocated", + "msg": "Tree was initialized without allocating space for the canopy" + }, + { + "code": 6010, + "name": "TreeAlreadyInitialized", + "msg": "Tree was already initialized" + }, + { + "code": 6011, + "name": "BatchNotInitialized", + "msg": "Tree header was not initialized for batch processing" + }, + { + "code": 6012, + "name": "CanopyRootMismatch", + "msg": "Canopy root does not match the root of the tree" + }, + { + "code": 6013, + "name": "CanopyRightmostLeafMismatch", + "msg": "Canopy contains nodes to the right of the rightmost leaf of the tree" } ], "metadata": { "address": "cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK", "origin": "anchor", - "binaryVersion": "0.25.0", - "libVersion": "0.25.0" + "binaryVersion": "0.29.0", + "libVersion": "0.29.0" } } diff --git a/account-compression/sdk/package.json b/account-compression/sdk/package.json index 5d3e0584f74..9d2e1d9c165 100644 --- a/account-compression/sdk/package.json +++ b/account-compression/sdk/package.json @@ -39,7 +39,7 @@ "lint:fix": "eslint . --fix --ext .js,.ts", "docs": "rm -rf docs/ && typedoc --out docs", "deploy:docs": "npm run docs && gh-pages --dest account-compression/sdk --dist docs --dotfiles", - "start-validator": "solana-test-validator --reset --quiet --bpf-program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK ../target/deploy/spl_account_compression.so --bpf-program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV ../target/deploy/spl_noop.so", + "start-validator": "solana-test-validator --reset --quiet --bpf-program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK ../target/deploy/spl_account_compression.so --bpf-program noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV ../target/deploy/spl_noop.so --account 27QMkDMpBoAhmWj6xxQNYdqXZL5nnC8tkZcEtkNxCqeX pre-batch-init-tree-account.json", "run-tests": "jest tests --detectOpenHandles", "run-tests:events": "jest tests/events --detectOpenHandles", "run-tests:accounts": "jest tests/accounts --detectOpenHandles", @@ -66,13 +66,13 @@ "@coral-xyz/anchor": "^0.29.0", "@solana/eslint-config-solana": "^3.0.3", "@types/bn.js": "^5.1.0", - "@types/jest": "^29.5.12", + "@types/jest": "^29.5.13", "@types/node": "^22.5.4", "@types/node-fetch": "^2.6.11", - "@typescript-eslint/eslint-plugin": "^8.5.0", - "@typescript-eslint/parser": "^8.5.0", + "@typescript-eslint/eslint-plugin": "^8.4.0", + "@typescript-eslint/parser": "^8.4.0", "eslint": "^8.57.0", - "eslint-config-turbo": "^2.1.1", + "eslint-config-turbo": "^2.1.2", "eslint-plugin-import": "^2.30.0", "eslint-plugin-jest": "^28.8.3", "eslint-plugin-mocha": "^10.5.0", diff --git a/account-compression/sdk/src/accounts/ConcurrentMerkleTreeAccount.ts b/account-compression/sdk/src/accounts/ConcurrentMerkleTreeAccount.ts index 2e602599b18..bef2f4954cb 100644 --- a/account-compression/sdk/src/accounts/ConcurrentMerkleTreeAccount.ts +++ b/account-compression/sdk/src/accounts/ConcurrentMerkleTreeAccount.ts @@ -121,6 +121,14 @@ export class ConcurrentMerkleTreeAccount { getCanopyDepth(): number { return getCanopyDepth(this.canopy.canopyBytes.length); } + + /** + * Returns the flag that indicates if the tree has been batch initialized + * @returns the flag + */ + getIsBatchInitialized(): boolean { + return this.getHeaderV1().isBatchInitialized; + } } /** diff --git a/account-compression/sdk/src/generated/errors/index.ts b/account-compression/sdk/src/generated/errors/index.ts index 4600510da7f..03066ebcf49 100644 --- a/account-compression/sdk/src/generated/errors/index.ts +++ b/account-compression/sdk/src/generated/errors/index.ts @@ -194,6 +194,106 @@ export class LeafIndexOutOfBoundsError extends Error { createErrorFromCodeLookup.set(0x1778, () => new LeafIndexOutOfBoundsError()); createErrorFromNameLookup.set('LeafIndexOutOfBounds', () => new LeafIndexOutOfBoundsError()); +/** + * CanopyNotAllocated: 'Tree was initialized without allocating space for the canopy' + * + * @category Errors + * @category generated + */ +export class CanopyNotAllocatedError extends Error { + readonly code: number = 0x1779; + readonly name: string = 'CanopyNotAllocated'; + constructor() { + super('Tree was initialized without allocating space for the canopy'); + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, CanopyNotAllocatedError); + } + } +} + +createErrorFromCodeLookup.set(0x1779, () => new CanopyNotAllocatedError()); +createErrorFromNameLookup.set('CanopyNotAllocated', () => new CanopyNotAllocatedError()); + +/** + * TreeAlreadyInitialized: 'Tree was already initialized' + * + * @category Errors + * @category generated + */ +export class TreeAlreadyInitializedError extends Error { + readonly code: number = 0x177a; + readonly name: string = 'TreeAlreadyInitialized'; + constructor() { + super('Tree was already initialized'); + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, TreeAlreadyInitializedError); + } + } +} + +createErrorFromCodeLookup.set(0x177a, () => new TreeAlreadyInitializedError()); +createErrorFromNameLookup.set('TreeAlreadyInitialized', () => new TreeAlreadyInitializedError()); + +/** + * BatchNotInitialized: 'Tree header was not initialized for batch processing' + * + * @category Errors + * @category generated + */ +export class BatchNotInitializedError extends Error { + readonly code: number = 0x177b; + readonly name: string = 'BatchNotInitialized'; + constructor() { + super('Tree header was not initialized for batch processing'); + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, BatchNotInitializedError); + } + } +} + +createErrorFromCodeLookup.set(0x177b, () => new BatchNotInitializedError()); +createErrorFromNameLookup.set('BatchNotInitialized', () => new BatchNotInitializedError()); + +/** + * CanopyRootMismatch: 'Canopy root does not match the root of the tree' + * + * @category Errors + * @category generated + */ +export class CanopyRootMismatchError extends Error { + readonly code: number = 0x177c; + readonly name: string = 'CanopyRootMismatch'; + constructor() { + super('Canopy root does not match the root of the tree'); + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, CanopyRootMismatchError); + } + } +} + +createErrorFromCodeLookup.set(0x177c, () => new CanopyRootMismatchError()); +createErrorFromNameLookup.set('CanopyRootMismatch', () => new CanopyRootMismatchError()); + +/** + * CanopyRightmostLeafMismatch: 'Canopy contains nodes to the right of the rightmost leaf of the tree' + * + * @category Errors + * @category generated + */ +export class CanopyRightmostLeafMismatchError extends Error { + readonly code: number = 0x177d; + readonly name: string = 'CanopyRightmostLeafMismatch'; + constructor() { + super('Canopy contains nodes to the right of the rightmost leaf of the tree'); + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, CanopyRightmostLeafMismatchError); + } + } +} + +createErrorFromCodeLookup.set(0x177d, () => new CanopyRightmostLeafMismatchError()); +createErrorFromNameLookup.set('CanopyRightmostLeafMismatch', () => new CanopyRightmostLeafMismatchError()); + /** * Attempts to resolve a custom program error from the provided error code. * @category Errors diff --git a/account-compression/sdk/src/generated/instructions/appendCanopyNodes.ts b/account-compression/sdk/src/generated/instructions/appendCanopyNodes.ts new file mode 100644 index 00000000000..5cf4ddff765 --- /dev/null +++ b/account-compression/sdk/src/generated/instructions/appendCanopyNodes.ts @@ -0,0 +1,105 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet'; +import * as web3 from '@solana/web3.js'; + +/** + * @category Instructions + * @category AppendCanopyNodes + * @category generated + */ +export type AppendCanopyNodesInstructionArgs = { + canopyNodes: number[] /* size: 32 */[]; + startIndex: number; +}; +/** + * @category Instructions + * @category AppendCanopyNodes + * @category generated + */ +export const appendCanopyNodesStruct = new beet.FixableBeetArgsStruct< + AppendCanopyNodesInstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */; + } +>( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['startIndex', beet.u32], + ['canopyNodes', beet.array(beet.uniformFixedSizeArray(beet.u8, 32))], + ], + 'AppendCanopyNodesInstructionArgs', +); +/** + * Accounts required by the _appendCanopyNodes_ instruction + * + * @property [_writable_] merkleTree + * @property [**signer**] authority + * @property [] noop + * @category Instructions + * @category AppendCanopyNodes + * @category generated + */ +export type AppendCanopyNodesInstructionAccounts = { + anchorRemainingAccounts?: web3.AccountMeta[]; + authority: web3.PublicKey; + merkleTree: web3.PublicKey; + noop: web3.PublicKey; +}; + +export const appendCanopyNodesInstructionDiscriminator = [139, 155, 238, 167, 11, 243, 132, 205]; + +/** + * Creates a _AppendCanopyNodes_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category AppendCanopyNodes + * @category generated + */ +export function createAppendCanopyNodesInstruction( + accounts: AppendCanopyNodesInstructionAccounts, + args: AppendCanopyNodesInstructionArgs, + programId = new web3.PublicKey('cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK'), +) { + const [data] = appendCanopyNodesStruct.serialize({ + instructionDiscriminator: appendCanopyNodesInstructionDiscriminator, + ...args, + }); + const keys: web3.AccountMeta[] = [ + { + isSigner: false, + isWritable: true, + pubkey: accounts.merkleTree, + }, + { + isSigner: true, + isWritable: false, + pubkey: accounts.authority, + }, + { + isSigner: false, + isWritable: false, + pubkey: accounts.noop, + }, + ]; + + if (accounts.anchorRemainingAccounts != null) { + for (const acc of accounts.anchorRemainingAccounts) { + keys.push(acc); + } + } + + const ix = new web3.TransactionInstruction({ + data, + keys, + programId, + }); + return ix; +} diff --git a/account-compression/sdk/src/generated/instructions/index.ts b/account-compression/sdk/src/generated/instructions/index.ts index 7194bbb5b9e..10605ab4704 100644 --- a/account-compression/sdk/src/generated/instructions/index.ts +++ b/account-compression/sdk/src/generated/instructions/index.ts @@ -1,7 +1,10 @@ export * from './append'; +export * from './appendCanopyNodes'; export * from './closeEmptyTree'; export * from './initEmptyMerkleTree'; +export * from './initPreparedTreeWithRoot'; export * from './insertOrAppend'; +export * from './prepareBatchMerkleTree'; export * from './replaceLeaf'; export * from './transferAuthority'; export * from './verifyLeaf'; diff --git a/account-compression/sdk/src/generated/instructions/initPreparedTreeWithRoot.ts b/account-compression/sdk/src/generated/instructions/initPreparedTreeWithRoot.ts new file mode 100644 index 00000000000..b729a877a95 --- /dev/null +++ b/account-compression/sdk/src/generated/instructions/initPreparedTreeWithRoot.ts @@ -0,0 +1,107 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet'; +import * as web3 from '@solana/web3.js'; + +/** + * @category Instructions + * @category InitPreparedTreeWithRoot + * @category generated + */ +export type InitPreparedTreeWithRootInstructionArgs = { + rightmostIndex: number; + rightmostLeaf: number[] /* size: 32 */; + root: number[] /* size: 32 */; +}; +/** + * @category Instructions + * @category InitPreparedTreeWithRoot + * @category generated + */ +export const initPreparedTreeWithRootStruct = new beet.BeetArgsStruct< + InitPreparedTreeWithRootInstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */; + } +>( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['root', beet.uniformFixedSizeArray(beet.u8, 32)], + ['rightmostLeaf', beet.uniformFixedSizeArray(beet.u8, 32)], + ['rightmostIndex', beet.u32], + ], + 'InitPreparedTreeWithRootInstructionArgs', +); +/** + * Accounts required by the _initPreparedTreeWithRoot_ instruction + * + * @property [_writable_] merkleTree + * @property [**signer**] authority + * @property [] noop + * @category Instructions + * @category InitPreparedTreeWithRoot + * @category generated + */ +export type InitPreparedTreeWithRootInstructionAccounts = { + anchorRemainingAccounts?: web3.AccountMeta[]; + authority: web3.PublicKey; + merkleTree: web3.PublicKey; + noop: web3.PublicKey; +}; + +export const initPreparedTreeWithRootInstructionDiscriminator = [218, 248, 192, 55, 91, 205, 122, 10]; + +/** + * Creates a _InitPreparedTreeWithRoot_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category InitPreparedTreeWithRoot + * @category generated + */ +export function createInitPreparedTreeWithRootInstruction( + accounts: InitPreparedTreeWithRootInstructionAccounts, + args: InitPreparedTreeWithRootInstructionArgs, + programId = new web3.PublicKey('cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK'), +) { + const [data] = initPreparedTreeWithRootStruct.serialize({ + instructionDiscriminator: initPreparedTreeWithRootInstructionDiscriminator, + ...args, + }); + const keys: web3.AccountMeta[] = [ + { + isSigner: false, + isWritable: true, + pubkey: accounts.merkleTree, + }, + { + isSigner: true, + isWritable: false, + pubkey: accounts.authority, + }, + { + isSigner: false, + isWritable: false, + pubkey: accounts.noop, + }, + ]; + + if (accounts.anchorRemainingAccounts != null) { + for (const acc of accounts.anchorRemainingAccounts) { + keys.push(acc); + } + } + + const ix = new web3.TransactionInstruction({ + data, + keys, + programId, + }); + return ix; +} diff --git a/account-compression/sdk/src/generated/instructions/prepareBatchMerkleTree.ts b/account-compression/sdk/src/generated/instructions/prepareBatchMerkleTree.ts new file mode 100644 index 00000000000..0d5aa007db4 --- /dev/null +++ b/account-compression/sdk/src/generated/instructions/prepareBatchMerkleTree.ts @@ -0,0 +1,105 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet'; +import * as web3 from '@solana/web3.js'; + +/** + * @category Instructions + * @category PrepareBatchMerkleTree + * @category generated + */ +export type PrepareBatchMerkleTreeInstructionArgs = { + maxBufferSize: number; + maxDepth: number; +}; +/** + * @category Instructions + * @category PrepareBatchMerkleTree + * @category generated + */ +export const prepareBatchMerkleTreeStruct = new beet.BeetArgsStruct< + PrepareBatchMerkleTreeInstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */; + } +>( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['maxDepth', beet.u32], + ['maxBufferSize', beet.u32], + ], + 'PrepareBatchMerkleTreeInstructionArgs', +); +/** + * Accounts required by the _prepareBatchMerkleTree_ instruction + * + * @property [_writable_] merkleTree + * @property [**signer**] authority + * @property [] noop + * @category Instructions + * @category PrepareBatchMerkleTree + * @category generated + */ +export type PrepareBatchMerkleTreeInstructionAccounts = { + anchorRemainingAccounts?: web3.AccountMeta[]; + authority: web3.PublicKey; + merkleTree: web3.PublicKey; + noop: web3.PublicKey; +}; + +export const prepareBatchMerkleTreeInstructionDiscriminator = [230, 124, 120, 196, 249, 134, 199, 128]; + +/** + * Creates a _PrepareBatchMerkleTree_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category PrepareBatchMerkleTree + * @category generated + */ +export function createPrepareBatchMerkleTreeInstruction( + accounts: PrepareBatchMerkleTreeInstructionAccounts, + args: PrepareBatchMerkleTreeInstructionArgs, + programId = new web3.PublicKey('cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK'), +) { + const [data] = prepareBatchMerkleTreeStruct.serialize({ + instructionDiscriminator: prepareBatchMerkleTreeInstructionDiscriminator, + ...args, + }); + const keys: web3.AccountMeta[] = [ + { + isSigner: false, + isWritable: true, + pubkey: accounts.merkleTree, + }, + { + isSigner: true, + isWritable: false, + pubkey: accounts.authority, + }, + { + isSigner: false, + isWritable: false, + pubkey: accounts.noop, + }, + ]; + + if (accounts.anchorRemainingAccounts != null) { + for (const acc of accounts.anchorRemainingAccounts) { + keys.push(acc); + } + } + + const ix = new web3.TransactionInstruction({ + data, + keys, + programId, + }); + return ix; +} diff --git a/account-compression/sdk/src/generated/types/AccountCompressionEvent.ts b/account-compression/sdk/src/generated/types/AccountCompressionEvent.ts index 4bce6560620..ee25ced07a1 100644 --- a/account-compression/sdk/src/generated/types/AccountCompressionEvent.ts +++ b/account-compression/sdk/src/generated/types/AccountCompressionEvent.ts @@ -62,4 +62,4 @@ export const accountCompressionEventBeet = beet.dataEnum; +]) as beet.FixableBeet; diff --git a/account-compression/sdk/src/generated/types/ApplicationDataEvent.ts b/account-compression/sdk/src/generated/types/ApplicationDataEvent.ts index eef6c13b391..d6fd2aff4bf 100644 --- a/account-compression/sdk/src/generated/types/ApplicationDataEvent.ts +++ b/account-compression/sdk/src/generated/types/ApplicationDataEvent.ts @@ -49,4 +49,4 @@ export const applicationDataEventBeet = beet.dataEnum; +]) as beet.FixableBeet; diff --git a/account-compression/sdk/src/generated/types/ChangeLogEvent.ts b/account-compression/sdk/src/generated/types/ChangeLogEvent.ts index ffd34de7791..9418cff3bcf 100644 --- a/account-compression/sdk/src/generated/types/ChangeLogEvent.ts +++ b/account-compression/sdk/src/generated/types/ChangeLogEvent.ts @@ -48,4 +48,4 @@ export const changeLogEventBeet = beet.dataEnum([ 'ChangeLogEventRecord["V1"]', ), ], -]) as beet.FixableBeet; +]) as beet.FixableBeet; diff --git a/account-compression/sdk/src/generated/types/ConcurrentMerkleTreeHeaderData.ts b/account-compression/sdk/src/generated/types/ConcurrentMerkleTreeHeaderData.ts index f49f8f38996..3c1c8011f10 100644 --- a/account-compression/sdk/src/generated/types/ConcurrentMerkleTreeHeaderData.ts +++ b/account-compression/sdk/src/generated/types/ConcurrentMerkleTreeHeaderData.ts @@ -53,4 +53,4 @@ export const concurrentMerkleTreeHeaderDataBeet = beet.dataEnum; +]) as beet.FixableBeet; diff --git a/account-compression/sdk/src/generated/types/ConcurrentMerkleTreeHeaderDataV1.ts b/account-compression/sdk/src/generated/types/ConcurrentMerkleTreeHeaderDataV1.ts index 19a48b0d366..7e3b0e54e78 100644 --- a/account-compression/sdk/src/generated/types/ConcurrentMerkleTreeHeaderDataV1.ts +++ b/account-compression/sdk/src/generated/types/ConcurrentMerkleTreeHeaderDataV1.ts @@ -11,9 +11,10 @@ import * as web3 from '@solana/web3.js'; export type ConcurrentMerkleTreeHeaderDataV1 = { authority: web3.PublicKey; creationSlot: beet.bignum; + isBatchInitialized: boolean; maxBufferSize: number; maxDepth: number; - padding: number[] /* size: 6 */; + padding: number[] /* size: 5 */; }; /** @@ -26,7 +27,8 @@ export const concurrentMerkleTreeHeaderDataV1Beet = new beet.BeetArgsStruct[] | Buffer[], + startIndex: number, +): TransactionInstruction { + return createAppendCanopyNodesInstruction( + { + authority, + merkleTree, + noop: SPL_NOOP_PROGRAM_ID, + }, + { + canopyNodes: canopyNodes.map(node => Array.from(node)), + startIndex, + }, + ); +} + +/** + * Helper function for {@link createInitPreparedTreeWithRootInstruction} + * @param merkleTree + * @param authority + * @param root + * @param rightmostLeaf + * @param rightmostIndex + * @param proof + * @returns + */ +export function createInitPreparedTreeWithRootIx( + merkleTree: PublicKey, + authority: PublicKey, + root: ArrayLike | Buffer, + rightmostLeaf: ArrayLike | Buffer, + rightmostIndex: number, + proof: Buffer[], +): TransactionInstruction { + return createInitPreparedTreeWithRootInstruction( + { + anchorRemainingAccounts: proof.map(node => { + return { + isSigner: false, + isWritable: false, + pubkey: new PublicKey(node), + }; + }), + authority, + merkleTree, + noop: SPL_NOOP_PROGRAM_ID, + }, + { + rightmostIndex, + rightmostLeaf: Array.from(rightmostLeaf), + root: Array.from(root), + }, + ); +} + /** * Helper function for {@link createReplaceLeafInstruction} * @param merkleTree diff --git a/account-compression/sdk/tests/accountCompression.test.ts b/account-compression/sdk/tests/accountCompression.test.ts index d7a3338b7eb..deb7a2e3173 100644 --- a/account-compression/sdk/tests/accountCompression.test.ts +++ b/account-compression/sdk/tests/accountCompression.test.ts @@ -8,15 +8,21 @@ import * as crypto from 'crypto'; import { ConcurrentMerkleTreeAccount, + createAppendCanopyNodesIx, createAppendIx, createCloseEmptyTreeInstruction, + createCloseEmptyTreeIx, + createInitEmptyMerkleTreeIx, + createInitPreparedTreeWithRootIx, createReplaceIx, createTransferAuthorityIx, createVerifyLeafIx, + prepareTreeIx, ValidDepthSizePair, } from '../src'; import { hash, MerkleTree } from '../src/merkle-tree'; -import { createTreeOnChain, execute } from './utils'; +import { assertCMTProperties } from './accounts/concurrentMerkleTreeAccount.test'; +import { createTreeOnChain, execute, prepareTree } from './utils'; // eslint-disable-next-line no-empty describe('Account Compression', () => { @@ -54,6 +60,547 @@ describe('Account Compression', () => { ); }); + describe('Having prepared a tree without canopy', () => { + const depth = 3; + const size = 8; + const canopyDepth = 0; + const leaves = [ + crypto.randomBytes(32), + crypto.randomBytes(32), + crypto.randomBytes(32), + crypto.randomBytes(32), + crypto.randomBytes(32), + crypto.randomBytes(32), + crypto.randomBytes(32), + crypto.randomBytes(32), + ]; + let anotherKeyPair: Keypair; + let another: PublicKey; + + beforeEach(async () => { + const cmtKeypair = await prepareTree({ + canopyDepth, + depthSizePair: { + maxBufferSize: size, + maxDepth: depth, + }, + payer: payerKeypair, + provider, + }); + cmt = cmtKeypair.publicKey; + anotherKeyPair = Keypair.generate(); + another = anotherKeyPair.publicKey; + await provider.connection.confirmTransaction( + await provider.connection.requestAirdrop(another, 1e10), + 'confirmed', + ); + }); + it('Should be able to finalize the tree', async () => { + const merkleTreeRaw = new MerkleTree(leaves); + const root = merkleTreeRaw.root; + const leaf = leaves[leaves.length - 1]; + const canopyDepth = 0; + const finalize = createInitPreparedTreeWithRootIx( + cmt, + payer, + root, + leaf, + leaves.length - 1, + merkleTreeRaw.getProof(leaves.length - 1).proof, + ); + + await execute(provider, [finalize], [payerKeypair]); + + const splCMT = await ConcurrentMerkleTreeAccount.fromAccountAddress(connection, cmt); + assertCMTProperties(splCMT, depth, size, payer, root, canopyDepth, true); + assert(splCMT.getBufferSize() == 1, 'Buffer size does not match'); + }); + it('Should fail to append canopy node for a tree without canopy', async () => { + const appendIx = createAppendCanopyNodesIx(cmt, payer, [crypto.randomBytes(32)], 0); + try { + await execute(provider, [appendIx], [payerKeypair]); + assert(false, 'Canopy appending should have failed to execute for a tree without canopy'); + } catch {} + }); + it('Should fail to finalize the tree with another payer authority', async () => { + const merkleTreeRaw = new MerkleTree(leaves); + const root = merkleTreeRaw.root; + const leaf = leaves[leaves.length - 1]; + + const finalize = createInitPreparedTreeWithRootIx( + cmt, + another, + root, + leaf, + leaves.length - 1, + merkleTreeRaw.getProof(leaves.length - 1).proof, + ); + + try { + await execute(provider, [finalize], [anotherKeyPair]); + assert(false, 'Finalizing with another payer should have failed'); + } catch {} + }); + it('Should fail to finalize the tree with a wrong proof', async () => { + const merkleTreeRaw = new MerkleTree(leaves); + const root = merkleTreeRaw.root; + const leaf = leaves[leaves.length - 1]; + // Replace valid proof with random bytes so it is wrong + const proof = merkleTreeRaw.getProof(leaves.length - 1); + proof.proof = proof.proof.map(_ => { + return crypto.randomBytes(32); + }); + + const finalize = createInitPreparedTreeWithRootIx(cmt, payer, root, leaf, leaves.length - 1, proof.proof); + + try { + await execute(provider, [finalize], [payerKeypair]); + assert(false, 'Finalizing with a wrong proof should have failed'); + } catch {} + }); + it('Should fail to double finalize the tree', async () => { + const merkleTreeRaw = new MerkleTree(leaves); + const root = merkleTreeRaw.root; + const leaf = leaves[leaves.length - 1]; + + const finalize = createInitPreparedTreeWithRootIx( + cmt, + payer, + root, + leaf, + leaves.length - 1, + merkleTreeRaw.getProof(leaves.length - 1).proof, + ); + + await execute(provider, [finalize], [payerKeypair]); + + try { + await execute(provider, [finalize], [payerKeypair]); + assert(false, 'Double finalizing should have failed'); + } catch {} + }); + + it('Should be able to close a prepared tree', async () => { + let payerInfo = await provider.connection.getAccountInfo(payer, 'confirmed')!; + let treeInfo = await provider.connection.getAccountInfo(cmt, 'confirmed')!; + + const payerLamports = payerInfo!.lamports; + const treeLamports = treeInfo!.lamports; + + const closeIx = createCloseEmptyTreeIx(cmt, payer, payer); + await execute(provider, [closeIx], [payerKeypair]); + + payerInfo = await provider.connection.getAccountInfo(payer, 'confirmed')!; + const finalLamports = payerInfo!.lamports; + assert( + finalLamports === payerLamports + treeLamports - 5000, + 'Expected payer to have received the lamports from the closed tree account', + ); + + treeInfo = await provider.connection.getAccountInfo(cmt, 'confirmed'); + assert(treeInfo === null, 'Expected the merkle tree account info to be null'); + }); + }); + describe('Having prepared a tree with canopy', () => { + const depth = 3; + const size = 8; + const canopyDepth = 2; + const leaves = [ + crypto.randomBytes(32), + crypto.randomBytes(32), + crypto.randomBytes(32), + crypto.randomBytes(32), + crypto.randomBytes(32), + crypto.randomBytes(32), + crypto.randomBytes(32), + crypto.randomBytes(32), + ]; + let anotherKeyPair: Keypair; + let another: PublicKey; + beforeEach(async () => { + const cmtKeypair = await prepareTree({ + canopyDepth, + depthSizePair: { + maxBufferSize: size, + maxDepth: depth, + }, + payer: payerKeypair, + provider, + }); + cmt = cmtKeypair.publicKey; + anotherKeyPair = Keypair.generate(); + another = anotherKeyPair.publicKey; + await provider.connection.confirmTransaction( + await provider.connection.requestAirdrop(another, 1e10), + 'confirmed', + ); + }); + it('Should be able to append a single canopy node', async () => { + const appendIx = createAppendCanopyNodesIx(cmt, payer, [crypto.randomBytes(32)], 0); + await execute(provider, [appendIx], [payerKeypair]); + }); + it('Should be able to append a single canopy node at the index more then 0', async () => { + const appendIx = createAppendCanopyNodesIx(cmt, payer, [crypto.randomBytes(32)], 1); + await execute(provider, [appendIx], [payerKeypair]); + }); + it('Should be able to append several canopy nodes at the start of the node leaves', async () => { + const appendIx = createAppendCanopyNodesIx(cmt, payer, [crypto.randomBytes(32), crypto.randomBytes(32)], 0); + await execute(provider, [appendIx], [payerKeypair]); + }); + it('Should fail to append canopy node with another payer authority', async () => { + const appendIx = createAppendCanopyNodesIx(cmt, another, [crypto.randomBytes(32)], 0); + try { + await execute(provider, [appendIx], [anotherKeyPair]); + assert(false, 'Appending with another payer should have failed'); + } catch {} + }); + it('Should fail to append canopy nodes over the limit', async () => { + const appendIx = createAppendCanopyNodesIx( + cmt, + payer, + Array.from({ length: 3 }, () => crypto.randomBytes(32)), + 0, + ); + try { + await execute(provider, [appendIx], [payerKeypair]); + assert(false, 'Appending over the limit should have failed'); + } catch {} + }); + it('Should fail to append canopy nodes over the limit starting from the last index', async () => { + const appendIx = createAppendCanopyNodesIx( + cmt, + payer, + Array.from({ length: 2 }, () => crypto.randomBytes(32)), + 1, + ); + try { + await execute(provider, [appendIx], [payerKeypair]); + assert(false, 'Appending over the limit should have failed'); + } catch {} + }); + it('Should fail to append 0 canopy nodes', async () => { + const appendIx = createAppendCanopyNodesIx(cmt, payer, [], 0); + try { + await execute(provider, [appendIx], [payerKeypair]); + assert(false, 'Appending 0 nodes should have failed'); + } catch {} + }); + it('Should fail to finalize the tree without canopy', async () => { + const merkleTreeRaw = new MerkleTree(leaves); + const root = merkleTreeRaw.root; + const leaf = leaves[leaves.length - 1]; + + const finalize = createInitPreparedTreeWithRootIx( + cmt, + payer, + root, + leaf, + leaves.length - 1, + merkleTreeRaw.getProof(leaves.length - 1).proof, + ); + + try { + await execute(provider, [finalize], [payerKeypair]); + assert(false, 'Finalizing without canopy should have failed'); + } catch {} + }); + it('Should fail to finalize the tree with an incomplete canopy', async () => { + const merkleTreeRaw = new MerkleTree(leaves); + const root = merkleTreeRaw.root; + const leaf = leaves[leaves.length - 1]; + + const appendIx = createAppendCanopyNodesIx(cmt, payer, [merkleTreeRaw.leaves[0].parent!.node!], 0); + await execute(provider, [appendIx], [payerKeypair]); + const finalize = createInitPreparedTreeWithRootIx( + cmt, + payer, + root, + leaf, + leaves.length - 1, + merkleTreeRaw.getProof(leaves.length - 1).proof, + ); + + try { + await execute(provider, [finalize], [payerKeypair]); + assert(false, 'Finalization for an incomplete canopy should have failed'); + } catch {} + }); + it('Should finalize the tree with a complete canopy', async () => { + const merkleTreeRaw = new MerkleTree(leaves); + const root = merkleTreeRaw.root; + const leaf = leaves[leaves.length - 1]; + + // take every second leaf and append it's parent node to the canopy + const appendIx = createAppendCanopyNodesIx( + cmt, + payer, + merkleTreeRaw.leaves.filter((_, i) => i % 2 === 0).map(leaf => leaf.parent!.node!), + 0, + ); + await execute(provider, [appendIx], [payerKeypair]); + const finalize = createInitPreparedTreeWithRootIx( + cmt, + payer, + root, + leaf, + leaves.length - 1, + merkleTreeRaw.getProof(leaves.length - 1).proof, + ); + await execute(provider, [finalize], [payerKeypair]); + const splCMT = await ConcurrentMerkleTreeAccount.fromAccountAddress(connection, cmt); + assertCMTProperties(splCMT, depth, size, payer, root, canopyDepth, true); + }); + it('Should be able to setup canopy with several transactions', async () => { + const merkleTreeRaw = new MerkleTree(leaves); + const root = merkleTreeRaw.root; + const leaf = leaves[leaves.length - 1]; + // take every second leaf of the first half of a tree and append it's parent node to the canopy + const appendIx = createAppendCanopyNodesIx( + cmt, + payer, + merkleTreeRaw.leaves + .slice(0, leaves.length / 2) + .filter((_, i) => i % 2 === 0) + .map(leaf => leaf.parent!.node!), + 0, + ); + await execute(provider, [appendIx], [payerKeypair]); + // take every second leaf of the second half of a tree and append it's parent node to the canopy + const appendIx2 = createAppendCanopyNodesIx( + cmt, + payer, + merkleTreeRaw.leaves + .slice(leaves.length / 2) + .filter((_, i) => i % 2 === 0) + .map(leaf => leaf.parent!.node!), + 2, + ); + await execute(provider, [appendIx2], [payerKeypair]); + const finalize = createInitPreparedTreeWithRootIx( + cmt, + payer, + root, + leaf, + leaves.length - 1, + merkleTreeRaw.getProof(leaves.length - 1).proof, + ); + await execute(provider, [finalize], [payerKeypair]); + }); + it('Should be able to setup canopy with several transactions in reverse order', async () => { + const merkleTreeRaw = new MerkleTree(leaves); + const root = merkleTreeRaw.root; + const leaf = leaves[leaves.length - 1]; + + const appendIx = createAppendCanopyNodesIx( + cmt, + payer, + merkleTreeRaw.leaves + .slice(leaves.length / 2) + .filter((_, i) => i % 2 === 0) + .map(leaf => leaf.parent!.node!), + 2, + ); + await execute(provider, [appendIx], [payerKeypair]); + const appendIx2 = createAppendCanopyNodesIx( + cmt, + payer, + merkleTreeRaw.leaves + .slice(0, leaves.length / 2) + .filter((_, i) => i % 2 === 0) + .map(leaf => leaf.parent!.node!), + 0, + ); + await execute(provider, [appendIx2], [payerKeypair]); + const finalize = createInitPreparedTreeWithRootIx( + cmt, + payer, + root, + leaf, + leaves.length - 1, + merkleTreeRaw.getProof(leaves.length - 1).proof, + ); + await execute(provider, [finalize], [payerKeypair]); + }); + it('Should be able to replace a canopy node', async () => { + const merkleTreeRaw = new MerkleTree(leaves); + const root = merkleTreeRaw.root; + const leaf = leaves[leaves.length - 1]; + + const appendIx = createAppendCanopyNodesIx( + cmt, + payer, + merkleTreeRaw.leaves + .slice(0, leaves.length / 2) + .filter((_, i) => i % 2 === 0) + .map(leaf => leaf.parent!.node!), + 0, + ); + await execute(provider, [appendIx], [payerKeypair]); + const appendIx2 = createAppendCanopyNodesIx(cmt, payer, [crypto.randomBytes(32)], 2); + await execute(provider, [appendIx2], [payerKeypair]); + const replaceIx = createAppendCanopyNodesIx( + cmt, + payer, + merkleTreeRaw.leaves + .slice(leaves.length / 2) + .filter((_, i) => i % 2 === 0) + .map(leaf => leaf.parent!.node!), + 2, + ); + await execute(provider, [replaceIx], [payerKeypair]); + const finalize = createInitPreparedTreeWithRootIx( + cmt, + payer, + root, + leaf, + leaves.length - 1, + merkleTreeRaw.getProof(leaves.length - 1).proof, + ); + await execute(provider, [finalize], [payerKeypair]); + }); + it('Should fail to replace a canopy node for a finalised tree', async () => { + const merkleTreeRaw = new MerkleTree(leaves); + const root = merkleTreeRaw.root; + const leaf = leaves[leaves.length - 1]; + + const appendIx = createAppendCanopyNodesIx( + cmt, + payer, + merkleTreeRaw.leaves.filter((_, i) => i % 2 === 0).map(leaf => leaf.parent!.node!), + 0, + ); + await execute(provider, [appendIx], [payerKeypair]); + const finalize = createInitPreparedTreeWithRootIx( + cmt, + payer, + root, + leaf, + leaves.length - 1, + merkleTreeRaw.getProof(leaves.length - 1).proof, + ); + await execute(provider, [finalize], [payerKeypair]); + const replaceIx = createAppendCanopyNodesIx(cmt, payer, [crypto.randomBytes(32)], 0); + try { + await execute(provider, [replaceIx], [payerKeypair]); + assert(false, 'Replacing a canopy node for a finalised tree should have failed'); + } catch {} + }); + it('Should fail to initialize an empty tree after preparing a tree', async () => { + const ixs = [ + createInitEmptyMerkleTreeIx(cmt, payer, { + maxBufferSize: size, + maxDepth: depth, + }), + ]; + try { + await execute(provider, ixs, [payerKeypair]); + assert(false, 'Initializing an empty tree after preparing a tree should have failed'); + } catch {} + }); + it('Should be able to close a prepared tree after setting the canopy', async () => { + const merkleTreeRaw = new MerkleTree(leaves); + + const appendIx = createAppendCanopyNodesIx( + cmt, + payer, + merkleTreeRaw.leaves + .slice(0, leaves.length / 2) + .filter((_, i) => i % 2 === 0) + .map(leaf => leaf.parent!.node!), + 0, + ); + await execute(provider, [appendIx], [payerKeypair]); + let payerInfo = await provider.connection.getAccountInfo(payer, 'confirmed')!; + let treeInfo = await provider.connection.getAccountInfo(cmt, 'confirmed')!; + + const payerLamports = payerInfo!.lamports; + const treeLamports = treeInfo!.lamports; + + const closeIx = createCloseEmptyTreeIx(cmt, payer, payer); + await execute(provider, [closeIx], [payerKeypair]); + + payerInfo = await provider.connection.getAccountInfo(payer, 'confirmed')!; + const finalLamports = payerInfo!.lamports; + assert( + finalLamports === payerLamports + treeLamports - 5000, + 'Expected payer to have received the lamports from the closed tree account', + ); + + treeInfo = await provider.connection.getAccountInfo(cmt, 'confirmed'); + assert(treeInfo === null, 'Expected the merkle tree account info to be null'); + }); + }); + describe('Having prepared an empty tree with canopy', () => { + const depth = 3; + const size = 8; + const canopyDepth = 2; + // empty leaves represent the empty tree + const leaves = [ + Buffer.alloc(32), + Buffer.alloc(32), + Buffer.alloc(32), + Buffer.alloc(32), + Buffer.alloc(32), + Buffer.alloc(32), + Buffer.alloc(32), + Buffer.alloc(32), + ]; + let anotherKeyPair: Keypair; + let another: PublicKey; + beforeEach(async () => { + const cmtKeypair = await prepareTree({ + canopyDepth, + depthSizePair: { + maxBufferSize: size, + maxDepth: depth, + }, + payer: payerKeypair, + provider, + }); + cmt = cmtKeypair.publicKey; + anotherKeyPair = Keypair.generate(); + another = anotherKeyPair.publicKey; + await provider.connection.confirmTransaction( + await provider.connection.requestAirdrop(another, 1e10), + 'confirmed', + ); + }); + + it('Should be able to finalize an empty tree with empty canopy and close it afterwards', async () => { + const merkleTreeRaw = new MerkleTree(leaves); + const root = merkleTreeRaw.root; + const leaf = leaves[leaves.length - 1]; + + const finalize = createInitPreparedTreeWithRootIx( + cmt, + payer, + root, + leaf, + leaves.length - 1, + merkleTreeRaw.getProof(leaves.length - 1).proof, + ); + await execute(provider, [finalize], [payerKeypair]); + let payerInfo = await provider.connection.getAccountInfo(payer, 'confirmed')!; + let treeInfo = await provider.connection.getAccountInfo(cmt, 'confirmed')!; + + const payerLamports = payerInfo!.lamports; + const treeLamports = treeInfo!.lamports; + + const closeIx = createCloseEmptyTreeIx(cmt, payer, payer); + await execute(provider, [closeIx], [payerKeypair]); + + payerInfo = await provider.connection.getAccountInfo(payer, 'confirmed')!; + const finalLamports = payerInfo!.lamports; + assert( + finalLamports === payerLamports + treeLamports - 5000, + 'Expected payer to have received the lamports from the closed tree account', + ); + + treeInfo = await provider.connection.getAccountInfo(cmt, 'confirmed'); + assert(treeInfo === null, 'Expected the merkle tree account info to be null'); + }); + }); + describe('Having created a tree with a single leaf', () => { beforeEach(async () => { [cmtKeypair, offChainTree] = await createTreeOnChain(provider, payerKeypair, 1, DEPTH_SIZE_PAIR); @@ -162,6 +709,30 @@ describe('Account Compression', () => { 'Updated on chain root matches root of updated off chain tree', ); }); + + it('Should fail to prepare a batch ready tree for an existing tree', async () => { + const prepareIx = prepareTreeIx(cmt, payer, DEPTH_SIZE_PAIR); + try { + await execute(provider, [prepareIx], [payerKeypair]); + assert(false, 'Prepare a batch tree should have failed for the existing tree'); + } catch {} + }); + + it('Should fail to finalize an existing tree', async () => { + const index = offChainTree.leaves.length - 1; + const finalizeIx = createInitPreparedTreeWithRootIx( + cmt, + payer, + offChainTree.root, + offChainTree.leaves[index].node, + index, + offChainTree.getProof(index).proof, + ); + try { + await execute(provider, [finalizeIx], [payerKeypair]); + assert(false, 'Finalize an existing tree should have failed'); + } catch {} + }); }); describe('Examples transferring authority', () => { @@ -476,6 +1047,22 @@ describe('Account Compression', () => { await execute(provider, [replaceIx, replaceBackIx], [payerKeypair], true, true); } }); + + it('Should fail to append a canopy node for an existing tree', async () => { + [cmtKeypair, offChainTree] = await createTreeOnChain( + provider, + payerKeypair, + 0, + { maxBufferSize: 8, maxDepth: DEPTH }, + DEPTH, // Store full tree on chain + ); + cmt = cmtKeypair.publicKey; + const appendIx = createAppendCanopyNodesIx(cmt, payer, [crypto.randomBytes(32)], 0); + try { + await execute(provider, [appendIx], [payerKeypair]); + assert(false, 'Appending a canopy node for an existing tree should have failed'); + } catch {} + }); }); describe(`Having created a tree with 8 leaves`, () => { beforeEach(async () => { diff --git a/account-compression/sdk/tests/accounts/concurrentMerkleTreeAccount.test.ts b/account-compression/sdk/tests/accounts/concurrentMerkleTreeAccount.test.ts index 0b6d4a25e00..5e85aaca0c9 100644 --- a/account-compression/sdk/tests/accounts/concurrentMerkleTreeAccount.test.ts +++ b/account-compression/sdk/tests/accounts/concurrentMerkleTreeAccount.test.ts @@ -8,13 +8,14 @@ import { ALL_DEPTH_SIZE_PAIRS, ConcurrentMerkleTreeAccount, getConcurrentMerkleT import { emptyNode, MerkleTree } from '../../src/merkle-tree'; import { createEmptyTreeOnChain, createTreeOnChain } from '../utils'; -function assertCMTProperties( +export function assertCMTProperties( onChainCMT: ConcurrentMerkleTreeAccount, expectedMaxDepth: number, expectedMaxBufferSize: number, expectedAuthority: PublicKey, expectedRoot: Buffer, expectedCanopyDepth?: number, + expectedIsBatchInitialized = false, ) { assert( onChainCMT.getMaxDepth() === expectedMaxDepth, @@ -32,6 +33,10 @@ function assertCMTProperties( 'On chain canopy depth does not match expected canopy depth', ); } + assert( + onChainCMT.getIsBatchInitialized() === expectedIsBatchInitialized, + 'On chain isBatchInitialized does not match expected value', + ); } describe('ConcurrentMerkleTreeAccount tests', () => { @@ -142,4 +147,30 @@ describe('ConcurrentMerkleTreeAccount tests', () => { } }); }); + + describe('Can deserialize an existing CMTAccount from a real on-chain CMT created before the is_batch_initialized field was introduced inplace of the first byte of _padding', () => { + it('Interpreted on-chain fields correctly', async () => { + // The account data was generated by running: + // $ solana account 27QMkDMpBoAhmWj6xxQNYdqXZL5nnC8tkZcEtkNxCqeX \ + // --output-file tests/fixtures/pre-batch-init-tree-account.json \ + // --output json + const deployedAccount = new PublicKey('27QMkDMpBoAhmWj6xxQNYdqXZL5nnC8tkZcEtkNxCqeX'); + const cmt = await ConcurrentMerkleTreeAccount.fromAccountAddress(connection, deployedAccount, 'confirmed'); + const expectedMaxDepth = 10; + const expectedMaxBufferSize = 32; + const expectedCanopyDepth = 0; + const expectedAuthority = new PublicKey('BFNT941iRwYPe2Js64dTJSoksGCptWAwrkKMaSN73XK2'); + const expectedRoot = new PublicKey('83UjseEuEgxyVyDTmrJCQ9QbeksdRZ7KPDZGQYc5cAgF').toBuffer(); + const expectedIsBatchInitialized = false; + await assertCMTProperties( + cmt, + expectedMaxDepth, + expectedMaxBufferSize, + expectedAuthority, + expectedRoot, + expectedCanopyDepth, + expectedIsBatchInitialized, + ); + }); + }); }); diff --git a/account-compression/sdk/tests/fixtures/pre-batch-init-tree-account.json b/account-compression/sdk/tests/fixtures/pre-batch-init-tree-account.json new file mode 100644 index 00000000000..98adaa774dc --- /dev/null +++ b/account-compression/sdk/tests/fixtures/pre-batch-init-tree-account.json @@ -0,0 +1,14 @@ +{ + "pubkey": "27QMkDMpBoAhmWj6xxQNYdqXZL5nnC8tkZcEtkNxCqeX", + "account": { + "lamports": 84132480, + "data": [ + "AQAgAAAACgAAAJhDQUO8VHxyuU2+8Gbazk9rGe2MW3Xu3mPjn5qN+Mnd8qEyDgAAAAAAAAAAAAAJAAAAAAAAAAkAAAAAAAAACgAAAAAAAAD53D5/4BbgUO/yYDNPGKXU/jkdggkjGfWWTy4ut8HDpQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAArTIotnb3081ChKVEPxfxlis25JGzCkCyQFhJ5Ze6X7W0wRlRlXxvj2QsSvYc1rJGQP7G3H/GB+6CBqmekkENMCHduaNWgVw/rBAmtt7F3zEkr7rbSFybpaPjOYoEt7qF5Ydpsyob6vHqJzdaRAlaDR+2ZM4t01jn/L+3jCahk0QOsB6/ye0nUAzU38l5Jy0fCRPMn2ZUDX6ABYERCeHPLYh8Ir2HUNNAFqw8ZrX/EC2s3XP2sBTnELUegCKvmhlo/9cBV+SAY/wzyXoFD39kAjO/ZGzJjZUkxrkrzzq1b4OYZ8xffxlrk7rh4n5jIHQkRdKQ8iY4J0mLVP7FOfdWr8761OUIwJi5p+HY/rGZVfsCupZ1WFB4cQlp00QPUFTgAAAAAAAAAABCQcymud9cvMqkqaz7rb8B4fJGipunRCEkcpIwwf1bqgG0jc8ty2NKzqQ11mfpEgaWHx71Zkd+KhVW6g9RF++oo50UCdctGN9/Yj9M997BjhZjf9P0egTPElcLkXHmGEjtm+CkQBf+3Sbkkq2UmWgP+WnCKKzej7Rk25CTxmlTwR0oez8apwcYlgkfdQNxr3jMbmyJlZungLUL0qdF54IhXXYkra5BQiu8imQXVs5vHpaJmrjUhQCiQT0GdoO+zap3nFWBCJyTSPURgM3qEQhr6WeAPbarDAZnu2Hn6KOJBJiGW8/Mdyb2UHzslHH5v92pUD262jHxv9Dxka11yDv8R21qTggJggnyd9Y9xfFQJmMOlsHxK/OXAbJx15vj+kmxMg/C5+70VPL35eDhb5k8H351PyxQoZH0RK2eXBcIbwO6kdDpcOt1lYsFGxVJOtPzJ+Eq2c5sWwdFSlaopv7WAAAAAAAAAAC4lUsocxFm8dZSnJRk0dztzMPjp1Tw++Vr9kJl2nwJK9rUpl3vmHv+1y5vONxkqse+I11hYnCbfPjlMwi1H3TGbXub1fM1JIxVi6J8yee8q1pLyvHD6l8RgltY7GeLcH/U4Ya37FWODitELZNDut7d8YRI66XliIh458WBFM9ZkWXQAtLrm0fso9F+CE4K2hmZHGSd1tnCGMe1CN5PYnAd32gpAsQAHgFgjiYdxQ+Ezfk2PTE2KL6ltcvyeQ2fVk/xBR50NHfWV4D8JytRVz0YXRQ3fYf4EniFtQtPmQ2CntMT1AroOs/TbheIWHf427xgdUB6Nbd0xZqaRnp3PgDnBy4iDWhVohnVlke8mes2HV+4PTvKtS8QvSek76DrEV0oeh2SM/j5OOPFLfsK2gHRGX20vcKOYa3Rcs9g7X+F3uMazFbBC/vFwbCK/Pfjhmj6ZWod2MSVhsq2Xn9ezssqAQAAAAAAAAARmQ/IUesWNRXIOks0o34cvGsvVMbFCIZqluF+zkUg3hdpJ3DYGBOMaX3Jtz5u3/HqSrb4Uio0kx5VrkfTuCStLPB7V+PqjzBlN326Cg9WoxfTC6nEgo++DbKYbMIW+kYXnWqnD0lliL2PQlq/MxvtOYbZSWYNIg687iKjYnZpTKfNIilBb7JXh6KfMm2xDEpsmNy+djJBXPNfcB9y90yP7xXZA4/4HL5hPpQTNnestf54Wvoo2ZFUKHF/b1hmH7EzF4X5SJlrI/RlinkvdEjGtPzaIL2jYEjQ2S0rSYMD9impk0XbfWrsrHNs8UwnVVrAzmYtnvI0q4YFRoHl0hO45BUJhpj4wEBrSEt11HdPrSj9ENeYuMHmDeN8j7I3oZ0YQ8BhI/FSTMKss2tGL8oa0Z0QPCvQHmL+6bAj38xXGmE/mHkgRdP2Cfy9f++ql/p2oA0Fh6zlzDHT5GjR1V4aAgAAAAAAAAB8dN1Ir+WmUJgJmzlUI8WF6f4d7ht9QkK/wnbsSBjBRpJkz7UEmWcwil8CTYmNwaV2Du1lW32sVcgVEJujtpMImqpKqpjfJAV7T+VhZOwOrz4Cxbhy0Uk6AP5Dzt03oE2am+sbNkvl+u4EBJ7nI9+OF5a2fd+T8QZNfFKuK+sbcJ5MDyIH+szPX7DliqrMLVHTov3qzMMT1BnoZIM7oxf94ksHIuS7WwOlsyXxkLuD/r+QiLUacD4bdxSxxR8+5YlhEhEUKbOZvMJCq4o+0LWkNvYGY7Dy4J3MbXY4yzp/VEu7WuifydqNvwfmiHT9Z/o5T4z1ApBoFyXyblXRAJH9FwVy0dLmQP9M13f6Akv1vxEzPg4SaSZcwK1onN8GLnvz2b4OH2bpevfc8pZId4ukEjaMURY9n7GtYyuvIK8rZS9OoNUDYlgULbDKfheL6sYW/j5plzTZOxRDjZIMyX4pAwAAAAAAAADNp9MXV4O0q8K4yJ4rPS4VXwuq4t6hgk27MVZJKGlunsiS1BPzCUsYrjY3HC+M6PP9IYX6/C1lnCIJ0o+cGJbYD19oYWHldkOUktrvzv1YeIT02RPpXgD7krjwyw6GMkhnmRHM08ykLrx+su31lZ/7InhGHjMGkXE2RSSpISmXIKyTFqBiz4VpTggxrfX9+GjlvMNkhfOFIfePNnEkf+68dCkpR1dgZNsp8j7B2L6NTDXn0xlzzOFCPdYIW+khPYsvornwd8zpbJ5MrdNJ83iC52PruW+e81UFB2tsjX/V8hfGsXTW9Himu9kyZl20vhO5ySw0Z75NoEX2FF5w7z1I9BgV2QhCFgzt0zrTY6OZbibtCjzVuhbEcXSt78iQY6P4u2kfGGabhJ8jRzmrTl7fXpjX/NASK+oOTkveW5gvCh+4k9wetZPvkoCJOSj7rPsxrFs4ac13sWE6OK/Vu+yABAAAAAAAAACMUJhFEGPVsytUOFCvzFCerXY67FhmJvjd7P8/Ab3J9VSkSuNkq51kaKlKK1xhWXrj1UgdBRbh1Esks6jgfWyPNinZuBr83cBaOA09aMy5pE4aD3NM9VnTXi/qfIRcTBhYE15K4fr+Qq+1stfqGG79wM9CwprrCpltIbuAoTjySt4mWHDzbROyEl4QNmaCm/GMyIT5joPvINeBDngrSmzV7J1lAC0hj27wLpADHGbGmL21SgKBIka32HJvVbRnN6GCL+Bk7OmUUNnUoKK0c/mlFguIN7DoiyOWdhCHN3YbM23aZIHefz1HkjUMnW4sIP3jF5I9Za+lJLOR+Y80vL/X8P2Ti5wEFqYOTNjg+y6Yz20KF7Z2GCwOTSgIEshFms939zWviwqjq1KT5KeHoyAVUuHXe6ZaI2l9FIhLOlFDZFz/39I24yvRY3ZCp6XhC5X9EbwbXJ7+dwCwQgbzES1ABQAAAAAAAABjqS9H5fcSRNDGyBaFKz79o8xeMIRKzzlT7sFa/bMX3kkyS5iXq/g0JVqbjB2dQajXxf9E3+IOICDQd1ZPSxHDFJSUjQD9vX44uD9ITmVSGxgS7j59qA7RCYXZadRg+TMmfB9Ji2xLITEiD31ZnNzCAjTvf2nYowheYXqzIK2OpXKvcAwtvYZ6jQUXYQN8MbRpugjApKVETYk3uXwnZE3VK8i5NLxjGokDVRdbGjlKV4Whugr31MZOvQrOhpW0vO3C97zgdgI/HyNGJeILnH3Xjn0b47Lzq1r/0FJx/ECc5D2kXvbpgCQLc1b69+hVg1N4H9/qvMCtpYwILqNbk0My1FzSfUupEqsFEcfhiVXz2uM5JaUm5ei0h8S2zmH/eBAz4smlHxG/xtAT3NnLmEUkI1jtSnIquo2kuKrOoU7Dyv2AE0zHGJRLQGwDCzfDasneG68qjdK7WioqemFHYB5GBgAAAAAAAAC67j8goNDnD6JGvBqXvwGS77IyYa56EExRm96UvuqOjz6G8UCwmDpNdjVbd0qr1ViFXTtoozYLJPsM1Q3xmxPxAKRZU70n/FaS1N4p4SPQ/TN7i+a/rph3DHMrpd3RsI7OyYZIRPCCqLdQrpA7xz4lXRaEt9hqN96n4UJvafy3d1CQJss98bv5Y+8je4Wb+bqavGOBnT7XQlrcTPnMTYIsJjE5Wjv8iP1IUuo0C4QsTF6CFGT7dfIf3sQx8O7Coz1dficqvARL6riF3SlIZ9V0K7YWDinCWUSqt9s2jul37QjE/wYvGg7E8lz2yeVpXXjDxl4gh1GzOEn0OWDTc+XzJ/yNCzDeI0Z/52C1rj9hMf2D5/Cl5DJAR4HJsk7nVMV9QT88uOxYi5TWVR2qChpSJ0pULmoPTLAiyW47/g7yfqLPJ+8vhMP2TXv7rcnviSHPo2CGxSBc4ZmhpojK3pY1BwAAAAAAAABopFW8eCE4hGyMUCyvKkQ+GSMhBfrBc6HcBAOIRfpeAPWxOuEEHeBOEdf46B5t7j+RhQ6FEju+knqr9ZyHLaiTGWyc6JkD47GcNMQmldLaVovSYKtB+BESBYjL0jLzGCp58k+YbjuCUpp6iImugZizQSMOYtYVUjbc1DyM3IdHVsohegHZOROewEmAImpFV56O8G+tCcYlnRWQI2cfduj9GjJOGcVqB3xYTcV9WEc1Yrr1JtHSWI4W3r3zo2UbnSpDmpI3fmG2VuvHQLh5h8sFt/1fgrRkI5yZ5eFPHsp1E7LHzoD5y/rVHBYsqKhxKPOgWrUJnXjUrwNJfB1RNWQPzXMz/WJ1OdGvxQOsnVfqLrh7VS9pDPqS+FuJPoX66qT3pdemBFWruc87OG9JPadnG6whSDrCnPiAswOYQL0ArJGPPuHqjtPw/OTL2DDIvOy08OYVQfJiJ2cHEuJaSNrICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK0yKLZ299PNQoSlRD8X8ZYrNuSRswpAskBYSeWXul+1tMEZUZV8b49kLEr2HNayRkD+xtx/xgfuggapnpJBDTBQkCbLPfG7+WPvI3uFm/m6mrxjgZ0+10Ja3Ez5zE2CLOWHabMqG+rx6ic3WkQJWg0ftmTOLdNY5/y/t4wmoZNEDrAev8ntJ1AM1N/JeSctHwkTzJ9mVA1+gAWBEQnhzy2IfCK9h1DTQBasPGa1/xAtrN1z9rAU5xC1HoAir5oZaP/XAVfkgGP8M8l6BQ9/ZAIzv2RsyY2VJMa5K886tW+DmGfMX38Za5O64eJ+YyB0JEXSkPImOCdJi1T+xTn3Vq/O+tTlCMCYuafh2P6xmVX7ArqWdVhQeHEJadNED1BU4PWxOuEEHeBOEdf46B5t7j+RhQ6FEju+knqr9ZyHLaiTCQAAAAAAAAA=", + "base64" + ], + "owner": "cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK", + "executable": false, + "rentEpoch": 18446744073709551615, + "space": 11960 + } +} \ No newline at end of file diff --git a/account-compression/sdk/tests/utils.ts b/account-compression/sdk/tests/utils.ts index c7737120dd9..4e49c305c71 100644 --- a/account-compression/sdk/tests/utils.ts +++ b/account-compression/sdk/tests/utils.ts @@ -2,7 +2,13 @@ import { AnchorProvider } from '@coral-xyz/anchor'; import { Keypair, SendTransactionError, Signer, Transaction, TransactionInstruction } from '@solana/web3.js'; import * as crypto from 'crypto'; -import { createAllocTreeIx, createAppendIx, createInitEmptyMerkleTreeIx, ValidDepthSizePair } from '../src'; +import { + createAllocTreeIx, + createAppendIx, + createInitEmptyMerkleTreeIx, + prepareTreeIx, + ValidDepthSizePair, +} from '../src'; import { MerkleTree } from '../src/merkle-tree'; /// Wait for a transaction of a certain id to confirm and optionally log its messages @@ -115,3 +121,28 @@ export async function createEmptyTreeOnChain( return cmtKeypair; } + +export type PrepareTreeArgs = { + canopyDepth: number; + depthSizePair: ValidDepthSizePair; + payer: Keypair; + provider: AnchorProvider; +}; + +export async function prepareTree(args: PrepareTreeArgs): Promise { + const { provider, payer, depthSizePair, canopyDepth } = args; + const cmtKeypair = Keypair.generate(); + const allocAccountIx = await createAllocTreeIx( + provider.connection, + cmtKeypair.publicKey, + payer.publicKey, + depthSizePair, + canopyDepth, + ); + + const ixs = [allocAccountIx, prepareTreeIx(cmtKeypair.publicKey, payer.publicKey, depthSizePair)]; + + const txId = await execute(provider, ixs, [payer, cmtKeypair]); + await confirmAndLogTx(provider, txId as string); + return cmtKeypair; +} diff --git a/binary-option/program/Cargo.toml b/binary-option/program/Cargo.toml index fc7d4c68d69..001184cd43b 100644 --- a/binary-option/program/Cargo.toml +++ b/binary-option/program/Cargo.toml @@ -16,7 +16,7 @@ spl-token = { version = "6.0", path = "../../token/program", features = [ ] } arrayref = "0.3.8" borsh = "1.5.1" -uint = "0.9" +uint = "0.10" [lib] crate-type = ["cdylib", "lib"] diff --git a/binary-oracle-pair/program/Cargo.toml b/binary-oracle-pair/program/Cargo.toml index 2042a33c970..92579f83566 100644 --- a/binary-oracle-pair/program/Cargo.toml +++ b/binary-oracle-pair/program/Cargo.toml @@ -18,7 +18,7 @@ spl-token = { version = "6.0", path = "../../token/program", features = [ "no-entrypoint", ] } thiserror = "1.0" -uint = "0.9" +uint = "0.10" borsh = "1.5.1" [dev-dependencies] diff --git a/docs/package-lock.json b/docs/package-lock.json index a89dea5153d..b38b7e9722e 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -5646,9 +5646,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -5658,7 +5658,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -5906,12 +5906,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7094,6 +7100,22 @@ "node": ">=10" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -7452,9 +7474,9 @@ } }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "engines": { "node": ">= 0.8" } @@ -7534,6 +7556,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", @@ -8088,36 +8129,36 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -8158,9 +8199,9 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/express/node_modules/range-parser": { "version": "1.2.1", @@ -8379,12 +8420,12 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -8740,9 +8781,12 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/functional-red-black-tree": { "version": "1.0.1", @@ -8777,13 +8821,18 @@ } }, "node_modules/get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8952,6 +9001,17 @@ "node": ">= 4" } }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/got": { "version": "12.6.1", "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", @@ -9041,10 +9101,32 @@ "node": ">=4" } }, - "node_modules/has-symbols": { + "node_modules/has-property-descriptors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "engines": { "node": ">= 0.4" }, @@ -9082,6 +9164,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hast-util-from-parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-6.0.1.tgz", @@ -11712,9 +11805,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -13859,9 +13955,12 @@ } }, "node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -15179,11 +15278,11 @@ "integrity": "sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==" }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -16937,9 +17036,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -16972,6 +17071,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -17079,19 +17186,35 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -17162,13 +17285,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -23317,9 +23444,9 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" }, "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "requires": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -23329,7 +23456,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -23496,12 +23623,15 @@ } }, "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" } }, "callsites": { @@ -24301,6 +24431,16 @@ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==" }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, "define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -24567,9 +24707,9 @@ "integrity": "sha512-dqx7eA9YaqyvYtUhJwT4rC1HIp82j5ybS1/vQ42ur+jBe17dJMwZE4+gvL1XadSFfxaPFFGt3Xsw+Y8akThDlw==" }, "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" }, "enhanced-resolve": { "version": "5.17.1", @@ -24628,6 +24768,19 @@ "unbox-primitive": "^1.0.1" } }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, "es-module-lexer": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", @@ -25023,36 +25176,36 @@ } }, "express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -25087,9 +25240,9 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "range-parser": { "version": "1.2.1", @@ -25253,12 +25406,12 @@ } }, "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "requires": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -25495,9 +25648,9 @@ "optional": true }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "functional-red-black-tree": { "version": "1.0.1", @@ -25526,13 +25679,15 @@ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" }, "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" } }, "get-own-enumerable-property-symbols": { @@ -25653,6 +25808,14 @@ } } }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, "got": { "version": "12.6.1", "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", @@ -25718,10 +25881,23 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, - "has-symbols": { + "has-property-descriptors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" }, "has-tostringtag": { "version": "1.0.0", @@ -25741,6 +25917,14 @@ "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz", "integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==" }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, "hast-util-from-parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-6.0.1.tgz", @@ -27626,9 +27810,9 @@ } }, "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" }, "merge-stream": { "version": "2.0.0", @@ -28820,9 +29004,9 @@ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" }, "object-keys": { "version": "1.1.1", @@ -29697,11 +29881,11 @@ "integrity": "sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==" }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } }, "queue": { @@ -30961,9 +31145,9 @@ } }, "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "requires": { "debug": "2.6.9", "depd": "2.0.0", @@ -30995,6 +31179,11 @@ } } }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -31091,14 +31280,27 @@ } }, "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "requires": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" + } + }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" } }, "setimmediate": { @@ -31153,13 +31355,14 @@ } }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } }, "signal-exit": { diff --git a/docs/src/token-2022.md b/docs/src/token-2022.md index fee446e6be0..21fb3d866b2 100644 --- a/docs/src/token-2022.md +++ b/docs/src/token-2022.md @@ -43,13 +43,14 @@ Token-2022 are a strict superset of Token. ### Instructions Token-2022 supports the exact same instruction layouts as Token, byte for -byte. For example, if you want to transfer 100 tokens on a mint with 2 decimals, -you create a `TransferChecked` instruction, with this byte-represented data: +byte. For example, if you want to transfer 0.75 tokens in UI amount, on a mint with 2 decimals, +then the transfer amount is 75 tokens. You create a `TransferChecked` instruction, with +this byte-represented data: ``` -[12, 100, 0, 0, 0, 0, 0, 0, 0, 2] +[12, 75, 0, 0, 0, 0, 0, 0, 0, 2] ^^ TransferChecked enum - ^^^^^^^^^^^^^^^^^^^^^^^^ 100, as a little-endian 64-bit unsigned integer + ^^^^^^^^^^^^^^^^^^^^^^^^ 75, as a little-endian 64-bit unsigned integer ^ 2, as a byte ``` diff --git a/libraries/concurrent-merkle-tree/Cargo.toml b/libraries/concurrent-merkle-tree/Cargo.toml index 0d0c7490ce5..0c3c91ec723 100644 --- a/libraries/concurrent-merkle-tree/Cargo.toml +++ b/libraries/concurrent-merkle-tree/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spl-concurrent-merkle-tree" -version = "0.3.0" +version = "0.4.0" description = "Solana Program Library Concurrent Merkle Tree" authors = ["Solana Labs Maintainers "] repository = "https://github.com/solana-labs/solana-program-library" diff --git a/libraries/concurrent-merkle-tree/src/node.rs b/libraries/concurrent-merkle-tree/src/node.rs index 876ce61f095..e04613a4f6b 100644 --- a/libraries/concurrent-merkle-tree/src/node.rs +++ b/libraries/concurrent-merkle-tree/src/node.rs @@ -11,7 +11,7 @@ pub fn empty_node(level: u32) -> Node { empty_node_cached::<0>(level, &[]) } -/// Calculates and caches the hash of empty nodes up to level i +/// Calculates the hash of empty nodes up to level i using an existing cache pub fn empty_node_cached(level: u32, cache: &[Node; N]) -> Node { let mut data = EMPTY; if level != 0 { @@ -26,3 +26,20 @@ pub fn empty_node_cached(level: u32, cache: &[Node; N]) -> Node } data } + +/// Calculates and caches the hash of empty nodes up to level i +pub fn empty_node_cached_mut(level: u32, cache: &mut [Node; N]) -> Node { + let mut data = EMPTY; + if level != 0 { + let target = (level - 1) as usize; + let lower_empty = if target < cache.len() && cache[target] != EMPTY { + cache[target] + } else { + empty_node(target as u32) + }; + let hash = hashv(&[lower_empty.as_ref(), lower_empty.as_ref()]); + data.copy_from_slice(hash.as_ref()); + } + cache[level as usize] = data; + data +} diff --git a/libraries/math/Cargo.toml b/libraries/math/Cargo.toml index 0c9d3a7838f..d2ae096756b 100644 --- a/libraries/math/Cargo.toml +++ b/libraries/math/Cargo.toml @@ -17,7 +17,7 @@ num-derive = "0.4" num-traits = "0.2" solana-program = "2.0.3" thiserror = "1.0" -uint = "0.9" +uint = "0.10" [dev-dependencies] proptest = "1.5.0" diff --git a/libraries/math/src/uint.rs b/libraries/math/src/uint.rs index da7adc00d20..43f952fc3b8 100644 --- a/libraries/math/src/uint.rs +++ b/libraries/math/src/uint.rs @@ -5,6 +5,7 @@ #![allow(clippy::assign_op_pattern)] #![allow(clippy::ptr_offset_with_cast)] #![allow(clippy::manual_range_contains)] +#![allow(missing_docs)] use uint::construct_uint; diff --git a/libraries/type-length-value/js/package.json b/libraries/type-length-value/js/package.json index b8ea3af1fe3..7f1d43e65a5 100644 --- a/libraries/type-length-value/js/package.json +++ b/libraries/type-length-value/js/package.json @@ -47,10 +47,10 @@ }, "devDependencies": { "@types/chai": "^4.3.19", - "@types/mocha": "^10.0.7", + "@types/mocha": "^10.0.8", "@types/node": "^22.5.4", - "@typescript-eslint/eslint-plugin": "^8.5.0", - "@typescript-eslint/parser": "^8.5.0", + "@typescript-eslint/eslint-plugin": "^8.4.0", + "@typescript-eslint/parser": "^8.4.0", "chai": "^5.1.1", "eslint": "^8.57.0", "eslint-plugin-require-extensions": "^0.1.1", diff --git a/memo/js/package.json b/memo/js/package.json index 0c90eb61425..bd54b379420 100644 --- a/memo/js/package.json +++ b/memo/js/package.json @@ -53,11 +53,11 @@ "devDependencies": { "@solana/web3.js": "^1.95.3", "@types/chai": "^4.3.19", - "@types/jest": "^29.5.12", + "@types/jest": "^29.5.13", "@types/node": "^22.5.4", "@types/node-fetch": "^2.6.11", - "@typescript-eslint/eslint-plugin": "^8.5.0", - "@typescript-eslint/parser": "^8.5.0", + "@typescript-eslint/eslint-plugin": "^8.4.0", + "@typescript-eslint/parser": "^8.4.0", "chai": "^5.1.1", "eslint": "^8.57.0", "eslint-plugin-require-extensions": "^0.1.1", diff --git a/name-service/js/package.json b/name-service/js/package.json index b3a9c82c838..a11f2352e10 100644 --- a/name-service/js/package.json +++ b/name-service/js/package.json @@ -44,10 +44,10 @@ "devDependencies": { "@jest/globals": "^29.7.0", "@types/bn.js": "^5.1.1", - "@types/jest": "^29.5.12", + "@types/jest": "^29.5.13", "@types/node": "^22.5.4", - "@typescript-eslint/eslint-plugin": "^8.5.0", - "@typescript-eslint/parser": "^8.5.0", + "@typescript-eslint/eslint-plugin": "^8.4.0", + "@typescript-eslint/parser": "^8.4.0", "eslint": "^8.57.0", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-functional": "^7.0.2", diff --git a/package.json b/package.json index dae3414b3dd..26b0cc1730c 100644 --- a/package.json +++ b/package.json @@ -25,10 +25,10 @@ "@solana/eslint-config-solana": "^3.0.3", "@solana/prettier-config-solana": "^0.0.5", "eslint-config-prettier": "^9.1.0", - "eslint-config-turbo": "^2.1.1", + "eslint-config-turbo": "^2.1.2", "eslint-plugin-prettier": "^5.2.1", "prettier": "^3.3.3", - "turbo": "^2.1.1" + "turbo": "^2.1.2" }, "engines": { "node": ">=14.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85704d2c1cf..b0328fb487d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^9.1.0 version: 9.1.0(eslint@8.57.0) eslint-config-turbo: - specifier: ^2.1.1 - version: 2.1.1(eslint@8.57.0) + specifier: ^2.1.2 + version: 2.1.2(eslint@8.57.0) eslint-plugin-prettier: specifier: ^5.2.1 version: 5.2.1(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.3.3) @@ -27,8 +27,8 @@ importers: specifier: ^3.3.3 version: 3.3.3 turbo: - specifier: ^2.1.1 - version: 2.1.1 + specifier: ^2.1.2 + version: 2.1.2 account-compression/sdk: dependencies: @@ -62,13 +62,13 @@ importers: version: 0.20.1 '@solana/eslint-config-solana': specifier: ^3.0.3 - version: 3.0.3(@typescript-eslint/eslint-plugin@8.5.0)(@typescript-eslint/parser@8.5.0)(eslint-plugin-jest@28.8.3)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-simple-import-sort@12.1.1)(eslint-plugin-sort-keys-fix@1.1.2)(eslint-plugin-typescript-sort-keys@3.2.0)(eslint@8.57.0)(typescript@5.6.2) + version: 3.0.3(@typescript-eslint/eslint-plugin@8.4.0)(@typescript-eslint/parser@8.4.0)(eslint-plugin-jest@28.8.3)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-simple-import-sort@12.1.1)(eslint-plugin-sort-keys-fix@1.1.2)(eslint-plugin-typescript-sort-keys@3.2.0)(eslint@8.57.0)(typescript@5.6.2) '@types/bn.js': specifier: ^5.1.0 version: 5.1.5 '@types/jest': - specifier: ^29.5.12 - version: 29.5.12 + specifier: ^29.5.13 + version: 29.5.13 '@types/node': specifier: ^22.5.4 version: 22.5.4 @@ -76,23 +76,23 @@ importers: specifier: ^2.6.11 version: 2.6.11 '@typescript-eslint/eslint-plugin': - specifier: ^8.5.0 - version: 8.5.0(@typescript-eslint/parser@8.5.0)(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.6.2) '@typescript-eslint/parser': - specifier: ^8.5.0 - version: 8.5.0(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(eslint@8.57.0)(typescript@5.6.2) eslint: specifier: ^8.57.0 version: 8.57.0 eslint-config-turbo: - specifier: ^2.1.1 - version: 2.1.1(eslint@8.57.0) + specifier: ^2.1.2 + version: 2.1.2(eslint@8.57.0) eslint-plugin-import: specifier: ^2.30.0 - version: 2.30.0(@typescript-eslint/parser@8.5.0)(eslint@8.57.0) + version: 2.30.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0) eslint-plugin-jest: specifier: ^28.8.3 - version: 28.8.3(@typescript-eslint/eslint-plugin@8.5.0)(eslint@8.57.0)(jest@29.7.0)(typescript@5.6.2) + version: 28.8.3(@typescript-eslint/eslint-plugin@8.4.0)(eslint@8.57.0)(jest@29.7.0)(typescript@5.6.2) eslint-plugin-mocha: specifier: ^10.5.0 version: 10.5.0(eslint@8.57.0) @@ -140,17 +140,17 @@ importers: specifier: ^4.3.19 version: 4.3.19 '@types/mocha': - specifier: ^10.0.7 - version: 10.0.7 + specifier: ^10.0.8 + version: 10.0.8 '@types/node': specifier: ^22.5.4 version: 22.5.4 '@typescript-eslint/eslint-plugin': - specifier: ^8.5.0 - version: 8.5.0(@typescript-eslint/parser@8.5.0)(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.6.2) '@typescript-eslint/parser': - specifier: ^8.5.0 - version: 8.5.0(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(eslint@8.57.0)(typescript@5.6.2) chai: specifier: ^5.1.1 version: 5.1.1 @@ -192,8 +192,8 @@ importers: specifier: ^4.3.19 version: 4.3.19 '@types/jest': - specifier: ^29.5.12 - version: 29.5.12 + specifier: ^29.5.13 + version: 29.5.13 '@types/node': specifier: ^22.5.4 version: 22.5.4 @@ -201,11 +201,11 @@ importers: specifier: ^2.6.11 version: 2.6.11 '@typescript-eslint/eslint-plugin': - specifier: ^8.5.0 - version: 8.5.0(@typescript-eslint/parser@8.5.0)(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.6.2) '@typescript-eslint/parser': - specifier: ^8.5.0 - version: 8.5.0(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(eslint@8.57.0)(typescript@5.6.2) chai: specifier: ^5.1.1 version: 5.1.1 @@ -262,17 +262,17 @@ importers: specifier: ^5.1.1 version: 5.1.5 '@types/jest': - specifier: ^29.5.12 - version: 29.5.12 + specifier: ^29.5.13 + version: 29.5.13 '@types/node': specifier: ^22.5.4 version: 22.5.4 '@typescript-eslint/eslint-plugin': - specifier: ^8.5.0 - version: 8.5.0(@typescript-eslint/parser@8.5.0)(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.6.2) '@typescript-eslint/parser': - specifier: ^8.5.0 - version: 8.5.0(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(eslint@8.57.0)(typescript@5.6.2) eslint: specifier: ^8.57.0 version: 8.57.0 @@ -284,7 +284,7 @@ importers: version: 7.0.2(eslint@8.57.0)(typescript@5.6.2) eslint-plugin-import: specifier: ^2.30.0 - version: 2.30.0(@typescript-eslint/parser@8.5.0)(eslint@8.57.0) + version: 2.30.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0) jest: specifier: ^29.7.0 version: 29.7.0(@types/node@22.5.4)(ts-node@10.9.2) @@ -323,8 +323,8 @@ importers: specifier: ^22.5.4 version: 22.5.4 '@typescript-eslint/eslint-plugin': - specifier: ^8.5.0 - version: 8.5.0(@typescript-eslint/parser@8.5.0)(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.6.2) ava: specifier: ^6.1.3 version: 6.1.3(@ava/typescript@5.0.0) @@ -335,8 +335,8 @@ importers: specifier: ^0.2.0 version: 0.2.0 tsx: - specifier: ^4.19.0 - version: 4.19.0 + specifier: ^4.19.1 + version: 4.19.1 typescript: specifier: ^5.6.2 version: 5.6.2 @@ -357,8 +357,8 @@ importers: specifier: ^22.5.4 version: 22.5.4 '@typescript-eslint/eslint-plugin': - specifier: ^8.5.0 - version: 8.5.0(@typescript-eslint/parser@8.5.0)(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.6.2) eslint: specifier: ^8.57.0 version: 8.57.0 @@ -392,31 +392,31 @@ importers: devDependencies: '@rollup/plugin-alias': specifier: ^5.1.0 - version: 5.1.0(rollup@4.21.2) + version: 5.1.0(rollup@4.21.3) '@rollup/plugin-commonjs': specifier: ^26.0.1 - version: 26.0.1(rollup@4.21.2) + version: 26.0.1(rollup@4.21.3) '@rollup/plugin-json': specifier: ^6.1.0 - version: 6.1.0(rollup@4.21.2) + version: 6.1.0(rollup@4.21.3) '@rollup/plugin-multi-entry': specifier: ^6.0.0 - version: 6.0.1(rollup@4.21.2) + version: 6.0.1(rollup@4.21.3) '@rollup/plugin-node-resolve': specifier: ^15.0.2 - version: 15.2.3(rollup@4.21.2) + version: 15.2.3(rollup@4.21.3) '@rollup/plugin-terser': specifier: ^0.4.4 - version: 0.4.4(rollup@4.21.2) + version: 0.4.4(rollup@4.21.3) '@rollup/plugin-typescript': specifier: ^11.1.6 - version: 11.1.6(rollup@4.21.2)(tslib@2.7.0)(typescript@5.6.2) + version: 11.1.6(rollup@4.21.3)(tslib@2.7.0)(typescript@5.6.2) '@types/bn.js': specifier: ^5.1.0 version: 5.1.5 '@types/jest': - specifier: ^29.5.12 - version: 29.5.12 + specifier: ^29.5.13 + version: 29.5.13 '@types/node': specifier: ^22.5.4 version: 22.5.4 @@ -424,11 +424,11 @@ importers: specifier: ^2.6.11 version: 2.6.11 '@typescript-eslint/eslint-plugin': - specifier: ^8.5.0 - version: 8.5.0(@typescript-eslint/parser@8.5.0)(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.6.2) '@typescript-eslint/parser': - specifier: ^8.5.0 - version: 8.5.0(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(eslint@8.57.0)(typescript@5.6.2) cross-env: specifier: ^7.0.3 version: 7.0.3 @@ -442,11 +442,11 @@ importers: specifier: ^6.0.1 version: 6.0.1 rollup: - specifier: ^4.21.2 - version: 4.21.2 + specifier: ^4.21.3 + version: 4.21.3 rollup-plugin-dts: specifier: ^6.1.1 - version: 6.1.1(rollup@4.21.2)(typescript@5.6.2) + version: 6.1.1(rollup@4.21.3)(typescript@5.6.2) ts-jest: specifier: ^29.2.5 version: 29.2.5(@babel/core@7.23.2)(jest@29.7.0)(typescript@5.6.2) @@ -470,17 +470,17 @@ importers: specifier: ^4.3.19 version: 4.3.19 '@types/mocha': - specifier: ^10.0.7 - version: 10.0.7 + specifier: ^10.0.8 + version: 10.0.8 '@types/node': specifier: ^22.5.4 version: 22.5.4 '@typescript-eslint/eslint-plugin': - specifier: ^8.5.0 - version: 8.5.0(@typescript-eslint/parser@8.5.0)(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.6.2) '@typescript-eslint/parser': - specifier: ^8.5.0 - version: 8.5.0(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(eslint@8.57.0)(typescript@5.6.2) chai: specifier: ^5.1.1 version: 5.1.1 @@ -529,13 +529,13 @@ importers: devDependencies: '@rollup/plugin-commonjs': specifier: ^26.0.1 - version: 26.0.1(rollup@4.21.2) + version: 26.0.1(rollup@4.21.3) '@rollup/plugin-node-resolve': specifier: ^15.0.2 - version: 15.2.3(rollup@4.21.2) + version: 15.2.3(rollup@4.21.3) '@rollup/plugin-typescript': specifier: ^11.1.6 - version: 11.1.6(rollup@4.21.2)(tslib@2.7.0)(typescript@5.6.2) + version: 11.1.6(rollup@4.21.3)(tslib@2.7.0)(typescript@5.6.2) '@solana/spl-token': specifier: 0.4.6 version: 0.4.6(@solana/web3.js@1.95.3)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.2) @@ -549,11 +549,11 @@ importers: specifier: ^22.5.4 version: 22.5.4 '@typescript-eslint/eslint-plugin': - specifier: ^8.5.0 - version: 8.5.0(@typescript-eslint/parser@8.5.0)(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.6.2) '@typescript-eslint/parser': - specifier: ^8.5.0 - version: 8.5.0(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(eslint@8.57.0)(typescript@5.6.2) eslint: specifier: ^8.57.0 version: 8.57.0 @@ -561,8 +561,8 @@ importers: specifier: ^6.1.1 version: 6.1.1 rollup: - specifier: ^4.21.2 - version: 4.21.2 + specifier: ^4.21.3 + version: 4.21.3 ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.5.4)(typescript@5.6.2) @@ -592,17 +592,17 @@ importers: specifier: ^4.3.19 version: 4.3.19 '@types/mocha': - specifier: ^10.0.7 - version: 10.0.7 + specifier: ^10.0.8 + version: 10.0.8 '@types/node': specifier: ^22.5.4 version: 22.5.4 '@typescript-eslint/eslint-plugin': - specifier: ^8.5.0 - version: 8.5.0(@typescript-eslint/parser@8.5.0)(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.6.2) '@typescript-eslint/parser': - specifier: ^8.5.0 - version: 8.5.0(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(eslint@8.57.0)(typescript@5.6.2) chai: specifier: ^5.1.1 version: 5.1.1 @@ -659,20 +659,20 @@ importers: specifier: ^8.0.0 version: 8.0.0 '@types/mocha': - specifier: ^10.0.7 - version: 10.0.7 + specifier: ^10.0.8 + version: 10.0.8 '@typescript-eslint/eslint-plugin': - specifier: ^8.5.0 - version: 8.5.0(@typescript-eslint/parser@8.5.0)(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.6.2) '@typescript-eslint/parser': - specifier: ^8.5.0 - version: 8.5.0(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(eslint@8.57.0)(typescript@5.6.2) eslint: specifier: ^8.57.0 version: 8.57.0 eslint-plugin-import: specifier: ^2.30.0 - version: 2.30.0(@typescript-eslint/parser@8.5.0)(eslint@8.57.0) + version: 2.30.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0) eslint-plugin-require-extensions: specifier: ^0.1.1 version: 0.1.3(eslint@8.57.0) @@ -723,8 +723,8 @@ importers: specifier: ^8.0.0 version: 8.0.0 '@types/mocha': - specifier: ^10.0.7 - version: 10.0.7 + specifier: ^10.0.8 + version: 10.0.8 '@types/node': specifier: ^22.5.4 version: 22.5.4 @@ -732,11 +732,11 @@ importers: specifier: ^2.6.11 version: 2.6.11 '@typescript-eslint/eslint-plugin': - specifier: ^8.5.0 - version: 8.5.0(@typescript-eslint/parser@8.5.0)(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.6.2) '@typescript-eslint/parser': - specifier: ^8.5.0 - version: 8.5.0(eslint@8.57.0)(typescript@5.6.2) + specifier: ^8.4.0 + version: 8.4.0(eslint@8.57.0)(typescript@5.6.2) chai: specifier: ^5.1.1 version: 5.1.1 @@ -1881,7 +1881,7 @@ packages: engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} dev: true - /@rollup/plugin-alias@5.1.0(rollup@4.21.2): + /@rollup/plugin-alias@5.1.0(rollup@4.21.3): resolution: {integrity: sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ==} engines: {node: '>=14.0.0'} peerDependencies: @@ -1890,11 +1890,11 @@ packages: rollup: optional: true dependencies: - rollup: 4.21.2 + rollup: 4.21.3 slash: 4.0.0 dev: true - /@rollup/plugin-commonjs@26.0.1(rollup@4.21.2): + /@rollup/plugin-commonjs@26.0.1(rollup@4.21.3): resolution: {integrity: sha512-UnsKoZK6/aGIH6AdkptXhNvhaqftcjq3zZdT+LY5Ftms6JR06nADcDsYp5hTU9E2lbJUEOhdlY5J4DNTneM+jQ==} engines: {node: '>=16.0.0 || 14 >= 14.17'} peerDependencies: @@ -1903,16 +1903,16 @@ packages: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.21.2) + '@rollup/pluginutils': 5.1.0(rollup@4.21.3) commondir: 1.0.1 estree-walker: 2.0.2 glob: 10.4.1 is-reference: 1.2.1 magic-string: 0.30.10 - rollup: 4.21.2 + rollup: 4.21.3 dev: true - /@rollup/plugin-json@6.1.0(rollup@4.21.2): + /@rollup/plugin-json@6.1.0(rollup@4.21.3): resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -1921,11 +1921,11 @@ packages: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.21.2) - rollup: 4.21.2 + '@rollup/pluginutils': 5.1.0(rollup@4.21.3) + rollup: 4.21.3 dev: true - /@rollup/plugin-multi-entry@6.0.1(rollup@4.21.2): + /@rollup/plugin-multi-entry@6.0.1(rollup@4.21.3): resolution: {integrity: sha512-AXm6toPyTSfbYZWghQGbom1Uh7dHXlrGa+HoiYNhQtDUE3Q7LqoUYdVQx9E1579QWS1uOiu+cZRSE4okO7ySgw==} engines: {node: '>=14.0.0'} peerDependencies: @@ -1934,12 +1934,12 @@ packages: rollup: optional: true dependencies: - '@rollup/plugin-virtual': 3.0.2(rollup@4.21.2) + '@rollup/plugin-virtual': 3.0.2(rollup@4.21.3) matched: 5.0.1 - rollup: 4.21.2 + rollup: 4.21.3 dev: true - /@rollup/plugin-node-resolve@15.2.3(rollup@4.21.2): + /@rollup/plugin-node-resolve@15.2.3(rollup@4.21.3): resolution: {integrity: sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==} engines: {node: '>=14.0.0'} peerDependencies: @@ -1948,16 +1948,16 @@ packages: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.0.5(rollup@4.21.2) + '@rollup/pluginutils': 5.0.5(rollup@4.21.3) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-builtin-module: 3.2.1 is-module: 1.0.0 resolve: 1.22.8 - rollup: 4.21.2 + rollup: 4.21.3 dev: true - /@rollup/plugin-terser@0.4.4(rollup@4.21.2): + /@rollup/plugin-terser@0.4.4(rollup@4.21.3): resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} engines: {node: '>=14.0.0'} peerDependencies: @@ -1966,13 +1966,13 @@ packages: rollup: optional: true dependencies: - rollup: 4.21.2 + rollup: 4.21.3 serialize-javascript: 6.0.1 smob: 1.4.1 terser: 5.24.0 dev: true - /@rollup/plugin-typescript@11.1.6(rollup@4.21.2)(tslib@2.7.0)(typescript@5.6.2): + /@rollup/plugin-typescript@11.1.6(rollup@4.21.3)(tslib@2.7.0)(typescript@5.6.2): resolution: {integrity: sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -1985,14 +1985,14 @@ packages: tslib: optional: true dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.21.2) + '@rollup/pluginutils': 5.1.0(rollup@4.21.3) resolve: 1.22.8 - rollup: 4.21.2 + rollup: 4.21.3 tslib: 2.7.0 typescript: 5.6.2 dev: true - /@rollup/plugin-virtual@3.0.2(rollup@4.21.2): + /@rollup/plugin-virtual@3.0.2(rollup@4.21.3): resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==} engines: {node: '>=14.0.0'} peerDependencies: @@ -2001,7 +2001,7 @@ packages: rollup: optional: true dependencies: - rollup: 4.21.2 + rollup: 4.21.3 dev: true /@rollup/pluginutils@4.2.1: @@ -2012,7 +2012,7 @@ packages: picomatch: 2.3.1 dev: true - /@rollup/pluginutils@5.0.5(rollup@4.21.2): + /@rollup/pluginutils@5.0.5(rollup@4.21.3): resolution: {integrity: sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==} engines: {node: '>=14.0.0'} peerDependencies: @@ -2024,10 +2024,10 @@ packages: '@types/estree': 1.0.5 estree-walker: 2.0.2 picomatch: 2.3.1 - rollup: 4.21.2 + rollup: 4.21.3 dev: true - /@rollup/pluginutils@5.1.0(rollup@4.21.2): + /@rollup/pluginutils@5.1.0(rollup@4.21.3): resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} engines: {node: '>=14.0.0'} peerDependencies: @@ -2039,131 +2039,131 @@ packages: '@types/estree': 1.0.5 estree-walker: 2.0.2 picomatch: 2.3.1 - rollup: 4.21.2 + rollup: 4.21.3 dev: true - /@rollup/rollup-android-arm-eabi@4.21.2: - resolution: {integrity: sha512-fSuPrt0ZO8uXeS+xP3b+yYTCBUd05MoSp2N/MFOgjhhUhMmchXlpTQrTpI8T+YAwAQuK7MafsCOxW7VrPMrJcg==} + /@rollup/rollup-android-arm-eabi@4.21.3: + resolution: {integrity: sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==} cpu: [arm] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-android-arm64@4.21.2: - resolution: {integrity: sha512-xGU5ZQmPlsjQS6tzTTGwMsnKUtu0WVbl0hYpTPauvbRAnmIvpInhJtgjj3mcuJpEiuUw4v1s4BimkdfDWlh7gA==} + /@rollup/rollup-android-arm64@4.21.3: + resolution: {integrity: sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==} cpu: [arm64] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-arm64@4.21.2: - resolution: {integrity: sha512-99AhQ3/ZMxU7jw34Sq8brzXqWH/bMnf7ZVhvLk9QU2cOepbQSVTns6qoErJmSiAvU3InRqC2RRZ5ovh1KN0d0Q==} + /@rollup/rollup-darwin-arm64@4.21.3: + resolution: {integrity: sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-x64@4.21.2: - resolution: {integrity: sha512-ZbRaUvw2iN/y37x6dY50D8m2BnDbBjlnMPotDi/qITMJ4sIxNY33HArjikDyakhSv0+ybdUxhWxE6kTI4oX26w==} + /@rollup/rollup-darwin-x64@4.21.3: + resolution: {integrity: sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.21.2: - resolution: {integrity: sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w==} + /@rollup/rollup-linux-arm-gnueabihf@4.21.3: + resolution: {integrity: sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g==} cpu: [arm] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm-musleabihf@4.21.2: - resolution: {integrity: sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w==} + /@rollup/rollup-linux-arm-musleabihf@4.21.3: + resolution: {integrity: sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA==} cpu: [arm] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-gnu@4.21.2: - resolution: {integrity: sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==} + /@rollup/rollup-linux-arm64-gnu@4.21.3: + resolution: {integrity: sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-musl@4.21.2: - resolution: {integrity: sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w==} + /@rollup/rollup-linux-arm64-musl@4.21.3: + resolution: {integrity: sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-powerpc64le-gnu@4.21.2: - resolution: {integrity: sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ==} + /@rollup/rollup-linux-powerpc64le-gnu@4.21.3: + resolution: {integrity: sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==} cpu: [ppc64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-riscv64-gnu@4.21.2: - resolution: {integrity: sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg==} + /@rollup/rollup-linux-riscv64-gnu@4.21.3: + resolution: {integrity: sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==} cpu: [riscv64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-s390x-gnu@4.21.2: - resolution: {integrity: sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ==} + /@rollup/rollup-linux-s390x-gnu@4.21.3: + resolution: {integrity: sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==} cpu: [s390x] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-gnu@4.21.2: - resolution: {integrity: sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==} + /@rollup/rollup-linux-x64-gnu@4.21.3: + resolution: {integrity: sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-musl@4.21.2: - resolution: {integrity: sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==} + /@rollup/rollup-linux-x64-musl@4.21.3: + resolution: {integrity: sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-arm64-msvc@4.21.2: - resolution: {integrity: sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ==} + /@rollup/rollup-win32-arm64-msvc@4.21.3: + resolution: {integrity: sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-ia32-msvc@4.21.2: - resolution: {integrity: sha512-5rA4vjlqgrpbFVVHX3qkrCo/fZTj1q0Xxpg+Z7yIo3J2AilW7t2+n6Q8Jrx+4MrYpAnjttTYF8rr7bP46BPzRw==} + /@rollup/rollup-win32-ia32-msvc@4.21.3: + resolution: {integrity: sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==} cpu: [ia32] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-x64-msvc@4.21.2: - resolution: {integrity: sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA==} + /@rollup/rollup-win32-x64-msvc@4.21.3: + resolution: {integrity: sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==} cpu: [x64] os: [win32] requiresBuild: true @@ -2395,7 +2395,7 @@ packages: typescript: 5.6.2 dev: true - /@solana/eslint-config-solana@3.0.3(@typescript-eslint/eslint-plugin@8.5.0)(@typescript-eslint/parser@8.5.0)(eslint-plugin-jest@28.8.3)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-simple-import-sort@12.1.1)(eslint-plugin-sort-keys-fix@1.1.2)(eslint-plugin-typescript-sort-keys@3.2.0)(eslint@8.57.0)(typescript@5.6.2): + /@solana/eslint-config-solana@3.0.3(@typescript-eslint/eslint-plugin@8.4.0)(@typescript-eslint/parser@8.4.0)(eslint-plugin-jest@28.8.3)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-simple-import-sort@12.1.1)(eslint-plugin-sort-keys-fix@1.1.2)(eslint-plugin-typescript-sort-keys@3.2.0)(eslint@8.57.0)(typescript@5.6.2): resolution: {integrity: sha512-yTaeCbOBwjmK4oUkknixOpwOzzAK8+4YWvJEJFNHuueESetieDnAeEHV7rzJllFgHEWa9nXps9Q3aD4/XJp71A==} peerDependencies: '@typescript-eslint/eslint-plugin': ^6.0.0 @@ -2408,14 +2408,14 @@ packages: eslint-plugin-typescript-sort-keys: ^3.2.0 typescript: ^5.1.6 dependencies: - '@typescript-eslint/eslint-plugin': 8.5.0(@typescript-eslint/parser@8.5.0)(eslint@8.57.0)(typescript@5.6.2) - '@typescript-eslint/parser': 8.5.0(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/eslint-plugin': 8.4.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/parser': 8.4.0(eslint@8.57.0)(typescript@5.6.2) eslint: 8.57.0 - eslint-plugin-jest: 28.8.3(@typescript-eslint/eslint-plugin@8.5.0)(eslint@8.57.0)(jest@29.7.0)(typescript@5.6.2) + eslint-plugin-jest: 28.8.3(@typescript-eslint/eslint-plugin@8.4.0)(eslint@8.57.0)(jest@29.7.0)(typescript@5.6.2) eslint-plugin-react-hooks: 4.6.0(eslint@8.57.0) eslint-plugin-simple-import-sort: 12.1.1(eslint@8.57.0) eslint-plugin-sort-keys-fix: 1.1.2 - eslint-plugin-typescript-sort-keys: 3.2.0(@typescript-eslint/parser@8.5.0)(eslint@8.57.0)(typescript@5.6.2) + eslint-plugin-typescript-sort-keys: 3.2.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.6.2) typescript: 5.6.2 dev: true @@ -2703,8 +2703,8 @@ packages: '@types/istanbul-lib-report': 3.0.3 dev: true - /@types/jest@29.5.12: - resolution: {integrity: sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==} + /@types/jest@29.5.13: + resolution: {integrity: sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==} dependencies: expect: 29.7.0 pretty-format: 29.7.0 @@ -2718,8 +2718,8 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true - /@types/mocha@10.0.7: - resolution: {integrity: sha512-GN8yJ1mNTcFcah/wKEFIJckJx9iJLoMSzWcfRRuxz/Jk+U6KQNnml+etbtxFK8lPjzOw3zp4Ha/kjSst9fsHYw==} + /@types/mocha@10.0.8: + resolution: {integrity: sha512-HfMcUmy9hTMJh66VNcmeC9iVErIZJli2bszuXc6julh5YGuRb/W5OnkHjwLNYdFlMis0sY3If5SEAp+PktdJjw==} dev: true /@types/node-fetch@2.6.11: @@ -2805,8 +2805,8 @@ packages: - supports-color dev: true - /@typescript-eslint/eslint-plugin@8.5.0(@typescript-eslint/parser@8.5.0)(eslint@8.57.0)(typescript@5.6.2): - resolution: {integrity: sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==} + /@typescript-eslint/eslint-plugin@8.4.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.6.2): + resolution: {integrity: sha512-rg8LGdv7ri3oAlenMACk9e+AR4wUV0yrrG+XKsGKOK0EVgeEDqurkXMPILG2836fW4ibokTB5v4b6Z9+GYQDEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 @@ -2817,11 +2817,11 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 8.5.0(eslint@8.57.0)(typescript@5.6.2) - '@typescript-eslint/scope-manager': 8.5.0 - '@typescript-eslint/type-utils': 8.5.0(eslint@8.57.0)(typescript@5.6.2) - '@typescript-eslint/utils': 8.5.0(eslint@8.57.0)(typescript@5.6.2) - '@typescript-eslint/visitor-keys': 8.5.0 + '@typescript-eslint/parser': 8.4.0(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/scope-manager': 8.4.0 + '@typescript-eslint/type-utils': 8.4.0(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/utils': 8.4.0(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/visitor-keys': 8.4.0 eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.1 @@ -2866,8 +2866,8 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@8.5.0(eslint@8.57.0)(typescript@5.6.2): - resolution: {integrity: sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==} + /@typescript-eslint/parser@8.4.0(eslint@8.57.0)(typescript@5.6.2): + resolution: {integrity: sha512-NHgWmKSgJk5K9N16GIhQ4jSobBoJwrmURaLErad0qlLjrpP5bECYg+wxVTGlGZmJbU03jj/dfnb6V9bw+5icsA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -2876,11 +2876,11 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 8.5.0 - '@typescript-eslint/types': 8.5.0 - '@typescript-eslint/typescript-estree': 8.5.0(typescript@5.6.2) - '@typescript-eslint/visitor-keys': 8.5.0 - debug: 4.3.7 + '@typescript-eslint/scope-manager': 8.4.0 + '@typescript-eslint/types': 8.4.0 + '@typescript-eslint/typescript-estree': 8.4.0(typescript@5.6.2) + '@typescript-eslint/visitor-keys': 8.4.0 + debug: 4.3.6(supports-color@8.1.1) eslint: 8.57.0 typescript: 5.6.2 transitivePeerDependencies: @@ -2919,14 +2919,6 @@ packages: '@typescript-eslint/visitor-keys': 8.4.0 dev: true - /@typescript-eslint/scope-manager@8.5.0: - resolution: {integrity: sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dependencies: - '@typescript-eslint/types': 8.5.0 - '@typescript-eslint/visitor-keys': 8.5.0 - dev: true - /@typescript-eslint/type-utils@6.21.0(eslint@8.57.0)(typescript@5.6.2): resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} engines: {node: ^16.0.0 || >=18.0.0} @@ -2947,8 +2939,8 @@ packages: - supports-color dev: true - /@typescript-eslint/type-utils@8.5.0(eslint@8.57.0)(typescript@5.6.2): - resolution: {integrity: sha512-N1K8Ix+lUM+cIDhL2uekVn/ZD7TZW+9/rwz8DclQpcQ9rk4sIL5CAlBC0CugWKREmDjBzI/kQqU4wkg46jWLYA==} + /@typescript-eslint/type-utils@8.4.0(eslint@8.57.0)(typescript@5.6.2): + resolution: {integrity: sha512-pu2PAmNrl9KX6TtirVOrbLPLwDmASpZhK/XU7WvoKoCUkdtq9zF7qQ7gna0GBZFN0hci0vHaSusiL2WpsQk37A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -2956,8 +2948,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 8.5.0(typescript@5.6.2) - '@typescript-eslint/utils': 8.5.0(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/typescript-estree': 8.4.0(typescript@5.6.2) + '@typescript-eslint/utils': 8.4.0(eslint@8.57.0)(typescript@5.6.2) debug: 4.3.7 ts-api-utils: 1.3.0(typescript@5.6.2) typescript: 5.6.2 @@ -2986,11 +2978,6 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dev: true - /@typescript-eslint/types@8.5.0: - resolution: {integrity: sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dev: true - /@typescript-eslint/typescript-estree@5.62.0(typescript@5.6.2): resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3078,28 +3065,6 @@ packages: - supports-color dev: true - /@typescript-eslint/typescript-estree@8.5.0(typescript@5.6.2): - resolution: {integrity: sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/types': 8.5.0 - '@typescript-eslint/visitor-keys': 8.5.0 - debug: 4.3.7 - fast-glob: 3.3.2 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.6.3 - ts-api-utils: 1.3.0(typescript@5.6.2) - typescript: 5.6.2 - transitivePeerDependencies: - - supports-color - dev: true - /@typescript-eslint/utils@5.62.0(eslint@8.57.0)(typescript@5.6.2): resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3171,22 +3136,6 @@ packages: - typescript dev: true - /@typescript-eslint/utils@8.5.0(eslint@8.57.0)(typescript@5.6.2): - resolution: {integrity: sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@typescript-eslint/scope-manager': 8.5.0 - '@typescript-eslint/types': 8.5.0 - '@typescript-eslint/typescript-estree': 8.5.0(typescript@5.6.2) - eslint: 8.57.0 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - /@typescript-eslint/visitor-keys@5.62.0: resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3219,14 +3168,6 @@ packages: eslint-visitor-keys: 3.4.3 dev: true - /@typescript-eslint/visitor-keys@8.5.0: - resolution: {integrity: sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dependencies: - '@typescript-eslint/types': 8.5.0 - eslint-visitor-keys: 3.4.3 - dev: true - /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true @@ -4591,13 +4532,13 @@ packages: eslint: 8.57.0 dev: true - /eslint-config-turbo@2.1.1(eslint@8.57.0): - resolution: {integrity: sha512-JJF8SZErmgKCGkt124WUmTt0sQ5YLvPo2YxDsfzn9avGJC7/BQIa+3FZoDb3zeYYsZx91pZ6htQAJaKK8NQQAg==} + /eslint-config-turbo@2.1.2(eslint@8.57.0): + resolution: {integrity: sha512-UCNwxBrTOx0K41h1OrwMg7vPdGvcGSAlj40ZzpuUi0S2Muac2UOs+6F2dMYQiKg7lX2HAtyHXlF0T2wlWNHjGg==} peerDependencies: eslint: '>6.6.0' dependencies: eslint: 8.57.0 - eslint-plugin-turbo: 2.1.1(eslint@8.57.0) + eslint-plugin-turbo: 2.1.2(eslint@8.57.0) dev: true /eslint-import-resolver-node@0.3.9: @@ -4610,7 +4551,7 @@ packages: - supports-color dev: true - /eslint-module-utils@2.9.0(@typescript-eslint/parser@8.5.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0): + /eslint-module-utils@2.9.0(@typescript-eslint/parser@8.4.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0): resolution: {integrity: sha512-McVbYmwA3NEKwRQY5g4aWMdcZE5xZxV8i8l7CqJSrameuGSQJtSWaL/LxTEzSKKaCcOhlpDR8XEfYXWPrdo/ZQ==} engines: {node: '>=4'} peerDependencies: @@ -4631,7 +4572,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 8.5.0(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/parser': 8.4.0(eslint@8.57.0)(typescript@5.6.2) debug: 3.2.7 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 @@ -4672,7 +4613,7 @@ packages: - supports-color dev: true - /eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.5.0)(eslint@8.57.0): + /eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0): resolution: {integrity: sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==} engines: {node: '>=4'} peerDependencies: @@ -4683,7 +4624,7 @@ packages: optional: true dependencies: '@rtsao/scc': 1.1.0 - '@typescript-eslint/parser': 8.5.0(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/parser': 8.4.0(eslint@8.57.0)(typescript@5.6.2) array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 array.prototype.flat: 1.3.2 @@ -4692,7 +4633,7 @@ packages: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.9.0(@typescript-eslint/parser@8.5.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0) + eslint-module-utils: 2.9.0(@typescript-eslint/parser@8.4.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -4729,7 +4670,7 @@ packages: - typescript dev: true - /eslint-plugin-jest@28.8.3(@typescript-eslint/eslint-plugin@8.5.0)(eslint@8.57.0)(jest@29.7.0)(typescript@5.6.2): + /eslint-plugin-jest@28.8.3(@typescript-eslint/eslint-plugin@8.4.0)(eslint@8.57.0)(jest@29.7.0)(typescript@5.6.2): resolution: {integrity: sha512-HIQ3t9hASLKm2IhIOqnu+ifw7uLZkIlR7RYNv7fMcEi/p0CIiJmfriStQS2LDkgtY4nyLbIZAD+JL347Yc2ETQ==} engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0} peerDependencies: @@ -4742,7 +4683,7 @@ packages: jest: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 8.5.0(@typescript-eslint/parser@8.5.0)(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/eslint-plugin': 8.4.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.6.2) '@typescript-eslint/utils': 8.4.0(eslint@8.57.0)(typescript@5.6.2) eslint: 8.57.0 jest: 29.7.0(@types/node@22.5.4)(ts-node@10.9.2) @@ -4828,8 +4769,8 @@ packages: requireindex: 1.2.0 dev: true - /eslint-plugin-turbo@2.1.1(eslint@8.57.0): - resolution: {integrity: sha512-E/34kdQd0n3RP18+e0DSV0f3YTSCOojUh1p4X0Xrho2PBYmJ3umSnNo9FhkZt6UDACl+nBQcYTFkRHMz76lJdw==} + /eslint-plugin-turbo@2.1.2(eslint@8.57.0): + resolution: {integrity: sha512-q2ikGubfVLZDPEKliiuubZc3sI5oqbKIZJ6fRi6Bldv8E3cMNH3Qt7g6hXZV4+GxwQbzEEteCYSBNbOn1DBqRg==} peerDependencies: eslint: '>6.6.0' dependencies: @@ -4855,7 +4796,7 @@ packages: - supports-color dev: true - /eslint-plugin-typescript-sort-keys@3.2.0(@typescript-eslint/parser@8.5.0)(eslint@8.57.0)(typescript@5.6.2): + /eslint-plugin-typescript-sort-keys@3.2.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.6.2): resolution: {integrity: sha512-GutszvriaVtwmn7pQjuj9/9o0iXhD7XZs0/424+zsozdRr/fdg5e8206t478Vnqnqi1GjuxcAolj1kf74KnhPA==} engines: {node: '>= 16'} peerDependencies: @@ -4864,7 +4805,7 @@ packages: typescript: ^3 || ^4 || ^5 dependencies: '@typescript-eslint/experimental-utils': 5.62.0(eslint@8.57.0)(typescript@5.6.2) - '@typescript-eslint/parser': 8.5.0(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/parser': 8.4.0(eslint@8.57.0)(typescript@5.6.2) eslint: 8.57.0 json-schema: 0.4.0 natural-compare-lite: 1.4.0 @@ -5806,7 +5747,7 @@ packages: eslint: '*' typescript: '>=4.7.4' dependencies: - '@typescript-eslint/type-utils': 8.5.0(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/type-utils': 8.4.0(eslint@8.57.0)(typescript@5.6.2) eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@5.6.2) ts-declaration-location: 1.0.4(typescript@5.6.2) @@ -7472,7 +7413,7 @@ packages: package-json-from-dist: 1.0.0 dev: true - /rollup-plugin-dts@6.1.1(rollup@4.21.2)(typescript@5.6.2): + /rollup-plugin-dts@6.1.1(rollup@4.21.3)(typescript@5.6.2): resolution: {integrity: sha512-aSHRcJ6KG2IHIioYlvAOcEq6U99sVtqDDKVhnwt70rW6tsz3tv5OSjEiWcgzfsHdLyGXZ/3b/7b/+Za3Y6r1XA==} engines: {node: '>=16'} peerDependencies: @@ -7480,35 +7421,35 @@ packages: typescript: ^4.5 || ^5.0 dependencies: magic-string: 0.30.10 - rollup: 4.21.2 + rollup: 4.21.3 typescript: 5.6.2 optionalDependencies: '@babel/code-frame': 7.24.2 dev: true - /rollup@4.21.2: - resolution: {integrity: sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==} + /rollup@4.21.3: + resolution: {integrity: sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true dependencies: '@types/estree': 1.0.5 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.21.2 - '@rollup/rollup-android-arm64': 4.21.2 - '@rollup/rollup-darwin-arm64': 4.21.2 - '@rollup/rollup-darwin-x64': 4.21.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.21.2 - '@rollup/rollup-linux-arm-musleabihf': 4.21.2 - '@rollup/rollup-linux-arm64-gnu': 4.21.2 - '@rollup/rollup-linux-arm64-musl': 4.21.2 - '@rollup/rollup-linux-powerpc64le-gnu': 4.21.2 - '@rollup/rollup-linux-riscv64-gnu': 4.21.2 - '@rollup/rollup-linux-s390x-gnu': 4.21.2 - '@rollup/rollup-linux-x64-gnu': 4.21.2 - '@rollup/rollup-linux-x64-musl': 4.21.2 - '@rollup/rollup-win32-arm64-msvc': 4.21.2 - '@rollup/rollup-win32-ia32-msvc': 4.21.2 - '@rollup/rollup-win32-x64-msvc': 4.21.2 + '@rollup/rollup-android-arm-eabi': 4.21.3 + '@rollup/rollup-android-arm64': 4.21.3 + '@rollup/rollup-darwin-arm64': 4.21.3 + '@rollup/rollup-darwin-x64': 4.21.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.21.3 + '@rollup/rollup-linux-arm-musleabihf': 4.21.3 + '@rollup/rollup-linux-arm64-gnu': 4.21.3 + '@rollup/rollup-linux-arm64-musl': 4.21.3 + '@rollup/rollup-linux-powerpc64le-gnu': 4.21.3 + '@rollup/rollup-linux-riscv64-gnu': 4.21.3 + '@rollup/rollup-linux-s390x-gnu': 4.21.3 + '@rollup/rollup-linux-x64-gnu': 4.21.3 + '@rollup/rollup-linux-x64-musl': 4.21.3 + '@rollup/rollup-win32-arm64-msvc': 4.21.3 + '@rollup/rollup-win32-ia32-msvc': 4.21.3 + '@rollup/rollup-win32-x64-msvc': 4.21.3 fsevents: 2.3.3 dev: true @@ -8213,8 +8154,8 @@ packages: typescript: 5.6.2 dev: true - /tsx@4.19.0: - resolution: {integrity: sha512-bV30kM7bsLZKZIOCHeMNVMJ32/LuJzLVajkQI/qf92J2Qr08ueLQvW00PUZGiuLPP760UINwupgUj8qrSCPUKg==} + /tsx@4.19.1: + resolution: {integrity: sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==} engines: {node: '>=18.0.0'} hasBin: true dependencies: @@ -8224,64 +8165,64 @@ packages: fsevents: 2.3.3 dev: true - /turbo-darwin-64@2.1.1: - resolution: {integrity: sha512-aYNuJpZlCoi0Htd79fl/2DywpewGKijdXeOfg9KzNuPVKzSMYlAXuAlNGh0MKjiOcyqxQGL7Mq9LFhwA0VpDpQ==} + /turbo-darwin-64@2.1.2: + resolution: {integrity: sha512-3TEBxHWh99h2yIzkuIigMEOXt/ItYQp0aPiJjPd1xN4oDcsKK5AxiFKPH9pdtfIBzYsY59kQhZiFj0ELnSP7Bw==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64@2.1.1: - resolution: {integrity: sha512-tifJKD8yHY48rHXPMcM8o1jI/Jk2KCaXiNjTKvvy9Zsim61BZksNVLelIbrRoCGwAN6PUBZO2lGU5iL/TQJ5Pw==} + /turbo-darwin-arm64@2.1.2: + resolution: {integrity: sha512-he0miWNq2WxJzsH82jS2Z4MXpnkzn9SH8a79iPXiJkq25QREImucscM4RPasXm8wARp91pyysJMq6aasD45CeA==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-linux-64@2.1.1: - resolution: {integrity: sha512-Js6d/bSQe9DuV9c7ITXYpsU/ADzFHABdz1UIHa7Oqjj9VOEbFeA9WpAn0c+mdJrVD+IXJFbbDZUjN7VYssmtcg==} + /turbo-linux-64@2.1.2: + resolution: {integrity: sha512-fKUBcc0rK8Vdqv5a/E3CSpMBLG1bzwv+Q0Q83F8fG2ZfNCNKGbcEYABdonNZkkx141Rj03cZQFCgxu3MVEGU+A==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm64@2.1.1: - resolution: {integrity: sha512-LidzTCq0yvQ+N8w8Qub9FmhQ/mmEIeoqFi7DSupekEV2EjvE9jw/zYc9Pk67X+g7dHVfgOnvVzmrjChdxpFePw==} + /turbo-linux-arm64@2.1.2: + resolution: {integrity: sha512-sV8Bpmm0WiuxgbhxymcC7wSsuxfBBieI98GegSwbr/bs1ANAgzCg93urIrdKdQ3/b31zZxQwcaP4FBF1wx1Qdg==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-windows-64@2.1.1: - resolution: {integrity: sha512-GKc9ZywKwy4xLDhwXd6H07yzl0TB52HjXMrFLyHGhCVnf/w0oq4sLJv2sjbvuarPjsyx4xnCBJ3m3oyL2XmFtA==} + /turbo-windows-64@2.1.2: + resolution: {integrity: sha512-wcmIJZI9ORT9ykHGliFE6kWRQrlH930QGSjSgWC8uFChFFuOyUlvC7ttcxuSvU9VqC7NF4C+GVAcFJQ8lTjN7g==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64@2.1.1: - resolution: {integrity: sha512-oFKkMj11KKUv3xSK9/fhAEQTxLUp1Ol1EOktwc32+SFtEU0uls7kosAz0b+qe8k3pJGEMFdDPdqoEjyJidbxtQ==} + /turbo-windows-arm64@2.1.2: + resolution: {integrity: sha512-zdnXjrhk7YO6CP+Q5wPueEvOCLH4lDa6C4rrwiakcWcPgcQGbVozJlo4uaQ6awo8HLWQEvOwu84RkWTdLAc/Hw==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo@2.1.1: - resolution: {integrity: sha512-u9gUDkmR9dFS8b5kAYqIETK4OnzsS4l2ragJ0+soSMHh6VEeNHjTfSjk1tKxCqLyziCrPogadxP680J+v6yGHw==} + /turbo@2.1.2: + resolution: {integrity: sha512-Jb0rbU4iHEVQ18An/YfakdIv9rKnd3zUfSE117EngrfWXFHo3RndVH96US3GsT8VHpwTncPePDBT2t06PaFLrw==} hasBin: true optionalDependencies: - turbo-darwin-64: 2.1.1 - turbo-darwin-arm64: 2.1.1 - turbo-linux-64: 2.1.1 - turbo-linux-arm64: 2.1.1 - turbo-windows-64: 2.1.1 - turbo-windows-arm64: 2.1.1 + turbo-darwin-64: 2.1.2 + turbo-darwin-arm64: 2.1.2 + turbo-linux-64: 2.1.2 + turbo-linux-arm64: 2.1.2 + turbo-windows-64: 2.1.2 + turbo-windows-arm64: 2.1.2 dev: true /type-check@0.4.0: diff --git a/single-pool/js/packages/classic/package.json b/single-pool/js/packages/classic/package.json index 9abca332fbe..f04939387bb 100644 --- a/single-pool/js/packages/classic/package.json +++ b/single-pool/js/packages/classic/package.json @@ -20,11 +20,11 @@ "devDependencies": { "@types/node": "^22.5.4", "@ava/typescript": "^5.0.0", - "@typescript-eslint/eslint-plugin": "^8.5.0", + "@typescript-eslint/eslint-plugin": "^8.4.0", "ava": "^6.1.3", "eslint": "^8.57.0", "solana-bankrun": "^0.2.0", - "tsx": "^4.19.0", + "tsx": "^4.19.1", "typescript": "^5.6.2" }, "dependencies": { diff --git a/single-pool/js/packages/modern/package.json b/single-pool/js/packages/modern/package.json index 77aa8228f20..31463ca173c 100644 --- a/single-pool/js/packages/modern/package.json +++ b/single-pool/js/packages/modern/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@types/node": "^22.5.4", - "@typescript-eslint/eslint-plugin": "^8.5.0", + "@typescript-eslint/eslint-plugin": "^8.4.0", "eslint": "^8.57.0", "typescript": "^5.6.2" }, diff --git a/stake-pool/js/package.json b/stake-pool/js/package.json index b58d63fd3e9..4998f5c107c 100644 --- a/stake-pool/js/package.json +++ b/stake-pool/js/package.json @@ -60,16 +60,16 @@ "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^11.1.6", "@types/bn.js": "^5.1.0", - "@types/jest": "^29.5.12", + "@types/jest": "^29.5.13", "@types/node": "^22.5.4", "@types/node-fetch": "^2.6.11", - "@typescript-eslint/eslint-plugin": "^8.5.0", - "@typescript-eslint/parser": "^8.5.0", + "@typescript-eslint/eslint-plugin": "^8.4.0", + "@typescript-eslint/parser": "^8.4.0", "cross-env": "^7.0.3", "eslint": "^8.57.0", "jest": "^29.0.0", "rimraf": "^6.0.1", - "rollup": "^4.21.2", + "rollup": "^4.21.3", "rollup-plugin-dts": "^6.1.1", "ts-jest": "^29.2.5", "typescript": "^5.6.2" diff --git a/token-group/js/package.json b/token-group/js/package.json index 4cbd520f5eb..24a82f0b7bc 100644 --- a/token-group/js/package.json +++ b/token-group/js/package.json @@ -52,10 +52,10 @@ "devDependencies": { "@solana/web3.js": "^1.95.3", "@types/chai": "^4.3.19", - "@types/mocha": "^10.0.7", + "@types/mocha": "^10.0.8", "@types/node": "^22.5.4", - "@typescript-eslint/eslint-plugin": "^8.5.0", - "@typescript-eslint/parser": "^8.5.0", + "@typescript-eslint/eslint-plugin": "^8.4.0", + "@typescript-eslint/parser": "^8.4.0", "chai": "^5.1.1", "eslint": "^8.57.0", "eslint-plugin-require-extensions": "^0.1.1", diff --git a/token-lending/js/package.json b/token-lending/js/package.json index b5eb21472a4..f753e5bd125 100644 --- a/token-lending/js/package.json +++ b/token-lending/js/package.json @@ -48,11 +48,11 @@ "@solana/web3.js": "^1.95.3", "@types/eslint": "^8.56.7", "@types/node": "^22.5.4", - "@typescript-eslint/eslint-plugin": "^8.5.0", - "@typescript-eslint/parser": "^8.5.0", + "@typescript-eslint/eslint-plugin": "^8.4.0", + "@typescript-eslint/parser": "^8.4.0", "eslint": "^8.57.0", "gh-pages": "^6.1.1", - "rollup": "^4.21.2", + "rollup": "^4.21.3", "ts-node": "^10.9.2", "tslib": "^2.7.0", "typedoc": "^0.26.7", diff --git a/token-lending/program/Cargo.toml b/token-lending/program/Cargo.toml index f6f8871bcf7..9bdfb9d8b83 100644 --- a/token-lending/program/Cargo.toml +++ b/token-lending/program/Cargo.toml @@ -19,7 +19,7 @@ num-traits = "0.2" solana-program = "2.0.3" spl-token = { version = "6.0", path = "../../token/program", features = [ "no-entrypoint" ] } thiserror = "1.0" -uint = "0.9" +uint = "0.10" [dev-dependencies] assert_matches = "1.5.0" diff --git a/token-lending/program/src/math/decimal.rs b/token-lending/program/src/math/decimal.rs index 8e835b15981..957778ab163 100644 --- a/token-lending/program/src/math/decimal.rs +++ b/token-lending/program/src/math/decimal.rs @@ -11,6 +11,7 @@ #![allow(clippy::assign_op_pattern)] #![allow(clippy::ptr_offset_with_cast)] #![allow(clippy::manual_range_contains)] +#![allow(missing_docs)] use { crate::{ diff --git a/token-lending/program/src/math/rate.rs b/token-lending/program/src/math/rate.rs index 166b6d5bf9f..12d3090486f 100644 --- a/token-lending/program/src/math/rate.rs +++ b/token-lending/program/src/math/rate.rs @@ -17,6 +17,7 @@ #![allow(clippy::ptr_offset_with_cast)] #![allow(clippy::reversed_empty_ranges)] #![allow(clippy::manual_range_contains)] +#![allow(missing_docs)] use { crate::{ diff --git a/token-metadata/js/package.json b/token-metadata/js/package.json index 446c0cd367e..83abcea1e6c 100644 --- a/token-metadata/js/package.json +++ b/token-metadata/js/package.json @@ -52,10 +52,10 @@ "devDependencies": { "@solana/web3.js": "^1.95.3", "@types/chai": "^4.3.19", - "@types/mocha": "^10.0.7", + "@types/mocha": "^10.0.8", "@types/node": "^22.5.4", - "@typescript-eslint/eslint-plugin": "^8.5.0", - "@typescript-eslint/parser": "^8.5.0", + "@typescript-eslint/eslint-plugin": "^8.4.0", + "@typescript-eslint/parser": "^8.4.0", "chai": "^5.1.1", "eslint": "^8.57.0", "eslint-plugin-require-extensions": "^0.1.1", diff --git a/token-swap/js/package.json b/token-swap/js/package.json index 424ab0793f0..a818aa6eac0 100644 --- a/token-swap/js/package.json +++ b/token-swap/js/package.json @@ -54,9 +54,9 @@ "@types/bn.js": "^5.1.0", "@types/chai-as-promised": "^8.0.0", "@types/chai": "^4.3.19", - "@types/mocha": "^10.0.7", - "@typescript-eslint/eslint-plugin": "^8.5.0", - "@typescript-eslint/parser": "^8.5.0", + "@types/mocha": "^10.0.8", + "@typescript-eslint/eslint-plugin": "^8.4.0", + "@typescript-eslint/parser": "^8.4.0", "eslint": "^8.57.0", "eslint-plugin-import": "^2.30.0", "eslint-plugin-require-extensions": "^0.1.1", diff --git a/token/cli/tests/command.rs b/token/cli/tests/command.rs index e13ce549c09..1f8f41e1c6d 100644 --- a/token/cli/tests/command.rs +++ b/token/cli/tests/command.rs @@ -139,6 +139,7 @@ async fn main() { async_trial!(group_pointer, test_validator, payer), async_trial!(group_member_pointer, test_validator, payer), async_trial!(transfer_hook, test_validator, payer), + async_trial!(transfer_hook_with_transfer_fee, test_validator, payer), async_trial!(metadata, test_validator, payer), async_trial!(group, test_validator, payer), async_trial!(confidential_transfer_with_fee, test_validator, payer), @@ -3872,6 +3873,107 @@ async fn transfer_hook(test_validator: &TestValidator, payer: &Keypair) { assert_eq!(extension.program_id, None.try_into().unwrap()); } +async fn transfer_hook_with_transfer_fee(test_validator: &TestValidator, payer: &Keypair) { + let program_id = spl_token_2022::id(); + let mut config = test_config_with_default_signer(test_validator, payer, &program_id); + let transfer_hook_program_id = Pubkey::new_unique(); + + let transfer_fee_basis_points = 100; + let maximum_fee: u64 = 10_000_000_000; + + let result = process_test_command( + &config, + payer, + &[ + "spl-token", + CommandName::CreateToken.into(), + "--program-id", + &program_id.to_string(), + "--transfer-hook", + &transfer_hook_program_id.to_string(), + "--transfer-fee", + &transfer_fee_basis_points.to_string(), + &maximum_fee.to_string(), + ], + ) + .await; + + // Check that the transfer-hook extension is correctly configured + let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); + let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); + let account = config.rpc_client.get_account(&mint).await.unwrap(); + let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let extension = mint_state.get_extension::().unwrap(); + assert_eq!( + extension.program_id, + Some(transfer_hook_program_id).try_into().unwrap() + ); + + // Check that the transfer-fee extension is correctly configured + let extension = mint_state.get_extension::().unwrap(); + assert_eq!( + u16::from(extension.older_transfer_fee.transfer_fee_basis_points), + transfer_fee_basis_points + ); + assert_eq!( + u64::from(extension.older_transfer_fee.maximum_fee), + maximum_fee + ); + assert_eq!( + u16::from(extension.newer_transfer_fee.transfer_fee_basis_points), + transfer_fee_basis_points + ); + assert_eq!( + u64::from(extension.newer_transfer_fee.maximum_fee), + maximum_fee + ); + + // Make sure that parsing transfer hook accounts and expected-fee works + let blockhash = Hash::default(); + let program_client: Arc> = Arc::new( + ProgramOfflineClient::new(blockhash, ProgramRpcClientSendTransaction), + ); + config.program_client = program_client; + + let _result = exec_test_cmd( + &config, + &[ + "spl-token", + CommandName::Transfer.into(), + &mint.to_string(), + "100", + &Pubkey::new_unique().to_string(), + "--blockhash", + &blockhash.to_string(), + "--nonce", + &Pubkey::new_unique().to_string(), + "--nonce-authority", + &Pubkey::new_unique().to_string(), + "--sign-only", + "--mint-decimals", + &format!("{}", TEST_DECIMALS), + "--from", + &Pubkey::new_unique().to_string(), + "--owner", + &Pubkey::new_unique().to_string(), + "--transfer-hook-account", + &format!("{}:readonly", Pubkey::new_unique()), + "--transfer-hook-account", + &format!("{}:writable", Pubkey::new_unique()), + "--transfer-hook-account", + &format!("{}:readonly-signer", Pubkey::new_unique()), + "--transfer-hook-account", + &format!("{}:writable-signer", Pubkey::new_unique()), + "--expected-fee", + "1", + "--program-id", + &program_id.to_string(), + ], + ) + .await + .unwrap(); +} + async fn metadata(test_validator: &TestValidator, payer: &Keypair) { let program_id = spl_token_2022::id(); let config = test_config_with_default_signer(test_validator, payer, &program_id); diff --git a/token/client/src/token.rs b/token/client/src/token.rs index 33352f62c27..165616a645f 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -1166,21 +1166,44 @@ where let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); let decimals = self.decimals.ok_or(TokenError::MissingDecimals)?; - self.process_ixs( - &[transfer_fee::instruction::transfer_checked_with_fee( + let fetch_account_data_fn = |address| { + self.client + .get_account(address) + .map_ok(|opt| opt.map(|acc| acc.data)) + }; + + let instruction = if let Some(transfer_hook_accounts) = &self.transfer_hook_accounts { + let mut instruction = transfer_fee::instruction::transfer_checked_with_fee( &self.program_id, source, - &self.pubkey, + self.get_address(), destination, authority, &multisig_signers, amount, decimals, fee, - )?], - signing_keypairs, - ) - .await + )?; + instruction.accounts.extend(transfer_hook_accounts.clone()); + instruction + } else { + offchain::create_transfer_checked_with_fee_instruction_with_extra_metas( + &self.program_id, + source, + self.get_address(), + destination, + authority, + &multisig_signers, + amount, + decimals, + fee, + fetch_account_data_fn, + ) + .await + .map_err(|_| TokenError::AccountNotFound)? + }; + + self.process_ixs(&[instruction], signing_keypairs).await } /// Burn tokens from account diff --git a/token/confidential-transfer/proof-extraction/src/burn.rs b/token/confidential-transfer/proof-extraction/src/burn.rs new file mode 100644 index 00000000000..1bfc706e369 --- /dev/null +++ b/token/confidential-transfer/proof-extraction/src/burn.rs @@ -0,0 +1,130 @@ +use { + crate::{encryption::PodBurnAmountCiphertext, errors::TokenProofExtractionError}, + solana_zk_sdk::{ + encryption::pod::elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, + zk_elgamal_proof_program::proof_data::{ + BatchedGroupedCiphertext3HandlesValidityProofContext, BatchedRangeProofContext, + CiphertextCommitmentEqualityProofContext, + }, + }, +}; + +/// The public keys associated with a confidential burn +pub struct BurnPubkeys { + pub source: PodElGamalPubkey, + pub auditor: PodElGamalPubkey, + pub supply: PodElGamalPubkey, +} + +/// The proof context information needed to process a confidential burn +/// instruction +pub struct BurnProofContext { + pub burn_amount_ciphertext_lo: PodBurnAmountCiphertext, + pub burn_amount_ciphertext_hi: PodBurnAmountCiphertext, + pub burn_pubkeys: BurnPubkeys, + pub remaining_balance_ciphertext: PodElGamalCiphertext, +} + +impl BurnProofContext { + pub fn verify_and_extract( + equality_proof_context: &CiphertextCommitmentEqualityProofContext, + ciphertext_validity_proof_context: &BatchedGroupedCiphertext3HandlesValidityProofContext, + range_proof_context: &BatchedRangeProofContext, + ) -> Result { + // The equality proof context consists of the source ElGamal public key, the new + // source available balance ciphertext, and the new source avaialble + // balance commitment. The public key should be checked with ciphertext + // validity proof context for consistency and the commitment should be + // checked with range proof for consistency. The public key and + // the cihpertext should be returned as part of `BurnProofContext`. + let CiphertextCommitmentEqualityProofContext { + pubkey: source_elgamal_pubkey_from_equality_proof, + ciphertext: remaining_balance_ciphertext, + commitment: remaining_balance_commitment, + } = equality_proof_context; + + // The ciphertext validity proof context consists of the source ElGamal public + // key, the auditor ElGamal public key, and the grouped ElGamal + // ciphertexts for the low and high bits of the burn amount. The source + // ElGamal public key should be checked with equality + // proof for consistency and the rest of the data should be returned as part of + // `BurnProofContext`. + let BatchedGroupedCiphertext3HandlesValidityProofContext { + first_pubkey: source_elgamal_pubkey_from_validity_proof, + second_pubkey: auditor_elgamal_pubkey, + third_pubkey: supply_elgamal_pubkey, + grouped_ciphertext_lo: burn_amount_ciphertext_lo, + grouped_ciphertext_hi: burn_amount_ciphertext_hi, + } = ciphertext_validity_proof_context; + + // The range proof context consists of the Pedersen commitments and bit-lengths + // for which the range proof is proved. The commitments must consist of + // three commitments pertaining to the new source available balance, the + // low bits of the burn amount, and high bits of the burn + // amount. These commitments must be checked for bit lengths `64`, `16`, + // and `32`. + let BatchedRangeProofContext { + commitments: range_proof_commitments, + bit_lengths: range_proof_bit_lengths, + } = range_proof_context; + + // check that the source pubkey is consistent between equality and ciphertext + // validity proofs + if source_elgamal_pubkey_from_equality_proof != source_elgamal_pubkey_from_validity_proof { + return Err(TokenProofExtractionError::ElGamalPubkeyMismatch); + } + + // check that the range proof was created for the correct set of Pedersen + // commitments + let burn_amount_commitment_lo = burn_amount_ciphertext_lo.extract_commitment(); + let burn_amount_commitment_hi = burn_amount_ciphertext_hi.extract_commitment(); + + let expected_commitments = [ + *remaining_balance_commitment, + burn_amount_commitment_lo, + burn_amount_commitment_hi, + ]; + + if !range_proof_commitments + .iter() + .zip(expected_commitments.iter()) + .all(|(proof_commitment, expected_commitment)| proof_commitment == expected_commitment) + { + return Err(TokenProofExtractionError::PedersenCommitmentMismatch); + } + + // check that the range proof was created for the correct number of bits + const REMAINING_BALANCE_BIT_LENGTH: u8 = 64; + const BURN_AMOUNT_LO_BIT_LENGTH: u8 = 16; + const BURN_AMOUNT_HI_BIT_LENGTH: u8 = 32; + const PADDING_BIT_LENGTH: u8 = 16; + let expected_bit_lengths = [ + REMAINING_BALANCE_BIT_LENGTH, + BURN_AMOUNT_LO_BIT_LENGTH, + BURN_AMOUNT_HI_BIT_LENGTH, + PADDING_BIT_LENGTH, + ] + .iter(); + + if !range_proof_bit_lengths + .iter() + .zip(expected_bit_lengths) + .all(|(proof_len, expected_len)| proof_len == expected_len) + { + return Err(TokenProofExtractionError::RangeProofLengthMismatch); + } + + let burn_pubkeys = BurnPubkeys { + source: *source_elgamal_pubkey_from_equality_proof, + auditor: *auditor_elgamal_pubkey, + supply: *supply_elgamal_pubkey, + }; + + Ok(BurnProofContext { + burn_amount_ciphertext_lo: PodBurnAmountCiphertext(*burn_amount_ciphertext_lo), + burn_amount_ciphertext_hi: PodBurnAmountCiphertext(*burn_amount_ciphertext_hi), + burn_pubkeys, + remaining_balance_ciphertext: *remaining_balance_ciphertext, + }) + } +} diff --git a/token/confidential-transfer/proof-extraction/src/encryption.rs b/token/confidential-transfer/proof-extraction/src/encryption.rs index 34ca2fff156..b0a991235f3 100644 --- a/token/confidential-transfer/proof-extraction/src/encryption.rs +++ b/token/confidential-transfer/proof-extraction/src/encryption.rs @@ -46,3 +46,11 @@ impl PodFeeCiphertext { .map_err(|_| TokenProofExtractionError::CiphertextExtraction) } } + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct PodBurnAmountCiphertext(pub(crate) PodGroupedElGamalCiphertext3Handles); + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct PodMintAmountCiphertext(pub(crate) PodGroupedElGamalCiphertext3Handles); diff --git a/token/confidential-transfer/proof-extraction/src/lib.rs b/token/confidential-transfer/proof-extraction/src/lib.rs index 15cee9bdc5d..f3f99bc1072 100644 --- a/token/confidential-transfer/proof-extraction/src/lib.rs +++ b/token/confidential-transfer/proof-extraction/src/lib.rs @@ -1,5 +1,7 @@ +pub mod burn; pub mod encryption; pub mod errors; +pub mod mint; pub mod transfer; pub mod transfer_with_fee; pub mod withdraw; diff --git a/token/confidential-transfer/proof-extraction/src/mint.rs b/token/confidential-transfer/proof-extraction/src/mint.rs new file mode 100644 index 00000000000..cffe8616dc6 --- /dev/null +++ b/token/confidential-transfer/proof-extraction/src/mint.rs @@ -0,0 +1,130 @@ +use { + crate::{encryption::PodMintAmountCiphertext, errors::TokenProofExtractionError}, + solana_zk_sdk::{ + encryption::pod::elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, + zk_elgamal_proof_program::proof_data::{ + BatchedGroupedCiphertext3HandlesValidityProofContext, BatchedRangeProofContext, + CiphertextCommitmentEqualityProofContext, + }, + }, +}; + +/// The public keys associated with a confidential mint +pub struct MintPubkeys { + pub destination: PodElGamalPubkey, + pub auditor: PodElGamalPubkey, + pub supply: PodElGamalPubkey, +} + +/// The proof context information needed to process a confidential mint +/// instruction +pub struct MintProofContext { + pub mint_amount_ciphertext_lo: PodMintAmountCiphertext, + pub mint_amount_ciphertext_hi: PodMintAmountCiphertext, + pub mint_pubkeys: MintPubkeys, + pub new_supply_ciphertext: PodElGamalCiphertext, +} + +impl MintProofContext { + pub fn verify_and_extract( + equality_proof_context: &CiphertextCommitmentEqualityProofContext, + ciphertext_validity_proof_context: &BatchedGroupedCiphertext3HandlesValidityProofContext, + range_proof_context: &BatchedRangeProofContext, + ) -> Result { + // The equality proof context consists of the supply ElGamal public key, the new + // supply ciphertext, and the new supply commitment. The supply ElGamal + // public key should be checked with ciphertext validity proof for + // consistency and the new supply commitment should be checked with + // range proof for consistency. The new supply ciphertext should be + // returned as part of `MintProofContext`. + let CiphertextCommitmentEqualityProofContext { + pubkey: supply_elgamal_pubkey_from_equality_proof, + ciphertext: new_supply_ciphertext, + commitment: new_supply_commitment, + } = equality_proof_context; + + // The ciphertext validity proof context consists of the destination ElGamal + // public key, the auditor ElGamal public key, and the grouped ElGamal + // ciphertexts for the low and high bits of the mint amount. These + // fields should be returned as part of `MintProofContext`. + let BatchedGroupedCiphertext3HandlesValidityProofContext { + first_pubkey: destination_elgamal_pubkey, + second_pubkey: auditor_elgamal_pubkey, + third_pubkey: supply_elgamal_pubkey_from_ciphertext_validity_proof, + grouped_ciphertext_lo: mint_amount_ciphertext_lo, + grouped_ciphertext_hi: mint_amount_ciphertext_hi, + } = ciphertext_validity_proof_context; + + // The range proof context consists of the Pedersen commitments and bit-lengths + // for which the range proof is proved. The commitments must consist of + // two commitments pertaining to the the + // low bits of the mint amount, and high bits of the mint + // amount. These commitments must be checked for bit lengths `16` and + // and `32`. + let BatchedRangeProofContext { + commitments: range_proof_commitments, + bit_lengths: range_proof_bit_lengths, + } = range_proof_context; + + // check that the supply pubkey is consistent between equality and ciphertext + // validity proofs + if supply_elgamal_pubkey_from_equality_proof + != supply_elgamal_pubkey_from_ciphertext_validity_proof + { + return Err(TokenProofExtractionError::ElGamalPubkeyMismatch); + } + + // check that the range proof was created for the correct set of Pedersen + // commitments + let mint_amount_commitment_lo = mint_amount_ciphertext_lo.extract_commitment(); + let mint_amount_commitment_hi = mint_amount_ciphertext_hi.extract_commitment(); + + let expected_commitments = [ + *new_supply_commitment, + mint_amount_commitment_lo, + mint_amount_commitment_hi, + ]; + + if !range_proof_commitments + .iter() + .zip(expected_commitments.iter()) + .all(|(proof_commitment, expected_commitment)| proof_commitment == expected_commitment) + { + return Err(TokenProofExtractionError::PedersenCommitmentMismatch); + } + + // check that the range proof was created for the correct number of bits + const NEW_SUPPLY_BIT_LENGTH: u8 = 64; + const MINT_AMOUNT_LO_BIT_LENGTH: u8 = 16; + const MINT_AMOUNT_HI_BIT_LENGTH: u8 = 32; + const PADDING_BIT_LENGTH: u8 = 16; + let expected_bit_lengths = [ + NEW_SUPPLY_BIT_LENGTH, + MINT_AMOUNT_LO_BIT_LENGTH, + MINT_AMOUNT_HI_BIT_LENGTH, + PADDING_BIT_LENGTH, + ] + .iter(); + + if !range_proof_bit_lengths + .iter() + .zip(expected_bit_lengths) + .all(|(proof_len, expected_len)| proof_len == expected_len) + { + return Err(TokenProofExtractionError::RangeProofLengthMismatch); + } + + let mint_pubkeys = MintPubkeys { + destination: *destination_elgamal_pubkey, + auditor: *auditor_elgamal_pubkey, + supply: *supply_elgamal_pubkey_from_equality_proof, + }; + + Ok(MintProofContext { + mint_amount_ciphertext_lo: PodMintAmountCiphertext(*mint_amount_ciphertext_lo), + mint_amount_ciphertext_hi: PodMintAmountCiphertext(*mint_amount_ciphertext_hi), + mint_pubkeys, + new_supply_ciphertext: *new_supply_ciphertext, + }) + } +} diff --git a/token/confidential-transfer/proof-generation/src/burn.rs b/token/confidential-transfer/proof-generation/src/burn.rs new file mode 100644 index 00000000000..9b927384ac8 --- /dev/null +++ b/token/confidential-transfer/proof-generation/src/burn.rs @@ -0,0 +1,146 @@ +use { + crate::{ + encryption::BurnAmountCiphertext, errors::TokenProofGenerationError, + try_combine_lo_hi_ciphertexts, try_split_u64, + }, + solana_zk_sdk::{ + encryption::{ + auth_encryption::{AeCiphertext, AeKey}, + elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey}, + pedersen::Pedersen, + }, + zk_elgamal_proof_program::proof_data::{ + BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU128Data, + CiphertextCommitmentEqualityProofData, + }, + }, +}; + +const REMAINING_BALANCE_BIT_LENGTH: usize = 64; +const BURN_AMOUNT_LO_BIT_LENGTH: usize = 16; +const BURN_AMOUNT_HI_BIT_LENGTH: usize = 32; +/// The padding bit length in range proofs to make the bit-length power-of-2 +const RANGE_PROOF_PADDING_BIT_LENGTH: usize = 16; + +/// The proof data required for a confidential burn instruction +pub struct BurnProofData { + pub equality_proof_data: CiphertextCommitmentEqualityProofData, + pub ciphertext_validity_proof_data: BatchedGroupedCiphertext3HandlesValidityProofData, + pub range_proof_data: BatchedRangeProofU128Data, +} + +pub fn burn_split_proof_data( + current_available_balance_ciphertext: &ElGamalCiphertext, + current_decryptable_available_balance: &AeCiphertext, + burn_amount: u64, + source_elgamal_keypair: &ElGamalKeypair, + source_aes_key: &AeKey, + auditor_elgamal_pubkey: &ElGamalPubkey, + supply_elgamal_pubkey: &ElGamalPubkey, +) -> Result { + // split the burn amount into low and high bits + let (burn_amount_lo, burn_amount_hi) = try_split_u64(burn_amount, BURN_AMOUNT_LO_BIT_LENGTH) + .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; + + // encrypt the burn amount under the source and auditor's ElGamal public key + let (burn_amount_ciphertext_lo, burn_amount_opening_lo) = BurnAmountCiphertext::new( + burn_amount_lo, + source_elgamal_keypair.pubkey(), + auditor_elgamal_pubkey, + supply_elgamal_pubkey, + ); + + let (burn_amount_ciphertext_hi, burn_amount_opening_hi) = BurnAmountCiphertext::new( + burn_amount_hi, + source_elgamal_keypair.pubkey(), + auditor_elgamal_pubkey, + supply_elgamal_pubkey, + ); + + // decrypt the current available balance at the source + let current_decrypted_available_balance = current_decryptable_available_balance + .decrypt(source_aes_key) + .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; + + // compute the remaining balance ciphertext + let burn_amount_ciphertext_source_lo = burn_amount_ciphertext_lo + .0 + .to_elgamal_ciphertext(0) + .unwrap(); + let burn_amount_ciphertext_source_hi = burn_amount_ciphertext_hi + .0 + .to_elgamal_ciphertext(0) + .unwrap(); + + #[allow(clippy::arithmetic_side_effects)] + let new_available_balance_ciphertext = current_available_balance_ciphertext + - try_combine_lo_hi_ciphertexts( + &burn_amount_ciphertext_source_lo, + &burn_amount_ciphertext_source_hi, + BURN_AMOUNT_LO_BIT_LENGTH, + ) + .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; + + // compute the remaining balance at the source + let remaining_balance = current_decrypted_available_balance + .checked_sub(burn_amount) + .ok_or(TokenProofGenerationError::NotEnoughFunds)?; + + let (new_available_balance_commitment, new_available_balance_opening) = + Pedersen::new(remaining_balance); + + // generate equality proof data + let equality_proof_data = CiphertextCommitmentEqualityProofData::new( + source_elgamal_keypair, + &new_available_balance_ciphertext, + &new_available_balance_commitment, + &new_available_balance_opening, + remaining_balance, + ) + .map_err(TokenProofGenerationError::from)?; + + // generate ciphertext validity data + let ciphertext_validity_proof_data = BatchedGroupedCiphertext3HandlesValidityProofData::new( + source_elgamal_keypair.pubkey(), + auditor_elgamal_pubkey, + supply_elgamal_pubkey, + &burn_amount_ciphertext_lo.0, + &burn_amount_ciphertext_hi.0, + burn_amount_lo, + burn_amount_hi, + &burn_amount_opening_lo, + &burn_amount_opening_hi, + ) + .map_err(TokenProofGenerationError::from)?; + + // generate range proof data + let (padding_commitment, padding_opening) = Pedersen::new(0_u64); + let range_proof_data = BatchedRangeProofU128Data::new( + vec![ + &new_available_balance_commitment, + burn_amount_ciphertext_lo.get_commitment(), + burn_amount_ciphertext_hi.get_commitment(), + &padding_commitment, + ], + vec![remaining_balance, burn_amount_lo, burn_amount_hi, 0], + vec![ + REMAINING_BALANCE_BIT_LENGTH, + BURN_AMOUNT_LO_BIT_LENGTH, + BURN_AMOUNT_HI_BIT_LENGTH, + RANGE_PROOF_PADDING_BIT_LENGTH, + ], + vec![ + &new_available_balance_opening, + &burn_amount_opening_lo, + &burn_amount_opening_hi, + &padding_opening, + ], + ) + .map_err(TokenProofGenerationError::from)?; + + Ok(BurnProofData { + equality_proof_data, + ciphertext_validity_proof_data, + range_proof_data, + }) +} diff --git a/token/confidential-transfer/proof-generation/src/encryption.rs b/token/confidential-transfer/proof-generation/src/encryption.rs index 8583cc7120c..3f3f875aa44 100644 --- a/token/confidential-transfer/proof-generation/src/encryption.rs +++ b/token/confidential-transfer/proof-generation/src/encryption.rs @@ -86,3 +86,55 @@ impl FeeCiphertext { self.0.handles.get(1).unwrap() } } + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct BurnAmountCiphertext(pub(crate) GroupedElGamalCiphertext<3>); + +impl BurnAmountCiphertext { + pub fn new( + amount: u64, + source_pubkey: &ElGamalPubkey, + auditor_pubkey: &ElGamalPubkey, + supply_pubkey: &ElGamalPubkey, + ) -> (Self, PedersenOpening) { + let opening = PedersenOpening::new_rand(); + let grouped_ciphertext = GroupedElGamal::<3>::encrypt_with( + [source_pubkey, auditor_pubkey, supply_pubkey], + amount, + &opening, + ); + + (Self(grouped_ciphertext), opening) + } + + pub fn get_commitment(&self) -> &PedersenCommitment { + &self.0.commitment + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct MintAmountCiphertext(pub(crate) GroupedElGamalCiphertext<3>); + +impl MintAmountCiphertext { + pub fn new( + amount: u64, + source_pubkey: &ElGamalPubkey, + auditor_pubkey: &ElGamalPubkey, + supply_pubkey: &ElGamalPubkey, + ) -> (Self, PedersenOpening) { + let opening = PedersenOpening::new_rand(); + let grouped_ciphertext = GroupedElGamal::<3>::encrypt_with( + [source_pubkey, auditor_pubkey, supply_pubkey], + amount, + &opening, + ); + + (Self(grouped_ciphertext), opening) + } + + pub fn get_commitment(&self) -> &PedersenCommitment { + &self.0.commitment + } +} diff --git a/token/confidential-transfer/proof-generation/src/lib.rs b/token/confidential-transfer/proof-generation/src/lib.rs index ba769a06fb4..f8883f31954 100644 --- a/token/confidential-transfer/proof-generation/src/lib.rs +++ b/token/confidential-transfer/proof-generation/src/lib.rs @@ -6,8 +6,10 @@ use { }, }; +pub mod burn; pub mod encryption; pub mod errors; +pub mod mint; pub mod transfer; pub mod transfer_with_fee; pub mod withdraw; diff --git a/token/confidential-transfer/proof-generation/src/mint.rs b/token/confidential-transfer/proof-generation/src/mint.rs new file mode 100644 index 00000000000..e25dc70a210 --- /dev/null +++ b/token/confidential-transfer/proof-generation/src/mint.rs @@ -0,0 +1,146 @@ +use { + crate::{ + encryption::MintAmountCiphertext, errors::TokenProofGenerationError, + try_combine_lo_hi_ciphertexts, try_split_u64, + }, + solana_zk_sdk::{ + encryption::{ + auth_encryption::{AeCiphertext, AeKey}, + elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey}, + pedersen::Pedersen, + }, + zk_elgamal_proof_program::proof_data::{ + BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU128Data, + CiphertextCommitmentEqualityProofData, + }, + }, +}; + +const NEW_SUPPLY_BIT_LENGTH: usize = 64; +const MINT_AMOUNT_LO_BIT_LENGTH: usize = 16; +const MINT_AMOUNT_HI_BIT_LENGTH: usize = 32; +/// The padding bit length in range proofs to make the bit-length power-of-2 +const RANGE_PROOF_PADDING_BIT_LENGTH: usize = 16; + +/// The proof data required for a confidential mint instruction +pub struct MintProofData { + pub equality_proof_data: CiphertextCommitmentEqualityProofData, + pub ciphertext_validity_proof_data: BatchedGroupedCiphertext3HandlesValidityProofData, + pub range_proof_data: BatchedRangeProofU128Data, +} + +pub fn mint_split_proof_data( + current_supply_ciphertext: &ElGamalCiphertext, + current_decryptable_supply: &AeCiphertext, + mint_amount: u64, + supply_elgamal_keypair: &ElGamalKeypair, + supply_aes_key: &AeKey, + destination_elgamal_pubkey: &ElGamalPubkey, + auditor_elgamal_pubkey: &ElGamalPubkey, +) -> Result { + // split the mint amount into low and high bits + let (mint_amount_lo, mint_amount_hi) = try_split_u64(mint_amount, MINT_AMOUNT_LO_BIT_LENGTH) + .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; + + // encrypt the mint amount under the destination and auditor's ElGamal public + // keys + let (mint_amount_grouped_ciphertext_lo, mint_amount_opening_lo) = MintAmountCiphertext::new( + mint_amount_lo, + destination_elgamal_pubkey, + auditor_elgamal_pubkey, + supply_elgamal_keypair.pubkey(), + ); + + let (mint_amount_grouped_ciphertext_hi, mint_amount_opening_hi) = MintAmountCiphertext::new( + mint_amount_hi, + destination_elgamal_pubkey, + auditor_elgamal_pubkey, + supply_elgamal_keypair.pubkey(), + ); + + // compute the new supply ciphertext + let mint_amount_ciphertext_supply_lo = mint_amount_grouped_ciphertext_lo + .0 + .to_elgamal_ciphertext(2) + .unwrap(); + let mint_amount_ciphertext_supply_hi = mint_amount_grouped_ciphertext_hi + .0 + .to_elgamal_ciphertext(2) + .unwrap(); + + #[allow(clippy::arithmetic_side_effects)] + let new_supply_ciphertext = current_supply_ciphertext + + try_combine_lo_hi_ciphertexts( + &mint_amount_ciphertext_supply_lo, + &mint_amount_ciphertext_supply_hi, + MINT_AMOUNT_LO_BIT_LENGTH, + ) + .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; + + // decrypt the current supply + let current_supply = current_decryptable_supply + .decrypt(supply_aes_key) + .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; + + // compute the new supply + let new_supply = current_supply + .checked_add(mint_amount) + .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; + + let (new_supply_commitment, new_supply_opening) = Pedersen::new(new_supply); + + // generate equality proof data + let equality_proof_data = CiphertextCommitmentEqualityProofData::new( + supply_elgamal_keypair, + &new_supply_ciphertext, + &new_supply_commitment, + &new_supply_opening, + new_supply, + ) + .map_err(TokenProofGenerationError::from)?; + + // generate ciphertext validity proof data + let ciphertext_validity_proof_data = BatchedGroupedCiphertext3HandlesValidityProofData::new( + destination_elgamal_pubkey, + auditor_elgamal_pubkey, + supply_elgamal_keypair.pubkey(), + &mint_amount_grouped_ciphertext_lo.0, + &mint_amount_grouped_ciphertext_hi.0, + mint_amount_lo, + mint_amount_hi, + &mint_amount_opening_lo, + &mint_amount_opening_hi, + ) + .map_err(TokenProofGenerationError::from)?; + + // generate range proof data + let (padding_commitment, padding_opening) = Pedersen::new(0_u64); + let range_proof_data = BatchedRangeProofU128Data::new( + vec![ + &new_supply_commitment, + mint_amount_grouped_ciphertext_lo.get_commitment(), + mint_amount_grouped_ciphertext_hi.get_commitment(), + &padding_commitment, + ], + vec![new_supply, mint_amount_lo, mint_amount_hi, 0], + vec![ + NEW_SUPPLY_BIT_LENGTH, + MINT_AMOUNT_LO_BIT_LENGTH, + MINT_AMOUNT_HI_BIT_LENGTH, + RANGE_PROOF_PADDING_BIT_LENGTH, + ], + vec![ + &new_supply_opening, + &mint_amount_opening_lo, + &mint_amount_opening_hi, + &padding_opening, + ], + ) + .map_err(TokenProofGenerationError::from)?; + + Ok(MintProofData { + equality_proof_data, + ciphertext_validity_proof_data, + range_proof_data, + }) +} diff --git a/token/confidential-transfer/proof-tests/tests/proof_test.rs b/token/confidential-transfer/proof-tests/tests/proof_test.rs index 0796cbb9a6a..f4c3a7f3a9e 100644 --- a/token/confidential-transfer/proof-tests/tests/proof_test.rs +++ b/token/confidential-transfer/proof-tests/tests/proof_test.rs @@ -4,10 +4,12 @@ use { zk_elgamal_proof_program::proof_data::ZkProofData, }, spl_token_confidential_transfer_proof_extraction::{ - transfer::TransferProofContext, transfer_with_fee::TransferWithFeeProofContext, - withdraw::WithdrawProofContext, + burn::BurnProofContext, mint::MintProofContext, transfer::TransferProofContext, + transfer_with_fee::TransferWithFeeProofContext, withdraw::WithdrawProofContext, }, spl_token_confidential_transfer_proof_generation::{ + burn::{burn_split_proof_data, BurnProofData}, + mint::{mint_split_proof_data, MintProofData}, transfer::{transfer_split_proof_data, TransferProofData}, transfer_with_fee::{transfer_with_fee_split_proof_data, TransferWithFeeProofData}, withdraw::{withdraw_proof_data, WithdrawProofData}, @@ -182,3 +184,113 @@ fn test_withdraw_validity(spendable_balance: u64, withdraw_amount: u64) { ) .unwrap(); } + +#[test] +fn test_mint_proof_correctness() { + test_mint_validity(0, 0); + test_mint_validity(1, 0); + test_mint_validity(65535, 0); + test_mint_validity(65536, 0); + test_mint_validity(281474976710655, 0); + + test_mint_validity(0, 65535); + test_mint_validity(1, 65535); + test_mint_validity(65535, 65535); + test_mint_validity(65536, 65535); + test_mint_validity(281474976710655, 65535); + + test_mint_validity(0, 281474976710655); + test_mint_validity(1, 281474976710655); + test_mint_validity(65535, 281474976710655); + test_mint_validity(65536, 281474976710655); + test_mint_validity(281474976710655, 281474976710655); +} + +fn test_mint_validity(mint_amount: u64, supply: u64) { + let destination_keypair = ElGamalKeypair::new_rand(); + let destination_pubkey = destination_keypair.pubkey(); + + let auditor_keypair = ElGamalKeypair::new_rand(); + let auditor_pubkey = auditor_keypair.pubkey(); + + let supply_keypair = ElGamalKeypair::new_rand(); + let supply_aes_key = AeKey::new_rand(); + + let supply_ciphertext = supply_keypair.pubkey().encrypt(supply); + let decryptable_supply = supply_aes_key.encrypt(supply); + + let MintProofData { + equality_proof_data, + ciphertext_validity_proof_data, + range_proof_data, + } = mint_split_proof_data( + &supply_ciphertext, + &decryptable_supply, + mint_amount, + &supply_keypair, + &supply_aes_key, + destination_pubkey, + auditor_pubkey, + ) + .unwrap(); + + equality_proof_data.verify_proof().unwrap(); + ciphertext_validity_proof_data.verify_proof().unwrap(); + range_proof_data.verify_proof().unwrap(); + + MintProofContext::verify_and_extract( + equality_proof_data.context_data(), + ciphertext_validity_proof_data.context_data(), + range_proof_data.context_data(), + ) + .unwrap(); +} + +#[test] +fn test_burn_proof_correctness() { + test_burn_validity(0, 0); + test_burn_validity(77, 55); + test_burn_validity(65535, 65535); + test_burn_validity(65536, 65536); + test_burn_validity(281474976710655, 281474976710655); +} + +fn test_burn_validity(spendable_balance: u64, burn_amount: u64) { + let source_keypair = ElGamalKeypair::new_rand(); + let aes_key = AeKey::new_rand(); + + let auditor_keypair = ElGamalKeypair::new_rand(); + let auditor_pubkey = auditor_keypair.pubkey(); + + let supply_keypair = ElGamalKeypair::new_rand(); + let supply_pubkey = supply_keypair.pubkey(); + + let spendable_balance_ciphertext = source_keypair.pubkey().encrypt(spendable_balance); + let decryptable_balance = aes_key.encrypt(spendable_balance); + + let BurnProofData { + equality_proof_data, + ciphertext_validity_proof_data, + range_proof_data, + } = burn_split_proof_data( + &spendable_balance_ciphertext, + &decryptable_balance, + burn_amount, + &source_keypair, + &aes_key, + auditor_pubkey, + supply_pubkey, + ) + .unwrap(); + + equality_proof_data.verify_proof().unwrap(); + ciphertext_validity_proof_data.verify_proof().unwrap(); + range_proof_data.verify_proof().unwrap(); + + BurnProofContext::verify_and_extract( + equality_proof_data.context_data(), + ciphertext_validity_proof_data.context_data(), + range_proof_data.context_data(), + ) + .unwrap(); +} diff --git a/token/js/package.json b/token/js/package.json index 4cb51302f37..10efa0a53b6 100644 --- a/token/js/package.json +++ b/token/js/package.json @@ -65,11 +65,11 @@ "@solana/web3.js": "^1.95.3", "@types/chai-as-promised": "^8.0.0", "@types/chai": "^4.3.19", - "@types/mocha": "^10.0.7", + "@types/mocha": "^10.0.8", "@types/node": "^22.5.4", "@types/node-fetch": "^2.6.11", - "@typescript-eslint/eslint-plugin": "^8.5.0", - "@typescript-eslint/parser": "^8.5.0", + "@typescript-eslint/eslint-plugin": "^8.4.0", + "@typescript-eslint/parser": "^8.4.0", "chai": "^5.1.1", "chai-as-promised": "^8.0.0", "eslint": "^8.57.0", diff --git a/token/program-2022-test/tests/transfer_hook.rs b/token/program-2022-test/tests/transfer_hook.rs index d26511bd9fd..584b88e133d 100644 --- a/token/program-2022-test/tests/transfer_hook.rs +++ b/token/program-2022-test/tests/transfer_hook.rs @@ -13,6 +13,7 @@ use { entrypoint::ProgramResult, instruction::{AccountMeta, Instruction, InstructionError}, program_error::ProgramError, + program_option::COption, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, @@ -23,6 +24,7 @@ use { spl_token_2022::{ error::TokenError, extension::{ + transfer_fee::{TransferFee, TransferFeeAmount, TransferFeeConfig}, transfer_hook::{TransferHook, TransferHookAccount}, BaseStateWithExtensions, }, @@ -36,6 +38,9 @@ use { std::{convert::TryInto, sync::Arc}, }; +const TEST_MAXIMUM_FEE: u64 = 10_000_000; +const TEST_FEE_BASIS_POINTS: u16 = 100; + /// Test program to fail transfer hook, conforms to transfer-hook-interface pub fn process_instruction_fail( _program_id: &Pubkey, @@ -139,6 +144,58 @@ pub fn process_instruction_swap( Ok(()) } +// Test program to transfer two types of tokens with transfer hooks at once with +// fees +pub fn process_instruction_swap_with_fee( + _program_id: &Pubkey, + accounts: &[AccountInfo], + _input: &[u8], +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let source_a_account_info = next_account_info(account_info_iter)?; + let mint_a_info = next_account_info(account_info_iter)?; + let destination_a_account_info = next_account_info(account_info_iter)?; + let authority_a_info = next_account_info(account_info_iter)?; + let token_program_a_info = next_account_info(account_info_iter)?; + + let source_b_account_info = next_account_info(account_info_iter)?; + let mint_b_info = next_account_info(account_info_iter)?; + let destination_b_account_info = next_account_info(account_info_iter)?; + let authority_b_info = next_account_info(account_info_iter)?; + let token_program_b_info = next_account_info(account_info_iter)?; + + let remaining_accounts = account_info_iter.as_slice(); + + onchain::invoke_transfer_checked_with_fee( + token_program_a_info.key, + source_a_account_info.clone(), + mint_a_info.clone(), + destination_a_account_info.clone(), + authority_a_info.clone(), + remaining_accounts, + 1_000_000_000, + 9, + 10_000_000, + &[], + )?; + + onchain::invoke_transfer_checked_with_fee( + token_program_b_info.key, + source_b_account_info.clone(), + mint_b_info.clone(), + destination_b_account_info.clone(), + authority_b_info.clone(), + remaining_accounts, + 1_000_000_000, + 9, + 10_000_000, + &[], + )?; + + Ok(()) +} + async fn setup_accounts( token_context: &TokenContext, alice_account: Keypair, @@ -261,6 +318,44 @@ async fn setup(mint: Keypair, program_id: &Pubkey, authority: &Pubkey) -> TestCo context } +async fn setup_with_fee(mint: Keypair, program_id: &Pubkey, authority: &Pubkey) -> TestContext { + let mut program_test = setup_program_test(program_id); + + let transfer_fee_config_authority = Keypair::new(); + let withdraw_withheld_authority = Keypair::new(); + let transfer_fee_basis_points = TEST_FEE_BASIS_POINTS; + let maximum_fee = TEST_MAXIMUM_FEE; + add_validation_account(&mut program_test, &mint.pubkey(), program_id); + + let context = program_test.start_with_context().await; + let context = Arc::new(tokio::sync::Mutex::new(context)); + + let mut context = TestContext { + context, + token_context: None, + }; + context + .init_token_with_mint_keypair_and_freeze_authority( + mint, + vec![ + ExtensionInitializationParams::TransferHook { + authority: Some(*authority), + program_id: Some(*program_id), + }, + ExtensionInitializationParams::TransferFeeConfig { + transfer_fee_config_authority: transfer_fee_config_authority.pubkey().into(), + withdraw_withheld_authority: withdraw_withheld_authority.pubkey().into(), + transfer_fee_basis_points, + maximum_fee, + }, + ], + None, + ) + .await + .unwrap(); + context +} + async fn setup_with_confidential_transfers( mint: Keypair, program_id: &Pubkey, @@ -549,6 +644,99 @@ async fn success_transfer() { ); } +#[tokio::test] +async fn success_transfer_with_fee() { + let authority = Keypair::new(); + let program_id = Pubkey::new_unique(); + let mint_keypair = Keypair::new(); + + let maximum_fee = TEST_MAXIMUM_FEE; + let alice_amount = maximum_fee * 100; + let transfer_amount = maximum_fee; + + let token_context = setup_with_fee(mint_keypair, &program_id, &authority.pubkey()) + .await + .token_context + .take() + .unwrap(); + + let (alice_account, bob_account) = + setup_accounts(&token_context, Keypair::new(), Keypair::new(), alice_amount).await; + + let transfer_fee = TransferFee { + epoch: 0.into(), + transfer_fee_basis_points: TEST_FEE_BASIS_POINTS.into(), + maximum_fee: TEST_MAXIMUM_FEE.into(), + }; + let transfer_fee_config = TransferFeeConfig { + transfer_fee_config_authority: COption::Some(Pubkey::new_unique()).try_into().unwrap(), + withdraw_withheld_authority: COption::Some(Pubkey::new_unique()).try_into().unwrap(), + withheld_amount: 0.into(), + older_transfer_fee: transfer_fee, + newer_transfer_fee: transfer_fee, + }; + let fee = transfer_fee_config + .calculate_epoch_fee(0, transfer_amount) + .unwrap(); + + token_context + .token + .transfer_with_fee( + &alice_account, + &bob_account, + &token_context.alice.pubkey(), + transfer_amount, + fee, + &[&token_context.alice], + ) + .await + .unwrap(); + + // Get the accounts' state after the transfer + let alice_state = token_context + .token + .get_account_info(&alice_account) + .await + .unwrap(); + let bob_state = token_context + .token + .get_account_info(&bob_account) + .await + .unwrap(); + + // Check that the correct amount was deducted from Alice's account + assert_eq!(alice_state.base.amount, alice_amount - transfer_amount); + + // Check the there are no tokens withheld in Alice's account + let extension = alice_state.get_extension::().unwrap(); + assert_eq!(extension.withheld_amount, 0.into()); + + // Check the fee tokens are withheld in Bobs's account + let extension = bob_state.get_extension::().unwrap(); + assert_eq!(extension.withheld_amount, fee.into()); + + // Check that the correct amount was added to Bobs's account + assert_eq!(bob_state.base.amount, transfer_amount - fee); + + // the example program checks that the transferring flag was set to true, + // so make sure that it was correctly unset by the token program + assert_eq!( + bob_state + .get_extension::() + .unwrap() + .transferring, + false.into() + ); + + assert_eq!( + alice_state + .get_extension::() + .unwrap() + .transferring, + false.into() + ); +} + #[tokio::test] async fn fail_transfer_hook_program() { let authority = Pubkey::new_unique(); @@ -821,6 +1009,150 @@ async fn success_transfers_using_onchain_helper() { .unwrap(); } +#[tokio::test] +async fn success_transfers_with_fee_using_onchain_helper() { + let authority = Pubkey::new_unique(); + let program_id = Pubkey::new_unique(); + let mint_a_keypair = Keypair::new(); + let mint_a = mint_a_keypair.pubkey(); + let mint_b_keypair = Keypair::new(); + let mint_b = mint_b_keypair.pubkey(); + let amount = 10_000_000_000; + + let transfer_fee_config_authority = Keypair::new(); + let withdraw_withheld_authority = Keypair::new(); + let transfer_fee_basis_points = TEST_FEE_BASIS_POINTS; + let maximum_fee = TEST_MAXIMUM_FEE; + + let swap_program_id = Pubkey::new_unique(); + let mut program_test = setup_program_test(&program_id); + program_test.add_program( + "my_swap", + swap_program_id, + processor!(process_instruction_swap_with_fee), + ); + add_validation_account(&mut program_test, &mint_a, &program_id); + add_validation_account(&mut program_test, &mint_b, &program_id); + + let context = program_test.start_with_context().await; + let context = Arc::new(tokio::sync::Mutex::new(context)); + let mut context_a = TestContext { + context: context.clone(), + token_context: None, + }; + context_a + .init_token_with_mint_keypair_and_freeze_authority( + mint_a_keypair, + vec![ + ExtensionInitializationParams::TransferHook { + authority: Some(authority), + program_id: Some(program_id), + }, + ExtensionInitializationParams::TransferFeeConfig { + transfer_fee_config_authority: transfer_fee_config_authority.pubkey().into(), + withdraw_withheld_authority: withdraw_withheld_authority.pubkey().into(), + transfer_fee_basis_points, + maximum_fee, + }, + ], + None, + ) + .await + .unwrap(); + let token_a_context = context_a.token_context.unwrap(); + let (source_a_account, destination_a_account) = + setup_accounts(&token_a_context, Keypair::new(), Keypair::new(), amount).await; + let authority_a = token_a_context.alice; + let token_a = token_a_context.token; + let mut context_b = TestContext { + context, + token_context: None, + }; + context_b + .init_token_with_mint_keypair_and_freeze_authority( + mint_b_keypair, + vec![ + ExtensionInitializationParams::TransferHook { + authority: Some(authority), + program_id: Some(program_id), + }, + ExtensionInitializationParams::TransferFeeConfig { + transfer_fee_config_authority: transfer_fee_config_authority.pubkey().into(), + withdraw_withheld_authority: withdraw_withheld_authority.pubkey().into(), + transfer_fee_basis_points, + maximum_fee, + }, + ], + None, + ) + .await + .unwrap(); + let token_b_context = context_b.token_context.unwrap(); + let (source_b_account, destination_b_account) = + setup_accounts(&token_b_context, Keypair::new(), Keypair::new(), amount).await; + let authority_b = token_b_context.alice; + let account_metas = vec![ + AccountMeta::new(source_a_account, false), + AccountMeta::new_readonly(mint_a, false), + AccountMeta::new(destination_a_account, false), + AccountMeta::new_readonly(authority_a.pubkey(), true), + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new(source_b_account, false), + AccountMeta::new_readonly(mint_b, false), + AccountMeta::new(destination_b_account, false), + AccountMeta::new_readonly(authority_b.pubkey(), true), + AccountMeta::new_readonly(spl_token_2022::id(), false), + ]; + + let mut instruction = Instruction::new_with_bytes(swap_program_id, &[], account_metas); + + add_extra_account_metas_for_execute( + &mut instruction, + &program_id, + &source_a_account, + &mint_a, + &destination_a_account, + &authority_a.pubkey(), + amount, + |address| { + token_a.get_account(address).map_ok_or_else( + |e| match e { + TokenClientError::AccountNotFound => Ok(None), + _ => Err(offchain::AccountFetchError::from(e)), + }, + |acc| Ok(Some(acc.data)), + ) + }, + ) + .await + .unwrap(); + add_extra_account_metas_for_execute( + &mut instruction, + &program_id, + &source_b_account, + &mint_b, + &destination_b_account, + &authority_b.pubkey(), + amount, + |address| { + token_a.get_account(address).map_ok_or_else( + |e| match e { + TokenClientError::AccountNotFound => Ok(None), + _ => Err(offchain::AccountFetchError::from(e)), + }, + |acc| Ok(Some(acc.data)), + ) + }, + ) + .await + .unwrap(); + + token_a + .process_ixs(&[instruction], &[&authority_a, &authority_b]) + .await + .unwrap(); +} + #[tokio::test] async fn success_confidential_transfer() { let authority = Keypair::new(); diff --git a/token/program-2022/src/offchain.rs b/token/program-2022/src/offchain.rs index 800f3dd2b31..eda4d658620 100644 --- a/token/program-2022/src/offchain.rs +++ b/token/program-2022/src/offchain.rs @@ -3,7 +3,7 @@ pub use spl_transfer_hook_interface::offchain::{AccountDataResult, AccountFetchError}; use { crate::{ - extension::{transfer_hook, StateWithExtensions}, + extension::{transfer_fee, transfer_hook, StateWithExtensions}, state::Mint, }, solana_program::{instruction::Instruction, program_error::ProgramError, pubkey::Pubkey}, @@ -74,6 +74,72 @@ where Ok(transfer_instruction) } +/// Offchain helper to create a `TransferCheckedWithFee` instruction with all +/// additional required account metas for a transfer, including the ones +/// required by the transfer hook. +/// +/// To be client-agnostic and to avoid pulling in the full solana-sdk, this +/// simply takes a function that will return its data as `Future>` for +/// the given address. Can be called in the following way: +/// +/// ```rust,ignore +/// let instruction = create_transfer_checked_with_fee_instruction_with_extra_metas( +/// &spl_token_2022::id(), +/// &source, +/// &mint, +/// &destination, +/// &authority, +/// &[], +/// amount, +/// decimals, +/// fee, +/// |address| self.client.get_account(&address).map_ok(|opt| opt.map(|acc| acc.data)), +/// ) +/// .await? +/// ``` +#[allow(clippy::too_many_arguments)] +pub async fn create_transfer_checked_with_fee_instruction_with_extra_metas( + token_program_id: &Pubkey, + source_pubkey: &Pubkey, + mint_pubkey: &Pubkey, + destination_pubkey: &Pubkey, + authority_pubkey: &Pubkey, + signer_pubkeys: &[&Pubkey], + amount: u64, + decimals: u8, + fee: u64, + fetch_account_data_fn: F, +) -> Result +where + F: Fn(Pubkey) -> Fut, + Fut: Future, +{ + let mut transfer_instruction = transfer_fee::instruction::transfer_checked_with_fee( + token_program_id, + source_pubkey, + mint_pubkey, + destination_pubkey, + authority_pubkey, + signer_pubkeys, + amount, + decimals, + fee, + )?; + + add_extra_account_metas( + &mut transfer_instruction, + source_pubkey, + mint_pubkey, + destination_pubkey, + authority_pubkey, + amount, + fetch_account_data_fn, + ) + .await?; + + Ok(transfer_instruction) +} + /// Offchain helper to add required account metas to an instruction, including /// the ones required by the transfer hook. /// @@ -318,4 +384,103 @@ mod tests { assert_eq!(instruction.accounts, check_metas); } + + #[tokio::test] + async fn test_create_transfer_checked_with_fee_instruction_with_extra_metas() { + let source = Pubkey::new_unique(); + let destination = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let amount = 100u64; + let fee = 1u64; + + let validate_state_pubkey = + get_extra_account_metas_address(&MINT_PUBKEY, &TRANSFER_HOOK_PROGRAM_ID); + let extra_meta_3_pubkey = Pubkey::find_program_address( + &[ + source.as_ref(), + destination.as_ref(), + validate_state_pubkey.as_ref(), + ], + &TRANSFER_HOOK_PROGRAM_ID, + ) + .0; + let extra_meta_4_pubkey = Pubkey::find_program_address( + &[ + amount.to_le_bytes().as_ref(), + destination.as_ref(), + EXTRA_META_1.as_ref(), + extra_meta_3_pubkey.as_ref(), + ], + &TRANSFER_HOOK_PROGRAM_ID, + ) + .0; + + let instruction = create_transfer_checked_with_fee_instruction_with_extra_metas( + &crate::id(), + &source, + &MINT_PUBKEY, + &destination, + &authority, + &[], + amount, + DECIMALS, + fee, + mock_fetch_account_data_fn, + ) + .await + .unwrap(); + + let check_metas = [ + AccountMeta::new(source, false), + AccountMeta::new_readonly(MINT_PUBKEY, false), + AccountMeta::new(destination, false), + AccountMeta::new_readonly(authority, true), + AccountMeta::new_readonly(EXTRA_META_1, true), + AccountMeta::new_readonly(EXTRA_META_2, true), + AccountMeta::new(extra_meta_3_pubkey, false), + AccountMeta::new(extra_meta_4_pubkey, false), + AccountMeta::new_readonly(TRANSFER_HOOK_PROGRAM_ID, false), + AccountMeta::new_readonly(validate_state_pubkey, false), + ]; + + assert_eq!(instruction.accounts, check_metas); + + // With additional signers + let signer_1 = Pubkey::new_unique(); + let signer_2 = Pubkey::new_unique(); + let signer_3 = Pubkey::new_unique(); + + let instruction = create_transfer_checked_with_fee_instruction_with_extra_metas( + &crate::id(), + &source, + &MINT_PUBKEY, + &destination, + &authority, + &[&signer_1, &signer_2, &signer_3], + amount, + DECIMALS, + fee, + mock_fetch_account_data_fn, + ) + .await + .unwrap(); + + let check_metas = [ + AccountMeta::new(source, false), + AccountMeta::new_readonly(MINT_PUBKEY, false), + AccountMeta::new(destination, false), + AccountMeta::new_readonly(authority, false), // False because of additional signers + AccountMeta::new_readonly(signer_1, true), + AccountMeta::new_readonly(signer_2, true), + AccountMeta::new_readonly(signer_3, true), + AccountMeta::new_readonly(EXTRA_META_1, true), + AccountMeta::new_readonly(EXTRA_META_2, true), + AccountMeta::new(extra_meta_3_pubkey, false), + AccountMeta::new(extra_meta_4_pubkey, false), + AccountMeta::new_readonly(TRANSFER_HOOK_PROGRAM_ID, false), + AccountMeta::new_readonly(validate_state_pubkey, false), + ]; + + assert_eq!(instruction.accounts, check_metas); + } } diff --git a/token/program-2022/src/onchain.rs b/token/program-2022/src/onchain.rs index 255800d7877..874822f00fc 100644 --- a/token/program-2022/src/onchain.rs +++ b/token/program-2022/src/onchain.rs @@ -3,7 +3,7 @@ use { crate::{ - extension::{transfer_hook, StateWithExtensions}, + extension::{transfer_fee, transfer_hook, StateWithExtensions}, instruction, state::Mint, }, @@ -78,3 +78,71 @@ pub fn invoke_transfer_checked<'a>( invoke_signed(&cpi_instruction, &cpi_account_infos, seeds) } + +/// Helper to CPI into token-2022 on-chain, looking through the additional +/// account infos to create the proper instruction with the fee +/// and proper account infos +#[allow(clippy::too_many_arguments)] +pub fn invoke_transfer_checked_with_fee<'a>( + token_program_id: &Pubkey, + source_info: AccountInfo<'a>, + mint_info: AccountInfo<'a>, + destination_info: AccountInfo<'a>, + authority_info: AccountInfo<'a>, + additional_accounts: &[AccountInfo<'a>], + amount: u64, + decimals: u8, + fee: u64, + seeds: &[&[&[u8]]], +) -> ProgramResult { + let mut cpi_instruction = transfer_fee::instruction::transfer_checked_with_fee( + token_program_id, + source_info.key, + mint_info.key, + destination_info.key, + authority_info.key, + &[], // add them later, to avoid unnecessary clones + amount, + decimals, + fee, + )?; + + let mut cpi_account_infos = vec![ + source_info.clone(), + mint_info.clone(), + destination_info.clone(), + authority_info.clone(), + ]; + + // if it's a signer, it might be a multisig signer, throw it in! + additional_accounts + .iter() + .filter(|ai| ai.is_signer) + .for_each(|ai| { + cpi_account_infos.push(ai.clone()); + cpi_instruction + .accounts + .push(AccountMeta::new_readonly(*ai.key, ai.is_signer)); + }); + + // scope the borrowing to avoid a double-borrow during CPI + { + let mint_data = mint_info.try_borrow_data()?; + let mint = StateWithExtensions::::unpack(&mint_data)?; + if let Some(program_id) = transfer_hook::get_program_id(&mint) { + add_extra_accounts_for_execute_cpi( + &mut cpi_instruction, + &mut cpi_account_infos, + &program_id, + source_info, + mint_info.clone(), + destination_info, + authority_info, + amount, + additional_accounts, + )?; + } + } + + invoke_signed(&cpi_instruction, &cpi_account_infos, seeds) +}