Skip to content

Commit e43df20

Browse files
committed
Add favorites and escrow projects from professional-education repo
Minor biome fixes
1 parent 65e979e commit e43df20

32 files changed

+913
-0
lines changed

basics/favorites/anchor/.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
.anchor
3+
.DS_Store
4+
target
5+
**/*.rs.bk
6+
node_modules
7+
test-ledger
8+
.yarn
9+
.env
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
.anchor
3+
.DS_Store
4+
target
5+
node_modules
6+
dist
7+
build
8+
test-ledger

basics/favorites/anchor/Anchor.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[toolchain]
2+
3+
[features]
4+
resolution = true
5+
skip-lint = false
6+
7+
[programs.localnet]
8+
favorites = "ww9C83noARSQVBnqmCUmaVdbJjmiwcV9j2LkXYMoUCV"
9+
10+
[registry]
11+
url = "https://api.apr.dev"
12+
13+
[provider]
14+
cluster = "Localnet"
15+
wallet = "~/.config/solana/id.json"
16+
17+
[scripts]
18+
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

basics/favorites/anchor/Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[workspace]
2+
members = [
3+
"programs/*"
4+
]
5+
resolver = "2"
6+
7+
[profile.release]
8+
overflow-checks = true
9+
lto = "fat"
10+
codegen-units = 1
11+
12+
[profile.release.build-override]
13+
opt-level = 3
14+
incremental = false
15+
codegen-units = 1

basics/favorites/anchor/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Favorites
2+
3+
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.
4+
5+
It's used by the [https://github.com/solana-developers/professional-education](Solana Professional Education) course.
6+
7+
## Usage
8+
9+
`anchor test`, `anchor deploy` etc.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Migrations are an early feature. Currently, they're nothing more than this
2+
// single deploy script that's invoked from the CLI, injecting a provider
3+
// configured from the workspace's Anchor.toml.
4+
5+
const anchor = require('@coral-xyz/anchor');
6+
7+
module.exports = async (provider) => {
8+
// Configure client to use the provider.
9+
anchor.setProvider(provider);
10+
11+
// Add your deploy script here.
12+
};

basics/favorites/anchor/package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"scripts": {
3+
"lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
4+
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
5+
},
6+
"dependencies": {
7+
"@coral-xyz/anchor": "^0.30.0",
8+
"@solana-developers/helpers": "^2.0.0"
9+
},
10+
"license": "UNLICENSED",
11+
"devDependencies": {
12+
"@types/bn.js": "^5.1.0",
13+
"@types/chai": "^4.3.0",
14+
"@types/mocha": "^9.0.0",
15+
"chai": "^4.3.4",
16+
"mocha": "^9.0.3",
17+
"prettier": "^2.6.2",
18+
"ts-mocha": "^10.0.0",
19+
"typescript": "^4.3.5"
20+
}
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "favorites"
3+
version = "0.1.0"
4+
description = "Created with Anchor"
5+
edition = "2021"
6+
7+
[lib]
8+
crate-type = ["cdylib", "lib"]
9+
name = "favorites"
10+
11+
[features]
12+
no-entrypoint = []
13+
no-idl = []
14+
no-log-ix-name = []
15+
cpi = ["no-entrypoint"]
16+
default = []
17+
idl-build = ["anchor-lang/idl-build"]
18+
19+
[dependencies]
20+
anchor-lang = {version = "0.30.0", features = ["init-if-needed"]}
21+
solana-program = "=1.18.5"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[target.bpfel-unknown-unknown.dependencies.std]
2+
features = []
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
use anchor_lang::prelude::*;
2+
// Our program's address!
3+
// This matches the key in the target/deploy directory
4+
declare_id!("ww9C83noARSQVBnqmCUmaVdbJjmiwcV9j2LkXYMoUCV");
5+
6+
// Anchor programs always use 8 bits for the discriminator
7+
pub const ANCHOR_DISCRIMINATOR_SIZE: usize = 8;
8+
9+
// Our Solana program!
10+
#[program]
11+
pub mod favorites {
12+
use super::*;
13+
14+
// Our instruction handler! It sets the user's favorite number and color
15+
pub fn set_favorites(context: Context<SetFavorites>, number: u64, color: String, hobbies: Vec<String>) -> Result<()> {
16+
let user_public_key = context.accounts.user.key();
17+
msg!("Greetings from {}", context.program_id);
18+
msg!(
19+
"User {user_public_key}'s favorite number is {number}, favorite color is: {color}",
20+
);
21+
22+
msg!(
23+
"User's hobbies are: {:?}",
24+
hobbies
25+
);
26+
27+
context.accounts.favorites.set_inner(Favorites {
28+
number,
29+
color,
30+
hobbies
31+
});
32+
Ok(())
33+
}
34+
35+
// We can also add a get_favorites instruction handler to return the user's favorite number and color
36+
}
37+
38+
// What we will put inside the Favorites PDA
39+
#[account]
40+
#[derive(InitSpace)]
41+
pub struct Favorites {
42+
pub number: u64,
43+
44+
#[max_len(50)]
45+
pub color: String,
46+
47+
#[max_len(5, 50)]
48+
pub hobbies: Vec<String>
49+
}
50+
// When people call the set_favorites instruction, they will need to provide the accounts that will be modifed. This keeps Solana fast!
51+
#[derive(Accounts)]
52+
pub struct SetFavorites<'info> {
53+
#[account(mut)]
54+
pub user: Signer<'info>,
55+
56+
#[account(
57+
init_if_needed,
58+
payer = user,
59+
space = ANCHOR_DISCRIMINATOR_SIZE + Favorites::INIT_SPACE,
60+
seeds=[b"favorites", user.key().as_ref()],
61+
bump)]
62+
pub favorites: Account<'info, Favorites>,
63+
64+
pub system_program: Program<'info, System>,
65+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import * as anchor from '@coral-xyz/anchor';
2+
import type { Program } from '@coral-xyz/anchor';
3+
import { getCustomErrorMessage } from '@solana-developers/helpers';
4+
import { assert } from 'chai';
5+
import type { Favorites } from '../target/types/favorites';
6+
import { systemProgramErrors } from './system-errors';
7+
const web3 = anchor.web3;
8+
9+
describe('Favorites', () => {
10+
// Use the cluster and the keypair from Anchor.toml
11+
const provider = anchor.AnchorProvider.env();
12+
anchor.setProvider(provider);
13+
const user = (provider.wallet as anchor.Wallet).payer;
14+
const someRandomGuy = anchor.web3.Keypair.generate();
15+
const program = anchor.workspace.Favorites as Program<Favorites>;
16+
17+
// Here's what we want to write to the blockchain
18+
const favoriteNumber = new anchor.BN(23);
19+
const favoriteColor = 'purple';
20+
const favoriteHobbies = ['skiing', 'skydiving', 'biking'];
21+
22+
// We don't need to airdrop if we're using the local cluster
23+
// because the local cluster gives us 85 billion dollars worth of SOL
24+
before(async () => {
25+
const balance = await provider.connection.getBalance(user.publicKey);
26+
const balanceInSOL = balance / web3.LAMPORTS_PER_SOL;
27+
const formattedBalance = new Intl.NumberFormat().format(balanceInSOL);
28+
console.log(`Balance: ${formattedBalance} SOL`);
29+
});
30+
31+
it('Writes our favorites to the blockchain', async () => {
32+
await program.methods
33+
// set_favourites in Rust becomes setFavorites in TypeScript
34+
.setFavorites(favoriteNumber, favoriteColor, favoriteHobbies)
35+
// Sign the transaction
36+
.signers([user])
37+
// Send the transaction to the cluster or RPC
38+
.rpc();
39+
40+
// Find the PDA for the user's favorites
41+
const favoritesPdaAndBump = web3.PublicKey.findProgramAddressSync([Buffer.from('favorites'), user.publicKey.toBuffer()], program.programId);
42+
const favoritesPda = favoritesPdaAndBump[0];
43+
const dataFromPda = await program.account.favorites.fetch(favoritesPda);
44+
// And make sure it matches!
45+
assert.equal(dataFromPda.color, favoriteColor);
46+
// A little extra work to make sure the BNs are equal
47+
assert.equal(dataFromPda.number.toString(), favoriteNumber.toString());
48+
// And check the hobbies too
49+
assert.deepEqual(dataFromPda.hobbies, favoriteHobbies);
50+
});
51+
52+
it('Updates the favorites', async () => {
53+
const newFavoriteHobbies = ['skiing', 'skydiving', 'biking', 'swimming'];
54+
try {
55+
await program.methods.setFavorites(favoriteNumber, favoriteColor, newFavoriteHobbies).signers([user]).rpc();
56+
} catch (error) {
57+
console.error((error as Error).message);
58+
const customErrorMessage = getCustomErrorMessage(systemProgramErrors, error);
59+
throw new Error(customErrorMessage);
60+
}
61+
});
62+
63+
it('Rejects transactions from unauthorized signers', async () => {
64+
try {
65+
await program.methods
66+
// set_favourites in Rust becomes setFavorites in TypeScript
67+
.setFavorites(favoriteNumber, favoriteColor, favoriteHobbies)
68+
// Sign the transaction
69+
.signers([someRandomGuy])
70+
// Send the transaction to the cluster or RPC
71+
.rpc();
72+
} catch (error) {
73+
const errorMessage = (error as Error).message;
74+
assert.isTrue(errorMessage.includes('unknown signer'));
75+
}
76+
});
77+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// From https://github.com/solana-labs/solana/blob/a94920a4eadf1008fc292e47e041c1b3b0d949df/sdk/program/src/system_instruction.rs
2+
export const systemProgramErrors = [
3+
'an account with the same address already exists',
4+
5+
'account does not have enough SOL to perform the operation',
6+
7+
'cannot assign account to this program id',
8+
9+
'cannot allocate account data of this length',
10+
11+
'length of requested seed is too long',
12+
13+
'provided address does not match addressed derived from seed',
14+
15+
'advancing stored nonce requires a populated RecentBlockhashes sysvar',
16+
17+
'stored nonce is still in recent_blockhashes',
18+
19+
'specified nonce does not match stored nonce',
20+
];

basics/favorites/anchor/tsconfig.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"compilerOptions": {
3+
"types": ["mocha", "chai"],
4+
"typeRoots": ["./node_modules/@types"],
5+
"lib": ["es2015"],
6+
"module": "commonjs",
7+
"target": "es6",
8+
"esModuleInterop": true
9+
}
10+
}

tokens/escrow/anchor/.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.anchor
2+
.DS_Store
3+
target
4+
**/*.rs.bk
5+
node_modules
6+
test-ledger
7+
.yarn

