diff --git a/tokens/spl-token-minter/steel/.gitignore b/tokens/spl-token-minter/steel/.gitignore new file mode 100644 index 00000000..052739db --- /dev/null +++ b/tokens/spl-token-minter/steel/.gitignore @@ -0,0 +1,2 @@ +target +test-ledger diff --git a/tokens/spl-token-minter/steel/Cargo.toml b/tokens/spl-token-minter/steel/Cargo.toml new file mode 100644 index 00000000..f3d794ab --- /dev/null +++ b/tokens/spl-token-minter/steel/Cargo.toml @@ -0,0 +1,24 @@ +[workspace] +resolver = "2" +members = ["api", "program"] + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +homepage = "" +documentation = "" +respository = "" +readme = "./README.md" +keywords = ["solana"] + +[workspace.dependencies] +steel-api = { path = "./api", version = "0.1.0" } +bytemuck = "1.14" +num_enum = "0.7" +solana-program = "1.18" +steel = { version = "1.3", features = ["spl"] } +thiserror = "1.0" +spl-token = "^4" +mpl-token-metadata = { version = "4.1.2" } +# mpl-token-metadata = "=2.0.0-beta.1" # mpl-token-metadata = { version = "1.11", features = ["no-entrypoint"] } diff --git a/tokens/spl-token-minter/steel/README.md b/tokens/spl-token-minter/steel/README.md new file mode 100644 index 00000000..4f4fe2a5 --- /dev/null +++ b/tokens/spl-token-minter/steel/README.md @@ -0,0 +1,22 @@ +# Steel + +**Steel** is a ... + +## API +- [`Consts`](api/src/consts.rs) – Program constants. +- [`Error`](api/src/error.rs) – Custom program errors. +- [`Event`](api/src/event.rs) – Custom program events. +- [`Instruction`](api/src/instruction.rs) – Declared instructions. + +## Instructions +- [`Hello`](program/src/hello.rs) – Hello ... + +## State +- [`User`](api/src/state/user.rs) – User ... + +## Tests + +To run the test suit, use the Solana toolchain: +``` +cargo test-sbf +``` diff --git a/tokens/spl-token-minter/steel/api/Cargo.toml b/tokens/spl-token-minter/steel/api/Cargo.toml new file mode 100644 index 00000000..7b73f5ce --- /dev/null +++ b/tokens/spl-token-minter/steel/api/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "steel-api" +version = "0.1.0" +edition = "2021" + +[dependencies] +bytemuck.workspace = true +num_enum.workspace = true +solana-program.workspace = true +steel.workspace = true +thiserror.workspace = true +spl-token.workspace = true +mpl-token-metadata.workspace = true diff --git a/tokens/spl-token-minter/steel/api/src/consts.rs b/tokens/spl-token-minter/steel/api/src/consts.rs new file mode 100644 index 00000000..7e7b02db --- /dev/null +++ b/tokens/spl-token-minter/steel/api/src/consts.rs @@ -0,0 +1,10 @@ +/// The seed of the mint account PDA. +pub const MINT: &[u8] = b"mint"; + +/// Noise for deriving the mint pda +pub const MINT_NOISE: [u8; 16] = [ + 89, 157, 88, 232, 243, 249, 197, 132, 199, 49, 19, 234, 91, 94, 150, 41, +]; + +/// The seed of the metadata account PDA. +pub const METADATA: &[u8] = b"metadata"; diff --git a/tokens/spl-token-minter/steel/api/src/error.rs b/tokens/spl-token-minter/steel/api/src/error.rs new file mode 100644 index 00000000..96e84da4 --- /dev/null +++ b/tokens/spl-token-minter/steel/api/src/error.rs @@ -0,0 +1,10 @@ +use steel::*; + +#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, IntoPrimitive)] +#[repr(u32)] +pub enum SteelError { + #[error("This is a dummy error")] + Dummy = 0, +} + +error!(SteelError); diff --git a/tokens/spl-token-minter/steel/api/src/instruction.rs b/tokens/spl-token-minter/steel/api/src/instruction.rs new file mode 100644 index 00000000..5a27449e --- /dev/null +++ b/tokens/spl-token-minter/steel/api/src/instruction.rs @@ -0,0 +1,26 @@ +use std::str; +use steel::*; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] +pub enum SteelInstruction { + Create = 0, + Mint = 1, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Create { + pub token_name: [u8; 32], + pub token_symbol: [u8; 8], + pub token_uri: [u8; 64], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Mint { + pub amount: [u8; 8], +} + +instruction!(SteelInstruction, Mint); +instruction!(SteelInstruction, Create); diff --git a/tokens/spl-token-minter/steel/api/src/lib.rs b/tokens/spl-token-minter/steel/api/src/lib.rs new file mode 100644 index 00000000..25c5c8be --- /dev/null +++ b/tokens/spl-token-minter/steel/api/src/lib.rs @@ -0,0 +1,16 @@ +pub mod consts; +pub mod error; +pub mod instruction; +pub mod sdk; + +pub mod prelude { + pub use crate::consts::*; + pub use crate::error::*; + pub use crate::instruction::*; + pub use crate::sdk::*; +} + +use steel::*; + +// TODO Set program id +declare_id!("z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35"); diff --git a/tokens/spl-token-minter/steel/api/src/sdk.rs b/tokens/spl-token-minter/steel/api/src/sdk.rs new file mode 100644 index 00000000..b9c70923 --- /dev/null +++ b/tokens/spl-token-minter/steel/api/src/sdk.rs @@ -0,0 +1,76 @@ +use steel::*; + +use crate::prelude::*; + +pub fn create( + payer: Pubkey, + mint_authority: Pubkey, + token_name: String, + token_symbol: String, + token_uri: String, +) -> Instruction { + let token_name_bytes: [u8; 32] = token_name + .as_bytes() + .try_into() + .expect("String wrong length, expected 32 bytes"); + let token_symbol_bytes: [u8; 8] = token_symbol + .as_bytes() + .try_into() + .expect("String wrong length, expected 32 bytes"); + let token_uri_bytes: [u8; 64] = token_uri + .as_bytes() + .try_into() + .expect("String wrong length, expected 32 bytes"); + + let mint_pda = Pubkey::find_program_address(&[MINT, MINT_NOISE.as_slice()], &crate::ID); + let metadata_pda = Pubkey::find_program_address( + &[ + METADATA, + mpl_token_metadata::ID.as_ref(), + mint_pda.0.as_ref(), + ], + &mpl_token_metadata::ID, + ); + + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(payer, true), + AccountMeta::new(mint_pda.0, false), + AccountMeta::new(mint_authority, false), + AccountMeta::new(metadata_pda.0, false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(spl_token::ID, false), + AccountMeta::new_readonly(mpl_token_metadata::ID, false), + AccountMeta::new_readonly(sysvar::rent::ID, false), + ], + data: Create { + token_name: token_name_bytes, + token_symbol: token_symbol_bytes, + token_uri: token_uri_bytes, + } + .to_bytes(), + } +} +pub fn mint( + signer: Pubkey, + mint: Pubkey, + to: Pubkey, + authority: Pubkey, + amount: u64, +) -> Instruction { + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(signer, true), + AccountMeta::new(mint, false), + AccountMeta::new(to, false), + AccountMeta::new(authority, false), + AccountMeta::new_readonly(spl_token::ID, false), + ], + data: Mint { + amount: amount.to_le_bytes(), + } + .to_bytes(), + } +} diff --git a/tokens/spl-token-minter/steel/program/Cargo.toml b/tokens/spl-token-minter/steel/program/Cargo.toml new file mode 100644 index 00000000..a4e9e9a5 --- /dev/null +++ b/tokens/spl-token-minter/steel/program/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "steel-program" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +steel-api.workspace = true +solana-program.workspace = true +steel.workspace = true +spl-token.workspace = true +mpl-token-metadata.workspace = true + + +[dev-dependencies] +bs64 = "0.1.2" +rand = "0.8.5" +solana-program-test = "1.18" +solana-sdk = "1.18" +tokio = { version = "1.35", features = ["full"] } diff --git a/tokens/spl-token-minter/steel/program/src/create.rs b/tokens/spl-token-minter/steel/program/src/create.rs new file mode 100644 index 00000000..0cd72d73 --- /dev/null +++ b/tokens/spl-token-minter/steel/program/src/create.rs @@ -0,0 +1,96 @@ +use solana_program::msg; +use solana_program::program_pack::Pack; +use spl_token::state::Mint; +use steel::*; +use steel_api::prelude::*; + +pub fn process_create(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { + // parse args. + let args = Create::try_from_bytes(data)?; + let token_name = String::from_utf8(args.token_name.to_vec()).expect("Invalid UTF-8"); + let token_symbol = String::from_utf8(args.token_symbol.to_vec()).expect("Invalid UTF-8"); + let token_uri = String::from_utf8(args.token_uri.to_vec()).expect("Invalid UTF-8"); + + // Load accounts. + let [payer_info, mint_info, mint_authority_info, metadata_info, token_program, system_program, rent_sysvar, token_metadata_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + // validation + payer_info.is_signer()?; + mint_info.to_mint()?; + token_program.is_program(&spl_token::ID)?; + rent_sysvar.is_sysvar(&sysvar::rent::ID)?; + system_program.is_program(&system_program::ID)?; + token_program.is_program(&spl_token::ID)?; + token_metadata_program.is_program(&mpl_token_metadata::ID)?; + rent_sysvar.is_sysvar(&sysvar::rent::ID)?; + + // First create the account for the Mint + // + msg!("Creating mint account..."); + msg!("Mint: {}", mint_info.key); + allocate_account( + mint_info, + &spl_token::ID, + Mint::LEN, + &[MINT, MINT_NOISE.as_slice()], + system_program, + payer_info, + )?; + + // Now initialize that account as a Mint (standard Mint) + // + msg!("Initializing mint account..."); + msg!("Mint: {}", mint_info.key); + solana_program::program::invoke( + &spl_token::instruction::initialize_mint( + &spl_token::ID, + mint_info.key, + mint_authority_info.key, + Some(mint_authority_info.key), + 9, // 9 Decimals for the default SPL Token standard, + )?, + &[ + token_program.clone(), + mint_info.clone(), + mint_authority_info.clone(), + rent_sysvar.clone(), + ], + )?; + + // Now create the account for that Mint's metadata + // + msg!("Creating metadata account..."); + msg!("Metadata account address: {}", metadata_info.key); + mpl_token_metadata::instructions::CreateMetadataAccountV3Cpi { + __program: token_metadata_program, + metadata: metadata_info, + mint: mint_info, + mint_authority: mint_authority_info, + payer: payer_info, + update_authority: (payer_info, true), + system_program, + rent: Some(rent_sysvar), + __args: mpl_token_metadata::instructions::CreateMetadataAccountV3InstructionArgs { + data: mpl_token_metadata::types::DataV2 { + name: token_name, + symbol: token_symbol, + uri: token_uri, + seller_fee_basis_points: 0, + creators: None, + collection: None, + uses: None, + }, + is_mutable: true, + collection_details: None, + }, + } + .invoke()?; + + msg!("Token mint created successfully."); + + Ok(()) +} diff --git a/tokens/spl-token-minter/steel/program/src/lib.rs b/tokens/spl-token-minter/steel/program/src/lib.rs new file mode 100644 index 00000000..f2a7c09c --- /dev/null +++ b/tokens/spl-token-minter/steel/program/src/lib.rs @@ -0,0 +1,25 @@ +mod create; +mod mint; + +use create::*; +use mint::*; + +use steel::*; +use steel_api::prelude::*; + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let (ix, data) = parse_instruction(&steel_api::ID, program_id, data)?; + + match ix { + SteelInstruction::Mint => process_mint(accounts, data)?, + SteelInstruction::Create => process_create(accounts, data)?, + } + + Ok(()) +} + +entrypoint!(process_instruction); diff --git a/tokens/spl-token-minter/steel/program/src/mint.rs b/tokens/spl-token-minter/steel/program/src/mint.rs new file mode 100644 index 00000000..cd4a23a6 --- /dev/null +++ b/tokens/spl-token-minter/steel/program/src/mint.rs @@ -0,0 +1,52 @@ +use solana_program::msg; +use steel::*; +use steel_api::prelude::*; + +pub fn process_mint(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { + // parse args. + let args = Mint::try_from_bytes(data)?; + let amount = u64::from_le_bytes(args.amount); + + // Load accounts. + let [signer_info, mint_info, to_info, authority_info, token_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + msg!("Minting tokens to associated token account..."); + msg!("Mint: {:?}", mint_info); + msg!("Token Address: {:?}", &to_info); + + // validation + signer_info.is_signer()?; + mint_info.to_mint()?; + token_program.is_program(&spl_token::ID)?; + + to_info + .is_writable()? + .to_associated_token_account(signer_info.key, mint_info.key)? + .check(|t| t.owner == *signer_info.key)? + .check(|t| t.mint == *mint_info.key)?; + + token_program.is_program(&spl_token::ID)?; + + solana_program::program::invoke( + &spl_token::instruction::mint_to( + &spl_token::id(), + mint_info.key, + to_info.key, + authority_info.key, + &[authority_info.key], + amount, + )?, + &[ + token_program.clone(), + mint_info.clone(), + to_info.clone(), + authority_info.clone(), + ], + )?; + + msg!("Token minted successfully."); + + Ok(()) +} diff --git a/tokens/spl-token-minter/steel/program/tests/test.rs b/tokens/spl-token-minter/steel/program/tests/test.rs new file mode 100644 index 00000000..c5bb50e1 --- /dev/null +++ b/tokens/spl-token-minter/steel/program/tests/test.rs @@ -0,0 +1,46 @@ +use steel_api::prelude::*; +use solana_program::hash::Hash; +use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_sdk::{signature::Keypair, signer::Signer, transaction::Transaction}; +use steel::*; + +async fn setup() -> (BanksClient, Keypair, Hash) { + let mut program_test = ProgramTest::new( + "steel", + steel_api::ID, + processor!(steel_program::process_instruction), + ); + program_test.prefer_bpf(true); + program_test.start().await +} + +#[tokio::test] +async fn run_test() { + // Setup test + let (mut banks, payer, blockhash) = setup().await; + + // Submit initialize transaction. + let ix = initialize(payer.pubkey()); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + // Verify counter was initialized. + let counter_address = counter_pda().0; + let counter_account = banks.get_account(counter_address).await.unwrap().unwrap(); + let counter = Counter::try_from_bytes(&counter_account.data).unwrap(); + assert_eq!(counter_account.owner, steel_api::ID); + assert_eq!(counter.value, 0); + + // Submit add transaction. + let ix = add(payer.pubkey(), 42); + let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash); + let res = banks.process_transaction(tx).await; + assert!(res.is_ok()); + + // Verify counter was incremented. + let counter_account = banks.get_account(counter_address).await.unwrap().unwrap(); + let counter = Counter::try_from_bytes(&counter_account.data).unwrap(); + assert_eq!(counter.value, 42); +} +