diff --git a/.gitignore b/.gitignore index c5362743..30327dc1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ **/target/* !**/target/types +.cursorrules + local-scripts .anchor diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 00000000..21a5f6d9 --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,3 @@ +{ + "timeout": 1000000 +} diff --git a/.prettierrc.js b/.prettierrc.js index 57b18d55..13ee303a 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,6 +1,8 @@ module.exports = { - trailingComma: 'es5', - tabWidth: 2, - semi: true, - singleQuote: true, + trailingComma: 'es5', + tabWidth: 2, + printWidth: 160, + + semi: true, + singleQuote: true, }; diff --git a/Anchor.toml b/Anchor.toml index 056f7951..2fc8db5a 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -5,18 +5,20 @@ resolution = true skip-lint = false [programs.localnet] -libreplex_editions = "587DoLBH2H39i5bToWBc6zRgbD2iJZtc4Kb8nYsskYTq" -libreplex_editions_controls = "5hEa5j38yNJRM9vQA44Q6gXVj4Db8y3mWxkDtQeofKKs" +libreplex_editions = "GHVmd43GebD2SSedqG1AAh76izf5CPAhz6KotkZyRzUD" +libreplex_editions_controls = "CCWfNBFcrjbKSjKes9DqgiQYsnmCFgCVKx21mmV2xWgN" [registry] url = "https://api.apr.dev" [provider] cluster = "Localnet" +### @dev testing wallet +# wallet = "~/.config/solana/id.json" wallet = "usb://ledger" [scripts] -test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 js-tests/**/*.ts" +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 test/tests/**/*.ts" [test] startup_wait = 100000 @@ -25,6 +27,8 @@ upgradeable = false [test.validator] bind_address = "0.0.0.0" +### @dev: Utilize the testnet cluster for running the testsuite +# url = "https://api.testnet.solana.com" url = "https://api.mainnet-beta.solana.com" ledger = ".anchor/test-ledger" rpc_port = 8899 @@ -40,3 +44,7 @@ address = "cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK" [[test.validator.clone]] address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" + +[[test.validator.clone]] +address = "5hx15GaPPqsYA61v6QpcGPpo125v7rfvEfZQ4dJErG5V" + diff --git a/Cargo.lock b/Cargo.lock index 500435b3..7551fc2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2207,6 +2207,7 @@ dependencies = [ "arrayref", "libreplex_editions", "libreplex_shared", + "rarible-merkle-verify", "solana-program", "solana-program-test", "solana-sdk", @@ -3173,6 +3174,15 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rarible-merkle-verify" +version = "0.0.1" +dependencies = [ + "anchor-lang", + "anchor-spl", + "solana-program", +] + [[package]] name = "rayon" version = "1.10.0" diff --git a/Cargo.toml b/Cargo.toml index 436fcebf..eaaf8cf1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,4 @@ codegen-units = 1 [profile.release.build-override] opt-level = 3 incremental = false -codegen-units = 1 +codegen-units = 1 \ No newline at end of file diff --git a/README.md b/README.md index 7af55bf1..1dc6f546 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,66 @@ -
- +# Eclipse Program Library Testing Guide + +This guide provides step-by-step instructions for setting up and running tests for the Eclipse Program Library. + +## Prerequisites + +- [Rust](https://www.rust-lang.org/tools/install) +- [Solana CLI](https://docs.solana.com/cli/install-solana-cli-tools) +- [Anchor](https://www.anchor-lang.com/docs/installation) +- [Node.js and npm](https://nodejs.org/en/download/) +- [Yarn](https://classic.yarnpkg.com/en/docs/install) + +## Setup and Testing Steps + +1. Clone the repository: + ``` + git clone https://github.com/rarible/eclipse-program-library.git + cd eclipse-program-library + ``` + +2. Install dependencies: + ``` + yarn install + ``` + +3. Sync program IDs: + - Open `Anchor.toml` and `constants.ts` + - Ensure that the program IDs in both files match + - Example: + ```toml + # In Anchor.toml + [programs.localnet] + libreplex_editions = "587DoLBH2H39i5bToWBc6zRgbD2iJZtc4Kb8nYsskYTq" + libreplex_editions_controls = "5hEa5j38yNJRM9vQA44Q6gXVj4Db8y3mWxkDtQeofKKs" + ``` + ```typescript + // In constants.ts + export const LIBREPLEX_EDITIONS_PROGRAM_ID = new PublicKey("587DoLBH2H39i5bToWBc6zRgbD2iJZtc4Kb8nYsskYTq"); + export const LIBREPLEX_EDITIONS_CONTROLS_PROGRAM_ID = new PublicKey("5hEa5j38yNJRM9vQA44Q6gXVj4Db8y3mWxkDtQeofKKs"); + ``` + +4. Run tests: + - To build and run tests: + ``` + anchor test + ``` + - To skip building and only run tests (faster for repeated testing): + ``` + anchor test --skip-build + ``` + +## Notes + +- If you encounter wallet-related errors, ensure your `Anchor.toml` is configured to use your local keypair: + ```toml + [provider] + cluster = "localnet" + wallet = "~/.config/solana/id.json" + ``` + +- For any issues or questions, please open an issue in the GitHub repository. + +## Contributing + +We welcome contributions! Please see our [CONTRIBUTING.md](CONTRIBUTING.md) file for details on how to contribute to this project. -

Libreplex

- -

- Documentation (stable) -

-
- - - -The mission of Libreplex is to provide a community-driven, open license protocol to the Solana SPL Token and NFT community. The protocol must meet the following criteria: - -1) Distributed deployment keys - -To ensure that no single entity can unilaterally make changes that impact or jeopardise the integrity of the applications that depend on the protocol. - -2) Open license held by a Trust / Foundation - -The licensing must ensure that any applications utilising the protocol can do so knowing that the nature of the protocol remains constant, to minimise uncertainty and maximise transparency. - -3) Guaranteed fees-free for life - -The fee-free nature of the protocol ensures that even though applications built on top of the protocol may introduce fees, the protocol itself will never do so. This establishes a level playing field to all and enforces predictability and transparency. - - 4) Open source - -The source of the protocol will be made available on github or similar. After initial launch, any changes will be subject to 30-day vetting and a community vote. - -INSTRUCTIONS: - -Install dependencies - -``` -yarn -``` - -Build - -``` -anchor build -``` - -To run unit tests (cargo): - -`cargo test` - -To run unit tests (cargo, for a single program): - -`cargo test libreplex_metadata` diff --git a/constants.ts b/constants.ts new file mode 100644 index 00000000..e612cfe6 --- /dev/null +++ b/constants.ts @@ -0,0 +1,4 @@ +// @dev: sync with Anchor.toml to ensure correct program ids are used on tests +export const EDITIONS_PROGRAM_ID = 'GHVmd43GebD2SSedqG1AAh76izf5CPAhz6KotkZyRzUD'; +export const EDITIONS_CONTROLS_PROGRAM_ID = 'CCWfNBFcrjbKSjKes9DqgiQYsnmCFgCVKx21mmV2xWgN'; +export const TOKEN_GROUP_EXTENSION_PROGRAM_ID = '5hx15GaPPqsYA61v6QpcGPpo125v7rfvEfZQ4dJErG5V'; diff --git a/docs/book.toml b/docs/book.toml deleted file mode 100644 index 62fad5f9..00000000 --- a/docs/book.toml +++ /dev/null @@ -1,9 +0,0 @@ -[book] -authors = ["FallingHazard"] -language = "en" -multilingual = false -src = "src" -title = "Libreplex Docs" - -[output.html] -default-theme = "rust" \ No newline at end of file diff --git a/docs/pdf/20231130-fairlaunch-base-architecture.pdf b/docs/pdf/20231130-fairlaunch-base-architecture.pdf deleted file mode 100644 index b5003328..00000000 Binary files a/docs/pdf/20231130-fairlaunch-base-architecture.pdf and /dev/null differ diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md deleted file mode 100644 index bcba8905..00000000 --- a/docs/src/SUMMARY.md +++ /dev/null @@ -1,6 +0,0 @@ -# Summary - -- [Quickstart](./chapter_1.md) -- [Programs](./programs.md) -- [Data Model](./data_model.md) -- [Creator Machine](./creator_machine.md) diff --git a/docs/src/chapter_1.md b/docs/src/chapter_1.md deleted file mode 100644 index e23e9800..00000000 --- a/docs/src/chapter_1.md +++ /dev/null @@ -1,95 +0,0 @@ -# Quickstart - - -Install the [SDK](https://www.npmjs.com/package/@libreplex/sdk) - - -``` -import {mintSingle} from "@libreplex/sdk" -import * as anchor from "@coral-xyz/anchor"; - -const provider = anchor.AnchorProvider.env() - -const {method, mint} = (await mintSingle({ - provider, - mintData: { - assetUrl: { - type: "jsonUrl", - value: "COOL.com" - }, - name: "COOL", - symbol: "COOL", - } -})) - -await method.rpc() - -``` - -## Creating a collection/group. - - - -``` -import {mintSingle} from "@libreplex/sdk" -import * as anchor from "@coral-xyz/anchor"; - -const provider = anchor.AnchorProvider.env() - -const {method, group} = await setupGroup({ - connector: { - type: "provider", - provider, - }, - groupAuthority: me, - input: { - description: "A very cool group", - name: "COOLIO", - symbol: "GRP", - url: "COOL.com", - royalties: { - bps: 0, - shares: [{ - recipient: me, - share: 100, - }], - }, - permittedSigners: [], - onChainAttributes: [], - }, - }) - -await method.rpc() - -``` - -## Minting to a group. - -``` -import {mintSingle} from "@libreplex/sdk" -import * as anchor from "@coral-xyz/anchor"; - -const provider = anchor.AnchorProvider.env() - - -const group = "....Some Public Key..." - -const {method, mint} = (await mintSingle({ - provider, - mintData: { - assetUrl: { - type: "jsonUrl", - value: "COOL.com" - }, - name: "COOL", - symbol: "COOL", - }, - mintToGroup: { - group, - checkValidGroup: false, - } -})) - -await method.rpc() - -``` diff --git a/docs/src/creator_machine.md b/docs/src/creator_machine.md deleted file mode 100644 index 2adec254..00000000 --- a/docs/src/creator_machine.md +++ /dev/null @@ -1,3 +0,0 @@ -# Creator Machine - -Work in progress... \ No newline at end of file diff --git a/docs/src/data_model.md b/docs/src/data_model.md deleted file mode 100644 index 9bc591f4..00000000 --- a/docs/src/data_model.md +++ /dev/null @@ -1,17 +0,0 @@ -# Data Model - -Libreplex provides Metadata for [Token-2022](https://spl.solana.com/token-2022) mints. - -## Metadata structure - -| Field Name | Type | Extra Info | -|:-------------------------------------------|:--------------------------------------------------------------------------|--------------------------------------------------------------------------| -| Mint | [Pubkey](https://docs.rs/solana-program/latest/solana_program/pubkey/struct.Pubkey.html) | -| Update Authority | [Pubkey](https://docs.rs/solana-program/latest/solana_program/pubkey/struct.Pubkey.html) | -| Creator | [Pubkey](https://docs.rs/solana-program/latest/solana_program/pubkey/struct.Pubkey.html) | Cannot be changed -| Is Mutable | Bool | -| Group ( Collection ) | Optional [Pubkey](https://docs.rs/solana-program/latest/solana_program/pubkey/struct.Pubkey.html) | -| Name | String | -| Symbol | String | -| Asset | | -| Extension | | \ No newline at end of file diff --git a/docs/src/programs.md b/docs/src/programs.md deleted file mode 100644 index 603a46e8..00000000 --- a/docs/src/programs.md +++ /dev/null @@ -1,10 +0,0 @@ -# Programs - -| Name | Address | -|:-------------------------------------------|:--------------------------------------------------------------------------| -| Metadata | LibrQsXf9V1DmTtJLkEghoaF1kjJcAzWiEGoJn8mz7p | -| Creator | 78deTr7qycJ6498vSd3pNMhdCKKWxMipniitVHQcM8RM | -| Creator Controls | G9whLiLT9nSkxwWzWvbiKKrTL6yWxvzh2UXqNht5VXqV | -| Shop | ListjawGEdhxuAErSyYwcTEGWQswFoi6FScnGG1RKSB | -| Inscriptions | inscokhJarcjaEs59QbQ7hYjrKz25LEPRfCbP8EmdUp | -| NFT | 9SXDHUdtfvBGT3H2uPCNEkxmWREoqdeS1qdBudLDD6KX | diff --git a/js-tests/creator.ts b/js-tests/creator.ts deleted file mode 100644 index 459b2040..00000000 --- a/js-tests/creator.ts +++ /dev/null @@ -1,176 +0,0 @@ -import * as anchor from '@coral-xyz/anchor'; -import { Program } from '@coral-xyz/anchor'; -import { - Keypair, - PublicKey, - SYSVAR_EPOCH_SCHEDULE_PUBKEY -} from '@solana/web3.js'; -import { LibreplexCreator } from '../target/types/libreplex_creator'; -import { LibreplexCreatorControls } from '../target/types/libreplex_creator_controls'; -import { LibreplexMetadata } from '../target/types/libreplex_metadata'; -import { LibreplexNft } from '../target/types/libreplex_nft'; - -import { - LIBREPLEX_CREATOR_CONTROLS_PROGRAM_ID, - mintFromCreatorController, - setupCreatorWithCustomSalePhases, - setupCollection -} from '@libreplex/sdk'; -import { sha256 } from 'js-sha256'; - -describe('libreplex creator', () => { - const provider = anchor.AnchorProvider.env(); - anchor.setProvider(provider); - - const program = anchor.workspace - .LibreplexCreator as Program; - const metadataProgram = anchor.workspace - .LibreplexMetadata as Program; - const nftProgram = anchor.workspace.LibreplexNft as Program; - const controllerProgram = anchor.workspace - .LibreplexCreatorControls as Program; - - console.log(Object.keys(anchor.workspace)); - - const authority = anchor.getProvider().publicKey; - - it('has minted', async () => { - const groupSeed = Keypair.generate(); - - console.log('Setting up group'); - const grpSetupCtx = await setupCollection({ - connector: { - type: 'provider', - provider, - }, - groupSeedKp: groupSeed, - groupAuthority: program.provider.publicKey as PublicKey, - input: { - description: 'A very cool group', - name: 'COOL GROUP', - symbol: 'COOL', - url: 'COOL.com/group', - royalties: { - bps: 100, - shares: [ - { - recipient: program.provider.publicKey as PublicKey, - share: 100, - }, - ], - }, - }, - }); - - const collection = grpSetupCtx.collection; - await grpSetupCtx.method.rpc({ - skipPreflight: false, - }); - - const startTime = new Date(); - startTime.setDate(startTime.getDate() - 1); - - const pingDiscrim = Buffer.from(sha256.digest('global:ping')).slice(0, 8); - - console.log('Setting up controller'); - const creatorControllerCtx = await setupCreatorWithCustomSalePhases( - { - collection, - metadataProgram, - mintAuthority: program.provider.publicKey as PublicKey, - program, - creatorData: { - baseName: 'COOL #', - baseUrl: { - type: 'json-prefix', - url: 'COOL.com/', - }, - description: 'The coolest metadatas', - ordered: false, - symbol: 'COOL', - supply: 2000, - }, - }, - controllerProgram, - [ - { - end: null, - start: startTime, - label: 'Public', - /* No controls anyone can mint and it's free*/ - control: [ - { - name: 'CustomProgram', - instructionData: pingDiscrim, - label: 'Ping', - programId: LIBREPLEX_CREATOR_CONTROLS_PROGRAM_ID, - remainingAccountsMetas: [ - { - isSigner: false, - isWritable: true, - key: { - type: 'key', - value: Keypair.generate().publicKey, - }, - }, - { - isSigner: false, - isWritable: true, - key: { - type: 'seedDerivation', - programId: SYSVAR_EPOCH_SCHEDULE_PUBKEY, - seeds: [ - { - type: 'mintPlaceHolder', - }, - ], - }, - }, - ], - }, - ], - }, - ] - ); - - await creatorControllerCtx.method.rpc(); - - const { creator, minterNumbers, creatorController } = creatorControllerCtx; - - // const controllerData = await controllerProgram.account.creatorController.fetch(creatorController) - - console.log('Creator initialised'); - - { - // Set some dummy values for transfer hook. - const mintMethod = await mintFromCreatorController({ - addTransferHookToMint: { - authority: program.provider.publicKey as PublicKey, - programId: program.provider.publicKey as PublicKey, - }, - creatorController: creatorControllerCtx.creatorController, - creatorControllerProgram: controllerProgram, - creatorProgram: program, - }); - - const txId = await mintMethod.method.rpc({ - skipPreflight: true, - }); - - console.log(txId); - } - - { - // Mint without transfer hook - const mintMethod = await mintFromCreatorController({ - creatorController: creatorControllerCtx.creatorController, - creatorControllerProgram: controllerProgram, - creatorProgram: program, - }); - - await mintMethod.method.rpc({ - skipPreflight: true, - }); - } - }); -}); diff --git a/js-tests/mint.ts b/js-tests/mint.ts deleted file mode 100644 index c8a06eae..00000000 --- a/js-tests/mint.ts +++ /dev/null @@ -1,144 +0,0 @@ -import * as anchor from "@coral-xyz/anchor"; -import { BN, Program } from "@coral-xyz/anchor"; -import { AnchorProvider, getProvider } from "@coral-xyz/anchor"; -import {mintSingle, setupCollection, setUserPermissionsForGroup, UserPermission, updateCollectionAuthority} from "@libreplex/sdk" -import { Keypair } from "@solana/web3.js"; - - -describe("mint", () => { - const provider = anchor.AnchorProvider.env() - anchor.setProvider(provider); - - it("has minted", async () => { - const mintCtx = (await mintSingle({ - provider, - mintData: { - assetUrl: { - type: "jsonUrl", - value: "COOL.com" - }, - name: "COOL", - symbol: "COOL", - } - })) - - await mintCtx.method.rpc() - }) - - it ("has minted to a collection", async () => { - const me = provider.publicKey - - const grpCtx = await setupCollection({ - connector: { - type: "provider", - provider, - }, - groupAuthority: me, - input: { - description: "A very cool group", - name: "COOLIO", - symbol: "GRP", - url: "COOL.com", - royalties: { - bps: 0, - shares: [{ - recipient: me, - share: 100, - }], - }, - permittedSigners: [], - onChainAttributes: [], - }, - }) - - await grpCtx.method.rpc() - - const collection = grpCtx.collection - - const mintCtx = (await mintSingle({ - provider, - mintData: { - assetUrl: { - type: "jsonUrl", - value: "COOL.com" - }, - name: "COOL", - symbol: "COOL", - }, - mintToCollection: { - collection, - checkValidGroup: true, - } - })) - - await mintCtx.method.rpc() - }) - - it ("has minted to a collection where I am not the authority", async () => { - const me = provider.publicKey - - const grpCtx = await setupCollection({ - connector: { - type: "provider", - provider, - }, - groupAuthority: me, - input: { - description: "A very cool group", - name: "COOLIO", - symbol: "GRP", - url: "COOL.com", - royalties: { - bps: 0, - shares: [], - }, - permittedSigners: [], - onChainAttributes: [], - }, - }) - - await grpCtx.method.rpc(); - - const collection = grpCtx.collection; - - await (await setUserPermissionsForGroup({ - connector: { - type: "provider", - provider, - }, - collection, - user: me, - groupUpdateAuthority: me, - permissions: [UserPermission.AddToGroup] - })).rpc() - - await (await updateCollectionAuthority({ - collection, - new_authority: Keypair.generate().publicKey, - connector: { - type: "provider", - provider, - }, - })).rpc() - - const mintCtx = (await mintSingle({ - provider, - mintData: { - assetUrl: { - type: "jsonUrl", - value: "COOL.com" - }, - name: "COOL", - symbol: "COOL", - }, - mintToCollection: { - collection, - checkValidGroup: true, - } - })) - - await mintCtx.method.rpc() - }) - - -}) \ No newline at end of file diff --git a/libraries/rarible-merkle-verify/Cargo.toml b/libraries/rarible-merkle-verify/Cargo.toml new file mode 100644 index 00000000..b2db1128 --- /dev/null +++ b/libraries/rarible-merkle-verify/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "rarible-merkle-verify" +version = "0.0.1" +edition = "2021" + +[dependencies] +anchor-lang = {version = "~0.30"} +anchor-spl = {version = "~0.30"} +solana-program = {version = "1.17.13"} + +[features] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] diff --git a/libraries/rarible-merkle-verify/src/lib.rs b/libraries/rarible-merkle-verify/src/lib.rs new file mode 100644 index 00000000..1a86fdbc --- /dev/null +++ b/libraries/rarible-merkle-verify/src/lib.rs @@ -0,0 +1,23 @@ +use solana_program::hash::hashv; + +/// modified version of https://github.com/saber-hq/merkle-distributor/blob/ac937d1901033ecb7fa3b0db22f7b39569c8e052/programs/merkle-distributor/src/merkle_proof.rs#L8 +/// This function deals with verification of Merkle trees (hash trees). +/// Direct port of https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.4.0/contracts/cryptography/MerkleProof.sol +/// Returns true if a `leaf` can be proved to be a part of a Merkle tree +/// defined by `root`. For this, a `proof` must be provided, containing +/// sibling hashes on the branch from the leaf to the root of the tree. Each +/// pair of leaves and each pair of pre-images are assumed to be sorted. +pub fn verify(proof: Vec<[u8; 32]>, root: [u8; 32], leaf: [u8; 32]) -> bool { + let mut computed_hash = leaf; + for proof_element in proof.into_iter() { + if computed_hash <= proof_element { + // Hash(current computed hash + current element of the proof) + computed_hash = hashv(&[&[1u8], &computed_hash, &proof_element]).to_bytes(); + } else { + // Hash(current element of the proof + current computed hash) + computed_hash = hashv(&[&[1u8], &proof_element, &computed_hash]).to_bytes(); + } + } + // Check if the computed hash (root) is equal to the provided root + computed_hash == root +} diff --git a/package.json b/package.json index 5753d190..858ffa5e 100644 --- a/package.json +++ b/package.json @@ -8,49 +8,22 @@ "build-interfaces": "cd packages/libreplex-idls && yarn package && yarn build && cd .. && cd libreplex-sdk && yarn build && cd ../..", "build": "lerna run clean && lerna run build", "libre-cli": "ts-node src/cli/index.ts", - "libreplex_nifty_hybrid:deploy:devnet": "anchor deploy -p libreplex_nifty_hybrid --provider.cluster https://api.devnet.solana.com --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_monoswap:deploy:devnet": "anchor deploy -p libreplex_monoswap --provider.cluster https://api.devnet.solana.com --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_monoswap:deploy:mainnet": "anchor deploy -p libreplex_monoswap --provider.cluster https://api.mainnet-beta.solana.com --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_pipelines:deploy:devnet": "anchor deploy -p libreplex_pipelines --provider.cluster https://api.devnet.solana.com --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_pipelines:deploy:mainnet": "anchor deploy -p libreplex_pipelines --provider.cluster https://api.mainnet-beta.solana.com --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_editions:deploy:devnet": "anchor deploy -p libreplex_editions --provider.cluster https://api.devnet.solana.com --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_editions_controls:deploy:devnet": "anchor deploy -p libreplex_editions_controls --provider.cluster https://api.devnet.solana.com --provider.wallet ~/.config/solana/id.json", - "libreplex_editions:deploy:mainnet": "anchor deploy -p libreplex_editions --provider.cluster https://api.mainnet-beta.solana.com --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_editions:deploy:eclipse": "anchor deploy -p libreplex_editions --provider.cluster https://testnet.dev2.eclipsenetwork.xyz --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_editions:deploy:eclipse-dev": "anchor deploy -p libreplex_editions --provider.cluster https://staging-rpc.dev.eclipsenetwork.xyz --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_liquidity:deploy:devnet": "anchor deploy -p libreplex_liquidity --provider.cluster https://api.devnet.solana.com --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_liquidity:deploy:mainnet": "anchor build -p libreplex_liquidity&& anchor deploy -p libreplex_liquidity --provider.cluster https://api.mainnet-beta.solana.com --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_inscriptions:deploy:eclipse-testnet": "anchor build -p libreplex_inscriptions && anchor deploy -p libreplex_inscriptions --provider.cluster https://testnet.dev2.eclipsenetwork.xyz --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_inscriptions:deploy:localnet": "anchor build -p libreplex_inscriptions && anchor deploy -p libreplex_inscriptions --provider.cluster http://localhost:8899 --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_metadata:deploy:localnet": "anchor build -p libreplex_metadata && anchor deploy -p libreplex_metadata --provider.cluster http://localhost:8899 --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_creator:deploy:devnet": "anchor build -p libreplex_creator && anchor deploy -p libreplex_creator --provider.cluster https://api.devnet.solana.com --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_creator_controls:deploy:devnet": "anchor build -p libreplex_creator_controls && anchor deploy -p libreplex_creator_controls --provider.cluster https://api.devnet.solana.com --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_inscriptions:deploy:devnet": "anchor build -p libreplex_inscriptions && anchor deploy -p libreplex_inscriptions --provider.cluster https://api.devnet.solana.com --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_metadata:deploy:devnet": "anchor build -p libreplex_metadata && anchor deploy -p libreplex_metadata --provider.cluster https://api.devnet.solana.com --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_shop:deploy:devnet": "anchor build -p libreplex_shop && anchor deploy -p libreplex_shop --provider.cluster https://api.devnet.solana.com --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_legacy:deploy:devnet": "anchor build -p libreplex_legacy && anchor deploy -p libreplex_legacy --provider.cluster https://api.devnet.solana.com --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_legacy:deploy:mainnet": "anchor build -p libreplex_legacy && anchor deploy -p libreplex_legacy --provider.cluster https://api.mainnet-beta.solana.com --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_metadata:deploy:mainnet": "anchor build -p libreplex_metadata && anchor deploy -p libreplex_metadata --provider.cluster https://api.mainnet-beta.solana.com --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_creator:deploy:mainnet": "anchor build -p libreplex_creator && anchor deploy -p libreplex_creator --provider.cluster https://api.mainnet-beta.solana.com --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_inscriptions:deploy:mainnet": "anchor build -p libreplex_inscriptions && anchor deploy -p libreplex_inscriptions --provider.cluster https://api.mainnet-beta.solana.com --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_creator_controls:deploy:mainnet": "anchor build -p libreplex_creator_controls && anchor deploy -p libreplex_creator_controls --provider.cluster https://api.mainnet-beta.solana.com --provider.wallet ~/.config/solana/libreplex-deployer.json", - "libreplex_shop:deploy:mainnet": "anchor build -p libreplex_creator_controls && anchor deploy -p libreplex_creator_controls --provider.cluster https://api.mainnet-beta.solana.com --provider.wallet ~/.config/solana/libreplex-deployer.json", + "lint": "next lint", "libreplex_editions:deploy:eclipse-dev2": "anchor deploy -p libreplex_editions --provider.cluster https://staging-rpc.dev2.eclipsenetwork.xyz --provider.wallet ~/.config/solana/id.json", "libreplex_editions_controls:deploy:eclipse-dev2": "anchor build -p libreplex_editions_controls && anchor deploy -p libreplex_editions_controls --provider.cluster https://staging-rpc.dev2.eclipsenetwork.xyz --provider.wallet ~/.config/solana/id.json", + + "libreplex_editions:deploy:eclipse-testnet": "anchor build -p libreplex_editions && anchor deploy -p libreplex_editions --provider.cluster https://testnet.dev2.eclipsenetwork.xyz --provider.wallet ~/.config/solana/id.json", + "libreplex_editions_controls:deploy:eclipse-testnet": "anchor build -p libreplex_editions_controls && anchor deploy -p libreplex_editions_controls --provider.cluster https://testnet.dev2.eclipsenetwork.xyz --provider.wallet ~/.config/solana/id.json", "libreplex_editions:deploy:solana-dev-net": "anchor build -p libreplex_editions && anchor deploy -p libreplex_editions --provider.cluster https://api.devnet.solana.com --provider.wallet ~/.config/solana/id.json", "libreplex_editions_controls:solana-dev-net": "anchor build -p libreplex_editions_controls && anchor deploy -p libreplex_editions_controls --provider.cluster https://api.devnet.solana.com --provider.wallet ~/.config/solana/id.json", + "transfer-program-owner:solana-dev-net": "solana program set-upgrade-authority --skip-new-upgrade-authority-signer-check 6g8ZaBxx2tng8EhVqvaad948Vht9YFJFGG5gvAejrJj9 --new-upgrade-authority 674s1Sap3KVnr8WGrY5KGQ69oTYjjgr1disKJo6GpTYw -k ~/.config/solana/id.json -u https://api.devnet.solana.com", "transfer-program-owner-back:solana-dev-net": "solana program set-upgrade-authority --skip-new-upgrade-authority-signer-check 6g8ZaBxx2tng8EhVqvaad948Vht9YFJFGG5gvAejrJj9 --new-upgrade-authority QjzRL6VwKGnpco8wx3cPjtq8ZPhewy7ohq7F5mv2eeR -k usb://ledger -u https://api.devnet.solana.com", - "libreplex_editions:deploy:solana-test-net": "anchor build -p libreplex_editions && anchor deploy -p libreplex_editions --provider.cluster https://api.testnet.solana.com --provider.wallet ~/.config/solana/prod-keypair.json", "libreplex_editions_controls:solana-test-net": "anchor build -p libreplex_editions_controls && anchor deploy -p libreplex_editions_controls --provider.cluster https://api.testnet.solana.com --provider.wallet ~/.config/solana/prod-keypair.json", - - "libreplex_editions:deploy:eclipse-testnet": "anchor build -p libreplex_editions && anchor deploy -p libreplex_editions --provider.cluster https://testnet.dev2.eclipsenetwork.xyz --provider.wallet ~/.config/solana/prod-keypair.json", - "libreplex_editions_controls:deploy:eclipse-testnet": "anchor build -p libreplex_editions_controls && anchor deploy -p libreplex_editions_controls --provider.cluster https://testnet.dev2.eclipsenetwork.xyz --provider.wallet ~/.config/solana/prod-keypair.json", - "lint": "next lint", "libreplex_editions:deploy:ledger:solana-dev-net": "anchor build -p libreplex_editions && anchor deploy -p libreplex_editions --provider.cluster https://api.devnet.solana.com --provider.wallet usb://ledger", "libreplex_editions_controls:deploy:ledger:solana-dev-net": "anchor build -p libreplex_editions_controls && anchor deploy -p libreplex_editions_controls --provider.cluster https://api.devnet.solana.com --provider.wallet usb://ledger", @@ -64,8 +37,9 @@ }, "dependencies": { "@coral-xyz/anchor": "0.30.0", - "@solana/spl-token": "^0.3.8", - "@solana/web3.js": "^1.73.0" + "@solana/spl-token": "^0.4.8", + "@solana/web3.js": "^1.73.0", + "@solana/spl-token-metadata": "^0.1.5" }, "devDependencies": { "@types/chai": "^4.3.5", diff --git a/packages/libreplex-idls/package.json b/packages/libreplex-idls/package.json deleted file mode 100644 index 63935387..00000000 --- a/packages/libreplex-idls/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@libreplex/idls", - "version": "0.5.2", - "description": "Idls for libreplex programs", - "sideEffects": false, - "files": [ - "lib" - ], - "types": "./lib/types/index.d.ts", - "exports": { - "./lib/esm/*": "./lib/esm/*", - "./lib/cjs/*": "./lib/cjs/*.js", - "./lib/types/*": "./lib/types/*" - }, - "scripts": { - "clean": "npx shx mkdir -p lib && npx shx rm -rf lib", - "package": "npx shx mkdir -p lib/cjs lib/esm lib/types", - "build": "tsc -b ./tsconfig.cjs.json ./tsconfig.esm.json ./tsconfig.types.json" - }, - "keywords": [], - "author": "", - "license": "ISC" -} diff --git a/packages/libreplex-idls/tsconfig.cjs.json b/packages/libreplex-idls/tsconfig.cjs.json deleted file mode 100644 index ff528415..00000000 --- a/packages/libreplex-idls/tsconfig.cjs.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "include": ["../../target/types/**/*"], - "extends": "./tsconfig.json", - "compilerOptions": { - "target": "ES2016", - "module": "CommonJS", - "sourceMap": false, - "outDir": "lib/cjs", - "rootDir": "../../target/types", - } - } \ No newline at end of file diff --git a/packages/libreplex-idls/tsconfig.esm.json b/packages/libreplex-idls/tsconfig.esm.json deleted file mode 100644 index d1625681..00000000 --- a/packages/libreplex-idls/tsconfig.esm.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "./tsconfig.json", - "include": ["../../target/types"], - "compilerOptions": { - "rootDir": "../../target/types", - "outDir": "lib/esm", - "declarationDir": "lib/types", - "target": "ESNext", - "module": "ESNext", - "sourceMap": false, - "declaration": true, - "composite": true - } -} diff --git a/packages/libreplex-idls/tsconfig.json b/packages/libreplex-idls/tsconfig.json deleted file mode 100644 index d01bd73a..00000000 --- a/packages/libreplex-idls/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "include": [], - "compilerOptions": { - "lib": ["ES2021.String"], - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Node", - "esModuleInterop": true, - "isolatedModules": true, - "noImplicitAny": false, - "noEmitOnError": true, - "resolveJsonModule": true, - "strict": true, - "stripInternal": true, - "skipLibCheck": true, - "noEmit": false, - "allowSyntheticDefaultImports": true, - }, - "references": [ - { - "path": "./tsconfig.esm.json" - }, - { - "path": "./tsconfig.cjs.json" - }, - ] -} diff --git a/packages/libreplex-idls/tsconfig.types.json b/packages/libreplex-idls/tsconfig.types.json deleted file mode 100644 index 0c118a89..00000000 --- a/packages/libreplex-idls/tsconfig.types.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "include": ["../../target/types/**/*"], - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./lib/types", - "declaration": true, - "emitDeclarationOnly": true - } -} diff --git a/packages/libreplex-sdk/README.md b/packages/libreplex-sdk/README.md deleted file mode 100644 index 455182ec..00000000 --- a/packages/libreplex-sdk/README.md +++ /dev/null @@ -1,9 +0,0 @@ -
- - -