tokens/escrow/anchor/.prettierignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.anchor
2+
.DS_Store
3+
target
4+
node_modules
5+
dist
6+
build
7+
test-ledger

tokens/escrow/anchor/Anchor.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[toolchain]
2+
3+
[features]
4+
resolution = true
5+
skip-lint = false
6+
7+
[programs.localnet]
8+
escrow = "qbuMdeYxYJXBjU6C6qFKjZKjXmrU83eDQomHdrch826"
9+
10+
[registry]
11+
url = "https://api.apr.dev"
12+
13+
[provider]
14+
cluster = "Localnet"
15+
wallet = "~/.config/solana/id.json"
16+
17+
[scripts]
18+
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

tokens/escrow/anchor/Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[workspace]
2+
members = [
3+
"programs/*"
4+
]
5+
resolver = "2"
6+
7+
[profile.release]
8+
overflow-checks = true
9+
lto = "fat"
10+
codegen-units = 1
11+
12+
[profile.release.build-override]
13+
opt-level = 3
14+
incremental = false
15+
codegen-units = 1

tokens/escrow/anchor/README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Anchor Escrow
2+
3+
## Introduction
4+
5+
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.
6+
7+
For example, Alice is offering 10 USDC, and wants 100 WIF in return.
8+
9+
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.
10+
11+
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.
12+
13+
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!
14+
15+
## Usage
16+
17+
`anchor test`, `anchor deploy` etc.
18+
19+
## Credit
20+
21+
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.
22+
23+
### Changes from original
24+
25+
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.
26+
27+
- Custom instructions were replaced by `@solana-developers/helpers` for many tasks to reduce the file size.
28+
- The upstream project has a custom file layout. We use the 'multiple files' Anchor layout.
29+
- 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!
30+
- The name 'deposit' was being used in multiple contexts, and `deposit` can be tough because it's a verb and a noun:
31+
32+
- Renamed deposit #1 -> 'token_a_offered_amount'
33+
- Renamed deposit #2 (in make() ) -> 'send_offered_tokens_to_vault'
34+
- Renamed deposit #3 (in take() ) -> 'send_wanted_tokens_to_maker'
35+
36+
- 'seed' was renamed to 'id' because 'seed' as it conflicted with the 'seeds' used for PDA address generation.
37+
- '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'.
38+
39+
- Escrow (the program) -> remains Escrow
40+
- Escrow (the offer) -> Offer.
41+
42+
- 'receive' was renamed to 'token_b_wanted_amount' as 'receive' is a verb and not a suitable name for an integer.
43+
- mint_a -> token_mint_a (ie, what the maker has offered and what the taker wants)
44+
- mint_b -> token_mint_b (ie, what that maker wants and what the taker must offer)
45+
- makerAtaA -> makerTokenAccountA,
46+
- makerAtaB -> makerTokenAccountB
47+
- takerAtaA -> takerTokenAccountA
48+
- takerAtaB -> takerTokenAccountB

0 commit comments

Comments
 (0)