diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7c401b4ba..c905be16a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,68 +1,66 @@ # Contribution Guidelines -Thank you for considering contributing to the Solana Program Examples repository. We greatly appreciate your interest and efforts in helping us improve and expand this valuable resource for the Solana developer community. - -We believe that a welcoming and inclusive environment fosters collaboration and encourages participation from developers of all backgrounds and skill levels. - -To ensure a smooth and effective contribution process, please take a moment to review and follow the guidelines outlined below. +Thank you for considering a contribution to this repository. We welcome new examples, fixes, and improvements from the community. ## How to Contribute -We welcome contributions in the form of code, documentation, bug reports, feature requests, and other forms of feedback. Here are some ways you can contribute: - -- **Code Contributions:** You can contribute code examples in Rust that demonstrate various Solana program functionalities. You can also contribute improvements to existing examples, such as bug fixes, optimizations, or additional features. +- **Code:** Add new examples or improve existing ones (bug fixes, optimizations, additional features). +- **Bug reports, ideas, feedback:** Open an issue describing what you found or what you'd like to see. -- **Bug Reports, Ideas or Feedback:** If you encounter any issues or have ideas for new examples, please submit a bug report or feature request. Your feedback is valuable and helps us improve the quality and relevance of the examples. +## Project structure -## General coding and writing guidelines +- Each example lives at `category/example-name//`, e.g. `basics/counter/anchor/`. +- Supported frameworks: `anchor`, `quasar`, `pinocchio`, `native`. Use the existing layout as a reference. +- Tests live alongside the program in a `tests/` directory. -Please follow the [Contributing and Style Guide from the Developer Content Repo](https://github.com/solana-foundation/developer-content/blob/main/CONTRIBUTING.md). +## Tooling -Specifically for code in this repo: +- **Package manager:** `pnpm`. Commit `pnpm-lock.yaml`. Do not use yarn or npm here. +- **Formatter / linter:** [Biome](https://biomejs.dev/). Run `pnpm fix` from the repo root before submitting a PR. -1. Use pnpm as the default package manager for the project. You can [install pnpm by following the instructions](https://pnpm.io/installation). Commit `pnpm-lock.yaml` to the repository. +## Testing -2. Solana Programs written for the Anchor framework should be in directory [`anchor`](https://www.anchor-lang.com), Solana Native in [`native`](https://solana.com/developers/guides/getstarted/intro-to-native-rust), respectively. - - Project path structure: `/program-examples/category/example-name/` - - Project path structure example for anchor: `/program-examples/category/example-name/anchor` +This repo uses an in-process test runtime β€” no local validator boot, no `solana-test-validator`, no `anchor test --validator legacy`. -3. Tests for Anchor and Solana native programs should be written with [solana-bankrun](https://kevinheavey.github.io/solana-bankrun). +For Anchor and Quasar examples, tests are written in TypeScript and run with `node:test` via `tsx`: -4. For Solana native programs ensure adding these mandatory pnpm run scripts to your `package.json` file for successful CI/CD builds: - -```json -"scripts": { - "test": "pnpm ts-mocha -p ./tests/tsconfig.test.json -t 1000000 ./tests/realloc.test.ts", - "build-and-test": "cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./tests/fixtures && pnpm test", - "build": "cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./program/target/so", - "deploy": "solana program deploy ./program/target/so/program.so" -}, +```bash +npx tsx --test --test-reporter=spec tests/*.ts ``` -5. Test command for Anchor should execute `pnpm test` instead of `yarn run test` for anchor programs. Replace `yarn` with `pnpm` in `[script]` table inside [Anchor.toml file.](https://www.anchor-lang.com/docs/manifest#scripts-required-for-testing) +The conventional `Anchor.toml` `[scripts]` entry is: -``` +```toml [scripts] -test = "pnpm ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" +test = "npx create-codama-clients; npx tsx --test --test-reporter=spec tests/*.ts" ``` -6. TypeScript, JavaScript and JSON files are formatted and linted using - [Biome](https://biomejs.dev/). Execute the following command to format and lint your code at the root of this project before submitting a pull request: +The TypeScript tests use: -```bash -pnpm fix -``` +- [`solana-kite`](https://solanakite.org) for the connection, wallet creation, token mint helpers, PDA derivation, and `sendTransactionFromInstructions`. +- [`@solana/kit`](https://solanakit.com) for the core types (`KeyPairSigner`, `Address`, `lamports`). +- A [Codama](https://github.com/codama-idl/codama)-generated client (via `npx create-codama-clients`) for invoking the program instructions. Do **not** use `anchor.workspace` or `program.methods.X().rpc()`. + +Native and Pinocchio examples may use `litesvm` directly from Rust where appropriate. + +## Style + +Write American English in prose (e.g. "behavior", "initialize", "favor"). Code identifiers stay as-is. + +Other conventions: -7. Some projects can be ignored from the building and testing process by adding the project name to the `.ghaignore` file. -When removing or updating an example, please ensure that the example is removed from the `.ghaignore` file -and there's a change in that example's directory. +- One H1 per markdown file. +- Fenced code blocks include a language tag (` ```rust `, ` ```typescript `, ` ```bash `, ` ```toml `). +- Use full words rather than abbreviations (`transaction`, not `tx` or `txn`; `account`, not `acc`). +- Prefer `async`/`await` over `.then()`/`.catch()`. +- Use `Array` rather than `T[]` in TypeScript. +- Avoid magic numbers β€” name or explain them. +- Write "onchain" / "offchain" as single words (no hyphen). -## Code of Conduct +## Excluding an example from CI -We are committed to providing a friendly, safe, and welcoming environment for all contributors, regardless of their background, experience level, or personal characteristics. As a contributor, you are expected to: +Add the project path to `.ghaignore` to skip it during CI builds. If you remove or replace an example, update `.ghaignore` accordingly. -Be respectful and inclusive in your interactions with others. -Refrain from engaging in any form of harassment, discrimination, or offensive behavior. Be open to constructive feedback and be willing to learn from others. -Help create a positive and supportive community where everyone feels valued and respected. +## Code of conduct -If you encounter any behavior that violates our code of conduct, please report it to the project maintainers immediately. +Be respectful and inclusive. Constructive feedback only. Report any conduct issues to the maintainers. diff --git a/README.md b/README.md index 657d57a98..cba68d679 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ Each example is available in one or more of the following frameworks: - [βš“ Anchor](https://www.anchor-lang.com/) β€” the most popular framework for Solana development. Build with `anchor build`, test with `pnpm test` as defined in `Anchor.toml`. - [πŸ’« Quasar](https://quasar-lang.com/docs) β€” a newer, more performant framework with Anchor-compatible ergonomics. Run `pnpm test` to execute tests. -- [πŸ€₯ Pinocchio](https://github.com/febo/pinocchio) β€” a zero-copy, zero-allocation library for Solana programs. Run `pnpm test` to execute tests. -- [πŸ¦€ Native Rust](https://docs.solana.com/) β€” vanilla Rust using Solana's native crates. Run `pnpm test` to execute tests. +- [πŸ€₯ Pinocchio](https://github.com/anza-xyz/pinocchio) β€” a zero-copy, zero-allocation library for Solana programs. Run `pnpm test` to execute tests. +- [πŸ¦€ Native Rust](https://docs.anza.xyz/) β€” vanilla Rust using Solana's native crates. Run `pnpm test` to execute tests. - [🧬 ASM](https://github.com/blueshift-gg/sbpf) β€” hand-written sBPF assembly built with the `sbpf` toolchain. Run `pnpm build-and-test` to build and test. > [!NOTE] @@ -25,7 +25,7 @@ Constant product AMM (xΒ·y=k) β€” create liquidity pools, deposit and withdraw l ### Escrow -Peer-to-peer OTC trade β€” one user deposits token A and specifies how much token B they want. A counterparty fulfils the offer and both sides receive their tokens atomically. +Peer-to-peer OTC trade β€” one user deposits token A and specifies how much token B they want. A counterparty fulfills the offer and both sides receive their tokens atomically. [βš“ Anchor](./tokens/escrow/anchor) [πŸ’« Quasar](./tokens/escrow/quasar) [πŸ¦€ Native](./tokens/escrow/native) diff --git a/basics/checking-accounts/README.md b/basics/checking-accounts/README.md index 504035b87..e163e7c25 100644 --- a/basics/checking-accounts/README.md +++ b/basics/checking-accounts/README.md @@ -1,13 +1,9 @@ # Checking Accounts -Solana Programs should perform checks on instructions to ensure security and that required invariants -are not being violated. +Solana programs should check the instructions they receive to ensure security and to make sure required invariants hold. -These checks vary and depend on the exact task of the Solana Program. +The exact checks depend on what the program does. Common ones include: -In this example we see some of the common checks a Solana Program can perform: - -- checking the program ID from the instruction is the program ID of your program -- checking that the order and number of accounts are correct -- checking the initialization state of an account -- etc. \ No newline at end of file +- Verifying that the `program_id` on the instruction matches your own program. +- Verifying the order and number of accounts. +- Checking the initialization state of an account. diff --git a/basics/checking-accounts/asm/README.md b/basics/checking-accounts/asm/README.md index 0f08fea01..30fdcdf8d 100644 --- a/basics/checking-accounts/asm/README.md +++ b/basics/checking-accounts/asm/README.md @@ -1,3 +1,3 @@ # checking-account-asm-program -Created with [sbpf](https://github.com/blueshift-gg/sbpf) \ No newline at end of file +A Solana SBPF assembly implementation, scaffolded with [sbpf](https://github.com/blueshift-gg/sbpf). diff --git a/basics/close-account/anchor/README.md b/basics/close-account/anchor/README.md index 8ed1ad84a..4da8bdb2c 100644 --- a/basics/close-account/anchor/README.md +++ b/basics/close-account/anchor/README.md @@ -1,33 +1,30 @@ # Destroy an Account -1. We're creating a `PDA` using [create_user.rs](programs/destroy-an-account/src/instructions/create_user.rs) - instruction. - ```rust - #[account( - init, - seeds=[User::PREFIX.as_bytes(), user.key().as_ref()], - payer=user, - space=User::SIZE, - bump - )] - pub user_account: Box>, - ``` +1. A `PDA` is created using the [create_user.rs](programs/destroy-an-account/src/instructions/create_user.rs) instruction. -2. We're closing it using [destroy_user.rs](programs/destroy-an-account/src/instructions/destroy_user.rs) - instruction, which uses `Anchor` `AccoutClose` `trait`. + ```rust + #[account( + init, + seeds = [User::PREFIX.as_bytes(), user.key().as_ref()], + payer = user, + space = User::SIZE, + bump, + )] + pub user_account: Box>, + ``` - ```rust - user_account.close(user.to_account_info())?; - ``` +2. The account is closed in [destroy_user.rs](programs/destroy-an-account/src/instructions/destroy_user.rs), using Anchor's `close` helper on the account info: -3. In our test [destroy-an-account.ts](tests/destroy-an-account.ts) we're using `fetchNullable` since we expect - the account to be `null` prior to creation and after closing. + ```rust + user_account.close(user.to_account_info())?; + ``` - ```typescript - const userAccountBefore = await program.account.user.fetchNullable(userAccountAddress, "processed"); - assert.equal(userAccountBefore, null); - ... - ... +3. The test [destroy-an-account.ts](tests/destroy-an-account.ts) verifies that the account is null both before creation and after closing, via `fetchNullable`: + + ```typescript + const userAccountBefore = await program.account.user.fetchNullable(userAccountAddress, "processed"); + assert.equal(userAccountBefore, null); + // ... const userAccountAfter = await program.account.user.fetchNullable(userAccountAddress, "processed"); assert.notEqual(userAccountAfter, null); - ``` \ No newline at end of file + ``` diff --git a/basics/counter/README.md b/basics/counter/README.md index 318436b23..4a90da540 100644 --- a/basics/counter/README.md +++ b/basics/counter/README.md @@ -1,14 +1,3 @@ # Counter -This example program allows anyone to create a counter and increment it. - -Any counter can be incremented by any key. - -## Note: Seahorse - -Seahorse currently does not allow the program to initialize anchor -accounts unless they are PDAs. - -Seahorse example only allows users to increment the counter that corresponds to their public key. - - +Anyone can create a counter and increment it. Any counter can be incremented by any key. diff --git a/basics/counter/anchor/README.md b/basics/counter/anchor/README.md index 097f6f3e7..07bf09ca3 100644 --- a/basics/counter/anchor/README.md +++ b/basics/counter/anchor/README.md @@ -1,5 +1,5 @@ # Anchor Counter -Anchor enforces init constraints that enforces good programming paradigms. +Anchor enforces `init` constraints that nudge you towards good programming patterns. -This means this program has an additional initialization instruction for `Counter`s that the Solana native program does not. \ No newline at end of file +This program has an additional initialization handler for `Counter`s that the Solana native equivalent does not. diff --git a/basics/counter/mpl-stack/README.md b/basics/counter/mpl-stack/README.md index 9a4923707..bd1543eac 100644 --- a/basics/counter/mpl-stack/README.md +++ b/basics/counter/mpl-stack/README.md @@ -1,13 +1,13 @@ # Counter: MPL Stack -This example program is written using Solana native using MPL stack. - +A Solana-native counter built using the MPL (Metaplex) stack. ## Setup -1. Build the program with `cargo build-sbf` -2. Compile the idl with `shank build` -3. Build the typescript SDK with `yarn solita` - - Temporarily, we have to modify line 58 in ts/generated/accounts/Counter.ts - to `const accountInfo = await connection.getAccountInfo(address, { commitment: "confirmed" });` in order to allow the tests to pass. In the future versions of Solita, this will be fixed. -4. Run tests with `yarn test` +1. Build the program: `cargo build-sbf` +2. Build the IDL: `shank build` +3. Build the TypeScript SDK: `pnpm solita` + - Temporary workaround: edit `ts/generated/accounts/Counter.ts` line 58 to + `const accountInfo = await connection.getAccountInfo(address, { commitment: "confirmed" });` + so that the tests pass. Future Solita versions will fix this. +4. Run tests: `pnpm test` diff --git a/basics/counter/native/README.md b/basics/counter/native/README.md index 6369c41d7..05336d246 100644 --- a/basics/counter/native/README.md +++ b/basics/counter/native/README.md @@ -1,14 +1,14 @@ # Counter: Solana Native -This example program is written in Solana using only the Solana toolsuite. +Counter written in Solana native, using only the Solana toolchain. ## Setup -1. Build the program with `cargo build-sbf` -2. Run tests + local validator with `yarn test` +1. Build the program: `cargo build-sbf` +2. Run the tests: `pnpm test` ## Debugging -1. Start test validator with `yarn start-validator` -2. Start listening to program logs with `solana config set -ul && solana logs` -3. Run tests with `yarn run-tests` +1. Start a test validator: `pnpm start-validator` +2. Listen to program logs: `solana config set -ul && solana logs` +3. Run the tests: `pnpm run-tests` diff --git a/basics/counter/pinocchio/README.md b/basics/counter/pinocchio/README.md index c50d4896a..be5a616cd 100644 --- a/basics/counter/pinocchio/README.md +++ b/basics/counter/pinocchio/README.md @@ -1,14 +1,14 @@ # Counter: Solana Pinocchio -This example program is written in Solana using only the Solana toolsuite. +Counter written using the Pinocchio framework, with only the Solana toolchain. ## Setup -1. Build the program with `cargo build-sbf` -2. Run tests + local validator with `yarn test` +1. Build the program: `cargo build-sbf` +2. Run the tests: `pnpm test` ## Debugging -1. Start test validator with `yarn start-validator` -2. Start listening to program logs with `solana config set -ul && solana logs` -3. Run tests with `yarn run-tests` +1. Start a test validator: `pnpm start-validator` +2. Listen to program logs: `solana config set -ul && solana logs` +3. Run the tests: `pnpm run-tests` diff --git a/basics/create-account/README.md b/basics/create-account/README.md index 7ab5f5811..9255bf2ce 100644 --- a/basics/create-account/README.md +++ b/basics/create-account/README.md @@ -1,17 +1,17 @@ # Create Account -:wrench: We're going to create a Solana account. :wrench: - -This account is going to be a **system account** - meaning it will be owned by the System Program. In short, this means only the System Program will be allowed to modify it's data. +Create a Solana account. -In the test, we use two methods for creating the accounts. One of the methods uses Cross program invocation and the other calls the System Program directly. +The account is a **system account** β€” owned by the System Program, which means only the System Program can modify its data. In this example, the account simply holds some SOL. -Cross program invocation means that we send the transaction to create the account first to our deployed Solana Program, which then calls the System Program. See [here](https://github.com/solana-developers/program-examples/tree/main/basics/cross-program-invocation) for more Cross Program Invocation examples. +The tests cover two ways to create the account: -Calling the System Program directly means that the client sends the transaction to create the account directly to the Solana Program - -In this example, this account will simply hold some SOL. +1. **Via cross-program invocation (CPI):** the client sends a transaction to our deployed program, which in turn calls the System Program. +2. **Directly:** the client sends the create-account transaction straight to the System Program. -### Links: -- [Solana Cookbook - How to Create a System Account](https://solanacookbook.com/references/accounts.html#how-to-create-a-system-account) -- [Rust Docs - solana_program::system_instruction::create_account](https://docs.rs/solana-program/latest/solana_program/system_instruction/fn.create_account.html) \ No newline at end of file +See [cross-program-invocation](../cross-program-invocation) for more CPI examples. + +## Links + +- [Solana Cookbook β€” How to Create a System Account](https://solana.com/developers/cookbook/accounts/create-account) +- [Rust Docs β€” `solana_system_interface::instruction::create_account`](https://docs.rs/solana-system-interface/latest/solana_system_interface/instruction/fn.create_account.html) diff --git a/basics/create-account/asm/README.md b/basics/create-account/asm/README.md index dcd92df85..78fabdb95 100644 --- a/basics/create-account/asm/README.md +++ b/basics/create-account/asm/README.md @@ -1,3 +1,3 @@ # create-account-asm-program -Created with [sbpf](https://github.com/blueshift-gg/sbpf) \ No newline at end of file +A Solana SBPF assembly implementation, scaffolded with [sbpf](https://github.com/blueshift-gg/sbpf). diff --git a/basics/cross-program-invocation/README.md b/basics/cross-program-invocation/README.md index 4a728cca4..a11c91245 100644 --- a/basics/cross-program-invocation/README.md +++ b/basics/cross-program-invocation/README.md @@ -1,60 +1,51 @@ -# Cross Program Invocation (CPI) +# Cross-Program Invocation (CPI) -A cross-program invocation *literally* means invoking a program from another program (hence the term "cross-program"). There's really nothing special about it besides that. You're leveraging other programs from within your program to conduct business on Solana accounts. +A cross-program invocation is calling one program from another. You use CPIs when your program needs to compose with other onchain programs to do its work. -Whether or not you should send instructions to a program using a cross-program invocation or a client RPC call is a design choice that's completely up to the developer. +Whether a given operation should be done via a CPI or via separate RPC calls from the client is a design choice. The main reason to use a CPI is a **dependent operation** that must happen atomically with the rest of your logic. -There are many design considerations when making this decision, but the most common one to acknowledge is a **dependent operation** embedded in your program. +Consider this sequence in a token mint program: -Consider the below sequence of operations of an example **token mint** program: -1. Create & initialize the mint. -2. Create a metadata account for that mint. -3. Create & initialize a user's token account for that mint. +1. Create and initialize the mint. +2. Create a metadata account for the mint. +3. Create and initialize a user's token account for the mint. 4. Mint some tokens to the user's token account. -In the above steps, we can't create a metadata account without first creating a mint! In fact, we have to do all of these operations in order. +You cannot create a metadata account without first having the mint. Once you decide that steps 1 and 4 must be onchain, the only sensible option is to also do steps 2 and 3 onchain β€” you cannot pause a program mid-flight to let the client do work. -Let's say we decided it was essential to have our mint (operation 1) and our "mint to user" (operation 4) tasks onchain. We would have no choice but to also include the other two operations, since we can't do operation #1, pause the program while we do #2 & #3 from the client, and then resume the program for #4. +## Native setup notes -#### Notes on Native setup: +With the `native` implementation there is a small bit of setup to import one crate into another inside a Cargo workspace. -With the `native` implementation, you have to do a little bit of lifting to import one crate into another within your Cargo workspace. +A Solana program needs exactly one entry point, so a program that depends on another program must disable the other program's entry point. This is done with Cargo `[features]`. -This is because a Solana Program needs to have a single entry point. This means a Solana Program that depends on -other Solana Programs needs a way to disable the other entry points. This is done using `[features]` in Cargo. +In the `lever` crate's `Cargo.toml`: -Add the `no-entrypoint` feature to Cargo.toml of the `lever` crate: ```toml [features] no-entrypoint = [] ``` -Then, in the `hand` crate, use the import just like we did in the `anchor` example: + +Then, in the `hand` crate, import `lever` with that feature enabled: + ```toml [dependencies] -... -lever = { path = "../lever", features = [ "no-entrypoint" ] } +lever = { path = "../lever", features = ["no-entrypoint"] } ``` -Lastly, add this annotation over the `entrypoint!` macro that you wish to disable on import (the child program): + +In the `lever` crate, gate the `entrypoint!` macro on the feature being absent: + ```rust #[cfg(not(feature = "no-entrypoint"))] entrypoint!(process_instruction); ``` -The above configuration defines `no-entrypoint` as a _feature_ in the `lever` crate. This controls whether the line -`entrypoint!(process_instruction)` gets compiled or not depending on how the `lever` crate is included as a dependency. +This means `lever`'s entrypoint is compiled away when it's pulled in as a dependency, leaving only `hand`'s entrypoint in the final binary. -When adding `lever` as a dependency in the Cargo.tml of `hand` crate, we configure it with `features = [ "no-entrypoint" ]` -this makes sure that the `entrypoint!(process_instruction)` line is not part of the compilation. This ensures that only -the `entrypoint!(process_instruction)` from the `hand` crate is part of the compilation. +See the [Features chapter of the Cargo Book](https://doc.rust-lang.org/cargo/reference/features.html) for more on Cargo features. -For more about how `[features]` see [Features chapter in the Rust Book](https://doc.rust-lang.org/cargo/reference/features.html) - -### Let's switch the power on and off using a CPI! +## The example lever -In this example, we're just going to simulate a simple CPI - using one program's method from another program. - -Inside our `hand` program's `pull_lever` function, there's a cross-program invocation to our `lever` program's `switch_power` method. - -Simply put, **our hand program will pull the lever on the lever program to switch the power on and off**. \ No newline at end of file +The `hand` program's `pull_lever` instruction handler does a CPI into the `lever` program's `switch_power` instruction handler. Pull the lever, switch the power. diff --git a/basics/cross-program-invocation/quasar/README.md b/basics/cross-program-invocation/quasar/README.md index 89900308f..f86ba4c4d 100644 --- a/basics/cross-program-invocation/quasar/README.md +++ b/basics/cross-program-invocation/quasar/README.md @@ -2,7 +2,7 @@ This example contains **two separate Quasar programs** that work together: -- **`lever/`** β€” A program with onchain `PowerStatus` state and a `switch_power` instruction that toggles a boolean. +- **`lever/`** β€” A program with onchain `PowerStatus` state and a `switch_power` instruction handler that toggles a boolean. - **`hand/`** β€” A program that calls the lever program's `switch_power` via CPI. ## Building @@ -14,7 +14,7 @@ cd lever && quasar build cd hand && quasar build ``` -The hand program must be built **after** the lever, since its tests load the lever's compiled `.so` file. +Build the hand program **after** the lever, since its tests load the lever's compiled `.so` file. ## Testing @@ -25,12 +25,12 @@ cd hand && cargo test The hand tests load **both** programs into `QuasarSvm` and verify that the CPI correctly toggles the lever's power state. -## CPI Pattern +## CPI pattern Quasar doesn't have a `declare_program!` equivalent for importing arbitrary program instruction types (unlike Anchor). Instead, the hand program: -1. Defines a **marker type** (`LeverProgram`) that implements the `Id` trait with the lever's program address -2. Uses `Program` in the accounts struct for compile-time address + executable validation -3. Builds the CPI instruction data **manually** using `BufCpiCall`, constructing the lever's wire format directly +1. Defines a **marker type** (`LeverProgram`) that implements the `Id` trait with the lever's program address. +2. Uses `Program` in the accounts struct for compile-time address and executable validation. +3. Builds the CPI instruction data **manually** using `BufCpiCall`, constructing the lever's wire format directly. This is lower-level than Anchor's CPI pattern but gives full control and works with any program. diff --git a/basics/favorites/anchor/README.md b/basics/favorites/anchor/README.md index 6edd69541..6153598f6 100644 --- a/basics/favorites/anchor/README.md +++ b/basics/favorites/anchor/README.md @@ -1,9 +1,9 @@ # Favorites -This is a basic Anchor app using PDAs to store data for a user, and Anchor's account checks to ensure each user is only allowed to modify their own data. +A basic Anchor app that uses PDAs to store per-user data, and Anchor account constraints to ensure each user can only modify their own data. -It's used by the [https://github.com/solana-developers/professional-education](Solana Professional Education) course. +Used by the [Solana Professional Education](https://github.com/solana-developers/professional-education) course. ## Usage -`anchor test`, `anchor deploy` etc. +Run the tests with `pnpm test` (as configured in `Anchor.toml`). Deploy with `anchor deploy`. diff --git a/basics/hello-solana/README.md b/basics/hello-solana/README.md index 67283ccc0..721c5f4c2 100644 --- a/basics/hello-solana/README.md +++ b/basics/hello-solana/README.md @@ -1,21 +1,20 @@ # Hello Solana -This is it: our first Solana program. - -Naturally, we're going to start with "hello, world", but we'll take a look at some of the key things going on here. - +Our first Solana program β€” a "hello, world" that logs a greeting. Along the way, a quick look at what's inside a Solana transaction. + ## Transactions -First thing's first, we have to understand what's in a Solana transaction. - -> For a closer look at transactions, check out the [Solana Core Docs](https://docs.solana.com/developing/programming-model/transactions) or the [Solana Cookbook](https://solanacookbook.com/core-concepts/transactions.html#facts). +> For a closer look, see the [Solana docs on transactions](https://solana.com/docs/core/transactions). + +Two things to keep separate: + +- :key: **Transactions** are for **the Solana runtime**. They contain everything the runtime needs to allow or deny a transaction (signers, recent blockhash, etc.) and to decide what can run in parallel. +- :key: **Instructions** are for **Solana programs**. They tell a program what to do. +- :key: Your program receives one instruction at a time (`program_id`, `accounts`, `instruction_data`). -The anatomy of a transaction is as follows, but here's the keys: -:key: **Transactions** are for **the Solana runtime**. They contain information that Solana uses to allow or deny a transaction (signers, blockhash, etc.) and choose whether to process instructions in parallel. -:key: **Instructions** are for **Solana programs**. They tell the program what to do. -:key: Our program receives one instruction at a time (`program_id`, `accounts`, `instruction_data`). -#### Transaction -```shell +### Transaction + +```text signatures: [ s, s ] message: header: 000 @@ -23,8 +22,10 @@ message: recent_blockhash: int instructions: [ ix, ix ] ``` -#### Instruction -```shell + +### Instruction + +```text program_id: xxx accounts: [ aaa, aaa ] instruction_data: b[] diff --git a/basics/hello-solana/asm/README.md b/basics/hello-solana/asm/README.md index d02af51ec..31c3f6fac 100644 --- a/basics/hello-solana/asm/README.md +++ b/basics/hello-solana/asm/README.md @@ -1,3 +1,3 @@ # hello-solana-asm-program -Created with [sbpf](https://github.com/blueshift-gg/sbpf) \ No newline at end of file +A Solana SBPF assembly implementation, scaffolded with [sbpf](https://github.com/blueshift-gg/sbpf). diff --git a/basics/pda-rent-payer/README.md b/basics/pda-rent-payer/README.md index aa29bcb9d..7111f7057 100644 --- a/basics/pda-rent-payer/README.md +++ b/basics/pda-rent-payer/README.md @@ -1,5 +1,5 @@ # PDA Rent-Payer -This examples demonstrates how to use a PDA to pay the rent for the creation of a new account. - -The key here is accounts on Solana are automatically created under ownership of the System Program when you transfer lamports to them. So, you can just transfer lamports from your PDA to the new account's public key! \ No newline at end of file +Use a PDA to pay rent for a new account. + +Accounts on Solana are created under ownership of the System Program when you transfer lamports to them, so you can pay for a new account simply by transferring lamports from your PDA to the new account's public key. diff --git a/basics/processing-instructions/README.md b/basics/processing-instructions/README.md index 17e81dd06..a28188815 100644 --- a/basics/processing-instructions/README.md +++ b/basics/processing-instructions/README.md @@ -1,11 +1,6 @@ # Custom Instruction Data -Let's take a look at how to pass our own custom instruction data to a program. This data must be *serialized* to *Berkeley Packet Filter (BPF)* format - which is what the Solana runtime supports for serialized data. +Pass your own custom instruction data to a program. The data must be serialized in a format the Solana runtime can read β€” typically via the `borsh` crate on both the client and program sides. -BPF is exactly why we use `cargo build-sbf` to build Solana programs in Rust. For instructions sent over RPC it's no different. We'll use a library called `borsh` on both client and program side. - -_____ - -**For native**, we need to add `borsh` and `borsh-derive` to `Cargo.toml` so we can mark a struct as serializable to/from **BPF format**. - -**For Anchor**, you'll see that they've made it quite easy (as in, they do all of the serializing for you). +- **For `native`:** add `borsh` and `borsh-derive` to `Cargo.toml` so you can mark a struct as serializable. +- **For Anchor:** the framework handles serialization for you via the IDL. diff --git a/basics/realloc/README.md b/basics/realloc/README.md new file mode 100644 index 000000000..95c6c9a99 --- /dev/null +++ b/basics/realloc/README.md @@ -0,0 +1,9 @@ +# Realloc + +Resize a Solana account after it has been created β€” grow or shrink the data it can hold. + +## A note on `realloc` vs `resize` + +The runtime method `AccountInfo::realloc` has been deprecated in favor of `AccountInfo::resize` ([anchor#4526](https://github.com/solana-foundation/anchor/issues/4526)). New code should call `AccountInfo::resize`. + +The Anchor account-constraint macros (`#[account(realloc = ..., realloc::payer = ..., realloc::zero = ...)]`) are **not yet renamed** and still use the `realloc` spelling. That is the correct form to use today; track the issue above for any future change. diff --git a/basics/rent/README.md b/basics/rent/README.md index e4ea3c834..0795bcc85 100644 --- a/basics/rent/README.md +++ b/basics/rent/README.md @@ -1,17 +1,7 @@ # Rent -Ah, rent. Everybody's favorite thing to deal with. +All storage on Solana costs **rent**. -Luckily, rent is much less daunting on Solana than in the real world (sorry, best we could do). +In practice, rent is a small amount and accounts that hold at least two years' worth of rent are **rent-exempt** β€” they pay nothing. If your account holds more lamports than the two-year cost, it isn't charged rent. -___ - -Simply put, all storage on Solana costs **rent**. After all, this thing ain't free! - -Rent is typically a small amount and if you load your account with enough rent for **two years** it's actually exempt! That's right: **if your account holds more than the cost of rent for two years, that account is not charged any rent**. - -___ - -Rent itself is based off of the size of the data you're seeking to store in the account. - -Let's take a look. \ No newline at end of file +Rent is calculated from the size of the data stored in the account. diff --git a/basics/repository-layout/README.md b/basics/repository-layout/README.md index 2cd36a343..d9e214505 100644 --- a/basics/repository-layout/README.md +++ b/basics/repository-layout/README.md @@ -1,7 +1,7 @@ # Recommended Program Layout -This is the typical layout for a Solana program as it grows in size and begins to require multiple Rust files. You'll notice a lot of the programs in the [Solana Program Library](https://github.com/solana-labs/solana-program-library) follow this format. +A typical layout for a Solana program as it grows in size and starts to need multiple Rust files. Many programs follow this shape. -> Note: You can structure your Rust `src` folder however you wish - provided you follow Cargo's repository structure standards. You don't have to follow this pattern, but it's here so you can recognize other programs, too. +> You can structure your `src` folder however you like, as long as it follows Cargo's conventions. This layout is shown so that the patterns in other programs are recognizable. -You can see that the structure for a `native` repository is very similar to that of the `anchor` repository. The only difference is the inclusion of a `processor.rs` in the `native` setup - one of the many things Anchor abstracts away for you! \ No newline at end of file +The `native` and `anchor` layouts are similar. The main difference is the `processor.rs` file in the `native` setup β€” one of the things Anchor abstracts away for you. diff --git a/basics/transfer-sol/README.md b/basics/transfer-sol/README.md index 33c27c445..c1fec6815 100644 --- a/basics/transfer-sol/README.md +++ b/basics/transfer-sol/README.md @@ -1,5 +1,5 @@ # Transfer SOL -A simple example of transferring SOL between two system accounts. You can transfer SOL between many types of accounts, not just system accounts (owned by the System Program). +A simple example of transferring SOL between two system accounts. SOL can be transferred between many kinds of accounts, not just system accounts (accounts owned by the System Program). -One thing to note here is that we are generating a brand new keypair in the test - both for `native` and `anchor`. The act of transferring SOL to the new keypair's account will initialize it as a default system account (hence the `/// CHECK` above it in the `anchor` example). \ No newline at end of file +The tests generate a fresh keypair for both the `native` and `anchor` versions. Transferring SOL to the new keypair's address initializes it as a default system account β€” hence the `/// CHECK` annotation above it in the Anchor example. diff --git a/basics/transfer-sol/asm/README.md b/basics/transfer-sol/asm/README.md index 73fdfc9bb..2a839516c 100644 --- a/basics/transfer-sol/asm/README.md +++ b/basics/transfer-sol/asm/README.md @@ -1,3 +1,3 @@ -# asm +# transfer-sol-asm-program -Created with [sbpf](https://github.com/blueshift-gg/sbpf) \ No newline at end of file +A Solana SBPF assembly implementation, scaffolded with [sbpf](https://github.com/blueshift-gg/sbpf). diff --git a/compression/cnft-burn/anchor/README.md b/compression/cnft-burn/anchor/README.md index 63a25af3d..fb4da3b9e 100644 --- a/compression/cnft-burn/anchor/README.md +++ b/compression/cnft-burn/anchor/README.md @@ -1,26 +1,24 @@ # cnft-burn -This repository contains the cnft-burn program, a Solana Anchor program that allows you to burn compressed NFTs (cNFTs) in your collection. The program interacts with the Metaplex Bubblegum program through CPI to burn cNFTs. +An Anchor program that burns compressed NFTs (cNFTs) in your collection. The program performs a CPI into the Metaplex Bubblegum program to do the burn. ## Components -- programs: Contains the anchor program -- tests: Contains the tests for the anchor program +- `programs/` β€” the Anchor program. +- `tests/` β€” tests for the program. ## Deployment -The program is deployed on devnet at `FbeHkUEevbhKmdk5FE5orcTaJkCYn5drwZoZXaxQXXNn`. You can deploy it yourself by changing the respective values in lib.rs and Anchor.toml. +The program is deployed on devnet at `FbeHkUEevbhKmdk5FE5orcTaJkCYn5drwZoZXaxQXXNn`. To deploy your own copy, change the program ID in `lib.rs` and `Anchor.toml`. ## How to run -1. Configure RPC path in cnft-burn.ts. Personal preference: Helius RPCs. -2. run `anchor build` at the root of the project i.e cnft-burn in this case. -3. run `anchor deploy` to deploy and test the program on your own cluster. -4. run `anchor test` to run the tests. +1. Configure the RPC endpoint in `cnft-burn.ts`. +2. `anchor build` from the example root. +3. `anchor deploy` to deploy to your chosen cluster. +4. `pnpm test` to run the tests. ## Acknowledgements -This Example program would not have been possible without the work of: - -- [Metaplex](https://github.com/metaplex-foundation/) for providing the Bubblegum program with ix builders. -- [@nickfrosty](https://twitter.com/nickfrosty) for providing the sample code for fetching and creating cNFTs. +- [Metaplex](https://github.com/metaplex-foundation/) for the Bubblegum program and instruction builders. +- [@nickfrosty](https://twitter.com/nickfrosty) for the sample code that fetches and creates cNFTs. diff --git a/compression/cnft-vault/anchor/README.md b/compression/cnft-vault/anchor/README.md index aed85e599..894e9f9ab 100644 --- a/compression/cnft-vault/anchor/README.md +++ b/compression/cnft-vault/anchor/README.md @@ -1,28 +1,30 @@ -# Solana Program cNFT Transfer example +# cNFT Vault -This repo contains example code of how you can work with Metaplex compressed NFTs inside of Solana Anchor programs. +Example code for working with Metaplex compressed NFTs (cNFTs) inside Solana Anchor programs. -The basic idea is to allow for transfering cNFTs that are owned by a PDA account. So our program will have a vault (this PDA) that you can send cNFTs to manually and then withdraw them using the program instructions. +The program keeps a PDA-owned vault. You send cNFTs to the vault, then withdraw them via the program's instruction handlers. -There are two instructions: one simple transfer that can withdraw one cNFT, and one instructions that can withdraw two cNFTs at the same time. +Two handlers: -This program can be used as an inspiration on how to work with cNFTs in Solana programs. +- A simple transfer that withdraws one cNFT. +- A withdraw that handles two cNFTs in a single transaction. + +Use this as a reference for working with cNFTs in your own programs. ## Components -The Anchor program can be found in the *programs* folder and *tests* some clientside tests. There are also some typescript node scripts in *tests/scripts* to run them individually (plus there is one called *withdrawWithLookup.ts* which demonstrates the use of the program with account lookup tables). +- `programs/` β€” the Anchor program. +- `tests/` β€” TypeScript client-side tests. +- `tests/scripts/` β€” standalone scripts you can run individually. `withdrawWithLookup.ts` demonstrates using the program with Address Lookup Tables. ## Deployment -The program is deployed on devnet at `CNftyK7T8udPwYRzZUMWzbh79rKrz9a5GwV2wv7iEHpk`. -You can deploy it yourself by changing the respective values in lib.rs and Anchor.toml. +Deployed on devnet at `CNftyK7T8udPwYRzZUMWzbh79rKrz9a5GwV2wv7iEHpk`. To deploy your own, change the program ID in `lib.rs` and `Anchor.toml`. ## Limitations -This is just an example implementation. It is missing all logic wheter a transfer should be performed or not (everyone can withdraw any cNFT in the vault). -Furthermore it is not optimized for using lowest possible compute. It is intended as a proof of concept and reference implemention only. +This is a reference implementation. There's no authorization on withdraws β€” anyone can withdraw any cNFT in the vault. It's not optimized for compute either. Treat it as a proof of concept. ## Further resources -A video about the creation of this code which also contains further explanations has been publised on Solandy's YouTube channel: -https://youtu.be/qzr-q_E7H0M \ No newline at end of file +A video walkthrough is available on [Solandy's YouTube channel](https://youtu.be/qzr-q_E7H0M). diff --git a/compression/cutils/anchor/README.md b/compression/cutils/anchor/README.md index a34018983..373206718 100644 --- a/compression/cutils/anchor/README.md +++ b/compression/cutils/anchor/README.md @@ -1,52 +1,46 @@ -# Solana Program cNFT utils +# cNFT Utils -This repo contains example code of how you can work with Metaplex compressed NFTs inside of Solana Anchor programs. +Example code for working with Metaplex compressed NFTs (cNFTs) inside Solana Anchor programs. -The basic idea is to allow for custom logic in your own Solana program by doing a CPI to the bubblegum minting instruction. Two instructions: +This program shows how to add custom logic around the Bubblegum mint via CPI. Two handlers: -1. **mint**: mints a cNFT to your collection by doing a CPI to bubblegum. You could initialise your own program-specific PDA in this instruction -2. **verify**: verifies that the owner of the cNFT did in fact actuate the instruction. This is more of a utility function, which is to be used for future program-specific use-cases. +1. `mint` β€” mints a cNFT to your collection by CPI'ing Bubblegum. You can also initialize your own program-specific PDA in this handler. +2. `verify` β€” verifies that the owner of a given cNFT actually invoked the instruction. Useful as a building block for permissioned cNFT-gated logic. -This program can be used as an inspiration on how to work with cNFTs in Solana programs. +Use this as a reference for working with cNFTs in your own programs. ## Components -- **programs**: the Solana program - - There is a validate/actuate setup which allows you to validate some constraints through an `access_control` macro. This might be useful to use in conjunction with the cNFT verification logic. -- **tests**: - - `setup.ts` which is to be executed first if you don't already have a collection with merkle tree(s). - - `tests.ts` for running individual minting and verification tests +- `programs/` β€” the Anchor program. The setup uses a `validate`/`actuate` pattern via Anchor's `access_control` macro; this pairs well with the cNFT verification logic. +- `tests/` β€” TypeScript tests. + - `setup.ts` β€” run first if you don't already have a collection with a merkle tree. + - `tests.ts` β€” individual minting and verification tests. ## Deployment -The program is deployed on devnet at `burZc1SfqbrAP35XG63YZZ82C9Zd22QUwhCXoEUZWNF`. -You can deploy it yourself by changing the respective values in lib.rs and Anchor.toml. +Deployed on devnet at `burZc1SfqbrAP35XG63YZZ82C9Zd22QUwhCXoEUZWNF`. To deploy your own, change the program ID in `lib.rs` and `Anchor.toml`. ## Limitations -This is just an example implementation. Use at your own discretion +Reference implementation only. -**This only works on anchor 0.26.0 for now due to mpl-bubblegum dependencies** +**This example pins Anchor 0.26.0** because of mpl-bubblegum dependency constraints at the time of writing. -## Further resources -A video about the creation of this code which also contains further explanations has been publised on Burger Bob's YouTube channel: COMING SOON - -## How-to -1. Configure RPC path in _utils/readAPI.ts_. Personal preference: Helius RPCs. -2. cd root folder -2. Install packages: `yarn` -3. Optional: run `npx ts-node tests/setup.ts` to setup a NFT collection and its underlying merkle tree. -4. Comment-out the tests you don't want to execute in `tests/tests.ts` -5. If minting, change to your appropriate NFT uri -6. If verifying, change to your appropriate assetId (cNFT mint address) -7. Run `anchor test --skip-build --skip-deploy --skip-local-validator` -8. You can check your cNFTs on devnet through the Solflare wallet (thanks [@SolPlay_jonas](https://twitter.com/SolPlay_jonas)) -3. You might want to change the wallet-path in `Anchor.toml` +## How to run +1. Configure the RPC endpoint in `utils/readAPI.ts`. +2. `cd` to the example root. +3. `pnpm install`. +4. (Optional) `npx tsx tests/setup.ts` to create an NFT collection and its merkle tree. +5. Comment out the tests you don't want to run in `tests/tests.ts`. +6. If minting, set your NFT URI. +7. If verifying, set the asset ID (cNFT mint address) you want to verify. +8. Run `anchor test --skip-build --skip-deploy --skip-local-validator`. +9. View your cNFTs on devnet via the Solflare wallet. +10. You may also want to change the wallet path in `Anchor.toml`. ## Acknowledgements -This repo would not have been possible without the work of: -- [@nickfrosty](https://twitter.com/nickfrosty) for providing sample code and doing a live demo [here](https://youtu.be/LxhTxS9DexU) -- [@HeyAndyS](https://twitter.com/HeyAndyS) for laying the groundwork with cnft-vault -- The kind folks responding to this [thread](https://twitter.com/burger606/status/1669289672076320771?s=20) -- [Switchboard VRF-flip](https://github.com/switchboard-xyz/vrf-flip/tree/main/client) for inspiring the validate/actuate setup. \ No newline at end of file + +- [@nickfrosty](https://twitter.com/nickfrosty) for the sample code and [live demo](https://youtu.be/LxhTxS9DexU). +- [@HeyAndyS](https://twitter.com/HeyAndyS) for the groundwork in `cnft-vault`. +- Switchboard VRF-flip (since archived) for inspiring the validate/actuate setup. diff --git a/oracles/pyth/README.md b/oracles/pyth/README.md index ca801a8cc..cbea80352 100644 --- a/oracles/pyth/README.md +++ b/oracles/pyth/README.md @@ -1,22 +1,17 @@ -## What is Pyth ? +# Pyth Price Feeds -Pyth is an Oracle that offers onchain low-latency market data from institutional sources. -This means you can use prices from real-life assets in your Solana programs. +[Pyth](https://pyth.network/) is an oracle that publishes low-latency market data from institutional sources onchain. You can use it to read real-world asset prices from Solana programs. -The price for each asset will be represented inside of a Solana account. We call those accounts price feeds. +Each asset's price lives in its own Solana account β€” a **price feed**. -For example, the price feed for SOL/USD on mainnet is represented on this account address: `H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG`. +For example, the SOL/USD price feed on mainnet lives at `H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG`. -You can find more of these price feeds [here](https://pyth.network/price-feeds?cluster=mainnet-beta). +You can find more feeds in the [Pyth feed list](https://pyth.network/price-feeds?cluster=mainnet-beta). -To use such a price feed, you need to pass its account into your instructions context. - -You can get an asset's information by reading the account's data. The feed will consist of: - -- A price -- A confidence interval -- An exponent - -To read more about Pyth, please navigate to [the Pyth documentation](https://docs.pyth.network/solana-price-feeds). +To use a feed, pass its account into your instruction handler's context, then read the account's data. A feed contains: +- A price. +- A confidence interval. +- An exponent. +See the [Pyth Solana docs](https://docs.pyth.network/price-feeds/core/use-real-time-data/pull-integration/solana) for the full data layout and integration guide. diff --git a/tokens/create-token/README.md b/tokens/create-token/README.md index 479306710..058d25e59 100644 --- a/tokens/create-token/README.md +++ b/tokens/create-token/README.md @@ -1,50 +1,50 @@ # Create an SPL Token -This example demonstrates how to create an SPL Token on Solana with some metadata such as a token symbol and icon. +Create an SPL Token on Solana with metadata such as a symbol and an icon. + +All tokens on Solana β€” including NFTs β€” are SPL Tokens. They follow the SPL Token standard (similar in spirit to ERC-20). ---- -All tokens - including Non-Fungible Tokens (NFTs) are SPL Tokens on Solana. - -They follow the SPL Token standard (similar to ERC-20). - ```text -Default SPL Tokens : 9 decimals -NFTs : 0 decimals +Default SPL Tokens : 9 decimals +NFTs : 0 decimals ``` -### How Decimals Work -```text -Consider token JOE with 9 decimals: - 1 JOE = quantity * 10 ^ (-1 * decimals) = 1 * 10 ^ (-1 * 9) = 0.000000001 +## How decimals work + +For a token JOE with 9 decimals: + +```text +1 JOE = quantity * 10^(-decimals) = 1 * 10^(-9) = 0.000000001 ``` -### Mint & Metadata -SPL Tokens on Solana are referred to as a Mint. - -A Mint is defined by a specific type of account on Solana that describes information about a token: -```TypeScript + +## Mint and metadata + +An SPL Token is represented onchain by a **Mint Account**: + +```typescript { isInitialized, - supply, // The current supply of this token mint on Solana - decimals, // The number of decimals this mint breaks down to - mintAuthority, // The account who can authorize minting of new tokens - freezeAuthority, // The account who can authorize freezing of tokens + supply, // Current supply of this mint + decimals, // Number of decimals + mintAuthority, // Account that can authorise minting + freezeAuthority, // Account that can authorise freezing } ``` -Any metadata about this Mint - such as a nickname, symbol, or image - is stored in a **separate** account called a Metadata Account: -```TypeScript + +Metadata about a mint β€” name, symbol, image URI β€” lives in a separate **Metadata Account**: + +```typescript { title, symbol, - uri, // The URI to the hosted image + uri, // URI to the hosted image / off-asset metadata } ``` +> Metaplex is the de facto standard for SPL Token metadata on Solana. The [Metaplex Token Metadata Program](https://docs.metaplex.com/) is what creates these metadata accounts. -> Project Metaplex is the standard for SPL Token metadata on Solana -> You can use [Metaplex's Token Metadata Program](https://docs.metaplex.com/) to create metadata for your token. - +## Steps to create an SPL Token -### Steps to Create an SPL Token -1. Create an account for the Mint. +1. Create an account for the mint. 2. Initialize that account as a Mint Account. -3. Create a metadata account associated with that Mint Account. \ No newline at end of file +3. Create a metadata account associated with the mint. diff --git a/tokens/escrow/anchor/README.md b/tokens/escrow/anchor/README.md index e67de3059..310a17e70 100644 --- a/tokens/escrow/anchor/README.md +++ b/tokens/escrow/anchor/README.md @@ -2,48 +2,42 @@ ## Introduction -This Solana program is called an **_escrow_** - it allows a user to swap a specific amount of one token for a desired amount of another token. +This Solana program is an **escrow** β€” it lets a user swap a specific amount of one token for a desired amount of another token. -For example, Alice is offering 10 USDC, and wants 100 WIF in return. +For example: Alice offers 10 USDC and wants 100 WIF in return. -Without our program, users would have to engage in manual token swapping. Imagine the potential problems if Bob promised to send Alice 100 WIF, but instead took the 10 USDC and ran? Or what if Alice was dishonest, received the 10 USDC from Bob, and decided not to send the 100 WIF? Our Escrow program handles these complexities by acting a trusted entity that will only release tokens to both parties at the right time. +Without an escrow, users would have to swap tokens manually and trust each other. The escrow program acts as a trusted third party that only releases tokens to both sides when the swap can complete atomically. Neither party can take the other's tokens and run. -Our Escrow program is designed to provide a secure environment for users to swap a specific amount of one token with a specific amount of another token without having to trust each other. - -Better yet, since our program allows Alice and Bob to transact directly with each other, they both get a hundred percent of the token they desire! +Alice and Bob transact directly with each other through the program, so there's no spread or middleman fee taken on the swap. ## Usage -`anchor test`, `anchor deploy` etc. +Run the tests with `pnpm test` (as configured in `Anchor.toml`). ## Credit -This project is based on [Dean Little's Anchor Escrow,](https://github.com/deanmlittle/anchor-escrow-2024) with a few changes to make discussion in class easier. - -### Changes from original - -One of the challenges when teaching is avoiding ambiguity β€” names have to be carefully chosen to be clear and not possible to confuse with other times. - -- Custom instructions were replaced by `@solana-developers/helpers` for many tasks to reduce the file size. -- Shared functionality to transfer tokens is now in `instructions/shared.rs` -- The upstream project has a custom file layout. We use the 'multiple files' Anchor layout. -- Contexts are separate data structures from functions that use the contexts. There is no need for OO-like `impl` patterns here - there's no mutable state stored in the Context, and the 'methods' do not mutate that state. Besides, it's easier to type! -- The name 'deposit' was being used in multiple contexts, and `deposit` can be tough because it's a verb and a noun: - - - Renamed deposit #1 -> 'token_a_offered_amount' - - Renamed deposit #2 (in make() ) -> 'send_offered_tokens_to_vault' - - Renamed deposit #3 (in take() ) -> 'send_wanted_tokens_to_maker' - -- 'seed' was renamed to 'id' because 'seed' as it conflicted with the 'seeds' used for PDA address generation. -- 'Escrow' was used for the program's name and the account that records details of the offer. This wasn't great because people would confuse 'Escrow' with the 'Vault'. - - - Escrow (the program) -> remains Escrow - - Escrow (the offer) -> Offer. - -- 'receive' was renamed to 'token_b_wanted_amount' as 'receive' is a verb and not a suitable name for an integer. -- mint_a -> token_mint_a (ie, what the maker has offered and what the taker wants) -- mint_b -> token_mint_b (ie, what that maker wants and what the taker must offer) -- makerAtaA -> makerTokenAccountA, -- makerAtaB -> makerTokenAccountB -- takerAtaA -> takerTokenAccountA -- takerAtaB -> takerTokenAccountB +Based on [Dean Little's Anchor Escrow](https://github.com/deanmlittle/anchor-escrow-2024), with a few changes to make it easier to discuss in class. + +### Changes from the original + +One challenge when teaching is avoiding ambiguity β€” names have to be clear and not confused with anything else. + +- Several custom handler functions were replaced by helpers from `@solana-developers/helpers` to reduce file size. +- Shared token-transfer logic now lives in `instructions/shared.rs`. +- The upstream project uses a custom file layout. This version uses the 'multiple files' Anchor layout. +- Contexts are separate data structures from the functions that use them. There's no need for OO-style `impl` patterns here β€” no mutable state is stored in the context, and the methods don't mutate it. +- The name 'deposit' was overloaded. `deposit` is both a verb and a noun, which made the code hard to read: + - deposit #1 β†’ `token_a_offered_amount` + - deposit #2 (in `make()`) β†’ `send_offered_tokens_to_vault` + - deposit #3 (in `take()`) β†’ `send_wanted_tokens_to_maker` +- `seed` was renamed to `id`, because it conflicted with the `seeds` used for PDA derivation. +- `Escrow` was used for both the program name and the account that records an offer. People kept confusing the offer account with the vault. + - `Escrow` (the program) β†’ still `Escrow`. + - `Escrow` (the offer) β†’ `Offer`. +- `receive` was renamed to `token_b_wanted_amount`, since `receive` is a verb and not a good name for an integer. +- `mint_a` β†’ `token_mint_a` (what the maker offered and what the taker wants). +- `mint_b` β†’ `token_mint_b` (what the maker wants and what the taker must offer). +- `makerAtaA` β†’ `makerTokenAccountA` +- `makerAtaB` β†’ `makerTokenAccountB` +- `takerAtaA` β†’ `takerTokenAccountA` +- `takerAtaB` β†’ `takerTokenAccountB` diff --git a/tokens/escrow/anchor/programs/escrow/src/instructions/make_offer.rs b/tokens/escrow/anchor/programs/escrow/src/instructions/make_offer.rs index 43f06a847..b94d2862e 100644 --- a/tokens/escrow/anchor/programs/escrow/src/instructions/make_offer.rs +++ b/tokens/escrow/anchor/programs/escrow/src/instructions/make_offer.rs @@ -9,7 +9,7 @@ use crate::Offer; use super::transfer_tokens; -// See https://www.anchor-lang.com/docs/account-constraints#instruction-attribute +// See https://www.anchor-lang.com/docs/references/account-constraints#instruction-attribute #[derive(Accounts)] #[instruction(id: u64)] pub struct MakeOffer<'info> { diff --git a/tokens/nft-minter/README.md b/tokens/nft-minter/README.md index 37ab27502..8ccaa1135 100644 --- a/tokens/nft-minter/README.md +++ b/tokens/nft-minter/README.md @@ -1,27 +1,13 @@ # NFT Minter -Minting NFTs is exactly the same as [minting any SPL Token on Solana](../spl-token-minter/), except for immediately after the actual minting has occurred. - -What does this mean? Well, when you mint SPL Tokens, you can attach a fixed supply, but in most cases you can continue to mint new tokens at will - increasing the supply of that token. - -With an NFT, you're supposed to only have **one**. - -So, how do we ensure only one single token can be minted for any NFT? +Minting NFTs is the same as [minting any SPL Token on Solana](../spl-token-minter/), with one extra step at the end. ---- +When you mint SPL Tokens, you can in most cases continue to mint more tokens later, growing the supply. An NFT is supposed to have a supply of **one**. So we need to make sure no more can ever be minted. -We have to disable minting by changing the Mint Authority on the Mint. - -> The Mint Authority is the account that is permitted to mint new tokens into supply. - -If we remove this authority - effectively setting it to `null` - we can disable minting of new tokens for this Mint. - -> By design, **this is irreversible**. - ---- +The way to do that is to remove the mint authority from the mint: -Although we can set this authority to `null` manually, we can also make use of Metaplex again, this time to mark our NFT as Limited Edition. - -When we use an Edition - such as a Master Edition - for our NFT, we get some extra metadata associated with our NFT and we also get our Mint Authority deactivated by delegating this authority to the Master Edition account. - -This will effectively disable future minting, but make sure you understand the ramifications of having the Master Edition account be the Mint Authority - rather than setting it permanently to `null`. \ No newline at end of file +> The Mint Authority is the account allowed to mint new tokens into supply. + +Setting the mint authority to `null` permanently disables minting. **This is irreversible.** + +You can do this manually, or use Metaplex to mark the NFT as a Limited Edition. When you use an Edition β€” such as a Master Edition β€” for your NFT, you get extra Metaplex metadata, and the mint authority is delegated to the Master Edition account. That delegation effectively disables future minting. Be sure you understand the trade-offs of letting the Master Edition account hold the mint authority instead of setting it permanently to `null`. diff --git a/tokens/nft-operations/anchor/readme.MD b/tokens/nft-operations/anchor/README.md similarity index 60% rename from tokens/nft-operations/anchor/readme.MD rename to tokens/nft-operations/anchor/README.md index df13a202f..70978d39a 100644 --- a/tokens/nft-operations/anchor/readme.MD +++ b/tokens/nft-operations/anchor/README.md @@ -1,14 +1,12 @@ # NFT Operations -This example demonstrates how to create a NFT collection, how to mint a NFT and how to verify a NFT as part of a collection. +Create an NFT collection, mint an NFT, and verify an NFT as part of a collection β€” all using Metaplex Token Metadata. ---- +## Program setup -## Program Setup +This example clones the Metaplex Token Metadata program from mainnet. See `Anchor.toml`: -For this program example we will be cloning the Metaplex Token Metadata program from mainnet. You can find this in the Anchor.toml file - -```rust +```toml [test.validator] url = "https://api.mainnet-beta.solana.com" @@ -16,13 +14,11 @@ url = "https://api.mainnet-beta.solana.com" address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" ``` -We will need this program to perform CPIs to create the Metadata Accounts, Master Edition Accounts and to Verify NFTs as part of collections - ---- +The program is needed for CPIs that create metadata accounts and master edition accounts, and to verify NFTs as part of a collection. -## Create a NFT Collection: +## Create an NFT collection -The accounts needed to create a NFT Collection are the following: +The accounts needed to create an NFT collection are: ```rust #[derive(Accounts)] @@ -63,29 +59,21 @@ pub struct CreateCollection<'info> { } ``` -### Let's break down these accounts: - -- user: the account that is creating the collection NFT and the owner of the destination token account - -- mint: the collection NFT Mint account. We will be initializing this account with 0 decimals and giving the mint authority and freeze authority to the mint_authority account - -- mint_authority: the account with authority to mint tokens from the collection NFT mint account - -- metadata: the metadata account of the collection NFT - -- master_edition: the master edition account of the collection NFT - -- destination: the token account where the collection NFT will minted to. We will be initializing this account and verifying the correct mint and authority - -- system_program: Program resposible for the initialization of any new account - -- token_program and associated_token_program: We are creating new ATAs and minting tokens +### Account breakdown -- token_metadata_program: MPL token metadata program that will be used to create the metadata and master edition accounts +- `user`: the account creating the collection NFT and the owner of the destination token account. +- `mint`: the collection NFT mint account. Initialized with 0 decimals; the mint authority and freeze authority are set to `mint_authority`. +- `mint_authority`: the PDA authority used to mint tokens from the collection mint. +- `metadata`: the metadata account of the collection NFT. +- `master_edition`: the master edition account of the collection NFT. +- `destination`: the token account that receives the collection NFT. +- `system_program`: initializes new accounts. +- `token_program` / `associated_token_program`: create new ATAs and mint tokens. +- `token_metadata_program`: the MPL Token Metadata program, used to create the metadata and master edition accounts. -To note in here, that both the metadata account and the master_edition account are Unchecked Accounts. That is due to the fact that they are not initialized, and the initialization will be performed by the token_metadata_program when we perform a CPI (cross program invocation) to initialize both accounts. +Both `metadata` and `master_edition` are `UncheckedAccount` because they are uninitialized at the start of the instruction β€” the Token Metadata program initializes them via CPI. -If we had something like: +Had we written: ```rust #[derive(Accounts)] @@ -97,11 +85,11 @@ pub struct CreateCollection<'info> { } ``` -our instruction would fail because it would expect the accounts to be already initialized. +the instruction would fail because Anchor would expect the accounts to already be initialized. -However, if the account was already initialized (you'll see that while we verify collections), you should use the specific account types +When an account *is* already initialized (as in the verify-collection flow below), use the specific account types. -### We then implement some functionality for our CreateCollection context: +### Implementation for `CreateCollection` ```rust impl<'info> CreateCollection<'info> { @@ -197,22 +185,17 @@ impl<'info> CreateCollection<'info> { } ``` -The create collection method consists of 3 steps: +Three steps: -- Mint one token to the destination token account by performing a CPI to the Token Program +1. Mint one token to the destination token account via a CPI to the Token Program. +2. Create a metadata account for the mint via a CPI to the Token Metadata program. The mint authority signs the CPI, so we use `invoke_signed` with the authority PDA's seeds. +3. Create a master edition account for the mint via a CPI to the Token Metadata program. This enforces the NFT-specific constraints and transfers both the mint authority and freeze authority to the Master Edition PDA. Again, the mint authority signs. -- Create a metadata account for the mint account to store standardized data that can be understood by apps and marketplaces. This is achieved by performing a CPI to the Token Metadata Program. The mint authority needs to sign that CPI, therefore we use "invoke_signed" and pass in the seeds of our authority PDA +More on Token Metadata: -- Create a master edition account for the mint account by performing a CPI to the Token Metadata Program. That will ensure that the special characteristics on Non-Fungible Tokens are met. It will also transfer both the mint authority and the freeze authority to the Master Edition PDA. The mint authority needs to sign that CPI, therefore we use "invoke_signed" and pass in the seeds of our authority PDA +## Mint an NFT - -More information on Token Metadata can be found at https://developers.metaplex.com/token-metadata - ---- - -## Mint a NFT: - -The accounts needed to create a NFT Collection are the following: +The accounts needed to mint an NFT: ```rust #[derive(Accounts)] @@ -255,36 +238,22 @@ pub struct MintNFT<'info> { } ``` -### Let's break down these accounts: - -- owner: the account that is creating the NFT and the owner of the destination token account - -- mint: the collection NFT Mint account. We will be initializing this account with 0 decimals and giving the mint authority and freeze authority to the mint_authority account - -- destination: the token account where the collection NFT will minted to. We will be initializing this account and verifying the correct mint and authority - -- metadata: the metadata account of the collection NFT - -- master_edition: the master edition account of the collection NFT - -- mint_authority: the account with authority to mint tokens from the collection NFT mint account +### Account breakdown -- collection_mint: the collection account that the NFT that we are minting should be part of +- `owner`: the account minting the NFT and the owner of the destination token account. +- `mint`: the NFT mint account. 0 decimals; mint authority and freeze authority are the PDA. +- `destination`: the token account that receives the NFT. +- `metadata`: the metadata account. +- `master_edition`: the master edition account. +- `mint_authority`: the PDA authority used to mint tokens. +- `collection_mint`: the collection the NFT belongs to. +- `system_program`, `token_program`, `associated_token_program`, `token_metadata_program`: as above. -- system_program: Program resposible for the initialization of any new account +Apart from `collection_mint`, the accounts are the same as the collection creation flow. A collection is just a regular NFT with the `collection_details` field set and the `collection` field on `data` set to `None`. An NFT belonging to a collection has `collection_details` set to `None` and the `collection` field on `data` set to a `Collection` struct with the collection's key and a `verified` boolean. `verified` starts false and flips to true once the NFT is verified as part of the collection. -- token_program and associated_token_program: We are creating new ATAs and minting tokens +That's where the `collection` account comes from β€” it provides the address that goes into the `Collection` struct on the NFT's metadata. -- token_metadata_program: MPL token metadata program that will be used to create the metadata and master edition accounts - -If you take a closer look, you will see that the accounts (apart from "collection_mint") are the same. -This is due to the fact that the a collection is basically just a regular NFT but, the "collection_details" field will be set with a CollectionDetails struct and the "collection" field under "data" set to None. - -On the other hand, a NFT will have "collection_details" field set to None and with a CollectionDetails and the "collection" field under "data" set to a Collection struct, containing the key of the collection it belongs to and a verified boolean (set to False, it will be automatically set to True once the NFT gets verified as part of the collection) - -This is actually where the "collection" account comes from. This account is used to set the the address of the Collection struct when we are creating the NFT metadata account - -### We then implement some functionality for our MintNFT context: +### Implementation for `MintNFT` ```rust impl<'info> MintNFT<'info> { @@ -378,18 +347,15 @@ impl<'info> MintNFT<'info> { } ``` -Since a collection NFT is just a regular NFT with "special" metadata, again you can see that the same is happening as when created the Collection NFT. - -- Mint one token to the destination token account by performing a CPI to the Token Program +Because a collection NFT is just a regular NFT with special metadata, the implementation mirrors `CreateCollection`. The same three steps: -- Create a metadata account for the mint account to store standardized data that can be understood by apps and marketplaces. This is achieved by performing a CPI to the Token Metadata Program. The mint authority needs to sign that CPI, therefore we use "invoke_signed" and pass in the seeds of our authority PDA +1. Mint one token to the destination via a Token Program CPI. +2. Create a metadata account via a Token Metadata CPI (signed with the PDA seeds). +3. Create a master edition account via a Token Metadata CPI (signed with the PDA seeds). -- Create a master edition account for the mint account by performing a CPI to the Token Metadata Program. That will ensure that the special characteristics on Non-Fungible Tokens are met. It will also transfer both the mint authority and the freeze authority to the Master Edition PDA. The mint authority needs to sign that CPI, therefore we use "invoke_signed" and pass in the seeds of our authority PDA +The difference is in the data on the metadata account. - -The difference is in the data of our metadata account. - -for our collection NFT, we have +For the collection NFT: ```rust CreateMetadataAccountV3InstructionArgs { data: DataV2 { @@ -409,10 +375,9 @@ CreateMetadataAccountV3InstructionArgs { ) } ``` -where we set the "collection_details" field - +We set `collection_details`. -for our "regular" NFT we have +For a regular NFT: ```rust CreateMetadataAccountV3InstructionArgs { data: DataV2 { @@ -431,15 +396,11 @@ CreateMetadataAccountV3InstructionArgs { collection_details: None, } ``` -where we set the "collection" field with the key of the collection account. - -Again, we set the "verified" boolean to false, since this NFT has not yet been verified as part of the desired collection - ---- +We set the `collection` field with the key of the collection. `verified` starts false until the NFT is verified. -## Verify a NFT as part of a collection: +## Verify an NFT as part of a collection -The accounts needed to verify a NFT as part of a collection are the following: +The accounts needed to verify an NFT as part of a collection: ```rust #[derive(Accounts)] @@ -466,33 +427,22 @@ pub struct VerifyCollectionMint<'info> { } ``` -### Let's break down these accounts: +### Account breakdown -- authority: signer of the transaction. This can be used to restrict the address that can execute the verify collection method, by adding constraints +- `authority`: signer of the transaction. You can add constraints to restrict who can verify a collection. +- `metadata`: the metadata account of the NFT being verified. +- `mint`: the NFT mint being verified. +- `mint_authority`: the mint authority of the collection NFT. +- `collection_mint`: the mint account of the collection NFT. +- `collection_metadata`: the metadata account of the collection NFT. +- `collection_master_edition`: the master edition account of the collection NFT. +- `system_program`: as above. +- `sysvar_instruction`: provides access to the serialized instruction data for the running transaction. +- `token_metadata_program`: MPL Token Metadata, used to perform the verification CPI. -- metadata: the metadata account of the NFT that we want to verify +Only the NFT and collection NFT metadata accounts need to be mutable β€” both are updated. The NFT metadata gets its `verified` boolean flipped to true, and the collection NFT metadata has its collection size incremented. -- mint: the NFT that we want to verify - -- mint_authority: the mint_authority of the Collection NFT - -- collection_mint: the mint account of the Collection NFT - -- collection_metadata: the metadata account of the Collection NFT - -- collection_master_edition: the master edition account of the Collection NFT - -- system_program: program resposible for the initialization of any new account - -- sysvar_instruction: the instructions sysvar provides access to the serialized instruction data -for the currently-running transaction - -- token_metadata_program: MPL token metadata program that will be used to verify the NFT as part of the desired collection - -Note that the only account that need to be mutable in here, are the NFT and Colelction NFT metadata accounts. -This is due to the fact that both will be updated. The NFT metadata account will have the "verified" boolean set to true, and the Collection NFT metadata account will have the colelction size incremented - -### We then implement some functionality for our VerifyCollectionMint context: +### Implementation for `VerifyCollectionMint` ```rust impl<'info> VerifyCollectionMint<'info> { @@ -534,8 +484,6 @@ impl<'info> VerifyCollectionMint<'info> { } ``` -In this "verify_collection" method, we simply create a CPI to the to the Token Metadata Program with the appropriate accounts to verify the NFT as part of a collection. Since the authority of the Collection NFT will sign that CPI, the NFT will be verified as part of the collection. - ---- +`verify_collection` performs a CPI to the Token Metadata program with the right accounts. The collection NFT's mint authority signs the CPI, and the NFT is verified as part of the collection. -With this examples, you will be able to adjust / adapt it to your needs and create Collections, Mint NFTs, and verify NFTs as part of collections +Use this as a starting point for your own collections, NFTs, and verification flows. diff --git a/tokens/pda-mint-authority/README.md b/tokens/pda-mint-authority/README.md index 89f85454f..e8b58df23 100644 --- a/tokens/pda-mint-authority/README.md +++ b/tokens/pda-mint-authority/README.md @@ -1,5 +1,5 @@ # PDA Mint Authority -This example is exactly the same as the `NFT Minter` example, but it changes the `mint authority` account from the payer (System Account) to a PDA. - -πŸ’‘Notice the use of `invoke_signed` for CPIs. \ No newline at end of file +The same as the [NFT Minter](../nft-minter) example, except the **mint authority** is a PDA rather than a system account belonging to the payer. + +πŸ’‘ Notice the use of `invoke_signed` for CPIs. diff --git a/tokens/spl-token-minter/README.md b/tokens/spl-token-minter/README.md index e0641518e..e50914a81 100644 --- a/tokens/spl-token-minter/README.md +++ b/tokens/spl-token-minter/README.md @@ -1,23 +1,13 @@ # SPL Token Minter -Minting SPL Tokens is a conceptually straightforward process. - -The only tricky part is understanding how Solana tracks users' balance of SPL Tokens. - ---- +Minting SPL Tokens is conceptually straightforward. The only subtle part is understanding how Solana tracks per-user token balances. -After all, we know every account on Solana by default tracks that account's balance of SOL (the native token), but how could every account on Solana possibly track it's own balance of *any possible* SPL Token on the Solana network? - -TL/DR it's impossible. Instead, we have to use separate accounts that are specifically configured per SPL Token. These are called **Associated Token Accounts**. - ---- -For example, if I create the JOE token, and I want to know what someone's balance of JOE is, I would need to do the following: -```text -1. Create the JOE token -2. Create an Associated Token Account for this user's wallet to track his/her balance of JOE -3. Mint or transfer JOE token to their JOE Associated Token Account -``` +Every account on Solana tracks its own balance of SOL. It can't possibly also track its own balance of every SPL Token on the network. Instead, balances for SPL Tokens are held in separate accounts that are specific to a given mint and a given owner. These are called **Associated Token Accounts (ATAs)**. ---- +To know what someone's balance of token JOE is, you would: -Thus, you can think of Associated Token Accounts as simple counters, which point to a Mint and a Wallet. They simply say "here's the balance of this particular Mint for this particular person's Wallet". \ No newline at end of file +1. Create the JOE mint. +2. Create an Associated Token Account for the user's wallet, scoped to the JOE mint. +3. Mint or transfer JOE to that Associated Token Account. + +You can think of Associated Token Accounts as per-(mint, wallet) counters: "here is the balance of this mint for this wallet". diff --git a/tokens/token-extensions/default-account-state/native/README.md b/tokens/token-extensions/default-account-state/native/README.md index b26358ee9..32555eeb6 100644 --- a/tokens/token-extensions/default-account-state/native/README.md +++ b/tokens/token-extensions/default-account-state/native/README.md @@ -1,8 +1,8 @@ -## Token22 with default account state +# Token-2022 with Default Account State -This extension makes it possible to set a default state for all underlying Token accounts of a certain Mint. +This extension sets a default state for all token accounts of a given mint. -These account states can be: +Account states: -- initialized: normal token account that can perform actions like transfers -- frozen: the owner of this token account will not be able to perform any actions with his token. +- **initialized:** a normal token account that can transfer, etc. +- **frozen:** the owner cannot perform any token actions until the account is unfrozen. diff --git a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/README.md b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/README.md index 90af0f06f..be73fc0c1 100644 --- a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/README.md +++ b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/README.md @@ -1,528 +1,90 @@ -## Token Extension MetaData Pointer NFT +# Token Extension Metadata-Pointer NFT -This is a simple example of a program that creates a NFT using the new token extension program and facilitating the token extension meta data pointer. +An Anchor program that mints an NFT using the Token-2022 metadata-pointer extension. The mint itself stores its own metadata via the metadata extension, so no separate Metaplex metadata account is needed. -The cool thing about this especially for games is that we can now have additional metadata fields onchain as a key value store which can be used to save the state of the game character. In this example we save the level and the collected wood of the player. +This is particularly useful for games β€” you get arbitrary key/value metadata stored onchain that you can use to record character state. In this example, the player's level and collected wood are stored on the NFT. -This opens all kind of interesting possibilities for games. You can for example save the level and xp of the player, the current weapon and armor, the current quest and so on. When market places will eventually support additional meta data the nfts could be filtered and ordered by the meta data fields and NFTs with better values like higher level could potentially gain more value by playing. +When marketplaces support additional metadata, NFTs can be filtered or ranked by those fields, e.g. by character level. -The nft will be created in an anchor program so it is very easy to mint from the js client. The name, uri and symbol are saved in the meta data extension which is pointed to the mint. +A [video walkthrough](https://www.youtube.com/@SolanaFndn/videos) is available on the Solana Foundation YouTube channel. -The nft will have a name, symbol and a uri. The uri is a link to a json file which contains the meta data of the nft. +## How to run -There is a video walkthrough of this example on the Solana Foundation Youtube channel. - -[![Solana Foundation Youtube channel]](https://www.youtube.com/@SolanaFndn/videos) - -# How to run this example - -Running the tests - -```shell -cd program -anchor test --detach -``` - -Then you can set your https://solana.explorer.com url to local net an look at the transactions. - -The program is also already deployed to dev net so you can try it out on dev net. -Starting the js client - -```shell -cd app -yarn install -yarn dev -``` - -# Minting the NFT - -For the creating of the NFT we perform the following steps: - -1. Create a mint account -2. Initialize the mint account -3. Create a metadata pointer account -4. Initialize the metadata pointer account -5. Create the metadata account -6. Initialize the metadata account -7. Create the associated token account -8. Mint the token to the associated token account -9. Freeze the mint authority - -Here is the rust code for the minting of the NFT: - -```rust -let space = ExtensionType::try_calculate_account_len::( - &[ExtensionType::MetadataPointer]) - .unwrap(); - - // This is the space required for the metadata account. - // We put the meta data into the mint account at the end so we - // don't need to create and additional account. - let meta_data_space = 250; - - let lamports_required = (Rent::get()?).minimum_balance(space + meta_data_space); - - msg!( - "Create Mint and metadata account size and cost: {} lamports: {}", - space as u64, - lamports_required - ); - - system_program::create_account( - CpiContext::new( - ctx.accounts.token_program.to_account_info(), - system_program::CreateAccount { - from: ctx.accounts.signer.to_account_info(), - to: ctx.accounts.mint.to_account_info(), - }, - ), - lamports_required, - space as u64, - &ctx.accounts.token_program.key(), - )?; - - // Assign the mint to the token program - system_program::assign( - CpiContext::new( - ctx.accounts.token_program.to_account_info(), - system_program::Assign { - account_to_assign: ctx.accounts.mint.to_account_info(), - }, - ), - &token_2022::ID, - )?; - - // Initialize the metadata pointer (Need to do this before initializing the mint) - let init_meta_data_pointer_ix = - spl_token_2022::extension::metadata_pointer::instruction::initialize( - &Token2022::id(), - &ctx.accounts.mint.key(), - Some(ctx.accounts.nft_authority.key()), - Some(ctx.accounts.mint.key()), - ) - .unwrap(); - - invoke( - &init_meta_data_pointer_ix, - &[ - ctx.accounts.mint.to_account_info(), - ctx.accounts.nft_authority.to_account_info() - ], - )?; - - // Initialize the mint cpi - let mint_cpi_ix = CpiContext::new( - ctx.accounts.token_program.to_account_info(), - token_2022::InitializeMint2 { - mint: ctx.accounts.mint.to_account_info(), - }, - ); - - token_2022::initialize_mint2( - mint_cpi_ix, - 0, - &ctx.accounts.nft_authority.key(), - None).unwrap(); - - // We use a PDA as a mint authority for the metadata account because - // we want to be able to update the NFT from the program. - let seeds = b"nft_authority"; - let bump = ctx.bumps.nft_authority; - let signer: &[&[&[u8]]] = &[&[seeds, &[bump]]]; - - msg!("Init metadata {0}", ctx.accounts.nft_authority.to_account_info().key); - - // Init the metadata account - let init_token_meta_data_ix = - &spl_token_metadata_interface::instruction::initialize( - &spl_token_2022::id(), - ctx.accounts.mint.key, - ctx.accounts.nft_authority.to_account_info().key, - ctx.accounts.mint.key, - ctx.accounts.nft_authority.to_account_info().key, - "Beaver".to_string(), - "BVA".to_string(), - "https://arweave.net/MHK3Iopy0GgvDoM7LkkiAdg7pQqExuuWvedApCnzfj0".to_string(), - ); - - invoke_signed( - init_token_meta_data_ix, - &[ctx.accounts.mint.to_account_info().clone(), ctx.accounts.nft_authority.to_account_info().clone()], - signer, - )?; - - // Update the metadata account with an additional metadata field in this case the player level - invoke_signed( - &spl_token_metadata_interface::instruction::update_field( - &spl_token_2022::id(), - ctx.accounts.mint.key, - ctx.accounts.nft_authority.to_account_info().key, - spl_token_metadata_interface::state::Field::Key("level".to_string()), - "1".to_string(), - ), - &[ - ctx.accounts.mint.to_account_info().clone(), - ctx.accounts.nft_authority.to_account_info().clone(), - ], - signer - )?; - - // Create the associated token account - associated_token::create( - CpiContext::new( - ctx.accounts.associated_token_program.to_account_info(), - associated_token::Create { - payer: ctx.accounts.signer.to_account_info(), - associated_token: ctx.accounts.token_account.to_account_info(), - authority: ctx.accounts.signer.to_account_info(), - mint: ctx.accounts.mint.to_account_info(), - system_program: ctx.accounts.system_program.to_account_info(), - token_program: ctx.accounts.token_program.to_account_info(), - }, - ))?; - - // Mint one token to the associated token account of the player - token_2022::mint_to( - CpiContext::new_with_signer( - ctx.accounts.token_program.to_account_info(), - token_2022::MintTo { - mint: ctx.accounts.mint.to_account_info(), - to: ctx.accounts.token_account.to_account_info(), - authority: ctx.accounts.nft_authority.to_account_info(), - }, - signer - ), - 1, - )?; - - // Freeze the mint authority so no more tokens can be minted to make it an NFT - token_2022::set_authority( - CpiContext::new_with_signer( - ctx.accounts.token_program.to_account_info(), - token_2022::SetAuthority { - current_authority: ctx.accounts.nft_authority.to_account_info(), - account_or_mint: ctx.accounts.mint.to_account_info(), - }, - signer - ), - AuthorityType::MintTokens, - None, - )?; -``` - - - - -The example is based on the Solana Games Preset - -```shell -npx create-solana-game gamName -``` - -# Solana Game Preset - -This game is ment as a starter game for onchain games. -There is a js and a unity client for this game and both are talking to a solana anchor program. - -This game uses gum session keys for auto approval of transactions. -Note that neither the program nor session keys are audited. Use at your own risk. - -# How to run this example - -## Quickstart - -The unity client and the js client are both connected to the same program and should work out of the box connecting to the already deployed program. - -### Unity -Open the Unity project with Unity Version 2021.3.32.f1 (or similar), open the GameScene or LoginScene and hit play. -Use the editor login button in the bottom left. If you cant get devnet sol you can copy your address from the console and use the faucet here: https://faucet.solana.com/ to request some sol. - -### Js Client -To start the js client open the project in visual studio code and run: +### Tests ```bash -cd app -yarn install -yarn dev +cd anchor +anchor build +pnpm test ``` -To start changing the program and connecting to your own program follow the steps below. - -## Installing Solana dependencies - -Follow the installation here: https://www.anchor-lang.com/docs/installation -Install the latest 1.16 solana version (1.17 is not supported yet) -sh -c "$(curl -sSfL https://release.solana.com/v1.16.18/install)" - -Anchor program -1. Install the [Anchor CLI](https://project-serum.github.io/anchor/getting-started/installation.html) -2. `cd anchor` to end the program directory -3. Run `anchor build` to build the program -4. Run `anchor deploy` to deploy the program -5. Copy the program id from the terminal into the lib.rs, anchor.toml and within the unity project in the AnchorService and if you use js in the anchor.ts file -6. Build and deploy again - -Next js client -1. Install [Node.js](https://nodejs.org/en/download/) -2. Copy the program id into app/utils/anchor.ts -2. `cd app` to end the app directory -3. Run `yarn install` to install node modules -4. Run `yarn dev` to start the client -5. After doing changes to the anchor program make sure to copy over the types from the program into the client so you can use them. You can find the js types in the target/idl folder. - -Unity client -1. Install [Unity](https://unity.com/) -2. Open the MainScene -3. Hit play -4. After doing changes to the anchor program make sure to regenerate the C# client: https://solanacookbook.com/gaming/porting-anchor-to-unity.html#generating-the-client -Its done like this (after you have build the program): +### JS client ```bash -cd program -dotnet tool install Solana.Unity.Anchor.Tool <- run once -dotnet anchorgen -i target/idl/extension_nft.json -o target/idl/ExtensionNft.cs -``` - -(Replace extension_nft with the name of your program) - -then copy the c# code into the unity project. - -## Connect to local host (optional) -To connect to local host from Unity add these links on the wallet holder game object: -http://localhost:8899 -ws://localhost:8900 - -## Video walkthroughs -Here are two videos explaining the energy logic and session keys: -Session keys: -https://www.youtube.com/watch?v=oKvWZoybv7Y&t=17s&ab_channel=Solana -Energy system: -https://www.youtube.com/watch?v=YYQtRCXJBgs&t=4s&ab_channel=Solana - -# Project structure -The anchor project is structured like this: - -The entry point is in the lib.rs file. Here we define the program id and the instructions. -The instructions are defined in the instructions folder. -The state is defined in the state folder. - -So the calls arrive in the lib.rs file and are then forwarded to the instructions. -The instructions then call the state to get the data and update it. - -```shell -β”œβ”€β”€ src -β”‚ β”œβ”€β”€ instructions -β”‚ β”‚ β”œβ”€β”€ chop_tree.rs -β”‚ β”‚ β”œβ”€β”€ init_player.rs -β”‚ β”‚ └── update_energy.rs -β”‚ β”œβ”€β”€ state -β”‚ β”‚ β”œβ”€β”€ game_data.rs -β”‚ β”‚ β”œβ”€β”€ mod.rs -β”‚ β”‚ └── player_data.rs -β”‚ β”œβ”€β”€ lib.rs -β”‚ └── constants.rs -β”‚ └── errors.rs - -``` - -The project uses session keys (maintained by Magic Block) for auto approving transactions using an expiring token. - -# Energy System - -Many casual games in traditional gaming use energy systems. This is how you can build it onchain. - -If you have no prior knowledge in solana and rust programming it is recommended to start with the Solana cookbook [Hello world example]([https://unity.com/](https://solanacookbook.com/gaming/hello-world.html#getting-started-with-your-first-solana-game)). - -## Anchor program - -Here we will build a program which refills energy over time which the player can then use to perform actions in the game. -In our example it will be a lumber jack which chops trees. Every tree will reward on wood and cost one energy. - -### Creating the player account - -First the player needs to create an account which saves the state of our player. Notice the last_login time which will save the current unix time stamp of the player he interacts with the program. -Like this we will be able to calculate how much energy the player has at a certain point in time. -We also have a value for wood which will store the wood the lumber jack chucks in the game. - -```rust - -pub fn init_player(ctx: Context) -> Result<()> { - ctx.accounts.player.energy = MAX_ENERGY; - ctx.accounts.player.last_login = Clock::get()?.unix_timestamp; - ctx.accounts.player.authority = ctx.accounts.signer.key(); - Ok(()) -} - -#[derive(Accounts)] -pub struct InitPlayer<'info> { - #[account( - init, - payer = signer, - space = 1000, // 8+32+x+1+8+8+8 But taking 1000 to have space to expand easily. - seeds = [b"player".as_ref(), signer.key().as_ref()], - bump, - )] - pub player: Account<'info, PlayerData>, - - #[account( - init_if_needed, - payer = signer, - space = 1000, // 8 + 8 for anchor account discriminator and the u64. Using 1000 to have space to expand easily. - seeds = [b"gameData".as_ref()], - bump, - )] - pub game_data: Account<'info, GameData>, - - #[account(mut)] - pub signer: Signer<'info>, - pub system_program: Program<'info, System>, -} +cd app +pnpm install +pnpm dev ``` -### Chopping trees - -Then whenever the player calls the chop_tree instruction we will check if the player has enough energy and reward him with one wood. +## Minting flow -```rust - #[error_code] - pub enum ErrorCode { - #[msg("Not enough energy")] - NotEnoughEnergy, - } - - pub fn chop_tree(mut ctx: Context) -> Result<()> { - let account = &mut ctx.accounts; - update_energy(account)?; +Creating an NFT this way: - if ctx.accounts.player.energy == 0 { - return err!(ErrorCode::NotEnoughEnergy); - } +1. Create the mint account. +2. Initialize the metadata pointer (must happen *before* initializing the mint). +3. Initialize the mint with 0 decimals. +4. Initialize the metadata extension on the mint itself. +5. Add any custom fields (e.g. `level`). +6. Create the player's Associated Token Account. +7. Mint one token to the ATA. +8. Remove the mint authority β€” irreversible, makes it an NFT. - ctx.accounts.player.wood = ctx.accounts.player.wood + 1; - ctx.accounts.player.energy = ctx.accounts.player.energy - 1; - msg!("You chopped a tree and got 1 log. You have {} wood and {} energy left.", ctx.accounts.player.wood, ctx.accounts.player.energy); - Ok(()) - } -``` +See `programs/extension-nft/src/instructions/mint_nft.rs` for the Rust implementation. -### Calculating the energy +## Energy system (example onchain game) -The interesting part happens in the update_energy function. We check how much time has passed and calculate the energy that the player will have at the given time. -The same thing we will also do in the client. So we basically lazily update the energy instead of polling it all the time. -The is a common technic in game development. +The program includes a simple energy system: a player initializes a `PlayerData` account, then calls `chop_tree` to consume one energy and gain one wood. Energy refills over time, computed lazily from the last-login timestamp. ```rust - -const TIME_TO_REFILL_ENERGY: i64 = 60; +const TIME_TO_REFILL_ENERGY: i64 = 60; // seconds per energy point const MAX_ENERGY: u64 = 10; - -pub fn update_energy(&mut self) -> Result<()> { - // Get the current timestamp - let current_timestamp = Clock::get()?.unix_timestamp; - - // Calculate the time passed since the last login - let mut time_passed: i64 = current_timestamp - self.last_login; - - // Calculate the time spent refilling energy - let mut time_spent = 0; - - while time_passed >= TIME_TO_REFILL_ENERGY && self.energy < MAX_ENERGY { - self.energy += 1; - time_passed -= TIME_TO_REFILL_ENERGY; - time_spent += TIME_TO_REFILL_ENERGY; - } - - if self.energy >= MAX_ENERGY { - self.last_login = current_timestamp; - } else { - self.last_login += time_spent; - } - - Ok(()) -} ``` -## Js client - -### Subscribe to account updates - -It is possible to subscribe to account updates via a websocket. This get updates to this account pushed directly back to the client without the need to poll this data. This allows fast gameplay because the updates usually arrive after around 500ms. - -```js -useEffect(() => { - if (!publicKey) {return;} - const [pda] = PublicKey.findProgramAddressSync( - [Buffer.from("player", "utf8"), - publicKey.toBuffer()], - new PublicKey(ExtensionNft_PROGRAM_ID) - ); - try { - program.account.playerData.fetch(pda).then((data) => { - setGameState(data); - }); - } catch (e) { - window.alert("No player data found, please init!"); - } - - connection.onAccountChange(pda, (account) => { - setGameState(program.coder.accounts.decode("playerData", account.data)); - }); - - }, [publicKey]); +The JS client subscribes to the player account via WebSocket and runs the same energy calculation locally to show a countdown timer. + +## Project structure + +```text +anchor/programs/extension-nft/src/ +β”œβ”€β”€ instructions/ +β”‚ β”œβ”€β”€ chop_tree.rs +β”‚ β”œβ”€β”€ init_player.rs +β”‚ └── update_energy.rs +β”œβ”€β”€ state/ +β”‚ β”œβ”€β”€ game_data.rs +β”‚ β”œβ”€β”€ mod.rs +β”‚ └── player_data.rs +β”œβ”€β”€ constants.rs +β”œβ”€β”€ errors.rs +└── lib.rs ``` -### Calculate energy and show countdown - -In the java script client we can then perform the same logic and show a countdown timer for the player so that he knows when the next energy will be available: - -```js -const interval = setInterval(async () => { - if (gameState == null || gameState.lastLogin == undefined || gameState.energy >= 10) { - return; - } - - const lastLoginTime = gameState.lastLogin * 1000; - const currentTime = Date.now(); - const timePassed = (currentTime - lastLoginTime) / 1000; - - while (timePassed > TIME_TO_REFILL_ENERGY && gameState.energy < MAX_ENERGY) { - gameState.energy++; - gameState.lastLogin += TIME_TO_REFILL_ENERGY; - timePassed -= TIME_TO_REFILL_ENERGY; - } - - setTimePassed(timePassed); - - const nextEnergyIn = Math.floor(TIME_TO_REFILL_ENERGY - timePassed); - setEnergyNextIn(nextEnergyIn > 0 ? nextEnergyIn : 0); - }, 1000); +## Session keys - return () => clearInterval(interval); -}, [gameState, timePassed]); +The example uses [Gum session keys](https://github.com/magicblock-labs/session-keys) to auto-approve transactions: a local keypair is topped up with a small amount of SOL and is allowed to sign specific program instructions for a limited window (currently 23h). When it expires, the SOL is returned and a new session can be created. -... +Neither the program nor the session-keys library has been audited. Use at your own risk. -{(gameState &&
- {("Wood: " + gameState.wood + " Energy: " + gameState.energy + " Next energy in: " + nextEnergyIn )} -
)} - - ``` - -## Unity client - -In the Unity client everything interesting happens in the AnchorService. -To generate the client code you can follow the instructions here: https://solanacookbook.com/gaming/porting-anchor-to-unity.html#generating-the-client - -```bash -cd program -dotnet tool install Solana.Unity.Anchor.Tool <- run once -dotnet anchorgen -i target/idl/extension_nft.json -o target/idl/ExtensionNft.cs -``` +## Building your own -### Session keys +The example was scaffolded with `npx create-solana-game gamName`. -Session keys is an optional component. What it does is creating a local key pair which is toped up with some sol which can be used to autoapprove transactions. The session token is only allowed on certain functions of the program and has an expiry of 23 hours. Then the player will get the sol back and can create a new session. +If you want to start fresh: -With this you can now build any energy based game and even if someone builds a bot for the game the most he can do is play optimally, which maybe even easier to achieve when playing normally depending on the logic of your game. +1. Install [Anchor](https://www.anchor-lang.com/docs/installation). +2. `cd anchor`, then `anchor build` and `anchor deploy`. +3. Copy the printed program ID into `lib.rs`, `Anchor.toml`, and `app/utils/anchor.ts`. +4. Rebuild and redeploy. +5. `cd app && pnpm install && pnpm dev` to run the client. -This game becomes even better when combined with the Token example from Solana Cookbook and you actually drop some spl token to the players. +After changing the program, copy the regenerated IDL types from `target/idl/` into the client so they stay in sync. diff --git a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/Anchor.toml b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/Anchor.toml index 4d5cc7caf..f36d5d2ca 100644 --- a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/Anchor.toml +++ b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/Anchor.toml @@ -6,8 +6,7 @@ seeds = false [programs.localnet] extension_nft = "9aZZ7TJ2fQZxY8hMtWXywp5y6BgqC4N2BPcr9FDT47sW" -[registry] -url = "https://anchor.projectserum.com" +# [registry] section removed β€” no longer used in Anchor 1.0 [provider] cluster = "localnet" diff --git a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/README.md b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/README.md index 8421dcfe6..267c6883a 100644 --- a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/README.md +++ b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/README.md @@ -1,18 +1,15 @@ -# Anchor Solana Program +# Anchor Program -```shell +```bash anchor build anchor deploy ``` -Copy the **program ID** from the output logs; paste it in `Anchor.toml` & `lib.rs`. +Copy the **program ID** from the output logs and paste it into `Anchor.toml` and `lib.rs`. Then rebuild, redeploy, and run the tests: -```shell +```bash anchor build anchor deploy - -yarn install -yarn add ts-mocha - -anchor run test -``` \ No newline at end of file +pnpm install +pnpm test +``` diff --git a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/app/.env.local.example b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/app/.env.local.example new file mode 100644 index 000000000..aada4aa66 --- /dev/null +++ b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/app/.env.local.example @@ -0,0 +1,16 @@ +# Copy this file to `.env.local` and fill in your own values. +# `.env.local` is gitignored by Next.js and must never be committed. + +# RPC endpoint for the Solana connection. +# Defaults to https://rpc.magicblock.app/devnet when unset. +NEXT_PUBLIC_RPC= + +# WebSocket RPC endpoint for the Solana connection. +# Defaults to wss://rpc.magicblock.app/devnet when unset. +NEXT_PUBLIC_WSS_RPC= + +# RPC endpoint that supports the Metaplex DAS read API. Required for the +# read API features in this example. The public Solana RPC works for light +# use; for anything heavier, use a commercial provider like Quicknode +# (https://www.quicknode.com/chains/solana). +NEXT_PUBLIC_METAPLEX_READAPI_RPC= diff --git a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/app/README.md b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/app/README.md index 965a1228c..0fdda906f 100644 --- a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/app/README.md +++ b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/app/README.md @@ -1,38 +1,23 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). +# JS client (Next.js) -## Getting Started +A [Next.js](https://nextjs.org/) app, scaffolded from `create-next-app`. -First, run the development server: +## Running + +Copy `.env.local.example` to `.env.local` and fill in the RPC endpoints. ```bash -npm run dev -# or -yarn dev -# or +cp .env.local.example .env.local +pnpm install pnpm dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. - -[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. - -The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +Open . Editing `pages/index.tsx` reloads the page automatically. -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! +API routes live under `pages/api`. The default route is `pages/api/hello.ts`, reachable at . -## Deploy on Vercel +The app uses `next/font` to optimize and load Inter. -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +## Deploy -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. +The easiest way to deploy a Next.js app is on [Vercel](https://vercel.com/new). See [Next.js deployment docs](https://nextjs.org/docs/deployment) for alternatives. diff --git a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/app/pages/api/hello.ts b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/app/pages/api/hello.ts index e1b0dd0d2..53ec1d1ca 100644 --- a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/app/pages/api/hello.ts +++ b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/app/pages/api/hello.ts @@ -1,4 +1,4 @@ -// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +// Next.js API route support: https://nextjs.org/docs/pages/building-your-application/routing/api-routes import type { NextApiRequest, NextApiResponse } from "next"; type Data = { diff --git a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/app/utils/anchor.ts b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/app/utils/anchor.ts index 8db5bcd23..aaed15bbf 100644 --- a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/app/utils/anchor.ts +++ b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/app/utils/anchor.ts @@ -11,7 +11,8 @@ export const CONNECTION = new WrappedConnection( }, ); -export const METAPLEX_READAPI = "https://devnet.helius-rpc.com/?api-key=78065db3-87fb-431c-8d43-fcd190212125"; +// Reads NEXT_PUBLIC_METAPLEX_READAPI_RPC from .env.local; see .env.local.example for setup. +export const METAPLEX_READAPI = process.env.NEXT_PUBLIC_METAPLEX_READAPI_RPC ?? ""; // Here you can basically use what ever seed you want. For example one per level or city or whatever. export const GAME_DATA_SEED = "level_2"; diff --git a/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/update_fee.rs b/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/update_fee.rs index d6fc541d3..a0f8b81db 100644 --- a/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/update_fee.rs +++ b/tokens/token-extensions/transfer-fee/anchor/programs/transfer-fee/src/instructions/update_fee.rs @@ -12,7 +12,7 @@ pub struct UpdateFee<'info> { // Note that there is a 2 epoch delay from when new fee updates take effect // This is a safely feature built into the extension -// https://github.com/solana-labs/solana-program-library/blob/master/token/program-2022/src/extension/transfer_fee/processor.rs#L92-L109 +// https://github.com/solana-program/token-2022/blob/2d18d97f083627d3f13ce43b16fa4305cbfac4de/program/src/extension/transfer_fee/processor.rs#L92-L109 pub fn handle_process_update_fee( context: Context, transfer_fee_basis_points: u16, diff --git a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/README.md b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/README.md new file mode 100644 index 000000000..43cbb37f1 --- /dev/null +++ b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/README.md @@ -0,0 +1,129 @@ +# Using Token Account Data as a Seed in a Transfer Hook + +Sometimes you want to use account data to derive additional accounts in the extra-account-metas. For example, you might want to use the token account's owner as a seed for a PDA. + +When creating an `ExtraAccountMeta`, the data of any account can be used as an extra seed. In this example we derive a counter account from the token account owner and the literal `"counter"`. The counter records how many times that owner has transferred tokens. + +This is the setup in `extra_account_metas()`: + +```rust +// Define extra account metas to store on the extra_account_meta_list account +impl<'info> InitializeExtraAccountMetaList<'info> { + pub fn extra_account_metas() -> Result> { + Ok(vec![ExtraAccountMeta::new_with_seeds( + &[ + Seed::Literal { bytes: b"counter".to_vec() }, + Seed::AccountData { + account_index: 0, + data_index: 32, + length: 32, + }, + ], + false, // is_signer + true, // is_writable + )?]) + } +} +``` + +The token account layout is what makes `data_index: 32, length: 32` mean "the owner field". Bytes 0..32 are the mint and bytes 32..64 are the owner: + +```rust +/// Token account data. +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct Account { + pub mint: Pubkey, + pub owner: Pubkey, + pub amount: u64, + pub delegate: COption, + pub state: AccountState, + pub is_native: COption, + pub delegated_amount: u64, + pub close_authority: COption, +} +``` + +`account_index: 0` means the source token account, which is always the first account in a transfer hook's accounts array. The second is always the mint; the third is always the destination token account. The order matches the legacy token program. + +Because we derive the counter account from the *sender's* token account owner, we `init` the counter PDA when we initialize the `ExtraAccountMeta` list. Once initialized, the transfer hook increments the counter on every transfer: + +```rust +#[derive(Accounts)] +pub struct InitializeExtraAccountMetaList<'info> { + #[account(mut)] + payer: Signer<'info>, + + /// CHECK: ExtraAccountMetaList account, must use these seeds. + #[account( + init, + seeds = [b"extra-account-metas", mint.key().as_ref()], + bump, + space = ExtraAccountMetaList::size_of( + InitializeExtraAccountMetaList::extra_account_metas()?.len() + )?, + payer = payer, + )] + pub extra_account_meta_list: AccountInfo<'info>, + pub mint: InterfaceAccount<'info, Mint>, + #[account( + init, + seeds = [b"counter", payer.key().as_ref()], + bump, + payer = payer, + space = COUNTER_ACCOUNT_SIZE, + )] + pub counter_account: Account<'info, CounterAccount>, + pub token_program: Program<'info, Token2022>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub system_program: Program<'info, System>, +} +``` + +The counter account also has to appear on the `TransferHook` struct β€” the program needs to know about every account passed in by the runtime: + +```rust +#[derive(Accounts)] +pub struct TransferHook<'info> { + #[account(token::mint = mint, token::authority = owner)] + pub source_token: InterfaceAccount<'info, TokenAccount>, + pub mint: InterfaceAccount<'info, Mint>, + #[account(token::mint = mint)] + pub destination_token: InterfaceAccount<'info, TokenAccount>, + /// CHECK: source token account owner; may be a SystemAccount or a PDA owned by another program. + pub owner: UncheckedAccount<'info>, + /// CHECK: ExtraAccountMetaList account. + #[account(seeds = [b"extra-account-metas", mint.key().as_ref()], bump)] + pub extra_account_meta_list: UncheckedAccount<'info>, + #[account(seeds = [b"counter", owner.key().as_ref()], bump)] + pub counter_account: Account<'info, CounterAccount>, +} +``` + +On the client side, the helper resolves the extra account for you: + +```typescript +const transferInstructionWithHelper = await createTransferCheckedWithTransferHookInstruction( + connection, + sourceTokenAccount, + mint.publicKey, + destinationTokenAccount, + wallet.publicKey, + amountBigInt, + decimals, + [], + "confirmed", + TOKEN_2022_PROGRAM_ID, +); +``` + +If you wanted to derive the counter PDA manually: + +```typescript +const [counterPDA] = PublicKey.findProgramAddressSync( + [Buffer.from("counter"), wallet.publicKey.toBuffer()], + program.programId, +); +``` + +Note: the counter account must exist before a transfer, since the hook reads/writes it. In this example we initialize it alongside the extra-account-metas, so there's only ever one counter β€” the one for the wallet that initialized the metas. If you want a counter per holder, you'd need to expose an opt-in handler to create it (a "sign up for counter" button in your dapp, for example). diff --git a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/readme.md b/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/readme.md deleted file mode 100644 index 5fc1e5217..000000000 --- a/tokens/token-extensions/transfer-hook/account-data-as-seed/anchor/readme.md +++ /dev/null @@ -1,155 +0,0 @@ -# Using token account data in transfer hook - -Sometimes you may want to use account data to derive additional accounts in the -extra account metas. This is useful if, for example, you want to use the token -account's owner as a seed for a PDA. - -When creating the ExtraAccountMeta you can use the data of any account as an -extra seed. In this case we want to derive a counter account from the token -account owner and the string 'counter'. This means we will be always able to see -how often that token account owner has transferred tokens. - -This is how you set it up in the `extra_account_metas()` function. - -```rust -// Define extra account metas to store on extra_account_meta_list account -impl<'info> InitializeExtraAccountMetaList<'info> { - pub fn extra_account_metas() -> Result> { - Ok( - vec![ - ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: b"counter".to_vec(), - }, - Seed::AccountData { account_index: 0, data_index: 32, length: 32 }, - ], - false, // is_signer - true // is_writable - )? - ] - ) - } -} -``` - -Let's look at the token account struct to understand how the account data is -stored. Below is an example of a token account structure. So we can take 32 -bytes at position 32 to 64 as the owner of the token account, which is at -'account_index: 0'. 'account_index` refers to the index of the account in the -accounts array. In the case of a transfer hook, the owner token account is the -first entry in the accounts array. The second account is always the mint and the -third account is the destination token account. This account order is the same -as in the old token program. - -```rust -/// Account data. -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub struct Account { - /// The mint associated with this account - pub mint: Pubkey, - /// The owner of this account. - pub owner: Pubkey, - /// The amount of tokens this account holds. - pub amount: u64, - pub delegate: COption, - pub state: AccountState, - pub is_native: COption, - pub delegated_amount: u64, - pub close_authority: COption, -} -``` - -I our case we want to derive a counter account from the owner of the sender -token account so when we create the ExtraAccountMeta accounts we `init`this PDA -counter account that is derived from the sender token account owner and the -string 'counter'. When the PDA counter account is initialized we will be able to -use it with in the transfer hook to increase it in every transfer. - -```rust -#[derive(Accounts)] -pub struct InitializeExtraAccountMetaList<'info> { - #[account(mut)] - payer: Signer<'info>, - - /// CHECK: ExtraAccountMetaList Account, must use these seeds - #[account( - init, - seeds = [b"extra-account-metas", mint.key().as_ref()], - bump, - space = ExtraAccountMetaList::size_of( - InitializeExtraAccountMetaList::extra_account_metas()?.len() - )?, - payer = payer - )] - pub extra_account_meta_list: AccountInfo<'info>, - pub mint: InterfaceAccount<'info, Mint>, - #[account(init, seeds = [b"counter", payer.key().as_ref()], bump, payer = payer, space = 16)] - pub counter_account: Account<'info, CounterAccount>, - pub token_program: Program<'info, Token2022>, - pub associated_token_program: Program<'info, AssociatedToken>, - pub system_program: Program<'info, System>, -} -``` - -We also need to define this extra counter account in the TransferHook struct. -These are the accounts that are passed to our TransferHook program every time a -transfer is done. The client get the additional accounts from the -ExtraAccountsMetaList PDA but here in the program we still need to define it. - -```rust -#[derive(Accounts)] -pub struct TransferHook<'info> { - #[account(token::mint = mint, token::authority = owner)] - pub source_token: InterfaceAccount<'info, TokenAccount>, - pub mint: InterfaceAccount<'info, Mint>, - #[account(token::mint = mint)] - pub destination_token: InterfaceAccount<'info, TokenAccount>, - /// CHECK: source token account owner, can be SystemAccount or PDA owned by another program - pub owner: UncheckedAccount<'info>, - /// CHECK: ExtraAccountMetaList Account, - #[account(seeds = [b"extra-account-metas", mint.key().as_ref()], bump)] - pub extra_account_meta_list: UncheckedAccount<'info>, - #[account(seeds = [b"counter", owner.key().as_ref()], bump)] - pub counter_account: Account<'info, CounterAccount>, -} -``` - -In the client this account is auto generated and you can use it like this. - -```rust -const transferInstructionWithHelper = -await createTransferCheckedWithTransferHookInstruction( - connection, - sourceTokenAccount, - mint.publicKey, - destinationTokenAccount, - wallet.publicKey, - amountBigInt, - decimals, - [], - "confirmed", - TOKEN_2022_PROGRAM_ID -); -``` - -The helper function is resolving the account automatically from the -ExtraAccounts data account. How the account would be resolved in the client is -like this: - -```js -const [counterPDA] = PublicKey.findProgramAddressSync( - [Buffer.from("counter"), wallet.publicKey.toBuffer()], - program.programId, -); -``` - -Note that the counter account is derived from the owner of the token account and -needs to be initialized before doing a transfer. In the case of this example we -initialize the counter account when we initialize the extra account metas. So we -will only have a counter PDA for the owner of the token account that called that -function. If you want to have a counter account for every token account for your -mint out there you will need to have some functionality to create these PDAs -before hand. There could be a button on your dapp to sign up for a counter that -creates this PDA account and from then on the users can use this counter token. diff --git a/tokens/token-extensions/transfer-hook/allow-block-list-token/README.md b/tokens/token-extensions/transfer-hook/allow-block-list-token/README.md index bb499df42..d17dc5520 100644 --- a/tokens/token-extensions/transfer-hook/allow-block-list-token/README.md +++ b/tokens/token-extensions/transfer-hook/allow-block-list-token/README.md @@ -1,51 +1,41 @@ -# AllowBlockList Token +# Allow/Block-List Token -An example of a allow / block list token using token extensions. +A Token-2022 example that gates transfers through an allow/block list managed by a separate authority. The list is consumed by a transfer hook. -## Features - -Allows the creation of an allow block list with a list authority. -The allow/block list is then consumed by a transfer-hook. +One list authority can manage lists for many mints β€” useful when an issuer wants a third-party-managed list or wants to share a single list across a set of assets. -The list is managed by a single authority and can be used by several token mints. This enables a separation of concerns between token management and allow/block list management, ideal for scenarios where an issuer wants a 3rd party managed allow/block list or wants to share the same list across a group of assets. +## Features +New tokens are created with several configuration options: -Initializes new tokens with several configuration options: - Permanent delegate - Allow list - Block list - Metadata - Authorities -The issuer can configure the allow and block list with 3 distinct configurations: -- Force Allow: requires everyone receiving tokens to be explicitly allowed in -- Block: allows everyone to receive tokens unless explicitly blocked -- Threshold Allow: allows everyone to receive tokens unless explicitly blocked up until a given transfer amount threshold. Transfers larger than the threshold require explicitly allow - -These configurations are saved in the token mint metadata. +The issuer can choose one of three list modes: -This repo includes a UI to manage the allow/block list based on the `legacy-next-tailwind-basic` template. It also allows creating new token mints on the spot with transfer-hook enabled along with token transfers given that most wallets fail to fetch transfer-hook dependencies on devnet and locally. +- **Force Allow:** everyone receiving tokens must be explicitly allow-listed. +- **Block:** anyone can receive tokens unless they're block-listed. +- **Threshold Allow:** anyone can receive tokens unless block-listed *up to* a configurable threshold. Transfers above the threshold require explicit allow-listing. -## Setup - -Install dependencies: -`yarn install` +These configurations are stored in the token mint's metadata. -Compile the program (make sure to replace your program ID): -`anchor build` +The repo includes a UI (based on the `legacy-next-tailwind-basic` template) to manage allow/block lists. It also lets you create transfer-hook-enabled mints and perform transfers, since most wallets don't currently fetch transfer-hook dependencies on devnet or locally. -Compile the UI: -`yarn run build` +## Setup -Serve the UI: -`yarn run dev` +```bash +pnpm install +anchor build # replace your program ID first +pnpm run build # build the UI +pnpm run dev # serve the UI +``` ### Local testing -There are a couple scripts to manage the local validator and deployment. - -To start the local validator and deploy the program (uses the anchor CLI and default anchor keypair): -`./scripts/start.sh` +Scripts manage the local validator and deployment: -To stop the local validator: -`./scripts/stop.sh` \ No newline at end of file +- `./scripts/start.sh` β€” start the local validator and deploy the program (uses the Anchor CLI and the default Anchor keypair). +- `./scripts/stop.sh` β€” stop the local validator. diff --git a/tokens/token-extensions/transfer-hook/pblock-list/README.md b/tokens/token-extensions/transfer-hook/pblock-list/README.md new file mode 100644 index 000000000..6f10a553a --- /dev/null +++ b/tokens/token-extensions/transfer-hook/pblock-list/README.md @@ -0,0 +1,147 @@ +# Block List + +A block-list program that implements the Token-2022 transfer-hook `execute` instruction. + +A central authority maintains a block list β€” a collection of blocked wallets. Token issuers (transfer-hook extension authorities) can wire this program in as their hook and choose an operation mode: filter the source wallet only, or both source and destination. + +## Operation modes + +The mode depends on whether the block list is empty, plus the issuer's choice. Each mode corresponds to a different `extra-account-metas` account built for the mint (see `setup_extra_metas` below). When the list goes from empty to non-empty, the issuer must call `setup_extra_metas` again. + +- **Empty extra metas** β€” default when the config counter is 0. +- **Check source** β€” default when the config counter is > 0. +- **Check both source and destination** β€” optional behavior when the counter is > 0. + +## Accounts + +### `Config` + +- Defines the block-list authority. +- Tracks the number of blocked wallets. + +### `WalletBlock` + +- Marks a single wallet as blocked. + +## Instruction handlers + +### `init` + +Initializes the global `Config` account with an authority. + +### `block_wallet` + +Adds a wallet to the block list, creating a `WalletBlock` record. + +### `unblock_wallet` + +Removes a wallet from the block list, closing its `WalletBlock` record. + +### `setup_extra_metas` + +Sets up the `extra-account-metas` account that the transfer-hook extension depends on. Takes an optional bool to switch operation modes when the counter is non-zero. + +Once wallets are added to the block list, the issuer must call this again to pick one of the blocking modes. + +### `tx_hook` + +The hook invoked during token transfers. + +## Repository layout + +- **Program:** a Pinocchio-based block list under [`pinocchio/program/`](pinocchio/program/). +- **SDKs:** Codama-generated Rust and TypeScript SDKs under [`pinocchio/sdk/`](pinocchio/sdk/). +- **CLI:** a Rust CLI to interact with the program. + +## Building + +All commands below should be run from the [`pinocchio/`](pinocchio/) directory. + +Install dependencies: + +```bash +cd pinocchio +pnpm install +``` + +Build the program: + +```bash +cd program +cargo build-sbf +``` + +Deploy it: + +```bash +solana program deploy --program-id target/deploy/block_list.so +``` + +Generate the SDKs: + +```bash +pnpm run generate-sdks +``` + +Build the CLI: + +```bash +cd cli +cargo build +``` + +## Setup + +### Block list + +Initialize the list and set the authority: + +```bash +target/debug/block-list-cli init +``` + +Add a wallet: + +```bash +target/debug/block-list-cli block-wallet +``` + +Remove a wallet: + +```bash +target/debug/block-list-cli unblock-wallet +``` + +### Token mint + +Create a new mint with the hook wired up: + +```bash +spl-token create-token --program-2022 --transfer-hook BLoCKLSG2qMQ9YxEyrrKKAQzthvW4Lu8Eyv74axF6mf +``` + +Initialize the extra account metas: + +```bash +target/debug/block-list-cli setup-extra-metas +``` + +Switch to checking both source and destination wallets: + +```bash +target/debug/block-list-cli setup-extra-metas --check-both-wallets +``` + +## Devnet deployment + +The program is deployed to devnet at `BLoCKLSG2qMQ9YxEyrrKKAQzthvW4Lu8Eyv74axF6mf`. + +Example transactions: + +- [Empty block list β€” transfer succeeds](https://explorer.solana.com/tx/2EnQD5mFZvrR3EAyFamCfxJDS3yAtZQxNVhFtK46PanCgbX6rpvgcQ961ZAs8H3auawJZPaVZMpAxoj3qZK55mHT?cluster=devnet) +- [Block list checking source only β€” transfer succeeds](https://explorer.solana.com/tx/4pmx31Lx5mXS7FWUtRjAxdRiwKZKCwJv3Du2qGhbLpQUenBuRxRUbrCaGGVjLjeDtpt4AXHzoNex1ppBsmKWSS7r?cluster=devnet) +- [Block list checking both β€” transfer succeeds](https://explorer.solana.com/tx/Q5Bk6GjGQ9TJtwS5zjDKp7GiFZK6efmGNCcxjqcmzf1YoZZJVE3rQkkSgSBNo7tst4hjUX6SJMsmEGXQ2NAdBjF?cluster=devnet) + +## Disclaimer + +This code has not been audited or reviewed. Use at your own discretion. diff --git a/tokens/token-extensions/transfer-hook/pblock-list/readme.md b/tokens/token-extensions/transfer-hook/pblock-list/readme.md deleted file mode 100644 index 926b40195..000000000 --- a/tokens/token-extensions/transfer-hook/pblock-list/readme.md +++ /dev/null @@ -1,146 +0,0 @@ -# Block List - -This is a Block List program that implements the Token2022 Transfer-hook execute instruction. -It allows a centralized authority to defined a block list - a collection of wallets that are blocked. -Token issuers (transfer-hook extension authorities), can then setup this program as the hook to be used and choose an operation mode (either filter source wallet, or both source and destination). - -## Operation Mode - -The Block list has different operation modes depending whether the block list is empty or not and the issuer choice. These modes are achieved by building a different `extra-account-metas` account for the token mint (see `setup_extra_metas` bellow). When the list gets the first blocked wallet, the issuer needs to re-set the `extra-account-metas`. -The modes are the following: -- Empty extra metas - default behaviour when config account counter is 0 -- Check Source - default behaviour when config account counter is above 0 -- Check both source and destination - optional behaviour when config account counter is above 0 - -## Accounts - -### Config -- Defines the block list authority. -- Tracks the number of blocked wallets. - -### WalletBlock -- Defines a wallet as blocked - -## Instructions - -### init - -Initializes the global `Config` account with a given authority to control the block list. - -### block_wallet - -Adds a given wallet address to the blocked wallets. This creates a `WalletBlock` reccord account. - -### unblock_wallet - -Removes a given wallet address from the blocked wallets. This removes a `WalletBlock` reccord account. - -### setup_extra_metas - -Sets up the `extra-account-metas` account dependency for the Transfer-Hook extension. Receives an optional bool value to switch operation modes when the blocked wallet counter is non zero. -Note: once wallets are added to the block list, the issuer needs to call this method again to setup one of the blocking modes. - -### tx_hook - -The hook that is executed during token transfers. - -## Repo contents - -### Smart Contract - -A pinocchio based Block List smart contract under the [pinocchio/program](pinocchio/program/) folder. - -### SDKs - -Codama generated rust and ts [SDKs](pinocchio/sdk/). - -### CLI - -A rust CLI to interact with the contract. - -## Building - -All commands below should be run from the [pinocchio](pinocchio/) directory. - -First install dependencies: -``` -cd pinocchio -pnpm install -``` - -To build the smart contract: -``` -cd program -cargo build-sbf -``` - -To deploy the smart contract: -``` -solana program deploy --program-id target/deploy/block_list.so -``` - -To generate the SDKs: -``` -pnpm run generate-sdks -``` - -To build the CLI: -``` -cd cli -cargo build -``` - -## Setup - -### Block List - -Initialize the block list and defined the authority: -``` -target/debug/block-list-cli init -``` - -Add a wallet to the block list: -``` -target/debug/block-list-cli block-wallet -``` - -Remove a wallet from the block list: -``` -target/debug/block-list-cli unblock-wallet -``` - - -### Token Mint - -Initialize a new token mint: -``` -spl-token create-token --program-2022 --transfer-hook BLoCKLSG2qMQ9YxEyrrKKAQzthvW4Lu8Eyv74axF6mf -``` - -Initialize the extra account metas: -``` -target/debug/block-list-cli setup-extra-metas -``` - -Change the extra account metas to filter both source and destination token account wallets: -``` -target/debug/block-list-cli setup-extra-metas --check-both-wallets -``` - -## Devnet deployment - -Smart contract was deployed to devnet at address `BLoCKLSG2qMQ9YxEyrrKKAQzthvW4Lu8Eyv74axF6mf`. - -Test transfer with empty block list [here](https://explorer.solana.com/tx/2EnQD5mFZvrR3EAyFamCfxJDS3yAtZQxNVhFtK46PanCgbX6rpvgcQ961ZAs8H3auawJZPaVZMpAxoj3qZK55mHT?cluster=devnet&customUrl=http%3A%2F%2Flocalhost%3A8899). - -Test transfer with non empty block list only checking source TA [here](https://explorer.solana.com/tx/4pmx31Lx5mXS7FWUtRjAxdRiwKZKCwJv3Du2qGhbLpQUenBuRxRUbrCaGGVjLjeDtpt4AXHzoNex1ppBsmKWSS7r?cluster=devnet&customUrl=http%3A%2F%2Flocalhost%3A8899). - -Test transfer with non empty block list checking both source and destination TAs [here](https://explorer.solana.com/tx/Q5Bk6GjGQ9TJtwS5zjDKp7GiFZK6efmGNCcxjqcmzf1YoZZJVE3rQkkSgSBNo7tst4hjUX6SJMsmEGXQ2NAdBjF?cluster=devnet&customUrl=http%3A%2F%2Flocalhost%3A8899). - -Simulated transaction that fails due to destination TA owner being blocked [here](https://explorer.solana.com/tx/inspector?cluster=devnet&signatures=%255B%25221111111111111111111111111111111111111111111111111111111111111111%2522%255D&message=AQAHCgqDBmqk%252FDMT5D9rK85EOwBVSTyxwkSJNDGhjodJl5A8fkyFjtMOw8TOzjiallL3mM8ylDy3Dmf4kPO6zjRCB5meTp%252FmYh4SPAIwzTHZRyKqrqiz%252FskDcCP4xKa5KaJaNQKmMSi6syOX%252BagX8jS6oj8o9glIci7jjFsFtVKThVTSAwZGb%252BUhFzL%252F7K26csOb57yM5bvF9xJrLEObOkAAAAC1QoHXoRYodtouw5cKbwI1AuPk%252BVWEpzwvoAzgkyTWD7vvmloKSuwS0IrUHLk7n0Yfp3DOKmgbjiyFpaYfufnS5xfqCyGJ%252BEpC8iKMH9T%252FdgnUADYw6SCHmevlcTztM6TwOn%252FMbMOP4VGXJKhkykzArfWQd9JuJlU%252B0GDnERJVAQbd9uHudY%252FeGEJdvORszdq2GvxNg7kNJ%252F69%252BSjYoYv8sm6yFK1CM9Gp2RvGj6wbHdQmQ4vCDR59WzHPZ5aOHbIDBAAJA9i4BQAAAAAABAAFAkANAwAJCQEIAgAABQcDBgoMAMqaOwAAAAAJ) (press simulate to see logs). - -Simulated transaction that fails due to source TA owner being blocked [here](https://explorer.solana.com/tx/inspector?cluster=devnet&signatures=%255B%25221111111111111111111111111111111111111111111111111111111111111111%2522%255D&message=AQAHCrod5ZzEG06%252BJzr8OnDqiGNK2oQt0Rghykcx3Sw51mE4cZQ%252BDFc%252BtWThZi0XGFuhfdEKDoUp3bkLE8gIYc3DR2N%252BTIWO0w7DxM7OOJqWUveYzzKUPLcOZ%252FiQ87rONEIHmQKmMSi6syOX%252BagX8jS6oj8o9glIci7jjFsFtVKThVTSAwZGb%252BUhFzL%252F7K26csOb57yM5bvF9xJrLEObOkAAAAC1QoHXoRYodtouw5cKbwI1AuPk%252BVWEpzwvoAzgkyTWD7vvmloKSuwS0IrUHLk7n0Yfp3DOKmgbjiyFpaYfufnS8Dp%252FzGzDj%252BFRlySoZMpMwK31kHfSbiZVPtBg5xESVQH3LKeXpXVZHuJ4gl0YZu2j5%252FXT6SUfgp2Znq1tIs7tSwbd9uHudY%252FeGEJdvORszdq2GvxNg7kNJ%252F69%252BSjYoYv8tp02GkX6M1fpsk76QI9ZgGPx%252BxaMNWlOk82JXeuOngcDBAAJA9i4BQAAAAAABAAFAkANAwAJCQEHAgAACAUDBgoMAMqaOwAAAAAJ) (press simulate to see logs). - -## DISCLAIMER - -THIS CODE IS NOT AUDITED NOR REVIEWED. USE AT YOUR OWN DISCRETION. \ No newline at end of file diff --git a/tokens/token-extensions/transfer-hook/whitelist/anchor/README.md b/tokens/token-extensions/transfer-hook/whitelist/anchor/README.md new file mode 100644 index 000000000..4efef0980 --- /dev/null +++ b/tokens/token-extensions/transfer-hook/whitelist/anchor/README.md @@ -0,0 +1,5 @@ +# Transfer Hook β€” Whitelist (Anchor) + +A simple whitelist enforced by a Token-2022 transfer hook. + +This approach doesn't scale to large whitelists: it eventually runs out of account space. A better approach for larger lists is to store entries in external PDAs (one PDA per whitelisted wallet) β€” see the [`pblock-list`](../../pblock-list/) example for that pattern. diff --git a/tokens/token-extensions/transfer-hook/whitelist/anchor/readme.md b/tokens/token-extensions/transfer-hook/whitelist/anchor/readme.md deleted file mode 100644 index da772ae3b..000000000 --- a/tokens/token-extensions/transfer-hook/whitelist/anchor/readme.md +++ /dev/null @@ -1,2 +0,0 @@ -Note that the white list example is not really scalable for big projects since you at some points will run out of account space. -A better approach will be to use external PDAs that store the whitelist instead. \ No newline at end of file diff --git a/tokens/token-fundraiser/anchor/readme.MD b/tokens/token-fundraiser/anchor/README.md similarity index 58% rename from tokens/token-fundraiser/anchor/readme.MD rename to tokens/token-fundraiser/anchor/README.md index 840e41eb8..f0ef8a420 100644 --- a/tokens/token-fundraiser/anchor/readme.MD +++ b/tokens/token-fundraiser/anchor/README.md @@ -1,12 +1,8 @@ # Token Fundraiser -This example demonstrates how to create a fundraiser for SPL Tokens. +Create a fundraiser for SPL Tokens. A user creates a fundraiser account, specifies the mint they want to collect, the target amount, and a duration. Other users contribute. If the target is reached, the maker can claim the funds; if it isn't reached within the duration, contributors can refund. -In this example, a user will be able to create a fundraiser account, where he will be specify the mint he wants to collect and the fundraising target. - ---- - -## Let's walk through the architecture: +## Architecture A fundraising account consists of: @@ -40,11 +36,11 @@ pub struct Fundraiser { - bump: since our Fundraiser account will be a PDA (Program Derived Address), we will store the bump of the account -We use InitSpace derive macro to implement the space triat that will calculate the amount of space that our account will use on-chain (without taking the anchor discriminator into consideration) +The `InitSpace` derive macro implements the `Space` trait, which calculates the size of the account (not counting the Anchor discriminator). ---- +### Creating a Fundraiser -### The user will be able to create new Fundraiser accounts. For that, we create the following context: +Users create Fundraiser accounts via this context: ```rust #[derive(Accounts)] @@ -73,22 +69,16 @@ pub struct Initialize<'info> { } ``` -LetΒ΄s have a closer look at the accounts that we are passing in this context: - -- maker: will be the person starting the fundraising. He will be a signer of the transaction, and we mark his account as mutable as we will be deducting lamports from this account - -- mint_to_raise: The mint that the user wants to receive. This will be a Mint Account, that we will use to store the mint address - -- fundraiser: will be the state account that we will initialize and the maker will be paying for the initialization of the account. -We derive the Fundraiser PDA from the byte representation of the word "fundraiser" and the reference of the maker public key. Anchor will calculate the canonical bump (the first bump that throws that address out of the ed25519 eliptic curve) and save it for us in a struct +Account breakdown: -- vault: We will initialize a vault (ATA) to receive the contributions. This account will be derived from the mint that the user wants to receive, and the fundraiser account that we are just creating +- `maker`: the person starting the fundraiser. Signs the transaction; mutable so we can deduct lamports from it. +- `mint_to_raise`: the mint the maker wants to receive. +- `fundraiser`: the state account being initialized. The Fundraiser PDA is derived from `b"fundraiser"` and the maker's public key; Anchor calculates the canonical bump and stores it in the struct. +- `vault`: the ATA that receives contributions, derived from `mint_to_raise` and the Fundraiser account. +- `system_program`: initializes new accounts. +- `token_program`, `associated_token_program`: create new ATAs. -- system_program: Program resposible for the initialization of any new account - -- token_program and associated_token_program: We are creating new ATAs - -### We then implement some functionality for our Initialize context: +### Implementation for `Initialize` ```rust impl<'info> Initialize<'info> { @@ -116,11 +106,9 @@ impl<'info> Initialize<'info> { } ``` -In here, we basically just set the data of our Fundraiser account if the amount to raise is bigger than 3 (minimum amount) +Set the data on the Fundraiser account if the target amount meets the minimum. ---- - -### Users will be able to contribute to a fundraising +### Contributing A contribution account consists of: @@ -132,7 +120,7 @@ pub struct Contributor { } ```rust -In this account we will only store the total amount contributed by a specific contributor +Stores the total amount contributed by a specific contributor. #[derive(Accounts)] pub struct Contribute<'info> { @@ -171,23 +159,17 @@ pub struct Contribute<'info> { } ``` -In this context, we are passing all the accounts needed to contribute to a fundraising campaign: - -- contributor: The address of the person that is contributing +Account breakdown: -- mint_to_raise: the mint that the maker is expecting to receive as contributions +- `contributor`: the contributor. +- `mint_to_raise`: the mint being collected. +- `fundraiser`: an initialized Fundraiser account; constraints check the mint, seeds, and bump. +- `contributor_account`: initialized if needed; tracks the contributor's running total. +- `contributor_ata`: the ATA tokens are transferred *from*. Mint and authority are checked; mutable. +- `vault`: the ATA tokens are transferred *to*. Mint and authority are checked; mutable. +- `token_program`: used for token transfers. -- fundraiser: An initialized Fundraiser account where appropriate checks will be performed, such as the appropriate mint, the seeds and the bump of the Fundraiser PDA - -- contributor account: We initialize (if needed) a contributor account that will store the total amount that a specific contributor has contributed with so far - -- contributor_ata: The ata where we will be transfering tokens from. We make sure that the authority and mint of the ATA are correct (mint_to_raise and contributor address), and we mark it as mutable since we will be deducting tokens from that account - -- vault: The ata where we will be depositing tokens to. We make sure that the authority and mint of the ATA are correct (mint_to_raise and Fundraiser account), and we mark it as mutable since we will be depositing tokens in that account - -- token_program: We will performing CPIs (Cross Program Invocations) to the token program to transfer tokens - -### We then implement some functionality for our Contribute context: +### Implementation for `Contribute` ```rust impl<'info> Contribute<'info> { @@ -245,23 +227,16 @@ impl<'info> Contribute<'info> { } } ``` -In here, we make some checks: -- We check that the user is depositing at least one token - -- We Check that the user is not contributing with more than 10% of the target amount - -- We check that the total contributions of the user do not exceed a total of 10% of the target amount - -- We check that the fundraising duration has not elapsed +Checks performed: -After, we create a CPI to the token program, to transfer a certain amount of SPL tokens from the Contributor ATA to the vault. -We pass the authority of the account where the tokens are being deducted from (In this case is the contributor, as he is the authority of the contributor ata). +- Contribution is at least one token. +- Contribution is at most 10% of the target. +- Total contribution from this contributor doesn't exceed 10% of the target. +- Fundraising duration has not elapsed. -Lastly, we update our state acounts with the right amounts +A CPI to the token program transfers tokens from the contributor's ATA to the vault. The contributor signs (they own the source ATA). Finally, state accounts are updated. ---- - -### User will be able to claim the tokens once the fundraising target has been reached +### Claiming ```rust #[derive(Accounts)] @@ -295,24 +270,17 @@ pub struct CheckContributions<'info> { } ``` -In this context, we are passing all the accounts needed for a user to claim the raised tokens: - -- maker: The address of the person raising the the funds. We mark it as mutable since the maker will be paying for initialization fees and will receive lamports from rent back - -- mint_to_raise: the mint that the maker is expecting to receive as contributions - -- fundraiser: An initialized Fundraiser account where appropriate checks will be performed, such as the appropriate mint, the seeds and the bump of the Fundraiser PDA +Account breakdown: -- vault: The ata where we will be transfering tokens from. We make sure that the authority and mint of the ATA are correct (mint_to_raise and fundraiser account), and we mark it as mutable since we will be deducting tokens from that account +- `maker`: the fundraiser owner. Mutable; pays initialization fees and receives rent back when the Fundraiser account closes. +- `mint_to_raise`: the mint being collected. +- `fundraiser`: the initialized Fundraiser account. +- `vault`: the ATA tokens are transferred *from*. +- `maker_ata`: the ATA tokens are transferred *to*. Initialized if needed (the maker pays). +- `system_program`, `associated_token_program`: needed to initialize the maker's ATA if necessary. +- `token_program`: used for the transfer. -- maker_ata: The ata where we will be depositing tokens to. We make sure that the authority and mint of the ATA are correct (mint_to_raise and maker account), and we mark it as mutable since we will be depositing tokens in that account. -In case we need to initialize this ATA, the maker will be paying for the initialization fees - -- system_program and associated_token_program: Since we are initializing new ATAs - -- token_program: We will performing CPIs (Cross Program Invocations) to the token program to transfer tokens - -### We then implement some functionality for our Contribute context: +### Implementation for `CheckContributions` ```rust impl<'info> CheckContributions<'info> { @@ -353,15 +321,11 @@ impl<'info> CheckContributions<'info> { } ``` -In this implementation, we check if the amount of tokens in the vault is equal or bigger then the fundraising campaign target. -If it is, then we perform a CPI to the token program to transfer the funds from the vault to the maker ATA. Since the vault is an ATA, we need to create our CPI context with a signer and use the seeds and bump from the PDA (We are signing with our program on behalf of that PDA). - -Finally, we close our Fundraiser account and send the lamports from the rent back to the maker (done with the "close" constraint int the Fundraiser account). +Check the vault holds at least the target amount; if so, CPI into the token program to transfer the vault's balance to the maker's ATA. The vault is owned by the Fundraiser PDA, so the CPI uses `new_with_signer` with the PDA seeds. +Finally, the Fundraiser account is closed (via the `close` constraint) and its rent is refunded to the maker. ---- - -### Users will be able to refund their contributions, if the duration of the fundraising has elapsed and the target has not reached +### Refunding ```rust #[derive(Accounts)] @@ -401,25 +365,18 @@ pub struct Refund<'info> { } ``` -In this context, we are passing all the accounts needed for a contributor to refund their tokens: - -- contributor: The address of the person that is contributing - -- maker: The address of the person raising the the funds. - -- mint_to_raise: the mint that the maker is expecting to receive as contributions - -- fundraiser: An initialized Fundraiser account where appropriate checks will be performed, such as the appropriate mint, the seeds and the bump of the Fundraiser PDA - -- contributor account: An initialized Contributor account that will store the total amount that a specific contributor has contributed with so far - -- contributor_ata: The ata where we will be transfering tokens to. We make sure that the authority and mint of the ATA are correct (mint_to_raise and contributor address), and we mark it as mutable since we will be depositing tokens to that account - -- vault: The ata where we will be withdrawing tokens from. We make sure that the authority and mint of the ATA are correct (mint_to_raise and Fundraiser account), and we mark it as mutable since we will be withdrawing tokens from that account +Account breakdown: -- token_program: We will performing CPIs (Cross Program Invocations) to the token program to transfer tokens +- `contributor`: the contributor being refunded. +- `maker`: the fundraiser owner. +- `mint_to_raise`: the mint being collected. +- `fundraiser`: the Fundraiser account. +- `contributor_account`: the Contributor account. +- `contributor_ata`: the ATA the refund goes *to*. +- `vault`: the ATA the refund comes *from*. +- `token_program`: used for the transfer. -### We then implement some functionality for our Refund context: +### Implementation for `Refund` ```rust impl<'info> Refund<'info> { @@ -470,5 +427,4 @@ impl<'info> Refund<'info> { } ``` -In here, we will check if the fundrasing has already met the target and if it passed the duration time. -After doing the proper checks, we transfer the donated funds from the vault back to the contributor +Verify the fundraising duration has elapsed and the target was not met, then transfer the contributor's tokens from the vault back to their ATA. diff --git a/tokens/token-swap/README.md b/tokens/token-swap/README.md index 80ab1a67c..11d094a12 100644 --- a/tokens/token-swap/README.md +++ b/tokens/token-swap/README.md @@ -1,73 +1,43 @@ -## Token swap example amm in anchor rust +# Token Swap (AMM) -**Automated Market Makers (AMM)** - Your Gateway to Effortless Trading! -Welcome to the world of Automated Market Makers (AMM), where seamless trading is made possible with the power of automation. The primary goal of AMMs is to act as automatic buyers and sellers, readily available whenever users wish to trade their assets. +A Constant Product Automated Market Maker (AMM) in Anchor β€” the model popularized by Uniswap V2. -**Advantages of AMMs:** +The pool keeps `x * y = K` invariant: if `x` is the reserve of token A and `y` is the reserve of token B, then `x * y` stays constant for a given liquidity quantity. -- Always Available Trading: Thanks to the algorithmic trading, AMMs are operational round-the-clock, ensuring you never miss a trading opportunity. +## Why a CPAMM -- Low Operational Costs: Embrace cheaper trades as AMMs eliminate the need for a market-making firm. Say goodbye to hefty fees! (In practice, MEV bots handle this role.) +Other bonding-curve designs exist: -Selecting the right algorithm for the AMM becomes the essential task. One fascinating development in blockchain AMMs is the Constant Function AMM (CFAMM), which permits trades that preserve a predefined condition on a constant function of the AMM's reserves, known as the Invariant. This enforcement compels the reserves to evolve along a remarkable Bonding Curve. +- **Constant Sum AMM (CSAMM):** `x + y = K`. Constant price but reserves can be drained. +- **Curve Stableswap:** a mix of CSAMM and CPAMM, tuned for like-priced assets. +- **Uniswap V3 Concentrated Liquidity AMM (CLAMM):** splits the curve into buckets; LPs supply liquidity to specific price ranges. +- **Trader Joe CLAMM:** like Uniswap V3, but each bucket is a CSAMM. -Meet the Constant Product AMM (CPAMM): Among the simplest CFAMMs and made popular by Uniswap V2, the CPAMM ensures the product of both reserves (xy) remains constant (K) for a given liquidity quantity. Simply put, if x denotes the reserve of token A and y denotes the reserve of token B, then xy = K, with K depending on the liquidity. +A CPAMM is the simplest and the cheapest to keep in account state β€” one pool, one mint, easy to reason about. That's what this example implements. -*Discover Diverse Bonding Curves:* +## Design -- Constant Sum AMM (CSAMM): The pool's invariant, x + y = K, maintains a constant price, but reserves for each asset can be emptied. +Requirements: -- Curve's Stableswap: A clever mix of CSAMM and CPAMM, the Stableswap brings unique properties to the AMM, depending on the token balance. +- **Fee distribution.** Every pool charges a trading fee, paid in the traded token, that rewards LPs. To stay consistent across pools, the fee is shared. +- **Single pool per asset pair.** Avoids liquidity fragmentation. +- **LP accounting.** The program tracks each LP's deposits. -- Uniswap V3 Concentrated Liquidity AMM (CLAMM): Utilizing CPAMM, this model splits the curve into independent buckets, allowing liquidity provision to specific price buckets for efficient trading. +Implementation choices: -- Trader Joe CLAMM: Similar to UniV3 CLAMM, it divides the price range into buckets, where each bucket operates as a CSAMM instead of a CPAMM. +- **Shared parameters.** A single AMM account stores the shared trading-fee config and admin. Each pool then has its own account. +- **Unique pools.** Each pool is a PDA seeded from the AMM, `mint_a`, and `mint_b` (in that order, with `mint_a < mint_b`). +- **LP accounting via SPL Token.** The LP positions are tracked as SPL Tokens (the `mint_liquidity` mint), so they're composable with any wallet or downstream protocol. -*The Undeniable Perks of CPAMMs:* +## Onchain-design principles applied here -- Easier to Understand and Use: Unlike complex liquidity buckets, CPAMMs offer a single, user-friendly pool for straightforward trading. +- **Store keys in the account.** Even for PDAs, storing the parent keys in the account state makes lookups easier (you can rebuild the PDA without consulting external data) and works well with Anchor's `has_one` constraint. +- **Keep seeds simple.** Start with the parent's seeds, then the current object's identifiers in alphabetical order. For the pool, that means `[amm, mint_a, mint_b]`. +- **Keep instruction scope small.** Smaller instructions touch fewer accounts, leaving room in the transaction and improving composability and security. -- Memory Efficiency: With just one pool to maintain instead of multiple buckets, CPAMMs are incredibly memory-efficient, leading to lower memory usage and reduced costs. +## File structure -For these reasons, we focus on implementing the CPAMM. - -## Program Implementation - -### Design - -Let's go over the essential requirements for our smart contract design: - -- Fee Distribution: Every pool must have a fee to reward Liquidity Providers (LPs). This fee is charged on trades and paid directly in the traded token. To maintain consistency across all pools, the fees will be shared. - -- Single Pool per Asset Pair: Each asset pair will have precisely one pool. This approach avoids liquidity fragmentation and simplifies the process for developers to locate the appropriate pool. - -- LPs Deposit Accounting: We need to keep track of LPs deposits in the smart contract. - -To achieve an efficient and organized design, we can implement the following strategies: - -- Shared Parameters: As pools can share certain parameters like the trading fee, we can create a single account to store these shared parameters for all pools. Additionally, each pool will have its separate account. This approach saves storage space, except when the configuration is smaller than 32 bytes due to the need to store the public key. In our case, we'll include an admin for the AMM to control fees, which exceeds the limit. - -- Unique Pool Identification: To ensure each pool remains unique, we'll utilize seeds to generate a Program Derived Account (PDA). This helps avoid any ambiguity or confusion. - -- SPL Token for Liquidity Accounting: We'll utilize the SPL token standard for liquidity accounting. This choice ensures easy composability and simplifies the handling of liquidity in the contract. - -By implementing these strategies, we are creating a solana program that efficiently manages liquidity pools, rewards LPs, and maintains a seamless trading experience across various asset pairs. - -## Principals - -Here are some essential principles to consider when building onchain programs in Solana: - -- Store Keys in the Account: It's beneficial to store keys in the account when creating Program Derived Accounts (PDAs) using seeds. While this may increase account rent slightly, it offers significant advantages. By having all the necessary keys in the account, it becomes effortless to locate the account (since you can recreate its public key). Additionally, this approach works seamlessly with Anchor's has_one clause, streamlining the process. - -- Simplicity in Seeds: When creating PDA seeds, prioritize simplicity. Using a straightforward logic for seeds makes it easier to remember and clarifies the relationship between accounts. A logical approach is to first include the seeds of the parent account and then use the current object's identifiers, preferably in alphabetical order. For example, in an AMM account storing configuration (with no parent), adding an identifier attribute, usually a pubkey, becomes necessary since the admin can change. For pools, which have the AMM as a parent and are uniquely defined by the tokens they facilitate trades for, it's advisable to use the AMM's pubkey as the seed, followed by token A's pubkey and then token B's. - -- Minimize Instruction's Scope: Keeping each instruction's scope as small as possible is crucial for several reasons. It helps reduce transaction size by limiting the number of accounts touched simultaneously. Moreover, it enhances composability, readability, and security. However, a trade-off to consider is that it may lead to an increase in Lines Of Code (LOC). - -- By following these principles, you can build onchain programs in Solana that are efficient, well-organized, and conducive to seamless interactions, ensuring a robust foundation for your blockchain projects. - -## Code Examples - -```file structure +```text programs/token-swap/src/ β”œβ”€β”€ constants.rs β”œβ”€β”€ errors.rs @@ -82,251 +52,51 @@ programs/token-swap/src/ └── state.rs ``` +## State -1. **Entrypoint** - -This code is entrypoint for a swap example using the **`anchor_lang`** library. The **`anchor_lang`** library provides tools for creating Solana programs using the Anchor framework. The code defines several functions: - -(https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/lib.rs#L1-L8) - -The above section contains the necessary imports and module declarations for the program. It imports modules from the anchor_lang library and declares local modules for the crate. The pub use instructions::*; re-exports all items from the instructions module so that they can be accessed from outside this module. - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/lib.rs#L10-L11 - -This macro declares the program ID and associates it with the given string. This ID should match the deployed Solana program's ID to ensure the correct program is invoked when interacting with the smart contract. - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/lib.rs#L13-L45 - -This section defines the program module using the **`#[program]`** attribute. Each function in this module represents an entry point to the smart contract. Each entry point function takes a **`Context`** parameter, which provides essential information for executing the function, such as the accounts involved and the transaction context. - -The entry point functions call their respective functions from the **`instructions`** module, passing the required arguments. - -Overall, this code defines a Rust module for a Solana program using the Anchor framework. The program supports functions related to creating an Automated Market Maker (AMM) and interacting with it, such as creating a pool, depositing liquidity, withdrawing liquidity, and swapping tokens using an AMM mechanism. - -2. **Account Definitions** - -Let's embark on our exploration by charting the course for our accounts. Each account will be thoughtfully defined, beginning with their keys arranged in the precise order they will appear in the seeds. Following the keys, we'll list the attributes that are utilized for each account. As we journey through this process, we'll unravel the intricate web of connections and forge a path towards a cohesive and well-structured design. Let the exploration begin! - -The above code declares an account structure called **`Amm`**. The **`#[account]`** attribute indicates that this structure will be used as an account on the Solana blockchain. The **`#[derive(Default)]`** attribute automatically generates a default implementation of the struct with all fields set to their default values. - -The **`Amm`** struct has three fields: - -1. **`id`**: The primary key of the AMM, represented as a **`Pubkey`**. -2. **`admin`**: The account that has admin authority over the AMM, represented as a **`Pubkey`**. -3. **`fee`**: The LP fee taken on each trade, represented as a **`u16`** (unsigned 16-bit integer) in basis points. - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/state.rs#L1-L14 - - -The above code declares an account structure called Amm. The #[account] attribute indicates that this structure will be used as an account on the Solana blockchain. The #[derive(Default)] attribute automatically generates a default implementation of the struct with all fields set to their default values. - - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/state.rs#L16-L18 - -This code implements a constant LEN for the Amm struct, which represents the size of the Amm account in bytes. The size is calculated by adding the sizes of the individual fields (id, admin, and fee). For example, Pubkey has a fixed size of 32 bytes, and u16 has a size of 2 bytes. - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/state.rs#L20-L31 - -The code declares another account structure called **`Pool`**. As before, the **`#[account]`** attribute indicates that this struct will be used as an account on the Solana blockchain, and the **`#[derive(Default)]`** attribute generates a default implementation with all fields set to their default values. - -The **`Pool`** struct has three fields: - -1. **`amm`**: The primary key of the AMM (Automated Market Maker) that this pool belongs to, represented as a **`Pubkey`**. -2. **`mint_a`**: The mint of token A associated with this pool, represented as a **`Pubkey`**. -3. **`mint_b`**: The mint of token B associated with this pool, represented as a **`Pubkey`**. - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/state.rs#L33-L35 - -This code implements a constant LEN for the Pool struct, which represents the size of the Pool account in bytes. Similar to the Amm struct, the size is calculated by adding the sizes of the individual fields (amm, mint_a, and mint_b). Each Pubkey has a size of 32 bytes, and the total size is 8 bytes (for padding) + 32 bytes (amm) + 32 bytes (mint_a) + 32 bytes (mint_b) = 104 bytes. - -3. **Instructions** - - 3.1 **create amm** - - https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/create_amm.rs#L1-L12 - - The above code defines a function named **`create_amm`** that is used to create an AMM account. It takes four parameters: - -1. **`ctx`**: The **`Context`** parameter contains the context data required to execute the function. -2. **`id`**: The **`Pubkey`** parameter represents the ID for the new AMM account. -3. **`fee`**: The **`u16`** parameter represents the LP fee (in basis points) to be set for the new AMM account. - -The function does the following: - -- It gets a mutable reference to the AMM account from the context using **`let amm = &mut ctx.accounts.amm;`**. -- It sets the fields of the AMM account with the provided values using **`amm.id = id;`**, **`amm.admin = ctx.accounts.admin.key();`**, and **`amm.fee = fee;`**. -- It returns **`Ok(())`** to indicate the success of the operation. - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/create_amm.rs#L14-L39 - -This code defines a struct **`CreateAmm`** using the **`Accounts`** attribute, which serves as the accounts instruction for the **`create_amm`** function. - -The **`CreateAmm`** struct has four fields: - -1. **`amm`**: An account field marked with **`init`** attribute, which represents the AMM account to be created. It uses the provided **`id`** as a seed to derive the account address, sets the required space for the account using **`Amm::LEN`**, and uses the **`payer`** account for paying rent. Additionally, it specifies a constraint to ensure that the fee is less than 10000 basis points; otherwise, it will raise the error **`TutorialError::InvalidFee`**. -2. **`admin`**: An **`AccountInfo`** field representing the admin account for the AMM. It is read-only and not mutable. -3. **`payer`**: A **`Signer`** field representing the account that pays for the rent of the AMM account. It is marked as mutable. -4. **`system_program`**: A **`Program`** field representing the Solana system program, used for certain system operations. - -TLDR-, this code sets up the instruction structure for the **`create_amm`** function, defining how the accounts should be initialized, accessed, and used when calling the function. - - 3.2 **create pool** - - https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/create_pool.rs#L1-L20 - - The above code defines a function named **`create_pool`** that creates a liquidity pool. It takes a single parameter, **`ctx`**, which represents the **`Context`** used to execute the function. - -The function does the following: - -- It gets a mutable reference to the **`Pool`** account from the context using **`let pool = &mut ctx.accounts.pool;`**. -- It sets the fields of the **`Pool`** account with the keys of the associated accounts using **`pool.amm = ctx.accounts.amm.key();`**, **`pool.mint_a = ctx.accounts.mint_a.key();`**, and **`pool.mint_b = ctx.accounts.mint_b.key();`**. -- It returns **`Ok(())`** to indicate the success of the operation. - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/create_pool.rs#L22-L101 - -This code defines a struct named **`CreatePool`**, which serves as the accounts instruction for the **`create_pool`** function. - -The **`CreatePool`** struct has several fields, each representing an account that the **`create_pool`** function needs to access during its execution. The attributes applied to each field define the behavior of how the accounts are accessed and handled. - -Here's an explanation of each field: - -1. **`amm`**: An account field representing the AMM (Automated Market Maker) associated with the pool. It derives the address of the account using the seed of the AMM account. -2. **`pool`**: An account field that will be initialized as the new liquidity pool account. It specifies the required space for the account, derives the address using seeds derived from the AMM, and ensures that **`mint_a`**'s key is less than **`mint_b`**'s key (assumes lexicographic order) to prevent invalid creation of the pool. -3. **`pool_authority`**: An account info field representing the read-only authority account for the pool. It is used as a seed to derive the address of the **`mint_liquidity`** account. -4. **`mint_liquidity`**: A boxed account field representing the mint for the liquidity tokens (LP tokens) of the pool. It is initialized with the provided authority and has a fixed decimal precision of 6. -5. **`mint_a`** and **`mint_b`**: Boxed account fields representing the mints for token A and token B, respectively. -6. **`pool_account_a`** and **`pool_account_b`**: Boxed account fields representing the associated token accounts for token A and token B, respectively, for the pool. These accounts are associated with their respective mints and have **`pool_authority`** as their authority. -7. **`payer`**: A signer field representing the account that pays for the rent of the new accounts. -8. **`token_program`**, **`associated_token_program`**, and **`system_program`**: Program fields representing the Solana token program, associated token program, and system program, respectively. - -TLDR, this code defines the accounts instruction structure for the **`create_pool`** function, specifying how the accounts should be initialized, accessed, and used when calling the function. - - 3.3 **deposite liquidity** - - https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs#L1-L30 - -The above code defines a function named **`deposit_liquidity`** that allows depositing liquidity into the pool. It takes three parameters: - -1. **`ctx`**: The **`Context`** parameter contains the context data required to execute the function. -2. **`amount_a`**: The **`u64`** parameter represents the amount of token A to be deposited. -3. **`amount_b`**: The **`u64`** parameter represents the amount of token B to be deposited. - -The function does the following: - -- It checks if the depositor has enough tokens for each type (A and B) before depositing and restricts the amounts to the available balances using the **`if`** conditions. - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs#L32-L61 - -This code ensures that the amounts of tokens A and B being deposited are provided in the same proportion as the existing liquidity in the pool. If this is the first deposit (pool creation), the amounts are added as is. Otherwise, the function calculates the ratio of the existing liquidity (pool_a.amount and pool_b.amount) and adjusts the amounts being deposited accordingly. - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs#L63-L77 - -This code calculates the amount of liquidity that is about to be deposited into the pool. It calculates the square root of the product of **`amount_a`** and **`amount_b`**, using fixed-point arithmetic to ensure precision. - -If this is the first deposit (pool creation), the function checks if the calculated liquidity is greater than the **`MINIMUM_LIQUIDITY`** constant (a minimum liquidity required for the pool). If it's not, the function returns an error to indicate that the deposit is too small. Additionally, it subtracts the **`MINIMUM_LIQUIDITY`** from the calculated liquidity to lock it as the initial liquidity. - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs#L79-L101 - -This code uses the token::transfer function from the Anchor SPL token crate to transfer the deposited amounts of tokens A and B from the depositor's accounts (depositor_account_a and depositor_account_b, respectively) to the pool's accounts (pool_account_a and pool_account_b, respectively). It does this through cross-program invocation (CPI) using the token program, and the authority for the transfer is the depositor. - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/deposit_liquidity.rs#L102-L124 - -This code uses the **`token::mint_to`** function from the Anchor SPL token crate to mint the liquidity tokens to the depositor. It does this through cross-program invocation (CPI) using the token program. The minting is authorized by the pool authority (**`pool_authority`**). - -The function calculates the correct authority bump, as required by the SPL token program, and creates the necessary seeds for the authority. It then uses the **`CpiContext::new_with_signer`** function to set up the context for the CPI with the correct authority. - -TRDR, this code implements the logic to deposit liquidity into the pool, ensuring correct proportions, handling the initial pool creation, and minting the corresponding liquidity tokens to the depositor. - - 3.4 **swap exact tokens** - - https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/swap_exact_tokens_for_tokens.rs#L1-L27 - - This code defines a function named **`swap_exact_tokens_for_tokens`** that allows swapping tokens A for tokens B (and vice versa) in the AMM pool. It takes five parameters: - -1. **`ctx`**: The **`Context`** parameter contains the context data required to execute the function. -2. **`swap_a`**: The **`bool`** parameter indicates whether tokens A should be swapped for tokens B (**`true`**) or tokens B should be swapped for tokens A (**`false`**). -3. **`input_amount`**: The **`u64`** parameter represents the amount of tokens to be swapped. -4. **`min_output_amount`**: The **`u64`** parameter represents the minimum expected output amount after the swap. - -The function does the following: - -- It checks if the trader has enough tokens for the input amount of the specified token (**`swap_a`**) before proceeding with the swap. If the trader doesn't have enough tokens, it uses the available amount for the swap. - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/swap_exact_tokens_for_tokens.rs#L29-L31 - -This code applies the trading fee to the input amount (input) based on the amm (AMM) account's fee value. The trading fee is subtracted from the input amount to calculate the taxed_input, which is the actual amount of tokens available for the swap after deducting the fee. - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/swap_exact_tokens_for_tokens.rs#L33-L56 - -This code calculates the output amount of the swapped token based on the taxed_input, current pool balances (pool_a.amount and pool_b.amount), and whether the swap is from token A to token B or vice versa. It uses fixed-point arithmetic to ensure precise calculations. The resulting output represents the amount of tokens the trader will receive after the swap. - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/swap_exact_tokens_for_tokens.rs#L58-L60 - -This code checks if the calculated **`output`** is less than the specified **`min_output_amount`**. If so, it returns an error, indicating that the output amount is too small. - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/swap_exact_tokens_for_tokens.rs#L62-L63 - -This code calculates the invariant of the pool, which is the product of the current balances of token A (**`pool_a.amount`**) and token B (**`pool_b.amount`**). - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/swap_exact_tokens_for_tokens.rs#L65-L123 - -This code transfers the input and output amounts of tokens between the trader and the pool, performing the token swap. It uses the **`token::transfer`** function from the Anchor SPL token crate to transfer tokens from one account to another. The **`CpiContext`** is used for Cross-Program Invocation (CPI) to interact with the SPL token program. - -The code chooses the appropriate token accounts to perform the transfer based on whether the swap is from token A to token B or vice versa (**`swap_a`**). The transfer authority is specified as either **`trader`** or **`pool_authority`** based on the situation. - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/swap_exact_tokens_for_tokens.rs#L124-L130 - -This code logs a message indicating the details of the trade, including the input amount, the taxed input amount, and the output amount. - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/swap_exact_tokens_for_tokens.rs#L132-L141 - -This code reloads the pool token accounts (pool_account_a and pool_account_b) to get the updated balances after the swap. It then checks if the invariant still holds, ensuring that the product of the balances remains constant. If the invariant is violated, it returns an error. -Finally, this code returns Ok(()) if all operations in the function executed successfully. - - 3.5 **withdraw liquidity** - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/withdraw_liquidity.rs#L1-L11 - -The use statements import required modules and types for the function. - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/withdraw_liquidity.rs#L13 - -This code defines a function named **`withdraw_liquidity`** that allows a liquidity provider to withdraw their liquidity from the AMM pool. It takes two parameters: - -1. **`ctx`**: The **`Context`** parameter contains the context data required to execute the function. -2. **`amount`**: The **`u64`** parameter represents the amount of liquidity tokens the provider wants to withdraw. - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/withdraw_liquidity.rs#L14-L22 - -This code sets up the authority seeds and signer seeds required for performing token transfers and burning the liquidity tokens. The authority seeds include the AMM ID, mint keys of tokens A and B, the authority seed constant, and the authority bump seed. The signer seeds are derived from the authority seeds. - -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/withdraw_liquidity.rs#L24-L45 +### `Amm` -This code calculates the amount of token A to be transferred to the liquidity provider by performing the following steps: +- `id: Pubkey` β€” the primary key of the AMM (used as a seed). +- `admin: Pubkey` β€” the admin authority. +- `fee: u16` β€” LP fee in basis points (must be < 10000). -1. Calculate the ratio of the amount of liquidity tokens being withdrawn (**`amount`**) to the total supply of liquidity tokens (**`ctx.accounts.mint_liquidity.supply + MINIMUM_LIQUIDITY`**). -2. Calculate the proportional amount of token A based on the pool's token A balance (**`ctx.accounts.pool_account_a.amount`**). -3. Transfer the calculated amount of token A from the pool account to the liquidity provider's account using the **`token::transfer`** function. +### `Pool` -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/withdraw_liquidity.rs#L47-L67 +- `amm: Pubkey` β€” the parent AMM. +- `mint_a: Pubkey` β€” mint of token A. +- `mint_b: Pubkey` β€” mint of token B. -This code follows the same steps as above but for token B, transferring the calculated amount of token B from the pool account to the liquidity provider's account. +`Pool` PDA seeds: `[amm, mint_a, mint_b]` with `mint_a < mint_b`. -https://github.com/solana-developers/program-examples/blob/419cb6b6c20e8b1c65711b68a4dde2527725cc1a/tokens/token-swap/anchor/programs/token-swap/src/instructions/withdraw_liquidity.rs#L69-L83 +## Instruction handlers -This code burns the specified amount of liquidity tokens (amount) by calling the token::burn function. The liquidity tokens are destroyed, reducing the total supply. -Finally, this code returns Ok(()) if all operations in the function executed successfully. This indicates that the liquidity withdrawal was completed without any errors. +### `create_amm` +Initializes an `Amm` account with the supplied `id`, `admin`, and `fee`. Enforces `fee < 10000`. +### `create_pool` +Initializes a `Pool` account, an LP mint (`mint_liquidity`), and the two pool ATAs (`pool_account_a`, `pool_account_b`). Enforces `mint_a < mint_b` for canonical pool addressing. - +### `deposit_liquidity` +Transfers `amount_a` and `amount_b` from the depositor to the pool, then mints LP tokens to the depositor. +- For the first deposit, the LP amount is `sqrt(amount_a * amount_b)`, with `MINIMUM_LIQUIDITY` locked away forever (to prevent the empty-pool edge case). +- For later deposits, the amounts are scaled to match the current pool ratio. +### `swap_exact_tokens_for_tokens` +Swaps a fixed `input_amount` of one token for as much of the other as possible (subject to `min_output_amount`). +- The trading fee is taken off the input first (`taxed_input = input * (10_000 - fee) / 10_000`). +- The output is computed against the current `pool_a` and `pool_b` balances. +- After the swap, the invariant `pool_a * pool_b` is checked to ensure it has not decreased. +### `withdraw_liquidity` +Burns LP tokens and returns the proportional share of `pool_a` and `pool_b` to the LP. The proportion is `amount / (mint_liquidity.supply + MINIMUM_LIQUIDITY)`. +## Tests +Run `pnpm test` from the example directory. diff --git a/tokens/transfer-tokens/README.md b/tokens/transfer-tokens/README.md index f49991431..f8674f5a0 100644 --- a/tokens/transfer-tokens/README.md +++ b/tokens/transfer-tokens/README.md @@ -1,7 +1,7 @@ # Transfer Tokens -Just like with minting, transfers of SPL Tokens are conducted between Associated Token Accounts. - -You can use the `transfer()` function provided by the SPL Token Program to conduct a transfer of any SPL Token with the appropriate permissions. - -Check out [SPL Token Minter](../spl-token-minter) or [NFT Minter](../nft-minter) to learn more about Associated Token Accounts. \ No newline at end of file +Like minting, SPL Token transfers happen between Associated Token Accounts. + +Use the `transfer()` instruction provided by the SPL Token Program to transfer any SPL Token, given the appropriate permissions. + +See [SPL Token Minter](../spl-token-minter) and [NFT Minter](../nft-minter) for more on Associated Token Accounts. diff --git a/tools/clockwork/README.md b/tools/clockwork/README.md index 25e15b844..18b909e12 100644 --- a/tools/clockwork/README.md +++ b/tools/clockwork/README.md @@ -1,3 +1,5 @@ -Clockwork is an automation infrastructure for Solana. It allows you to schedule transactions and build automated, event driven programs. +# Clockwork -Here is a link to the Clockwork Program Examples repository: [Clockwork](https://github.com/clockwork-xyz/clockwork) +[Clockwork](https://github.com/clockwork-xyz/clockwork) is automation infrastructure for Solana. It lets you schedule transactions and build automated, event-driven programs. + +See the upstream [Clockwork repository](https://github.com/clockwork-xyz/clockwork) for examples and documentation. diff --git a/tools/shank-and-solita/native/README.md b/tools/shank-and-solita/native/README.md index 827f5f3aa..f36d801a4 100644 --- a/tools/shank-and-solita/native/README.md +++ b/tools/shank-and-solita/native/README.md @@ -1,18 +1,13 @@ -# Shank & Solita +# Shank and Solita -The devs at Metaplex created Shank & Solita for native Solana programs to be able to take advantage of serialization & IDLs just like Anchor programs. +The Metaplex team built **Shank** and **Solita** so that native Solana programs can have serialization and IDL support similar to Anchor. -### Shank +## Shank + +[Shank](https://github.com/metaplex-foundation/shank) is a Rust crate that generates an IDL for your program. + +Mark a struct as an account: -[Shank](https://docs.metaplex.com/developer-tools/shank) is the Rust crate responsible for generating an IDL for your program. - -It's super easy to use in your Rust code: - -Add this annotation to any struct to mark it as an account: -```rust -#[derive(ShankAccount)] -``` -ex: ```rust #[derive(BorshDeserialize, BorshSerialize, Clone, ShankAccount)] pub struct Car { @@ -22,11 +17,8 @@ pub struct Car { } ``` -Add this annotation to any enum to mark it as an instruction enum: -```rust -#[derive(ShankInstruction)] -``` -ex: +Mark an enum as your instruction set: + ```rust #[derive(BorshDeserialize, BorshSerialize, Clone, ShankInstruction)] pub enum CarRentalServiceInstruction { @@ -37,58 +29,54 @@ pub enum CarRentalServiceInstruction { } ``` -Then you just need to add the Shank CLI: -```shell +Install the CLI and generate the IDL: + +```bash cargo install shank-cli +shank idl ``` -```shell -USAGE: - shank -OPTIONS: - -h, --help Print help information +> Shank needs `declare_id!` in your program for the IDL generation to work: +> +> ```rust +> declare_id!("8avNGHVXDwsELJaWMSoUZ44CirQd4zyU9Ez4ZmP4jNjZ"); +> ``` -SUBCOMMANDS: - help Print this message or the help of the given subcommand(s) - idl -``` - -> Note: You do have to make use of `declare_id` in order for Shank to work properly: -```rust -declare_id!("8avNGHVXDwsELJaWMSoUZ44CirQd4zyU9Ez4ZmP4jNjZ"); -``` +## Solita -### Solita +[Solita](https://github.com/metaplex-foundation/solita) is the JavaScript SDK generator. It turns your IDL into a TypeScript client. -[Solita](https://docs.metaplex.com/developer-tools/solita/) is the JavaScript SDK responsible for building client-side SDK types from your program's IDL. +> Solita works with both Shank IDLs and Anchor IDLs. -> Note: Solita will work with an IDL from Shank or from Anchor! +Install it: -First add Solita to your project: -```shell -yarn add -D @metaplex-foundation/solita +```bash +pnpm add -D @metaplex-foundation/solita ``` -Then add a Solita config `.solitarc.js`: + +Then add a `.solitarc.js` at the example root: + ```javascript -const path = require('path'); -const programDir = path.join(__dirname, 'program'); -const idlDir = path.join(programDir, 'idl'); -const sdkDir = path.join(__dirname, 'tests', 'generated'); -const binaryInstallDir = path.join(__dirname, '.crates'); +const path = require("node:path"); +const programDir = path.join(__dirname, "program"); +const idlDir = path.join(programDir, "idl"); +const sdkDir = path.join(__dirname, "tests", "generated"); +const binaryInstallDir = path.join(__dirname, ".crates"); module.exports = { - idlGenerator: 'shank', - programName: 'car_rental_service', - idlDir, - sdkDir, - binaryInstallDir, - programDir, + idlGenerator: "shank", + programName: "car_rental_service", + idlDir, + sdkDir, + binaryInstallDir, + programDir, }; ``` -Once you've got that file configured to match your repository layout, go ahead and run: -```shell -yarn solita +Generate the client: + +```bash +pnpm solita ``` -That should build all your types from your IDL! Check for a folder called `generated` to see them! \ No newline at end of file +The generated TypeScript lands in `tests/generated/`.