Libreplex

- -

- Documentation (stable) -

-
\ No newline at end of file diff --git a/packages/libreplex-sdk/package.json b/packages/libreplex-sdk/package.json deleted file mode 100644 index 096c92d0..00000000 --- a/packages/libreplex-sdk/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@libreplex/sdk", - "version": "0.3.5", - "description": "", - "main": "./lib/cjs/src/index.js", - "module": "./lib/esm/src/index.js", - "types": "./lib/types/src/index.d.ts", - "sideEffects": false, - "files": [ - "lib" - ], - "dependencies": { - "@libreplex/idls": "0.5.2", - "@solana/spl-token": "0.3.8" - }, - "devDependencies": { - "shx": "^0.3.4" - }, - "exports": { - "import": "./lib/esm/src/index.js", - "require": "./lib/cjs/src/index.js", - "types": "./lib/types/src/index.d.ts" - }, - "scripts": { - "clean": "npx shx mkdir -p lib && npx shx rm -rf lib", - "build": "tsc --build" - }, - "keywords": [], - "author": "", - "license": "ISC" -} diff --git a/packages/libreplex-sdk/src/constants.ts b/packages/libreplex-sdk/src/constants.ts deleted file mode 100644 index 9ef6b4a7..00000000 --- a/packages/libreplex-sdk/src/constants.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { PublicKey } from "@solana/web3.js"; - -export const LIBREPLEX_METADATA_PROGRAM_ID = new PublicKey("LibrQsXf9V1DmTtJLkEghoaF1kjJcAzWiEGoJn8mz7p") -export const LIBREPLEX_CREATOR_CONTROLS_PROGRAM_ID = new PublicKey("G9whLiLT9nSkxwWzWvbiKKrTL6yWxvzh2UXqNht5VXqV") -export const LIBREPLEX_CREATOR_PROGRAM_ID = new PublicKey("78deTr7qycJ6498vSd3pNMhdCKKWxMipniitVHQcM8RM") -export const LIBREPLEX_NFT_PROGRAM_ID = new PublicKey("9SXDHUdtfvBGT3H2uPCNEkxmWREoqdeS1qdBudLDD6KX") \ No newline at end of file diff --git a/packages/libreplex-sdk/src/createCollection.ts b/packages/libreplex-sdk/src/createCollection.ts deleted file mode 100644 index 13b3e2d9..00000000 --- a/packages/libreplex-sdk/src/createCollection.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Program, BN, Provider } from "@coral-xyz/anchor"; -import { Keypair, PublicKey, SystemProgram } from "@solana/web3.js"; -import {LibreplexMetadata} from "@libreplex/idls/lib/types/libreplex_metadata" -import { getCollectionAddress } from "./pda"; -import { loadMetadataProgram } from "./programs"; - -type AttributeType = { - name: string, - possibleValues: (string | BN | number)[] -} - -export type RoyaltyConfig = { - bps: number, - shares: { - recipient: PublicKey, - share: number, - }[] -} - -type SetupCollectionInput = { - name: string, - symbol: string, - url: string, - royalties: RoyaltyConfig - description: string, - - /** - * The set of possible attributes for metadatas in your collection. - */ - onChainAttributes?: AttributeType[] - - /** - * The set of all addresses that are allowed to sign your collection. - */ - permittedSigners?: PublicKey[] -} - - -export type Connector = { - type: "provider", - provider: Provider, -} | { - type: "program", - metadataProgram: Program, -} - - -export async function setupCollection( - groupInfo: { - connector: Connector, - input: SetupCollectionInput, - collectionAuthority: PublicKey, - groupSeedKp?: Keypair - } - ) { - const { - connector, - input, - collectionAuthority, - groupSeedKp = Keypair.generate() - } = groupInfo - const collection = getCollectionAddress(groupSeedKp.publicKey) - - const metadataProgram = connector.type === "program" ? connector.metadataProgram : await loadMetadataProgram(connector.provider) - - return { - method: metadataProgram.methods.createCollection({ - permittedSigners: input.permittedSigners || [], - attributeTypes: input.onChainAttributes?.map(v => { - return { - permittedValues: v.possibleValues, - continuedFromIndex: null, - continuedAtIndex: null, - deleted: false, - name: v.name, - } - }) || [], - description: input.description, - name: input.name, - symbol: input.symbol, - url: input.url, - royalties: input.royalties - }).accounts({ - authority: collectionAuthority, - seed: groupSeedKp.publicKey, - systemProgram: SystemProgram.programId, - collection, - }), - - collection, - } - } - \ No newline at end of file diff --git a/packages/libreplex-sdk/src/creatorControls.ts b/packages/libreplex-sdk/src/creatorControls.ts deleted file mode 100644 index 441a3725..00000000 --- a/packages/libreplex-sdk/src/creatorControls.ts +++ /dev/null @@ -1,306 +0,0 @@ -import {BN, IdlAccounts, IdlTypes} from "@coral-xyz/anchor" -import { PublicKey } from "@solana/web3.js" -import {LibreplexCreatorControls} from "@libreplex/idls/lib/types/libreplex_creator_controls" - -export type CreatorControl = SolPaymentControl | - MintLimitControl | - SplPaymentControl | - AllowListControl | - CustomProgramControl - -export type SolPaymentControl = { - name: "SolPayment", - price: BN, - receiver: PublicKey, -} - -export type MintLimitControl = { - name: "MintLimitControl", - - amount: number, - - // Is this a global limit or does each buy have their own limit - scopedToBuyer: boolean, - - extraSeeds: PublicKey[] -} - - -export type SplPaymentControl = { - name: "SplPayment", - amount: BN, - mint: PublicKey, - recepient: PublicKey, - tokenProgram: PublicKey, -} - -export type AllowListControl = { - label: string, - name: "AllowList", - merkleRoot: number[], -} - - -type CustomProgramAccountMetaKeySeed = { - type: "bytes", - value: Buffer -} | { - type: "mintPlaceHolder", -} | { - type: "receiverPlaceHolder", -} | { - type: "payerPlaceHolder", -} - -type CustomProgramAccountMetaKey = { - type: "key", - value: PublicKey, -} | { - type: "seedDerivation", - programId: PublicKey, - seeds: CustomProgramAccountMetaKeySeed[], -} - -export type CustomProgramControl = { - label: string, - name: "CustomProgram", - programId: PublicKey, - instructionData: Buffer, - remainingAccountsMetas: { - isSigner: boolean, - isWritable: boolean, - key: CustomProgramAccountMetaKey, - }[], -} - -export function anchorToControl(c: IdlAccounts["creatorController"]["phases"][0]["controls"][0]): CreatorControl { - if (c.allowList) { - return { - name: "AllowList", - label: c.allowList[0].label, - merkleRoot: c.allowList[0].root - } - } - - if (c.customProgram) { - return { - name: "CustomProgram", - instructionData: c.customProgram[0].instructionData, - label: c.customProgram[0].label, - programId: c.customProgram[0].programId, - remainingAccountsMetas: c.customProgram[0].remainingAccountMetas.map(m => { - let anchorKey: IdlTypes["CustomProgramAcountMetaKey"] = m.key as any; - - let key: CustomProgramAccountMetaKey; - if (anchorKey.pubkey) { - key = { - type: "key", - value: anchorKey.pubkey[0] - } - } - else if (anchorKey.derivedFromSeeds) { - const anchorSeeds = anchorKey.derivedFromSeeds[0].seeds; - - const seeds: CustomProgramAccountMetaKeySeed[] = [] - for (let anchorSeed of anchorSeeds) { - if (anchorSeed.bytes) { - seeds.push({ - type: "bytes", - value: anchorSeed.bytes[0] - }) - } - else if (anchorSeed.mintPlaceHolder) { - seeds.push({ - type: "mintPlaceHolder", - }) - } - else if (anchorSeed.payerPlaceHolder) { - seeds.push({ - type: "payerPlaceHolder", - }) - } - else if (anchorSeed.receiverPlaceHolder) { - seeds.push({ - type: "receiverPlaceHolder", - }) - } - else { - throw new Error("Unsupported custom program anchor seed.") - } - } - - key = { - type: "seedDerivation", - seeds, - programId: anchorKey.derivedFromSeeds[0].programId, - } - } - else { - throw new Error("Unsupported custom program anchor key.") - } - - - return { - isSigner: m.isSigner, - isWritable: m.isWritable, - key, - } - }), - } - } - - if (c.mintLimit) { - return { - name: "MintLimitControl", - amount: c.mintLimit[0].limit, - extraSeeds: c.mintLimit[0].accountKey, - scopedToBuyer: c.mintLimit[0].scopedToBuyer, - } - } - - if (c.payment) { - const control = c.payment[0]; - - return { - name: "SolPayment", - price: control.amount, - receiver: control.recepient - } - } - - if (c.splPayment) { - const control = c.splPayment[0] - - return { - name: "SplPayment", - amount: control.amount, - mint: control.mint, - recepient: control.recepient, - tokenProgram: control.tokenProgram - } - } - - throw new Error("Tried to convert invalid anchor control") -} - -export function controlToAnchor(c: CreatorControl): IdlTypes["ControlType"] { - switch (c.name) { - case "SolPayment": - return { - payment: { - 0: { - amount: c.price, - recepient: c.receiver, - } - } - } - - case "AllowList": - return { - allowList: { - "0": { - root: c.merkleRoot, - label: c.label, - } - } - } - - case "CustomProgram": - - const remainingAccountMetas: IdlTypes["CustomProgramAccountMeta"][] = [] - - for (let meta of c.remainingAccountsMetas) { - let key: IdlTypes["CustomProgramAcountMetaKey"]; - - switch (meta.key.type) { - case "key": - key = { - pubkey: { - "0": meta.key.value, - } - } - break; - case "seedDerivation": - const seeds: IdlTypes["Seed"][] = [] - - for (const seed of meta.key.seeds) { - switch (seed.type) { - case "bytes": - seeds.push({ - bytes: { - "0": seed.value - } - }) - break; - case "mintPlaceHolder": - seeds.push({ - mintPlaceHolder: {} - }) - break; - case "receiverPlaceHolder": - seeds.push({ - receiverPlaceHolder: {} - }) - break; - case "payerPlaceHolder": - seeds.push({ - payerPlaceHolder: {} - }) - break; - - default: - throw new Error(`Invalid seed ${seed}`) - } - } - - key = { - derivedFromSeeds: { - "0": { - programId: meta.key.programId, - seeds, - } - } - } - break; - default: - throw new Error(`Invalid custom program account meta key type. ${meta.key}`) - } - - remainingAccountMetas.push({ - isSigner: meta.isSigner, - isWritable: meta.isWritable, - key, - }) - } - - return { - customProgram: { - 0: { - ...c, - remainingAccountMetas - } - } - } - - case "MintLimitControl": - return { - mintLimit: { - "0": { - limit: c.amount, - scopedToBuyer: c.scopedToBuyer, - accountKey: c.extraSeeds, - } - } - } - - case "SplPayment": - return { - splPayment: { - "0": c - } - } - - default: - throw new Error("Invalid control") - }; -} \ No newline at end of file diff --git a/packages/libreplex-sdk/src/groupPermissions.ts b/packages/libreplex-sdk/src/groupPermissions.ts deleted file mode 100644 index a8f4b3f7..00000000 --- a/packages/libreplex-sdk/src/groupPermissions.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Program } from "@coral-xyz/anchor" -import { PublicKey, SystemProgram } from "@solana/web3.js" -import {LibreplexMetadata} from "@libreplex/idls/lib/types/libreplex_metadata"; - -import { LIBREPLEX_METADATA_PROGRAM_ID } from "./constants"; -import { Connector } from "./createCollection"; -import { loadMetadataProgram } from "./programs"; - -export enum UserPermission { - Update, - Delete, - AddToGroup, - } - - export function getCollectionWideUserPermissionsAddress(collection: PublicKey, user: PublicKey, program = LIBREPLEX_METADATA_PROGRAM_ID) { - return PublicKey.findProgramAddressSync([Buffer.from("permissions"), user.toBuffer(), collection.toBuffer()], program)[0] - - } - - function convertPermission(p: UserPermission) { - if (p === UserPermission.AddToGroup) { - return { - addToGroup: {} - } - } - - if (p === UserPermission.Delete) { - return { - addToGroup: {} - } - } - - if (p === UserPermission.Update) { - return { - addToGroup: {} - } - } - - throw new Error("Invalid permission enum") - } - - -export async function setUserPermissionsForGroup( - { - connector, - collection, - user, - permissions, - groupUpdateAuthority, - }: { - connector: Connector, - collection: PublicKey, - user: PublicKey, - permissions: UserPermission[], - groupUpdateAuthority: PublicKey, - } - ) { - const permissionsAccountAddress = getCollectionWideUserPermissionsAddress(collection, user) - - const metadataProgram = connector.type === "program" ? connector.metadataProgram : await loadMetadataProgram(connector.provider) - - - const existingPermissionsInfo = await metadataProgram.provider.connection.getAccountInfo(permissionsAccountAddress) - - const anchorPermissions = permissions.map(convertPermission); - - if (!existingPermissionsInfo) { - return metadataProgram.methods.delegateCollectionPermissions({ - permissions: anchorPermissions, - }).accounts({ - collection, - delegatedUser: user, - systemProgram: SystemProgram.programId, - updateAuthority: groupUpdateAuthority, - userPermissions: permissionsAccountAddress - }) - } - - return metadataProgram.methods.updatePermissions({ - permissions: anchorPermissions - }).accounts({ - updateAuthority: groupUpdateAuthority, - user: user, - userPermissions: permissionsAccountAddress - }) - } - \ No newline at end of file diff --git a/packages/libreplex-sdk/src/index.ts b/packages/libreplex-sdk/src/index.ts deleted file mode 100644 index 47a875b2..00000000 --- a/packages/libreplex-sdk/src/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -export {setupCollection} from "./createCollection" -export * from "./constants" -export * from "./pda" -export {setUserPermissionsForGroup, UserPermission} from "./groupPermissions" - -export {setupCreator, setupCreatorWithCustomSalePhases} from "./setupCreator" -export type {Phase} from "./setupCreator" - -export {mintFromCreatorController, mintFromCreatorControllerState} from "./mint" - -export type {CreatorControl, AllowListControl, CustomProgramControl, - MintLimitControl, SolPaymentControl, SplPaymentControl as SplPayment} from "./creatorControls" - -export {anchorToControl, controlToAnchor} from "./creatorControls" - -export {updateCreator} from "./updateCreator" - - -export type {UpdateCreatorInput} from "./updateCreator" - -export {mintSingle, setupLibreplexReadyMint} from "./mint" - -export {updateCollectionAuthority} from "./updateCollection" \ No newline at end of file diff --git a/packages/libreplex-sdk/src/mint.ts b/packages/libreplex-sdk/src/mint.ts deleted file mode 100644 index c12ea3df..00000000 --- a/packages/libreplex-sdk/src/mint.ts +++ /dev/null @@ -1,800 +0,0 @@ -import { - AccountMeta, - Connection, - Keypair, - PublicKey, - SYSVAR_SLOT_HASHES_PUBKEY, - SystemProgram, - Transaction, - TransactionInstruction, -} from '@solana/web3.js'; -import { LibreplexCreator } from '@libreplex/idls/lib/types/libreplex_creator'; -import { LibreplexMetadata } from '@libreplex/idls/lib/types/libreplex_metadata'; -import { LibreplexNft } from '@libreplex/idls/lib/types/libreplex_nft'; -import { LibreplexCreatorControls } from '@libreplex/idls/lib/types/libreplex_creator_controls'; -import { - Program, - AccountClient, - IdlAccounts, - IdlTypes, - Provider, -} from '@coral-xyz/anchor'; -import { - LIBREPLEX_CREATOR_CONTROLS_PROGRAM_ID, - LIBREPLEX_CREATOR_PROGRAM_ID, - LIBREPLEX_METADATA_PROGRAM_ID, - LIBREPLEX_NFT_PROGRAM_ID, -} from './constants'; -import { - MINT_SIZE, - TOKEN_2022_PROGRAM_ID, - createInitializeMint2Instruction, - getMinimumBalanceForRentExemptMint, - getAssociatedTokenAddressSync, - createAssociatedTokenAccountInstruction, - createMintToInstruction, -} from '@solana/spl-token'; -import { struct, u8 } from '@solana/buffer-layout'; -import { publicKey } from '@solana/buffer-layout-utils'; -import { getMetadataAddress, getMintWrapperAddress } from './pda'; -import { getCollectionWideUserPermissionsAddress } from './groupPermissions'; - -import { RoyaltyConfig } from './createCollection'; -import { loadMetadataProgram, loadNftProgram } from './programs'; - -type CustomProgramAccountMeta = Omit & { - key: - | { - keyType: 'PublicKey'; - value: PublicKey; - } - | { - keyType: 'PDA'; - seeds: Buffer[]; - deriveFromMint: boolean; - deriveFromBuyer: boolean; - programIdToDeriveFrom: PublicKey; - }; -}; - -export type MintFromCreatorControllerInput = { - creatorControllerProgram: Program; - creatorProgram: Program; - creatorController: PublicKey; - - mintKeyPair?: Keypair; - - // If there are multiple active sale phases, specify the one to mint in. - phaseToMintIn?: string; - - merkleProofsForAllowLists?: { - label: string; - proof: Buffer[]; - }[]; - - addTransferHookToMint?: { - programId: PublicKey; - authority: PublicKey; - }; -}; - -type MintFromCreatorControllerStateInput = { - creator: PublicKey; - targetPhase: IdlAccounts['creatorController']['phases'][0]; - minterNumbers: PublicKey | null; - collection: PublicKey; -} & Omit; - -export async function mintFromCreatorControllerState( - input: MintFromCreatorControllerStateInput -) { - const { - creatorControllerProgram, - creatorController, - merkleProofsForAllowLists, - addTransferHookToMint, - minterNumbers, - targetPhase, - collection, - creator, - } = input; - - let mintKeyPair = input.mintKeyPair || Keypair.generate(); - - const connection = creatorControllerProgram.provider.connection; - const me = creatorControllerProgram.provider.publicKey; - - if (!me) { - throw new Error('Provider not setup. Perhaps your wallet is not connected'); - } - - const args: Buffer[] = []; - const remainingAccounts: AccountMeta[] = []; - - const controls = targetPhase.controls; - - for (const control of controls) { - if (control.payment) { - remainingAccounts.push({ - isSigner: false, - isWritable: true, - pubkey: control.payment[0].recepient, - }); - } else if (control.splPayment) { - remainingAccounts.push({ - isSigner: false, - isWritable: true, - pubkey: control.splPayment[0].recepient, - }); - - remainingAccounts.push({ - isSigner: false, - isWritable: true, - pubkey: getAssociatedTokenAddressSync( - control.splPayment[0].mint, - me, - undefined, - control.splPayment[0].tokenProgram - ), - }); - - remainingAccounts.push({ - isSigner: false, - isWritable: false, - pubkey: control.splPayment[0].tokenProgram, - }); - } else if (control.mintLimit) { - const seeds: Buffer[] = [Buffer.from('mint_limit')]; - - if (control.mintLimit[0].scopedToBuyer) { - seeds.push(me.toBuffer()); - } - - control.mintLimit[0].accountKey.forEach((keyElement) => { - seeds.push(keyElement.toBuffer()); - }); - - const mintLimitAccount = PublicKey.findProgramAddressSync( - seeds, - LIBREPLEX_CREATOR_CONTROLS_PROGRAM_ID - )[0]; - - remainingAccounts.push({ - isSigner: false, - isWritable: true, - pubkey: mintLimitAccount, - }); - } else if (control.allowList) { - if (!merkleProofsForAllowLists) { - throw new Error( - 'Must provide merkle proofs when your creator as an allowlist' - ); - } - - const proofEntry = merkleProofsForAllowLists.find( - (mp) => mp.label === control.allowList[0].label - ); - - if (!proofEntry) { - throw new Error( - `Proof entry not found for allowlist: ${control.allowList[0].label}` - ); - } - - args.push(Buffer.concat(proofEntry.proof)); - } else if (control.customProgram) { - const remainingAccountMetas: AccountMeta[] = [ - { - isSigner: false, - isWritable: false, - pubkey: control.customProgram[0].programId, - }, - ]; - - for (const meta of control.customProgram[0].remainingAccountMetas) { - const key: IdlTypes['CustomProgramAcountMetaKey'] = - meta.key as any; - - if (key.pubkey) { - remainingAccountMetas.push({ - ...meta, - pubkey: key.pubkey[0], - }); - } else if (key.derivedFromSeeds) { - const programId = key.derivedFromSeeds[0].programId; - - const seeds: Buffer[] = []; - for (const seed of key.derivedFromSeeds[0].seeds) { - if (seed.bytes) { - seeds.push(seed.bytes[0]); - } else if (seed.mintPlaceHolder) { - seeds.push(mintKeyPair.publicKey.toBuffer()); - } else if (seed.payerPlaceHolder || seed.receiverPlaceHolder) { - seeds.push(me.toBuffer()); - } else { - throw new Error('Invalid seed derivation'); - } - } - - remainingAccountMetas.push({ - ...meta, - pubkey: PublicKey.findProgramAddressSync(seeds, programId)[0], - }); - } else { - throw new Error('Invalid CustomProgramAcountMetaKey'); - } - } - - remainingAccounts.push(...remainingAccountMetas); - } - } - - const metadata = getMetadataAddress(mintKeyPair.publicKey); - const setupMintCtx = await setupLibreplexReadyMint( - connection, - me, - me, - me, - me, - 0, - mintKeyPair, - metadata, - addTransferHookToMint - ); - - return { - method: creatorControllerProgram.methods - .mint({ - chosenPhase: targetPhase.label, - args, - }) - .accounts({ - attributeConfig: null, - creator, - creatorController, - collection, - libreplexCreatorProgram: LIBREPLEX_CREATOR_PROGRAM_ID, - libreplexMetadataProgram: LIBREPLEX_METADATA_PROGRAM_ID, - libreplexNftProgram: LIBREPLEX_NFT_PROGRAM_ID, - mint: mintKeyPair.publicKey, - metadata, - mintAuthority: me, - minterNumbers, - mintWrapper: getMintWrapperAddress(mintKeyPair.publicKey), - payer: me, - receiver: me, - receiverTokenAccount: getAssociatedTokenAddressSync( - mintKeyPair.publicKey, - me, - undefined, - TOKEN_2022_PROGRAM_ID - ), - recentSlothashes: SYSVAR_SLOT_HASHES_PUBKEY, - systemProgram: SystemProgram.programId, - tokenProgram: TOKEN_2022_PROGRAM_ID, - collectionPermissions: getCollectionWideUserPermissionsAddress( - collection, - creator - ), - }) - .preInstructions([...setupMintCtx.transaction.instructions]) - .signers([mintKeyPair]) - .remainingAccounts(remainingAccounts), - - mint: mintKeyPair, - }; -} - -export async function mintFromCreatorController( - input: MintFromCreatorControllerInput -) { - const { - creatorControllerProgram, - creatorController, - creatorProgram, - phaseToMintIn, - } = input; - - const controller = - await creatorControllerProgram.account.creatorController.fetchNullable( - creatorController - ); - - if (!controller) { - throw new Error( - `Creator controller at address: ${creatorController.toString()} not found` - ); - } - - const creator = await creatorProgram.account.creator.fetchNullable( - controller.creator - ); - - if (!creator) { - throw new Error( - `Creator at address ${controller.creator?.toString()} not found` - ); - } - - const now = Date.now() / 1000; - - const availableSalePhases = controller.phases; - - const activePhases = availableSalePhases.filter( - (ph) => now > ph.start && (ph.end === null || now < ph.end) - ); - - if (activePhases.length === 0) { - throw new Error('No currently active phases to mint from'); - } - - let targetPhase = activePhases[0]; - - if (activePhases.length > 1) { - if (!phaseToMintIn) { - throw new Error( - 'Must provide a target phase to mint in when multiple are active' - ); - } - - const maybeTargetPhase = activePhases.find( - (ph) => ph.label === phaseToMintIn - ); - - if (!maybeTargetPhase) { - throw new Error( - `Specified phase to mint in ${phaseToMintIn} is not active` - ); - } - - targetPhase = maybeTargetPhase; - } - - return mintFromCreatorControllerState({ - ...input, - targetPhase, - creator: controller.creator, - minterNumbers: creator.minterNumbers, - collection: creator.collection, - }); -} - -export type MintAssetUrl = - | { - type: 'jsonUrl'; - value: string; - } - | { - type: 'imageUrl'; - value: string; - } - | { - type: 'renderedOnChain'; - programId: PublicKey; - description: string | null; - }; - -export type MetadataExtension = { - // Metadata extension data. If you don't want it don't take it. - - licenseUrl?: string; - - /** - * The list of keys that can add their signature to your metadata. - */ - permittedSigners?: PublicKey[]; - - /** - * Only works when part of a group. - * Attribute defintions exist on the group. - * List of pointers to the on chain attributes stored in the group. - */ - onChainAttributes?: number[]; - - royalties?: RoyaltyConfig; -}; - -export type MetadataData = { - name: string; - symbol: string; - assetUrl: MintAssetUrl; - - extension?: MetadataExtension; -}; - -export type MintSingleInput = { - provider: Provider; - - mintData: MetadataData; - - mintToCollection?: { - collection: PublicKey; - checkValidGroup: boolean; - - /** - * If you are not the update auth of the group. - * But have been given permission to add metadatas to it. - * Set this to true. - * - * Defaults to false. - */ - groupDelegate?: boolean; - }; - - mintKp?: Keypair; - - receiver?: PublicKey; - - transferHook?: TransferHookConfig; - - metadataProgram?: Program; - nftProgram?: Program; - - updateAuthority?: PublicKey; -}; - -export async function mintSingle(input: MintSingleInput) { - const { - provider, - metadataProgram = await loadMetadataProgram(provider), - nftProgram = await loadNftProgram(provider), - mintToCollection, - receiver = provider.publicKey, - mintKp = Keypair.generate(), - transferHook, - updateAuthority = provider.publicKey, - mintData, - } = input; - - const me = provider.publicKey; - - if (!me) { - throw new Error( - 'Provider does have a wallet loaded into it. Are you sure your wallet is connected' - ); - } - - if (mintToCollection) { - if (mintToCollection.checkValidGroup) { - const groupData = await metadataProgram.account.collection.fetchNullable( - mintToCollection.collection - ); - - if (!groupData) { - throw new Error('Group does not exist'); - } - - if (groupData.updateAuthority.toString() != me.toString()) { - const groupWideAddress = getCollectionWideUserPermissionsAddress( - mintToCollection.collection, - me - ); - - const permissionsData = - await metadataProgram.account.delegatePermissions.fetchNullable( - groupWideAddress - ); - - const hasDelegatedPermission = !!permissionsData?.permissions.find( - (perm) => !!perm.addToGroup - ); - - if (!permissionsData || !hasDelegatedPermission) { - throw new Error( - 'You do not have permission to add metadata to this group.' - ); - } - - mintToCollection.groupDelegate = true; - } - } - } - - const connection = provider.connection; - - const metadata = getMetadataAddress(mintKp.publicKey); - - const mintCtx = await setupLibreplexReadyMint( - connection, - me, - me, - receiver as PublicKey, - me, - 0, - mintKp, - metadata, - transferHook - ); - - let anchorAssetUrl: IdlTypes['Asset']; - - const { assetUrl, name, symbol } = mintData; - - switch (assetUrl.type) { - case 'jsonUrl': - anchorAssetUrl = { - json: { - url: assetUrl.value, - }, - }; - break; - case 'imageUrl': - anchorAssetUrl = { - image: { - url: assetUrl.value, - description: null, - }, - }; - break; - case 'renderedOnChain': - anchorAssetUrl = { - chainRenderer: { - programId: assetUrl.programId, - }, - }; - break; - - default: - throw new Error('Invalid asset type'); - } - - type ExtensionType = IdlTypes['MetadataExtension']; - let extensions: ExtensionType[] = []; - - if (mintData.extension) { - if (mintData.extension.onChainAttributes) { - extensions.push({ - attributes: { - attributes: Buffer.from(mintData.extension.onChainAttributes ?? []), - }, - }); - } - if (mintData.extension.royalties) { - extensions.push({ - royalties: { - royalties: mintData.extension.royalties || null, - }, - }); - } - - if (mintData.extension.licenseUrl) { - extensions.push({ - license: { - license: { - custom: { - licenseUrl: mintData.extension.licenseUrl, - }, - }, - }, - }); - } - - if (mintData.extension.permittedSigners) { - extensions.push({ - signers: { - signers: mintData.extension.permittedSigners || [], - }, - }); - } - } - console.log('Creating metadata instructions'); - const createMetaData = metadataProgram.methods - .createMetadata({ - asset: anchorAssetUrl, - extensions, - name, - symbol, - updateAuthority: updateAuthority as PublicKey, - }) - .accounts({ - authority: me, - metadata, - payer: me, - mint: mintKp.publicKey, - invokedMigratorProgram: null, - systemProgram: SystemProgram.programId, - }) - .preInstructions(mintCtx.transaction.instructions) - .signers([mintCtx.keypair]); - - console.log('Created metadata instruction'); - - const postIxs: TransactionInstruction[] = [ - await nftProgram.methods - .wrap() - .accounts({ - authority: me, - mint: mintKp.publicKey, - payer: me, - systemProgram: SystemProgram.programId, - tokenProgram: TOKEN_2022_PROGRAM_ID, - wrappedMint: getMintWrapperAddress(mintKp.publicKey), - }) - .instruction(), - ]; - - if (mintToCollection) { - const ix = await metadataProgram.methods - .addMetadataToCollection() - .accounts({ - delegatedCollectionWidePermissions: mintToCollection.groupDelegate - ? getCollectionWideUserPermissionsAddress( - mintToCollection.collection, - me - ) - : null, - systemProgram: SystemProgram.programId, - payer: me, - metadata, - metadataAuthority: me, - collectionAuthority: me, - delegatedMetadataSpecificPermissions: null, - collection: mintToCollection.collection, - }) - .instruction(); - - postIxs.push(ix); - } - - return { - method: createMetaData.postInstructions(postIxs), - mint: mintKp, - }; -} - -const MetadataPointerMintSize = 234; -const MintSizeForTranserHookAndPointer = 302; - -interface InitializeMetadataPointerIx { - instruction: 39; - metadataPointerInitIx: 0; - authority: PublicKey; - metadataAddress: PublicKey; -} - -const initializeMetadataPointerInstructionData = - struct([ - u8('instruction') as any, - u8('metadataPointerInitIx'), - publicKey('authority'), - publicKey('metadataAddress'), - ]); - -interface InitializeTransferHookInit { - instruction: 36; - transferHookInstruction: 0; - authority: PublicKey; - transferHookProgramId: PublicKey; -} - -const initializeTransferHookInitInstructionData = - struct([ - u8('instruction') as any, - u8('transferHookInstruction'), - publicKey('authority'), - publicKey('transferHookProgramId'), - ]); - -type TransferHookConfig = { - programId: PublicKey; - authority: PublicKey; -}; - -export async function setupLibreplexReadyMint( - connection: Connection, - payer: PublicKey, - receiver: PublicKey, - mintAuthority: PublicKey, - freezeAuthority: PublicKey | null, - decimals: number, - mintKeypair = Keypair.generate(), - metadata: PublicKey, - transferHook?: TransferHookConfig, - programId = TOKEN_2022_PROGRAM_ID -) { - const mintSize = transferHook - ? MintSizeForTranserHookAndPointer - : MetadataPointerMintSize; - const lamports = await connection.getMinimumBalanceForRentExemption(mintSize); - - const initMetadataPointerExtensionIx = (() => { - const initMetadataPointerIxSpan = Buffer.alloc( - initializeMetadataPointerInstructionData.span - ); - - initializeMetadataPointerInstructionData.encode( - { - instruction: 39, - authority: PublicKey.default, - metadataPointerInitIx: 0, - metadataAddress: metadata, - }, - initMetadataPointerIxSpan - ); - - return new TransactionInstruction({ - keys: [ - { - isSigner: false, - isWritable: true, - pubkey: mintKeypair.publicKey, - }, - ], - programId, - data: initMetadataPointerIxSpan, - }); - })(); - - const preInitMintIxs: TransactionInstruction[] = []; - - if (transferHook) { - const accounts = [ - { pubkey: mintKeypair.publicKey, isSigner: false, isWritable: true }, - ]; - const transferHookIxBuf = Buffer.alloc( - initializeTransferHookInitInstructionData.span - ); - initializeTransferHookInitInstructionData.encode( - { - authority: transferHook.authority, - transferHookProgramId: transferHook.programId, - instruction: 36, - transferHookInstruction: 0, - }, - transferHookIxBuf - ); - - preInitMintIxs.push( - new TransactionInstruction({ - keys: accounts, - programId: TOKEN_2022_PROGRAM_ID, - data: transferHookIxBuf, - }) - ); - } - - const assocTokenAccount = getAssociatedTokenAddressSync( - mintKeypair.publicKey, - receiver, - undefined, - TOKEN_2022_PROGRAM_ID - ); - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer, - newAccountPubkey: mintKeypair.publicKey, - space: mintSize, - lamports, - programId, - }), - initMetadataPointerExtensionIx, - ...preInitMintIxs, - createInitializeMint2Instruction( - mintKeypair.publicKey, - decimals, - mintAuthority, - freezeAuthority, - programId - ), - createAssociatedTokenAccountInstruction( - payer, - assocTokenAccount, - receiver, - mintKeypair.publicKey, - TOKEN_2022_PROGRAM_ID - ), - createMintToInstruction( - mintKeypair.publicKey, - assocTokenAccount, - mintAuthority, - 1, - undefined, - TOKEN_2022_PROGRAM_ID - ) - ); - - return { - transaction, - keypair: mintKeypair, - }; -} diff --git a/packages/libreplex-sdk/src/pda.ts b/packages/libreplex-sdk/src/pda.ts deleted file mode 100644 index 6a9ffa30..00000000 --- a/packages/libreplex-sdk/src/pda.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { PublicKey } from "@solana/web3.js"; -import { LIBREPLEX_METADATA_PROGRAM_ID, LIBREPLEX_CREATOR_PROGRAM_ID, LIBREPLEX_CREATOR_CONTROLS_PROGRAM_ID, LIBREPLEX_NFT_PROGRAM_ID } from "./constants"; - - - -export function getCollectionAddress(collectionSeed: PublicKey, program = LIBREPLEX_METADATA_PROGRAM_ID) { - return PublicKey.findProgramAddressSync([Buffer.from("collection"), collectionSeed.toBuffer()], LIBREPLEX_METADATA_PROGRAM_ID)[0] - - } - -export function getCreatorAddress(seed: PublicKey, program = LIBREPLEX_CREATOR_PROGRAM_ID) { - return PublicKey.findProgramAddressSync([Buffer.from("creator"), seed.toBuffer()], program)[0] - -} - -export function getCreatorControllerAddress(seed: PublicKey, programId: PublicKey = LIBREPLEX_CREATOR_CONTROLS_PROGRAM_ID) { - return PublicKey.findProgramAddressSync([seed.toBuffer()], programId)[0] -} - -export function getMetadataAddress(mint: PublicKey, program = LIBREPLEX_METADATA_PROGRAM_ID) { - return PublicKey.findProgramAddressSync([Buffer.from("metadata"), - mint.toBuffer()], program)[0] -} - -export function getMintWrapperAddress(mint: PublicKey, program = LIBREPLEX_NFT_PROGRAM_ID) { - return PublicKey.findProgramAddressSync([mint.toBuffer()], program)[0] -} \ No newline at end of file diff --git a/packages/libreplex-sdk/src/programs.ts b/packages/libreplex-sdk/src/programs.ts deleted file mode 100644 index 45d56967..00000000 --- a/packages/libreplex-sdk/src/programs.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {LibreplexCreator} from "@libreplex/idls/lib/types/libreplex_creator" -import {LibreplexMetadata} from "@libreplex/idls/lib/types/libreplex_metadata" -import {LibreplexNft} from "@libreplex/idls/lib/types/libreplex_nft" -import {LibreplexCreatorControls} from "@libreplex/idls/lib/types/libreplex_creator_controls" -import { Program, AccountClient , IdlAccounts, IdlTypes, Provider} from "@coral-xyz/anchor" -import { LIBREPLEX_METADATA_PROGRAM_ID, LIBREPLEX_NFT_PROGRAM_ID } from "./constants" - -export async function loadMetadataProgram(provider: Provider) { - return new Program((await import("@libreplex/idls/lib/cjs/libreplex_metadata")).IDL, - LIBREPLEX_METADATA_PROGRAM_ID, provider) -} - -export async function loadNftProgram(provider: Provider) { - return new Program((await import("@libreplex/idls/lib/cjs/libreplex_nft")).IDL, - LIBREPLEX_NFT_PROGRAM_ID, provider) -} diff --git a/packages/libreplex-sdk/src/setupCreator.ts b/packages/libreplex-sdk/src/setupCreator.ts deleted file mode 100644 index 7499e3a0..00000000 --- a/packages/libreplex-sdk/src/setupCreator.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { BN, Program } from "@coral-xyz/anchor" -import {LibreplexCreator} from "@libreplex/idls/lib/types/libreplex_creator" -import {LibreplexMetadata} from "@libreplex/idls/lib/types/libreplex_metadata" -import {LibreplexCreatorControls} from "@libreplex/idls/lib/types/libreplex_creator_controls" - -import { Keypair, PublicKey, SystemProgram, TransactionInstruction } from "@solana/web3.js"; -import {getCreatorAddress, getCreatorControllerAddress} from "./pda" -import {UserPermission, setUserPermissionsForGroup} from "./groupPermissions" -import { CreatorControl, controlToAnchor } from "./creatorControls"; -import { LIBREPLEX_CREATOR_PROGRAM_ID } from "./constants"; - -export type SetupCreatorData = { - description: string, - baseName: string, - ordered: boolean, - supply: number, - symbol: string, - baseUrl: { - type: "json-prefix", - url: string, - } | { - type: "chain-renderer", - programId: PublicKey, - description?: string, - }, -} - -export type SetupCreatorInput = { - mintAuthority: PublicKey, - program: Program, - metadataProgram: Program, - collection: PublicKey, - creatorData: SetupCreatorData, -} - - - -export type Phase = { - start: Date, - end: Date | null, - label: string, - control: CreatorControl[] -} - -export async function setupCreatorWithCustomSalePhases( - input: SetupCreatorInput, - creatorControllerProgram: Program, - salePhases: Phase[], - checkGroupIsValid = true) { - const me = creatorControllerProgram.provider.publicKey - const setupCreatorCtx = await setupCreator(input, checkGroupIsValid) - - const creatorControllerSeed = Keypair.generate() - - const anchorPhases = salePhases.map(p => { - const anchorControls = p.control.map(controlToAnchor) - - return { - start: new BN(Math.floor(p.start.getTime() / 1000)), - end: p.end != null ? new BN(Math.floor(p.end.getTime() / 1000)) : null, - label: p.label, - controls: anchorControls, - } - }); - - const creatorController = getCreatorControllerAddress(creatorControllerSeed.publicKey) - - const controllerCtx = await creatorControllerProgram.methods.initialize({ - seed: creatorControllerSeed.publicKey, - phases: anchorPhases - }).accounts({ - creator: setupCreatorCtx.creator, - creatorController, - libreplexCreatorProgram: LIBREPLEX_CREATOR_PROGRAM_ID, - systemProgram: SystemProgram.programId, - updateAuthority: me, - payer: me, - }).prepare() - - const method = setupCreatorCtx.method.postInstructions([controllerCtx.instruction]) - - return { - method, - creatorController, - creator: setupCreatorCtx.creator, - minterNumbers: setupCreatorCtx.minterNumbers, - } -} - - -export async function setupCreator(input: SetupCreatorInput, checkGroupIsValid = true) { - const {program, collection, creatorData, mintAuthority, metadataProgram} = input; - const {description, baseName, ordered, supply, symbol, baseUrl} = creatorData; - - const me = program.provider.publicKey - - if (checkGroupIsValid) { - const groupAccount = await metadataProgram.account.collection.fetchNullable(collection) - - if (!groupAccount) { - throw new Error("Provided group does not exist") - } - - - if (groupAccount.updateAuthority.toString() !== me?.toString()) { - throw new Error(`You do not have authority over the provided group. - ${groupAccount.updateAuthority.toString()} ${me?.toString()}`) - } - } - - const creatorSeed = Keypair.generate() - const creator = getCreatorAddress(creatorSeed.publicKey); - - const preIx: TransactionInstruction[] = [] - const signers: Keypair[] = [] - let minterNumbers: PublicKey | null = null - - if (!ordered) { - const minterNumbersKp = Keypair.generate() - const minterNumbersSize = 8 + 32 + 4 * supply - const rent = await program.provider.connection.getMinimumBalanceForRentExemption(minterNumbersSize, "confirmed") - - const creatorMinterNumbersIx = SystemProgram.createAccount({ - fromPubkey: program.provider.publicKey as PublicKey, - lamports: rent, - newAccountPubkey: minterNumbersKp.publicKey, - programId: program.programId, - space: minterNumbersSize, - }) - - minterNumbers = minterNumbersKp.publicKey - - preIx.push(creatorMinterNumbersIx) - signers.push(minterNumbersKp) - } - - - let createCreatorMethod = await program.methods.createCreator({ - attributeMappings: null, - collection, - description, - isOrdered: ordered, - maxMints: supply, - mintAuthority, - name: baseName, - seed: creatorSeed.publicKey, - symbol: symbol, - assetUrl: baseUrl.type === "json-prefix" ? { - jsonPrefix: { - url: baseUrl.url, - } - } : { - chainRenderer: { - programId: baseUrl.programId - } - } - }).accounts({ - creator, - minterNumbers, - signer: program.provider.publicKey, - systemProgram: SystemProgram.programId, - }).preInstructions(preIx).signers(signers) - - const delegateToGroupMethod = await (await setUserPermissionsForGroup({ - collection, - groupUpdateAuthority: me as PublicKey, - user: creator, - connector: { - type: "program", - metadataProgram, - }, - permissions: [UserPermission.AddToGroup] - })).prepare() - - createCreatorMethod = createCreatorMethod.postInstructions([delegateToGroupMethod.instruction]) - - - - return { - method: createCreatorMethod, - creator, - minterNumbers - } -} \ No newline at end of file diff --git a/packages/libreplex-sdk/src/updateCollection.ts b/packages/libreplex-sdk/src/updateCollection.ts deleted file mode 100644 index 9c5011bd..00000000 --- a/packages/libreplex-sdk/src/updateCollection.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { PublicKey } from "@solana/web3.js"; -import { Connector } from "./createCollection"; -import { loadMetadataProgram } from "./programs"; - - - -export async function updateCollectionAuthority( - { - connector, - collection, - new_authority - }: { - connector: Connector, - collection: PublicKey, - new_authority: PublicKey, - } - ) { - const metadataProgram = connector.type === "program" ? connector.metadataProgram : await loadMetadataProgram(connector.provider) - - const me = metadataProgram.provider.publicKey; - - if (!me) { - throw new Error("Provider not setup. Perhaps your wallet is not connected"); - } - - return metadataProgram.methods.updateCollectionAuthority(new_authority).accounts({ - collection, - updateAuthority: me, - }) - } diff --git a/packages/libreplex-sdk/src/updateCreator.ts b/packages/libreplex-sdk/src/updateCreator.ts deleted file mode 100644 index 0354cbd6..00000000 --- a/packages/libreplex-sdk/src/updateCreator.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Keypair, PublicKey, SystemProgram, TransactionInstruction } from "@solana/web3.js"; -import { Phase } from "./setupCreator"; -import {controlToAnchor} from "./creatorControls" -import {LibreplexCreatorControls} from "@libreplex/idls/lib/types/libreplex_creator_controls" -import { Program, AccountClient , IdlAccounts, IdlTypes} from "@coral-xyz/anchor" - -export type UpdateCreatorInput = { - program: Program, - creatorController: PublicKey, - phases: Phase[], -} - - -export async function updateCreator(input: UpdateCreatorInput) { - const {program, phases, creatorController} = input; - - const me = program.provider.publicKey - - if (!me) { - throw new Error("Missing provider. Are you sure your wallet is connected?") - } - - - - const anchorPhases: IdlTypes["Phase"][] = []; - - for (const ph of phases) { - const controls: IdlTypes["ControlType"][] = [] - - for (const control of ph.control) { - controls.push(controlToAnchor(control)) - } - - anchorPhases.push({ - controls, - end: ph.end ? ph.end.getTime()/1000 : null, - label: ph.label, - start: ph.start.getTime()/1000, - }) - } - - return program.methods.update({ - phases: anchorPhases, - }).accounts({ - systemProgram: SystemProgram.programId, - payer: me, - updateAuthority: me, - creatorController, - }); -} \ No newline at end of file diff --git a/packages/libreplex-sdk/tsconfig.cjs.json b/packages/libreplex-sdk/tsconfig.cjs.json deleted file mode 100644 index d1d9da77..00000000 --- a/packages/libreplex-sdk/tsconfig.cjs.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "include": ["src"], - "extends": "./tsconfig.json", - "compilerOptions": { - "target": "ES2016", - "module": "CommonJS", - "outDir": "lib/cjs" - } - } \ No newline at end of file diff --git a/packages/libreplex-sdk/tsconfig.esm.json b/packages/libreplex-sdk/tsconfig.esm.json deleted file mode 100644 index 0e27a423..00000000 --- a/packages/libreplex-sdk/tsconfig.esm.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "./tsconfig.json", - "include": ["src"], - "compilerOptions": { - "outDir": "lib/esm", - "declarationDir": "lib/types", - "target": "ESNext", - "module": "ESNext", - "sourceMap": true, - "declaration": true, - "declarationMap": true, - } -} \ No newline at end of file diff --git a/packages/libreplex-sdk/tsconfig.json b/packages/libreplex-sdk/tsconfig.json deleted file mode 100644 index 169a901e..00000000 --- a/packages/libreplex-sdk/tsconfig.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "include": [], - "compilerOptions": { - "lib": ["ES2021.String"], - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Node", - "esModuleInterop": true, - "isolatedModules": true, - "noImplicitAny": false, - "noEmitOnError": true, - "resolveJsonModule": true, - "strict": true, - "stripInternal": true, - "skipLibCheck": true, - "noEmit": false, - "allowSyntheticDefaultImports": true, - "composite": true - }, - "references": [ - { - "path": "./tsconfig.cjs.json" - }, - { - "path": "./tsconfig.esm.json" - }, - ] -} diff --git a/programs/libreplex_editions/Cargo.toml b/programs/libreplex_editions/Cargo.toml index 53ddcbe1..1f9fe037 100644 --- a/programs/libreplex_editions/Cargo.toml +++ b/programs/libreplex_editions/Cargo.toml @@ -19,7 +19,6 @@ default = [] idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] - [dependencies] anchor-lang = {version = "~0.30", features = ["init-if-needed"]} anchor-spl = {version = "~0.30"} diff --git a/programs/libreplex_editions/src/errors.rs b/programs/libreplex_editions/src/errors.rs index 20bafb21..eb204d10 100644 --- a/programs/libreplex_editions/src/errors.rs +++ b/programs/libreplex_editions/src/errors.rs @@ -8,7 +8,6 @@ pub enum EditionsError { #[msg("Mint template too long")] MintTemplateTooLong, - #[msg("Deployment template too long")] DeploymentTemplateTooLong, diff --git a/programs/libreplex_editions/src/instructions/add_to_hashlist.rs b/programs/libreplex_editions/src/instructions/add_to_hashlist.rs index c3e2dd3d..3ca58b9e 100644 --- a/programs/libreplex_editions/src/instructions/add_to_hashlist.rs +++ b/programs/libreplex_editions/src/instructions/add_to_hashlist.rs @@ -4,8 +4,6 @@ use solana_program::program::invoke; use solana_program::pubkey::Pubkey; use solana_program::system_instruction; - - pub fn add_to_hashlist<'a>( new_number_of_mints: u32, hashlist: &mut UncheckedAccount<'a>, @@ -13,9 +11,8 @@ pub fn add_to_hashlist<'a>( system_program: &Program<'a, System>, mint: &Pubkey, deployment: &Pubkey, - order_number: u64) -> Result<()> { - - msg!("add_to_hashlist called"); + order_number: u64 +) -> Result<()> { let new_size = 8 + 32 + 4 + (new_number_of_mints) * (32 + 8); let rent = Rent::get()?; let new_minimum_balance = rent.minimum_balance(new_size as usize); @@ -32,9 +29,9 @@ pub fn add_to_hashlist<'a>( } hashlist.realloc(new_size as usize, false)?; let hashlist_account_info = hashlist.to_account_info(); - + let mut hashlist_data = hashlist_account_info.data.borrow_mut(); - + hashlist_data[40..44].copy_from_slice(&new_number_of_mints.to_le_bytes()); let mint_start_pos:usize = (44+(new_number_of_mints-1)*40) as usize; hashlist_data[ @@ -43,13 +40,11 @@ pub fn add_to_hashlist<'a>( hashlist_data[ mint_start_pos + 32..mint_start_pos + 40 ].copy_from_slice(&order_number.to_le_bytes()); - + emit!(HashlistEvent { mint: mint.key(), deployment: deployment.key() }); - Ok(()) - } diff --git a/programs/libreplex_editions/src/instructions/initialise.rs b/programs/libreplex_editions/src/instructions/initialise.rs index 1c026d8d..38327675 100644 --- a/programs/libreplex_editions/src/instructions/initialise.rs +++ b/programs/libreplex_editions/src/instructions/initialise.rs @@ -1,4 +1,4 @@ -use crate::{group_extension_program, utils::update_account_lamports_to_minimum_balance, EditionsDeployment, Hashlist, NAME_LIMIT, OFFCHAIN_URL_LIMIT, SYMBOL_LIMIT}; +use crate::{group_extension_program, utils::update_account_lamports_to_minimum_balance, EditionsDeployment, Hashlist, NAME_LIMIT, URI_LIMIT, SYMBOL_LIMIT}; use anchor_lang::prelude::*; use libreplex_shared::{create_token_2022_and_metadata, MintAccounts2022, TokenGroupInput}; use solana_program::system_program; @@ -7,22 +7,21 @@ use spl_token_metadata_interface::state::TokenMetadata; #[derive(AnchorDeserialize, AnchorSerialize, Clone)] pub struct InitialiseInput { - pub max_number_of_tokens: u64, // this is the max *number* of tokens pub symbol: String, + pub collection_name: String, + pub collection_uri: String, + pub max_number_of_tokens: u64, // this is the max *number* of tokens + pub creator_cosign_program_id: Option, // add curlies if you want this to be created dynamically. For example - // hippo #{} -> turns into hippo #0, hippo #1, etc + // ipfs://pippo/{} -> turns into ipfs://pippo/1, ipfs://pippo/2, etc // without curlies the url is the same for all mints - pub name: String, + pub item_base_uri: String, // add curlies if you want this to be created dynamically. For example - // ipfs://pippo/{} -> turns into ipfs://pippo/1, ipfs://pippo/2, etc + // hippo #{} -> turns into hippo #0, hippo #1, etc // without curlies the url is the same for all mints - pub offchain_url: String, - pub creator_cosign_program_id: Option, - pub item_base_uri: String, - pub item_name: String, + pub item_base_name: String, } - #[derive(Accounts)] #[instruction(input: InitialiseInput)] pub struct InitialiseCtx<'info> { @@ -61,24 +60,22 @@ pub struct InitialiseCtx<'info> { pub group_extension_program: AccountInfo<'info>, } - pub fn initialise(ctx: Context, input: InitialiseInput) -> Result<()> { - if input.offchain_url.len() > OFFCHAIN_URL_LIMIT { - panic!("Offchain url too long"); - } if input.symbol.len() > SYMBOL_LIMIT { panic!("Symbol too long"); } - if input.name.len() > NAME_LIMIT { + if input.collection_name.len() > NAME_LIMIT { panic!("Name too long"); } + if input.collection_uri.len() > URI_LIMIT { + panic!("Offchain url too long"); + } let group_mint = &ctx.accounts.group_mint; let group = &ctx.accounts.group; - - let url_is_template = match input.item_base_uri.matches("{}").count() { + let item_uri_is_template = match input.item_base_uri.matches("{}").count() { 0 => false, 1 => true, _ => { @@ -86,8 +83,7 @@ pub fn initialise(ctx: Context, input: InitialiseInput) -> Result } }; - - let name_is_template = match input.item_name.matches("{}").count() { + let item_name_is_template = match input.item_base_name.matches("{}").count() { 0 => false, 1 => true, _ => { @@ -95,7 +91,6 @@ pub fn initialise(ctx: Context, input: InitialiseInput) -> Result } }; - ctx.accounts.editions_deployment.set_inner(EditionsDeployment { creator: ctx.accounts.creator.key(), max_number_of_tokens: input.max_number_of_tokens, @@ -107,14 +102,13 @@ pub fn initialise(ctx: Context, input: InitialiseInput) -> Result _ => system_program::ID }, symbol: input.symbol, - name: input.item_name, - url_is_template, - name_is_template, - offchain_url: input.item_base_uri, + item_base_name: input.item_base_name, + item_base_uri: input.item_base_uri, + item_name_is_template, + item_uri_is_template, padding: [0; 98], }); - let editions_deployment = &ctx.accounts.editions_deployment; let payer = &ctx.accounts.payer; let group_mint = &ctx.accounts.group_mint; @@ -130,7 +124,6 @@ pub fn initialise(ctx: Context, input: InitialiseInput) -> Result &[ctx.bumps.editions_deployment], ]; - // msg!("Create token 2022 w/ metadata"); create_token_2022_and_metadata( MintAccounts2022 { authority: editions_deployment.to_account_info(), @@ -141,9 +134,9 @@ pub fn initialise(ctx: Context, input: InitialiseInput) -> Result }, 0, Some(TokenMetadata { - name: input.name.clone(), + name: input.collection_name.clone(), symbol: editions_deployment.symbol.clone(), - uri: input.offchain_url.clone(), + uri: input.collection_uri.clone(), update_authority, mint: group_mint.key(), additional_metadata: vec![], // Leave this empty for now, diff --git a/programs/libreplex_editions/src/instructions/mint.rs b/programs/libreplex_editions/src/instructions/mint.rs index 2fba7e67..04246bbc 100644 --- a/programs/libreplex_editions/src/instructions/mint.rs +++ b/programs/libreplex_editions/src/instructions/mint.rs @@ -85,7 +85,6 @@ pub struct MintCtx<'info> { #[account()] pub system_program: Program<'info, System>, - } pub fn mint<'info>(ctx: Context<'_, '_, '_, 'info, MintCtx<'info>>) -> Result<()> { @@ -120,7 +119,6 @@ pub fn mint<'info>(ctx: Context<'_, '_, '_, 'info, MintCtx<'info>>) -> Result<() return Err(EditionsError::MintedOut.into()); } - let update_authority = OptionalNonZeroPubkey::try_from(Some(editions_deployment.key())).expect("Bad update auth"); @@ -130,16 +128,16 @@ pub fn mint<'info>(ctx: Context<'_, '_, '_, 'info, MintCtx<'info>>) -> Result<() &[ctx.bumps.editions_deployment], ]; - let name = match editions_deployment.name_is_template { - true => editions_deployment.name.format(&[editions_deployment.number_of_tokens_issued + 1]), - false => editions_deployment.name.clone() + let item_name = match editions_deployment.item_name_is_template { + true => editions_deployment.item_base_name.format(&[editions_deployment.number_of_tokens_issued + 1]), + false => editions_deployment.item_base_name.clone() }; - let url = match editions_deployment.url_is_template { - true => editions_deployment.offchain_url.format(&[editions_deployment.number_of_tokens_issued + 1]), - false => editions_deployment.offchain_url.clone() + let item_url = match editions_deployment.item_uri_is_template { + true => editions_deployment.item_base_uri.format(&[editions_deployment.number_of_tokens_issued + 1]), + false => editions_deployment.item_base_uri.clone() }; - // msg!("Create token 2022 w/ metadata"); + create_token_2022_and_metadata( MintAccounts2022 { authority: editions_deployment.to_account_info(), @@ -150,9 +148,9 @@ pub fn mint<'info>(ctx: Context<'_, '_, '_, 'info, MintCtx<'info>>) -> Result<() }, 0, Some(TokenMetadata { - name, + name: item_name, symbol: editions_deployment.symbol.clone(), - uri: url, + uri: item_url, update_authority, mint: mint.key(), additional_metadata: vec![], @@ -167,7 +165,6 @@ pub fn mint<'info>(ctx: Context<'_, '_, '_, 'info, MintCtx<'info>>) -> Result<() Some(group_extension_program.key()), )?; - // msg!("Minting 2022"); mint_non_fungible_2022_logic( &mint.to_account_info(), minter_token_account, @@ -191,10 +188,8 @@ pub fn mint<'info>(ctx: Context<'_, '_, '_, 'info, MintCtx<'info>>) -> Result<() )?; // Retrieve metadata from the group mint - msg!("Mint: get meta start"); let meta = get_mint_metadata(&mut group_mint.to_account_info())?; let additional_meta = meta.additional_metadata; - msg!("Mint: get meta end: meta len {}", additional_meta.len()); // Process each additional metadata key-value pair, excluding platform fee metadata for additional_metadatum in additional_meta { @@ -222,12 +217,11 @@ pub fn mint<'info>(ctx: Context<'_, '_, '_, 'info, MintCtx<'info>>) -> Result<() } // Transfer minimum rent to the mint account - msg!("Mint: transfer minimum rent to mint account"); update_account_lamports_to_minimum_balance( ctx.accounts.mint.to_account_info(), ctx.accounts.payer.to_account_info(), ctx.accounts.system_program.to_account_info(), )?; - msg!("Mint: done"); + Ok(()) } diff --git a/programs/libreplex_editions/src/lib.rs b/programs/libreplex_editions/src/lib.rs index aff92787..6864fcdc 100644 --- a/programs/libreplex_editions/src/lib.rs +++ b/programs/libreplex_editions/src/lib.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; pub mod instructions; pub use instructions::*; -declare_id!("587DoLBH2H39i5bToWBc6zRgbD2iJZtc4Kb8nYsskYTq"); +declare_id!("GHVmd43GebD2SSedqG1AAh76izf5CPAhz6KotkZyRzUD"); pub mod errors; pub mod state; diff --git a/programs/libreplex_editions/src/logic/add_to_hashlist.rs b/programs/libreplex_editions/src/logic/add_to_hashlist.rs index 0b718f19..af337c87 100644 --- a/programs/libreplex_editions/src/logic/add_to_hashlist.rs +++ b/programs/libreplex_editions/src/logic/add_to_hashlist.rs @@ -4,17 +4,14 @@ use solana_program::program::invoke; use solana_program::pubkey::Pubkey; use solana_program::system_instruction; - - pub fn add_to_hashlist<'a>( new_number_of_mints: u32, hashlist: &mut UncheckedAccount<'a>, payer: &Signer<'a>, system_program: &Program<'a, System>, mint: &Pubkey, - order_number: u64) -> Result<()> { - - msg!("add_to_hashlist called"); + order_number: u64 +) -> Result<()> { let new_size = 8 + 32 + 4 + (new_number_of_mints) * (32 + 8); let rent = Rent::get()?; let new_minimum_balance = rent.minimum_balance(new_size as usize); @@ -31,9 +28,9 @@ pub fn add_to_hashlist<'a>( } hashlist.realloc(new_size as usize, false)?; let hashlist_account_info = hashlist.to_account_info(); - + let mut hashlist_data = hashlist_account_info.data.borrow_mut(); - + hashlist_data[40..44].copy_from_slice(&new_number_of_mints.to_le_bytes()); let mint_start_pos:usize = (44+(new_number_of_mints-1)*40) as usize; hashlist_data[ @@ -42,7 +39,6 @@ pub fn add_to_hashlist<'a>( hashlist_data[ mint_start_pos + 32..mint_start_pos + 40 ].copy_from_slice(&order_number.to_le_bytes()); - - Ok(()) + Ok(()) } diff --git a/programs/libreplex_editions/src/state.rs b/programs/libreplex_editions/src/state.rs index 49e07fbb..ed537ffd 100644 --- a/programs/libreplex_editions/src/state.rs +++ b/programs/libreplex_editions/src/state.rs @@ -1,9 +1,9 @@ use anchor_lang::prelude::*; use solana_program::pubkey::Pubkey; -pub const NAME_LIMIT: usize = 400; pub const SYMBOL_LIMIT: usize = 100; -pub const OFFCHAIN_URL_LIMIT: usize = 1200; +pub const NAME_LIMIT: usize = 400; +pub const URI_LIMIT: usize = 1200; pub const META_LIST_ACCOUNT_SEED: &[u8] = b"extra-account-metas"; pub const APPROVE_ACCOUNT_SEED: &[u8] = b"approve-account"; pub const ROYALTY_BASIS_POINTS_FIELD: &str = "royalty_basis_points"; @@ -22,6 +22,7 @@ pub struct EditionsDeployment { pub creator: Pubkey, // set to 0 for unlimited pub max_number_of_tokens: u64, + pub number_of_tokens_issued: u64, // set to system account for no cosign @@ -35,20 +36,18 @@ pub struct EditionsDeployment { pub symbol: String, #[max_len(NAME_LIMIT)] - pub name: String, + pub item_base_name: String, - #[max_len(OFFCHAIN_URL_LIMIT)] - pub offchain_url: String, // pub padding: Vec - - pub name_is_template: bool, + #[max_len(URI_LIMIT)] + pub item_base_uri: String, - pub url_is_template: bool, + pub item_name_is_template: bool, + + pub item_uri_is_template: bool, pub padding: [u8; 98] - } - // slightly more extended #[account] pub struct HashlistMarker { diff --git a/programs/libreplex_editions_controls/Cargo.toml b/programs/libreplex_editions_controls/Cargo.toml index d24fa948..eaf2c5fa 100644 --- a/programs/libreplex_editions_controls/Cargo.toml +++ b/programs/libreplex_editions_controls/Cargo.toml @@ -20,13 +20,10 @@ idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] anchor-lang = {version = "~0.30", features = ["init-if-needed"]} - anchor-spl = {version = "~0.30"} - - libreplex_editions = {version="*", path = "../libreplex_editions", features =["cpi", "no-entrypoint"]} - +rarible-merkle-verify = { version="*", path = "../../libraries/rarible-merkle-verify" } solana-program = {version = "1.17.13"} arrayref = "0.3.7" diff --git a/programs/libreplex_editions_controls/src/errors.rs b/programs/libreplex_editions_controls/src/errors.rs index f616ae1e..6536f7a0 100644 --- a/programs/libreplex_editions_controls/src/errors.rs +++ b/programs/libreplex_editions_controls/src/errors.rs @@ -1,14 +1,13 @@ use anchor_lang::prelude::*; #[error_code] -pub enum EditionsError { +pub enum EditionsControlsError { #[msg("Ticker too long")] TickerTooLong, #[msg("Mint template too long")] MintTemplateTooLong, - #[msg("Deployment template too long")] DeploymentTemplateTooLong, @@ -50,7 +49,44 @@ pub enum EditionsError { #[msg("No phases have been added. Cannot mint.")] NoPhasesAdded, + #[msg("Invalid phase index.")] InvalidPhaseIndex, + #[msg("Private phase but no merkle proof provided")] + PrivatePhaseNoProof, + + #[msg("Merkle root not set for allow list mint")] + MerkleRootNotSet, + + #[msg("Merkle proof required for allow list mint")] + MerkleProofRequired, + + #[msg("Allow list price and max claims are required for allow list mint")] + AllowListPriceAndMaxClaimsRequired, + + #[msg("Invalid merkle proof")] + InvalidMerkleProof, + + #[msg("This wallet has exceeded allow list max_claims in the current phase")] + ExceededAllowListMaxClaims, + + #[msg("Phase not active")] + PhaseNotActive, + + #[msg("Phase not yet started")] + PhaseNotStarted, + + #[msg("Phase already finished")] + PhaseAlreadyFinished, + + #[msg("Exceeded max mints for this phase")] + ExceededMaxMintsForPhase, + + #[msg("Exceeded wallet max mints for this phase")] + ExceededWalletMaxMintsForPhase, + + #[msg("Exceeded wallet max mints for the collection")] + ExceededWalletMaxMintsForCollection, + } diff --git a/programs/libreplex_editions_controls/src/instructions/add_phase.rs b/programs/libreplex_editions_controls/src/instructions/add_phase.rs index f9e355cd..54569e81 100644 --- a/programs/libreplex_editions_controls/src/instructions/add_phase.rs +++ b/programs/libreplex_editions_controls/src/instructions/add_phase.rs @@ -2,10 +2,8 @@ use anchor_lang::prelude::*; use libreplex_editions::program::LibreplexEditions; use libreplex_shared::wrapped_sol; - use crate::{EditionsControls, Phase}; - #[derive(AnchorDeserialize, AnchorSerialize, Clone)] pub struct InitialisePhaseInput { pub price_amount: u64, @@ -13,15 +11,14 @@ pub struct InitialisePhaseInput { pub start_time: i64, pub max_mints_per_wallet: u64, pub max_mints_total: u64, - pub end_time: i64 // set to i64::MAX if not supplied + pub end_time: i64, + pub is_private: bool, + pub merkle_root: Option<[u8; 32]>, } - #[derive(Accounts)] #[instruction(input: InitialisePhaseInput)] pub struct AddPhaseCtx<'info> { - - #[account(mut, realloc = EditionsControls::get_size(editions_controls.phases.len()+1), realloc::zero = false, @@ -48,7 +45,6 @@ pub struct AddPhaseCtx<'info> { } pub fn add_phase(ctx: Context, input: InitialisePhaseInput) -> Result<()> { - if !input.price_token.eq(&wrapped_sol::ID) { panic!("Only native price currently supported") } @@ -61,12 +57,12 @@ pub fn add_phase(ctx: Context, input: InitialisePhaseInput) -> Resu max_mints_per_wallet: input.max_mints_per_wallet, active: true, // everything starts out as active - end_time: input.end_time, - padding: [0; 200], max_mints_total: input.max_mints_total, - current_mints: 0 + current_mints: 0, + is_private: input.is_private, + merkle_root: input.merkle_root, + padding: [0; 200], }); - - Ok(()) } diff --git a/programs/libreplex_editions_controls/src/instructions/initialise.rs b/programs/libreplex_editions_controls/src/instructions/initialise.rs index d1af9c18..949ea466 100644 --- a/programs/libreplex_editions_controls/src/instructions/initialise.rs +++ b/programs/libreplex_editions_controls/src/instructions/initialise.rs @@ -3,7 +3,7 @@ use libreplex_editions::{cpi::accounts::InitialiseCtx, group_extension_program, use libreplex_editions::cpi::accounts::AddMetadata; use libreplex_editions::cpi::accounts::AddRoyalties; use crate::{EditionsControls, PlatformFeeRecipient, UpdatePlatformFeeArgs, DEFAULT_PLATFORM_FEE_PRIMARY_ADMIN, DEFAULT_PLATFORM_FEE_SECONDARY_ADMIN}; -use crate::errors::EditionsError; +use crate::errors::EditionsControlsError; #[derive(AnchorDeserialize, AnchorSerialize, Clone)] pub struct InitialiseControlInput { @@ -11,13 +11,13 @@ pub struct InitialiseControlInput { pub treasury: Pubkey, pub max_number_of_tokens: u64, pub symbol: String, - pub name: String, - pub offchain_url: String, + pub collection_name: String, + pub collection_uri: String, pub cosigner_program_id: Option, pub royalties: UpdateRoyaltiesArgs, pub extra_meta: Vec, pub item_base_uri: String, - pub item_name: String, + pub item_base_name: String, pub platform_fee: UpdatePlatformFeeArgs } @@ -75,7 +75,6 @@ pub fn initialise_editions_controls( ) -> Result<()> { let libreplex_editions_program = &ctx.accounts.libreplex_editions_program; let editions_controls = &mut ctx.accounts.editions_controls; - let editions_deployment = &ctx.accounts.editions_deployment; let hashlist = &ctx.accounts.hashlist; let payer = &ctx.accounts.payer; @@ -89,10 +88,10 @@ pub fn initialise_editions_controls( let core_input = InitialiseInput { max_number_of_tokens: input.max_number_of_tokens, symbol: input.symbol, - name: input.name, - offchain_url: input.offchain_url, + collection_name: input.collection_name, + collection_uri: input.collection_uri, creator_cosign_program_id: Some(crate::ID), - item_name: input.item_name, + item_base_name: input.item_base_name, item_base_uri: input.item_base_uri }; @@ -118,13 +117,13 @@ pub fn initialise_editions_controls( // Validate that platform_fee has up to 5 recipients let provided_recipients = input.platform_fee.recipients.len(); if provided_recipients > 5 { - return Err(EditionsError::TooManyRecipients.into()); + return Err(EditionsControlsError::TooManyRecipients.into()); } // Ensure that the sum of shares equals 100 let total_shares: u8 = input.platform_fee.recipients.iter().map(|r| r.share).sum(); if total_shares != 100 { - return Err(EditionsError::InvalidFeeShares.into()); + return Err(EditionsControlsError::InvalidFeeShares.into()); } // Initialize an array of 5 PlatformFeeRecipient with default values @@ -161,15 +160,15 @@ pub fn initialise_editions_controls( editions_deployment: editions_deployment.key(), creator: creator.key(), max_mints_per_wallet: input.max_mints_per_wallet, - padding: [0; 200], cosigner_program_id: input.cosigner_program_id.unwrap_or(system_program::ID), - phases: vec![], treasury: input.treasury, platform_fee_value: input.platform_fee.platform_fee_value, is_fee_flat: input.platform_fee.is_fee_flat, platform_fee_recipients: recipients_array.clone(), platform_fee_primary_admin: DEFAULT_PLATFORM_FEE_PRIMARY_ADMIN.parse().unwrap(), platform_fee_secondary_admin: DEFAULT_PLATFORM_FEE_SECONDARY_ADMIN.parse().unwrap(), + phases: vec![], + padding: [0; 200], }); let editions_deployment_key = editions_deployment.key(); diff --git a/programs/libreplex_editions_controls/src/instructions/mint_with_controls.rs b/programs/libreplex_editions_controls/src/instructions/mint_with_controls.rs index 6562319d..bbebf44e 100644 --- a/programs/libreplex_editions_controls/src/instructions/mint_with_controls.rs +++ b/programs/libreplex_editions_controls/src/instructions/mint_with_controls.rs @@ -1,16 +1,28 @@ -use anchor_lang::prelude::*; -use anchor_lang::system_program; +use anchor_lang::{prelude::*, system_program}; use anchor_spl::{ associated_token::AssociatedToken, token_2022::{self, ID as TOKEN_2022_ID}, }; -use libreplex_editions::{cpi::accounts::MintCtx, program::LibreplexEditions, EditionsDeployment}; -use crate::{check_phase_constraints, errors::EditionsError, EditionsControls, MinterStats}; -use libreplex_editions::group_extension_program; +use libreplex_editions::{ + group_extension_program, + program::LibreplexEditions, + EditionsDeployment, + cpi::accounts::MintCtx +}; +use crate::{ + EditionsControls, + MinterStats, + errors::EditionsControlsError, + check_phase_constraints, + check_allow_list_constraints +}; #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct MintInput { pub phase_index: u32, + pub merkle_proof: Option>, + pub allow_list_price: Option, + pub allow_list_max_claims: Option, } #[derive(Accounts)] @@ -26,11 +38,11 @@ pub struct MintWithControlsCtx<'info> { )] pub editions_controls: Box>, - /// CHECK: Checked via CPI + // CHECK: Checked via CPI #[account(mut)] pub hashlist: UncheckedAccount<'info>, - /// CHECK: Checked via CPI + // CHECK: Checked via CPI #[account(mut)] pub hashlist_marker: UncheckedAccount<'info>, @@ -45,7 +57,7 @@ pub struct MintWithControlsCtx<'info> { )] pub signer: Signer<'info>, - /// CHECK: Anybody can sign, anybody can receive the inscription + // CHECK: Anybody can sign, anybody can receive the inscription #[account(mut)] pub minter: UncheckedAccount<'info>, @@ -137,8 +149,8 @@ pub fn mint_with_controls( let minter_stats_phase = &mut ctx.accounts.minter_stats_phase; let minter = &ctx.accounts.minter; - // Phase validation and price retrieval - let price_amount = validate_phase(editions_controls, mint_input.phase_index)?; + // Phase validation + validate_phase(editions_controls, mint_input.phase_index)?; // Check phase constraints check_phase_constraints( @@ -146,10 +158,32 @@ pub fn mint_with_controls( minter_stats, minter_stats_phase, editions_controls, - ); + )?; - // Update minter statistics - update_minter_stats( + // Get the default/standard price amount for the phase + let mut price_amount = editions_controls.phases[mint_input.phase_index as usize].price_amount; + + // Check if it's a normal mint or an allow list mint based on the presence of a merkle proof + if mint_input.merkle_proof.is_some() { + check_allow_list_constraints( + &editions_controls.phases[mint_input.phase_index as usize], + &minter.key(), + minter_stats_phase, + mint_input.merkle_proof, + mint_input.allow_list_price, + mint_input.allow_list_max_claims, + )?; + // Override the price amount with the allow list price + price_amount = mint_input.allow_list_price.unwrap_or(0); + } else { + // if the phase is private, and the merkle proof was not provided, throw error + if editions_controls.phases[mint_input.phase_index as usize].is_private { + return Err(EditionsControlsError::PrivatePhaseNoProof.into()); + } + } + + // Update minter and phase states + update_minter_and_phase_stats( minter_stats, minter_stats_phase, &minter.key(), @@ -177,20 +211,19 @@ pub fn mint_with_controls( fn validate_phase( editions_controls: &EditionsControls, phase_index: u32, -) -> Result { +) -> Result<()> { if phase_index >= editions_controls.phases.len() as u32 { if editions_controls.phases.is_empty() { - return Err(EditionsError::NoPhasesAdded.into()); + return Err(EditionsControlsError::NoPhasesAdded.into()); } else { - return Err(EditionsError::InvalidPhaseIndex.into()); + return Err(EditionsControlsError::InvalidPhaseIndex.into()); } } - let price_amount = editions_controls.phases[phase_index as usize].price_amount; - Ok(price_amount) + Ok(()) } -fn update_minter_stats( +fn update_minter_and_phase_stats( minter_stats: &mut MinterStats, minter_stats_phase: &mut MinterStats, minter_key: &Pubkey, @@ -220,7 +253,7 @@ fn process_platform_fees( // Ensure that the sum of shares equals 100 let total_shares: u8 = recipients.iter().map(|r| r.share).sum(); if total_shares != 100 { - return Err(EditionsError::InvalidFeeShares.into()); + return Err(EditionsControlsError::InvalidFeeShares.into()); } let total_fee: u64; @@ -233,12 +266,12 @@ fn process_platform_fees( // Calculate fee as (price_amount * platform_fee_value) / 10,000 (assuming basis points) total_fee = price_amount .checked_mul(editions_controls.platform_fee_value as u64) - .ok_or(EditionsError::FeeCalculationError)? + .ok_or(EditionsControlsError::FeeCalculationError)? .checked_div(10_000) - .ok_or(EditionsError::FeeCalculationError)?; + .ok_or(EditionsControlsError::FeeCalculationError)?; remaining_amount = price_amount.checked_sub(total_fee) - .ok_or(EditionsError::FeeCalculationError)?; + .ok_or(EditionsControlsError::FeeCalculationError)?; } // Distribute fees to recipients @@ -249,17 +282,16 @@ fn process_platform_fees( let recipient_account = &ctx.accounts.platform_fee_recipient_1; - msg!("check recipients {}: {} vs {}", i, recipient_account.key(), recipient_struct.address.key()); // Ensure that the account matches the expected recipient if recipient_account.key() != recipient_struct.address.key() { - return Err(EditionsError::RecipientMismatch.into()); + return Err(EditionsControlsError::RecipientMismatch.into()); } let recipient_fee = total_fee .checked_mul(recipient_struct.share as u64) - .ok_or(EditionsError::FeeCalculationError)? + .ok_or(EditionsControlsError::FeeCalculationError)? .checked_div(100) - .ok_or(EditionsError::FeeCalculationError)?; + .ok_or(EditionsControlsError::FeeCalculationError)?; // Transfer platform fee to recipient system_program::transfer( diff --git a/programs/libreplex_editions_controls/src/instructions/mod.rs b/programs/libreplex_editions_controls/src/instructions/mod.rs index 818ff67b..cd4b141f 100644 --- a/programs/libreplex_editions_controls/src/instructions/mod.rs +++ b/programs/libreplex_editions_controls/src/instructions/mod.rs @@ -1,8 +1,3 @@ - -/* - initialises a new launch. does not create any - on-chain accounts, mints, token accounts etc -*/ pub mod initialise; pub use initialise::*; diff --git a/programs/libreplex_editions_controls/src/instructions/update_platform_fee.rs b/programs/libreplex_editions_controls/src/instructions/update_platform_fee.rs index b7f0dc5d..8ea925f3 100644 --- a/programs/libreplex_editions_controls/src/instructions/update_platform_fee.rs +++ b/programs/libreplex_editions_controls/src/instructions/update_platform_fee.rs @@ -32,7 +32,6 @@ pub fn update_platform_fee(ctx: Context, platform_fee_inpu let platform_fee_value = platform_fee_input.platform_fee_value; let is_fee_flat = platform_fee_input.is_fee_flat; - msg!("libreplex_editions::cpi::modify_platform_ffe start"); let editions_controls = &mut ctx.accounts.editions_controls; // Initialize an array of 5 PlatformFeeRecipient with default values @@ -59,7 +58,6 @@ pub fn update_platform_fee(ctx: Context, platform_fee_inpu }, ]; - msg!("libreplex_editions_controls:: update editions_controls"); // Populate the array with provided recipients for (i, recipient) in platform_fee_input.recipients.iter().enumerate() { recipients_array[i] = recipient.clone(); @@ -68,7 +66,5 @@ pub fn update_platform_fee(ctx: Context, platform_fee_inpu editions_controls.is_fee_flat = is_fee_flat; editions_controls.platform_fee_recipients = recipients_array; - msg!("libreplex_editions::cpi::modify_platform_fee done"); - Ok(()) } diff --git a/programs/libreplex_editions_controls/src/instructions/update_platform_fee_secondary_admin.rs b/programs/libreplex_editions_controls/src/instructions/update_platform_fee_secondary_admin.rs index a5e98852..ad6a8be4 100644 --- a/programs/libreplex_editions_controls/src/instructions/update_platform_fee_secondary_admin.rs +++ b/programs/libreplex_editions_controls/src/instructions/update_platform_fee_secondary_admin.rs @@ -1,17 +1,13 @@ use anchor_lang::prelude::*; use libreplex_editions::program::LibreplexEditions; use libreplex_shared::wrapped_sol; - - use crate::{EditionsControls, Phase}; - #[derive(AnchorDeserialize, AnchorSerialize, Clone)] pub struct UpdatePlatformFeeSecondaryAdminInput { pub new_admin: Pubkey, } - #[derive(Accounts)] #[instruction(input: UpdatePlatformFeeSecondaryAdminInput)] pub struct UpdatePlatformFeeSecondaryAdminCtx<'info> { diff --git a/programs/libreplex_editions_controls/src/instructions/update_royalties.rs b/programs/libreplex_editions_controls/src/instructions/update_royalties.rs index 3793e8a9..3916431f 100644 --- a/programs/libreplex_editions_controls/src/instructions/update_royalties.rs +++ b/programs/libreplex_editions_controls/src/instructions/update_royalties.rs @@ -58,7 +58,7 @@ pub fn update_royalties(ctx: Context, royalties_input: Updat editions_deployment_key.as_ref(), &[ctx.bumps.editions_controls], ]; - msg!("libreplex_editions::cpi::modify_royalties start"); + libreplex_editions::cpi::modify_royalties( CpiContext::new_with_signer( libreplex_editions_program.to_account_info(), @@ -72,6 +72,6 @@ pub fn update_royalties(ctx: Context, royalties_input: Updat }, &[seeds] ), royalties_input)?; - msg!("libreplex_editions::cpi::modify_royalties done"); + Ok(()) } diff --git a/programs/libreplex_editions_controls/src/lib.rs b/programs/libreplex_editions_controls/src/lib.rs index 584e083e..c8dd0375 100644 --- a/programs/libreplex_editions_controls/src/lib.rs +++ b/programs/libreplex_editions_controls/src/lib.rs @@ -5,7 +5,7 @@ pub use logic::*; pub mod instructions; pub use instructions::*; -declare_id!("5hEa5j38yNJRM9vQA44Q6gXVj4Db8y3mWxkDtQeofKKs"); +declare_id!("CCWfNBFcrjbKSjKes9DqgiQYsnmCFgCVKx21mmV2xWgN"); pub mod errors; pub mod state; diff --git a/programs/libreplex_editions_controls/src/logic/check_allow_list_constraints.rs b/programs/libreplex_editions_controls/src/logic/check_allow_list_constraints.rs new file mode 100644 index 00000000..280a5eb5 --- /dev/null +++ b/programs/libreplex_editions_controls/src/logic/check_allow_list_constraints.rs @@ -0,0 +1,57 @@ +use anchor_lang::{ + accounts::account::Account, + prelude::*, + solana_program::hash::hashv, +}; +use rarible_merkle_verify::verify; +use crate::{ + MinterStats, Phase, + errors::{EditionsControlsError}, +}; + +/// We need to discern between leaf and intermediate nodes to prevent trivial second +/// pre-image attacks. +/// https://flawed.net.nz/2018/02/21/attacking-merkle-trees-with-a-second-preimage-attack +const LEAF_PREFIX: &[u8] = &[0]; + +pub fn check_allow_list_constraints( + phase: &Phase, + minter: &Pubkey, + minter_stats_phase: &mut Account, + merkle_proof: Option>, + allow_list_price: Option, + allow_list_max_claims: Option, +) -> Result<()> { + if let Some(merkle_root) = phase.merkle_root { + if let Some(proof) = merkle_proof { + if let (Some(phase_list_price), Some(phase_max_claims)) = (allow_list_price, allow_list_max_claims) { + /// 1. check constraints + /// dev: notice that if phase_max_claims is 0, this constraint is disabled + if phase_max_claims > 0 && minter_stats_phase.mint_count >= phase_max_claims { + return Err(EditionsControlsError::ExceededAllowListMaxClaims.into()); + } + + /// 2. construct leaf + let leaf = hashv(&[ + &minter.to_bytes(), + &phase_list_price.to_le_bytes(), + &phase_max_claims.to_le_bytes(), + ]); + let node = hashv(&[LEAF_PREFIX, &leaf.to_bytes()]); + + /// 3. verify proof against merkle root + if !verify(proof, merkle_root, node.to_bytes()) { + return Err(EditionsControlsError::InvalidMerkleProof.into()); + } + } else { + return Err(EditionsControlsError::AllowListPriceAndMaxClaimsRequired.into()); + } + } else { + return Err(EditionsControlsError::MerkleProofRequired.into()); + } + } else { + return Err(EditionsControlsError::MerkleRootNotSet.into()); + } + + Ok(()) +} diff --git a/programs/libreplex_editions_controls/src/logic/check_phase_constraints.rs b/programs/libreplex_editions_controls/src/logic/check_phase_constraints.rs index 64ef4069..9d208f53 100644 --- a/programs/libreplex_editions_controls/src/logic/check_phase_constraints.rs +++ b/programs/libreplex_editions_controls/src/logic/check_phase_constraints.rs @@ -1,43 +1,50 @@ -use anchor_lang::accounts::account::Account; - -use anchor_lang::prelude::*; - -use crate::{EditionsControls, MinterStats, Phase}; +use anchor_lang::{ + accounts::account::Account, + prelude::*, +}; +use crate::{ + EditionsControls,MinterStats,Phase, + errors::EditionsControlsError, +}; pub fn check_phase_constraints( phase: &Phase, minter_stats: &mut Account, minter_stats_phase: &mut Account, editions_controls: &Account, -) { - // check +) -> Result<()> { let clock = Clock::get().unwrap(); let current_time = clock.unix_timestamp; if !phase.active { - panic!("Phase not active") + return Err(EditionsControlsError::PhaseNotActive.into()); } - msg!("{} {}", phase.start_time, current_time); if phase.start_time > current_time { - panic!("Phase not yet started") + return Err(EditionsControlsError::PhaseNotStarted.into()); } if phase.end_time <= current_time { - panic!("Phase already finished") - } - - if phase.max_mints_per_wallet > 0 && minter_stats_phase.mint_count >= phase.max_mints_per_wallet { - panic!("This wallet has exceeded max mints in the current phase") + return Err(EditionsControlsError::PhaseAlreadyFinished.into()); } + /// Checks if the total mints for the phase has been exceeded (phase sold out) + /// @dev dev: notice that if max_mints_total is 0, this constraint is disabled if phase.max_mints_total > 0 && phase.current_mints >= phase.max_mints_total { - panic!("Total mints exceeded in this phase") + return Err(EditionsControlsError::ExceededMaxMintsForPhase.into()); } + /// Checks if the user has exceeded the max mints for the deployment (across all phases!) + /// dev: notice that if max_mints_per_wallet is 0, this constraint is disabled if editions_controls.max_mints_per_wallet > 0 && minter_stats.mint_count >= editions_controls.max_mints_per_wallet { - panic!("This wallet has exceeded max mints for the deployment") + return Err(EditionsControlsError::ExceededWalletMaxMintsForCollection.into()); + } + + /// Checks if the user has exceeded the max mints for the current phase + /// dev: notice that if max_mints_per_wallet is 0, this constraint is disabled + if phase.max_mints_per_wallet > 0 && minter_stats_phase.mint_count >= phase.max_mints_per_wallet { + return Err(EditionsControlsError::ExceededWalletMaxMintsForPhase.into()); } - + Ok(()) } diff --git a/programs/libreplex_editions_controls/src/logic/mod.rs b/programs/libreplex_editions_controls/src/logic/mod.rs index 3ee46df1..6b143b6c 100644 --- a/programs/libreplex_editions_controls/src/logic/mod.rs +++ b/programs/libreplex_editions_controls/src/logic/mod.rs @@ -1,3 +1,5 @@ - pub mod check_phase_constraints; pub use check_phase_constraints::*; + +pub mod check_allow_list_constraints; +pub use check_allow_list_constraints::*; diff --git a/programs/libreplex_editions_controls/src/state.rs b/programs/libreplex_editions_controls/src/state.rs index 5fd11fbb..58f99365 100644 --- a/programs/libreplex_editions_controls/src/state.rs +++ b/programs/libreplex_editions_controls/src/state.rs @@ -10,15 +10,16 @@ pub struct PlatformFeeRecipient { #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct UpdatePlatformFeeArgs { pub platform_fee_value: u64, // Always required - pub recipients: Vec, + pub recipients: Vec, pub is_fee_flat: bool, // Flag to indicate if the fee is flat } impl PlatformFeeRecipient { - pub const SIZE: usize = 8 + 32 + 1; // Pubkey + u8 + pub const SIZE: usize = 8 // discriminator + + 32 // address + + 1; // share } -// max length: 8 + 8 + 8 + 1 + 8 = 33 #[derive(Clone, AnchorDeserialize, AnchorSerialize)] pub struct Phase { pub price_amount: u64, @@ -26,22 +27,27 @@ pub struct Phase { pub start_time: i64, // set to any date before now for instant activate pub active: bool, pub max_mints_per_wallet: u64, // set to 0 for unlimited - pub max_mints_total: u64, // set to 0 for unlimited (applied across all the phases) + pub max_mints_total: u64, // set to 0 for unlimited pub end_time: i64, // set to i64::MAX for unlimited pub current_mints: u64, + pub is_private: bool, + pub merkle_root: Option<[u8; 32]>, pub padding: [u8; 200] } impl Phase { - pub const SIZE: usize = 8 - + 32 - + 8 - + 1 - + 8 - + 8 - + 8 - + 8 - + 200; + pub const SIZE: usize = 8 // discriminator + + 8 // price_amount + + 32 // price_token + + 8 // start_time + + 1 // active + + 8 // max_mints_per_wallet + + 8 // max_mints_total + + 8 // end_time + + 8 // current_mints + + 1 // is_private + + 32 + 1 // merkle_root + + 200; // padding } pub const DEFAULT_PLATFORM_FEE_PRIMARY_ADMIN: &str = "674s1Sap3KVnr8WGrY5KGQ69oTYjjgr1disKJo6GpTYw"; @@ -50,12 +56,15 @@ pub const DEFAULT_PLATFORM_FEE_SECONDARY_ADMIN: &str = "9qGioKcyuh1mwXS8MbmMgdtQ #[account] pub struct MinterStats { pub wallet: Pubkey, - pub mint_count: u64, // set to any date before now for instant activate + pub mint_count: u64, pub padding: [u8; 50] } impl MinterStats { - pub const SIZE: usize = 8 + 32 + 8 + 8 + 50; + pub const SIZE: usize = 8 // discriminator + + 32 // wallet + + 8 // mint_count + + 50; // padding } #[account] @@ -70,8 +79,8 @@ pub struct EditionsControls { pub platform_fee_value: u64, // Fee amount or basis points pub is_fee_flat: bool, // True for flat fee, false for percentage-based fee pub platform_fee_recipients: [PlatformFeeRecipient; 5], // Fixed-length array of 5 recipients and their shares + pub phases: Vec, // Vec of phases pub padding: [u8; 200], // in case we need some more stuff in the future - pub phases: Vec, } impl EditionsControls { @@ -86,8 +95,9 @@ impl EditionsControls { + 8 // platform_fee_value + 1 // is_fee_flat + (PlatformFeeRecipient::SIZE * 5) // platform_fee_recipients (5 * 33 = 165) - + 200 // padding - + 4; // Vec length for phases + + 4 // Vec length for phases + + 200; // padding + pub fn get_size(number_of_phases: usize) -> usize { EditionsControls::INITIAL_SIZE + Phase::SIZE * number_of_phases } diff --git a/target/types/libreplex_default_renderer.ts b/target/types/libreplex_default_renderer.ts deleted file mode 100644 index 312568ba..00000000 --- a/target/types/libreplex_default_renderer.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Program IDL in camelCase format in order to be used in JS/TS. - * - * Note that this is only a type helper and is not the actual IDL. The original - * IDL can be found at `target/idl/libreplex_default_renderer.json`. - */ -export type LibreplexDefaultRenderer = { - "address": "EfC2horkE4hCaCruMvoDDWxUhcCgVaufHeG3Hkj69eR8", - "metadata": { - "name": "libreplexDefaultRenderer", - "version": "0.1.2", - "spec": "0.1.0", - "description": "Created with Anchor" - }, - "instructions": [ - { - "name": "canonical", - "discriminator": [ - 233, - 11, - 68, - 244, - 108, - 63, - 142, - 79 - ], - "accounts": [ - { - "name": "metadata" - }, - { - "name": "mint" - }, - { - "name": "group" - }, - { - "name": "renderState", - "pda": { - "seeds": [ - { - "kind": "account", - "path": "mint" - } - ] - } - }, - { - "name": "outputAccount" - } - ], - "args": [ - { - "name": "renderInput", - "type": { - "defined": { - "name": "renderInput" - } - } - } - ], - "returns": "bytes" - } - ], - "types": [ - { - "name": "renderInput", - "type": { - "kind": "struct", - "fields": [] - } - } - ] -}; diff --git a/target/types/libreplex_editions.ts b/target/types/libreplex_editions.ts index 0b51055c..c40adcad 100644 --- a/target/types/libreplex_editions.ts +++ b/target/types/libreplex_editions.ts @@ -820,19 +820,19 @@ export type LibreplexEditions = { "type": "string" }, { - "name": "name", + "name": "itemBaseName", "type": "string" }, { - "name": "offchainUrl", + "name": "itemBaseUri", "type": "string" }, { - "name": "nameIsTemplate", + "name": "itemNameIsTemplate", "type": "bool" }, { - "name": "urlIsTemplate", + "name": "itemUriIsTemplate", "type": "bool" }, { @@ -890,22 +890,22 @@ export type LibreplexEditions = { "type": { "kind": "struct", "fields": [ - { - "name": "maxNumberOfTokens", - "type": "u64" - }, { "name": "symbol", "type": "string" }, { - "name": "name", + "name": "collectionName", "type": "string" }, { - "name": "offchainUrl", + "name": "collectionUri", "type": "string" }, + { + "name": "maxNumberOfTokens", + "type": "u64" + }, { "name": "creatorCosignProgramId", "type": { @@ -917,7 +917,7 @@ export type LibreplexEditions = { "type": "string" }, { - "name": "itemName", + "name": "itemBaseName", "type": "string" } ] diff --git a/target/types/libreplex_editions_controls.ts b/target/types/libreplex_editions_controls.ts index f2519fa6..758cdd30 100644 --- a/target/types/libreplex_editions_controls.ts +++ b/target/types/libreplex_editions_controls.ts @@ -5,7 +5,7 @@ * IDL can be found at `target/idl/libreplex_editions_controls.json`. */ export type LibreplexEditionsControls = { - "address": "5hEa5j38yNJRM9vQA44Q6gXVj4Db8y3mWxkDtQeofKKs", + "address": "CCWfNBFcrjbKSjKes9DqgiQYsnmCFgCVKx21mmV2xWgN", "metadata": { "name": "libreplexEditionsControls", "version": "0.2.1", @@ -51,7 +51,7 @@ export type LibreplexEditionsControls = { }, { "name": "libreplexEditionsProgram", - "address": "587DoLBH2H39i5bToWBc6zRgbD2iJZtc4Kb8nYsskYTq" + "address": "GHVmd43GebD2SSedqG1AAh76izf5CPAhz6KotkZyRzUD" } ], "args": [ @@ -152,7 +152,7 @@ export type LibreplexEditionsControls = { }, { "name": "libreplexEditionsProgram", - "address": "587DoLBH2H39i5bToWBc6zRgbD2iJZtc4Kb8nYsskYTq" + "address": "GHVmd43GebD2SSedqG1AAh76izf5CPAhz6KotkZyRzUD" } ], "args": [ @@ -365,7 +365,7 @@ export type LibreplexEditionsControls = { }, { "name": "libreplexEditionsProgram", - "address": "587DoLBH2H39i5bToWBc6zRgbD2iJZtc4Kb8nYsskYTq" + "address": "GHVmd43GebD2SSedqG1AAh76izf5CPAhz6KotkZyRzUD" } ], "args": [ @@ -595,7 +595,7 @@ export type LibreplexEditionsControls = { }, { "name": "libreplexEditionsProgram", - "address": "587DoLBH2H39i5bToWBc6zRgbD2iJZtc4Kb8nYsskYTq" + "address": "GHVmd43GebD2SSedqG1AAh76izf5CPAhz6KotkZyRzUD" } ], "args": [ @@ -736,6 +736,66 @@ export type LibreplexEditionsControls = { "code": 6016, "name": "invalidPhaseIndex", "msg": "Invalid phase index." + }, + { + "code": 6017, + "name": "privatePhaseNoProof", + "msg": "Private phase but no merkle proof provided" + }, + { + "code": 6018, + "name": "merkleRootNotSet", + "msg": "Merkle root not set for allow list mint" + }, + { + "code": 6019, + "name": "merkleProofRequired", + "msg": "Merkle proof required for allow list mint" + }, + { + "code": 6020, + "name": "allowListPriceAndMaxClaimsRequired", + "msg": "Allow list price and max claims are required for allow list mint" + }, + { + "code": 6021, + "name": "invalidMerkleProof", + "msg": "Invalid merkle proof" + }, + { + "code": 6022, + "name": "exceededAllowListMaxClaims", + "msg": "This wallet has exceeded allow list max_claims in the current phase" + }, + { + "code": 6023, + "name": "phaseNotActive", + "msg": "Phase not active" + }, + { + "code": 6024, + "name": "phaseNotStarted", + "msg": "Phase not yet started" + }, + { + "code": 6025, + "name": "phaseAlreadyFinished", + "msg": "Phase already finished" + }, + { + "code": 6026, + "name": "exceededMaxMintsForPhase", + "msg": "Exceeded max mints for this phase" + }, + { + "code": 6027, + "name": "exceededWalletMaxMintsForPhase", + "msg": "Exceeded wallet max mints for this phase" + }, + { + "code": 6028, + "name": "exceededWalletMaxMintsForCollection", + "msg": "Exceeded wallet max mints for the collection" } ], "types": [ @@ -825,15 +885,6 @@ export type LibreplexEditionsControls = { ] } }, - { - "name": "padding", - "type": { - "array": [ - "u8", - 200 - ] - } - }, { "name": "phases", "type": { @@ -843,6 +894,15 @@ export type LibreplexEditionsControls = { } } } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 200 + ] + } } ] } @@ -881,19 +941,19 @@ export type LibreplexEditionsControls = { "type": "string" }, { - "name": "name", + "name": "itemBaseName", "type": "string" }, { - "name": "offchainUrl", + "name": "itemBaseUri", "type": "string" }, { - "name": "nameIsTemplate", + "name": "itemNameIsTemplate", "type": "bool" }, { - "name": "urlIsTemplate", + "name": "itemUriIsTemplate", "type": "bool" }, { @@ -930,11 +990,11 @@ export type LibreplexEditionsControls = { "type": "string" }, { - "name": "name", + "name": "collectionName", "type": "string" }, { - "name": "offchainUrl", + "name": "collectionUri", "type": "string" }, { @@ -966,7 +1026,7 @@ export type LibreplexEditionsControls = { "type": "string" }, { - "name": "itemName", + "name": "itemBaseName", "type": "string" }, { @@ -1008,6 +1068,21 @@ export type LibreplexEditionsControls = { { "name": "endTime", "type": "i64" + }, + { + "name": "isPrivate", + "type": "bool" + }, + { + "name": "merkleRoot", + "type": { + "option": { + "array": [ + "u8", + 32 + ] + } + } } ] } @@ -1020,6 +1095,31 @@ export type LibreplexEditionsControls = { { "name": "phaseIndex", "type": "u32" + }, + { + "name": "merkleProof", + "type": { + "option": { + "vec": { + "array": [ + "u8", + 32 + ] + } + } + } + }, + { + "name": "allowListPrice", + "type": { + "option": "u64" + } + }, + { + "name": "allowListMaxClaims", + "type": { + "option": "u64" + } } ] } @@ -1086,6 +1186,21 @@ export type LibreplexEditionsControls = { "name": "currentMints", "type": "u64" }, + { + "name": "isPrivate", + "type": "bool" + }, + { + "name": "merkleRoot", + "type": { + "option": { + "array": [ + "u8", + 32 + ] + } + } + }, { "name": "padding", "type": { diff --git a/target/types/libreplex_monoswap.ts b/target/types/libreplex_monoswap.ts deleted file mode 100644 index 28b3c05e..00000000 --- a/target/types/libreplex_monoswap.ts +++ /dev/null @@ -1,467 +0,0 @@ -/** - * Program IDL in camelCase format in order to be used in JS/TS. - * - * Note that this is only a type helper and is not the actual IDL. The original - * IDL can be found at `target/idl/libreplex_monoswap.json`. - */ -export type LibreplexMonoswap = { - "address": "8Ktjj9tddWm4QM1ywrxgNRyxkNHnhBCwr9Tq3iGbPDxc", - "metadata": { - "name": "libreplexMonoswap", - "version": "0.0.0", - "spec": "0.1.0", - "description": "Created with Anchor", - "repository": "https://github.com/LibrePlex/metadata" - }, - "instructions": [ - { - "name": "createMonoswap", - "discriminator": [ - 98, - 245, - 160, - 178, - 252, - 5, - 159, - 225 - ], - "accounts": [ - { - "name": "swapMarker", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 115, - 119, - 97, - 112, - 95, - 109, - 97, - 114, - 107, - 101, - 114 - ] - }, - { - "kind": "account", - "path": "namespace" - }, - { - "kind": "account", - "path": "mintOutgoing" - }, - { - "kind": "account", - "path": "mintIncoming" - } - ] - } - }, - { - "name": "mintIncoming", - "writable": true - }, - { - "name": "mintOutgoing" - }, - { - "name": "mintOutgoingTokenAccountSource", - "writable": true - }, - { - "name": "escrowHolder", - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 115, - 119, - 97, - 112, - 95, - 101, - 115, - 99, - 114, - 111, - 119 - ] - }, - { - "kind": "account", - "path": "namespace" - }, - { - "kind": "account", - "path": "mintIncoming" - } - ] - } - }, - { - "name": "mintOutgoingTokenAccountEscrow", - "writable": true - }, - { - "name": "payer", - "writable": true, - "signer": true - }, - { - "name": "mintOutgoingOwner", - "writable": true, - "signer": true - }, - { - "name": "namespace", - "signer": true - }, - { - "name": "tokenProgram" - }, - { - "name": "associatedTokenProgram", - "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" - }, - { - "name": "systemProgram", - "address": "11111111111111111111111111111111" - } - ], - "args": [ - { - "name": "input", - "type": { - "defined": { - "name": "createMonoSwapInput" - } - } - } - ] - }, - { - "name": "swap", - "discriminator": [ - 248, - 198, - 158, - 145, - 225, - 117, - 135, - 200 - ], - "accounts": [ - { - "name": "swapMarker", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 115, - 119, - 97, - 112, - 95, - 109, - 97, - 114, - 107, - 101, - 114 - ] - }, - { - "kind": "account", - "path": "swap_marker.namespace", - "account": "swapMarker" - }, - { - "kind": "account", - "path": "mintOutgoing" - }, - { - "kind": "account", - "path": "mintIncoming" - } - ] - } - }, - { - "name": "swapMarkerReverse", - "docs": [ - "swapping always creates a symmetrical swap marker that enables a swap back" - ], - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 115, - 119, - 97, - 112, - 95, - 109, - 97, - 114, - 107, - 101, - 114 - ] - }, - { - "kind": "account", - "path": "swap_marker.namespace", - "account": "swapMarker" - }, - { - "kind": "account", - "path": "mintIncoming" - }, - { - "kind": "account", - "path": "mintOutgoing" - } - ] - } - }, - { - "name": "mintIncoming" - }, - { - "name": "mintOutgoing" - }, - { - "name": "mintIncomingTokenAccountSource", - "writable": true - }, - { - "name": "escrowHolder", - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 115, - 119, - 97, - 112, - 95, - 101, - 115, - 99, - 114, - 111, - 119 - ] - }, - { - "kind": "account", - "path": "swap_marker.namespace", - "account": "swapMarker" - }, - { - "kind": "account", - "path": "mintIncoming" - } - ] - } - }, - { - "name": "escrowHolderReverse", - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 115, - 119, - 97, - 112, - 95, - 101, - 115, - 99, - 114, - 111, - 119 - ] - }, - { - "kind": "account", - "path": "swap_marker.namespace", - "account": "swapMarker" - }, - { - "kind": "account", - "path": "mintOutgoing" - } - ] - } - }, - { - "name": "mintIncomingTokenAccountTarget", - "writable": true - }, - { - "name": "mintOutgoingTokenAccountSource", - "writable": true - }, - { - "name": "mintOutgoingTokenAccountTarget", - "writable": true - }, - { - "name": "payer", - "writable": true, - "signer": true - }, - { - "name": "tokenProgram", - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" - }, - { - "name": "tokenProgram2022", - "address": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" - }, - { - "name": "associatedTokenProgram", - "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" - }, - { - "name": "systemProgram", - "address": "11111111111111111111111111111111" - } - ], - "args": [] - } - ], - "accounts": [ - { - "name": "swapMarker", - "discriminator": [ - 186, - 7, - 231, - 231, - 117, - 67, - 107, - 191 - ] - } - ], - "errors": [ - { - "code": 6000, - "name": "badMint", - "msg": "Metadata has a bad mint" - }, - { - "code": 6001, - "name": "cannotInscribeFungible", - "msg": "Cannot inscribe a fungible asset" - }, - { - "code": 6002, - "name": "badAuthority", - "msg": "Bad authority" - }, - { - "code": 6003, - "name": "badAuthorityForHolderInscription", - "msg": "Bad authority for holder inscription" - }, - { - "code": 6004, - "name": "badAuthorityForUpdateAuthInscription", - "msg": "Bad authority for update auth inscription" - }, - { - "code": 6005, - "name": "multiSigThresholdMustBeOne", - "msg": "Multi Signature threshold must be one to create / edit inscriptions" - }, - { - "code": 6006, - "name": "notSquadsMember", - "msg": "Not squads member" - }, - { - "code": 6007, - "name": "inscription2KeyMismatch", - "msg": "Inscription V2 key mismatch" - }, - { - "code": 6008, - "name": "inscriptionV3KeyMismatch", - "msg": "Inscription V3 key mismatch" - }, - { - "code": 6009, - "name": "dataHashMismatch", - "msg": "Metadata data missmatch" - } - ], - "types": [ - { - "name": "createMonoSwapInput", - "type": { - "kind": "struct", - "fields": [ - { - "name": "mintOutgoingAmount", - "type": "u64" - }, - { - "name": "mintIncomingAmount", - "type": "u64" - } - ] - } - }, - { - "name": "swapMarker", - "type": { - "kind": "struct", - "fields": [ - { - "name": "namespace", - "type": "pubkey" - }, - { - "name": "mintIncoming", - "type": "pubkey" - }, - { - "name": "mintOutgoing", - "type": "pubkey" - }, - { - "name": "mintIncomingAmount", - "type": "u64" - }, - { - "name": "mintOutgoingAmount", - "type": "u64" - }, - { - "name": "used", - "type": "bool" - } - ] - } - } - ] -}; diff --git a/test/data/merkle_tree.json b/test/data/merkle_tree.json new file mode 100644 index 00000000..2121a02e --- /dev/null +++ b/test/data/merkle_tree.json @@ -0,0 +1,41 @@ +{ + "merkle_root": [ + 125, 184, 194, 116, 52, 36, 65, 219, 171, 135, 154, 27, 188, 122, 207, 204, + 111, 70, 66, 115, 161, 228, 44, 84, 67, 97, 29, 70, 253, 69, 11, 245 + ], + "max_num_nodes": 2, + "max_total_claim": 6, + "tree_nodes": [ + { + "claimant": [ + 104, 164, 83, 51, 23, 177, 193, 29, 252, 241, 86, 132, 173, 155, 114, + 131, 130, 73, 27, 101, 233, 95, 12, 45, 107, 255, 120, 26, 121, 221, + 120, 54 + ], + "claim_price": 500000, + "max_claims": 3, + "proof": [ + [ + 64, 131, 242, 169, 206, 112, 155, 119, 81, 214, 17, 137, 174, 140, + 208, 220, 141, 177, 213, 131, 127, 104, 181, 15, 121, 228, 87, 25, + 232, 172, 235, 168 + ] + ] + }, + { + "claimant": [ + 186, 39, 133, 92, 39, 241, 42, 161, 180, 15, 92, 18, 15, 101, 248, 80, + 238, 254, 220, 231, 1, 14, 231, 145, 170, 49, 163, 111, 239, 112, 135, 6 + ], + "claim_price": 500000, + "max_claims": 3, + "proof": [ + [ + 86, 37, 15, 136, 192, 159, 125, 244, 163, 213, 251, 242, 217, 215, + 159, 249, 93, 166, 82, 38, 187, 58, 199, 64, 161, 50, 122, 122, 17, + 125, 63, 188 + ] + ] + } + ] +} diff --git a/test/data/minter1.json b/test/data/minter1.json new file mode 100644 index 00000000..018778a6 --- /dev/null +++ b/test/data/minter1.json @@ -0,0 +1,6 @@ +[ + 110, 76, 59, 154, 201, 225, 246, 121, 152, 90, 45, 211, 52, 244, 216, 108, + 118, 248, 113, 239, 61, 248, 207, 122, 98, 26, 184, 92, 51, 97, 52, 218, 104, + 164, 83, 51, 23, 177, 193, 29, 252, 241, 86, 132, 173, 155, 114, 131, 130, 73, + 27, 101, 233, 95, 12, 45, 107, 255, 120, 26, 121, 221, 120, 54 +] diff --git a/test/data/minter2.json b/test/data/minter2.json new file mode 100644 index 00000000..6abe9d9b --- /dev/null +++ b/test/data/minter2.json @@ -0,0 +1,6 @@ +[ + 16, 27, 49, 140, 228, 142, 201, 93, 199, 209, 62, 136, 151, 212, 238, 114, 46, + 204, 155, 132, 26, 227, 44, 245, 239, 29, 195, 63, 77, 162, 28, 220, 186, 39, + 133, 92, 39, 241, 42, 161, 180, 15, 92, 18, 15, 101, 248, 80, 238, 254, 220, + 231, 1, 14, 231, 145, 170, 49, 163, 111, 239, 112, 135, 6 +] diff --git a/test/tests/editions_controls.test.ts b/test/tests/editions_controls.test.ts new file mode 100644 index 00000000..903fc022 --- /dev/null +++ b/test/tests/editions_controls.test.ts @@ -0,0 +1,1428 @@ +import * as anchor from '@coral-xyz/anchor'; +import { Program } from '@coral-xyz/anchor'; +import { PublicKey, Keypair, SystemProgram, ComputeBudgetProgram, LAMPORTS_PER_SOL } from '@solana/web3.js'; +import { ASSOCIATED_TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID } from '@solana/spl-token'; +import { LibreplexEditionsControls } from '../../target/types/libreplex_editions_controls'; +import { LibreplexEditions } from '../../target/types/libreplex_editions'; +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import { estimateTransactionFee, getCluster } from '../utils/utils'; +import { + getEditions, + getEditionsControls, + getMinterStats, + getTokenMetadata, + logEditions, + logEditionsControls, + logMinterStats, + logMinterStatsPhase, + logTokenMetadata, +} from '../utils/getters'; +import { Transaction } from '@solana/web3.js'; +// devnote: try to make tests don't rely on hard addresses but on dynamic runtime ids. +import { TOKEN_GROUP_EXTENSION_PROGRAM_ID } from '../../constants'; +import { getEditionsPda, getEditionsControlsPda, getHashlistPda, getHashlistMarkerPda, getMinterStatsPda, getMinterStatsPhasePda } from '../utils/pdas'; +import { CollectionConfig, AllowListConfig, PhaseConfig } from '../utils/types'; + +const VERBOSE_LOGGING = false; + +describe('Editions Controls Test Suite', () => { + const provider = anchor.AnchorProvider.env(); + anchor.setProvider(provider); + + // Modify compute units + const modifiedComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({ + units: 800000, + }); + + let editionsControlsProgram: Program; + let editionsProgram: Program; + + let editionsPda: PublicKey; + let editionsControlsPda: PublicKey; + let hashlistPda: PublicKey; + + let payer: Keypair; + let creator1: Keypair; + let creator2: Keypair; + let treasury: Keypair; + let platformFeeAdmin: Keypair; + let groupMint: Keypair; + let group: Keypair; + + let minter0: Keypair; // in allowlist + let minter1: Keypair; // in allowlist + let minter2: Keypair; // not in allowlist + + let collectionConfig: CollectionConfig; + let allowListConfig: AllowListConfig; + + let phase0Config: PhaseConfig; + let phase1Config: PhaseConfig; + let phase2Config: PhaseConfig; + let phase3Config: PhaseConfig; + let phase4Config: PhaseConfig; + let phase5Config: PhaseConfig; + + // Generate dynamic keypairs and collection config + before(async () => { + if (VERBOSE_LOGGING) { + const cluster = await getCluster(provider.connection); + console.log('Cluster:', cluster); + } + + editionsControlsProgram = anchor.workspace.LibreplexEditionsControls as Program; + editionsProgram = anchor.workspace.LibreplexEditions as Program; + + payer = (provider.wallet as anchor.Wallet).payer; + creator1 = Keypair.generate(); + creator2 = Keypair.generate(); + treasury = Keypair.generate(); + platformFeeAdmin = Keypair.generate(); + groupMint = Keypair.generate(); + group = Keypair.generate(); + + collectionConfig = { + maxNumberOfTokens: new anchor.BN(20), + symbol: 'COOLX55', + collectionName: 'Collection name with meta, platform fee and royalties', + collectionUri: 'ipfs://QmbsXNSkPUtYNmKfYw1mUSVuz9QU8nhu7YvzM1aAQsv6xw/0', + treasury: treasury.publicKey, + maxMintsPerWallet: new anchor.BN(100), + royalties: { + royaltyBasisPoints: new anchor.BN(1000), + creators: [ + { + address: creator1.publicKey, + share: 50, + }, + { + address: creator2.publicKey, + share: 50, + }, + ], + }, + platformFee: { + platformFeeValue: new anchor.BN(50000), + recipients: [ + { + address: platformFeeAdmin.publicKey, + share: 100, + }, + ], + isFeeFlat: true, + }, + extraMeta: [ + { field: 'field1', value: 'value1' }, + { field: 'field2', value: 'value2' }, + { field: 'field3', value: 'value3' }, + { field: 'field4', value: 'value4' }, + ], + itemBaseUri: 'ipfs://QmbsXNSkPUtYNmKfYw1mUSVuz9QU8nhu7YvzM1aAQsv6xw/{}', + itemBaseName: 'Item T8 V4 #{}', + cosignerProgramId: null, + }; + if (VERBOSE_LOGGING) { + console.log('Collection config: ', collectionConfig); + } + + editionsPda = getEditionsPda(collectionConfig.symbol, editionsProgram.programId); + editionsControlsPda = getEditionsControlsPda(editionsPda, editionsControlsProgram.programId); + hashlistPda = getHashlistPda(editionsPda, editionsProgram.programId); + }); + + // Generate allowlist variables + before(async () => { + minter0 = Keypair.fromSecretKey( + new Uint8Array([ + 110, 76, 59, 154, 201, 225, 246, 121, 152, 90, 45, 211, 52, 244, 216, 108, 118, 248, 113, 239, 61, 248, 207, 122, 98, 26, 184, 92, 51, 97, 52, 218, 104, + 164, 83, 51, 23, 177, 193, 29, 252, 241, 86, 132, 173, 155, 114, 131, 130, 73, 27, 101, 233, 95, 12, 45, 107, 255, 120, 26, 121, 221, 120, 54, + ]) + ); + minter1 = Keypair.fromSecretKey( + new Uint8Array([ + 16, 27, 49, 140, 228, 142, 201, 93, 199, 209, 62, 136, 151, 212, 238, 114, 46, 204, 155, 132, 26, 227, 44, 245, 239, 29, 195, 63, 77, 162, 28, 220, 186, + 39, 133, 92, 39, 241, 42, 161, 180, 15, 92, 18, 15, 101, 248, 80, 238, 254, 220, 231, 1, 14, 231, 145, 170, 49, 163, 111, 239, 112, 135, 6, + ]) + ); + minter2 = Keypair.generate(); + allowListConfig = { + merkleRoot: Buffer.from([ + 125, 184, 194, 116, 52, 36, 65, 219, 171, 135, 154, 27, 188, 122, 207, 204, 111, 70, 66, 115, 161, 228, 44, 84, 67, 97, 29, 70, 253, 69, 11, 245, + ]), + list: [ + { + address: minter0.publicKey, + price: new anchor.BN(500000), // 0.005 SOL + max_claims: new anchor.BN(3), + proof: [ + Buffer.from([ + 64, 131, 242, 169, 206, 112, 155, 119, 81, 214, 17, 137, 174, 140, 208, 220, 141, 177, 213, 131, 127, 104, 181, 15, 121, 228, 87, 25, 232, 172, + 235, 168, + ]), + ], + }, + { + address: minter1.publicKey, + price: new anchor.BN(500000), // 0.005 SOL + max_claims: new anchor.BN(3), + proof: [ + Buffer.from([ + 86, 37, 15, 136, 192, 159, 125, 244, 163, 213, 251, 242, 217, 215, 159, 249, 93, 166, 82, 38, 187, 58, 199, 64, 161, 50, 122, 122, 17, 125, 63, + 188, + ]), + ], + }, + ], + }; + }); + + // Perform needed airdrops to minter0, treasury and platformFeeRecipient + before(async () => { + // Airdrop SOL to minter0 + const minter0AirdropSignature = await provider.connection.requestAirdrop(minter0.publicKey, 1 * LAMPORTS_PER_SOL); + await provider.connection.confirmTransaction(minter0AirdropSignature); + + // Airdrop SOL to minter1 + const minter1AirdropSignature = await provider.connection.requestAirdrop(minter1.publicKey, 1 * LAMPORTS_PER_SOL); + await provider.connection.confirmTransaction(minter1AirdropSignature); + + // Airdrop SOL to minter2 + const minter2AirdropSignature = await provider.connection.requestAirdrop(minter2.publicKey, 1 * LAMPORTS_PER_SOL); + await provider.connection.confirmTransaction(minter2AirdropSignature); + + // Airdrop SOL to treasury + const treasuryAirdropSignature = await provider.connection.requestAirdrop(collectionConfig.treasury, 1 * LAMPORTS_PER_SOL); + await provider.connection.confirmTransaction(treasuryAirdropSignature); + + // Airdrop SOL to platformFeeRecipient + const platformFeeRecipientAirdropSignature = await provider.connection.requestAirdrop(platformFeeAdmin.publicKey, 1 * LAMPORTS_PER_SOL); + await provider.connection.confirmTransaction(platformFeeRecipientAirdropSignature); + }); + + describe('Deploying', () => { + it('Should deploy a collection', async () => { + // Modify compute units for the transaction + const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({ + units: 800000, + }); + + try { + const initialiseIx = await editionsControlsProgram.methods + .initialiseEditionsControls({ + maxMintsPerWallet: collectionConfig.maxMintsPerWallet, + treasury: collectionConfig.treasury, + maxNumberOfTokens: collectionConfig.maxNumberOfTokens, + symbol: collectionConfig.symbol, + collectionName: collectionConfig.collectionName, + collectionUri: collectionConfig.collectionUri, + cosignerProgramId: collectionConfig.cosignerProgramId, + royalties: collectionConfig.royalties, + platformFee: collectionConfig.platformFee, + extraMeta: collectionConfig.extraMeta, + itemBaseUri: collectionConfig.itemBaseUri, + itemBaseName: collectionConfig.itemBaseName, + }) + .accountsStrict({ + editionsControls: editionsControlsPda, + editionsDeployment: editionsPda, + hashlist: hashlistPda, + payer: payer.publicKey, + creator: payer.publicKey, + groupMint: groupMint.publicKey, + group: group.publicKey, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + libreplexEditionsProgram: editionsProgram.programId, + groupExtensionProgram: new PublicKey('5hx15GaPPqsYA61v6QpcGPpo125v7rfvEfZQ4dJErG5V'), + }) + .instruction(); + + const transaction = new Transaction().add(modifyComputeUnits).add(initialiseIx); + + await provider.sendAndConfirm(transaction, [groupMint, group, payer]); + + // Fetch updated state + const editionsDecoded = await getEditions(provider.connection, editionsPda, editionsProgram); + const editionsControlsDecoded = await getEditionsControls(provider.connection, editionsControlsPda, editionsControlsProgram); + const metadata = await getTokenMetadata(provider.connection, groupMint.publicKey); + if (VERBOSE_LOGGING) { + logEditions(editionsDecoded); + logEditionsControls(editionsControlsDecoded); + logTokenMetadata(metadata); + } + + // Verify Editions deployment + expect(editionsDecoded.data.symbol).to.equal(collectionConfig.symbol); + expect(editionsDecoded.data.creator.toBase58()).to.equal(editionsControlsPda.toBase58()); + expect(editionsDecoded.data.maxNumberOfTokens.toString()).to.equal(collectionConfig.maxNumberOfTokens.toString()); + expect(editionsDecoded.data.itemBaseName).to.equal(collectionConfig.itemBaseName); + expect(editionsDecoded.data.itemBaseUri).to.equal(collectionConfig.itemBaseUri); + + // Verify EditionsControls deployment + expect(editionsControlsDecoded.data.editionsDeployment.toBase58()).to.equal(editionsPda.toBase58()); + expect(editionsControlsDecoded.data.creator.toBase58()).to.equal(payer.publicKey.toBase58()); + expect(editionsControlsDecoded.data.treasury.toBase58()).to.equal(collectionConfig.treasury.toBase58()); + expect(Number(editionsControlsDecoded.data.maxMintsPerWallet)).to.equal(Number(collectionConfig.maxMintsPerWallet)); + expect(editionsControlsDecoded.data.phases.length).to.equal(0); + + // Verify metadata + expect(metadata.name).to.equal(collectionConfig.collectionName); + expect(metadata.uri).to.equal(collectionConfig.collectionUri); + expect(metadata.mint.toBase58()).to.equal(groupMint.publicKey.toBase58()); + // Verify that every key in extraMeta is present in metadata.additionalMetadata + collectionConfig.extraMeta.forEach((meta) => { + expect(metadata.additionalMetadata).to.have.property(meta.field); + expect(metadata.additionalMetadata[meta.field]).to.equal(meta.value); + }); + + // Add more assertions as needed + } catch (error) { + console.error('Error in initialiseEditionsControls:', error); + throw error; + } + }); + }); + + describe('Adding phases', () => { + it('Should add a private phase with allowlist. [Phase Index 0]: Open: Not Available, Allowlist: 0.05 SOL', async () => { + phase0Config = { + maxMintsPerWallet: new anchor.BN(100), + maxMintsTotal: new anchor.BN(1000), + priceAmount: new anchor.BN(0), // Not openly buyable, only available to allowlist + startTime: new anchor.BN(new Date().getTime() / 1000), + endTime: new anchor.BN(new Date().getTime() / 1000 + 60 * 60 * 24), // 1 day from now + priceToken: new PublicKey('So11111111111111111111111111111111111111112'), + isPrivate: true, + merkleRoot: allowListConfig.merkleRoot, + }; + const phaseIx = await editionsControlsProgram.methods + .addPhase(phase0Config) + .accountsStrict({ + editionsControls: editionsControlsPda, + creator: payer.publicKey, + payer: payer.publicKey, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + libreplexEditionsProgram: editionsProgram.programId, + }) + .signers([]) + .instruction(); + + const transaction = new Transaction().add(phaseIx); + await provider.sendAndConfirm(transaction, [payer]); + + const editionsControlsDecoded = await getEditionsControls(provider.connection, editionsControlsPda, editionsControlsProgram); + if (VERBOSE_LOGGING) { + logEditionsControls(editionsControlsDecoded); + } + + // verify state + expect(editionsControlsDecoded.data.phases.length).to.equal(1); + }); + + it('Should add a public phase with allowlist. [Phase Index 1]: Open: O.1 SOL, Allowlist: 0.05 SOL', async () => { + phase1Config = { + maxMintsPerWallet: new anchor.BN(5), + maxMintsTotal: new anchor.BN(10), + priceAmount: new anchor.BN(1000000), // 0.1 SOL + startTime: new anchor.BN(new Date().getTime() / 1000), + endTime: new anchor.BN(new Date().getTime() / 1000 + 60 * 60 * 24), // 1 day from now + priceToken: new PublicKey('So11111111111111111111111111111111111111112'), + isPrivate: false, + merkleRoot: allowListConfig.merkleRoot, + }; + + const phaseIx = await editionsControlsProgram.methods + .addPhase(phase1Config) + .accountsStrict({ + editionsControls: editionsControlsPda, + creator: payer.publicKey, + payer: payer.publicKey, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + libreplexEditionsProgram: editionsProgram.programId, + }) + .signers([]) + .instruction(); + + const transaction = new Transaction().add(phaseIx); + await provider.sendAndConfirm(transaction, [payer]); + + // get state + const editionsControlsDecoded = await getEditionsControls(provider.connection, editionsControlsPda, editionsControlsProgram); + if (VERBOSE_LOGGING) { + logEditionsControls(editionsControlsDecoded); + } + + // verify state + expect(editionsControlsDecoded.data.phases.length).to.equal(2); + }); + + it('Should add a public phase without allowlist. [Phase Index 2]: Open: 0.05 SOL, Allowlist: Not Available', async () => { + phase2Config = { + maxMintsPerWallet: new anchor.BN(100), + maxMintsTotal: new anchor.BN(1000), + priceAmount: new anchor.BN(500000), // 0.05 SOL + startTime: new anchor.BN(new Date().getTime() / 1000), + endTime: new anchor.BN(new Date().getTime() / 1000 + 60 * 60 * 24), // 1 day from now + priceToken: new PublicKey('So11111111111111111111111111111111111111112'), + isPrivate: false, + merkleRoot: null, + }; + + const phaseIx = await editionsControlsProgram.methods + .addPhase(phase2Config) + .accountsStrict({ + editionsControls: editionsControlsPda, + creator: payer.publicKey, + payer: payer.publicKey, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + libreplexEditionsProgram: editionsProgram.programId, + }) + .signers([]) + .instruction(); + + const transaction = new Transaction().add(phaseIx); + await provider.sendAndConfirm(transaction, [payer]); + + // get state + const editionsControlsDecoded = await getEditionsControls(provider.connection, editionsControlsPda, editionsControlsProgram); + if (VERBOSE_LOGGING) { + logEditionsControls(editionsControlsDecoded); + } + + // verify state + expect(editionsControlsDecoded.data.phases.length).to.equal(3); + }); + + it('Should add a public phase without max supply, up to collection max. [Phase Index 3]', async () => { + phase3Config = { + maxMintsPerWallet: new anchor.BN(100), + maxMintsTotal: new anchor.BN(0), // unlimited supply + priceAmount: new anchor.BN(500000), // 0.05 SOL + startTime: new anchor.BN(new Date().getTime() / 1000), + endTime: new anchor.BN(new Date().getTime() / 1000 + 60 * 60 * 24), // 1 day from now + priceToken: new PublicKey('So11111111111111111111111111111111111111112'), + isPrivate: false, + merkleRoot: null, + }; + + const phaseIx = await editionsControlsProgram.methods + .addPhase(phase3Config) + .accountsStrict({ + editionsControls: editionsControlsPda, + creator: payer.publicKey, + payer: payer.publicKey, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + libreplexEditionsProgram: editionsProgram.programId, + }) + .signers([]) + .instruction(); + + const transaction = new Transaction().add(phaseIx); + await provider.sendAndConfirm(transaction, [payer]); + + // get state + const editionsControlsDecoded = await getEditionsControls(provider.connection, editionsControlsPda, editionsControlsProgram); + if (VERBOSE_LOGGING) { + logEditionsControls(editionsControlsDecoded); + } + + // verify state + expect(editionsControlsDecoded.data.phases.length).to.equal(4); + }); + + it('Should add a phase that starts in the future. [Phase Index 4]', async () => { + phase4Config = { + maxMintsPerWallet: new anchor.BN(100), + maxMintsTotal: new anchor.BN(1000), + priceAmount: new anchor.BN(500000), // 0.05 SOL + startTime: new anchor.BN(new Date().getTime() / 1000 + 60 * 60 * 24), // 1 day from now + endTime: new anchor.BN(new Date().getTime() / 1000 + 60 * 60 * 24 * 7), // 1 week from now + priceToken: new PublicKey('So11111111111111111111111111111111111111112'), + isPrivate: false, + merkleRoot: null, + }; + + const phaseIx = await editionsControlsProgram.methods + .addPhase(phase4Config) + .accountsStrict({ + editionsControls: editionsControlsPda, + creator: payer.publicKey, + payer: payer.publicKey, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + libreplexEditionsProgram: editionsProgram.programId, + }) + .signers([]) + .instruction(); + + const transaction = new Transaction().add(phaseIx); + await provider.sendAndConfirm(transaction, [payer]); + + // get state + const editionsControlsDecoded = await getEditionsControls(provider.connection, editionsControlsPda, editionsControlsProgram); + if (VERBOSE_LOGGING) { + logEditionsControls(editionsControlsDecoded); + } + + // verify state + expect(editionsControlsDecoded.data.phases.length).to.equal(5); + }); + + it('Should add a phase that has already ended. [Phase Index 5]', async () => { + phase5Config = { + maxMintsPerWallet: new anchor.BN(100), + maxMintsTotal: new anchor.BN(1000), + priceAmount: new anchor.BN(500000), // 0.05 SOL + startTime: new anchor.BN(new Date().getTime() / 1000 - 60 * 60 * 24 * 7), // 1 week ago + endTime: new anchor.BN(new Date().getTime() / 1000 - 60 * 60 * 24), // 1 day associatedTokenProgram + priceToken: new PublicKey('So11111111111111111111111111111111111111112'), + isPrivate: false, + merkleRoot: null, + }; + + const phaseIx = await editionsControlsProgram.methods + .addPhase(phase5Config) + .accountsStrict({ + editionsControls: editionsControlsPda, + creator: payer.publicKey, + payer: payer.publicKey, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + libreplexEditionsProgram: editionsProgram.programId, + }) + .signers([]) + .instruction(); + + const transaction = new Transaction().add(phaseIx); + await provider.sendAndConfirm(transaction, [payer]); + + // get state + const editionsControlsDecoded = await getEditionsControls(provider.connection, editionsControlsPda, editionsControlsProgram); + if (VERBOSE_LOGGING) { + logEditionsControls(editionsControlsDecoded); + } + + // verify state + expect(editionsControlsDecoded.data.phases.length).to.equal(6); + }); + }); + + describe('Minting', () => { + describe('Minting on a private allowlist-only phase [Phase Index 0]', () => { + it('Should be able to mint with valid allowlist proof', async () => { + const mintConfig = { + phaseIndex: 0, + merkleProof: allowListConfig.list[0].proof, + allowListPrice: allowListConfig.list[0].price, + allowListMaxClaims: allowListConfig.list[0].max_claims, + }; + + const mint = Keypair.generate(); + const member = Keypair.generate(); + + const hashlistMarkerPda = getHashlistMarkerPda(editionsPda, mint.publicKey, editionsProgram.programId); + const minterStatsPda = getMinterStatsPda(editionsPda, minter0.publicKey, editionsControlsProgram.programId); + const minterStatsPhasePda = getMinterStatsPhasePda(editionsPda, minter0.publicKey, 0, editionsControlsProgram.programId); + const associatedTokenAddressSync = getAssociatedTokenAddressSync(mint.publicKey, minter0.publicKey, false, TOKEN_2022_PROGRAM_ID); + + const mintIx = await editionsControlsProgram.methods + .mintWithControls(mintConfig) + .accountsStrict({ + editionsDeployment: editionsPda, + editionsControls: editionsControlsPda, + hashlist: hashlistPda, + hashlistMarker: hashlistMarkerPda, + payer: minter0.publicKey, + signer: minter0.publicKey, + minter: minter0.publicKey, + minterStats: minterStatsPda, + minterStatsPhase: minterStatsPhasePda, + mint: mint.publicKey, + member: member.publicKey, + group: group.publicKey, + groupMint: groupMint.publicKey, + platformFeeRecipient1: platformFeeAdmin.publicKey, + tokenAccount: associatedTokenAddressSync, + treasury: treasury.publicKey, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + groupExtensionProgram: TOKEN_GROUP_EXTENSION_PROGRAM_ID, + systemProgram: SystemProgram.programId, + libreplexEditionsProgram: editionsProgram.programId, + }) + .instruction(); + const transaction = new Transaction().add(modifiedComputeUnits).add(mintIx); + + const minter0BalanceBefore = await provider.connection.getBalance(minter0.publicKey); + const treasuryBalanceBefore = await provider.connection.getBalance(treasury.publicKey); + const platformFeeRecipientBalanceBefore = await provider.connection.getBalance(platformFeeAdmin.publicKey); + + // Estimate transaction fee + const estimatedFee = await estimateTransactionFee(provider.connection, transaction, [minter0, mint, member]); + if (VERBOSE_LOGGING) { + console.log(`Estimated transaction fee: ${estimatedFee} lamports (${estimatedFee / LAMPORTS_PER_SOL} SOL)`); + } + + try { + await provider.sendAndConfirm(transaction, [minter0, mint, member]); + } catch (error) { + console.error('Error in mintWithControls:', error); + throw error; + } + + // Verify state after minting + const editionsDecoded = await getEditions(provider.connection, editionsPda, editionsProgram); + const editionsControlsDecoded = await getEditionsControls(provider.connection, editionsControlsPda, editionsControlsProgram); + const minterStatsDecoded = await getMinterStats(provider.connection, minterStatsPda, editionsControlsProgram); + const minterStatsPhaseDecoded = await getMinterStats(provider.connection, minterStatsPhasePda, editionsControlsProgram); + + // Pull new balances + const minter0BalanceAfter = await provider.connection.getBalance(minter0.publicKey); + const treasuryBalanceAfter = await provider.connection.getBalance(treasury.publicKey); + const platformFeeRecipientBalanceAfter = await provider.connection.getBalance(platformFeeAdmin.publicKey); + + // Verify that the token was minted + expect(editionsDecoded.data.numberOfTokensIssued.toString()).to.equal('1'); + expect(editionsControlsDecoded.data.phases[0].currentMints.toString()).to.equal('1'); + expect(minterStatsDecoded.data.mintCount.toString()).to.equal('1'); + expect(minterStatsPhaseDecoded.data.mintCount.toString()).to.equal('1'); + + // When the fee is flat, is paid by the minter. + const expectedPlatformFee = collectionConfig.platformFee.platformFeeValue; + const expectedTreasuryIncome = mintConfig.allowListPrice; + + if (VERBOSE_LOGGING) { + console.log('balances before', { + minter0: minter0BalanceBefore / LAMPORTS_PER_SOL, + treasury: treasuryBalanceBefore / LAMPORTS_PER_SOL, + platformFeeRecipient: platformFeeRecipientBalanceBefore / LAMPORTS_PER_SOL, + }); + + console.log('balances after', { + minter0: minter0BalanceAfter / LAMPORTS_PER_SOL, + treasury: treasuryBalanceAfter / LAMPORTS_PER_SOL, + platformFeeRecipient: platformFeeRecipientBalanceAfter / LAMPORTS_PER_SOL, + }); + } + + // Verify that the treasury received the correct amount of SOL (0.05 SOL from allowlist price) + expect(treasuryBalanceAfter - treasuryBalanceBefore).to.equal(expectedTreasuryIncome.toNumber()); + // Verify that the platform fee recipient received the correct amount of royalties (0.05 SOL from collectionConfig platform fee) + expect(platformFeeRecipientBalanceAfter - platformFeeRecipientBalanceBefore).to.equal(expectedPlatformFee.toNumber()); + }); + + it('Should not be able to mint without allowlist proof (private phase)', async () => { + const mintConfig = { + phaseIndex: 0, + merkleProof: null, + allowListPrice: null, + allowListMaxClaims: null, + }; + + const mint = Keypair.generate(); + const member = Keypair.generate(); + + const hashlistMarkerPda = getHashlistMarkerPda(editionsPda, mint.publicKey, editionsProgram.programId); + const minterStatsPda = getMinterStatsPda(editionsPda, minter0.publicKey, editionsControlsProgram.programId); + const minterStatsPhasePda = getMinterStatsPhasePda(editionsPda, minter0.publicKey, 0, editionsControlsProgram.programId); + const associatedTokenAddressSync = getAssociatedTokenAddressSync(mint.publicKey, minter0.publicKey, false, TOKEN_2022_PROGRAM_ID); + + const mintIx = await editionsControlsProgram.methods + .mintWithControls(mintConfig) + .accountsStrict({ + editionsDeployment: editionsPda, + editionsControls: editionsControlsPda, + hashlist: hashlistPda, + hashlistMarker: hashlistMarkerPda, + payer: minter0.publicKey, + signer: minter0.publicKey, + minter: minter0.publicKey, + minterStats: minterStatsPda, + minterStatsPhase: minterStatsPhasePda, + mint: mint.publicKey, + member: member.publicKey, + group: group.publicKey, + groupMint: groupMint.publicKey, + platformFeeRecipient1: platformFeeAdmin.publicKey, + tokenAccount: associatedTokenAddressSync, + treasury: treasury.publicKey, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + groupExtensionProgram: TOKEN_GROUP_EXTENSION_PROGRAM_ID, + systemProgram: SystemProgram.programId, + libreplexEditionsProgram: editionsProgram.programId, + }) + .instruction(); + + const transaction = new Transaction().add(mintIx); + try { + await provider.sendAndConfirm(transaction, [minter0, mint, member]); + } catch (error) { + const errorString = JSON.stringify(error); + expect(errorString).to.include('Private phase but no merkle proof provided'); + } + }); + + it('Should fail to mint when exceeding allowlist max claims', async () => { + const mintConfig = { + phaseIndex: 0, + merkleProof: allowListConfig.list[0].proof, + allowListPrice: allowListConfig.list[0].price, + allowListMaxClaims: allowListConfig.list[0].max_claims, + }; + + // mint twice, then the third mint should fail because the max claims for the allowlist is 3 + const mintWithControls = async () => { + const mint = Keypair.generate(); + const member = Keypair.generate(); + + const hashlistMarkerPda = getHashlistMarkerPda(editionsPda, mint.publicKey, editionsProgram.programId); + const minterStatsPda = getMinterStatsPda(editionsPda, minter0.publicKey, editionsControlsProgram.programId); + const minterStatsPhasePda = getMinterStatsPhasePda(editionsPda, minter0.publicKey, 0, editionsControlsProgram.programId); + const associatedTokenAddressSync = getAssociatedTokenAddressSync(mint.publicKey, minter0.publicKey, false, TOKEN_2022_PROGRAM_ID); + + const mintIx = await editionsControlsProgram.methods + .mintWithControls(mintConfig) + .accountsStrict({ + editionsDeployment: editionsPda, + editionsControls: editionsControlsPda, + hashlist: hashlistPda, + hashlistMarker: hashlistMarkerPda, + payer: minter0.publicKey, + signer: minter0.publicKey, + minter: minter0.publicKey, + minterStats: minterStatsPda, + minterStatsPhase: minterStatsPhasePda, + mint: mint.publicKey, + member: member.publicKey, + group: group.publicKey, + groupMint: groupMint.publicKey, + platformFeeRecipient1: platformFeeAdmin.publicKey, + tokenAccount: associatedTokenAddressSync, + treasury: treasury.publicKey, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + groupExtensionProgram: TOKEN_GROUP_EXTENSION_PROGRAM_ID, + systemProgram: SystemProgram.programId, + libreplexEditionsProgram: editionsProgram.programId, + }) + .instruction(); + + const transaction = new Transaction().add(modifiedComputeUnits).add(mintIx); + try { + await provider.sendAndConfirm(transaction, [minter0, mint, member]); + } catch (error) { + throw error; + } + }; + + for (let i = 0; i < 2; i++) { + try { + await mintWithControls(); + } catch (error) { + throw error; + } + } + + try { + await mintWithControls(); + } catch (error) { + const errorString = JSON.stringify(error); + expect(errorString).to.include('This wallet has exceeded allow list max_claims in the current phase.'); + } + + // expect the user to have minted three total items on the phase + const minterStatsPhasePda = getMinterStatsPhasePda(editionsPda, minter0.publicKey, 0, editionsControlsProgram.programId); + const editionsControlsDecoded = await getEditionsControls(provider.connection, editionsControlsPda, editionsControlsProgram); + const minterStatsPhaseDecoded = await getMinterStats(provider.connection, minterStatsPhasePda, editionsControlsProgram); + + expect(editionsControlsDecoded.data.phases[0].currentMints.toString()).to.equal('3'); + expect(minterStatsPhaseDecoded.data.mintCount.toString()).to.equal('3'); + }); + }); + + describe('Minting on a public phase with optional allowlist [Phase Index 1]', () => { + it('Should be able to mint with allowlist proof at discounted price', async () => { + const mintConfig = { + phaseIndex: 1, + merkleProof: allowListConfig.list[1].proof, + allowListPrice: allowListConfig.list[1].price, + allowListMaxClaims: allowListConfig.list[1].max_claims, + }; + + const mint = Keypair.generate(); + const member = Keypair.generate(); + + const hashlistMarkerPda = getHashlistMarkerPda(editionsPda, mint.publicKey, editionsProgram.programId); + const minterStatsPda = getMinterStatsPda(editionsPda, minter1.publicKey, editionsControlsProgram.programId); + const minterStatsPhasePda = getMinterStatsPhasePda(editionsPda, minter1.publicKey, 1, editionsControlsProgram.programId); + const associatedTokenAddressSync = getAssociatedTokenAddressSync(mint.publicKey, minter1.publicKey, false, TOKEN_2022_PROGRAM_ID); + + // pull before balances + const treasuryBalanceBefore = await provider.connection.getBalance(treasury.publicKey); + const platformFeeRecipientBalanceBefore = await provider.connection.getBalance(platformFeeAdmin.publicKey); + + const mintIx = await editionsControlsProgram.methods + .mintWithControls(mintConfig) + .accountsStrict({ + editionsDeployment: editionsPda, + editionsControls: editionsControlsPda, + hashlist: hashlistPda, + hashlistMarker: hashlistMarkerPda, + payer: minter1.publicKey, + signer: minter1.publicKey, + minter: minter1.publicKey, + minterStats: minterStatsPda, + minterStatsPhase: minterStatsPhasePda, + mint: mint.publicKey, + member: member.publicKey, + group: group.publicKey, + groupMint: groupMint.publicKey, + platformFeeRecipient1: platformFeeAdmin.publicKey, + tokenAccount: associatedTokenAddressSync, + treasury: treasury.publicKey, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + groupExtensionProgram: TOKEN_GROUP_EXTENSION_PROGRAM_ID, + systemProgram: SystemProgram.programId, + libreplexEditionsProgram: editionsProgram.programId, + }) + .instruction(); + + const transaction = new Transaction().add(modifiedComputeUnits).add(mintIx); + try { + await provider.sendAndConfirm(transaction, [minter1, mint, member]); + } catch (error) { + console.error('Error in mintWithControls:', error); + throw error; + } + + // Verify state after minting + const editionsDecoded = await getEditions(provider.connection, editionsPda, editionsProgram); + const editionsControlsDecoded = await getEditionsControls(provider.connection, editionsControlsPda, editionsControlsProgram); + const minterStatsDecoded = await getMinterStats(provider.connection, minterStatsPda, editionsControlsProgram); + const minterStatsPhaseDecoded = await getMinterStats(provider.connection, minterStatsPhasePda, editionsControlsProgram); + + // Verify mint + expect(editionsControlsDecoded.data.phases[1].currentMints.toString()).to.equal('1'); + expect(minterStatsDecoded.data.mintCount.toString()).to.equal('1'); // user already minted on phase 0 + expect(minterStatsPhaseDecoded.data.mintCount.toString()).to.equal('1'); + + // Verify protocol fees & treasury income + const expectedPlatformFee = collectionConfig.platformFee.platformFeeValue; + const expectedTreasuryIncome = mintConfig.allowListPrice; + + const treasuryBalanceAfter = await provider.connection.getBalance(treasury.publicKey); + const platformFeeRecipientBalanceAfter = await provider.connection.getBalance(platformFeeAdmin.publicKey); + + expect(treasuryBalanceAfter - treasuryBalanceBefore).to.equal(expectedTreasuryIncome.toNumber()); + expect(platformFeeRecipientBalanceAfter - platformFeeRecipientBalanceBefore).to.equal(expectedPlatformFee.toNumber()); + }); + + it('Should be able to mint without allowlist proof at full price', async () => { + const mintConfig = { + phaseIndex: 1, + merkleProof: null, + allowListPrice: null, + allowListMaxClaims: null, + }; + + const mint = Keypair.generate(); + const member = Keypair.generate(); + + const hashlistMarkerPda = getHashlistMarkerPda(editionsPda, mint.publicKey, editionsProgram.programId); + const minterStatsPda = getMinterStatsPda(editionsPda, minter1.publicKey, editionsControlsProgram.programId); + const minterStatsPhasePda = getMinterStatsPhasePda(editionsPda, minter1.publicKey, 1, editionsControlsProgram.programId); + const associatedTokenAddressSync = getAssociatedTokenAddressSync(mint.publicKey, minter1.publicKey, false, TOKEN_2022_PROGRAM_ID); + + // pull before balances + const treasuryBalanceBefore = await provider.connection.getBalance(treasury.publicKey); + const platformFeeRecipientBalanceBefore = await provider.connection.getBalance(platformFeeAdmin.publicKey); + + const mintIx = await editionsControlsProgram.methods + .mintWithControls(mintConfig) + .accountsStrict({ + editionsDeployment: editionsPda, + editionsControls: editionsControlsPda, + hashlist: hashlistPda, + hashlistMarker: hashlistMarkerPda, + payer: minter1.publicKey, + signer: minter1.publicKey, + minter: minter1.publicKey, + minterStats: minterStatsPda, + minterStatsPhase: minterStatsPhasePda, + mint: mint.publicKey, + member: member.publicKey, + group: group.publicKey, + groupMint: groupMint.publicKey, + platformFeeRecipient1: platformFeeAdmin.publicKey, + tokenAccount: associatedTokenAddressSync, + treasury: treasury.publicKey, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + groupExtensionProgram: TOKEN_GROUP_EXTENSION_PROGRAM_ID, + systemProgram: SystemProgram.programId, + libreplexEditionsProgram: editionsProgram.programId, + }) + .instruction(); + + const transaction = new Transaction().add(modifiedComputeUnits).add(mintIx); + try { + await provider.sendAndConfirm(transaction, [minter1, mint, member]); + } catch (error) { + console.error('Error in mintWithControls:', error); + throw error; + } + + // Verify state after minting + const editionsDecoded = await getEditions(provider.connection, editionsPda, editionsProgram); + const editionsControlsDecoded = await getEditionsControls(provider.connection, editionsControlsPda, editionsControlsProgram); + const minterStatsDecoded = await getMinterStats(provider.connection, minterStatsPda, editionsControlsProgram); + const minterStatsPhaseDecoded = await getMinterStats(provider.connection, minterStatsPhasePda, editionsControlsProgram); + + expect(editionsControlsDecoded.data.phases[1].currentMints.toString()).to.equal('2'); + expect(minterStatsDecoded.data.mintCount.toString()).to.equal('2'); + expect(minterStatsPhaseDecoded.data.mintCount.toString()).to.equal('2'); + + // Verify protocol fees & treasury income + const expectedPlatformFee = collectionConfig.platformFee.platformFeeValue; + const expectedTreasuryIncome = phase1Config.priceAmount; + + const treasuryBalanceAfter = await provider.connection.getBalance(treasury.publicKey); + const platformFeeRecipientBalanceAfter = await provider.connection.getBalance(platformFeeAdmin.publicKey); + + expect(treasuryBalanceAfter - treasuryBalanceBefore).to.equal(expectedTreasuryIncome.toNumber()); + expect(platformFeeRecipientBalanceAfter - platformFeeRecipientBalanceBefore).to.equal(expectedPlatformFee.toNumber()); + }); + + // Max wallet mints on Phase 1 is 5 + it('Should fail to mint when exceeding phase max per wallet', async () => { + const mintConfig = { + phaseIndex: 1, + merkleProof: null, + allowListPrice: null, + allowListMaxClaims: null, + }; + + const mintWithControls = async () => { + const mint = Keypair.generate(); + const member = Keypair.generate(); + + const hashlistMarkerPda = getHashlistMarkerPda(editionsPda, mint.publicKey, editionsProgram.programId); + const minterStatsPda = getMinterStatsPda(editionsPda, minter1.publicKey, editionsControlsProgram.programId); + const minterStatsPhasePda = getMinterStatsPhasePda(editionsPda, minter1.publicKey, 1, editionsControlsProgram.programId); + const associatedTokenAddressSync = getAssociatedTokenAddressSync(mint.publicKey, minter1.publicKey, false, TOKEN_2022_PROGRAM_ID); + + const mintIx = await editionsControlsProgram.methods + .mintWithControls(mintConfig) + .accountsStrict({ + editionsDeployment: editionsPda, + editionsControls: editionsControlsPda, + hashlist: hashlistPda, + hashlistMarker: hashlistMarkerPda, + payer: minter1.publicKey, + signer: minter1.publicKey, + minter: minter1.publicKey, + minterStats: minterStatsPda, + minterStatsPhase: minterStatsPhasePda, + mint: mint.publicKey, + member: member.publicKey, + group: group.publicKey, + groupMint: groupMint.publicKey, + platformFeeRecipient1: platformFeeAdmin.publicKey, + tokenAccount: associatedTokenAddressSync, + treasury: treasury.publicKey, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + groupExtensionProgram: TOKEN_GROUP_EXTENSION_PROGRAM_ID, + systemProgram: SystemProgram.programId, + libreplexEditionsProgram: editionsProgram.programId, + }) + .instruction(); + + const transaction = new Transaction().add(modifiedComputeUnits).add(mintIx); + try { + await provider.sendAndConfirm(transaction, [minter1, mint, member]); + } catch (error) { + throw error; + } + }; + + for (let i = 0; i < 3; i++) { + try { + await mintWithControls(); + } catch (error) { + throw error; + } + } + + // 5'th mint should fail + try { + await mintWithControls(); + throw new Error('Mint should fail'); + } catch (error) { + const errorString = JSON.stringify(error); + expect(errorString).to.include('Exceeded wallet max mints for this phase.'); + } + }); + + // Max total mints on Phase 1 is 10 + it('Should fail to mint when exceeding phase max mints', async () => { + // phase mint count is 5, perform 5 mints more with random wallets that should succeed, then the 11th mint should fail + const mintWithControls = async (minter: Keypair, mint: Keypair, member: Keypair) => { + const mintConfig = { + phaseIndex: 1, + merkleProof: null, + allowListPrice: null, + allowListMaxClaims: null, + }; + + const hashlistMarkerPda = getHashlistMarkerPda(editionsPda, mint.publicKey, editionsProgram.programId); + const minterStatsPda = getMinterStatsPda(editionsPda, minter.publicKey, editionsControlsProgram.programId); + const minterStatsPhasePda = getMinterStatsPhasePda(editionsPda, minter.publicKey, 1, editionsControlsProgram.programId); + const associatedTokenAddressSync = getAssociatedTokenAddressSync(mint.publicKey, minter.publicKey, false, TOKEN_2022_PROGRAM_ID); + + const mintIx = await editionsControlsProgram.methods + .mintWithControls(mintConfig) + .accountsStrict({ + editionsDeployment: editionsPda, + editionsControls: editionsControlsPda, + hashlist: hashlistPda, + hashlistMarker: hashlistMarkerPda, + payer: minter.publicKey, + signer: minter.publicKey, + minter: minter.publicKey, + minterStats: minterStatsPda, + minterStatsPhase: minterStatsPhasePda, + mint: mint.publicKey, + member: member.publicKey, + group: group.publicKey, + groupMint: groupMint.publicKey, + platformFeeRecipient1: platformFeeAdmin.publicKey, + tokenAccount: associatedTokenAddressSync, + treasury: treasury.publicKey, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + groupExtensionProgram: TOKEN_GROUP_EXTENSION_PROGRAM_ID, + systemProgram: SystemProgram.programId, + libreplexEditionsProgram: editionsProgram.programId, + }) + .instruction(); + + const transaction = new Transaction().add(modifiedComputeUnits).add(mintIx); + try { + await provider.sendAndConfirm(transaction, [minter, mint, member]); + } catch (error) { + throw error; + } + }; + if (VERBOSE_LOGGING) { + console.log('Performing 5 mints with random wallets, takes a while..'); + } + const mintPromises = []; + for (let i = 0; i < 5; i++) { + const minter = Keypair.generate(); + const mint = Keypair.generate(); + const member = Keypair.generate(); + + // airdrop minter + const airdropTx = await provider.connection.requestAirdrop(minter.publicKey, 1 * LAMPORTS_PER_SOL); + await provider.connection.confirmTransaction(airdropTx, 'confirmed'); + + mintPromises.push(mintWithControls(minter, mint, member)); + } + + try { + await Promise.all(mintPromises); + } catch (error) { + console.error('Error in parallel minting:', error); + throw error; + } + + // 11st mint should fail + try { + const minter = Keypair.generate(); + const mint = Keypair.generate(); + const member = Keypair.generate(); + + // airdrop minter + const airdropTx = await provider.connection.requestAirdrop(minter.publicKey, 1 * LAMPORTS_PER_SOL); + await provider.connection.confirmTransaction(airdropTx, 'confirmed'); + + await mintWithControls(minter, mint, member); + throw new Error('Mint should fail'); + } catch (error) { + const errorString = JSON.stringify(error); + expect(errorString).to.include('Exceeded max mints for this phase.'); + } + }); + + describe('Minting on a public phase without allowlist [Phase Index 2]', () => { + it('Should be able to mint without any allowlist data (open mint)', async () => { + const mintConfig = { + phaseIndex: 2, + merkleProof: null, + allowListPrice: null, + allowListMaxClaims: null, + }; + + const mint = Keypair.generate(); + const member = Keypair.generate(); + + const hashlistMarkerPda = getHashlistMarkerPda(editionsPda, mint.publicKey, editionsProgram.programId); + const minterStatsPda = getMinterStatsPda(editionsPda, minter2.publicKey, editionsControlsProgram.programId); + const minterStatsPhasePda = getMinterStatsPhasePda(editionsPda, minter2.publicKey, 2, editionsControlsProgram.programId); + const associatedTokenAddressSync = getAssociatedTokenAddressSync(mint.publicKey, minter2.publicKey, false, TOKEN_2022_PROGRAM_ID); + + // pull before balances + const treasuryBalanceBefore = await provider.connection.getBalance(treasury.publicKey); + const platformFeeRecipientBalanceBefore = await provider.connection.getBalance(platformFeeAdmin.publicKey); + + const mintIx = await editionsControlsProgram.methods + .mintWithControls(mintConfig) + .accountsStrict({ + editionsDeployment: editionsPda, + editionsControls: editionsControlsPda, + hashlist: hashlistPda, + hashlistMarker: hashlistMarkerPda, + payer: minter2.publicKey, + signer: minter2.publicKey, + minter: minter2.publicKey, + minterStats: minterStatsPda, + minterStatsPhase: minterStatsPhasePda, + mint: mint.publicKey, + member: member.publicKey, + group: group.publicKey, + groupMint: groupMint.publicKey, + platformFeeRecipient1: platformFeeAdmin.publicKey, + tokenAccount: associatedTokenAddressSync, + treasury: treasury.publicKey, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + groupExtensionProgram: TOKEN_GROUP_EXTENSION_PROGRAM_ID, + systemProgram: SystemProgram.programId, + libreplexEditionsProgram: editionsProgram.programId, + }) + .instruction(); + + const transaction = new Transaction().add(modifiedComputeUnits).add(mintIx); + try { + await provider.sendAndConfirm(transaction, [minter2, mint, member]); + } catch (error) { + console.error('Error in mintWithControls:', error); + throw error; + } + + // Verify state after minting + const editionsDecoded = await getEditions(provider.connection, editionsPda, editionsProgram); + const editionsControlsDecoded = await getEditionsControls(provider.connection, editionsControlsPda, editionsControlsProgram); + const minterStatsDecoded = await getMinterStats(provider.connection, minterStatsPda, editionsControlsProgram); + const minterStatsPhaseDecoded = await getMinterStats(provider.connection, minterStatsPhasePda, editionsControlsProgram); + + expect(editionsControlsDecoded.data.phases[2].currentMints.toString()).to.equal('1'); + expect(minterStatsDecoded.data.mintCount.toString()).to.equal('1'); + expect(minterStatsPhaseDecoded.data.mintCount.toString()).to.equal('1'); + + // Verify protocol fees & treasury income + const expectedPlatformFee = collectionConfig.platformFee.platformFeeValue; + const expectedTreasuryIncome = phase3Config.priceAmount; + + const treasuryBalanceAfter = await provider.connection.getBalance(treasury.publicKey); + const platformFeeRecipientBalanceAfter = await provider.connection.getBalance(platformFeeAdmin.publicKey); + + expect(treasuryBalanceAfter - treasuryBalanceBefore).to.equal(expectedTreasuryIncome.toNumber()); + expect(platformFeeRecipientBalanceAfter - platformFeeRecipientBalanceBefore).to.equal(expectedPlatformFee.toNumber()); + }); + + it('Should not be able to mint with allowlist (phase does not have allowlist)', async () => { + const mintConfig = { + phaseIndex: 2, + merkleProof: allowListConfig.list[0].proof, + allowListPrice: allowListConfig.list[0].price, + allowListMaxClaims: allowListConfig.list[0].max_claims, + }; + + const mint = Keypair.generate(); + const member = Keypair.generate(); + + const hashlistMarkerPda = getHashlistMarkerPda(editionsPda, mint.publicKey, editionsProgram.programId); + const minterStatsPda = getMinterStatsPda(editionsPda, minter2.publicKey, editionsControlsProgram.programId); + const minterStatsPhasePda = getMinterStatsPhasePda(editionsPda, minter2.publicKey, 2, editionsControlsProgram.programId); + const associatedTokenAddressSync = getAssociatedTokenAddressSync(mint.publicKey, minter2.publicKey, false, TOKEN_2022_PROGRAM_ID); + + const mintIx = await editionsControlsProgram.methods + .mintWithControls(mintConfig) + .accountsStrict({ + editionsDeployment: editionsPda, + editionsControls: editionsControlsPda, + hashlist: hashlistPda, + hashlistMarker: hashlistMarkerPda, + payer: minter2.publicKey, + signer: minter2.publicKey, + minter: minter2.publicKey, + minterStats: minterStatsPda, + minterStatsPhase: minterStatsPhasePda, + mint: mint.publicKey, + member: member.publicKey, + group: group.publicKey, + groupMint: groupMint.publicKey, + platformFeeRecipient1: platformFeeAdmin.publicKey, + tokenAccount: associatedTokenAddressSync, + treasury: treasury.publicKey, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + groupExtensionProgram: TOKEN_GROUP_EXTENSION_PROGRAM_ID, + systemProgram: SystemProgram.programId, + libreplexEditionsProgram: editionsProgram.programId, + }) + .instruction(); + + const transaction = new Transaction().add(modifiedComputeUnits).add(mintIx); + try { + await provider.sendAndConfirm(transaction, [minter2, mint, member]); + } catch (error) { + const errorString = JSON.stringify(error); + expect(errorString).to.include('Merkle root not set for allow list mint'); + } + // get state + const editionsDecoded = await getEditions(provider.connection, editionsPda, editionsProgram); + const editionsControlsDecoded = await getEditionsControls(provider.connection, editionsControlsPda, editionsControlsProgram); + const minterStatsDecoded = await getMinterStats(provider.connection, minterStatsPda, editionsControlsProgram); + const minterStatsPhaseDecoded = await getMinterStats(provider.connection, minterStatsPhasePda, editionsControlsProgram); + + expect(editionsControlsDecoded.data.phases[2].currentMints.toString()).to.equal('1'); + expect(minterStatsDecoded.data.mintCount.toString()).to.equal('1'); + expect(minterStatsPhaseDecoded.data.mintCount.toString()).to.equal('1'); + + // log final state + if (VERBOSE_LOGGING) { + logEditions(editionsDecoded); + logEditionsControls(editionsControlsDecoded); + logMinterStats(minterStatsDecoded); + logMinterStatsPhase(minterStatsPhaseDecoded); + } + }); + }); + + describe('Minting on a public phase without allowlist and unlimited supply up to collection max [Phase Index 3]', () => { + // should fail to mint if it exceeds the max mints for the entire collection, max mints for the collection is 20. + it('Should fail to mint when exceeding collection max mints', async () => { + if (VERBOSE_LOGGING) { + const decodedEditions = await getEditions(provider.connection, editionsPda, editionsProgram); + logEditions(decodedEditions); + } + + // current mint count is 14, perform 6 mints with random wallets, then the 21st mint should fail + const mintWithControls = async (minter: Keypair, mint: Keypair, member: Keypair) => { + const mintConfig = { + phaseIndex: 3, + merkleProof: null, + allowListPrice: null, + allowListMaxClaims: null, + }; + + const hashlistMarkerPda = getHashlistMarkerPda(editionsPda, mint.publicKey, editionsProgram.programId); + const minterStatsPda = getMinterStatsPda(editionsPda, minter.publicKey, editionsControlsProgram.programId); + const minterStatsPhasePda = getMinterStatsPhasePda(editionsPda, minter.publicKey, 3, editionsControlsProgram.programId); + const associatedTokenAddressSync = getAssociatedTokenAddressSync(mint.publicKey, minter.publicKey, false, TOKEN_2022_PROGRAM_ID); + + const mintIx = await editionsControlsProgram.methods + .mintWithControls(mintConfig) + .accountsStrict({ + editionsDeployment: editionsPda, + editionsControls: editionsControlsPda, + hashlist: hashlistPda, + hashlistMarker: hashlistMarkerPda, + payer: minter.publicKey, + signer: minter.publicKey, + minter: minter.publicKey, + minterStats: minterStatsPda, + minterStatsPhase: minterStatsPhasePda, + mint: mint.publicKey, + member: member.publicKey, + group: group.publicKey, + groupMint: groupMint.publicKey, + platformFeeRecipient1: platformFeeAdmin.publicKey, + tokenAccount: associatedTokenAddressSync, + treasury: treasury.publicKey, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + groupExtensionProgram: TOKEN_GROUP_EXTENSION_PROGRAM_ID, + systemProgram: SystemProgram.programId, + libreplexEditionsProgram: editionsProgram.programId, + }) + .instruction(); + + const transaction = new Transaction().add(modifiedComputeUnits).add(mintIx); + try { + await provider.sendAndConfirm(transaction, [minter, mint, member]); + } catch (error) { + throw error; + } + }; + if (VERBOSE_LOGGING) { + console.log('Performing 6 mints with random wallets, takes a while..'); + } + const mintPromises = []; + for (let i = 0; i < 6; i++) { + const minter = Keypair.generate(); + const mint = Keypair.generate(); + const member = Keypair.generate(); + + // airdrop minter + const airdropTx = await provider.connection.requestAirdrop(minter.publicKey, 1 * LAMPORTS_PER_SOL); + await provider.connection.confirmTransaction(airdropTx, 'confirmed'); + + mintPromises.push(mintWithControls(minter, mint, member)); + } + + try { + await Promise.all(mintPromises); + } catch (error) { + console.error('Error in parallel minting:', error); + throw error; + } + if (VERBOSE_LOGGING) { + console.log('Performing 21th mint with random wallet..'); + } + try { + const minter = Keypair.generate(); + const mint = Keypair.generate(); + const member = Keypair.generate(); + + // airdrop minter + const airdropTx = await provider.connection.requestAirdrop(minter.publicKey, 1 * LAMPORTS_PER_SOL); + await provider.connection.confirmTransaction(airdropTx, 'confirmed'); + + await mintWithControls(minter, mint, member); + throw new Error('Mint should fail'); + } catch (error) { + const errorString = JSON.stringify(error); + expect(errorString).to.include('Minted out.'); + } + }); + }); + + describe('Attempting to mint on invalid phases', () => { + it('Should fail to mint on a phase that has not started yet', async () => { + const mintConfig = { + phaseIndex: 4, + merkleProof: null, + allowListPrice: null, + allowListMaxClaims: null, + }; + + const minter = Keypair.generate(); + const mint = Keypair.generate(); + const member = Keypair.generate(); + + // airdrop minter + const airdropTx = await provider.connection.requestAirdrop(minter.publicKey, 1 * LAMPORTS_PER_SOL); + await provider.connection.confirmTransaction(airdropTx, 'confirmed'); + + const hashlistMarkerPda = getHashlistMarkerPda(editionsPda, mint.publicKey, editionsProgram.programId); + const minterStatsPda = getMinterStatsPda(editionsPda, minter.publicKey, editionsControlsProgram.programId); + const minterStatsPhasePda = getMinterStatsPhasePda(editionsPda, minter.publicKey, 4, editionsControlsProgram.programId); + const associatedTokenAddressSync = getAssociatedTokenAddressSync(mint.publicKey, minter.publicKey, false, TOKEN_2022_PROGRAM_ID); + + const mintIx = await editionsControlsProgram.methods + .mintWithControls(mintConfig) + .accountsStrict({ + editionsDeployment: editionsPda, + editionsControls: editionsControlsPda, + hashlist: hashlistPda, + hashlistMarker: hashlistMarkerPda, + payer: minter.publicKey, + signer: minter.publicKey, + minter: minter.publicKey, + minterStats: minterStatsPda, + minterStatsPhase: minterStatsPhasePda, + mint: mint.publicKey, + member: member.publicKey, + group: group.publicKey, + groupMint: groupMint.publicKey, + platformFeeRecipient1: platformFeeAdmin.publicKey, + tokenAccount: associatedTokenAddressSync, + treasury: treasury.publicKey, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + groupExtensionProgram: TOKEN_GROUP_EXTENSION_PROGRAM_ID, + systemProgram: SystemProgram.programId, + libreplexEditionsProgram: editionsProgram.programId, + }) + .instruction(); + + const transaction = new Transaction().add(modifiedComputeUnits).add(mintIx); + try { + await provider.sendAndConfirm(transaction, [minter, mint, member]); + } catch (error) { + const errorString = JSON.stringify(error); + expect(errorString).to.include('Phase not yet started'); + } + }); + + it('Should fail to mint on a phase that has already ended', async () => { + const mintConfig = { + phaseIndex: 5, + merkleProof: null, + allowListPrice: null, + allowListMaxClaims: null, + }; + + const minter = Keypair.generate(); + const mint = Keypair.generate(); + const member = Keypair.generate(); + + // airdrop minter + const airdropTx = await provider.connection.requestAirdrop(minter.publicKey, 1 * LAMPORTS_PER_SOL); + await provider.connection.confirmTransaction(airdropTx, 'confirmed'); + + const hashlistMarkerPda = getHashlistMarkerPda(editionsPda, mint.publicKey, editionsProgram.programId); + const minterStatsPda = getMinterStatsPda(editionsPda, minter.publicKey, editionsControlsProgram.programId); + const minterStatsPhasePda = getMinterStatsPhasePda(editionsPda, minter.publicKey, 5, editionsControlsProgram.programId); + const associatedTokenAddressSync = getAssociatedTokenAddressSync(mint.publicKey, minter.publicKey, false, TOKEN_2022_PROGRAM_ID); + + const mintIx = await editionsControlsProgram.methods + .mintWithControls(mintConfig) + .accountsStrict({ + editionsDeployment: editionsPda, + editionsControls: editionsControlsPda, + hashlist: hashlistPda, + hashlistMarker: hashlistMarkerPda, + payer: minter.publicKey, + signer: minter.publicKey, + minter: minter.publicKey, + minterStats: minterStatsPda, + minterStatsPhase: minterStatsPhasePda, + mint: mint.publicKey, + member: member.publicKey, + group: group.publicKey, + groupMint: groupMint.publicKey, + platformFeeRecipient1: platformFeeAdmin.publicKey, + tokenAccount: associatedTokenAddressSync, + treasury: treasury.publicKey, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + groupExtensionProgram: TOKEN_GROUP_EXTENSION_PROGRAM_ID, + systemProgram: SystemProgram.programId, + libreplexEditionsProgram: editionsProgram.programId, + }) + .instruction(); + + const transaction = new Transaction().add(modifiedComputeUnits).add(mintIx); + try { + await provider.sendAndConfirm(transaction, [minter, mint, member]); + } catch (error) { + const errorString = JSON.stringify(error); + expect(errorString).to.include('Phase already finished'); + } + }); + }); + }); + }); +}); diff --git a/test/utils/getters.ts b/test/utils/getters.ts new file mode 100644 index 00000000..96f73f63 --- /dev/null +++ b/test/utils/getters.ts @@ -0,0 +1,228 @@ +import { BorshCoder, Program } from '@coral-xyz/anchor'; +import { Connection, PublicKey } from '@solana/web3.js'; +import { LibreplexEditionsControls } from '../../target/types/libreplex_editions_controls'; +import { LibreplexEditions } from '../../target/types/libreplex_editions'; +import { IdlAccounts } from '@coral-xyz/anchor'; +import { getTokenMetadata as getSplTokenMetadata } from '@solana/spl-token'; + +export type EditionsDeployment = + IdlAccounts['editionsDeployment']; + +export type EditionsControls = + IdlAccounts['editionsControls']; + +export type MinterStats = IdlAccounts['minterStats']; + +export const decodeEditions = + (program: Program) => + (buffer: Buffer | undefined, pubkey: PublicKey) => { + const coder = new BorshCoder(program.idl); + const data = buffer + ? coder.accounts.decode('editionsDeployment', buffer) + : null; + + return { + data, + pubkey, + }; + }; + +export const getEditions = async ( + connection: Connection, + editionsPda: PublicKey, + editionsProgram: Program +) => { + const editionsAccountInfo = await connection.getAccountInfo(editionsPda); + if (!editionsAccountInfo) { + throw new Error('Editions account not found'); + } + const editionsDecoded = decodeEditions(editionsProgram)( + editionsAccountInfo.data, + editionsPda + ); + return editionsDecoded; +}; + +export const logEditions = (editionsDecoded: { + data: EditionsDeployment; + pubkey: PublicKey; +}) => { + console.log({ + Editions: { + symbol: editionsDecoded.data.symbol, + creator: editionsDecoded.data.creator.toBase58(), + groupMint: editionsDecoded.data.groupMint.toBase58(), + maxNumberOfTokens: editionsDecoded.data.maxNumberOfTokens.toString(), + cosignerProgramId: editionsDecoded.data.cosignerProgramId + ? editionsDecoded.data.cosignerProgramId.toBase58() + : null, + tokensMinted: editionsDecoded.data.numberOfTokensIssued.toString(), + itemBaseName: editionsDecoded.data.itemBaseName, + itemBaseUri: editionsDecoded.data.itemBaseUri, + itemNameIsTemplate: editionsDecoded.data.itemNameIsTemplate, + itemUriIsTemplate: editionsDecoded.data.itemUriIsTemplate, + }, + }); +}; + +export const decodeEditionsControls = + (program: Program) => + (buffer: Buffer | undefined, pubkey: PublicKey) => { + const coder = new BorshCoder(program.idl); + const data = buffer + ? coder.accounts.decode('editionsControls', buffer) + : null; + + return { + data, + pubkey, + }; + }; + +export const getEditionsControls = async ( + connection: Connection, + editionsControlsPda: PublicKey, + editionsControlsProgram: Program +) => { + const editionsControlsAccountInfo = await connection.getAccountInfo( + editionsControlsPda + ); + if (!editionsControlsAccountInfo) { + throw new Error( + 'EditionsControls account not found. The collection was not initialized with controls.' + ); + } + const editionsControlsDecoded = decodeEditionsControls( + editionsControlsProgram + )(editionsControlsAccountInfo.data, editionsControlsPda); + return editionsControlsDecoded; +}; + +export const logEditionsControls = (editionsControlsDecoded: { + data: EditionsControls; + pubkey: PublicKey; +}) => { + console.log({ + EditionsControls: { + address: editionsControlsDecoded.pubkey.toBase58(), + coreDeployment: + editionsControlsDecoded.data.editionsDeployment.toBase58(), + creator: editionsControlsDecoded.data.creator.toBase58(), + treasury: editionsControlsDecoded.data.treasury.toBase58(), + maxMintsPerWallet: Number(editionsControlsDecoded.data.maxMintsPerWallet), + }, + phases: editionsControlsDecoded.data.phases.map((item, idx) => ({ + phaseIndex: idx, + currentMints: Number(item.currentMints), + maxMintsPerWallet: Number(item.maxMintsPerWallet), + maxMintsTotal: Number(item.maxMintsTotal), + startTime: Number(item.startTime), + endTime: Number(item.endTime), + priceAmount: Number(item.priceAmount), + priceToken: item.priceToken ? item.priceToken.toBase58() : null, + isPrivate: item.isPrivate, + merkleRoot: item.merkleRoot ? JSON.stringify(item.merkleRoot) : null, + })), + }); +}; + +export const parseMetadata = ( + rawMetadata: (readonly [string, string])[] +): Record => { + const metadata: Record = {}; + for (const [key, value] of rawMetadata) { + metadata[key] = value; + } + return metadata; +}; + +export const getTokenMetadata = async ( + connection: Connection, + mint: PublicKey +) => { + const rawMetadata = await getSplTokenMetadata(connection, mint); + const additionalMetadata = rawMetadata.additionalMetadata; + const parsedMetadata = parseMetadata(additionalMetadata); + return { + ...rawMetadata, + additionalMetadata: parsedMetadata, + }; +}; + +export const logTokenMetadata = (metadata: { + name: string; + symbol: string; + uri: string; + updateAuthority?: PublicKey; + mint: PublicKey; + additionalMetadata: Record; +}) => { + console.log({ + TokenMetadata: { + name: metadata.name, + symbol: metadata.symbol, + uri: metadata.uri, + updateAuthority: metadata.updateAuthority.toBase58(), + mint: metadata.mint.toBase58(), + additionalMetadata: metadata.additionalMetadata, + }, + }); +}; + +export const decodeMinterStats = + (program: Program) => + (buffer: Buffer | undefined, pubkey: PublicKey) => { + const coder = new BorshCoder(program.idl); + const data = buffer + ? coder.accounts.decode('minterStats', buffer) + : null; + + return { + data, + pubkey, + }; + }; + +export const getMinterStats = async ( + connection: Connection, + minterStatsPda: PublicKey, + editionsControlsProgram: Program +) => { + const minterStatsAccountInfo = await connection.getAccountInfo( + minterStatsPda + ); + if (!minterStatsAccountInfo) { + throw new Error('MinterStats account not found'); + } + const minterStatsDecoded = decodeMinterStats(editionsControlsProgram)( + minterStatsAccountInfo.data, + minterStatsPda + ); + return minterStatsDecoded; +}; + +export const logMinterStats = (minterStatsDecoded: { + data: MinterStats; + pubkey: PublicKey; +}) => { + console.log({ + MinterStats: { + address: minterStatsDecoded.pubkey.toBase58(), + wallet: minterStatsDecoded.data.wallet.toBase58(), + mintCount: minterStatsDecoded.data.mintCount.toString(), + }, + }); +}; + +export const logMinterStatsPhase = (minterStatsDecoded: { + data: MinterStats; + pubkey: PublicKey; +}) => { + console.log({ + MinterStatsPhase: { + address: minterStatsDecoded.pubkey.toBase58(), + wallet: minterStatsDecoded.data.wallet.toBase58(), + mintCount: minterStatsDecoded.data.mintCount.toString(), + }, + }); +}; diff --git a/test/utils/pdas.ts b/test/utils/pdas.ts new file mode 100644 index 00000000..0af25bdc --- /dev/null +++ b/test/utils/pdas.ts @@ -0,0 +1,75 @@ +import { PublicKey } from '@solana/web3.js'; +import { toBufferLE } from 'bigint-buffer'; + +export const getEditionsPda = ( + symbol: string, + editionsProgramId: PublicKey +) => { + return PublicKey.findProgramAddressSync( + [Buffer.from('editions_deployment'), Buffer.from(symbol)], + editionsProgramId + )[0]; +}; + +export const getEditionsControlsPda = ( + editionsDeployment: PublicKey, + editionsControlsProgramId: PublicKey +) => { + return PublicKey.findProgramAddressSync( + [Buffer.from('editions_controls'), editionsDeployment.toBuffer()], + editionsControlsProgramId + )[0]; +}; + +export const getHashlistPda = ( + deployment: PublicKey, + editionsProgramId: PublicKey +) => { + return PublicKey.findProgramAddressSync( + [Buffer.from('hashlist'), deployment.toBuffer()], + editionsProgramId + )[0]; +}; + +export const getHashlistMarkerPda = ( + editionsDeployment: PublicKey, + mint: PublicKey, + editionsProgramId: PublicKey +) => { + return PublicKey.findProgramAddressSync( + [ + Buffer.from('hashlist_marker'), + editionsDeployment.toBuffer(), + mint.toBuffer(), + ], + editionsProgramId + )[0]; +}; + +export const getMinterStatsPda = ( + deployment: PublicKey, + minter: PublicKey, + editionsControlsProgramId: PublicKey +) => { + return PublicKey.findProgramAddressSync( + [Buffer.from('minter_stats'), deployment.toBuffer(), minter.toBuffer()], + editionsControlsProgramId + )[0]; +}; + +export const getMinterStatsPhasePda = ( + deployment: PublicKey, + minter: PublicKey, + phaseIndex: number, + editionsControlsProgramId: PublicKey +) => { + return PublicKey.findProgramAddressSync( + [ + Buffer.from('minter_stats_phase'), + deployment.toBuffer(), + minter.toBuffer(), + toBufferLE(BigInt(phaseIndex), 4), + ], + editionsControlsProgramId + )[0]; +}; diff --git a/test/utils/types.ts b/test/utils/types.ts new file mode 100644 index 00000000..4b0c2d8f --- /dev/null +++ b/test/utils/types.ts @@ -0,0 +1,45 @@ +import * as anchor from '@coral-xyz/anchor'; +import { PublicKey } from '@solana/web3.js'; + +export interface CollectionConfig { + symbol: string; + maxMintsPerWallet: anchor.BN; + maxNumberOfTokens: anchor.BN; + collectionName: string; + collectionUri: string; + royalties: { + royaltyBasisPoints: anchor.BN; + creators: { address: PublicKey; share: number }[]; + }; + platformFee: { + platformFeeValue: anchor.BN; + recipients: { address: PublicKey; share: number }[]; + isFeeFlat: boolean; + }; + extraMeta: { field: string; value: string }[]; + itemBaseUri: string; + itemBaseName: string; + treasury: PublicKey; + cosignerProgramId: PublicKey | null; +} + +export interface AllowListConfig { + merkleRoot: Buffer; + list: { + address: PublicKey; + price: anchor.BN; + max_claims: anchor.BN; + proof: Buffer[]; + }[]; +} + +export interface PhaseConfig { + maxMintsPerWallet: anchor.BN; + maxMintsTotal: anchor.BN; + priceAmount: anchor.BN; + startTime: anchor.BN; + endTime: anchor.BN; + priceToken: PublicKey; + isPrivate: boolean; + merkleRoot: Buffer | null; +} diff --git a/test/utils/utils.ts b/test/utils/utils.ts new file mode 100644 index 00000000..b34f0570 --- /dev/null +++ b/test/utils/utils.ts @@ -0,0 +1,35 @@ +import { Connection, Keypair, Transaction } from '@solana/web3.js'; + +export async function getCluster(connection: Connection): Promise { + // Get the genesis hash + const genesisHash = await connection.getGenesisHash(); + + // Compare the genesis hash with known cluster genesis hashes + switch (genesisHash) { + case '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d': + return 'mainnet-beta'; + case 'EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG': + return 'testnet'; + case '4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY': + return 'devnet'; + default: + // If it doesn't match any known cluster, it's likely localhost + return 'localhost'; + } +} + + +export async function estimateTransactionFee(connection: Connection, transaction: Transaction, signers: Keypair[]): Promise { + const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(); + transaction.recentBlockhash = blockhash; + transaction.sign(...signers); + + const message = transaction.compileMessage(); + const fees = await connection.getFeeForMessage(message); + + if (fees === null) { + throw new Error('Failed to estimate fee'); + } + + return fees.value; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index a58c9f14..713f4570 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,18 +1,16 @@ { "compilerOptions": { - "types": ["mocha", "chai"], - "typeRoots": ["./node_modules/@types"], - "lib": ["es2015"], - "module": "commonjs", - "target": "es6", - "esModuleInterop": true - }, - "include": [], - "exclude": ["node_modules"], - "references": [ - { - "path": "./packages/libreplex-idls" - }, - - ], + "types": ["mocha", "chai"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true, + "baseUrl": ".", + "paths": { + "*": ["node_modules/*"] + } + }, + "include": ["test/**/*"], + "exclude": ["node_modules"] } diff --git a/yarn.lock b/yarn.lock index 2f896775..1b4c1879 100644 --- a/yarn.lock +++ b/yarn.lock @@ -510,13 +510,164 @@ dependencies: buffer "~6.0.3" -"@solana/spl-token@0.3.8", "@solana/spl-token@^0.3.8": - version "0.3.8" - resolved "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.8.tgz" - integrity sha512-ogwGDcunP9Lkj+9CODOWMiVJEdRtqHAtX2rWF62KxnnSWtMZtV9rDhTrZFshiyJmxDnRL/1nKE1yJHg4jjs3gg== +"@solana/codecs-core@2.0.0-preview.4": + version "2.0.0-preview.4" + resolved "https://registry.yarnpkg.com/@solana/codecs-core/-/codecs-core-2.0.0-preview.4.tgz#770826105f2f884110a21662573e7a2014654324" + integrity sha512-A0VVuDDA5kNKZUinOqHxJQK32aKTucaVbvn31YenGzHX1gPqq+SOnFwgaEY6pq4XEopSmaK16w938ZQS8IvCnw== + dependencies: + "@solana/errors" "2.0.0-preview.4" + +"@solana/codecs-core@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs-core/-/codecs-core-2.0.0-rc.1.tgz#1a2d76b9c7b9e7b7aeb3bd78be81c2ba21e3ce22" + integrity sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ== + dependencies: + "@solana/errors" "2.0.0-rc.1" + +"@solana/codecs-data-structures@2.0.0-preview.4": + version "2.0.0-preview.4" + resolved "https://registry.yarnpkg.com/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-preview.4.tgz#f8a2470982a9792334737ea64000ccbdff287247" + integrity sha512-nt2k2eTeyzlI/ccutPcG36M/J8NAYfxBPI9h/nQjgJ+M+IgOKi31JV8StDDlG/1XvY0zyqugV3I0r3KAbZRJpA== + dependencies: + "@solana/codecs-core" "2.0.0-preview.4" + "@solana/codecs-numbers" "2.0.0-preview.4" + "@solana/errors" "2.0.0-preview.4" + +"@solana/codecs-data-structures@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-rc.1.tgz#d47b2363d99fb3d643f5677c97d64a812982b888" + integrity sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/codecs-numbers" "2.0.0-rc.1" + "@solana/errors" "2.0.0-rc.1" + +"@solana/codecs-numbers@2.0.0-preview.4": + version "2.0.0-preview.4" + resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.0.0-preview.4.tgz#6a53b456bb7866f252d8c032c81a92651e150f66" + integrity sha512-Q061rLtMadsO7uxpguT+Z7G4UHnjQ6moVIxAQxR58nLxDPCC7MB1Pk106/Z7NDhDLHTcd18uO6DZ7ajHZEn2XQ== + dependencies: + "@solana/codecs-core" "2.0.0-preview.4" + "@solana/errors" "2.0.0-preview.4" + +"@solana/codecs-numbers@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.0.0-rc.1.tgz#f34978ddf7ea4016af3aaed5f7577c1d9869a614" + integrity sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/errors" "2.0.0-rc.1" + +"@solana/codecs-strings@2.0.0-preview.4": + version "2.0.0-preview.4" + resolved "https://registry.yarnpkg.com/@solana/codecs-strings/-/codecs-strings-2.0.0-preview.4.tgz#4d06bb722a55a5d04598d362021bfab4bd446760" + integrity sha512-YDbsQePRWm+xnrfS64losSGRg8Wb76cjK1K6qfR8LPmdwIC3787x9uW5/E4icl/k+9nwgbIRXZ65lpF+ucZUnw== + dependencies: + "@solana/codecs-core" "2.0.0-preview.4" + "@solana/codecs-numbers" "2.0.0-preview.4" + "@solana/errors" "2.0.0-preview.4" + +"@solana/codecs-strings@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs-strings/-/codecs-strings-2.0.0-rc.1.tgz#e1d9167075b8c5b0b60849f8add69c0f24307018" + integrity sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/codecs-numbers" "2.0.0-rc.1" + "@solana/errors" "2.0.0-rc.1" + +"@solana/codecs@2.0.0-preview.4": + version "2.0.0-preview.4" + resolved "https://registry.yarnpkg.com/@solana/codecs/-/codecs-2.0.0-preview.4.tgz#a1923cc78a6f64ebe656c7ec6335eb6b70405b22" + integrity sha512-gLMupqI4i+G4uPi2SGF/Tc1aXcviZF2ybC81x7Q/fARamNSgNOCUUoSCg9nWu1Gid6+UhA7LH80sWI8XjKaRog== + dependencies: + "@solana/codecs-core" "2.0.0-preview.4" + "@solana/codecs-data-structures" "2.0.0-preview.4" + "@solana/codecs-numbers" "2.0.0-preview.4" + "@solana/codecs-strings" "2.0.0-preview.4" + "@solana/options" "2.0.0-preview.4" + +"@solana/codecs@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs/-/codecs-2.0.0-rc.1.tgz#146dc5db58bd3c28e04b4c805e6096c2d2a0a875" + integrity sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/codecs-data-structures" "2.0.0-rc.1" + "@solana/codecs-numbers" "2.0.0-rc.1" + "@solana/codecs-strings" "2.0.0-rc.1" + "@solana/options" "2.0.0-rc.1" + +"@solana/errors@2.0.0-preview.4": + version "2.0.0-preview.4" + resolved "https://registry.yarnpkg.com/@solana/errors/-/errors-2.0.0-preview.4.tgz#056ba76b6dd900dafa70117311bec3aef0f5250b" + integrity sha512-kadtlbRv2LCWr8A9V22On15Us7Nn8BvqNaOB4hXsTB3O0fU40D1ru2l+cReqLcRPij4znqlRzW9Xi0m6J5DIhA== + dependencies: + chalk "^5.3.0" + commander "^12.1.0" + +"@solana/errors@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/errors/-/errors-2.0.0-rc.1.tgz#3882120886eab98a37a595b85f81558861b29d62" + integrity sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ== + dependencies: + chalk "^5.3.0" + commander "^12.1.0" + +"@solana/options@2.0.0-preview.4": + version "2.0.0-preview.4" + resolved "https://registry.yarnpkg.com/@solana/options/-/options-2.0.0-preview.4.tgz#212d35d1da87c7efb13de4d3569ad9eb070f013d" + integrity sha512-tv2O/Frxql/wSe3jbzi5nVicIWIus/BftH+5ZR+r9r3FO0/htEllZS5Q9XdbmSboHu+St87584JXeDx3xm4jaA== + dependencies: + "@solana/codecs-core" "2.0.0-preview.4" + "@solana/codecs-data-structures" "2.0.0-preview.4" + "@solana/codecs-numbers" "2.0.0-preview.4" + "@solana/codecs-strings" "2.0.0-preview.4" + "@solana/errors" "2.0.0-preview.4" + +"@solana/options@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/options/-/options-2.0.0-rc.1.tgz#06924ba316dc85791fc46726a51403144a85fc4d" + integrity sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/codecs-data-structures" "2.0.0-rc.1" + "@solana/codecs-numbers" "2.0.0-rc.1" + "@solana/codecs-strings" "2.0.0-rc.1" + "@solana/errors" "2.0.0-rc.1" + +"@solana/spl-token-group@^0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@solana/spl-token-group/-/spl-token-group-0.0.5.tgz#f955dcca782031c85e862b2b46878d1bb02db6c2" + integrity sha512-CLJnWEcdoUBpQJfx9WEbX3h6nTdNiUzswfFdkABUik7HVwSNA98u5AYvBVK2H93d9PGMOHAak2lHW9xr+zAJGQ== + dependencies: + "@solana/codecs" "2.0.0-preview.4" + "@solana/spl-type-length-value" "0.1.0" + +"@solana/spl-token-metadata@^0.1.3", "@solana/spl-token-metadata@^0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@solana/spl-token-metadata/-/spl-token-metadata-0.1.5.tgz#91616470d6862ec6b762e6cfcf882b8a8a24b1e8" + integrity sha512-DSBlo7vjuLe/xvNn75OKKndDBkFxlqjLdWlq6rf40StnrhRn7TDntHGLZpry1cf3uzQFShqeLROGNPAJwvkPnA== + dependencies: + "@solana/codecs" "2.0.0-rc.1" + "@solana/spl-type-length-value" "0.1.0" + +"@solana/spl-token@^0.4.8": + version "0.4.8" + resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.4.8.tgz#a84e4131af957fa9fbd2727e5fc45dfbf9083586" + integrity sha512-RO0JD9vPRi4LsAbMUdNbDJ5/cv2z11MGhtAvFeRzT4+hAGE/FUzRi0tkkWtuCfSIU3twC6CtmAihRp/+XXjWsA== dependencies: "@solana/buffer-layout" "^4.0.0" "@solana/buffer-layout-utils" "^0.2.0" + "@solana/spl-token-group" "^0.0.5" + "@solana/spl-token-metadata" "^0.1.3" + buffer "^6.0.3" + +"@solana/spl-type-length-value@0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@solana/spl-type-length-value/-/spl-type-length-value-0.1.0.tgz#b5930cf6c6d8f50c7ff2a70463728a4637a2f26b" + integrity sha512-JBMGB0oR4lPttOZ5XiUGyvylwLQjt1CPJa6qQ5oM+MBCndfjz2TKKkw0eATlLLcYmq1jBVsNlJ2cD6ns2GR7lA== + dependencies: buffer "^6.0.3" "@solana/web3.js@^1.32.0", "@solana/web3.js@^1.68.0", "@solana/web3.js@^1.73.0": @@ -1059,6 +1210,11 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + chardet@^0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz" @@ -1202,6 +1358,11 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +commander@^12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + commander@^2.20.3: version "2.20.3" resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" @@ -1934,18 +2095,6 @@ glob@^10.2.2: minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-scurry "^1.10.1" -glob@^7.0.0: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@^7.1.3, glob@^7.1.4: version "7.1.7" resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz" @@ -2221,11 +2370,6 @@ inquirer@^8.2.4: through "^2.3.6" wrap-ansi "^6.0.1" -interpret@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" - integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== - ip@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz" @@ -2257,13 +2401,6 @@ is-core-module@^2.11.0, is-core-module@^2.5.0, is-core-module@^2.8.1: dependencies: has "^1.0.3" -is-core-module@^2.13.0: - version "2.13.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" - integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== - dependencies: - has "^1.0.3" - is-docker@^2.0.0, is-docker@^2.1.1: version "2.2.1" resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" @@ -2897,7 +3034,7 @@ minimatch@4.2.1: dependencies: brace-expansion "^1.1.7" -minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.4, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -2934,7 +3071,7 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: +minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: version "1.2.8" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -3824,13 +3961,6 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -rechoir@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" - integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== - dependencies: - resolve "^1.1.6" - redent@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz" @@ -3866,15 +3996,6 @@ resolve-from@^4.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve@^1.1.6: - version "1.22.4" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.4.tgz#1dc40df46554cdaf8948a486a10f6ba1e2026c34" - integrity sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg== - dependencies: - is-core-module "^2.13.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - resolve@^1.10.0: version "1.22.2" resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz" @@ -4011,23 +4132,6 @@ shebang-regex@^3.0.0: resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shelljs@^0.8.5: - version "0.8.5" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" - integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - -shx@^0.3.4: - version "0.3.4" - resolved "https://registry.yarnpkg.com/shx/-/shx-0.3.4.tgz#74289230b4b663979167f94e1935901406e40f02" - integrity sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g== - dependencies: - minimist "^1.2.3" - shelljs "^0.8.5" - signal-exit@3.0.7, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" @@ -4163,7 +4267,16 @@ ssri@^9.0.1: dependencies: minipass "^3.1.1" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -4195,7 +4308,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -4598,7 +4718,7 @@ workerpool@6.2.0: resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz" integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -4616,6 +4736,15 @@ wrap-ansi@^6.0.1: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"