diff --git a/Cargo.lock b/Cargo.lock index 4733c31d..78065b71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2263,6 +2263,7 @@ dependencies = [ "sequencer_core", "sequencer_runner", "tempfile", + "token_core", "tokio", "wallet", ] @@ -2671,6 +2672,7 @@ dependencies = [ "test-case", "test_program_methods", "thiserror", + "token_core", ] [[package]] @@ -3052,6 +3054,8 @@ dependencies = [ "nssa_core", "risc0-zkvm", "serde", + "token_core", + "token_program", ] [[package]] @@ -4440,6 +4444,23 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "token_core" +version = "0.1.0" +dependencies = [ + "borsh", + "nssa_core", + "serde", +] + +[[package]] +name = "token_program" +version = "0.1.0" +dependencies = [ + "nssa_core", + "token_core", +] + [[package]] name = "tokio" version = "1.48.0" @@ -4801,6 +4822,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "token_core", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index ef1b881d..61a12d2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,8 @@ members = [ "common", "nssa", "nssa/core", + "programs/token/core", + "programs/token", "program_methods", "program_methods/guest", "test_program_methods", @@ -32,6 +34,8 @@ sequencer_core = { path = "sequencer_core" } sequencer_rpc = { path = "sequencer_rpc" } sequencer_runner = { path = "sequencer_runner" } wallet = { path = "wallet" } +token_core = { path = "programs/token/core" } +token_program = { path = "programs/token" } test_program_methods = { path = "test_program_methods" } tokio = { version = "1.28.2", features = [ diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index 1a2fabca..f1f73a62 100644 Binary files a/artifacts/program_methods/amm.bin and b/artifacts/program_methods/amm.bin differ diff --git a/artifacts/program_methods/authenticated_transfer.bin b/artifacts/program_methods/authenticated_transfer.bin index 73d5fec1..93b74534 100644 Binary files a/artifacts/program_methods/authenticated_transfer.bin and b/artifacts/program_methods/authenticated_transfer.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index 278e88f4..a28db19e 100644 Binary files a/artifacts/program_methods/pinata.bin and b/artifacts/program_methods/pinata.bin differ diff --git a/artifacts/program_methods/pinata_token.bin b/artifacts/program_methods/pinata_token.bin index 4a4c8bb6..b2ae5054 100644 Binary files a/artifacts/program_methods/pinata_token.bin and b/artifacts/program_methods/pinata_token.bin differ diff --git a/artifacts/program_methods/privacy_preserving_circuit.bin b/artifacts/program_methods/privacy_preserving_circuit.bin index d8f5915e..a9d2e420 100644 Binary files a/artifacts/program_methods/privacy_preserving_circuit.bin and b/artifacts/program_methods/privacy_preserving_circuit.bin differ diff --git a/artifacts/program_methods/token.bin b/artifacts/program_methods/token.bin index 5f6d3781..ffc1aa5f 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index 96e339c2..d572d0dc 100644 Binary files a/artifacts/test_program_methods/burner.bin and b/artifacts/test_program_methods/burner.bin differ diff --git a/artifacts/test_program_methods/chain_caller.bin b/artifacts/test_program_methods/chain_caller.bin index 731d2dc7..f98dadd2 100644 Binary files a/artifacts/test_program_methods/chain_caller.bin and b/artifacts/test_program_methods/chain_caller.bin differ diff --git a/artifacts/test_program_methods/changer_claimer.bin b/artifacts/test_program_methods/changer_claimer.bin index 692d152b..c468f849 100644 Binary files a/artifacts/test_program_methods/changer_claimer.bin and b/artifacts/test_program_methods/changer_claimer.bin differ diff --git a/artifacts/test_program_methods/claimer.bin b/artifacts/test_program_methods/claimer.bin index da0a9bef..6fc9979b 100644 Binary files a/artifacts/test_program_methods/claimer.bin and b/artifacts/test_program_methods/claimer.bin differ diff --git a/artifacts/test_program_methods/data_changer.bin b/artifacts/test_program_methods/data_changer.bin index 86fde894..cfe49a6a 100644 Binary files a/artifacts/test_program_methods/data_changer.bin and b/artifacts/test_program_methods/data_changer.bin differ diff --git a/artifacts/test_program_methods/extra_output.bin b/artifacts/test_program_methods/extra_output.bin index 1ae1cd98..f546d43d 100644 Binary files a/artifacts/test_program_methods/extra_output.bin and b/artifacts/test_program_methods/extra_output.bin differ diff --git a/artifacts/test_program_methods/malicious_authorization_changer.bin b/artifacts/test_program_methods/malicious_authorization_changer.bin index 8f80ab58..8341e3fd 100644 Binary files a/artifacts/test_program_methods/malicious_authorization_changer.bin and b/artifacts/test_program_methods/malicious_authorization_changer.bin differ diff --git a/artifacts/test_program_methods/minter.bin b/artifacts/test_program_methods/minter.bin index 50199403..23f9085e 100644 Binary files a/artifacts/test_program_methods/minter.bin and b/artifacts/test_program_methods/minter.bin differ diff --git a/artifacts/test_program_methods/missing_output.bin b/artifacts/test_program_methods/missing_output.bin index 4994ae3f..6382e9d2 100644 Binary files a/artifacts/test_program_methods/missing_output.bin and b/artifacts/test_program_methods/missing_output.bin differ diff --git a/artifacts/test_program_methods/modified_transfer.bin b/artifacts/test_program_methods/modified_transfer.bin index 796de2d3..e294d6d6 100644 Binary files a/artifacts/test_program_methods/modified_transfer.bin and b/artifacts/test_program_methods/modified_transfer.bin differ diff --git a/artifacts/test_program_methods/nonce_changer.bin b/artifacts/test_program_methods/nonce_changer.bin index 017de8b3..926e7ab4 100644 Binary files a/artifacts/test_program_methods/nonce_changer.bin and b/artifacts/test_program_methods/nonce_changer.bin differ diff --git a/artifacts/test_program_methods/noop.bin b/artifacts/test_program_methods/noop.bin index 23195ef2..76e76653 100644 Binary files a/artifacts/test_program_methods/noop.bin and b/artifacts/test_program_methods/noop.bin differ diff --git a/artifacts/test_program_methods/program_owner_changer.bin b/artifacts/test_program_methods/program_owner_changer.bin index db1f87fa..de2675a5 100644 Binary files a/artifacts/test_program_methods/program_owner_changer.bin and b/artifacts/test_program_methods/program_owner_changer.bin differ diff --git a/artifacts/test_program_methods/simple_balance_transfer.bin b/artifacts/test_program_methods/simple_balance_transfer.bin index 17c95475..e6c4bd77 100644 Binary files a/artifacts/test_program_methods/simple_balance_transfer.bin and b/artifacts/test_program_methods/simple_balance_transfer.bin differ diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index b888c177..9ea8319b 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -11,6 +11,7 @@ sequencer_runner.workspace = true wallet.workspace = true common.workspace = true key_protocol.workspace = true +token_core.workspace = true anyhow.workspace = true env_logger.workspace = true diff --git a/integration_tests/tests/token.rs b/integration_tests/tests/token.rs index 9a8b714a..2dd1d90d 100644 --- a/integration_tests/tests/token.rs +++ b/integration_tests/tests/token.rs @@ -8,6 +8,7 @@ use integration_tests::{ use key_protocol::key_management::key_tree::chain_index::ChainIndex; use log::info; use nssa::program::Program; +use token_core::{TokenDefinition, TokenHolding}; use tokio::test; use wallet::cli::{ Command, SubcommandReturnValue, @@ -59,11 +60,13 @@ async fn create_and_transfer_public_token() -> Result<()> { }; // Create new token + let name = "A NAME".to_string(); + let total_supply = 37; let subcommand = TokenProgramAgnosticSubcommand::New { definition_account_id: format_public_account_id(&definition_account_id.to_string()), supply_account_id: format_public_account_id(&supply_account_id.to_string()), - name: "A NAME".to_string(), - total_supply: 37, + name: name.clone(), + total_supply, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -76,16 +79,16 @@ async fn create_and_transfer_public_token() -> Result<()> { .get_account(definition_account_id.to_string()) .await? .account; + let token_definition = TokenDefinition::try_from(&definition_acc.data)?; assert_eq!(definition_acc.program_owner, Program::token().id()); - // The data of a token definition account has the following layout: - // [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) || metadata id (32 bytes)] assert_eq!( - definition_acc.data.as_ref(), - &[ - 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ] + token_definition, + TokenDefinition::Fungible { + name: name.clone(), + total_supply, + metadata_id: None + } ); // Check the status of the token holding account with the total supply @@ -97,24 +100,23 @@ async fn create_and_transfer_public_token() -> Result<()> { // The account must be owned by the token program assert_eq!(supply_acc.program_owner, Program::token().id()); - // The data of a token holding account has the following layout: - // [ 0x01 || corresponding_token_definition_id (32 bytes) || balance (little endian 16 bytes) ] - // First byte of the data equal to 1 means it's a token holding account - assert_eq!(supply_acc.data.as_ref()[0], 1); - // Bytes from 1 to 33 represent the id of the token this account is associated with + let token_holding = TokenHolding::try_from(&supply_acc.data)?; assert_eq!( - &supply_acc.data.as_ref()[1..33], - definition_account_id.to_bytes() + token_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: total_supply + } ); - assert_eq!(u128::from_le_bytes(supply_acc.data[33..].try_into()?), 37); // Transfer 7 tokens from supply_acc to recipient_account_id + let transfer_amount = 7; let subcommand = TokenProgramAgnosticSubcommand::Send { from: format_public_account_id(&supply_account_id.to_string()), to: Some(format_public_account_id(&recipient_account_id.to_string())), to_npk: None, to_ipk: None, - amount: 7, + amount: transfer_amount, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -129,9 +131,14 @@ async fn create_and_transfer_public_token() -> Result<()> { .await? .account; assert_eq!(supply_acc.program_owner, Program::token().id()); - assert_eq!(supply_acc.data[0], 1); - assert_eq!(&supply_acc.data[1..33], definition_account_id.to_bytes()); - assert_eq!(u128::from_le_bytes(supply_acc.data[33..].try_into()?), 30); + let token_holding = TokenHolding::try_from(&supply_acc.data)?; + assert_eq!( + token_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: total_supply - transfer_amount + } + ); // Check the status of the recipient account after transfer let recipient_acc = ctx @@ -140,15 +147,21 @@ async fn create_and_transfer_public_token() -> Result<()> { .await? .account; assert_eq!(recipient_acc.program_owner, Program::token().id()); - assert_eq!(recipient_acc.data[0], 1); - assert_eq!(&recipient_acc.data[1..33], definition_account_id.to_bytes()); - assert_eq!(u128::from_le_bytes(recipient_acc.data[33..].try_into()?), 7); + let token_holding = TokenHolding::try_from(&recipient_acc.data)?; + assert_eq!( + token_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: transfer_amount + } + ); // Burn 3 tokens from recipient_acc + let burn_amount = 3; let subcommand = TokenProgramAgnosticSubcommand::Burn { definition: format_public_account_id(&definition_account_id.to_string()), holder: format_public_account_id(&recipient_account_id.to_string()), - amount: 3, + amount: burn_amount, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -162,13 +175,15 @@ async fn create_and_transfer_public_token() -> Result<()> { .get_account(definition_account_id.to_string()) .await? .account; + let token_definition = TokenDefinition::try_from(&definition_acc.data)?; assert_eq!( - definition_acc.data.as_ref(), - &[ - 0, 65, 32, 78, 65, 77, 69, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ] + token_definition, + TokenDefinition::Fungible { + name: name.clone(), + total_supply: total_supply - burn_amount, + metadata_id: None + } ); // Check the status of the recipient account after burn @@ -177,16 +192,24 @@ async fn create_and_transfer_public_token() -> Result<()> { .get_account(recipient_account_id.to_string()) .await? .account; + let token_holding = TokenHolding::try_from(&recipient_acc.data)?; - assert_eq!(u128::from_le_bytes(recipient_acc.data[33..].try_into()?), 4); + assert_eq!( + token_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: transfer_amount - burn_amount + } + ); // Mint 10 tokens at recipient_acc + let mint_amount = 10; let subcommand = TokenProgramAgnosticSubcommand::Mint { definition: format_public_account_id(&definition_account_id.to_string()), holder: Some(format_public_account_id(&recipient_account_id.to_string())), holder_npk: None, holder_ipk: None, - amount: 10, + amount: mint_amount, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -200,13 +223,15 @@ async fn create_and_transfer_public_token() -> Result<()> { .get_account(definition_account_id.to_string()) .await? .account; + let token_definition = TokenDefinition::try_from(&definition_acc.data)?; assert_eq!( - definition_acc.data.as_ref(), - &[ - 0, 65, 32, 78, 65, 77, 69, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ] + token_definition, + TokenDefinition::Fungible { + name, + total_supply: total_supply - burn_amount + mint_amount, + metadata_id: None + } ); // Check the status of the recipient account after mint @@ -215,10 +240,14 @@ async fn create_and_transfer_public_token() -> Result<()> { .get_account(recipient_account_id.to_string()) .await? .account; + let token_holding = TokenHolding::try_from(&recipient_acc.data)?; assert_eq!( - u128::from_le_bytes(recipient_acc.data[33..].try_into()?), - 14 + token_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: transfer_amount - burn_amount + mint_amount + } ); info!("Successfully created and transferred public token"); @@ -270,11 +299,13 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { }; // Create new token + let name = "A NAME".to_string(); + let total_supply = 37; let subcommand = TokenProgramAgnosticSubcommand::New { definition_account_id: format_public_account_id(&definition_account_id.to_string()), supply_account_id: format_private_account_id(&supply_account_id.to_string()), - name: "A NAME".to_string(), - total_supply: 37, + name: name.clone(), + total_supply, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -288,14 +319,16 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { .get_account(definition_account_id.to_string()) .await? .account; + let token_definition = TokenDefinition::try_from(&definition_acc.data)?; assert_eq!(definition_acc.program_owner, Program::token().id()); assert_eq!( - definition_acc.data.as_ref(), - &[ - 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ] + token_definition, + TokenDefinition::Fungible { + name: name.clone(), + total_supply, + metadata_id: None + } ); let new_commitment1 = ctx @@ -305,12 +338,13 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { assert!(verify_commitment_is_in_state(new_commitment1, ctx.sequencer_client()).await); // Transfer 7 tokens from supply_acc to recipient_account_id + let transfer_amount = 7; let subcommand = TokenProgramAgnosticSubcommand::Send { from: format_private_account_id(&supply_account_id.to_string()), to: Some(format_private_account_id(&recipient_account_id.to_string())), to_npk: None, to_ipk: None, - amount: 7, + amount: transfer_amount, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -331,10 +365,11 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { assert!(verify_commitment_is_in_state(new_commitment2, ctx.sequencer_client()).await); // Burn 3 tokens from recipient_acc + let burn_amount = 3; let subcommand = TokenProgramAgnosticSubcommand::Burn { definition: format_public_account_id(&definition_account_id.to_string()), holder: format_private_account_id(&recipient_account_id.to_string()), - amount: 3, + amount: burn_amount, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -348,13 +383,15 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { .get_account(definition_account_id.to_string()) .await? .account; + let token_definition = TokenDefinition::try_from(&definition_acc.data)?; assert_eq!( - definition_acc.data.as_ref(), - &[ - 0, 65, 32, 78, 65, 77, 69, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ] + token_definition, + TokenDefinition::Fungible { + name, + total_supply: total_supply - burn_amount, + metadata_id: None + } ); let new_commitment2 = ctx @@ -368,10 +405,14 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { .wallet() .get_account_private(&recipient_account_id) .context("Failed to get recipient account")?; + let token_holding = TokenHolding::try_from(&recipient_acc.data)?; assert_eq!( - u128::from_le_bytes(recipient_acc.data[33..].try_into()?), - 4 // 7 - 3 + token_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: transfer_amount - burn_amount + } ); info!("Successfully created and transferred token with private supply"); @@ -414,11 +455,13 @@ async fn create_token_with_private_definition() -> Result<()> { }; // Create token with private definition + let name = "A NAME".to_string(); + let total_supply = 37; let subcommand = TokenProgramAgnosticSubcommand::New { definition_account_id: format_private_account_id(&definition_account_id.to_string()), supply_account_id: format_public_account_id(&supply_account_id.to_string()), - name: "A NAME".to_string(), - total_supply: 37, + name: name.clone(), + total_supply, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -441,8 +484,14 @@ async fn create_token_with_private_definition() -> Result<()> { .account; assert_eq!(supply_acc.program_owner, Program::token().id()); - assert_eq!(supply_acc.data.as_ref()[0], 1); - assert_eq!(u128::from_le_bytes(supply_acc.data[33..].try_into()?), 37); + let token_holding = TokenHolding::try_from(&supply_acc.data)?; + assert_eq!( + token_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: total_supply + } + ); // Create private recipient account let result = wallet::cli::execute_subcommand( @@ -471,6 +520,7 @@ async fn create_token_with_private_definition() -> Result<()> { }; // Mint to public account + let mint_amount_public = 10; let subcommand = TokenProgramAgnosticSubcommand::Mint { definition: format_private_account_id(&definition_account_id.to_string()), holder: Some(format_public_account_id( @@ -478,7 +528,7 @@ async fn create_token_with_private_definition() -> Result<()> { )), holder_npk: None, holder_ipk: None, - amount: 10, + amount: mint_amount_public, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -491,10 +541,15 @@ async fn create_token_with_private_definition() -> Result<()> { .wallet() .get_account_private(&definition_account_id) .context("Failed to get definition account")?; + let token_definition = TokenDefinition::try_from(&definition_acc.data)?; assert_eq!( - u128::from_le_bytes(definition_acc.data[7..23].try_into()?), - 47 // 37 + 10 + token_definition, + TokenDefinition::Fungible { + name: name.clone(), + total_supply: total_supply + mint_amount_public, + metadata_id: None + } ); // Verify public recipient received tokens @@ -503,13 +558,18 @@ async fn create_token_with_private_definition() -> Result<()> { .get_account(recipient_account_id_public.to_string()) .await? .account; + let token_holding = TokenHolding::try_from(&recipient_acc.data)?; assert_eq!( - u128::from_le_bytes(recipient_acc.data[33..].try_into()?), - 10 + token_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: mint_amount_public + } ); // Mint to private account + let mint_amount_private = 5; let subcommand = TokenProgramAgnosticSubcommand::Mint { definition: format_private_account_id(&definition_account_id.to_string()), holder: Some(format_private_account_id( @@ -517,7 +577,7 @@ async fn create_token_with_private_definition() -> Result<()> { )), holder_npk: None, holder_ipk: None, - amount: 5, + amount: mint_amount_private, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -537,10 +597,14 @@ async fn create_token_with_private_definition() -> Result<()> { .wallet() .get_account_private(&recipient_account_id_private) .context("Failed to get private recipient account")?; + let token_holding = TokenHolding::try_from(&recipient_acc_private.data)?; assert_eq!( - u128::from_le_bytes(recipient_acc_private.data[33..].try_into()?), - 5 + token_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: mint_amount_private + } ); info!("Successfully created token with private definition and minted to both account types"); @@ -579,11 +643,13 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> { }; // Create token with both private definition and supply + let name = "A NAME".to_string(); + let total_supply = 37; let subcommand = TokenProgramAgnosticSubcommand::New { definition_account_id: format_private_account_id(&definition_account_id.to_string()), supply_account_id: format_private_account_id(&supply_account_id.to_string()), - name: "A NAME".to_string(), - total_supply: 37, + name, + total_supply, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -610,8 +676,15 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> { .wallet() .get_account_private(&supply_account_id) .context("Failed to get supply account")?; + let token_holding = TokenHolding::try_from(&supply_acc.data)?; - assert_eq!(u128::from_le_bytes(supply_acc.data[33..].try_into()?), 37); + assert_eq!( + token_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: total_supply + } + ); // Create recipient account let result = wallet::cli::execute_subcommand( @@ -627,12 +700,13 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> { }; // Transfer tokens + let transfer_amount = 7; let subcommand = TokenProgramAgnosticSubcommand::Send { from: format_private_account_id(&supply_account_id.to_string()), to: Some(format_private_account_id(&recipient_account_id.to_string())), to_npk: None, to_ipk: None, - amount: 7, + amount: transfer_amount, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -658,13 +732,27 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> { .wallet() .get_account_private(&supply_account_id) .context("Failed to get supply account")?; - assert_eq!(u128::from_le_bytes(supply_acc.data[33..].try_into()?), 30); + let token_holding = TokenHolding::try_from(&supply_acc.data)?; + assert_eq!( + token_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: total_supply - transfer_amount + } + ); let recipient_acc = ctx .wallet() .get_account_private(&recipient_account_id) .context("Failed to get recipient account")?; - assert_eq!(u128::from_le_bytes(recipient_acc.data[33..].try_into()?), 7); + let token_holding = TokenHolding::try_from(&recipient_acc.data)?; + assert_eq!( + token_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: transfer_amount + } + ); info!("Successfully created and transferred token with both private definition and supply"); @@ -715,11 +803,13 @@ async fn shielded_token_transfer() -> Result<()> { }; // Create token + let name = "A NAME".to_string(); + let total_supply = 37; let subcommand = TokenProgramAgnosticSubcommand::New { definition_account_id: format_public_account_id(&definition_account_id.to_string()), supply_account_id: format_public_account_id(&supply_account_id.to_string()), - name: "A NAME".to_string(), - total_supply: 37, + name, + total_supply, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -728,12 +818,13 @@ async fn shielded_token_transfer() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Perform shielded transfer: public supply -> private recipient + let transfer_amount = 7; let subcommand = TokenProgramAgnosticSubcommand::Send { from: format_public_account_id(&supply_account_id.to_string()), to: Some(format_private_account_id(&recipient_account_id.to_string())), to_npk: None, to_ipk: None, - amount: 7, + amount: transfer_amount, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -747,7 +838,14 @@ async fn shielded_token_transfer() -> Result<()> { .get_account(supply_account_id.to_string()) .await? .account; - assert_eq!(u128::from_le_bytes(supply_acc.data[33..].try_into()?), 30); + let token_holding = TokenHolding::try_from(&supply_acc.data)?; + assert_eq!( + token_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: total_supply - transfer_amount + } + ); // Verify recipient commitment exists let new_commitment = ctx @@ -761,7 +859,14 @@ async fn shielded_token_transfer() -> Result<()> { .wallet() .get_account_private(&recipient_account_id) .context("Failed to get recipient account")?; - assert_eq!(u128::from_le_bytes(recipient_acc.data[33..].try_into()?), 7); + let token_holding = TokenHolding::try_from(&recipient_acc.data)?; + assert_eq!( + token_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: transfer_amount + } + ); info!("Successfully performed shielded token transfer"); @@ -812,11 +917,13 @@ async fn deshielded_token_transfer() -> Result<()> { }; // Create token with private supply + let name = "A NAME".to_string(); + let total_supply = 37; let subcommand = TokenProgramAgnosticSubcommand::New { definition_account_id: format_public_account_id(&definition_account_id.to_string()), supply_account_id: format_private_account_id(&supply_account_id.to_string()), - name: "A NAME".to_string(), - total_supply: 37, + name, + total_supply, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -825,12 +932,13 @@ async fn deshielded_token_transfer() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Perform deshielded transfer: private supply -> public recipient + let transfer_amount = 7; let subcommand = TokenProgramAgnosticSubcommand::Send { from: format_private_account_id(&supply_account_id.to_string()), to: Some(format_public_account_id(&recipient_account_id.to_string())), to_npk: None, to_ipk: None, - amount: 7, + amount: transfer_amount, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -850,7 +958,14 @@ async fn deshielded_token_transfer() -> Result<()> { .wallet() .get_account_private(&supply_account_id) .context("Failed to get supply account")?; - assert_eq!(u128::from_le_bytes(supply_acc.data[33..].try_into()?), 30); + let token_holding = TokenHolding::try_from(&supply_acc.data)?; + assert_eq!( + token_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: total_supply - transfer_amount + } + ); // Verify recipient balance let recipient_acc = ctx @@ -858,7 +973,14 @@ async fn deshielded_token_transfer() -> Result<()> { .get_account(recipient_account_id.to_string()) .await? .account; - assert_eq!(u128::from_le_bytes(recipient_acc.data[33..].try_into()?), 7); + let token_holding = TokenHolding::try_from(&recipient_acc.data)?; + assert_eq!( + token_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: transfer_amount + } + ); info!("Successfully performed deshielded token transfer"); @@ -896,11 +1018,13 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> { }; // Create token + let name = "A NAME".to_string(); + let total_supply = 37; let subcommand = TokenProgramAgnosticSubcommand::New { definition_account_id: format_private_account_id(&definition_account_id.to_string()), supply_account_id: format_private_account_id(&supply_account_id.to_string()), - name: "A NAME".to_string(), - total_supply: 37, + name, + total_supply, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -931,12 +1055,13 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> { .context("Failed to get private account keys")?; // Mint using claiming path (foreign account) + let mint_amount = 9; let subcommand = TokenProgramAgnosticSubcommand::Mint { definition: format_private_account_id(&definition_account_id.to_string()), holder: None, holder_npk: Some(hex::encode(holder_keys.nullifer_public_key.0)), holder_ipk: Some(hex::encode(holder_keys.incoming_viewing_public_key.0)), - amount: 9, + amount: mint_amount, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -960,7 +1085,14 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> { .wallet() .get_account_private(&recipient_account_id) .context("Failed to get recipient account")?; - assert_eq!(u128::from_le_bytes(recipient_acc.data[33..].try_into()?), 9); + let token_holding = TokenHolding::try_from(&recipient_acc.data)?; + assert_eq!( + token_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: mint_amount + } + ); info!("Successfully minted tokens using claiming path"); diff --git a/nssa/Cargo.toml b/nssa/Cargo.toml index a508cc08..f339c2ae 100644 --- a/nssa/Cargo.toml +++ b/nssa/Cargo.toml @@ -23,7 +23,9 @@ risc0-build = "3.0.3" risc0-binfmt = "3.0.2" [dev-dependencies] +token_core.workspace = true test_program_methods.workspace = true + env_logger.workspace = true hex-literal = "1.0.0" test-case = "3.3.1" diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 32b3e2c0..a6a04425 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -20,8 +20,7 @@ pub struct ProgramInput { /// Each program can derive up to `2^256` unique account IDs by choosing different /// seeds. PDAs allow programs to control namespaced account identifiers without /// collisions between programs. -#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] -#[cfg_attr(any(feature = "host", test), derive(Debug))] +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] pub struct PdaSeed([u8; 32]); impl PdaSeed { @@ -65,23 +64,44 @@ impl From<(&ProgramId, &PdaSeed)> for AccountId { } } -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] -#[cfg_attr(any(feature = "host", test), derive(Debug,))] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct ChainedCall { /// The program ID of the program to execute pub program_id: ProgramId, + pub pre_states: Vec, /// The instruction data to pass pub instruction_data: InstructionData, - pub pre_states: Vec, pub pda_seeds: Vec, } +impl ChainedCall { + /// Creates a new chained call serializing the given instruction. + pub fn new( + program_id: ProgramId, + pre_states: Vec, + instruction: &I, + ) -> Self { + Self { + program_id, + pre_states, + instruction_data: risc0_zkvm::serde::to_vec(instruction) + .expect("Serialization to Vec should not fail"), + pda_seeds: Vec::new(), + } + } + + pub fn with_pda_seeds(mut self, pda_seeds: Vec) -> Self { + self.pda_seeds = pda_seeds; + self + } +} + /// Represents the final state of an `Account` after a program execution. /// A post state may optionally request that the executing program /// becomes the owner of the account (a “claim”). This is used to signal /// that the program intends to take ownership of the account. -#[derive(Serialize, Deserialize, Clone)] -#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(any(feature = "host", test), derive(PartialEq, Eq))] pub struct AccountPostState { account: Account, claim: bool, diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 1a384b2f..9ab6a150 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -273,6 +273,7 @@ pub mod tests { encryption::{EphemeralPublicKey, IncomingViewingPublicKey, Scalar}, program::{PdaSeed, ProgramId}, }; + use token_core::{TokenDefinition, TokenHolding}; use crate::{ PublicKey, PublicTransaction, V02State, @@ -2284,53 +2285,6 @@ pub mod tests { )); } - // TODO: repeated code needs to be cleaned up - // from token.rs (also repeated in amm.rs) - const TOKEN_DEFINITION_DATA_SIZE: usize = 55; - - const TOKEN_HOLDING_DATA_SIZE: usize = 49; - - struct TokenDefinition { - account_type: u8, - name: [u8; 6], - total_supply: u128, - metadata_id: AccountId, - } - - struct TokenHolding { - account_type: u8, - definition_id: AccountId, - balance: u128, - } - impl TokenDefinition { - fn into_data(self) -> Data { - let mut bytes = Vec::::new(); - bytes.extend_from_slice(&[self.account_type]); - bytes.extend_from_slice(&self.name); - bytes.extend_from_slice(&self.total_supply.to_le_bytes()); - bytes.extend_from_slice(&self.metadata_id.to_bytes()); - - if bytes.len() != TOKEN_DEFINITION_DATA_SIZE { - panic!("Invalid Token Definition data"); - } - - Data::try_from(bytes).expect("Token definition data size must fit into data") - } - } - - impl TokenHolding { - fn into_data(self) -> Data { - let mut bytes = [0; TOKEN_HOLDING_DATA_SIZE]; - bytes[0] = self.account_type; - bytes[1..33].copy_from_slice(&self.definition_id.to_bytes()); - bytes[33..].copy_from_slice(&self.balance.to_le_bytes()); - bytes - .to_vec() - .try_into() - .expect("33 bytes should fit into Data") - } - } - // TODO repeated code should ultimately be removed; fn compute_pool_pda( amm_program_id: ProgramId, @@ -2703,8 +2657,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_a_definition_id(), balance: BalanceForTests::user_token_a_holding_init(), }), @@ -2716,8 +2669,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_b_definition_id(), balance: BalanceForTests::user_token_b_holding_init(), }), @@ -2749,11 +2701,10 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenDefinition::into_data(TokenDefinition { - account_type: 0u8, - name: [1u8; 6], + data: Data::from(&TokenDefinition::Fungible { + name: String::from("test"), total_supply: BalanceForTests::token_a_supply(), - metadata_id: AccountId::new([0; 32]), + metadata_id: None, }), nonce: 0, } @@ -2763,11 +2714,10 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenDefinition::into_data(TokenDefinition { - account_type: 0u8, - name: [1u8; 6], + data: Data::from(&TokenDefinition::Fungible { + name: String::from("test"), total_supply: BalanceForTests::token_b_supply(), - metadata_id: AccountId::new([0; 32]), + metadata_id: None, }), nonce: 0, } @@ -2777,11 +2727,10 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenDefinition::into_data(TokenDefinition { - account_type: 0u8, - name: [1u8; 6], + data: Data::from(&TokenDefinition::Fungible { + name: String::from("LP Token"), total_supply: BalanceForTests::token_lp_supply(), - metadata_id: AccountId::new([0; 32]), + metadata_id: None, }), nonce: 0, } @@ -2791,8 +2740,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_a_definition_id(), balance: BalanceForTests::vault_a_balance_init(), }), @@ -2804,8 +2752,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_b_definition_id(), balance: BalanceForTests::vault_b_balance_init(), }), @@ -2817,8 +2764,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_lp_definition_id(), balance: BalanceForTests::user_token_lp_holding_init(), }), @@ -2830,8 +2776,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_a_definition_id(), balance: BalanceForTests::vault_a_balance_swap_1(), }), @@ -2843,8 +2788,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_b_definition_id(), balance: BalanceForTests::vault_b_balance_swap_1(), }), @@ -2876,8 +2820,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_a_definition_id(), balance: BalanceForTests::user_token_a_holding_swap_1(), }), @@ -2889,8 +2832,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_b_definition_id(), balance: BalanceForTests::user_token_b_holding_swap_1(), }), @@ -2902,8 +2844,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_a_definition_id(), balance: BalanceForTests::vault_a_balance_swap_2(), }), @@ -2915,8 +2856,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_b_definition_id(), balance: BalanceForTests::vault_b_balance_swap_2(), }), @@ -2948,8 +2888,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_a_definition_id(), balance: BalanceForTests::user_token_a_holding_swap_2(), }), @@ -2961,8 +2900,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_b_definition_id(), balance: BalanceForTests::user_token_b_holding_swap_2(), }), @@ -2974,8 +2912,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_a_definition_id(), balance: BalanceForTests::vault_a_balance_add(), }), @@ -2987,8 +2924,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_b_definition_id(), balance: BalanceForTests::vault_b_balance_add(), }), @@ -3020,8 +2956,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_a_definition_id(), balance: BalanceForTests::user_token_a_holding_add(), }), @@ -3033,8 +2968,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_b_definition_id(), balance: BalanceForTests::user_token_b_holding_add(), }), @@ -3046,8 +2980,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_lp_definition_id(), balance: BalanceForTests::user_token_lp_holding_add(), }), @@ -3059,11 +2992,10 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenDefinition::into_data(TokenDefinition { - account_type: 0u8, - name: [1u8; 6], + data: Data::from(&TokenDefinition::Fungible { + name: String::from("LP Token"), total_supply: BalanceForTests::token_lp_supply_add(), - metadata_id: AccountId::new([0; 32]), + metadata_id: None, }), nonce: 0, } @@ -3073,8 +3005,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_a_definition_id(), balance: BalanceForTests::vault_a_balance_remove(), }), @@ -3086,8 +3017,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_b_definition_id(), balance: BalanceForTests::vault_b_balance_remove(), }), @@ -3119,8 +3049,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_a_definition_id(), balance: BalanceForTests::user_token_a_holding_remove(), }), @@ -3132,8 +3061,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_b_definition_id(), balance: BalanceForTests::user_token_b_holding_remove(), }), @@ -3145,8 +3073,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_lp_definition_id(), balance: BalanceForTests::user_token_lp_holding_remove(), }), @@ -3158,11 +3085,10 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenDefinition::into_data(TokenDefinition { - account_type: 0u8, - name: [1u8; 6], + data: Data::from(&TokenDefinition::Fungible { + name: String::from("LP Token"), total_supply: BalanceForTests::token_lp_supply_remove(), - metadata_id: AccountId::new([0; 32]), + metadata_id: None, }), nonce: 0, } @@ -3172,11 +3098,10 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenDefinition::into_data(TokenDefinition { - account_type: 0u8, - name: [1u8; 6], + data: Data::from(&TokenDefinition::Fungible { + name: String::from("LP Token"), total_supply: 0, - metadata_id: AccountId::new([0; 32]), + metadata_id: None, }), nonce: 0, } @@ -3186,8 +3111,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_a_definition_id(), balance: 0, }), @@ -3199,8 +3123,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_b_definition_id(), balance: 0, }), @@ -3232,8 +3155,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_a_definition_id(), balance: BalanceForTests::user_token_a_holding_new_definition(), }), @@ -3245,8 +3167,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_b_definition_id(), balance: BalanceForTests::user_token_b_holding_new_definition(), }), @@ -3258,8 +3179,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_lp_definition_id(), balance: BalanceForTests::user_token_a_holding_new_definition(), }), @@ -3271,11 +3191,10 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenDefinition::into_data(TokenDefinition { - account_type: 0u8, - name: [1u8; 6], + data: Data::from(&TokenDefinition::Fungible { + name: String::from("LP Token"), total_supply: BalanceForTests::vault_a_balance_init(), - metadata_id: AccountId::new([0; 32]), + metadata_id: None, }), nonce: 0, } @@ -3305,8 +3224,7 @@ pub mod tests { Account { program_owner: Program::token().id(), balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_lp_definition_id(), balance: 0, }), @@ -4071,13 +3989,13 @@ pub mod tests { let pinata_token_holding_id = AccountId::from((&pinata_token.id(), &PdaSeed::new([0; 32]))); let winner_token_holding_id = AccountId::new([3; 32]); - let mut expected_winner_account_data = [0; 49]; - expected_winner_account_data[0] = 1; - expected_winner_account_data[1..33].copy_from_slice(pinata_token_definition_id.value()); - expected_winner_account_data[33..].copy_from_slice(&150u128.to_le_bytes()); + let expected_winner_account_holding = token_core::TokenHolding::Fungible { + definition_id: pinata_token_definition_id, + balance: 150, + }; let expected_winner_token_holding_post = Account { program_owner: token.id(), - data: expected_winner_account_data.to_vec().try_into().unwrap(), + data: Data::from(&expected_winner_account_holding), ..Account::default() }; @@ -4087,10 +4005,10 @@ pub mod tests { // Execution of the token program to create new token for the pinata token // definition and supply accounts let total_supply: u128 = 10_000_000; - // instruction: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)] - let mut instruction = vec![0; 23]; - instruction[1..17].copy_from_slice(&total_supply.to_le_bytes()); - instruction[17..].copy_from_slice(b"PINATA"); + let instruction = token_core::Instruction::NewFungibleDefinition { + name: String::from("PINATA"), + total_supply, + }; let message = public_transaction::Message::try_new( token.id(), vec![pinata_token_definition_id, pinata_token_holding_id], @@ -4102,9 +4020,8 @@ pub mod tests { let tx = PublicTransaction::new(message, witness_set); state.transition_from_public_transaction(&tx).unwrap(); - // Execution of the token program transfer just to initialize the winner token account - let mut instruction = vec![0; 23]; - instruction[0] = 2; + // Execution of winner's token holding account initialization + let instruction = token_core::Instruction::InitializeAccount; let message = public_transaction::Message::try_new( token.id(), vec![pinata_token_definition_id, winner_token_holding_id], diff --git a/program_methods/guest/Cargo.toml b/program_methods/guest/Cargo.toml index 37c1a8d9..5eb9670b 100644 --- a/program_methods/guest/Cargo.toml +++ b/program_methods/guest/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] nssa_core.workspace = true - +token_core.workspace = true +token_program.workspace = true risc0-zkvm.workspace = true serde = { workspace = true, default-features = false } diff --git a/program_methods/guest/src/bin/amm.rs b/program_methods/guest/src/bin/amm.rs index 9488db13..2c7a7f8f 100644 --- a/program_methods/guest/src/bin/amm.rs +++ b/program_methods/guest/src/bin/amm.rs @@ -152,56 +152,6 @@ impl PoolDefinition { } } -// TODO: remove repeated code for Token_Definition and TokenHoldling - -const TOKEN_HOLDING_TYPE: u8 = 1; -const TOKEN_HOLDING_DATA_SIZE: usize = 49; - -struct TokenHolding { - #[cfg_attr(not(test), expect(dead_code, reason = "TODO: fix later"))] - account_type: u8, - definition_id: AccountId, - balance: u128, -} - -impl TokenHolding { - fn parse(data: &[u8]) -> Option { - if data.len() != TOKEN_HOLDING_DATA_SIZE || data[0] != TOKEN_HOLDING_TYPE { - None - } else { - let account_type = data[0]; - let definition_id = AccountId::new( - data[1..33] - .try_into() - .expect("Defintion ID must be 32 bytes long"), - ); - let balance = u128::from_le_bytes( - data[33..] - .try_into() - .expect("balance must be 16 bytes little-endian"), - ); - Some(Self { - definition_id, - balance, - account_type, - }) - } - } - - #[cfg(test)] - fn into_data(self) -> Data { - let mut bytes = [0; TOKEN_HOLDING_DATA_SIZE]; - bytes[0] = self.account_type; - bytes[1..33].copy_from_slice(&self.definition_id.to_bytes()); - bytes[33..].copy_from_slice(&self.balance.to_le_bytes()); - - bytes - .to_vec() - .try_into() - .expect("49 bytes should fit into Data") - } -} - type Instruction = Vec; fn main() { let ( @@ -412,32 +362,6 @@ fn compute_liquidity_token_pda_seed(pool_id: AccountId) -> PdaSeed { ) } -const TOKEN_PROGRAM_NEW: u8 = 0; -const TOKEN_PROGRAM_TRANSFER: u8 = 1; -const TOKEN_PROGRAM_MINT: u8 = 4; -const TOKEN_PROGRAM_BURN: u8 = 3; - -fn initialize_token_transfer_chained_call( - token_program_command: u8, - sender: AccountWithMetadata, - recipient: AccountWithMetadata, - amount_to_move: u128, - pda_seed: Vec, -) -> ChainedCall { - let mut instruction_data = vec![0u8; 23]; - instruction_data[0] = token_program_command; - instruction_data[1..17].copy_from_slice(&amount_to_move.to_le_bytes()); - let instruction_data = risc0_zkvm::serde::to_vec(&instruction_data) - .expect("AMM Program expects valid token transfer instruction data"); - - ChainedCall { - program_id: sender.account.program_owner, - instruction_data, - pre_states: vec![sender, recipient], - pda_seeds: pda_seed, - } -} - fn new_definition( pre_states: &[AccountWithMetadata], balance_in: &[u128], @@ -471,12 +395,12 @@ fn new_definition( } // Verify token_a and token_b are different - let definition_token_a_id = TokenHolding::parse(&user_holding_a.account.data) + let definition_token_a_id = token_core::TokenHolding::try_from(&user_holding_a.account.data) .expect("New definition: AMM Program expects valid Token Holding account for Token A") - .definition_id; - let definition_token_b_id = TokenHolding::parse(&user_holding_b.account.data) + .definition_id(); + let definition_token_b_id = token_core::TokenHolding::try_from(&user_holding_b.account.data) .expect("New definition: AMM Program expects valid Token Holding account for Token B") - .definition_id; + .definition_id(); // both instances of the same token program let token_program = user_holding_a.account.program_owner; @@ -543,57 +467,48 @@ fn new_definition( AccountPostState::new(pool_post.clone()) }; - let mut chained_calls = Vec::::new(); + let token_program_id = user_holding_a.account.program_owner; // Chain call for Token A (user_holding_a -> Vault_A) - let call_token_a = initialize_token_transfer_chained_call( - TOKEN_PROGRAM_TRANSFER, - user_holding_a.clone(), - vault_a.clone(), - amount_a, - Vec::::new(), + let call_token_a = ChainedCall::new( + token_program_id, + vec![user_holding_a.clone(), vault_a.clone()], + &token_core::Instruction::Transfer { + amount_to_transfer: amount_a, + }, ); // Chain call for Token B (user_holding_b -> Vault_B) - let call_token_b = initialize_token_transfer_chained_call( - TOKEN_PROGRAM_TRANSFER, - user_holding_b.clone(), - vault_b.clone(), - amount_b, - Vec::::new(), + let call_token_b = ChainedCall::new( + token_program_id, + vec![user_holding_b.clone(), vault_b.clone()], + &token_core::Instruction::Transfer { + amount_to_transfer: amount_b, + }, ); // Chain call for liquidity token (TokenLP definition -> User LP Holding) - let mut instruction_data = vec![0u8; 23]; - instruction_data[0] = if pool.account == Account::default() { - TOKEN_PROGRAM_NEW + let instruction = if pool.account == Account::default() { + token_core::Instruction::NewFungibleDefinition { + name: String::from("LP Token"), + total_supply: amount_a, + } } else { - TOKEN_PROGRAM_MINT - }; //new or mint - let nme = if pool.account == Account::default() { - [1u8; 6] - } else { - [0u8; 6] + token_core::Instruction::Mint { + amount_to_mint: amount_a, + } }; - instruction_data[1..17].copy_from_slice(&amount_a.to_le_bytes()); - instruction_data[17..].copy_from_slice(&nme); - let instruction_data = risc0_zkvm::serde::to_vec(&instruction_data) - .expect("New definition: AMM Program expects valid instruction_data"); - let mut pool_lp_auth = pool_lp.clone(); pool_lp_auth.is_authorized = true; - let token_program_id = user_holding_a.account.program_owner; - let call_token_lp = ChainedCall { - program_id: token_program_id, - instruction_data, - pre_states: vec![pool_lp_auth.clone(), user_holding_lp.clone()], - pda_seeds: vec![compute_liquidity_token_pda_seed(pool.account_id)], - }; + let call_token_lp = ChainedCall::new( + token_program_id, + vec![pool_lp_auth.clone(), user_holding_lp.clone()], + &instruction, + ) + .with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]); - chained_calls.push(call_token_lp); - chained_calls.push(call_token_b); - chained_calls.push(call_token_a); + let chained_calls = vec![call_token_lp, call_token_b, call_token_a]; let post_states = vec![ pool_post.clone(), @@ -645,18 +560,30 @@ fn swap( // fetch pool reserves // validates reserves is at least the vaults' balances - if TokenHolding::parse(&vault_a.account.data) - .expect("Swap: AMM Program expects a valid Token Holding Account for Vault A") - .balance - < pool_def_data.reserve_a - { + let vault_a_token_holding = token_core::TokenHolding::try_from(&vault_a.account.data) + .expect("Swap: AMM Program expects a valid Token Holding Account for Vault A"); + let token_core::TokenHolding::Fungible { + definition_id: _, + balance: vault_a_balance, + } = vault_a_token_holding + else { + panic!("Swap: AMM Program expects a valid Fungible Token Holding Account for Vault A"); + }; + if vault_a_balance < pool_def_data.reserve_a { panic!("Reserve for Token A exceeds vault balance"); } - if TokenHolding::parse(&vault_b.account.data) - .expect("Swap: AMM Program expects a valid Token Holding Account for Vault B") - .balance - < pool_def_data.reserve_b - { + + let vault_b_token_holding = token_core::TokenHolding::try_from(&vault_b.account.data) + .expect("Swap: AMM Program expects a valid Token Holding Account for Vault B"); + let token_core::TokenHolding::Fungible { + definition_id: _, + balance: vault_b_balance, + } = vault_b_token_holding + else { + panic!("Swap: AMM Program expects a valid Fungible Token Holding Account for Vault B"); + }; + + if vault_b_balance < pool_def_data.reserve_b { panic!("Reserve for Token B exceeds vault balance"); } @@ -741,30 +668,37 @@ fn swap_logic( panic!("Withdraw amount should be nonzero"); } + let token_program_id = user_deposit.account.program_owner; + let mut chained_calls = Vec::new(); - chained_calls.push(initialize_token_transfer_chained_call( - TOKEN_PROGRAM_TRANSFER, - user_deposit.clone(), - vault_deposit.clone(), - deposit_amount, - Vec::::new(), + chained_calls.push(ChainedCall::new( + token_program_id, + vec![user_deposit, vault_deposit], + &token_core::Instruction::Transfer { + amount_to_transfer: deposit_amount, + }, )); let mut vault_withdraw = vault_withdraw.clone(); vault_withdraw.is_authorized = true; - chained_calls.push(initialize_token_transfer_chained_call( - TOKEN_PROGRAM_TRANSFER, - vault_withdraw.clone(), - user_withdraw.clone(), - withdraw_amount, - vec![compute_vault_pda_seed( - pool_id, - TokenHolding::parse(&vault_withdraw.account.data) - .expect("Swap Logic: AMM Program expects valid token data") - .definition_id, - )], - )); + let pda_seed = compute_vault_pda_seed( + pool_id, + token_core::TokenHolding::try_from(&vault_withdraw.account.data) + .expect("Swap Logic: AMM Program expects valid token data") + .definition_id(), + ); + + chained_calls.push( + ChainedCall::new( + token_program_id, + vec![vault_withdraw, user_withdraw], + &token_core::Instruction::Transfer { + amount_to_transfer: withdraw_amount, + }, + ) + .with_pda_seeds(vec![pda_seed]), + ); (chained_calls, deposit_amount, withdraw_amount) } @@ -816,12 +750,29 @@ fn add_liquidity( } // 2. Determine deposit amount - let vault_b_balance = TokenHolding::parse(&vault_b.account.data) - .expect("Add liquidity: AMM Program expects valid Token Holding Account for Vault B") - .balance; - let vault_a_balance = TokenHolding::parse(&vault_a.account.data) - .expect("Add liquidity: AMM Program expects valid Token Holding Account for Vault A") - .balance; + let vault_b_token_holding = token_core::TokenHolding::try_from(&vault_b.account.data) + .expect("Add liquidity: AMM Program expects valid Token Holding Account for Vault B"); + let token_core::TokenHolding::Fungible { + definition_id: _, + balance: vault_b_balance, + } = vault_b_token_holding + else { + panic!( + "Add liquidity: AMM Program expects valid Fungible Token Holding Account for Vault B" + ); + }; + + let vault_a_token_holding = token_core::TokenHolding::try_from(&vault_a.account.data) + .expect("Add liquidity: AMM Program expects valid Token Holding Account for Vault A"); + let token_core::TokenHolding::Fungible { + definition_id: _, + balance: vault_a_balance, + } = vault_a_token_holding + else { + panic!( + "Add liquidity: AMM Program expects valid Fungible Token Holding Account for Vault A" + ); + }; if pool_def_data.reserve_a == 0 || pool_def_data.reserve_b == 0 { panic!("Reserves must be nonzero"); @@ -879,38 +830,37 @@ fn add_liquidity( }; pool_post.data = pool_post_definition.into_data(); - let mut chained_call = Vec::new(); + let token_program_id = user_holding_a.account.program_owner; // Chain call for Token A (UserHoldingA -> Vault_A) - let call_token_a = initialize_token_transfer_chained_call( - TOKEN_PROGRAM_TRANSFER, - user_holding_a.clone(), - vault_a.clone(), - actual_amount_a, - Vec::::new(), + let call_token_a = ChainedCall::new( + token_program_id, + vec![user_holding_a.clone(), vault_a.clone()], + &token_core::Instruction::Transfer { + amount_to_transfer: actual_amount_a, + }, ); // Chain call for Token B (UserHoldingB -> Vault_B) - let call_token_b = initialize_token_transfer_chained_call( - TOKEN_PROGRAM_TRANSFER, - user_holding_b.clone(), - vault_b.clone(), - actual_amount_b, - Vec::::new(), + let call_token_b = ChainedCall::new( + token_program_id, + vec![user_holding_b.clone(), vault_b.clone()], + &token_core::Instruction::Transfer { + amount_to_transfer: actual_amount_b, + }, ); // Chain call for LP (mint new tokens for user_holding_lp) let mut pool_definition_lp_auth = pool_definition_lp.clone(); pool_definition_lp_auth.is_authorized = true; - let call_token_lp = initialize_token_transfer_chained_call( - TOKEN_PROGRAM_MINT, - pool_definition_lp_auth.clone(), - user_holding_lp.clone(), - delta_lp, - vec![compute_liquidity_token_pda_seed(pool.account_id)], - ); + let call_token_lp = ChainedCall::new( + token_program_id, + vec![pool_definition_lp_auth.clone(), user_holding_lp.clone()], + &token_core::Instruction::Mint { + amount_to_mint: delta_lp, + }, + ) + .with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]); - chained_call.push(call_token_lp); - chained_call.push(call_token_b); - chained_call.push(call_token_a); + let chained_calls = vec![call_token_lp, call_token_b, call_token_a]; let post_states = vec![ AccountPostState::new(pool_post), @@ -922,7 +872,7 @@ fn add_liquidity( AccountPostState::new(pre_states[6].account.clone()), ]; - (post_states, chained_call) + (post_states, chained_calls) } fn remove_liquidity( @@ -986,11 +936,20 @@ fn remove_liquidity( } // 2. Compute withdrawal amounts - let user_holding_lp_data = TokenHolding::parse(&user_holding_lp.account.data) + let user_holding_lp_data = token_core::TokenHolding::try_from(&user_holding_lp.account.data) .expect("Remove liquidity: AMM Program expects a valid Token Account for liquidity token"); + let token_core::TokenHolding::Fungible { + definition_id: _, + balance: user_lp_balance, + } = user_holding_lp_data + else { + panic!( + "Remove liquidity: AMM Program expects a valid Fungible Token Holding Account for liquidity token" + ); + }; - if user_holding_lp_data.balance > pool_def_data.liquidity_pool_supply - || user_holding_lp_data.definition_id != pool_def_data.liquidity_pool_id + if user_lp_balance > pool_def_data.liquidity_pool_supply + || user_holding_lp_data.definition_id() != pool_def_data.liquidity_pool_id { panic!("Invalid liquidity account provided"); } @@ -1026,44 +985,45 @@ fn remove_liquidity( pool_post.data = pool_post_definition.into_data(); - let mut chained_calls = Vec::new(); + let token_program_id = user_holding_a.account.program_owner; // Chaincall for Token A withdraw - let call_token_a = initialize_token_transfer_chained_call( - TOKEN_PROGRAM_TRANSFER, - running_vault_a, - user_holding_a.clone(), - withdraw_amount_a, - vec![compute_vault_pda_seed( - pool.account_id, - pool_def_data.definition_token_a_id, - )], - ); + let call_token_a = ChainedCall::new( + token_program_id, + vec![running_vault_a, user_holding_a.clone()], + &token_core::Instruction::Transfer { + amount_to_transfer: withdraw_amount_a, + }, + ) + .with_pda_seeds(vec![compute_vault_pda_seed( + pool.account_id, + pool_def_data.definition_token_a_id, + )]); // Chaincall for Token B withdraw - let call_token_b = initialize_token_transfer_chained_call( - TOKEN_PROGRAM_TRANSFER, - running_vault_b, - user_holding_b.clone(), - withdraw_amount_b, - vec![compute_vault_pda_seed( - pool.account_id, - pool_def_data.definition_token_b_id, - )], - ); + let call_token_b = ChainedCall::new( + token_program_id, + vec![running_vault_b, user_holding_b.clone()], + &token_core::Instruction::Transfer { + amount_to_transfer: withdraw_amount_b, + }, + ) + .with_pda_seeds(vec![compute_vault_pda_seed( + pool.account_id, + pool_def_data.definition_token_b_id, + )]); // Chaincall for LP adjustment let mut pool_definition_lp_auth = pool_definition_lp.clone(); pool_definition_lp_auth.is_authorized = true; - let call_token_lp = initialize_token_transfer_chained_call( - TOKEN_PROGRAM_BURN, - pool_definition_lp_auth.clone(), - user_holding_lp.clone(), - delta_lp, - vec![compute_liquidity_token_pda_seed(pool.account_id)], - ); + let call_token_lp = ChainedCall::new( + token_program_id, + vec![pool_definition_lp_auth, user_holding_lp.clone()], + &token_core::Instruction::Burn { + amount_to_burn: delta_lp, + }, + ) + .with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]); - chained_calls.push(call_token_lp); - chained_calls.push(call_token_b); - chained_calls.push(call_token_a); + let chained_calls = vec![call_token_lp, call_token_b, call_token_a]; let post_states = vec![ AccountPostState::new(pool_post.clone()), @@ -1082,41 +1042,18 @@ fn remove_liquidity( mod tests { use nssa_core::{ account::{Account, AccountId, AccountWithMetadata, Data}, - program::{ChainedCall, PdaSeed, ProgramId}, + program::{ChainedCall, ProgramId}, }; + use token_core::{TokenDefinition, TokenHolding}; use crate::{ - PoolDefinition, TokenHolding, add_liquidity, compute_liquidity_token_pda, + PoolDefinition, add_liquidity, compute_liquidity_token_pda, compute_liquidity_token_pda_seed, compute_pool_pda, compute_vault_pda, compute_vault_pda_seed, new_definition, remove_liquidity, swap, }; const TOKEN_PROGRAM_ID: ProgramId = [15; 8]; const AMM_PROGRAM_ID: ProgramId = [42; 8]; - const TOKEN_DEFINITION_DATA_SIZE: usize = 55; - - struct TokenDefinition { - account_type: u8, - name: [u8; 6], - total_supply: u128, - metadata_id: AccountId, - } - - impl TokenDefinition { - fn into_data(self) -> Data { - let mut bytes = Vec::::new(); - bytes.extend_from_slice(&[self.account_type]); - bytes.extend_from_slice(&self.name); - bytes.extend_from_slice(&self.total_supply.to_le_bytes()); - bytes.extend_from_slice(&self.metadata_id.to_bytes()); - - if bytes.len() != TOKEN_DEFINITION_DATA_SIZE { - panic!("Invalid Token Definition data"); - } - - Data::try_from(bytes).expect("Token definition data size must fit into data") - } - } struct BalanceForTests; @@ -1250,21 +1187,16 @@ mod tests { impl ChainedCallForTests { fn cc_swap_token_a_test_1() -> ChainedCall { - let mut instruction_data = vec![0; 23]; - instruction_data[0] = 1; - instruction_data[1..17] - .copy_from_slice(&BalanceForTests::add_max_amount_a().to_le_bytes()); - let instruction_data = risc0_zkvm::serde::to_vec(&instruction_data) - .expect("AMM Program expects valid transaction instruction data"); - ChainedCall { - program_id: TOKEN_PROGRAM_ID, - instruction_data, - pre_states: vec![ + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![ AccountForTests::user_holding_a(), AccountForTests::vault_a_init(), ], - pda_seeds: Vec::::new(), - } + &token_core::Instruction::Transfer { + amount_to_transfer: BalanceForTests::add_max_amount_a(), + }, + ) } fn cc_swap_token_b_test_1() -> ChainedCall { @@ -1273,20 +1205,17 @@ mod tests { let mut vault_b_auth = AccountForTests::vault_b_init(); vault_b_auth.is_authorized = true; - let mut instruction = vec![0; 23]; - instruction[0] = 1; - instruction[1..17].copy_from_slice(&swap_amount.to_le_bytes()); - let instruction_data = risc0_zkvm::serde::to_vec(&instruction) - .expect("AMM Program expects valid transaction instruction data"); - ChainedCall { - program_id: TOKEN_PROGRAM_ID, - instruction_data, - pre_states: vec![vault_b_auth, AccountForTests::user_holding_b()], - pda_seeds: vec![compute_vault_pda_seed( - IdForTests::pool_definition_id(), - IdForTests::token_b_definition_id(), - )], - } + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![vault_b_auth, AccountForTests::user_holding_b()], + &token_core::Instruction::Transfer { + amount_to_transfer: swap_amount, + }, + ) + .with_pda_seeds(vec![compute_vault_pda_seed( + IdForTests::pool_definition_id(), + IdForTests::token_b_definition_id(), + )]) } fn cc_swap_token_a_test_2() -> ChainedCall { @@ -1295,214 +1224,164 @@ mod tests { let mut vault_a_auth = AccountForTests::vault_a_init(); vault_a_auth.is_authorized = true; - let mut instruction_data = vec![0; 23]; - instruction_data[0] = 1; - instruction_data[1..17].copy_from_slice(&swap_amount.to_le_bytes()); - let instruction_data = risc0_zkvm::serde::to_vec(&instruction_data) - .expect("AMM Program expects valid transaction instruction data"); - ChainedCall { - program_id: TOKEN_PROGRAM_ID, - instruction_data, - pre_states: vec![vault_a_auth, AccountForTests::user_holding_a()], - pda_seeds: vec![compute_vault_pda_seed( - IdForTests::pool_definition_id(), - IdForTests::token_a_definition_id(), - )], - } + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![vault_a_auth, AccountForTests::user_holding_a()], + &token_core::Instruction::Transfer { + amount_to_transfer: swap_amount, + }, + ) + .with_pda_seeds(vec![compute_vault_pda_seed( + IdForTests::pool_definition_id(), + IdForTests::token_a_definition_id(), + )]) } fn cc_swap_token_b_test_2() -> ChainedCall { - let mut instruction = vec![0; 23]; - instruction[0] = 1; - instruction[1..17].copy_from_slice(&BalanceForTests::add_max_amount_b().to_le_bytes()); - let instruction_data = risc0_zkvm::serde::to_vec(&instruction) - .expect("AMM Program expects valid transaction instruction data"); - ChainedCall { - program_id: TOKEN_PROGRAM_ID, - instruction_data, - pre_states: vec![ + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![ AccountForTests::user_holding_b(), AccountForTests::vault_b_init(), ], - pda_seeds: Vec::::new(), - } + &token_core::Instruction::Transfer { + amount_to_transfer: BalanceForTests::add_max_amount_b(), + }, + ) } fn cc_add_token_a() -> ChainedCall { - let mut instruction = vec![0u8; 23]; - instruction[0] = 1; - instruction[1..17] - .copy_from_slice(&BalanceForTests::add_successful_amount_a().to_le_bytes()); - let instruction_data = risc0_zkvm::serde::to_vec(&instruction) - .expect("AMM Program expects valid transaction instruction data"); - ChainedCall { - program_id: TOKEN_PROGRAM_ID, - instruction_data, - pre_states: vec![ + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![ AccountForTests::user_holding_a(), AccountForTests::vault_a_init(), ], - pda_seeds: Vec::::new(), - } + &token_core::Instruction::Transfer { + amount_to_transfer: BalanceForTests::add_successful_amount_a(), + }, + ) } fn cc_add_token_b() -> ChainedCall { - let mut instruction = vec![0u8; 23]; - instruction[0] = 1; - instruction[1..17] - .copy_from_slice(&BalanceForTests::add_successful_amount_b().to_le_bytes()); - let instruction_data = risc0_zkvm::serde::to_vec(&instruction) - .expect("Swap Logic: AMM Program expects valid transaction instruction data"); - ChainedCall { - program_id: TOKEN_PROGRAM_ID, - instruction_data, - pre_states: vec![ + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![ AccountForTests::user_holding_b(), AccountForTests::vault_b_init(), ], - pda_seeds: Vec::::new(), - } + &token_core::Instruction::Transfer { + amount_to_transfer: BalanceForTests::add_successful_amount_b(), + }, + ) } fn cc_add_pool_lp() -> ChainedCall { let mut pool_lp_auth = AccountForTests::pool_lp_init(); pool_lp_auth.is_authorized = true; - let mut instruction = vec![0u8; 23]; - instruction[0] = 4; - instruction[1..17] - .copy_from_slice(&BalanceForTests::add_successful_amount_a().to_le_bytes()); - let instruction_data = risc0_zkvm::serde::to_vec(&instruction) - .expect("Swap Logic: AMM Program expects valid transaction instruction data"); - ChainedCall { - program_id: TOKEN_PROGRAM_ID, - instruction_data, - pre_states: vec![pool_lp_auth, AccountForTests::user_holding_lp_init()], - pda_seeds: vec![compute_liquidity_token_pda_seed( - IdForTests::pool_definition_id(), - )], - } + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![pool_lp_auth, AccountForTests::user_holding_lp_init()], + &token_core::Instruction::Mint { + amount_to_mint: BalanceForTests::add_successful_amount_a(), + }, + ) + .with_pda_seeds(vec![compute_liquidity_token_pda_seed( + IdForTests::pool_definition_id(), + )]) } fn cc_remove_token_a() -> ChainedCall { let mut vault_a_auth = AccountForTests::vault_a_init(); vault_a_auth.is_authorized = true; - let mut instruction = vec![0; 23]; - instruction[0] = 1; - instruction[1..17] - .copy_from_slice(&BalanceForTests::remove_actual_a_successful().to_le_bytes()); - let instruction_data = risc0_zkvm::serde::to_vec(&instruction) - .expect("AMM Program expects valid transaction instruction data"); - ChainedCall { - program_id: TOKEN_PROGRAM_ID, - instruction_data, - pre_states: vec![vault_a_auth, AccountForTests::user_holding_a()], - pda_seeds: vec![compute_vault_pda_seed( - IdForTests::pool_definition_id(), - IdForTests::token_a_definition_id(), - )], - } + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![vault_a_auth, AccountForTests::user_holding_a()], + &token_core::Instruction::Transfer { + amount_to_transfer: BalanceForTests::remove_actual_a_successful(), + }, + ) + .with_pda_seeds(vec![compute_vault_pda_seed( + IdForTests::pool_definition_id(), + IdForTests::token_a_definition_id(), + )]) } fn cc_remove_token_b() -> ChainedCall { let mut vault_b_auth = AccountForTests::vault_b_init(); vault_b_auth.is_authorized = true; - let mut instruction = vec![0; 23]; - instruction[0] = 1; - instruction[1..17] - .copy_from_slice(&BalanceForTests::remove_min_amount_b_low().to_le_bytes()); - let instruction_data = risc0_zkvm::serde::to_vec(&instruction) - .expect("AMM Program expects valid transaction instruction data"); - ChainedCall { - program_id: TOKEN_PROGRAM_ID, - instruction_data, - pre_states: vec![vault_b_auth, AccountForTests::user_holding_b()], - pda_seeds: vec![compute_vault_pda_seed( - IdForTests::pool_definition_id(), - IdForTests::token_b_definition_id(), - )], - } + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![vault_b_auth, AccountForTests::user_holding_b()], + &token_core::Instruction::Transfer { + amount_to_transfer: BalanceForTests::remove_min_amount_b_low(), + }, + ) + .with_pda_seeds(vec![compute_vault_pda_seed( + IdForTests::pool_definition_id(), + IdForTests::token_b_definition_id(), + )]) } fn cc_remove_pool_lp() -> ChainedCall { let mut pool_lp_auth = AccountForTests::pool_lp_init(); pool_lp_auth.is_authorized = true; - let mut instruction = vec![0; 23]; - instruction[0] = 3; - instruction[1..17] - .copy_from_slice(&BalanceForTests::remove_actual_a_successful().to_le_bytes()); - let instruction_data = risc0_zkvm::serde::to_vec(&instruction) - .expect("AMM Program expects valid transaction instruction data"); - ChainedCall { - program_id: TOKEN_PROGRAM_ID, - instruction_data, - pre_states: vec![ - AccountForTests::pool_lp_init(), - AccountForTests::user_holding_lp_init(), - ], - pda_seeds: vec![compute_liquidity_token_pda_seed( - IdForTests::pool_definition_id(), - )], - } + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![pool_lp_auth, AccountForTests::user_holding_lp_init()], + &token_core::Instruction::Burn { + amount_to_burn: BalanceForTests::remove_amount_lp(), + }, + ) + .with_pda_seeds(vec![compute_liquidity_token_pda_seed( + IdForTests::pool_definition_id(), + )]) } fn cc_new_definition_token_a() -> ChainedCall { - let mut instruction = vec![0; 23]; - instruction[0] = 1; - instruction[1..17] - .copy_from_slice(&BalanceForTests::add_successful_amount_a().to_le_bytes()); - let instruction_data = risc0_zkvm::serde::to_vec(&instruction) - .expect("AMM Program expects valid transaction instruction data"); - ChainedCall { - program_id: TOKEN_PROGRAM_ID, - instruction_data, - pre_states: vec![ + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![ AccountForTests::user_holding_a(), AccountForTests::vault_a_init(), ], - pda_seeds: Vec::::new(), - } + &token_core::Instruction::Transfer { + amount_to_transfer: BalanceForTests::add_successful_amount_a(), + }, + ) } fn cc_new_definition_token_b() -> ChainedCall { - let mut instruction = vec![0; 23]; - instruction[0] = 1; - instruction[1..17] - .copy_from_slice(&BalanceForTests::add_successful_amount_b().to_le_bytes()); - let instruction_data = risc0_zkvm::serde::to_vec(&instruction) - .expect("Swap Logic: AMM Program expects valid transaction instruction data"); - ChainedCall { - program_id: TOKEN_PROGRAM_ID, - instruction_data, - pre_states: vec![ + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![ AccountForTests::user_holding_b(), AccountForTests::vault_b_init(), ], - pda_seeds: Vec::::new(), - } + &token_core::Instruction::Transfer { + amount_to_transfer: BalanceForTests::add_successful_amount_b(), + }, + ) } fn cc_new_definition_token_lp() -> ChainedCall { - let mut instruction = vec![0; 23]; - instruction[0] = 1; - instruction[1..17] - .copy_from_slice(&BalanceForTests::add_successful_amount_a().to_le_bytes()); - let instruction_data = risc0_zkvm::serde::to_vec(&instruction) - .expect("AMM Program expects valid transaction instruction data"); - ChainedCall { - program_id: TOKEN_PROGRAM_ID, - instruction_data, - pre_states: vec![ + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![ AccountForTests::pool_lp_init(), AccountForTests::user_holding_lp_uninit(), ], - pda_seeds: vec![compute_liquidity_token_pda_seed( - IdForTests::pool_definition_id(), - )], - } + &token_core::Instruction::Mint { + amount_to_mint: BalanceForTests::add_successful_amount_a(), + }, + ) + .with_pda_seeds(vec![compute_liquidity_token_pda_seed( + IdForTests::pool_definition_id(), + )]) } } @@ -1566,8 +1445,7 @@ mod tests { account: Account { program_owner: TOKEN_PROGRAM_ID, balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_a_definition_id(), balance: BalanceForTests::user_token_a_balance(), }), @@ -1583,8 +1461,7 @@ mod tests { account: Account { program_owner: TOKEN_PROGRAM_ID, balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_b_definition_id(), balance: BalanceForTests::user_token_b_balance(), }), @@ -1600,8 +1477,7 @@ mod tests { account: Account { program_owner: TOKEN_PROGRAM_ID, balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_a_definition_id(), balance: BalanceForTests::vault_a_reserve_init(), }), @@ -1617,8 +1493,7 @@ mod tests { account: Account { program_owner: TOKEN_PROGRAM_ID, balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_b_definition_id(), balance: BalanceForTests::vault_b_reserve_init(), }), @@ -1634,8 +1509,7 @@ mod tests { account: Account { program_owner: TOKEN_PROGRAM_ID, balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_a_definition_id(), balance: BalanceForTests::vault_a_reserve_high(), }), @@ -1651,8 +1525,7 @@ mod tests { account: Account { program_owner: TOKEN_PROGRAM_ID, balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_b_definition_id(), balance: BalanceForTests::vault_b_reserve_high(), }), @@ -1668,8 +1541,7 @@ mod tests { account: Account { program_owner: TOKEN_PROGRAM_ID, balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_a_definition_id(), balance: BalanceForTests::vault_a_reserve_low(), }), @@ -1685,8 +1557,7 @@ mod tests { account: Account { program_owner: TOKEN_PROGRAM_ID, balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_b_definition_id(), balance: BalanceForTests::vault_b_reserve_low(), }), @@ -1702,8 +1573,7 @@ mod tests { account: Account { program_owner: TOKEN_PROGRAM_ID, balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_a_definition_id(), balance: 0, }), @@ -1719,8 +1589,7 @@ mod tests { account: Account { program_owner: TOKEN_PROGRAM_ID, balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_b_definition_id(), balance: 0, }), @@ -1736,11 +1605,10 @@ mod tests { account: Account { program_owner: TOKEN_PROGRAM_ID, balance: 0u128, - data: TokenDefinition::into_data(TokenDefinition { - account_type: 0u8, - name: [1; 6], + data: Data::from(&TokenDefinition::Fungible { + name: String::from("test"), total_supply: BalanceForTests::vault_a_reserve_init(), - metadata_id: AccountId::new([0; 32]), + metadata_id: None, }), nonce: 0, }, @@ -1754,11 +1622,10 @@ mod tests { account: Account { program_owner: TOKEN_PROGRAM_ID, balance: 0u128, - data: TokenDefinition::into_data(TokenDefinition { - account_type: 0u8, - name: [1; 6], + data: Data::from(&TokenDefinition::Fungible { + name: String::from("test"), total_supply: BalanceForTests::vault_a_reserve_init(), - metadata_id: AccountId::new([0; 32]), + metadata_id: None, }), nonce: 0, }, @@ -1772,8 +1639,7 @@ mod tests { account: Account { program_owner: TOKEN_PROGRAM_ID, balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_lp_definition_id(), balance: 0, }), @@ -1789,8 +1655,7 @@ mod tests { account: Account { program_owner: TOKEN_PROGRAM_ID, balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_lp_definition_id(), balance: BalanceForTests::user_token_lp_balance(), }), @@ -2102,8 +1967,7 @@ mod tests { account: Account { program_owner: TOKEN_PROGRAM_ID, balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_a_definition_id(), balance: BalanceForTests::vault_a_reserve_init(), }), @@ -2119,8 +1983,7 @@ mod tests { account: Account { program_owner: TOKEN_PROGRAM_ID, balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: 1u8, + data: Data::from(&TokenHolding::Fungible { definition_id: IdForTests::token_b_definition_id(), balance: BalanceForTests::vault_b_reserve_init(), }), @@ -3552,8 +3415,14 @@ mod tests { let chained_call_a = chained_calls[0].clone(); let chained_call_b = chained_calls[1].clone(); - assert!(chained_call_a == ChainedCallForTests::cc_swap_token_a_test_1()); - assert!(chained_call_b == ChainedCallForTests::cc_swap_token_b_test_1()); + assert_eq!( + chained_call_a, + ChainedCallForTests::cc_swap_token_a_test_1() + ); + assert_eq!( + chained_call_b, + ChainedCallForTests::cc_swap_token_b_test_1() + ); } #[test] @@ -3581,7 +3450,13 @@ mod tests { let chained_call_a = chained_calls[1].clone(); let chained_call_b = chained_calls[0].clone(); - assert!(chained_call_a == ChainedCallForTests::cc_swap_token_a_test_2()); - assert!(chained_call_b == ChainedCallForTests::cc_swap_token_b_test_2()); + assert_eq!( + chained_call_a, + ChainedCallForTests::cc_swap_token_a_test_2() + ); + assert_eq!( + chained_call_b, + ChainedCallForTests::cc_swap_token_b_test_2() + ); } } diff --git a/program_methods/guest/src/bin/pinata_token.rs b/program_methods/guest/src/bin/pinata_token.rs index 04613791..188597cb 100644 --- a/program_methods/guest/src/bin/pinata_token.rs +++ b/program_methods/guest/src/bin/pinata_token.rs @@ -5,10 +5,7 @@ use nssa_core::{ write_nssa_outputs_with_chained_call, }, }; -use risc0_zkvm::{ - serde::to_vec, - sha::{Impl, Sha256}, -}; +use risc0_zkvm::sha::{Impl, Sha256}; const PRIZE: u128 = 150; @@ -82,23 +79,21 @@ fn main() { let winner_token_holding_post = winner_token_holding.account.clone(); pinata_definition_post.data = data.next_data(); - let mut instruction_data = vec![0; 23]; - instruction_data[0] = 1; - instruction_data[1..17].copy_from_slice(&PRIZE.to_le_bytes()); - // Flip authorization to true for chained call let mut pinata_token_holding_for_chain_call = pinata_token_holding.clone(); pinata_token_holding_for_chain_call.is_authorized = true; - let chained_calls = vec![ChainedCall { - program_id: pinata_token_holding_post.program_owner, - instruction_data: to_vec(&instruction_data).unwrap(), - pre_states: vec![ + let chained_call = ChainedCall::new( + pinata_token_holding_post.program_owner, + vec![ pinata_token_holding_for_chain_call, winner_token_holding.clone(), ], - pda_seeds: vec![PdaSeed::new([0; 32])], - }]; + &token_core::Instruction::Transfer { + amount_to_transfer: PRIZE, + }, + ) + .with_pda_seeds(vec![PdaSeed::new([0; 32])]); write_nssa_outputs_with_chained_call( instruction_words, @@ -112,6 +107,6 @@ fn main() { AccountPostState::new(pinata_token_holding_post), AccountPostState::new(winner_token_holding_post), ], - chained_calls, + vec![chained_call], ); } diff --git a/program_methods/guest/src/bin/token.rs b/program_methods/guest/src/bin/token.rs index 0f7b6287..0bc3d245 100644 --- a/program_methods/guest/src/bin/token.rs +++ b/program_methods/guest/src/bin/token.rs @@ -1,700 +1,13 @@ -use nssa_core::{ - account::{Account, AccountId, AccountWithMetadata, Data}, - program::{ - AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs, - }, -}; - -// The token program has three functions: -// 1. New token definition. Arguments to this function are: -// * Two **default** accounts: [definition_account, holding_account]. The first default account -// will be initialized with the token definition account values. The second account will be -// initialized to a token holding account for the new token, holding the entire total supply. -// * An instruction data of 23-bytes, indicating the total supply and the token name, with the -// following layout: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)] The -// name cannot be equal to [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] -// 2. Token transfer Arguments to this function are: -// * Two accounts: [sender_account, recipient_account]. -// * An instruction data byte string of length 23, indicating the total supply with the -// following layout [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 -// || 0x00 || 0x00]. -// 3. Initialize account with zero balance Arguments to this function are: -// * Two accounts: [definition_account, account_to_initialize]. -// * An dummy byte string of length 23, with the following layout [0x02 || 0x00 || 0x00 || 0x00 -// || ... || 0x00 || 0x00]. -// 4. Burn tokens from a Token Holding account (thus lowering total supply) Arguments to this -// function are: -// * Two accounts: [definition_account, holding_account]. -// * Authorization required: holding_account -// * An instruction data byte string of length 23, indicating the balance to burn with the -// folloiwng layout -// [0x03 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. -// 5. Mint additional supply of tokens tokens to a Token Holding account (thus increasing total -// supply) Arguments to this function are: -// * Two accounts: [definition_account, holding_account]. -// * Authorization required: definition_account -// * An instruction data byte string of length 23, indicating the balance to mint with the -// folloiwng layout -// [0x04 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. -// 6. New token definition with metadata. Arguments to this function are: -// * Three **default** accounts: [definition_account, metadata_account. holding_account]. The -// first default account will be initialized with the token definition account values. The -// second account will be initialized to a token metadata account for the new token -// definition. The third account will be initialized to a token holding account for the new -// token, holding the entire total supply. -// * An instruction data of 474-bytes, indicating the token name, total supply, token standard, -// metadata standard and metadata_values (uri and creators). the following layout: [0x05 || -// total_supply (little-endian 16 bytes) || name (6 bytes) || token_standard || -// metadata_standard || metadata_values] The name cannot be equal to [0x00, 0x00, 0x00, 0x00, -// 0x00, 0x00] -// 7. Print NFT copy from Master NFT Arguments to this function are: -// * Two accounts: [master_nft, printed_account (default)]. -// * Authorization required: master_nft -// * An dummy byte string of length 23, with the following layout [0x06 || 0x00 || 0x00 || 0x00 -// || ... || 0x00 || 0x00]. -const TOKEN_STANDARD_FUNGIBLE_TOKEN: u8 = 0; -const TOKEN_STANDARD_FUNGIBLE_ASSET: u8 = 1; -const TOKEN_STANDARD_NONFUNGIBLE: u8 = 2; -const TOKEN_STANDARD_NONFUNGIBLE_PRINTABLE: u8 = 3; - -const METADATA_TYPE_SIMPLE: u8 = 0; -const METADATA_TYPE_EXPANDED: u8 = 1; - -const TOKEN_DEFINITION_DATA_SIZE: usize = 55; - -const TOKEN_HOLDING_STANDARD: u8 = 1; -const TOKEN_HOLDING_NFT_MASTER: u8 = 2; -const TOKEN_HOLDING_NFT_PRINTED_COPY: u8 = 3; - -const TOKEN_HOLDING_DATA_SIZE: usize = 49; -const CURRENT_VERSION: u8 = 1; - -const TOKEN_METADATA_DATA_SIZE: usize = 463; - -fn is_token_standard_valid(standard: u8) -> bool { - matches!( - standard, - TOKEN_STANDARD_FUNGIBLE_TOKEN - | TOKEN_STANDARD_FUNGIBLE_ASSET - | TOKEN_STANDARD_NONFUNGIBLE - | TOKEN_STANDARD_NONFUNGIBLE_PRINTABLE - ) -} - -fn is_metadata_type_valid(standard: u8) -> bool { - matches!(standard, METADATA_TYPE_SIMPLE | METADATA_TYPE_EXPANDED) -} - -fn is_token_holding_type_valid(standard: u8) -> bool { - matches!(standard, |TOKEN_HOLDING_STANDARD| TOKEN_HOLDING_NFT_MASTER - | TOKEN_HOLDING_NFT_PRINTED_COPY) -} - -struct TokenDefinition { - account_type: u8, - name: [u8; 6], - total_supply: u128, - metadata_id: AccountId, -} - -impl TokenDefinition { - fn into_data(self) -> Data { - let mut bytes = Vec::::new(); - bytes.extend_from_slice(&[self.account_type]); - bytes.extend_from_slice(&self.name); - bytes.extend_from_slice(&self.total_supply.to_le_bytes()); - bytes.extend_from_slice(&self.metadata_id.to_bytes()); - - if bytes.len() != TOKEN_DEFINITION_DATA_SIZE { - panic!("Invalid Token Definition data"); - } - - Data::try_from(bytes).expect("Token definition data size must fit into data") - } - - fn parse(data: &Data) -> Option { - let data = Vec::::from(data.clone()); - - if data.len() != TOKEN_DEFINITION_DATA_SIZE { - None - } else { - let account_type = data[0]; - let name = data[1..7].try_into().expect("Name must be a 6 bytes"); - let total_supply = u128::from_le_bytes( - data[7..23] - .try_into() - .expect("Total supply must be 16 bytes little-endian"), - ); - let metadata_id = AccountId::new( - data[23..TOKEN_DEFINITION_DATA_SIZE] - .try_into() - .expect("Token Program expects valid Account Id for Metadata"), - ); - - let this = Some(Self { - account_type, - name, - total_supply, - metadata_id, - }); - - match account_type { - TOKEN_STANDARD_NONFUNGIBLE if total_supply != 1 => None, - TOKEN_STANDARD_FUNGIBLE_TOKEN if metadata_id != AccountId::new([0; 32]) => None, - _ => this, - } - } - } -} - -struct TokenHolding { - account_type: u8, - definition_id: AccountId, - balance: u128, -} - -impl TokenHolding { - fn new(definition_id: &AccountId) -> Self { - Self { - account_type: TOKEN_HOLDING_STANDARD, - definition_id: *definition_id, - balance: 0, - } - } - - fn parse(data: &Data) -> Option { - let data = Vec::::from(data.clone()); - - if data.len() != TOKEN_HOLDING_DATA_SIZE { - return None; - } - - // Check account_type - if !is_token_holding_type_valid(data[0]) { - return None; - } - - let account_type = data[0]; - let definition_id = AccountId::new( - data[1..33] - .try_into() - .expect("Defintion ID must be 32 bytes long"), - ); - let balance = u128::from_le_bytes( - data[33..] - .try_into() - .expect("balance must be 16 bytes little-endian"), - ); - - Some(Self { - definition_id, - balance, - account_type, - }) - } - - fn into_data(self) -> Data { - if !is_token_holding_type_valid(self.account_type) { - panic!("Invalid Token Holding type"); - } - - let mut bytes = Vec::::new(); - bytes.extend_from_slice(&[self.account_type]); - bytes.extend_from_slice(&self.definition_id.to_bytes()); - bytes.extend_from_slice(&self.balance.to_le_bytes()); - - if bytes.len() != TOKEN_HOLDING_DATA_SIZE { - panic!("Invalid Token Holding data"); - } - - Data::try_from(bytes).expect("Invalid data") - } -} - -struct TokenMetadata { - account_type: u8, - version: u8, - definition_id: AccountId, - uri: [u8; 200], - creators: [u8; 250], - /// Block id - primary_sale_date: u64, -} - -impl TokenMetadata { - fn into_data(self) -> Data { - if !is_metadata_type_valid(self.account_type) { - panic!("Invalid Metadata type"); - } - - let mut bytes = Vec::::new(); - bytes.extend_from_slice(&[self.account_type]); - bytes.extend_from_slice(&[self.version]); - bytes.extend_from_slice(&self.definition_id.to_bytes()); - bytes.extend_from_slice(&self.uri); - bytes.extend_from_slice(&self.creators); - bytes.extend_from_slice(&self.primary_sale_date.to_le_bytes()); - - if bytes.len() != TOKEN_METADATA_DATA_SIZE { - panic!("Invalid Token Definition data length"); - } - - Data::try_from(bytes).expect("Invalid data") - } -} - -fn transfer(pre_states: &[AccountWithMetadata], balance_to_move: u128) -> Vec { - if pre_states.len() != 2 { - panic!("Invalid number of input accounts"); - } - let sender = &pre_states[0]; - let recipient = &pre_states[1]; - - if !sender.is_authorized { - panic!("Sender authorization is missing"); - } - - let sender_holding = TokenHolding::parse(&sender.account.data).expect("Invalid sender data"); - - let recipient_holding = if recipient.account == Account::default() { - TokenHolding::new(&sender_holding.definition_id) - } else { - TokenHolding::parse(&recipient.account.data).expect("Invalid recipient data") - }; - - if sender_holding.definition_id != recipient_holding.definition_id { - panic!("Sender and recipient definition id mismatch"); - } - - let (sender_holding, recipient_holding) = - if sender_holding.account_type != TOKEN_HOLDING_NFT_MASTER { - standard_transfer(sender_holding, recipient_holding, balance_to_move) - } else { - nft_master_transfer(sender_holding, recipient_holding, balance_to_move) - }; - - let sender_post = { - let mut this = sender.account.clone(); - this.data = sender_holding.into_data(); - AccountPostState::new(this) - }; - - let recipient_post = { - let mut this = recipient.account.clone(); - this.data = recipient_holding.into_data(); - - // Claim the recipient account if it has default program owner - if this.program_owner == DEFAULT_PROGRAM_ID { - AccountPostState::new_claimed(this) - } else { - AccountPostState::new(this) - } - }; - - vec![sender_post, recipient_post] -} - -fn standard_transfer( - sender_holding: TokenHolding, - recipient_holding: TokenHolding, - balance_to_move: u128, -) -> (TokenHolding, TokenHolding) { - let mut sender_holding = sender_holding; - let mut recipient_holding = recipient_holding; - - if sender_holding.balance < balance_to_move { - panic!("Insufficient balance"); - } - - sender_holding.balance = sender_holding - .balance - .checked_sub(balance_to_move) - .expect("Checked above"); - recipient_holding.balance = recipient_holding - .balance - .checked_add(balance_to_move) - .expect("Recipient balance overflow"); - - recipient_holding.account_type = sender_holding.account_type; - - (sender_holding, recipient_holding) -} - -fn nft_master_transfer( - sender_holding: TokenHolding, - recipient_holding: TokenHolding, - balance_to_move: u128, -) -> (TokenHolding, TokenHolding) { - let mut sender_holding = sender_holding; - let mut recipient_holding = recipient_holding; - - if recipient_holding.balance != 0 { - panic!("Invalid balance in recipient account for NFT transfer"); - } - - if sender_holding.balance != balance_to_move { - panic!("Invalid balance for NFT Master transfer"); - } - - sender_holding.balance = 0; - recipient_holding.balance = balance_to_move; - recipient_holding.account_type = sender_holding.account_type; - - (sender_holding, recipient_holding) -} - -fn new_definition( - pre_states: &[AccountWithMetadata], - name: [u8; 6], - total_supply: u128, -) -> Vec { - if pre_states.len() != 2 { - panic!("Invalid number of input accounts"); - } - - let definition_target_account = &pre_states[0]; - let holding_target_account = &pre_states[1]; - - if definition_target_account.account != Account::default() { - panic!("Definition target account must have default values"); - } - - if holding_target_account.account != Account::default() { - panic!("Holding target account must have default values"); - } - - let token_definition = TokenDefinition { - account_type: TOKEN_STANDARD_FUNGIBLE_TOKEN, - name, - total_supply, - metadata_id: AccountId::new([0; 32]), - }; - - let token_holding = TokenHolding { - account_type: TOKEN_HOLDING_STANDARD, - definition_id: definition_target_account.account_id, - balance: total_supply, - }; - - let mut definition_target_account_post = definition_target_account.account.clone(); - definition_target_account_post.data = token_definition.into_data(); - - let mut holding_target_account_post = holding_target_account.account.clone(); - holding_target_account_post.data = token_holding.into_data(); - - vec![ - AccountPostState::new_claimed(definition_target_account_post), - AccountPostState::new_claimed(holding_target_account_post), - ] -} - -fn new_definition_with_metadata( - pre_states: &[AccountWithMetadata], - name: [u8; 6], - total_supply: u128, - token_standard: u8, - metadata_standard: u8, - metadata_values: &Data, -) -> Vec { - if pre_states.len() != 3 { - panic!("Invalid number of input accounts"); - } - - let definition_target_account = &pre_states[0]; - let metadata_target_account = &pre_states[1]; - let holding_target_account = &pre_states[2]; - - if definition_target_account.account != Account::default() { - panic!("Definition target account must have default values"); - } - - if metadata_target_account.account != Account::default() { - panic!("Metadata target account must have default values"); - } - - if holding_target_account.account != Account::default() { - panic!("Holding target account must have default values"); - } - - if !is_token_standard_valid(token_standard) { - panic!("Invalid Token Standard provided"); - } - - if !is_metadata_type_valid(metadata_standard) { - panic!("Invalid Metadata Standadard provided"); - } - - if !valid_total_supply_for_token_standard(total_supply, token_standard) { - panic!("Invalid total supply for the specified token supply"); - } - - let token_definition = TokenDefinition { - account_type: token_standard, - name, - total_supply, - metadata_id: metadata_target_account.account_id, - }; - - let token_holding = TokenHolding { - account_type: TOKEN_HOLDING_STANDARD, - definition_id: definition_target_account.account_id, - balance: total_supply, - }; - - if metadata_values.len() != 450 { - panic!("Metadata values data should be 450 bytes"); - } - - let uri: [u8; 200] = metadata_values[0..200] - .try_into() - .expect("Token program expects valid uri for Metadata"); - let creators: [u8; 250] = metadata_values[200..450] - .try_into() - .expect("Token program expects valid creators for Metadata"); - - let token_metadata = TokenMetadata { - account_type: metadata_standard, - version: CURRENT_VERSION, - definition_id: definition_target_account.account_id, - uri, - creators, - primary_sale_date: 0u64, // TODO #261: future works to implement this - }; - - let mut definition_target_account_post = definition_target_account.account.clone(); - definition_target_account_post.data = token_definition.into_data(); - - let mut holding_target_account_post = holding_target_account.account.clone(); - holding_target_account_post.data = token_holding.into_data(); - - let mut metadata_target_account_post = metadata_target_account.account.clone(); - metadata_target_account_post.data = token_metadata.into_data(); - - vec![ - AccountPostState::new_claimed(definition_target_account_post), - AccountPostState::new_claimed(holding_target_account_post), - AccountPostState::new_claimed(metadata_target_account_post), - ] -} - -fn valid_total_supply_for_token_standard(total_supply: u128, token_standard: u8) -> bool { - token_standard != TOKEN_STANDARD_NONFUNGIBLE || total_supply == 1 -} - -fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec { - if pre_states.len() != 2 { - panic!("Invalid number of accounts"); - } - - let definition = &pre_states[0]; - let account_to_initialize = &pre_states[1]; - - if account_to_initialize.account != Account::default() { - panic!("Only Uninitialized accounts can be initialized"); - } - - // TODO: #212 We should check that this is an account owned by the token program. - // This check can't be done here since the ID of the program is known only after compiling it - // - // Check definition account is valid - let _definition_values = - TokenDefinition::parse(&definition.account.data).expect("Definition account must be valid"); - let holding_values = TokenHolding::new(&definition.account_id); - - let definition_post = definition.account.clone(); - let mut account_to_initialize = account_to_initialize.account.clone(); - account_to_initialize.data = holding_values.into_data(); - - vec![ - AccountPostState::new(definition_post), - AccountPostState::new_claimed(account_to_initialize), - ] -} - -fn burn(pre_states: &[AccountWithMetadata], balance_to_burn: u128) -> Vec { - if pre_states.len() != 2 { - panic!("Invalid number of accounts"); - } - - let definition = &pre_states[0]; - let user_holding = &pre_states[1]; - - if !user_holding.is_authorized { - panic!("Authorization is missing"); - } - - let definition_values = TokenDefinition::parse(&definition.account.data) - .expect("Token Definition account must be valid"); - let user_values = TokenHolding::parse(&user_holding.account.data) - .expect("Token Holding account must be valid"); - - if definition.account_id != user_values.definition_id { - panic!("Mismatch Token Definition and Token Holding"); - } - - if user_values.balance < balance_to_burn { - panic!("Insufficient balance to burn"); - } - - let mut post_user_holding = user_holding.account.clone(); - let mut post_definition = definition.account.clone(); - - post_user_holding.data = TokenHolding::into_data(TokenHolding { - account_type: user_values.account_type, - definition_id: user_values.definition_id, - balance: user_values - .balance - .checked_sub(balance_to_burn) - .expect("Checked above"), - }); - - post_definition.data = TokenDefinition::into_data(TokenDefinition { - account_type: definition_values.account_type, - name: definition_values.name, - total_supply: definition_values - .total_supply - .checked_sub(balance_to_burn) - .expect("Total supply underflow"), - metadata_id: definition_values.metadata_id, - }); - - vec![ - AccountPostState::new(post_definition), - AccountPostState::new(post_user_holding), - ] -} - -fn is_mintable(account_type: u8) -> bool { - account_type != TOKEN_STANDARD_NONFUNGIBLE -} - -fn mint_additional_supply( - pre_states: &[AccountWithMetadata], - amount_to_mint: u128, -) -> Vec { - if pre_states.len() != 2 { - panic!("Invalid number of accounts"); - } - - let definition = &pre_states[0]; - let token_holding = &pre_states[1]; - - if !definition.is_authorized { - panic!("Definition authorization is missing"); - } - - let definition_values = - TokenDefinition::parse(&definition.account.data).expect("Definition account must be valid"); - - let token_holding_values: TokenHolding = if token_holding.account == Account::default() { - TokenHolding::new(&definition.account_id) - } else { - TokenHolding::parse(&token_holding.account.data).expect("Holding account must be valid") - }; - - if !is_mintable(definition_values.account_type) { - panic!("Token Definition's standard does not permit minting additional supply"); - } - - if definition.account_id != token_holding_values.definition_id { - panic!("Mismatch Token Definition and Token Holding"); - } - - let token_holding_post_data = TokenHolding { - account_type: token_holding_values.account_type, - definition_id: token_holding_values.definition_id, - balance: token_holding_values - .balance - .checked_add(amount_to_mint) - .expect("New balance overflow"), - }; - - let post_total_supply = definition_values - .total_supply - .checked_add(amount_to_mint) - .expect("Total supply overflow"); - - let post_definition_data = TokenDefinition { - account_type: definition_values.account_type, - name: definition_values.name, - total_supply: post_total_supply, - metadata_id: definition_values.metadata_id, - }; - - let post_definition = { - let mut this = definition.account.clone(); - this.data = post_definition_data.into_data(); - AccountPostState::new(this) - }; - - let token_holding_post = { - let mut this = token_holding.account.clone(); - this.data = token_holding_post_data.into_data(); - - // Claim the recipient account if it has default program owner - if this.program_owner == DEFAULT_PROGRAM_ID { - AccountPostState::new_claimed(this) - } else { - AccountPostState::new(this) - } - }; - vec![post_definition, token_holding_post] -} - -fn print_nft(pre_states: &[AccountWithMetadata]) -> Vec { - if pre_states.len() != 2 { - panic!("Invalid number of accounts"); - } - - let master_account = &pre_states[0]; - let printed_account = &pre_states[1]; - - if !master_account.is_authorized { - panic!("Master NFT Account must be authorized"); - } - - if printed_account.account != Account::default() { - panic!("Printed Account must be uninitialized"); - } - - let mut master_account_data = - TokenHolding::parse(&master_account.account.data).expect("Invalid Token Holding data"); - - if master_account_data.account_type != TOKEN_HOLDING_NFT_MASTER { - panic!("Invalid Token Holding provided as NFT Master Account"); - } - - if master_account_data.balance < 2 { - panic!("Insufficient balance to print another NFT copy"); - } - - let definition_id = master_account_data.definition_id; - - let post_master_account = { - let mut this = master_account.account.clone(); - master_account_data.balance -= 1; - this.data = master_account_data.into_data(); - AccountPostState::new(this) - }; - - let post_printed_account = { - let mut this = printed_account.account.clone(); - - let printed_data = TokenHolding { - account_type: TOKEN_HOLDING_NFT_PRINTED_COPY, - definition_id, - balance: 1, - }; - - this.data = TokenHolding::into_data(printed_data); - - AccountPostState::new_claimed(this) - }; - - vec![post_master_account, post_printed_account] -} - -type Instruction = Vec; +//! The Token Program. +//! +//! This program implements a simple token system supporting both fungible and non-fungible tokens +//! (NFTs). +//! +//! Token program accepts [`Instruction`] as input, refer to the corresponding documentation +//! for more details. + +use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}; +use token_program::core::Instruction; fn main() { let ( @@ -705,1622 +18,68 @@ fn main() { instruction_words, ) = read_nssa_inputs::(); - let post_states = match instruction[0] { - 0 => { - // Parse instruction - let total_supply = u128::from_le_bytes( - instruction[1..17] - .try_into() - .expect("Total supply must be 16 bytes little-endian"), - ); - let name: [u8; 6] = instruction[17..] + let pre_states_clone = pre_states.clone(); + + let post_states = match instruction { + Instruction::Transfer { + amount_to_transfer: balance_to_move, + } => { + let [sender, recipient] = pre_states .try_into() - .expect("Name must be 6 bytes long"); - assert_ne!(name, [0; 6]); - - // Execute - new_definition(&pre_states, name, total_supply) + .expect("Transfer instruction requires exactly two accounts"); + token_program::transfer::transfer(sender, recipient, balance_to_move) } - 1 => { - // Parse instruction - let balance_to_move = u128::from_le_bytes( - instruction[1..17] - .try_into() - .expect("Balance to move must be 16 bytes little-endian"), - ); - let name: [u8; 6] = instruction[17..] + Instruction::NewFungibleDefinition { name, total_supply } => { + let [definition_account, holding_account] = pre_states .try_into() - .expect("Name must be 6 bytes long"); - assert_eq!(name, [0; 6]); - - // Execute - transfer(&pre_states, balance_to_move) - } - 2 => { - // Initialize account - if instruction[1..] != [0; 22] { - panic!("Invalid instruction for initialize account"); - } - initialize_account(&pre_states) - } - 3 => { - let balance_to_burn = u128::from_le_bytes( - instruction[1..17] - .try_into() - .expect("Balance to burn must be 16 bytes little-endian"), - ); - let name: [u8; 6] = instruction[17..] - .try_into() - .expect("Name must be 6 bytes long"); - assert_eq!(name, [0; 6]); - - // Execute - burn(&pre_states, balance_to_burn) - } - 4 => { - let balance_to_mint = u128::from_le_bytes( - instruction[1..17] - .try_into() - .expect("Balance to burn must be 16 bytes little-endian"), - ); - let name: [u8; 6] = instruction[17..] - .try_into() - .expect("Name must be 6 bytes long"); - assert_eq!(name, [0; 6]); - - // Execute - mint_additional_supply(&pre_states, balance_to_mint) - } - 5 => { - if instruction.len() != 474 { - panic!("Invalid instruction length") - } - - // Parse instruction - let total_supply = u128::from_le_bytes( - instruction[1..17] - .try_into() - .expect("Total supply must be 16 bytes little-endian"), - ); - let name = instruction[17..23] - .try_into() - .expect("Name must be 6 bytes long"); - assert_ne!(name, [0; 6]); - let token_standard = instruction[23]; - let metadata_standard = instruction[24]; - let metadata_values: Data = - Data::try_from(instruction[25..474].to_vec()).expect("Invalid metadata"); - - // Execute - new_definition_with_metadata( - &pre_states, + .expect("NewFungibleDefinition instruction requires exactly two accounts"); + token_program::new_definition::new_fungible_definition( + definition_account, + holding_account, name, total_supply, - token_standard, - metadata_standard, - &metadata_values, ) } - 6 => { - if instruction.len() != 23 { - panic!("Invalid instruction length"); - } - - // Initialize account - if instruction[1..] != [0; 22] { - panic!("Invalid instruction for initialize account"); - } - - print_nft(&pre_states) + Instruction::NewDefinitionWithMetadata { + new_definition, + metadata, + } => { + let [definition_account, holding_account, metadata_account] = pre_states + .try_into() + .expect("NewDefinitionWithMetadata instruction requires exactly three accounts"); + token_program::new_definition::new_definition_with_metadata( + definition_account, + holding_account, + metadata_account, + new_definition, + *metadata, + ) + } + Instruction::InitializeAccount => { + let [definition_account, account_to_initialize] = pre_states + .try_into() + .expect("InitializeAccount instruction requires exactly two accounts"); + token_program::initialize::initialize_account(definition_account, account_to_initialize) + } + Instruction::Burn { amount_to_burn } => { + let [definition_account, user_holding_account] = pre_states + .try_into() + .expect("Burn instruction requires exactly two accounts"); + token_program::burn::burn(definition_account, user_holding_account, amount_to_burn) + } + Instruction::Mint { amount_to_mint } => { + let [definition_account, user_holding_account] = pre_states + .try_into() + .expect("Mint instruction requires exactly two accounts"); + token_program::mint::mint(definition_account, user_holding_account, amount_to_mint) + } + Instruction::PrintNft => { + let [master_account, printed_account] = pre_states + .try_into() + .expect("PrintNft instruction requires exactly two accounts"); + token_program::print_nft::print_nft(master_account, printed_account) } - _ => panic!("Invalid instruction"), }; - write_nssa_outputs(instruction_words, pre_states, post_states); -} - -#[cfg(test)] -mod tests { - use nssa_core::account::{Account, AccountId, AccountWithMetadata, Data}; - - use crate::{ - TOKEN_DEFINITION_DATA_SIZE, TOKEN_HOLDING_DATA_SIZE, TOKEN_HOLDING_NFT_MASTER, - TOKEN_HOLDING_NFT_PRINTED_COPY, TOKEN_HOLDING_STANDARD, TOKEN_STANDARD_FUNGIBLE_TOKEN, - TOKEN_STANDARD_NONFUNGIBLE, TokenDefinition, TokenHolding, burn, mint_additional_supply, - new_definition, new_definition_with_metadata, print_nft, transfer, - }; - - struct BalanceForTests; - struct IdForTests; - - struct AccountForTests; - - impl AccountForTests { - fn definition_account_auth() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenDefinition::into_data(TokenDefinition { - account_type: TOKEN_STANDARD_FUNGIBLE_TOKEN, - name: [2; 6], - total_supply: BalanceForTests::init_supply(), - metadata_id: AccountId::new([0; 32]), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::pool_definition_id(), - } - } - - fn definition_account_without_auth() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenDefinition::into_data(TokenDefinition { - account_type: TOKEN_STANDARD_FUNGIBLE_TOKEN, - name: [2; 6], - total_supply: BalanceForTests::init_supply(), - metadata_id: AccountId::new([0; 32]), - }), - nonce: 0, - }, - is_authorized: false, - account_id: IdForTests::pool_definition_id(), - } - } - - fn holding_different_definition() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_STANDARD, - definition_id: IdForTests::pool_definition_id_diff(), - balance: BalanceForTests::holding_balance(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::holding_id(), - } - } - - fn holding_same_definition_with_authorization() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_STANDARD, - definition_id: IdForTests::pool_definition_id(), - balance: BalanceForTests::holding_balance(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::holding_id(), - } - } - - fn holding_same_definition_without_authorization() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_STANDARD, - definition_id: IdForTests::pool_definition_id(), - balance: BalanceForTests::holding_balance(), - }), - nonce: 0, - }, - is_authorized: false, - account_id: IdForTests::holding_id(), - } - } - - fn holding_same_definition_without_authorization_overflow() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_STANDARD, - definition_id: IdForTests::pool_definition_id(), - balance: BalanceForTests::init_supply(), - }), - nonce: 0, - }, - is_authorized: false, - account_id: IdForTests::holding_id(), - } - } - - fn definition_account_post_burn() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenDefinition::into_data(TokenDefinition { - account_type: TOKEN_STANDARD_FUNGIBLE_TOKEN, - name: [2; 6], - total_supply: BalanceForTests::init_supply_burned(), - metadata_id: AccountId::new([0; 32]), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::pool_definition_id(), - } - } - - fn holding_account_post_burn() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_STANDARD, - definition_id: IdForTests::pool_definition_id(), - balance: BalanceForTests::holding_balance_burned(), - }), - nonce: 0, - }, - is_authorized: false, - account_id: IdForTests::holding_id(), - } - } - - fn holding_account_uninit() -> AccountWithMetadata { - AccountWithMetadata { - account: Account::default(), - is_authorized: false, - account_id: IdForTests::holding_id_2(), - } - } - - fn init_mint() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [0u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_STANDARD, - definition_id: IdForTests::pool_definition_id(), - balance: BalanceForTests::mint_success(), - }), - nonce: 0, - }, - is_authorized: false, - account_id: IdForTests::holding_id(), - } - } - - fn holding_account_same_definition_mint() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_STANDARD, - definition_id: IdForTests::pool_definition_id(), - balance: BalanceForTests::holding_balance_mint(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::pool_definition_id(), - } - } - - fn definition_account_mint() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenDefinition::into_data(TokenDefinition { - account_type: TOKEN_STANDARD_FUNGIBLE_TOKEN, - name: [2; 6], - total_supply: BalanceForTests::init_supply_mint(), - metadata_id: AccountId::new([0; 32]), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::pool_definition_id(), - } - } - - fn holding_same_definition_with_authorization_and_large_balance() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_STANDARD, - definition_id: IdForTests::pool_definition_id(), - balance: BalanceForTests::mint_overflow(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::pool_definition_id(), - } - } - - fn definition_account_with_authorization_nonfungible() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenDefinition::into_data(TokenDefinition { - account_type: TOKEN_STANDARD_NONFUNGIBLE, - name: [2; 6], - total_supply: 1, - metadata_id: AccountId::new([0; 32]), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::pool_definition_id(), - } - } - - fn definition_account_uninit() -> AccountWithMetadata { - AccountWithMetadata { - account: Account::default(), - is_authorized: false, - account_id: IdForTests::pool_definition_id(), - } - } - - fn holding_account_init() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_STANDARD, - definition_id: IdForTests::pool_definition_id(), - balance: BalanceForTests::init_supply(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::holding_id(), - } - } - - fn definition_account_unclaimed() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [0u32; 8], - balance: 0u128, - data: TokenDefinition::into_data(TokenDefinition { - account_type: TOKEN_STANDARD_FUNGIBLE_TOKEN, - name: [2; 6], - total_supply: BalanceForTests::init_supply(), - metadata_id: AccountId::new([0; 32]), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::pool_definition_id(), - } - } - - fn holding_account_unclaimed() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [0u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_STANDARD, - definition_id: IdForTests::pool_definition_id(), - balance: BalanceForTests::init_supply(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::holding_id(), - } - } - - fn holding_account2_init() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_STANDARD, - definition_id: IdForTests::pool_definition_id(), - balance: BalanceForTests::init_supply(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::holding_id_2(), - } - } - - fn holding_account2_init_post_transfer() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_STANDARD, - definition_id: IdForTests::pool_definition_id(), - balance: BalanceForTests::recipient_post_transfer(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::holding_id_2(), - } - } - - fn holding_account_init_post_transfer() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_STANDARD, - definition_id: IdForTests::pool_definition_id(), - balance: BalanceForTests::sender_post_transfer(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::holding_id(), - } - } - - fn holding_account_master_nft() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_NFT_MASTER, - definition_id: IdForTests::pool_definition_id(), - balance: BalanceForTests::printable_copies(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::holding_id(), - } - } - - fn holding_account_master_nft_insufficient_balance() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_NFT_MASTER, - definition_id: IdForTests::pool_definition_id(), - balance: 1, - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::holding_id(), - } - } - - fn holding_account_master_nft_after_print() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_NFT_MASTER, - definition_id: IdForTests::pool_definition_id(), - balance: BalanceForTests::printable_copies() - 1, - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::holding_id(), - } - } - - fn holding_account_printed_nft() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [0u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_NFT_PRINTED_COPY, - definition_id: IdForTests::pool_definition_id(), - balance: 1, - }), - nonce: 0, - }, - is_authorized: false, - account_id: IdForTests::holding_id(), - } - } - - fn holding_account_with_master_nft_transferred_to() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [0u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_NFT_MASTER, - definition_id: IdForTests::pool_definition_id(), - balance: BalanceForTests::printable_copies(), - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::holding_id_2(), - } - } - - fn holding_account_master_nft_post_transfer() -> AccountWithMetadata { - AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_NFT_MASTER, - definition_id: IdForTests::pool_definition_id(), - balance: 0, - }), - nonce: 0, - }, - is_authorized: true, - account_id: IdForTests::holding_id(), - } - } - } - - impl BalanceForTests { - fn init_supply() -> u128 { - 100_000 - } - - fn holding_balance() -> u128 { - 1_000 - } - - fn init_supply_burned() -> u128 { - 99_500 - } - - fn holding_balance_burned() -> u128 { - 500 - } - - fn burn_success() -> u128 { - 500 - } - - fn burn_insufficient() -> u128 { - 1_500 - } - - fn mint_success() -> u128 { - 50_000 - } - - fn holding_balance_mint() -> u128 { - 51_000 - } - - fn mint_overflow() -> u128 { - u128::MAX - 40_000 - } - - fn init_supply_mint() -> u128 { - 150_000 - } - - fn sender_post_transfer() -> u128 { - 95_000 - } - - fn recipient_post_transfer() -> u128 { - 105_000 - } - - fn transfer_amount() -> u128 { - 5_000 - } - - fn printable_copies() -> u128 { - 10 - } - } - - impl IdForTests { - fn pool_definition_id() -> AccountId { - AccountId::new([15; 32]) - } - - fn pool_definition_id_diff() -> AccountId { - AccountId::new([16; 32]) - } - - fn holding_id() -> AccountId { - AccountId::new([17; 32]) - } - - fn holding_id_2() -> AccountId { - AccountId::new([42; 32]) - } - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_new_definition_with_invalid_number_of_accounts_1() { - let pre_states = vec![AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([1; 32]), - }]; - let _post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_new_definition_with_invalid_number_of_accounts_2() { - let pre_states = vec![ - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([1; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([2; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([3; 32]), - }, - ]; - let _post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10); - } - - #[should_panic(expected = "Definition target account must have default values")] - #[test] - fn test_new_definition_non_default_first_account_should_fail() { - let pre_states = vec![ - AccountWithMetadata { - account: Account { - program_owner: [1, 2, 3, 4, 5, 6, 7, 8], - ..Account::default() - }, - is_authorized: true, - account_id: AccountId::new([1; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([2; 32]), - }, - ]; - let _post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10); - } - - #[should_panic(expected = "Holding target account must have default values")] - #[test] - fn test_new_definition_non_default_second_account_should_fail() { - let pre_states = vec![ - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([1; 32]), - }, - AccountWithMetadata { - account: Account { - program_owner: [1, 2, 3, 4, 5, 6, 7, 8], - ..Account::default() - }, - is_authorized: true, - account_id: AccountId::new([2; 32]), - }, - ]; - let _post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10); - } - - #[test] - fn test_new_definition_with_valid_inputs_succeeds() { - let pre_states = vec![ - AccountForTests::definition_account_uninit(), - AccountForTests::holding_account_uninit(), - ]; - - let post_states = new_definition(&pre_states, [2u8; 6], BalanceForTests::init_supply()); - - let [definition_account, holding_account] = post_states.try_into().ok().unwrap(); - assert!( - *definition_account.account() - == AccountForTests::definition_account_unclaimed().account - ); - - assert!(*holding_account.account() == AccountForTests::holding_account_unclaimed().account); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_transfer_with_invalid_number_of_accounts_1() { - let pre_states = vec![AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([1; 32]), - }]; - let _post_states = transfer(&pre_states, 10); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_transfer_with_invalid_number_of_accounts_2() { - let pre_states = vec![ - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([1; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([2; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([3; 32]), - }, - ]; - let _post_states = transfer(&pre_states, 10); - } - - #[should_panic(expected = "Invalid sender data")] - #[test] - fn test_transfer_invalid_instruction_type_should_fail() { - let invalid_type = TOKEN_HOLDING_STANDARD ^ 1; - let pre_states = vec![ - AccountWithMetadata { - account: Account { - // First byte should be `TOKEN_HOLDING_STANDARD` for token holding accounts - data: Data::try_from(vec![invalid_type; TOKEN_HOLDING_DATA_SIZE]) - .expect("Invalid data"), - ..Account::default() - }, - is_authorized: true, - account_id: AccountId::new([1; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([2; 32]), - }, - ]; - let _post_states = transfer(&pre_states, 10); - } - - #[should_panic(expected = "Invalid sender data")] - #[test] - fn test_transfer_invalid_data_size_should_fail_1() { - let pre_states = vec![ - AccountWithMetadata { - account: Account { - // Data must be of exact length `TOKEN_HOLDING_DATA_SIZE` - data: Data::try_from(vec![1; TOKEN_HOLDING_DATA_SIZE - 1]).unwrap(), - ..Account::default() - }, - is_authorized: true, - account_id: AccountId::new([1; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([2; 32]), - }, - ]; - let _post_states = transfer(&pre_states, 10); - } - - #[should_panic(expected = "Invalid sender data")] - #[test] - fn test_transfer_invalid_data_size_should_fail_2() { - let pre_states = vec![ - AccountWithMetadata { - account: Account { - // Data must be of exact length `TOKEN_HOLDING_DATA_SIZE` - data: Data::try_from(vec![1; TOKEN_HOLDING_DATA_SIZE - 1]).unwrap(), - ..Account::default() - }, - is_authorized: true, - account_id: AccountId::new([1; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([2; 32]), - }, - ]; - let _post_states = transfer(&pre_states, 10); - } - - #[should_panic(expected = "Sender and recipient definition id mismatch")] - #[test] - fn test_transfer_with_different_definition_ids_should_fail() { - let pre_states = vec![ - AccountForTests::holding_same_definition_with_authorization(), - AccountForTests::holding_different_definition(), - ]; - let _post_states = transfer(&pre_states, 10); - } - - #[should_panic(expected = "Insufficient balance")] - #[test] - fn test_transfer_with_insufficient_balance_should_fail() { - let pre_states = vec![ - AccountForTests::holding_same_definition_with_authorization(), - AccountForTests::holding_account_same_definition_mint(), - ]; - // Attempt to transfer 38 tokens - let _post_states = transfer(&pre_states, BalanceForTests::burn_insufficient()); - } - - #[should_panic(expected = "Sender authorization is missing")] - #[test] - fn test_transfer_without_sender_authorization_should_fail() { - let mut def_data = Vec::::new(); - def_data.extend_from_slice(&[1; TOKEN_DEFINITION_DATA_SIZE - 16]); - def_data.extend_from_slice(&u128::to_le_bytes(37)); - - let pre_states = vec![ - AccountWithMetadata { - account: Account { - // Account with balance 37 - data: Data::try_from(def_data).unwrap(), - ..Account::default() - }, - is_authorized: false, - account_id: AccountId::new([1; 32]), - }, - AccountWithMetadata { - account: Account { - data: Data::try_from(vec![1; TOKEN_HOLDING_DATA_SIZE - 1]).unwrap(), - ..Account::default() - }, - is_authorized: true, - account_id: AccountId::new([2; 32]), - }, - ]; - let _post_states = transfer(&pre_states, 37); - } - - #[test] - fn test_transfer_with_valid_inputs_succeeds() { - let pre_states = vec![ - AccountForTests::holding_account_init(), - AccountForTests::holding_account2_init(), - ]; - let post_states = transfer(&pre_states, BalanceForTests::transfer_amount()); - let [sender_post, recipient_post] = post_states.try_into().ok().unwrap(); - - assert!( - *sender_post.account() == AccountForTests::holding_account_init_post_transfer().account - ); - assert!( - *recipient_post.account() - == AccountForTests::holding_account2_init_post_transfer().account - ); - } - - #[should_panic(expected = "Invalid balance for NFT Master transfer")] - #[test] - fn test_transfer_with_master_nft_invalid_balance() { - let pre_states = vec![ - AccountForTests::holding_account_master_nft(), - AccountForTests::holding_account_uninit(), - ]; - let _post_states = transfer(&pre_states, BalanceForTests::transfer_amount()); - } - - #[should_panic(expected = "Invalid balance in recipient account for NFT transfer")] - #[test] - fn test_transfer_with_master_nft_invalid_recipient_balance() { - let pre_states = vec![ - AccountForTests::holding_account_master_nft(), - AccountForTests::holding_account_with_master_nft_transferred_to(), - ]; - let _post_states = transfer(&pre_states, BalanceForTests::printable_copies()); - } - - #[test] - fn test_transfer_with_master_nft_success() { - let pre_states = vec![ - AccountForTests::holding_account_master_nft(), - AccountForTests::holding_account_uninit(), - ]; - let post_states = transfer(&pre_states, BalanceForTests::printable_copies()); - let [sender_post, recipient_post] = post_states.try_into().ok().unwrap(); - - assert!( - *sender_post.account() - == AccountForTests::holding_account_master_nft_post_transfer().account - ); - assert!( - *recipient_post.account() - == AccountForTests::holding_account_with_master_nft_transferred_to().account - ); - } - - #[test] - fn test_token_initialize_account_succeeds() { - let pre_states = vec![ - AccountForTests::holding_account_init(), - AccountForTests::holding_account2_init(), - ]; - let post_states = transfer(&pre_states, BalanceForTests::transfer_amount()); - let [sender_post, recipient_post] = post_states.try_into().ok().unwrap(); - - assert!( - *sender_post.account() == AccountForTests::holding_account_init_post_transfer().account - ); - assert!( - *recipient_post.account() - == AccountForTests::holding_account2_init_post_transfer().account - ); - } - - #[test] - #[should_panic(expected = "Invalid number of accounts")] - fn test_burn_invalid_number_of_accounts() { - let pre_states = vec![AccountForTests::definition_account_auth()]; - let _post_states = burn(&pre_states, BalanceForTests::burn_success()); - } - - #[test] - #[should_panic(expected = "Mismatch Token Definition and Token Holding")] - fn test_burn_mismatch_def() { - let pre_states = vec![ - AccountForTests::definition_account_auth(), - AccountForTests::holding_different_definition(), - ]; - let _post_states = burn(&pre_states, BalanceForTests::burn_success()); - } - - #[test] - #[should_panic(expected = "Authorization is missing")] - fn test_burn_missing_authorization() { - let pre_states = vec![ - AccountForTests::definition_account_auth(), - AccountForTests::holding_same_definition_without_authorization(), - ]; - let _post_states = burn(&pre_states, BalanceForTests::burn_success()); - } - - #[test] - #[should_panic(expected = "Insufficient balance to burn")] - fn test_burn_insufficient_balance() { - let pre_states = vec![ - AccountForTests::definition_account_auth(), - AccountForTests::holding_same_definition_with_authorization(), - ]; - let _post_states = burn(&pre_states, BalanceForTests::burn_insufficient()); - } - - #[test] - #[should_panic(expected = "Total supply underflow")] - fn test_burn_total_supply_underflow() { - let pre_states = vec![ - AccountForTests::definition_account_auth(), - AccountForTests::holding_same_definition_with_authorization_and_large_balance(), - ]; - let _post_states = burn(&pre_states, BalanceForTests::mint_overflow()); - } - - #[test] - fn test_burn_success() { - let pre_states = vec![ - AccountForTests::definition_account_auth(), - AccountForTests::holding_same_definition_with_authorization(), - ]; - let post_states = burn(&pre_states, BalanceForTests::burn_success()); - - let def_post = post_states[0].clone(); - let holding_post = post_states[1].clone(); - - assert!(*def_post.account() == AccountForTests::definition_account_post_burn().account); - assert!(*holding_post.account() == AccountForTests::holding_account_post_burn().account); - } - - #[test] - #[should_panic(expected = "Invalid number of accounts")] - fn test_mint_invalid_number_of_accounts_1() { - let pre_states = vec![AccountForTests::definition_account_auth()]; - let _post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_success()); - } - - #[test] - #[should_panic(expected = "Invalid number of accounts")] - fn test_mint_invalid_number_of_accounts_2() { - let pre_states = vec![ - AccountForTests::definition_account_auth(), - AccountForTests::holding_account_same_definition_mint(), - AccountForTests::holding_same_definition_with_authorization(), - ]; - let _post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_success()); - } - - #[test] - #[should_panic(expected = "Holding account must be valid")] - fn test_mint_not_valid_holding_account() { - let pre_states = vec![ - AccountForTests::definition_account_auth(), - AccountForTests::definition_account_without_auth(), - ]; - let _post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_success()); - } - - #[test] - #[should_panic(expected = "Definition account must be valid")] - fn test_mint_not_valid_definition_account() { - let pre_states = vec![ - AccountForTests::holding_same_definition_with_authorization(), - AccountForTests::holding_same_definition_without_authorization(), - ]; - let _post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_success()); - } - - #[test] - #[should_panic(expected = "Definition authorization is missing")] - fn test_mint_missing_authorization() { - let pre_states = vec![ - AccountForTests::definition_account_without_auth(), - AccountForTests::holding_same_definition_without_authorization(), - ]; - let _post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_success()); - } - - #[test] - #[should_panic(expected = "Mismatch Token Definition and Token Holding")] - fn test_mint_mismatched_token_definition() { - let pre_states = vec![ - AccountForTests::definition_account_auth(), - AccountForTests::holding_different_definition(), - ]; - let _post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_success()); - } - - #[test] - fn test_mint_success() { - let pre_states = vec![ - AccountForTests::definition_account_auth(), - AccountForTests::holding_same_definition_without_authorization(), - ]; - let post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_success()); - - let def_post = post_states[0].clone(); - let holding_post = post_states[1].clone(); - - assert!(*def_post.account() == AccountForTests::definition_account_mint().account); - assert!( - *holding_post.account() - == AccountForTests::holding_account_same_definition_mint().account - ); - } - - #[test] - fn test_mint_uninit_holding_success() { - let pre_states = vec![ - AccountForTests::definition_account_auth(), - AccountForTests::holding_account_uninit(), - ]; - let post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_success()); - - let def_post = post_states[0].clone(); - let holding_post = post_states[1].clone(); - - assert!(*def_post.account() == AccountForTests::definition_account_mint().account); - assert!(*holding_post.account() == AccountForTests::init_mint().account); - assert!(holding_post.requires_claim()); - } - - #[test] - #[should_panic(expected = "Total supply overflow")] - fn test_mint_total_supply_overflow() { - let pre_states = vec![ - AccountForTests::definition_account_auth(), - AccountForTests::holding_same_definition_without_authorization(), - ]; - let _post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_overflow()); - } - - #[test] - #[should_panic(expected = "New balance overflow")] - fn test_mint_holding_account_overflow() { - let pre_states = vec![ - AccountForTests::definition_account_auth(), - AccountForTests::holding_same_definition_without_authorization_overflow(), - ]; - let _post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_overflow()); - } - - #[test] - #[should_panic( - expected = "Token Definition's standard does not permit minting additional supply" - )] - fn test_mint_cannot_mint_unmintable_tokens() { - let pre_states = vec![ - AccountForTests::definition_account_with_authorization_nonfungible(), - AccountForTests::holding_same_definition_without_authorization(), - ]; - let _post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_success()); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_new_definition_metadata_with_invalid_number_of_accounts_1() { - let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; - let total_supply = 15u128; - let token_standard = 0u8; - let metadata_standard = 0u8; - let metadata_values: Data = Data::try_from([1u8; 450].to_vec()).unwrap(); - - let pre_states = vec![AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([1; 32]), - }]; - let _post_states = new_definition_with_metadata( - &pre_states, - name, - total_supply, - token_standard, - metadata_standard, - &metadata_values, - ); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_new_definition_metadata_with_invalid_number_of_accounts_2() { - let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; - let total_supply = 15u128; - let token_standard = 0u8; - let metadata_standard = 0u8; - let metadata_values: Data = Data::try_from([1u8; 450].to_vec()).unwrap(); - - let pre_states = vec![ - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([1; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([2; 32]), - }, - ]; - let _post_states = new_definition_with_metadata( - &pre_states, - name, - total_supply, - token_standard, - metadata_standard, - &metadata_values, - ); - } - - #[should_panic(expected = "Invalid number of input accounts")] - #[test] - fn test_call_new_definition_metadata_with_invalid_number_of_accounts_3() { - let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; - let total_supply = 15u128; - let token_standard = 0u8; - let metadata_standard = 0u8; - let metadata_values: Data = Data::try_from([1u8; 450].to_vec()).unwrap(); - - let pre_states = vec![ - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([1; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([2; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([3; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([4; 32]), - }, - ]; - let _post_states = new_definition_with_metadata( - &pre_states, - name, - total_supply, - token_standard, - metadata_standard, - &metadata_values, - ); - } - - #[should_panic(expected = "Definition target account must have default values")] - #[test] - fn test_call_new_definition_metadata_with_init_definition() { - let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; - let total_supply = 15u128; - let token_standard = 0u8; - let metadata_standard = 0u8; - let metadata_values: Data = Data::try_from([1u8; 450].to_vec()).unwrap(); - - let pre_states = vec![ - AccountForTests::definition_account_auth(), - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([2; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([3; 32]), - }, - ]; - let _post_states = new_definition_with_metadata( - &pre_states, - name, - total_supply, - token_standard, - metadata_standard, - &metadata_values, - ); - } - - #[should_panic(expected = "Metadata target account must have default values")] - #[test] - fn test_call_new_definition_metadata_with_init_metadata() { - let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; - let total_supply = 15u128; - let token_standard = 0u8; - let metadata_standard = 0u8; - let metadata_values: Data = Data::try_from([1u8; 450].to_vec()).unwrap(); - - let pre_states = vec![ - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([1; 32]), - }, - AccountForTests::holding_account_same_definition_mint(), - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([3; 32]), - }, - ]; - let _post_states = new_definition_with_metadata( - &pre_states, - name, - total_supply, - token_standard, - metadata_standard, - &metadata_values, - ); - } - - #[should_panic(expected = "Holding target account must have default values")] - #[test] - fn test_call_new_definition_metadata_with_init_holding() { - let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; - let total_supply = 15u128; - let token_standard = 0u8; - let metadata_standard = 0u8; - let metadata_values: Data = Data::try_from([1u8; 450].to_vec()).unwrap(); - - let pre_states = vec![ - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([1; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([2; 32]), - }, - AccountForTests::holding_account_same_definition_mint(), - ]; - let _post_states = new_definition_with_metadata( - &pre_states, - name, - total_supply, - token_standard, - metadata_standard, - &metadata_values, - ); - } - - #[should_panic(expected = "Metadata values data should be 450 bytes")] - #[test] - fn test_call_new_definition_metadata_with_too_short_metadata_length() { - let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; - let total_supply = 15u128; - let token_standard = 0u8; - let metadata_standard = 0u8; - let metadata_values: Data = Data::try_from([1u8; 449].to_vec()).unwrap(); - - let pre_states = vec![ - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([1; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([2; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([3; 32]), - }, - ]; - let _post_states = new_definition_with_metadata( - &pre_states, - name, - total_supply, - token_standard, - metadata_standard, - &metadata_values, - ); - } - - #[should_panic(expected = "Metadata values data should be 450 bytes")] - #[test] - fn test_call_new_definition_metadata_with_too_long_metadata_length() { - let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; - let total_supply = 15u128; - let token_standard = 0u8; - let metadata_standard = 0u8; - let metadata_values: Data = Data::try_from([1u8; 451].to_vec()).unwrap(); - - let pre_states = vec![ - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([1; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([2; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([3; 32]), - }, - ]; - let _post_states = new_definition_with_metadata( - &pre_states, - name, - total_supply, - token_standard, - metadata_standard, - &metadata_values, - ); - } - - #[should_panic(expected = "Invalid Token Standard provided")] - #[test] - fn test_call_new_definition_metadata_with_invalid_token_standard() { - let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; - let total_supply = 15u128; - let token_standard = 14u8; - let metadata_standard = 0u8; - let metadata_values: Data = Data::try_from([1u8; 450].to_vec()).unwrap(); - - let pre_states = vec![ - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([1; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([2; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([3; 32]), - }, - ]; - let _post_states = new_definition_with_metadata( - &pre_states, - name, - total_supply, - token_standard, - metadata_standard, - &metadata_values, - ); - } - - #[should_panic(expected = "Invalid Metadata Standadard provided")] - #[test] - fn test_call_new_definition_metadata_with_invalid_metadata_standard() { - let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; - let total_supply = 15u128; - let token_standard = 0u8; - let metadata_standard = 14u8; - let metadata_values: Data = Data::try_from([1u8; 450].to_vec()).unwrap(); - - let pre_states = vec![ - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([1; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([2; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([3; 32]), - }, - ]; - let _post_states = new_definition_with_metadata( - &pre_states, - name, - total_supply, - token_standard, - metadata_standard, - &metadata_values, - ); - } - - #[should_panic(expected = "Invalid total supply for the specified token supply")] - #[test] - fn test_call_new_definition_metadata_invalid_supply_for_nonfungible() { - let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; - let total_supply = 15u128; - let token_standard = TOKEN_STANDARD_NONFUNGIBLE; - let metadata_standard = 0u8; - let metadata_values: Data = Data::try_from([1u8; 450].to_vec()).unwrap(); - - let pre_states = vec![ - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([1; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([2; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: true, - account_id: AccountId::new([3; 32]), - }, - ]; - let _post_states = new_definition_with_metadata( - &pre_states, - name, - total_supply, - token_standard, - metadata_standard, - &metadata_values, - ); - } - - #[should_panic(expected = "Invalid number of accounts")] - #[test] - fn test_print_nft_invalid_number_of_accounts_1() { - let pre_states = vec![AccountForTests::holding_account_master_nft()]; - let _post_states = print_nft(&pre_states); - } - - #[should_panic(expected = "Invalid number of accounts")] - #[test] - fn test_print_nft_invalid_number_of_accounts_2() { - let pre_states = vec![ - AccountForTests::holding_account_master_nft(), - AccountForTests::definition_account_auth(), - AccountForTests::holding_account_uninit(), - ]; - let _post_states = print_nft(&pre_states); - } - - #[should_panic(expected = "Master NFT Account must be authorized")] - #[test] - fn test_print_nft_master_account_must_be_authorized() { - let pre_states = vec![ - AccountForTests::holding_account_uninit(), - AccountForTests::holding_account_uninit(), - ]; - let _post_states = print_nft(&pre_states); - } - - #[should_panic(expected = "Printed Account must be uninitialized")] - #[test] - fn test_print_nft_print_account_initialized() { - let pre_states = vec![ - AccountForTests::holding_account_master_nft(), - AccountForTests::holding_account_init(), - ]; - let _post_states = print_nft(&pre_states); - } - - #[should_panic(expected = "Invalid Token Holding data")] - #[test] - fn test_print_nft_master_nft_invalid_token_holding() { - let pre_states = vec![ - AccountForTests::definition_account_auth(), - AccountForTests::holding_account_uninit(), - ]; - let _post_states = print_nft(&pre_states); - } - - #[should_panic(expected = "Invalid Token Holding provided as NFT Master Account")] - #[test] - fn test_print_nft_master_nft_not_nft_master_account() { - let pre_states = vec![ - AccountForTests::holding_account_init(), - AccountForTests::holding_account_uninit(), - ]; - let _post_states = print_nft(&pre_states); - } - - #[should_panic(expected = "Insufficient balance to print another NFT copy")] - #[test] - fn test_print_nft_master_nft_insufficient_balance() { - let pre_states = vec![ - AccountForTests::holding_account_master_nft_insufficient_balance(), - AccountForTests::holding_account_uninit(), - ]; - let _post_states = print_nft(&pre_states); - } - - #[test] - fn test_print_nft_success() { - let pre_states = vec![ - AccountForTests::holding_account_master_nft(), - AccountForTests::holding_account_uninit(), - ]; - let post_states = print_nft(&pre_states); - - let post_master_nft = post_states[0].account(); - let post_printed = post_states[1].account(); - - assert!( - *post_master_nft == AccountForTests::holding_account_master_nft_after_print().account - ); - assert!(*post_printed == AccountForTests::holding_account_printed_nft().account); - } + write_nssa_outputs(instruction_words, pre_states_clone, post_states); } diff --git a/programs/token/Cargo.toml b/programs/token/Cargo.toml new file mode 100644 index 00000000..25b061a9 --- /dev/null +++ b/programs/token/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "token_program" +version = "0.1.0" +edition = "2024" + +[dependencies] +nssa_core.workspace = true +token_core.workspace = true diff --git a/programs/token/core/Cargo.toml b/programs/token/core/Cargo.toml new file mode 100644 index 00000000..4193bfe6 --- /dev/null +++ b/programs/token/core/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "token_core" +version = "0.1.0" +edition = "2024" + +[dependencies] +nssa_core.workspace = true +serde.workspace = true +borsh.workspace = true diff --git a/programs/token/core/src/lib.rs b/programs/token/core/src/lib.rs new file mode 100644 index 00000000..140ae38f --- /dev/null +++ b/programs/token/core/src/lib.rs @@ -0,0 +1,241 @@ +//! This crate contains core data structures and utilities for the Token Program. + +use borsh::{BorshDeserialize, BorshSerialize}; +use nssa_core::account::{AccountId, Data}; +use serde::{Deserialize, Serialize}; + +/// Token Program Instruction. +#[derive(Serialize, Deserialize)] +pub enum Instruction { + /// Transfer tokens from sender to recipient. + /// + /// Required accounts: + /// - Sender's Token Holding account (authorized), + /// - Recipient's Token Holding account. + Transfer { amount_to_transfer: u128 }, + + /// Create a new fungible token definition without metadata. + /// + /// Required accounts: + /// - Token Definition account (uninitialized), + /// - Token Holding account (uninitialized). + NewFungibleDefinition { name: String, total_supply: u128 }, + + /// Create a new fungible or non-fungible token definition with metadata. + /// + /// Required accounts: + /// - Token Definition account (uninitialized), + /// - Token Holding account (uninitialized), + /// - Token Metadata account (uninitialized). + NewDefinitionWithMetadata { + new_definition: NewTokenDefinition, + /// Boxed to avoid large enum variant size + metadata: Box, + }, + + /// Initialize a token holding account for a given token definition. + /// + /// Required accounts: + /// - Token Definition account (initialized), + /// - Token Holding account (uninitialized), + InitializeAccount, + + /// Burn tokens from the holder's account. + /// + /// Required accounts: + /// - Token Definition account (initialized), + /// - Token Holding account (authorized). + Burn { amount_to_burn: u128 }, + + /// Mint new tokens to the holder's account. + /// + /// Required accounts: + /// - Token Definition account (authorized), + /// - Token Holding account (uninitialized or initialized). + Mint { amount_to_mint: u128 }, + + /// Print a new NFT from the master copy. + /// + /// Required accounts: + /// - NFT Master Token Holding account (authorized), + /// - NFT Printed Copy Token Holding account (uninitialized). + PrintNft, +} + +#[derive(Serialize, Deserialize)] +pub enum NewTokenDefinition { + Fungible { + name: String, + total_supply: u128, + }, + NonFungible { + name: String, + printable_supply: u128, + }, +} + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub enum TokenDefinition { + Fungible { + name: String, + total_supply: u128, + metadata_id: Option, + }, + NonFungible { + name: String, + printable_supply: u128, + metadata_id: AccountId, + }, +} + +impl TryFrom<&Data> for TokenDefinition { + type Error = std::io::Error; + + fn try_from(data: &Data) -> Result { + TokenDefinition::try_from_slice(data.as_ref()) + } +} + +impl From<&TokenDefinition> for Data { + fn from(definition: &TokenDefinition) -> Self { + // Using size_of_val as size hint for Vec allocation + let mut data = Vec::with_capacity(std::mem::size_of_val(definition)); + + BorshSerialize::serialize(definition, &mut data) + .expect("Serialization to Vec should not fail"); + + Data::try_from(data).expect("Token definition encoded data should fit into Data") + } +} + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub enum TokenHolding { + Fungible { + definition_id: AccountId, + balance: u128, + }, + NftMaster { + definition_id: AccountId, + /// The amount of printed copies left - 1 (1 reserved for master copy itself). + print_balance: u128, + }, + NftPrintedCopy { + definition_id: AccountId, + /// Whether nft is owned by the holder. + owned: bool, + }, +} + +impl TokenHolding { + pub fn zeroized_clone_from(other: &Self) -> Self { + match other { + TokenHolding::Fungible { definition_id, .. } => TokenHolding::Fungible { + definition_id: *definition_id, + balance: 0, + }, + TokenHolding::NftMaster { definition_id, .. } => TokenHolding::NftMaster { + definition_id: *definition_id, + print_balance: 0, + }, + TokenHolding::NftPrintedCopy { definition_id, .. } => TokenHolding::NftPrintedCopy { + definition_id: *definition_id, + owned: false, + }, + } + } + + pub fn zeroized_from_definition( + definition_id: AccountId, + definition: &TokenDefinition, + ) -> Self { + match definition { + TokenDefinition::Fungible { .. } => TokenHolding::Fungible { + definition_id, + balance: 0, + }, + TokenDefinition::NonFungible { .. } => TokenHolding::NftPrintedCopy { + definition_id, + owned: false, + }, + } + } + + pub fn definition_id(&self) -> AccountId { + match self { + TokenHolding::Fungible { definition_id, .. } => *definition_id, + TokenHolding::NftMaster { definition_id, .. } => *definition_id, + TokenHolding::NftPrintedCopy { definition_id, .. } => *definition_id, + } + } +} + +impl TryFrom<&Data> for TokenHolding { + type Error = std::io::Error; + + fn try_from(data: &Data) -> Result { + TokenHolding::try_from_slice(data.as_ref()) + } +} + +impl From<&TokenHolding> for Data { + fn from(holding: &TokenHolding) -> Self { + // Using size_of_val as size hint for Vec allocation + let mut data = Vec::with_capacity(std::mem::size_of_val(holding)); + + BorshSerialize::serialize(holding, &mut data) + .expect("Serialization to Vec should not fail"); + + Data::try_from(data).expect("Token holding encoded data should fit into Data") + } +} + +#[derive(Serialize, Deserialize)] +pub struct NewTokenMetadata { + /// Metadata standard. + pub standard: MetadataStandard, + /// Pointer to off-chain metadata + pub uri: String, + /// Creators of the token. + pub creators: String, +} + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub struct TokenMetadata { + /// Token Definition account id. + pub definition_id: AccountId, + /// Metadata standard . + pub standard: MetadataStandard, + /// Pointer to off-chain metadata. + pub uri: String, + /// Creators of the token. + pub creators: String, + /// Block id of primary sale. + pub primary_sale_date: u64, +} + +/// Metadata standard defining the expected format of JSON located off-chain. +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub enum MetadataStandard { + Simple, + Expanded, +} + +impl TryFrom<&Data> for TokenMetadata { + type Error = std::io::Error; + + fn try_from(data: &Data) -> Result { + TokenMetadata::try_from_slice(data.as_ref()) + } +} + +impl From<&TokenMetadata> for Data { + fn from(metadata: &TokenMetadata) -> Self { + // Using size_of_val as size hint for Vec allocation + let mut data = Vec::with_capacity(std::mem::size_of_val(metadata)); + + BorshSerialize::serialize(metadata, &mut data) + .expect("Serialization to Vec should not fail"); + + Data::try_from(data).expect("Token metadata encoded data should fit into Data") + } +} diff --git a/programs/token/src/burn.rs b/programs/token/src/burn.rs new file mode 100644 index 00000000..94637d92 --- /dev/null +++ b/programs/token/src/burn.rs @@ -0,0 +1,104 @@ +use nssa_core::{ + account::{AccountWithMetadata, Data}, + program::AccountPostState, +}; +use token_core::{TokenDefinition, TokenHolding}; + +pub fn burn( + definition_account: AccountWithMetadata, + user_holding_account: AccountWithMetadata, + amount_to_burn: u128, +) -> Vec { + assert!( + user_holding_account.is_authorized, + "Authorization is missing" + ); + + let mut definition = TokenDefinition::try_from(&definition_account.account.data) + .expect("Token Definition account must be valid"); + let mut holding = TokenHolding::try_from(&user_holding_account.account.data) + .expect("Token Holding account must be valid"); + + assert_eq!( + definition_account.account_id, + holding.definition_id(), + "Mismatch Token Definition and Token Holding" + ); + + match (&mut definition, &mut holding) { + ( + TokenDefinition::Fungible { + name: _, + metadata_id: _, + total_supply, + }, + TokenHolding::Fungible { + definition_id: _, + balance, + }, + ) => { + *balance = balance + .checked_sub(amount_to_burn) + .expect("Insufficient balance to burn"); + + *total_supply = total_supply + .checked_sub(amount_to_burn) + .expect("Total supply underflow"); + } + ( + TokenDefinition::NonFungible { + name: _, + printable_supply, + metadata_id: _, + }, + TokenHolding::NftMaster { + definition_id: _, + print_balance, + }, + ) => { + *printable_supply = printable_supply + .checked_sub(amount_to_burn) + .expect("Printable supply underflow"); + + *print_balance = print_balance + .checked_sub(amount_to_burn) + .expect("Insufficient balance to burn"); + } + ( + TokenDefinition::NonFungible { + name: _, + printable_supply, + metadata_id: _, + }, + TokenHolding::NftPrintedCopy { + definition_id: _, + owned, + }, + ) => { + assert_eq!( + amount_to_burn, 1, + "Invalid balance to burn for NFT Printed Copy" + ); + + assert!(*owned, "Cannot burn unowned NFT Printed Copy"); + + *printable_supply = printable_supply + .checked_sub(1) + .expect("Printable supply underflow"); + + *owned = false; + } + _ => panic!("Mismatched Token Definition and Token Holding types"), + } + + let mut definition_post = definition_account.account; + definition_post.data = Data::from(&definition); + + let mut holding_post = user_holding_account.account; + holding_post.data = Data::from(&holding); + + vec![ + AccountPostState::new(definition_post), + AccountPostState::new(holding_post), + ] +} diff --git a/programs/token/src/initialize.rs b/programs/token/src/initialize.rs new file mode 100644 index 00000000..744fdb64 --- /dev/null +++ b/programs/token/src/initialize.rs @@ -0,0 +1,34 @@ +use nssa_core::{ + account::{Account, AccountWithMetadata, Data}, + program::AccountPostState, +}; +use token_core::{TokenDefinition, TokenHolding}; + +pub fn initialize_account( + definition_account: AccountWithMetadata, + account_to_initialize: AccountWithMetadata, +) -> Vec { + assert_eq!( + account_to_initialize.account, + Account::default(), + "Only Uninitialized accounts can be initialized" + ); + + // TODO: #212 We should check that this is an account owned by the token program. + // This check can't be done here since the ID of the program is known only after compiling it + // + // Check definition account is valid + let definition = TokenDefinition::try_from(&definition_account.account.data) + .expect("Definition account must be valid"); + let holding = + TokenHolding::zeroized_from_definition(definition_account.account_id, &definition); + + let definition_post = definition_account.account; + let mut account_to_initialize = account_to_initialize.account; + account_to_initialize.data = Data::from(&holding); + + vec![ + AccountPostState::new(definition_post), + AccountPostState::new_claimed(account_to_initialize), + ] +} diff --git a/programs/token/src/lib.rs b/programs/token/src/lib.rs new file mode 100644 index 00000000..8b0698c5 --- /dev/null +++ b/programs/token/src/lib.rs @@ -0,0 +1,12 @@ +//! The Token Program implementation. + +pub use token_core as core; + +pub mod burn; +pub mod initialize; +pub mod mint; +pub mod new_definition; +pub mod print_nft; +pub mod transfer; + +mod tests; diff --git a/programs/token/src/mint.rs b/programs/token/src/mint.rs new file mode 100644 index 00000000..2f17cc62 --- /dev/null +++ b/programs/token/src/mint.rs @@ -0,0 +1,71 @@ +use nssa_core::{ + account::{Account, AccountWithMetadata, Data}, + program::AccountPostState, +}; +use token_core::{TokenDefinition, TokenHolding}; + +pub fn mint( + definition_account: AccountWithMetadata, + user_holding_account: AccountWithMetadata, + amount_to_mint: u128, +) -> Vec { + assert!( + definition_account.is_authorized, + "Definition authorization is missing" + ); + + let mut definition = TokenDefinition::try_from(&definition_account.account.data) + .expect("Token Definition account must be valid"); + let mut holding = if user_holding_account.account == Account::default() { + TokenHolding::zeroized_from_definition(definition_account.account_id, &definition) + } else { + TokenHolding::try_from(&user_holding_account.account.data) + .expect("Token Holding account must be valid") + }; + + assert_eq!( + definition_account.account_id, + holding.definition_id(), + "Mismatch Token Definition and Token Holding" + ); + + match (&mut definition, &mut holding) { + ( + TokenDefinition::Fungible { + name: _, + metadata_id: _, + total_supply, + }, + TokenHolding::Fungible { + definition_id: _, + balance, + }, + ) => { + *balance = balance + .checked_add(amount_to_mint) + .expect("Balance overflow on minting"); + + *total_supply = total_supply + .checked_add(amount_to_mint) + .expect("Total supply overflow"); + } + ( + TokenDefinition::NonFungible { .. }, + TokenHolding::NftMaster { .. } | TokenHolding::NftPrintedCopy { .. }, + ) => { + panic!("Cannot mint additional supply for Non-Fungible Tokens"); + } + _ => panic!("Mismatched Token Definition and Token Holding types"), + } + + let mut definition_post = definition_account.account; + definition_post.data = Data::from(&definition); + + let mut holding_post = user_holding_account.account; + holding_post.data = Data::from(&holding); + + vec![ + AccountPostState::new(definition_post), + AccountPostState::new_claimed_if_default(holding_post), + ] +} diff --git a/programs/token/src/new_definition.rs b/programs/token/src/new_definition.rs new file mode 100644 index 00000000..b2a9ae9f --- /dev/null +++ b/programs/token/src/new_definition.rs @@ -0,0 +1,124 @@ +use nssa_core::{ + account::{Account, AccountWithMetadata, Data}, + program::AccountPostState, +}; +use token_core::{ + NewTokenDefinition, NewTokenMetadata, TokenDefinition, TokenHolding, TokenMetadata, +}; + +pub fn new_fungible_definition( + definition_target_account: AccountWithMetadata, + holding_target_account: AccountWithMetadata, + name: String, + total_supply: u128, +) -> Vec { + assert_eq!( + definition_target_account.account, + Account::default(), + "Definition target account must have default values" + ); + + assert_eq!( + holding_target_account.account, + Account::default(), + "Holding target account must have default values" + ); + + let token_definition = TokenDefinition::Fungible { + name, + total_supply, + metadata_id: None, + }; + let token_holding = TokenHolding::Fungible { + definition_id: definition_target_account.account_id, + balance: total_supply, + }; + + let mut definition_target_account_post = definition_target_account.account; + definition_target_account_post.data = Data::from(&token_definition); + + let mut holding_target_account_post = holding_target_account.account; + holding_target_account_post.data = Data::from(&token_holding); + + vec![ + AccountPostState::new_claimed(definition_target_account_post), + AccountPostState::new_claimed(holding_target_account_post), + ] +} + +pub fn new_definition_with_metadata( + definition_target_account: AccountWithMetadata, + holding_target_account: AccountWithMetadata, + metadata_target_account: AccountWithMetadata, + new_definition: NewTokenDefinition, + metadata: NewTokenMetadata, +) -> Vec { + assert_eq!( + definition_target_account.account, + Account::default(), + "Definition target account must have default values" + ); + + assert_eq!( + holding_target_account.account, + Account::default(), + "Holding target account must have default values" + ); + + assert_eq!( + metadata_target_account.account, + Account::default(), + "Metadata target account must have default values" + ); + + let (token_definition, token_holding) = match new_definition { + NewTokenDefinition::Fungible { name, total_supply } => ( + TokenDefinition::Fungible { + name, + total_supply, + metadata_id: Some(metadata_target_account.account_id), + }, + TokenHolding::Fungible { + definition_id: definition_target_account.account_id, + balance: total_supply, + }, + ), + NewTokenDefinition::NonFungible { + name, + printable_supply, + } => ( + TokenDefinition::NonFungible { + name, + printable_supply, + metadata_id: metadata_target_account.account_id, + }, + TokenHolding::NftMaster { + definition_id: definition_target_account.account_id, + print_balance: printable_supply, + }, + ), + }; + + let token_metadata = TokenMetadata { + definition_id: definition_target_account.account_id, + standard: metadata.standard, + uri: metadata.uri, + creators: metadata.creators, + primary_sale_date: 0u64, // TODO #261: future works to implement this + }; + + let mut definition_target_account_post = definition_target_account.account.clone(); + definition_target_account_post.data = Data::from(&token_definition); + + let mut holding_target_account_post = holding_target_account.account.clone(); + holding_target_account_post.data = Data::from(&token_holding); + + let mut metadata_target_account_post = metadata_target_account.account.clone(); + metadata_target_account_post.data = Data::from(&token_metadata); + + vec![ + AccountPostState::new_claimed(definition_target_account_post), + AccountPostState::new_claimed(holding_target_account_post), + AccountPostState::new_claimed(metadata_target_account_post), + ] +} diff --git a/programs/token/src/print_nft.rs b/programs/token/src/print_nft.rs new file mode 100644 index 00000000..d10533c1 --- /dev/null +++ b/programs/token/src/print_nft.rs @@ -0,0 +1,54 @@ +use nssa_core::{ + account::{Account, AccountWithMetadata, Data}, + program::AccountPostState, +}; +use token_core::TokenHolding; + +pub fn print_nft( + master_account: AccountWithMetadata, + printed_account: AccountWithMetadata, +) -> Vec { + assert!( + master_account.is_authorized, + "Master NFT Account must be authorized" + ); + + assert_eq!( + printed_account.account, + Account::default(), + "Printed Account must be uninitialized" + ); + + let mut master_account_data = + TokenHolding::try_from(&master_account.account.data).expect("Invalid Token Holding data"); + + let TokenHolding::NftMaster { + definition_id, + print_balance, + } = &mut master_account_data + else { + panic!("Invalid Token Holding provided as NFT Master Account"); + }; + + let definition_id = *definition_id; + + assert!( + *print_balance > 1, + "Insufficient balance to print another NFT copy" + ); + *print_balance -= 1; + + let mut master_account_post = master_account.account; + master_account_post.data = Data::from(&master_account_data); + + let mut printed_account_post = printed_account.account; + printed_account_post.data = Data::from(&TokenHolding::NftPrintedCopy { + definition_id, + owned: true, + }); + + vec![ + AccountPostState::new(master_account_post), + AccountPostState::new_claimed(printed_account_post), + ] +} diff --git a/programs/token/src/tests.rs b/programs/token/src/tests.rs new file mode 100644 index 00000000..cf95c4d4 --- /dev/null +++ b/programs/token/src/tests.rs @@ -0,0 +1,1040 @@ +#![cfg(test)] + +use nssa_core::account::{Account, AccountId, AccountWithMetadata, Data}; +use token_core::{ + MetadataStandard, NewTokenDefinition, NewTokenMetadata, TokenDefinition, TokenHolding, +}; + +use crate::{ + burn::burn, + mint::mint, + new_definition::{new_definition_with_metadata, new_fungible_definition}, + print_nft::print_nft, + transfer::transfer, +}; + +// TODO: Move tests to a proper modules like burn, mint, transfer, etc, so that they are more +// unit-test. + +struct BalanceForTests; +struct IdForTests; + +struct AccountForTests; + +impl AccountForTests { + fn definition_account_auth() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: Data::from(&TokenDefinition::Fungible { + name: String::from("test"), + total_supply: BalanceForTests::init_supply(), + metadata_id: None, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn definition_account_without_auth() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: Data::from(&TokenDefinition::Fungible { + name: String::from("test"), + total_supply: BalanceForTests::init_supply(), + metadata_id: None, + }), + nonce: 0, + }, + is_authorized: false, + account_id: IdForTests::pool_definition_id(), + } + } + + fn holding_different_definition() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::pool_definition_id_diff(), + balance: BalanceForTests::holding_balance(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id(), + } + } + + fn holding_same_definition_with_authorization() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::holding_balance(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id(), + } + } + + fn holding_same_definition_without_authorization() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::holding_balance(), + }), + nonce: 0, + }, + is_authorized: false, + account_id: IdForTests::holding_id(), + } + } + + fn holding_same_definition_without_authorization_overflow() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::init_supply(), + }), + nonce: 0, + }, + is_authorized: false, + account_id: IdForTests::holding_id(), + } + } + + fn definition_account_post_burn() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: Data::from(&TokenDefinition::Fungible { + name: String::from("test"), + total_supply: BalanceForTests::init_supply_burned(), + metadata_id: None, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn holding_account_post_burn() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::holding_balance_burned(), + }), + nonce: 0, + }, + is_authorized: false, + account_id: IdForTests::holding_id(), + } + } + + fn holding_account_uninit() -> AccountWithMetadata { + AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: IdForTests::holding_id_2(), + } + } + + fn init_mint() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [0u32; 8], + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::mint_success(), + }), + nonce: 0, + }, + is_authorized: false, + account_id: IdForTests::holding_id(), + } + } + + fn holding_account_same_definition_mint() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::holding_balance_mint(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn definition_account_mint() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: Data::from(&TokenDefinition::Fungible { + name: String::from("test"), + total_supply: BalanceForTests::init_supply_mint(), + metadata_id: None, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn holding_same_definition_with_authorization_and_large_balance() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::mint_overflow(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn definition_account_with_authorization_nonfungible() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: Data::from(&TokenDefinition::NonFungible { + name: String::from("test"), + printable_supply: BalanceForTests::printable_copies(), + metadata_id: AccountId::new([0; 32]), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn definition_account_uninit() -> AccountWithMetadata { + AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: IdForTests::pool_definition_id(), + } + } + + fn holding_account_init() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::init_supply(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id(), + } + } + + fn definition_account_unclaimed() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [0u32; 8], + balance: 0u128, + data: Data::from(&TokenDefinition::Fungible { + name: String::from("test"), + total_supply: BalanceForTests::init_supply(), + metadata_id: None, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn holding_account_unclaimed() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [0u32; 8], + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::init_supply(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id(), + } + } + + fn holding_account2_init() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::init_supply(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id_2(), + } + } + + fn holding_account2_init_post_transfer() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::recipient_post_transfer(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id_2(), + } + } + + fn holding_account_init_post_transfer() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::sender_post_transfer(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id(), + } + } + + fn holding_account_master_nft() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: Data::from(&TokenHolding::NftMaster { + definition_id: IdForTests::pool_definition_id(), + print_balance: BalanceForTests::printable_copies(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id(), + } + } + + fn holding_account_master_nft_insufficient_balance() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: Data::from(&TokenHolding::NftMaster { + definition_id: IdForTests::pool_definition_id(), + print_balance: 1, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id(), + } + } + + fn holding_account_master_nft_after_print() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: Data::from(&TokenHolding::NftMaster { + definition_id: IdForTests::pool_definition_id(), + print_balance: BalanceForTests::printable_copies() - 1, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id(), + } + } + + fn holding_account_printed_nft() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [0u32; 8], + balance: 0u128, + data: Data::from(&TokenHolding::NftPrintedCopy { + definition_id: IdForTests::pool_definition_id(), + owned: true, + }), + nonce: 0, + }, + is_authorized: false, + account_id: IdForTests::holding_id(), + } + } + + fn holding_account_with_master_nft_transferred_to() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [0u32; 8], + balance: 0u128, + data: Data::from(&TokenHolding::NftMaster { + definition_id: IdForTests::pool_definition_id(), + print_balance: BalanceForTests::printable_copies(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id_2(), + } + } + + fn holding_account_master_nft_post_transfer() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: Data::from(&TokenHolding::NftMaster { + definition_id: IdForTests::pool_definition_id(), + print_balance: 0, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id(), + } + } +} + +impl BalanceForTests { + fn init_supply() -> u128 { + 100_000 + } + + fn holding_balance() -> u128 { + 1_000 + } + + fn init_supply_burned() -> u128 { + 99_500 + } + + fn holding_balance_burned() -> u128 { + 500 + } + + fn burn_success() -> u128 { + 500 + } + + fn burn_insufficient() -> u128 { + 1_500 + } + + fn mint_success() -> u128 { + 50_000 + } + + fn holding_balance_mint() -> u128 { + 51_000 + } + + fn mint_overflow() -> u128 { + u128::MAX - 40_000 + } + + fn init_supply_mint() -> u128 { + 150_000 + } + + fn sender_post_transfer() -> u128 { + 95_000 + } + + fn recipient_post_transfer() -> u128 { + 105_000 + } + + fn transfer_amount() -> u128 { + 5_000 + } + + fn printable_copies() -> u128 { + 10 + } +} + +impl IdForTests { + fn pool_definition_id() -> AccountId { + AccountId::new([15; 32]) + } + + fn pool_definition_id_diff() -> AccountId { + AccountId::new([16; 32]) + } + + fn holding_id() -> AccountId { + AccountId::new([17; 32]) + } + + fn holding_id_2() -> AccountId { + AccountId::new([42; 32]) + } +} + +#[should_panic(expected = "Definition target account must have default values")] +#[test] +fn test_new_definition_non_default_first_account_should_fail() { + let definition_account = AccountWithMetadata { + account: Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([1; 32]), + }; + let holding_account = AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }; + let _post_states = new_fungible_definition( + definition_account, + holding_account, + String::from("test"), + 10, + ); +} + +#[should_panic(expected = "Holding target account must have default values")] +#[test] +fn test_new_definition_non_default_second_account_should_fail() { + let definition_account = AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }; + let holding_account = AccountWithMetadata { + account: Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + ..Account::default() + }, + is_authorized: true, + account_id: AccountId::new([2; 32]), + }; + let _post_states = new_fungible_definition( + definition_account, + holding_account, + String::from("test"), + 10, + ); +} + +#[test] +fn test_new_definition_with_valid_inputs_succeeds() { + let definition_account = AccountForTests::definition_account_uninit(); + let holding_account = AccountForTests::holding_account_uninit(); + + let post_states = new_fungible_definition( + definition_account, + holding_account, + String::from("test"), + BalanceForTests::init_supply(), + ); + + let [definition_account, holding_account] = post_states.try_into().unwrap(); + assert_eq!( + *definition_account.account(), + AccountForTests::definition_account_unclaimed().account + ); + + assert_eq!( + *holding_account.account(), + AccountForTests::holding_account_unclaimed().account + ); +} + +#[should_panic(expected = "Sender and recipient definition id mismatch")] +#[test] +fn test_transfer_with_different_definition_ids_should_fail() { + let sender = AccountForTests::holding_same_definition_with_authorization(); + let recipient = AccountForTests::holding_different_definition(); + let _post_states = transfer(sender, recipient, 10); +} + +#[should_panic(expected = "Insufficient balance")] +#[test] +fn test_transfer_with_insufficient_balance_should_fail() { + let sender = AccountForTests::holding_same_definition_with_authorization(); + let recipient = AccountForTests::holding_account_same_definition_mint(); + // Attempt to transfer more than balance + let _post_states = transfer(sender, recipient, BalanceForTests::burn_insufficient()); +} + +#[should_panic(expected = "Sender authorization is missing")] +#[test] +fn test_transfer_without_sender_authorization_should_fail() { + let sender = AccountForTests::holding_same_definition_without_authorization(); + let recipient = AccountForTests::holding_account_uninit(); + let _post_states = transfer(sender, recipient, 37); +} + +#[test] +fn test_transfer_with_valid_inputs_succeeds() { + let sender = AccountForTests::holding_account_init(); + let recipient = AccountForTests::holding_account2_init(); + let post_states = transfer(sender, recipient, BalanceForTests::transfer_amount()); + let [sender_post, recipient_post] = post_states.try_into().unwrap(); + + assert_eq!( + *sender_post.account(), + AccountForTests::holding_account_init_post_transfer().account + ); + assert_eq!( + *recipient_post.account(), + AccountForTests::holding_account2_init_post_transfer().account + ); +} + +#[should_panic(expected = "Invalid balance for NFT Master transfer")] +#[test] +fn test_transfer_with_master_nft_invalid_balance() { + let sender = AccountForTests::holding_account_master_nft(); + let recipient = AccountForTests::holding_account_uninit(); + let _post_states = transfer(sender, recipient, BalanceForTests::transfer_amount()); +} + +#[should_panic(expected = "Invalid balance in recipient account for NFT transfer")] +#[test] +fn test_transfer_with_master_nft_invalid_recipient_balance() { + let sender = AccountForTests::holding_account_master_nft(); + let recipient = AccountForTests::holding_account_with_master_nft_transferred_to(); + let _post_states = transfer(sender, recipient, BalanceForTests::printable_copies()); +} + +#[test] +fn test_transfer_with_master_nft_success() { + let sender = AccountForTests::holding_account_master_nft(); + let recipient = AccountForTests::holding_account_uninit(); + let post_states = transfer(sender, recipient, BalanceForTests::printable_copies()); + let [sender_post, recipient_post] = post_states.try_into().unwrap(); + + assert_eq!( + *sender_post.account(), + AccountForTests::holding_account_master_nft_post_transfer().account + ); + assert_eq!( + *recipient_post.account(), + AccountForTests::holding_account_with_master_nft_transferred_to().account + ); +} + +#[test] +fn test_token_initialize_account_succeeds() { + let sender = AccountForTests::holding_account_init(); + let recipient = AccountForTests::holding_account2_init(); + let post_states = transfer(sender, recipient, BalanceForTests::transfer_amount()); + let [sender_post, recipient_post] = post_states.try_into().unwrap(); + + assert_eq!( + *sender_post.account(), + AccountForTests::holding_account_init_post_transfer().account + ); + assert_eq!( + *recipient_post.account(), + AccountForTests::holding_account2_init_post_transfer().account + ); +} + +#[test] +#[should_panic(expected = "Mismatch Token Definition and Token Holding")] +fn test_burn_mismatch_def() { + let definition_account = AccountForTests::definition_account_auth(); + let holding_account = AccountForTests::holding_different_definition(); + let _post_states = burn( + definition_account, + holding_account, + BalanceForTests::burn_success(), + ); +} + +#[test] +#[should_panic(expected = "Authorization is missing")] +fn test_burn_missing_authorization() { + let definition_account = AccountForTests::definition_account_auth(); + let holding_account = AccountForTests::holding_same_definition_without_authorization(); + let _post_states = burn( + definition_account, + holding_account, + BalanceForTests::burn_success(), + ); +} + +#[test] +#[should_panic(expected = "Insufficient balance to burn")] +fn test_burn_insufficient_balance() { + let definition_account = AccountForTests::definition_account_auth(); + let holding_account = AccountForTests::holding_same_definition_with_authorization(); + let _post_states = burn( + definition_account, + holding_account, + BalanceForTests::burn_insufficient(), + ); +} + +#[test] +#[should_panic(expected = "Total supply underflow")] +fn test_burn_total_supply_underflow() { + let definition_account = AccountForTests::definition_account_auth(); + let holding_account = + AccountForTests::holding_same_definition_with_authorization_and_large_balance(); + let _post_states = burn( + definition_account, + holding_account, + BalanceForTests::mint_overflow(), + ); +} + +#[test] +fn test_burn_success() { + let definition_account = AccountForTests::definition_account_auth(); + let holding_account = AccountForTests::holding_same_definition_with_authorization(); + let post_states = burn( + definition_account, + holding_account, + BalanceForTests::burn_success(), + ); + + let [def_post, holding_post] = post_states.try_into().unwrap(); + + assert_eq!( + *def_post.account(), + AccountForTests::definition_account_post_burn().account + ); + assert_eq!( + *holding_post.account(), + AccountForTests::holding_account_post_burn().account + ); +} + +#[test] +#[should_panic(expected = "Holding account must be valid")] +fn test_mint_not_valid_holding_account() { + let definition_account = AccountForTests::definition_account_auth(); + let holding_account = AccountForTests::definition_account_without_auth(); + let _post_states = mint( + definition_account, + holding_account, + BalanceForTests::mint_success(), + ); +} + +#[test] +#[should_panic(expected = "Definition account must be valid")] +fn test_mint_not_valid_definition_account() { + let definition_account = AccountForTests::holding_same_definition_with_authorization(); + let holding_account = AccountForTests::holding_same_definition_without_authorization(); + let _post_states = mint( + definition_account, + holding_account, + BalanceForTests::mint_success(), + ); +} + +#[test] +#[should_panic(expected = "Definition authorization is missing")] +fn test_mint_missing_authorization() { + let definition_account = AccountForTests::definition_account_without_auth(); + let holding_account = AccountForTests::holding_same_definition_without_authorization(); + let _post_states = mint( + definition_account, + holding_account, + BalanceForTests::mint_success(), + ); +} + +#[test] +#[should_panic(expected = "Mismatch Token Definition and Token Holding")] +fn test_mint_mismatched_token_definition() { + let definition_account = AccountForTests::definition_account_auth(); + let holding_account = AccountForTests::holding_different_definition(); + let _post_states = mint( + definition_account, + holding_account, + BalanceForTests::mint_success(), + ); +} + +#[test] +fn test_mint_success() { + let definition_account = AccountForTests::definition_account_auth(); + let holding_account = AccountForTests::holding_same_definition_without_authorization(); + let post_states = mint( + definition_account, + holding_account, + BalanceForTests::mint_success(), + ); + + let [def_post, holding_post] = post_states.try_into().unwrap(); + + assert_eq!( + *def_post.account(), + AccountForTests::definition_account_mint().account + ); + assert_eq!( + *holding_post.account(), + AccountForTests::holding_account_same_definition_mint().account + ); +} + +#[test] +fn test_mint_uninit_holding_success() { + let definition_account = AccountForTests::definition_account_auth(); + let holding_account = AccountForTests::holding_account_uninit(); + let post_states = mint( + definition_account, + holding_account, + BalanceForTests::mint_success(), + ); + + let [def_post, holding_post] = post_states.try_into().unwrap(); + + assert_eq!( + *def_post.account(), + AccountForTests::definition_account_mint().account + ); + assert_eq!( + *holding_post.account(), + AccountForTests::init_mint().account + ); + assert!(holding_post.requires_claim()); +} + +#[test] +#[should_panic(expected = "Total supply overflow")] +fn test_mint_total_supply_overflow() { + let definition_account = AccountForTests::definition_account_auth(); + let holding_account = AccountForTests::holding_same_definition_without_authorization(); + let _post_states = mint( + definition_account, + holding_account, + BalanceForTests::mint_overflow(), + ); +} + +#[test] +#[should_panic(expected = "Balance overflow on minting")] +fn test_mint_holding_account_overflow() { + let definition_account = AccountForTests::definition_account_auth(); + let holding_account = AccountForTests::holding_same_definition_without_authorization_overflow(); + let _post_states = mint( + definition_account, + holding_account, + BalanceForTests::mint_overflow(), + ); +} + +#[test] +#[should_panic(expected = "Cannot mint additional supply for Non-Fungible Tokens")] +fn test_mint_cannot_mint_unmintable_tokens() { + let definition_account = AccountForTests::definition_account_with_authorization_nonfungible(); + let holding_account = AccountForTests::holding_account_master_nft(); + let _post_states = mint( + definition_account, + holding_account, + BalanceForTests::mint_success(), + ); +} + +#[should_panic(expected = "Definition target account must have default values")] +#[test] +fn test_call_new_definition_metadata_with_init_definition() { + let definition_account = AccountForTests::definition_account_auth(); + let metadata_account = AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }; + let holding_account = AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([3; 32]), + }; + let new_definition = NewTokenDefinition::Fungible { + name: String::from("test"), + total_supply: 15u128, + }; + let metadata = NewTokenMetadata { + standard: MetadataStandard::Simple, + uri: "test_uri".to_string(), + creators: "test_creators".to_string(), + }; + let _post_states = new_definition_with_metadata( + definition_account, + metadata_account, + holding_account, + new_definition, + metadata, + ); +} + +#[should_panic(expected = "Metadata target account must have default values")] +#[test] +fn test_call_new_definition_metadata_with_init_metadata() { + let definition_account = AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }; + let holding_account = AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([3; 32]), + }; + let metadata_account = AccountForTests::holding_account_same_definition_mint(); + let new_definition = NewTokenDefinition::Fungible { + name: String::from("test"), + total_supply: 15u128, + }; + let metadata = NewTokenMetadata { + standard: MetadataStandard::Simple, + uri: "test_uri".to_string(), + creators: "test_creators".to_string(), + }; + let _post_states = new_definition_with_metadata( + definition_account, + holding_account, + metadata_account, + new_definition, + metadata, + ); +} + +#[should_panic(expected = "Holding target account must have default values")] +#[test] +fn test_call_new_definition_metadata_with_init_holding() { + let definition_account = AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }; + let metadata_account = AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }; + let holding_account = AccountForTests::holding_account_same_definition_mint(); + let new_definition = NewTokenDefinition::Fungible { + name: String::from("test"), + total_supply: 15u128, + }; + let metadata = NewTokenMetadata { + standard: MetadataStandard::Simple, + uri: "test_uri".to_string(), + creators: "test_creators".to_string(), + }; + let _post_states = new_definition_with_metadata( + definition_account, + holding_account, + metadata_account, + new_definition, + metadata, + ); +} + +#[should_panic(expected = "Master NFT Account must be authorized")] +#[test] +fn test_print_nft_master_account_must_be_authorized() { + let master_account = AccountForTests::holding_account_uninit(); + let printed_account = AccountForTests::holding_account_uninit(); + let _post_states = print_nft(master_account, printed_account); +} + +#[should_panic(expected = "Printed Account must be uninitialized")] +#[test] +fn test_print_nft_print_account_initialized() { + let master_account = AccountForTests::holding_account_master_nft(); + let printed_account = AccountForTests::holding_account_init(); + let _post_states = print_nft(master_account, printed_account); +} + +#[should_panic(expected = "Invalid Token Holding data")] +#[test] +fn test_print_nft_master_nft_invalid_token_holding() { + let master_account = AccountForTests::definition_account_auth(); + let printed_account = AccountForTests::holding_account_uninit(); + let _post_states = print_nft(master_account, printed_account); +} + +#[should_panic(expected = "Invalid Token Holding provided as NFT Master Account")] +#[test] +fn test_print_nft_master_nft_not_nft_master_account() { + let master_account = AccountForTests::holding_account_init(); + let printed_account = AccountForTests::holding_account_uninit(); + let _post_states = print_nft(master_account, printed_account); +} + +#[should_panic(expected = "Insufficient balance to print another NFT copy")] +#[test] +fn test_print_nft_master_nft_insufficient_balance() { + let master_account = AccountForTests::holding_account_master_nft_insufficient_balance(); + let printed_account = AccountForTests::holding_account_uninit(); + let _post_states = print_nft(master_account, printed_account); +} + +#[test] +fn test_print_nft_success() { + let master_account = AccountForTests::holding_account_master_nft(); + let printed_account = AccountForTests::holding_account_uninit(); + let post_states = print_nft(master_account, printed_account); + + let [post_master_nft, post_printed] = post_states.try_into().unwrap(); + + assert_eq!( + *post_master_nft.account(), + AccountForTests::holding_account_master_nft_after_print().account + ); + assert_eq!( + *post_printed.account(), + AccountForTests::holding_account_printed_nft().account + ); +} diff --git a/programs/token/src/transfer.rs b/programs/token/src/transfer.rs new file mode 100644 index 00000000..a1087bb1 --- /dev/null +++ b/programs/token/src/transfer.rs @@ -0,0 +1,110 @@ +use nssa_core::{ + account::{Account, AccountWithMetadata, Data}, + program::AccountPostState, +}; +use token_core::TokenHolding; + +pub fn transfer( + sender: AccountWithMetadata, + recipient: AccountWithMetadata, + balance_to_move: u128, +) -> Vec { + assert!(sender.is_authorized, "Sender authorization is missing"); + + let mut sender_holding = + TokenHolding::try_from(&sender.account.data).expect("Invalid sender data"); + + let mut recipient_holding = if recipient.account == Account::default() { + TokenHolding::zeroized_clone_from(&sender_holding) + } else { + TokenHolding::try_from(&recipient.account.data).expect("Invalid recipient data") + }; + + assert_eq!( + sender_holding.definition_id(), + recipient_holding.definition_id(), + "Sender and recipient definition id mismatch" + ); + + match (&mut sender_holding, &mut recipient_holding) { + ( + TokenHolding::Fungible { + definition_id: _, + balance: sender_balance, + }, + TokenHolding::Fungible { + definition_id: _, + balance: recipient_balance, + }, + ) => { + *sender_balance = sender_balance + .checked_sub(balance_to_move) + .expect("Insufficient balance"); + + *recipient_balance = recipient_balance + .checked_add(balance_to_move) + .expect("Recipient balance overflow"); + } + ( + TokenHolding::NftMaster { + definition_id: _, + print_balance: sender_print_balance, + }, + TokenHolding::NftMaster { + definition_id: _, + print_balance: recipient_print_balance, + }, + ) => { + assert_eq!( + *recipient_print_balance, 0, + "Invalid balance in recipient account for NFT transfer" + ); + + assert_eq!( + *sender_print_balance, balance_to_move, + "Invalid balance for NFT Master transfer" + ); + + std::mem::swap(sender_print_balance, recipient_print_balance); + } + ( + TokenHolding::NftPrintedCopy { + definition_id: _, + owned: sender_owned, + }, + TokenHolding::NftPrintedCopy { + definition_id: _, + owned: recipient_owned, + }, + ) => { + assert_eq!( + balance_to_move, 1, + "Invalid balance for NFT Printed Copy transfer" + ); + + assert!(*sender_owned, "Sender does not own the NFT Printed Copy"); + + assert!( + !*recipient_owned, + "Recipient already owns the NFT Printed Copy" + ); + + *sender_owned = false; + *recipient_owned = true; + } + _ => { + panic!("Mismatched token holding types for transfer"); + } + }; + + let mut sender_post = sender.account; + sender_post.data = Data::from(&sender_holding); + + let mut recipient_post = recipient.account; + recipient_post.data = Data::from(&recipient_holding); + + vec![ + AccountPostState::new(sender_post), + AccountPostState::new_claimed_if_default(recipient_post), + ] +} diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index bef25007..9213ce04 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -8,6 +8,7 @@ nssa_core.workspace = true nssa.workspace = true common.workspace = true key_protocol.workspace = true +token_core.workspace = true anyhow.workspace = true serde_json.workspace = true diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 21e59366..b3663e02 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -4,10 +4,10 @@ use clap::Subcommand; use itertools::Itertools as _; use key_protocol::key_management::key_tree::chain_index::ChainIndex; use nssa::{Account, PublicKey, program::Program}; -use serde::Serialize; +use token_core::{TokenDefinition, TokenHolding}; use crate::{ - TokenDefinition, TokenHolding, WalletCore, + WalletCore, cli::{SubcommandReturnValue, WalletSubcommand}, helperfunctions::{AccountPrivacyKind, HumanReadableAccount, parse_addr_with_privacy_prefix}, }; @@ -111,83 +111,26 @@ impl WalletSubcommand for NewSubcommand { } } -#[derive(Debug, Serialize)] -pub struct AuthenticatedTransferAccountView { - pub balance: u128, -} - -impl From for AuthenticatedTransferAccountView { - fn from(value: nssa::Account) -> Self { - Self { - balance: value.balance, - } - } -} - -#[derive(Debug, Serialize)] -pub struct TokedDefinitionAccountView { - pub account_type: String, - pub name: String, - pub total_supply: u128, -} - -impl From for TokedDefinitionAccountView { - fn from(value: TokenDefinition) -> Self { - Self { - account_type: "Token definition".to_string(), - name: { - // Assuming, that name does not have UTF-8 NULL and all zeroes are padding. - let name_trimmed: Vec<_> = - value.name.into_iter().take_while(|ch| *ch != 0).collect(); - String::from_utf8(name_trimmed).unwrap_or(hex::encode(value.name)) - }, - total_supply: value.total_supply, - } - } -} - -#[derive(Debug, Serialize)] -pub struct TokedHoldingAccountView { - pub account_type: String, - pub definition_id: String, - pub balance: u128, -} - -impl From for TokedHoldingAccountView { - fn from(value: TokenHolding) -> Self { - Self { - account_type: "Token holding".to_string(), - definition_id: value.definition_id.to_string(), - balance: value.balance, - } - } -} - /// Formats account details for display, returning (description, json_view) fn format_account_details(account: &Account) -> (String, String) { let auth_tr_prog_id = Program::authenticated_transfer_program().id(); let token_prog_id = Program::token().id(); match &account.program_owner { - _ if account.program_owner == auth_tr_prog_id => { - let acc_view: AuthenticatedTransferAccountView = account.clone().into(); - ( - "Account owned by authenticated transfer program".to_string(), - serde_json::to_string(&acc_view).unwrap(), - ) - } - _ if account.program_owner == token_prog_id => { - if let Some(token_def) = TokenDefinition::parse(&account.data) { - let acc_view: TokedDefinitionAccountView = token_def.into(); + o if *o == auth_tr_prog_id => ( + "Account owned by authenticated transfer program".to_string(), + serde_json::to_string(&account).unwrap(), + ), + o if *o == token_prog_id => { + if let Ok(token_def) = TokenDefinition::try_from(&account.data) { ( "Definition account owned by token program".to_string(), - serde_json::to_string(&acc_view).unwrap(), + serde_json::to_string(&token_def).unwrap(), ) - } else if let Some(token_hold) = TokenHolding::parse(&account.data) { - let acc_view: TokedHoldingAccountView = token_hold.into(); + } else if let Ok(token_hold) = TokenHolding::try_from(&account.data) { ( "Holding account owned by token program".to_string(), - serde_json::to_string(&acc_view).unwrap(), + serde_json::to_string(&token_hold).unwrap(), ) } else { let account_hr: HumanReadableAccount = account.clone().into(); @@ -410,52 +353,3 @@ impl WalletSubcommand for AccountSubcommand { } } } - -#[cfg(test)] -mod tests { - use nssa::AccountId; - - use crate::cli::account::{TokedDefinitionAccountView, TokenDefinition}; - - #[test] - fn test_invalid_utf_8_name_of_token() { - let token_def = TokenDefinition { - account_type: 1, - name: [137, 12, 14, 3, 5, 4], - total_supply: 100, - metadata_id: AccountId::new([0; 32]), - }; - - let token_def_view: TokedDefinitionAccountView = token_def.into(); - - assert_eq!(token_def_view.name, "890c0e030504"); - } - - #[test] - fn test_valid_utf_8_name_of_token_all_bytes() { - let token_def = TokenDefinition { - account_type: 1, - name: [240, 159, 146, 150, 66, 66], - total_supply: 100, - metadata_id: AccountId::new([0; 32]), - }; - - let token_def_view: TokedDefinitionAccountView = token_def.into(); - - assert_eq!(token_def_view.name, "💖BB"); - } - - #[test] - fn test_valid_utf_8_name_of_token_less_bytes() { - let token_def = TokenDefinition { - account_type: 1, - name: [78, 65, 77, 69, 0, 0], - total_supply: 100, - metadata_id: AccountId::new([0; 32]), - }; - - let token_def_view: TokedDefinitionAccountView = token_def.into(); - - assert_eq!(token_def_view.name, "NAME"); - } -} diff --git a/wallet/src/cli/programs/token.rs b/wallet/src/cli/programs/token.rs index b5ea1b34..c16c0c94 100644 --- a/wallet/src/cli/programs/token.rs +++ b/wallet/src/cli/programs/token.rs @@ -1258,14 +1258,6 @@ impl WalletSubcommand for CreateNewTokenProgramSubcommand { name, total_supply, } => { - let name = name.as_bytes(); - if name.len() > 6 { - // TODO: return error - panic!("Name length mismatch"); - } - let mut name_bytes = [0; 6]; - name_bytes[..name.len()].copy_from_slice(name); - let definition_account_id: AccountId = definition_account_id.parse().unwrap(); let supply_account_id: AccountId = supply_account_id.parse().unwrap(); @@ -1273,7 +1265,7 @@ impl WalletSubcommand for CreateNewTokenProgramSubcommand { .send_new_definition_private_owned_definiton_and_supply( definition_account_id, supply_account_id, - name_bytes, + name, total_supply, ) .await?; @@ -1307,14 +1299,6 @@ impl WalletSubcommand for CreateNewTokenProgramSubcommand { name, total_supply, } => { - let name = name.as_bytes(); - if name.len() > 6 { - // TODO: return error - panic!("Name length mismatch"); - } - let mut name_bytes = [0; 6]; - name_bytes[..name.len()].copy_from_slice(name); - let definition_account_id: AccountId = definition_account_id.parse().unwrap(); let supply_account_id: AccountId = supply_account_id.parse().unwrap(); @@ -1322,7 +1306,7 @@ impl WalletSubcommand for CreateNewTokenProgramSubcommand { .send_new_definition_private_owned_definiton( definition_account_id, supply_account_id, - name_bytes, + name, total_supply, ) .await?; @@ -1353,14 +1337,6 @@ impl WalletSubcommand for CreateNewTokenProgramSubcommand { name, total_supply, } => { - let name = name.as_bytes(); - if name.len() > 6 { - // TODO: return error - panic!("Name length mismatch"); - } - let mut name_bytes = [0; 6]; - name_bytes[..name.len()].copy_from_slice(name); - let definition_account_id: AccountId = definition_account_id.parse().unwrap(); let supply_account_id: AccountId = supply_account_id.parse().unwrap(); @@ -1368,7 +1344,7 @@ impl WalletSubcommand for CreateNewTokenProgramSubcommand { .send_new_definition_private_owned_supply( definition_account_id, supply_account_id, - name_bytes, + name, total_supply, ) .await?; @@ -1399,18 +1375,11 @@ impl WalletSubcommand for CreateNewTokenProgramSubcommand { name, total_supply, } => { - let name = name.as_bytes(); - if name.len() > 6 { - // TODO: return error - panic!(); - } - let mut name_bytes = [0; 6]; - name_bytes[..name.len()].copy_from_slice(name); Token(wallet_core) .send_new_definition( definition_account_id.parse().unwrap(), supply_account_id.parse().unwrap(), - name_bytes, + name, total_supply, ) .await?; diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 45709d05..84144597 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -18,9 +18,7 @@ use nssa::{ circuit::ProgramWithDependencies, message::EncryptedAccountData, }, }; -use nssa_core::{ - Commitment, MembershipProof, SharedSecretKey, account::Data, program::InstructionData, -}; +use nssa_core::{Commitment, MembershipProof, SharedSecretKey, program::InstructionData}; pub use privacy_preserving_tx::PrivacyPreservingAccount; use tokio::io::AsyncWriteExt; @@ -45,82 +43,6 @@ pub enum AccDecodeData { Decode(nssa_core::SharedSecretKey, AccountId), } -const TOKEN_DEFINITION_DATA_SIZE: usize = 55; - -const TOKEN_HOLDING_TYPE: u8 = 1; -const TOKEN_HOLDING_DATA_SIZE: usize = 49; -const TOKEN_STANDARD_FUNGIBLE_TOKEN: u8 = 0; -const TOKEN_STANDARD_NONFUNGIBLE: u8 = 2; - -struct TokenDefinition { - #[allow(unused)] - account_type: u8, - name: [u8; 6], - total_supply: u128, - #[allow(unused)] - metadata_id: AccountId, -} - -struct TokenHolding { - #[allow(unused)] - account_type: u8, - definition_id: AccountId, - balance: u128, -} - -impl TokenDefinition { - fn parse(data: &Data) -> Option { - let data = Vec::::from(data.clone()); - - if data.len() != TOKEN_DEFINITION_DATA_SIZE { - None - } else { - let account_type = data[0]; - let name = data[1..7].try_into().expect("Name must be a 6 bytes"); - let total_supply = u128::from_le_bytes( - data[7..23] - .try_into() - .expect("Total supply must be 16 bytes little-endian"), - ); - let metadata_id = AccountId::new( - data[23..TOKEN_DEFINITION_DATA_SIZE] - .try_into() - .expect("Token Program expects valid Account Id for Metadata"), - ); - - let this = Some(Self { - account_type, - name, - total_supply, - metadata_id, - }); - - match account_type { - TOKEN_STANDARD_NONFUNGIBLE if total_supply != 1 => None, - TOKEN_STANDARD_FUNGIBLE_TOKEN if metadata_id != AccountId::new([0; 32]) => None, - _ => this, - } - } - } -} - -impl TokenHolding { - fn parse(data: &[u8]) -> Option { - if data.len() != TOKEN_HOLDING_DATA_SIZE || data[0] != TOKEN_HOLDING_TYPE { - None - } else { - let account_type = data[0]; - let definition_id = AccountId::new(data[1..33].try_into().unwrap()); - let balance = u128::from_le_bytes(data[33..].try_into().unwrap()); - Some(Self { - definition_id, - balance, - account_type, - }) - } - } -} - pub struct WalletCore { config_path: PathBuf, storage: WalletChainStore, diff --git a/wallet/src/program_facades/amm.rs b/wallet/src/program_facades/amm.rs index 3beb92cb..7039a937 100644 --- a/wallet/src/program_facades/amm.rs +++ b/wallet/src/program_facades/amm.rs @@ -1,8 +1,9 @@ use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse}; use nssa::{AccountId, ProgramId, program::Program}; use nssa_core::program::PdaSeed; +use token_core::TokenHolding; -use crate::{TokenHolding, WalletCore}; +use crate::WalletCore; fn compute_pool_pda( amm_program_id: ProgramId, @@ -123,12 +124,12 @@ impl Amm<'_> { .await .map_err(|_| ExecutionFailureKind::SequencerError)?; - let definition_token_a_id = TokenHolding::parse(&user_a_acc.data) - .ok_or(ExecutionFailureKind::AccountDataError(user_holding_a))? - .definition_id; - let definition_token_b_id = TokenHolding::parse(&user_b_acc.data) - .ok_or(ExecutionFailureKind::AccountDataError(user_holding_a))? - .definition_id; + let definition_token_a_id = TokenHolding::try_from(&user_a_acc.data) + .map_err(|_| ExecutionFailureKind::AccountDataError(user_holding_a))? + .definition_id(); + let definition_token_b_id = TokenHolding::try_from(&user_b_acc.data) + .map_err(|_| ExecutionFailureKind::AccountDataError(user_holding_b))? + .definition_id(); let amm_pool = compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id); @@ -208,12 +209,12 @@ impl Amm<'_> { .await .map_err(|_| ExecutionFailureKind::SequencerError)?; - let definition_token_a_id = TokenHolding::parse(&user_a_acc.data) - .ok_or(ExecutionFailureKind::AccountDataError(user_holding_a))? - .definition_id; - let definition_token_b_id = TokenHolding::parse(&user_b_acc.data) - .ok_or(ExecutionFailureKind::AccountDataError(user_holding_b))? - .definition_id; + let definition_token_a_id = TokenHolding::try_from(&user_a_acc.data) + .map_err(|_| ExecutionFailureKind::AccountDataError(user_holding_a))? + .definition_id(); + let definition_token_b_id = TokenHolding::try_from(&user_b_acc.data) + .map_err(|_| ExecutionFailureKind::AccountDataError(user_holding_b))? + .definition_id(); let amm_pool = compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id); @@ -242,14 +243,14 @@ impl Amm<'_> { .await .map_err(|_| ExecutionFailureKind::SequencerError)?; - let token_holder_a = TokenHolding::parse(&token_holder_acc_a.data) - .ok_or(ExecutionFailureKind::AccountDataError(user_holding_a))?; - let token_holder_b = TokenHolding::parse(&token_holder_acc_b.data) - .ok_or(ExecutionFailureKind::AccountDataError(user_holding_b))?; + let token_holder_a = TokenHolding::try_from(&token_holder_acc_a.data) + .map_err(|_| ExecutionFailureKind::AccountDataError(user_holding_a))?; + let token_holder_b = TokenHolding::try_from(&token_holder_acc_b.data) + .map_err(|_| ExecutionFailureKind::AccountDataError(user_holding_b))?; - if token_holder_a.definition_id == token_definition_id { + if token_holder_a.definition_id() == token_definition_id { account_id_auth = user_holding_a; - } else if token_holder_b.definition_id == token_definition_id { + } else if token_holder_b.definition_id() == token_definition_id { account_id_auth = user_holding_b; } else { return Err(ExecutionFailureKind::AccountDataError(token_definition_id)); @@ -309,12 +310,12 @@ impl Amm<'_> { .await .map_err(|_| ExecutionFailureKind::SequencerError)?; - let definition_token_a_id = TokenHolding::parse(&user_a_acc.data) - .ok_or(ExecutionFailureKind::AccountDataError(user_holding_a))? - .definition_id; - let definition_token_b_id = TokenHolding::parse(&user_b_acc.data) - .ok_or(ExecutionFailureKind::AccountDataError(user_holding_a))? - .definition_id; + let definition_token_a_id = TokenHolding::try_from(&user_a_acc.data) + .map_err(|_| ExecutionFailureKind::AccountDataError(user_holding_a))? + .definition_id(); + let definition_token_b_id = TokenHolding::try_from(&user_b_acc.data) + .map_err(|_| ExecutionFailureKind::AccountDataError(user_holding_b))? + .definition_id(); let amm_pool = compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id); @@ -395,12 +396,12 @@ impl Amm<'_> { .await .map_err(|_| ExecutionFailureKind::SequencerError)?; - let definition_token_a_id = TokenHolding::parse(&user_a_acc.data) - .ok_or(ExecutionFailureKind::AccountDataError(user_holding_a))? - .definition_id; - let definition_token_b_id = TokenHolding::parse(&user_b_acc.data) - .ok_or(ExecutionFailureKind::AccountDataError(user_holding_a))? - .definition_id; + let definition_token_a_id = TokenHolding::try_from(&user_a_acc.data) + .map_err(|_| ExecutionFailureKind::AccountDataError(user_holding_a))? + .definition_id(); + let definition_token_b_id = TokenHolding::try_from(&user_b_acc.data) + .map_err(|_| ExecutionFailureKind::AccountDataError(user_holding_b))? + .definition_id(); let amm_pool = compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id); diff --git a/wallet/src/program_facades/token.rs b/wallet/src/program_facades/token.rs index 0d3f79d7..4160e255 100644 --- a/wallet/src/program_facades/token.rs +++ b/wallet/src/program_facades/token.rs @@ -1,6 +1,7 @@ use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse}; use nssa::{AccountId, program::Program}; use nssa_core::{NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey}; +use token_core::Instruction; use crate::{PrivacyPreservingAccount, WalletCore}; @@ -11,15 +12,12 @@ impl Token<'_> { &self, definition_account_id: AccountId, supply_account_id: AccountId, - name: [u8; 6], + name: String, total_supply: u128, ) -> Result { let account_ids = vec![definition_account_id, supply_account_id]; let program_id = nssa::program::Program::token().id(); - // Instruction must be: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)] - let mut instruction = vec![0u8; 23]; - instruction[1..17].copy_from_slice(&total_supply.to_le_bytes()); - instruction[17..].copy_from_slice(&name); + let instruction = Instruction::NewFungibleDefinition { name, total_supply }; let message = nssa::public_transaction::Message::try_new( program_id, account_ids, @@ -39,10 +37,10 @@ impl Token<'_> { &self, definition_account_id: AccountId, supply_account_id: AccountId, - name: [u8; 6], + name: String, total_supply: u128, ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { - let instruction = token_program_preparation_definition(name, total_supply); + let instruction = Instruction::NewFungibleDefinition { name, total_supply }; let instruction_data = Program::serialize_instruction(instruction).expect("Instruction should serialize"); @@ -69,10 +67,10 @@ impl Token<'_> { &self, definition_account_id: AccountId, supply_account_id: AccountId, - name: [u8; 6], + name: String, total_supply: u128, ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { - let instruction = token_program_preparation_definition(name, total_supply); + let instruction = Instruction::NewFungibleDefinition { name, total_supply }; let instruction_data = Program::serialize_instruction(instruction).expect("Instruction should serialize"); @@ -99,10 +97,10 @@ impl Token<'_> { &self, definition_account_id: AccountId, supply_account_id: AccountId, - name: [u8; 6], + name: String, total_supply: u128, ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { - let instruction = token_program_preparation_definition(name, total_supply); + let instruction = Instruction::NewFungibleDefinition { name, total_supply }; let instruction_data = Program::serialize_instruction(instruction).expect("Instruction should serialize"); @@ -132,11 +130,9 @@ impl Token<'_> { ) -> Result { let account_ids = vec![sender_account_id, recipient_account_id]; let program_id = nssa::program::Program::token().id(); - // Instruction must be: [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || - // 0x00 || 0x00 || 0x00]. - let mut instruction = vec![0u8; 23]; - instruction[0] = 0x01; - instruction[1..17].copy_from_slice(&amount.to_le_bytes()); + let instruction = Instruction::Transfer { + amount_to_transfer: amount, + }; let Ok(nonces) = self.0.get_accounts_nonces(vec![sender_account_id]).await else { return Err(ExecutionFailureKind::SequencerError); }; @@ -170,7 +166,9 @@ impl Token<'_> { recipient_account_id: AccountId, amount: u128, ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { - let instruction = token_program_preparation_transfer(amount); + let instruction = Instruction::Transfer { + amount_to_transfer: amount, + }; let instruction_data = Program::serialize_instruction(instruction).expect("Instruction should serialize"); @@ -199,7 +197,9 @@ impl Token<'_> { recipient_ipk: IncomingViewingPublicKey, amount: u128, ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { - let instruction = token_program_preparation_transfer(amount); + let instruction = Instruction::Transfer { + amount_to_transfer: amount, + }; let instruction_data = Program::serialize_instruction(instruction).expect("Instruction should serialize"); @@ -230,7 +230,9 @@ impl Token<'_> { recipient_account_id: AccountId, amount: u128, ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { - let instruction = token_program_preparation_transfer(amount); + let instruction = Instruction::Transfer { + amount_to_transfer: amount, + }; let instruction_data = Program::serialize_instruction(instruction).expect("Instruction should serialize"); @@ -259,7 +261,9 @@ impl Token<'_> { recipient_account_id: AccountId, amount: u128, ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { - let instruction = token_program_preparation_transfer(amount); + let instruction = Instruction::Transfer { + amount_to_transfer: amount, + }; let instruction_data = Program::serialize_instruction(instruction).expect("Instruction should serialize"); @@ -289,7 +293,9 @@ impl Token<'_> { recipient_ipk: IncomingViewingPublicKey, amount: u128, ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { - let instruction = token_program_preparation_transfer(amount); + let instruction = Instruction::Transfer { + amount_to_transfer: amount, + }; let instruction_data = Program::serialize_instruction(instruction).expect("Instruction should serialize"); @@ -322,7 +328,9 @@ impl Token<'_> { amount: u128, ) -> Result { let account_ids = vec![definition_account_id, holder_account_id]; - let instruction = token_program_preparation_burn(amount); + let instruction = Instruction::Burn { + amount_to_burn: amount, + }; let Ok(nonces) = self.0.get_accounts_nonces(vec![holder_account_id]).await else { return Err(ExecutionFailureKind::SequencerError); @@ -355,7 +363,9 @@ impl Token<'_> { holder_account_id: AccountId, amount: u128, ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { - let instruction = token_program_preparation_burn(amount); + let instruction = Instruction::Burn { + amount_to_burn: amount, + }; let instruction_data = Program::serialize_instruction(instruction).expect("Instruction should serialize"); @@ -383,7 +393,9 @@ impl Token<'_> { holder_account_id: AccountId, amount: u128, ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { - let instruction = token_program_preparation_burn(amount); + let instruction = Instruction::Burn { + amount_to_burn: amount, + }; let instruction_data = Program::serialize_instruction(instruction).expect("Instruction should serialize"); @@ -412,7 +424,9 @@ impl Token<'_> { holder_account_id: AccountId, amount: u128, ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { - let instruction = token_program_preparation_burn(amount); + let instruction = Instruction::Burn { + amount_to_burn: amount, + }; let instruction_data = Program::serialize_instruction(instruction).expect("Instruction should serialize"); @@ -442,7 +456,9 @@ impl Token<'_> { amount: u128, ) -> Result { let account_ids = vec![definition_account_id, holder_account_id]; - let instruction = token_program_preparation_mint(amount); + let instruction = Instruction::Mint { + amount_to_mint: amount, + }; let Ok(nonces) = self .0 @@ -481,7 +497,9 @@ impl Token<'_> { holder_account_id: AccountId, amount: u128, ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { - let instruction = token_program_preparation_mint(amount); + let instruction = Instruction::Mint { + amount_to_mint: amount, + }; let instruction_data = Program::serialize_instruction(instruction).expect("Instruction should serialize"); @@ -510,7 +528,9 @@ impl Token<'_> { holder_ipk: IncomingViewingPublicKey, amount: u128, ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { - let instruction = token_program_preparation_mint(amount); + let instruction = Instruction::Mint { + amount_to_mint: amount, + }; let instruction_data = Program::serialize_instruction(instruction).expect("Instruction should serialize"); @@ -541,7 +561,9 @@ impl Token<'_> { holder_account_id: AccountId, amount: u128, ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { - let instruction = token_program_preparation_mint(amount); + let instruction = Instruction::Mint { + amount_to_mint: amount, + }; let instruction_data = Program::serialize_instruction(instruction).expect("Instruction should serialize"); @@ -570,7 +592,9 @@ impl Token<'_> { holder_account_id: AccountId, amount: u128, ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { - let instruction = token_program_preparation_mint(amount); + let instruction = Instruction::Mint { + amount_to_mint: amount, + }; let instruction_data = Program::serialize_instruction(instruction).expect("Instruction should serialize"); @@ -600,7 +624,9 @@ impl Token<'_> { holder_ipk: IncomingViewingPublicKey, amount: u128, ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { - let instruction = token_program_preparation_mint(amount); + let instruction = Instruction::Mint { + amount_to_mint: amount, + }; let instruction_data = Program::serialize_instruction(instruction).expect("Instruction should serialize"); @@ -626,42 +652,3 @@ impl Token<'_> { }) } } - -fn token_program_preparation_transfer(amount: u128) -> Vec { - // Instruction must be: [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || - // 0x00 || 0x00 || 0x00]. - let mut instruction = vec![0u8; 23]; - instruction[0] = 0x01; - instruction[1..17].copy_from_slice(&amount.to_le_bytes()); - - instruction -} - -fn token_program_preparation_definition(name: [u8; 6], total_supply: u128) -> Vec { - // Instruction must be: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)] - let mut instruction = vec![0u8; 23]; - instruction[1..17].copy_from_slice(&total_supply.to_le_bytes()); - instruction[17..].copy_from_slice(&name); - - instruction -} - -fn token_program_preparation_burn(amount: u128) -> Vec { - // Instruction must be: [0x03 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || - // 0x00 || 0x00 || 0x00]. - let mut instruction = vec![0; 23]; - instruction[0] = 0x03; - instruction[1..17].copy_from_slice(&amount.to_le_bytes()); - - instruction -} - -fn token_program_preparation_mint(amount: u128) -> Vec { - // Instruction must be: [0x04 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || - // 0x00 || 0x00 || 0x00]. - let mut instruction = vec![0; 23]; - instruction[0] = 0x04; - instruction[1..17].copy_from_slice(&amount.to_le_bytes()); - - instruction -}