refactor: split token program into crates

This commit is contained in:
Daniil Polyakov 2026-01-16 04:11:33 +03:00
parent fb62143398
commit 652be426ae
48 changed files with 2650 additions and 3341 deletions

22
Cargo.lock generated
View File

@ -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",
]

View File

@ -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 = [

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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

View File

@ -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");

View File

@ -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"

View File

@ -20,8 +20,7 @@ pub struct ProgramInput<T> {
/// 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<AccountWithMetadata>,
/// The instruction data to pass
pub instruction_data: InstructionData,
pub pre_states: Vec<AccountWithMetadata>,
pub pda_seeds: Vec<PdaSeed>,
}
impl ChainedCall {
/// Creates a new chained call serializing the given instruction.
pub fn new<I: Serialize>(
program_id: ProgramId,
pre_states: Vec<AccountWithMetadata>,
instruction: &I,
) -> Self {
Self {
program_id,
pre_states,
instruction_data: risc0_zkvm::serde::to_vec(instruction)
.expect("Serialization to Vec<u32> should not fail"),
pda_seeds: Vec::new(),
}
}
pub fn with_pda_seeds(mut self, pda_seeds: Vec<PdaSeed>) -> 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,

View File

@ -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::<u8>::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],

View File

@ -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 }

File diff suppressed because it is too large Load Diff

View File

@ -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],
);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
[package]
name = "token_program"
version = "0.1.0"
edition = "2024"
[dependencies]
nssa_core.workspace = true
token_core.workspace = true

View File

@ -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

View File

@ -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<NewTokenMetadata>,
},
/// 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<AccountId>,
},
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<Self, Self::Error> {
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<Self, Self::Error> {
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<Self, Self::Error> {
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")
}
}

104
programs/token/src/burn.rs Normal file
View File

@ -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<AccountPostState> {
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),
]
}

View File

@ -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<AccountPostState> {
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),
]
}

12
programs/token/src/lib.rs Normal file
View File

@ -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;

View File

@ -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<AccountPostState> {
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),
]
}

View File

@ -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<AccountPostState> {
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<AccountPostState> {
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),
]
}

View File

@ -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<AccountPostState> {
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),
]
}

1040
programs/token/src/tests.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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<AccountPostState> {
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),
]
}

View File

@ -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

View File

@ -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<nssa::Account> 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<TokenDefinition> 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<TokenHolding> 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");
}
}

View File

@ -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?;

View File

@ -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<Self> {
let data = Vec::<u8>::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<Self> {
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,

View File

@ -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);

View File

@ -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<SendTxResponse, ExecutionFailureKind> {
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<SendTxResponse, ExecutionFailureKind> {
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<SendTxResponse, ExecutionFailureKind> {
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<SendTxResponse, ExecutionFailureKind> {
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<u8> {
// 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<u8> {
// 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<u8> {
// 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<u8> {
// 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
}