Skip to content

Feature/api server token nonces #1906

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api-server/api-server-common/src/storage/impls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

pub const CURRENT_STORAGE_VERSION: u32 = 20;
pub const CURRENT_STORAGE_VERSION: u32 = 21;

pub mod in_memory;
pub mod postgres;
22 changes: 15 additions & 7 deletions api-server/api-server-common/src/storage/storage_api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ pub struct FungibleTokenData {
pub is_locked: bool,
pub frozen: IsTokenFrozen,
pub authority: Destination,
pub next_nonce: AccountNonce,
}

impl FungibleTokenData {
Expand All @@ -481,38 +482,45 @@ impl FungibleTokenData {
}
}

pub fn mint_tokens(mut self, amount: Amount) -> Self {
pub fn mint_tokens(mut self, amount: Amount, nonce: AccountNonce) -> Self {
self.circulating_supply = (self.circulating_supply + amount).expect("no overflow");
self.next_nonce = nonce.increment().expect("no overflow");
self
}

pub fn unmint_tokens(mut self, amount: Amount) -> Self {
pub fn unmint_tokens(mut self, amount: Amount, nonce: AccountNonce) -> Self {
self.circulating_supply = (self.circulating_supply - amount).expect("no underflow");
self.next_nonce = nonce.increment().expect("no overflow");
self
}

pub fn freeze(mut self, is_token_unfreezable: IsTokenUnfreezable) -> Self {
pub fn freeze(mut self, is_token_unfreezable: IsTokenUnfreezable, nonce: AccountNonce) -> Self {
self.frozen = IsTokenFrozen::Yes(is_token_unfreezable);
self.next_nonce = nonce.increment().expect("no overflow");
self
}

pub fn unfreeze(mut self) -> Self {
pub fn unfreeze(mut self, nonce: AccountNonce) -> Self {
self.frozen = IsTokenFrozen::No(IsTokenFreezable::Yes);
self.next_nonce = nonce.increment().expect("no overflow");
self
}

pub fn lock(mut self) -> Self {
pub fn lock(mut self, nonce: AccountNonce) -> Self {
self.is_locked = true;
self.next_nonce = nonce.increment().expect("no overflow");
self
}

pub fn change_authority(mut self, authority: Destination) -> Self {
pub fn change_authority(mut self, authority: Destination, nonce: AccountNonce) -> Self {
self.authority = authority;
self.next_nonce = nonce.increment().expect("no overflow");
self
}

pub fn change_metadata_uri(mut self, metadata_uri: Vec<u8>) -> Self {
pub fn change_metadata_uri(mut self, metadata_uri: Vec<u8>, nonce: AccountNonce) -> Self {
self.metadata_uri = metadata_uri;
self.next_nonce = nonce.increment().expect("no overflow");
self
}
}
Expand Down
17 changes: 9 additions & 8 deletions api-server/scanner-lib/src/blockchain_state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1032,12 +1032,12 @@ async fn update_tables_from_transaction_inputs<T: ApiServerStorageWrite>(

for input in inputs {
match input {
TxInput::AccountCommand(_, cmd) => match cmd {
TxInput::AccountCommand(nonce, cmd) => match cmd {
AccountCommand::MintTokens(token_id, amount) => {
let issuance =
db_tx.get_fungible_token_issuance(*token_id).await?.expect("must exist");

let issuance = issuance.mint_tokens(*amount);
let issuance = issuance.mint_tokens(*amount, *nonce);
db_tx.set_fungible_token_data(*token_id, block_height, issuance).await?;
increase_statistic_amount(
db_tx,
Expand Down Expand Up @@ -1072,7 +1072,7 @@ async fn update_tables_from_transaction_inputs<T: ApiServerStorageWrite>(
let issuance =
db_tx.get_fungible_token_issuance(*token_id).await?.expect("must exist");

let issuance = issuance.unmint_tokens(total_burned);
let issuance = issuance.unmint_tokens(total_burned, *nonce);
db_tx.set_fungible_token_data(*token_id, block_height, issuance).await?;
let amount = chain_config.token_supply_change_fee(block_height);
increase_statistic_amount(
Expand All @@ -1096,7 +1096,7 @@ async fn update_tables_from_transaction_inputs<T: ApiServerStorageWrite>(
let issuance =
db_tx.get_fungible_token_issuance(*token_id).await?.expect("must exist");

let issuance = issuance.freeze(*is_unfreezable);
let issuance = issuance.freeze(*is_unfreezable, *nonce);
db_tx.set_fungible_token_data(*token_id, block_height, issuance).await?;
let amount = chain_config.token_freeze_fee(block_height);
increase_statistic_amount(
Expand All @@ -1120,7 +1120,7 @@ async fn update_tables_from_transaction_inputs<T: ApiServerStorageWrite>(
let issuance =
db_tx.get_fungible_token_issuance(*token_id).await?.expect("must exist");

let issuance = issuance.unfreeze();
let issuance = issuance.unfreeze(*nonce);
db_tx.set_fungible_token_data(*token_id, block_height, issuance).await?;
let amount = chain_config.token_freeze_fee(block_height);
increase_statistic_amount(
Expand All @@ -1144,7 +1144,7 @@ async fn update_tables_from_transaction_inputs<T: ApiServerStorageWrite>(
let issuance =
db_tx.get_fungible_token_issuance(*token_id).await?.expect("must exist");

let issuance = issuance.lock();
let issuance = issuance.lock(*nonce);
db_tx.set_fungible_token_data(*token_id, block_height, issuance).await?;
let amount = chain_config.token_supply_change_fee(block_height);
increase_statistic_amount(
Expand All @@ -1168,7 +1168,7 @@ async fn update_tables_from_transaction_inputs<T: ApiServerStorageWrite>(
let issuance =
db_tx.get_fungible_token_issuance(*token_id).await?.expect("must exist");

let issuance = issuance.change_authority(destination.clone());
let issuance = issuance.change_authority(destination.clone(), *nonce);
db_tx.set_fungible_token_data(*token_id, block_height, issuance).await?;
let amount = chain_config.token_change_authority_fee(block_height);
increase_statistic_amount(
Expand All @@ -1192,7 +1192,7 @@ async fn update_tables_from_transaction_inputs<T: ApiServerStorageWrite>(
let issuance =
db_tx.get_fungible_token_issuance(*token_id).await?.expect("must exist");

let issuance = issuance.change_metadata_uri(metadata_uri.clone());
let issuance = issuance.change_metadata_uri(metadata_uri.clone(), *nonce);
db_tx.set_fungible_token_data(*token_id, block_height, issuance).await?;
let amount = chain_config.token_change_metadata_uri_fee();
increase_statistic_amount(
Expand Down Expand Up @@ -1549,6 +1549,7 @@ async fn update_tables_from_transaction_outputs<T: ApiServerStorageWrite>(
is_locked: false,
frozen: IsTokenFrozen::No(issuance.is_freezable),
authority: issuance.authority.clone(),
next_nonce: AccountNonce::new(0),
},
};
db_tx.set_fungible_token_issuance(token_id, block_height, issuance).await?;
Expand Down
3 changes: 3 additions & 0 deletions api-server/stack-test-suite/tests/v2/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use common::{
IsTokenFreezable, IsTokenFrozen, TokenId, TokenIssuance, TokenIssuanceV1,
TokenTotalSupply,
},
AccountNonce,
},
primitives::H256,
};
Expand Down Expand Up @@ -136,6 +137,7 @@ async fn ok(#[case] seed: Seed) {
is_locked: false,
frozen: IsTokenFrozen::No(token_issuance.is_freezable),
authority: token_issuance.authority.clone(),
next_nonce: AccountNonce::new(0),
};

_ = tx.send([(
Expand All @@ -153,6 +155,7 @@ async fn ok(#[case] seed: Seed) {
"frozen": false,
"is_token_freezable": false,
"is_token_unfreezable": None::<bool>,
"next_nonce": token_data.next_nonce,
}),
)]);

Expand Down
187 changes: 185 additions & 2 deletions api-server/stack-test-suite/tests/v2/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ async fn multiple_tx_in_same_block(#[case] seed: Seed) {
"is_replaceable": transaction.is_replaceable(),
"flags": transaction.flags(),
"inputs": transaction.inputs().iter().zip(utxos).map(|(inp, utxo)| json!({
"input": tx_input_to_json(inp, &chain_config),
"input": tx_input_to_json(inp, &TokenDecimals::Single(None), &chain_config),
"utxo": utxo.as_ref().map(|txo| txoutput_to_json(txo, &chain_config, &TokenDecimals::Single(None))),
})).collect::<Vec<_>>(),
"outputs": transaction.outputs()
Expand Down Expand Up @@ -342,7 +342,7 @@ async fn ok(#[case] seed: Seed) {
"is_replaceable": transaction.is_replaceable(),
"flags": transaction.flags(),
"inputs": transaction.inputs().iter().zip(utxos).map(|(inp, utxo)| json!({
"input": tx_input_to_json(inp, &chain_config),
"input": tx_input_to_json(inp, &TokenDecimals::Single(None), &chain_config),
"utxo": utxo.as_ref().map(|txo| txoutput_to_json(txo, &chain_config, &TokenDecimals::Single(None))),
})).collect::<Vec<_>>(),
"outputs": transaction.outputs()
Expand Down Expand Up @@ -434,3 +434,186 @@ async fn ok(#[case] seed: Seed) {

task.abort();
}

#[rstest]
#[trace]
#[case(Seed::from_entropy())]
#[tokio::test]
async fn mint_tokens(#[case] seed: Seed) {
use chainstate_test_framework::empty_witness;
use common::chain::{
make_token_id,
tokens::{TokenIssuance, TokenTotalSupply},
AccountCommand, AccountNonce, UtxoOutPoint,
};

let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();

let (tx, rx) = tokio::sync::oneshot::channel();

let task = tokio::spawn(async move {
let web_server_state = {
let mut rng = make_seedable_rng(seed);
let chain_config = create_unit_test_config();

let chainstate_blocks = {
let mut tf = TestFramework::builder(&mut rng)
.with_chain_config(chain_config.clone())
.build();

let token_issuance_fee =
tf.chainstate.get_chain_config().fungible_token_issuance_fee();

let issuance = test_utils::nft_utils::random_token_issuance_v1(
tf.chain_config(),
Destination::AnyoneCanSpend,
&mut rng,
);
let amount_to_mint = match issuance.total_supply {
TokenTotalSupply::Fixed(limit) => {
Amount::from_atoms(rng.gen_range(1..=limit.into_atoms()))
}
TokenTotalSupply::Lockable | TokenTotalSupply::Unlimited => {
Amount::from_atoms(rng.gen_range(100..1000))
}
};
let mint_amount_decimal =
amount_to_mint.into_fixedpoint_str(issuance.number_of_decimals);

let genesis_outpoint = UtxoOutPoint::new(tf.best_block_id().into(), 0);
let genesis_coins = chainstate_test_framework::get_output_value(
tf.chainstate.utxo(&genesis_outpoint).unwrap().unwrap().output(),
)
.unwrap()
.coin_amount()
.unwrap();
let coins_after_issue = (genesis_coins - token_issuance_fee).unwrap();

// Issue token
let tx1 = TransactionBuilder::new()
.add_input(genesis_outpoint.into(), empty_witness(&mut rng))
.add_output(TxOutput::Transfer(
OutputValue::Coin(coins_after_issue),
Destination::AnyoneCanSpend,
))
.add_output(TxOutput::IssueFungibleToken(Box::new(TokenIssuance::V1(
issuance,
))))
.build();
let token_id = make_token_id(
&chain_config,
BlockHeight::new(1),
tx1.transaction().inputs(),
)
.unwrap();
let tx1_id = tx1.transaction().get_id();
let block1 = tf.make_block_builder().add_transaction(tx1).build(&mut rng);

tf.process_block(block1.clone(), chainstate::BlockSource::Local).unwrap();

// Mint tokens
let token_supply_change_fee =
tf.chainstate.get_chain_config().token_supply_change_fee(BlockHeight::zero());
let coins_after_mint = (coins_after_issue - token_supply_change_fee).unwrap();

let tx2 = TransactionBuilder::new()
.add_input(
TxInput::from_command(
AccountNonce::new(0),
AccountCommand::MintTokens(token_id, amount_to_mint),
),
empty_witness(&mut rng),
)
.add_input(
TxInput::from_utxo(tx1_id.into(), 0),
empty_witness(&mut rng),
)
.add_output(TxOutput::Transfer(
OutputValue::Coin(coins_after_mint),
Destination::AnyoneCanSpend,
))
.add_output(TxOutput::Transfer(
OutputValue::TokenV1(token_id, amount_to_mint),
Destination::AnyoneCanSpend,
))
.build();

let tx2_id = tx2.transaction().get_id();
let block2 = tf.make_block_builder().add_transaction(tx2).build(&mut rng);

tf.process_block(block2.clone(), chainstate::BlockSource::Local).unwrap();

_ = tx.send((
tx2_id.to_hash().encode_hex::<String>(),
mint_amount_decimal,
Address::new(&chain_config, token_id).expect("no error").into_string(),
));

vec![block1, block2]
};

let storage = {
let mut storage = TransactionalApiServerInMemoryStorage::new(&chain_config);

let mut db_tx = storage.transaction_rw().await.unwrap();
db_tx.reinitialize_storage(&chain_config).await.unwrap();
db_tx.commit().await.unwrap();

storage
};

let chain_config = Arc::new(chain_config);
let mut local_node = BlockchainState::new(Arc::clone(&chain_config), storage);
local_node.scan_genesis(chain_config.genesis_block()).await.unwrap();
local_node.scan_blocks(BlockHeight::new(0), chainstate_blocks).await.unwrap();

ApiServerWebServerState {
db: Arc::new(local_node.storage().clone_storage().await),
chain_config: Arc::clone(&chain_config),
rpc: Arc::new(DummyRPC {}),
cached_values: Arc::new(CachedValues {
feerate_points: RwLock::new((get_time(), vec![])),
}),
time_getter: Default::default(),
}
};

web_server(listener, web_server_state, true).await
});

let (transaction_id, mint_amount, token_id) = rx.await.unwrap();
let url = format!("/api/v2/transaction/{transaction_id}");

// Given that the listener port is open, this will block until a
// response is made (by the web server, which takes the listener
// over)
let response = reqwest::get(format!("http://{}:{}{url}", addr.ip(), addr.port()))
.await
.unwrap();

assert_eq!(response.status(), 200);

let body = response.text().await.unwrap();
let body: serde_json::Value = serde_json::from_str(&body).unwrap();
let body = body.as_object().unwrap();

let inputs = body.get("inputs").unwrap().as_array().unwrap();
assert_eq!(inputs.len(), 2);
let mint_inp = inputs.first().unwrap().as_object().unwrap().get("input").unwrap();
assert_eq!(
mint_inp.as_object().unwrap().get("command").unwrap().as_str().unwrap(),
"MintTokens"
);
assert_eq!(
mint_inp.as_object().unwrap().get("token_id").unwrap().as_str().unwrap(),
token_id,
);
let amount = mint_inp.as_object().unwrap().get("amount").unwrap().as_object().unwrap();
assert_eq!(
amount.get("decimal").unwrap().as_str().unwrap(),
mint_amount
);

task.abort();
}
Loading