Skip to content

Commit 5e8c20a

Browse files
authored
Merge pull request #1906 from mintlayer/feature/api-server-token-nonces
Feature/api server token nonces
2 parents e83324a + c46a7fa commit 5e8c20a

File tree

23 files changed

+315
-71
lines changed

23 files changed

+315
-71
lines changed

api-server/api-server-common/src/storage/impls/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
1515

16-
pub const CURRENT_STORAGE_VERSION: u32 = 20;
16+
pub const CURRENT_STORAGE_VERSION: u32 = 21;
1717

1818
pub mod in_memory;
1919
pub mod postgres;

api-server/api-server-common/src/storage/storage_api/mod.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,7 @@ pub struct FungibleTokenData {
464464
pub is_locked: bool,
465465
pub frozen: IsTokenFrozen,
466466
pub authority: Destination,
467+
pub next_nonce: AccountNonce,
467468
}
468469

469470
impl FungibleTokenData {
@@ -481,38 +482,45 @@ impl FungibleTokenData {
481482
}
482483
}
483484

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

489-
pub fn unmint_tokens(mut self, amount: Amount) -> Self {
491+
pub fn unmint_tokens(mut self, amount: Amount, nonce: AccountNonce) -> Self {
490492
self.circulating_supply = (self.circulating_supply - amount).expect("no underflow");
493+
self.next_nonce = nonce.increment().expect("no overflow");
491494
self
492495
}
493496

494-
pub fn freeze(mut self, is_token_unfreezable: IsTokenUnfreezable) -> Self {
497+
pub fn freeze(mut self, is_token_unfreezable: IsTokenUnfreezable, nonce: AccountNonce) -> Self {
495498
self.frozen = IsTokenFrozen::Yes(is_token_unfreezable);
499+
self.next_nonce = nonce.increment().expect("no overflow");
496500
self
497501
}
498502

499-
pub fn unfreeze(mut self) -> Self {
503+
pub fn unfreeze(mut self, nonce: AccountNonce) -> Self {
500504
self.frozen = IsTokenFrozen::No(IsTokenFreezable::Yes);
505+
self.next_nonce = nonce.increment().expect("no overflow");
501506
self
502507
}
503508

504-
pub fn lock(mut self) -> Self {
509+
pub fn lock(mut self, nonce: AccountNonce) -> Self {
505510
self.is_locked = true;
511+
self.next_nonce = nonce.increment().expect("no overflow");
506512
self
507513
}
508514

509-
pub fn change_authority(mut self, authority: Destination) -> Self {
515+
pub fn change_authority(mut self, authority: Destination, nonce: AccountNonce) -> Self {
510516
self.authority = authority;
517+
self.next_nonce = nonce.increment().expect("no overflow");
511518
self
512519
}
513520

514-
pub fn change_metadata_uri(mut self, metadata_uri: Vec<u8>) -> Self {
521+
pub fn change_metadata_uri(mut self, metadata_uri: Vec<u8>, nonce: AccountNonce) -> Self {
515522
self.metadata_uri = metadata_uri;
523+
self.next_nonce = nonce.increment().expect("no overflow");
516524
self
517525
}
518526
}

api-server/scanner-lib/src/blockchain_state/mod.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,12 +1032,12 @@ async fn update_tables_from_transaction_inputs<T: ApiServerStorageWrite>(
10321032

10331033
for input in inputs {
10341034
match input {
1035-
TxInput::AccountCommand(_, cmd) => match cmd {
1035+
TxInput::AccountCommand(nonce, cmd) => match cmd {
10361036
AccountCommand::MintTokens(token_id, amount) => {
10371037
let issuance =
10381038
db_tx.get_fungible_token_issuance(*token_id).await?.expect("must exist");
10391039

1040-
let issuance = issuance.mint_tokens(*amount);
1040+
let issuance = issuance.mint_tokens(*amount, *nonce);
10411041
db_tx.set_fungible_token_data(*token_id, block_height, issuance).await?;
10421042
increase_statistic_amount(
10431043
db_tx,
@@ -1072,7 +1072,7 @@ async fn update_tables_from_transaction_inputs<T: ApiServerStorageWrite>(
10721072
let issuance =
10731073
db_tx.get_fungible_token_issuance(*token_id).await?.expect("must exist");
10741074

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

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

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

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

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

1195-
let issuance = issuance.change_metadata_uri(metadata_uri.clone());
1195+
let issuance = issuance.change_metadata_uri(metadata_uri.clone(), *nonce);
11961196
db_tx.set_fungible_token_data(*token_id, block_height, issuance).await?;
11971197
let amount = chain_config.token_change_metadata_uri_fee();
11981198
increase_statistic_amount(
@@ -1549,6 +1549,7 @@ async fn update_tables_from_transaction_outputs<T: ApiServerStorageWrite>(
15491549
is_locked: false,
15501550
frozen: IsTokenFrozen::No(issuance.is_freezable),
15511551
authority: issuance.authority.clone(),
1552+
next_nonce: AccountNonce::new(0),
15521553
},
15531554
};
15541555
db_tx.set_fungible_token_issuance(token_id, block_height, issuance).await?;

api-server/stack-test-suite/tests/v2/token.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use common::{
2222
IsTokenFreezable, IsTokenFrozen, TokenId, TokenIssuance, TokenIssuanceV1,
2323
TokenTotalSupply,
2424
},
25+
AccountNonce,
2526
},
2627
primitives::H256,
2728
};
@@ -136,6 +137,7 @@ async fn ok(#[case] seed: Seed) {
136137
is_locked: false,
137138
frozen: IsTokenFrozen::No(token_issuance.is_freezable),
138139
authority: token_issuance.authority.clone(),
140+
next_nonce: AccountNonce::new(0),
139141
};
140142

141143
_ = tx.send([(
@@ -153,6 +155,7 @@ async fn ok(#[case] seed: Seed) {
153155
"frozen": false,
154156
"is_token_freezable": false,
155157
"is_token_unfreezable": None::<bool>,
158+
"next_nonce": token_data.next_nonce,
156159
}),
157160
)]);
158161

api-server/stack-test-suite/tests/v2/transaction.rs

Lines changed: 190 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ async fn multiple_tx_in_same_block(#[case] seed: Seed) {
196196
"is_replaceable": transaction.is_replaceable(),
197197
"flags": transaction.flags(),
198198
"inputs": transaction.inputs().iter().zip(utxos).map(|(inp, utxo)| json!({
199-
"input": tx_input_to_json(inp, &chain_config),
199+
"input": tx_input_to_json(inp, &TokenDecimals::Single(None), &chain_config),
200200
"utxo": utxo.as_ref().map(|txo| txoutput_to_json(txo, &chain_config, &TokenDecimals::Single(None))),
201201
})).collect::<Vec<_>>(),
202202
"outputs": transaction.outputs()
@@ -342,7 +342,7 @@ async fn ok(#[case] seed: Seed) {
342342
"is_replaceable": transaction.is_replaceable(),
343343
"flags": transaction.flags(),
344344
"inputs": transaction.inputs().iter().zip(utxos).map(|(inp, utxo)| json!({
345-
"input": tx_input_to_json(inp, &chain_config),
345+
"input": tx_input_to_json(inp, &TokenDecimals::Single(None), &chain_config),
346346
"utxo": utxo.as_ref().map(|txo| txoutput_to_json(txo, &chain_config, &TokenDecimals::Single(None))),
347347
})).collect::<Vec<_>>(),
348348
"outputs": transaction.outputs()
@@ -434,3 +434,191 @@ async fn ok(#[case] seed: Seed) {
434434

435435
task.abort();
436436
}
437+
438+
#[rstest]
439+
#[trace]
440+
#[case(Seed::from_entropy())]
441+
#[tokio::test]
442+
async fn mint_tokens(#[case] seed: Seed) {
443+
use chainstate_test_framework::empty_witness;
444+
use common::chain::{
445+
make_token_id,
446+
tokens::{TokenIssuance, TokenTotalSupply},
447+
AccountCommand, AccountNonce, UtxoOutPoint,
448+
};
449+
450+
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
451+
let addr = listener.local_addr().unwrap();
452+
453+
let (tx, rx) = tokio::sync::oneshot::channel();
454+
455+
let task = tokio::spawn(async move {
456+
let web_server_state = {
457+
let mut rng = make_seedable_rng(seed);
458+
let chain_config = create_unit_test_config();
459+
460+
let chainstate_blocks = {
461+
let mut tf = TestFramework::builder(&mut rng)
462+
.with_chain_config(chain_config.clone())
463+
.build();
464+
465+
let token_issuance_fee =
466+
tf.chainstate.get_chain_config().fungible_token_issuance_fee();
467+
468+
let issuance = test_utils::nft_utils::random_token_issuance_v1(
469+
tf.chain_config(),
470+
Destination::AnyoneCanSpend,
471+
&mut rng,
472+
);
473+
let amount_to_mint = match issuance.total_supply {
474+
TokenTotalSupply::Fixed(limit) => {
475+
Amount::from_atoms(rng.gen_range(1..=limit.into_atoms()))
476+
}
477+
TokenTotalSupply::Lockable | TokenTotalSupply::Unlimited => {
478+
Amount::from_atoms(rng.gen_range(100..1000))
479+
}
480+
};
481+
let mint_amount_decimal =
482+
amount_to_mint.into_fixedpoint_str(issuance.number_of_decimals);
483+
484+
let genesis_outpoint = UtxoOutPoint::new(tf.best_block_id().into(), 0);
485+
let genesis_coins = chainstate_test_framework::get_output_value(
486+
tf.chainstate.utxo(&genesis_outpoint).unwrap().unwrap().output(),
487+
)
488+
.unwrap()
489+
.coin_amount()
490+
.unwrap();
491+
let coins_after_issue = (genesis_coins - token_issuance_fee).unwrap();
492+
493+
// Issue token
494+
let tx1 = TransactionBuilder::new()
495+
.add_input(genesis_outpoint.into(), empty_witness(&mut rng))
496+
.add_output(TxOutput::Transfer(
497+
OutputValue::Coin(coins_after_issue),
498+
Destination::AnyoneCanSpend,
499+
))
500+
.add_output(TxOutput::IssueFungibleToken(Box::new(TokenIssuance::V1(
501+
issuance,
502+
))))
503+
.build();
504+
let token_id = make_token_id(
505+
&chain_config,
506+
BlockHeight::new(1),
507+
tx1.transaction().inputs(),
508+
)
509+
.unwrap();
510+
let tx1_id = tx1.transaction().get_id();
511+
let block1 = tf.make_block_builder().add_transaction(tx1).build(&mut rng);
512+
513+
tf.process_block(block1.clone(), chainstate::BlockSource::Local).unwrap();
514+
515+
// Mint tokens
516+
let token_supply_change_fee =
517+
tf.chainstate.get_chain_config().token_supply_change_fee(BlockHeight::zero());
518+
let coins_after_mint = (coins_after_issue - token_supply_change_fee).unwrap();
519+
520+
let tx2 = TransactionBuilder::new()
521+
.add_input(
522+
TxInput::from_command(
523+
AccountNonce::new(0),
524+
AccountCommand::MintTokens(token_id, amount_to_mint),
525+
),
526+
empty_witness(&mut rng),
527+
)
528+
.add_input(
529+
TxInput::from_utxo(tx1_id.into(), 0),
530+
empty_witness(&mut rng),
531+
)
532+
.add_output(TxOutput::Burn(OutputValue::TokenV1(
533+
token_id,
534+
amount_to_mint,
535+
)))
536+
.add_output(TxOutput::Transfer(
537+
OutputValue::Coin(coins_after_mint),
538+
Destination::AnyoneCanSpend,
539+
))
540+
.build();
541+
542+
let tx2_id = tx2.transaction().get_id();
543+
let block2 = tf.make_block_builder().add_transaction(tx2).build(&mut rng);
544+
545+
tf.process_block(block2.clone(), chainstate::BlockSource::Local).unwrap();
546+
547+
_ = tx.send((
548+
tx2_id.to_hash().encode_hex::<String>(),
549+
mint_amount_decimal,
550+
Address::new(&chain_config, token_id).expect("no error").into_string(),
551+
));
552+
553+
vec![block1, block2]
554+
};
555+
556+
let storage = {
557+
let mut storage = TransactionalApiServerInMemoryStorage::new(&chain_config);
558+
559+
let mut db_tx = storage.transaction_rw().await.unwrap();
560+
db_tx.reinitialize_storage(&chain_config).await.unwrap();
561+
db_tx.commit().await.unwrap();
562+
563+
storage
564+
};
565+
566+
let chain_config = Arc::new(chain_config);
567+
let mut local_node = BlockchainState::new(Arc::clone(&chain_config), storage);
568+
local_node.scan_genesis(chain_config.genesis_block()).await.unwrap();
569+
local_node.scan_blocks(BlockHeight::new(0), chainstate_blocks).await.unwrap();
570+
571+
ApiServerWebServerState {
572+
db: Arc::new(local_node.storage().clone_storage().await),
573+
chain_config: Arc::clone(&chain_config),
574+
rpc: Arc::new(DummyRPC {}),
575+
cached_values: Arc::new(CachedValues {
576+
feerate_points: RwLock::new((get_time(), vec![])),
577+
}),
578+
time_getter: Default::default(),
579+
}
580+
};
581+
582+
web_server(listener, web_server_state, true).await
583+
});
584+
585+
let (transaction_id, mint_amount, token_id) = rx.await.unwrap();
586+
let url = format!("/api/v2/transaction/{transaction_id}");
587+
588+
// Given that the listener port is open, this will block until a
589+
// response is made (by the web server, which takes the listener
590+
// over)
591+
let response = reqwest::get(format!("http://{}:{}{url}", addr.ip(), addr.port()))
592+
.await
593+
.unwrap();
594+
595+
assert_eq!(response.status(), 200);
596+
597+
let body = response.text().await.unwrap();
598+
let body: serde_json::Value = serde_json::from_str(&body).unwrap();
599+
let body = body.as_object().unwrap();
600+
601+
let inputs = body.get("inputs").unwrap().as_array().unwrap();
602+
assert_eq!(inputs.len(), 2);
603+
let mint_inp = inputs.first().unwrap().as_object().unwrap().get("input").unwrap();
604+
assert_eq!(
605+
mint_inp.as_object().unwrap().get("command").unwrap().as_str().unwrap(),
606+
"MintTokens"
607+
);
608+
assert_eq!(
609+
mint_inp.as_object().unwrap().get("token_id").unwrap().as_str().unwrap(),
610+
token_id,
611+
);
612+
let amount = mint_inp.as_object().unwrap().get("amount").unwrap().as_object().unwrap();
613+
assert_eq!(
614+
amount.get("decimal").unwrap().as_str().unwrap(),
615+
mint_amount
616+
);
617+
618+
let outputs = body.get("outputs").unwrap().as_array().unwrap();
619+
assert_eq!(outputs.len(), 2);
620+
let burn_out = outputs.first().unwrap().as_object().unwrap();
621+
assert_eq!(burn_out.get("type").unwrap().as_str().unwrap(), "Burn",);
622+
623+
task.abort();
624+
}

0 commit comments

Comments
 (0)