From 8619f189c2324e8f7902155ed72ddedbbd57d88c Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Tue, 24 Dec 2024 14:51:28 +0100 Subject: [PATCH] Add NFT owner to API Server response --- .../src/storage/impls/in_memory/mod.rs | 52 ++++++++--- .../impls/in_memory/transactional/read.rs | 11 ++- .../impls/in_memory/transactional/write.rs | 14 +-- .../src/storage/impls/postgres/queries.rs | 90 ++++++++++++++++--- .../impls/postgres/transactional/read.rs | 9 +- .../impls/postgres/transactional/write.rs | 14 +-- .../src/storage/storage_api/mod.rs | 14 ++- .../scanner-lib/src/blockchain_state/mod.rs | 22 ++--- .../scanner-lib/src/sync/tests/simulation.rs | 2 +- api-server/stack-test-suite/tests/v2/nft.rs | 13 ++- api-server/storage-test-suite/src/basic.rs | 61 +++++++++++-- api-server/web-server/src/api/json_helpers.rs | 19 +++- api-server/web-server/src/api/v2.rs | 4 +- 13 files changed, 252 insertions(+), 73 deletions(-) diff --git a/api-server/api-server-common/src/storage/impls/in_memory/mod.rs b/api-server/api-server-common/src/storage/impls/in_memory/mod.rs index 432cb0f2b1..9df5caa635 100644 --- a/api-server/api-server-common/src/storage/impls/in_memory/mod.rs +++ b/api-server/api-server-common/src/storage/impls/in_memory/mod.rs @@ -18,9 +18,11 @@ pub mod transactional; use crate::storage::storage_api::{ block_aux_data::{BlockAuxData, BlockWithExtraData}, ApiServerStorageError, BlockInfo, CoinOrTokenStatistic, Delegation, FungibleTokenData, - LockedUtxo, Order, PoolBlockStats, TransactionInfo, Utxo, UtxoLock, UtxoWithExtraInfo, + LockedUtxo, NftWithOwner, Order, PoolBlockStats, TransactionInfo, Utxo, UtxoLock, + UtxoWithExtraInfo, }; use common::{ + address::Address, chain::{ block::timestamp::BlockTimestamp, tokens::{NftIssuance, TokenId}, @@ -55,7 +57,7 @@ struct ApiServerInMemoryStorage { locked_utxo_table: BTreeMap>, address_locked_utxos: BTreeMap>, fungible_token_issuances: BTreeMap>, - nft_token_issuances: BTreeMap>, + nft_token_issuances: BTreeMap>, statistics: BTreeMap>>, orders_table: BTreeMap>, @@ -599,7 +601,7 @@ impl ApiServerInMemoryStorage { fn get_nft_token_issuance( &self, token_id: TokenId, - ) -> Result, ApiServerStorageError> { + ) -> Result, ApiServerStorageError> { Ok(self .nft_token_issuances .get(&token_id) @@ -641,7 +643,7 @@ impl ApiServerInMemoryStorage { (value.values().last().expect("not empty").token_ticker == ticker).then_some(key) }) .chain(self.nft_token_issuances.iter().filter_map(|(key, value)| { - let value_ticker = match &value.values().last().expect("not empty") { + let value_ticker = match &value.values().last().expect("not empty").nft { NftIssuance::V0(data) => data.metadata.ticker(), }; (value_ticker == ticker).then_some(key) @@ -803,7 +805,7 @@ impl ApiServerInMemoryStorage { fn set_address_balance_at_height( &mut self, - address: &str, + address: &Address, amount: Amount, coin_or_token_id: CoinOrTokenId, block_height: BlockHeight, @@ -815,12 +817,36 @@ impl ApiServerInMemoryStorage { .and_modify(|e| *e = amount) .or_insert(amount); + self.update_nft_owner(coin_or_token_id, amount, address, block_height); + Ok(()) } + fn update_nft_owner( + &mut self, + coin_or_token_id: CoinOrTokenId, + amount: Amount, + address: &Address, + block_height: BlockHeight, + ) { + let CoinOrTokenId::TokenId(token_id) = coin_or_token_id else { + return; + }; + + if let Some(by_height) = self.nft_token_issuances.get_mut(&token_id) { + let last = by_height.values().last().expect("not empty"); + let owner = (amount > Amount::ZERO).then_some(address.as_object().clone()); + let new = NftWithOwner { + nft: last.nft.clone(), + owner, + }; + by_height.insert(block_height, new); + }; + } + fn set_address_locked_balance_at_height( &mut self, - address: &str, + address: &Address, amount: Amount, coin_or_token_id: CoinOrTokenId, block_height: BlockHeight, @@ -832,6 +858,8 @@ impl ApiServerInMemoryStorage { .and_modify(|e| *e = amount) .or_insert(amount); + self.update_nft_owner(coin_or_token_id, amount, address, block_height); + Ok(()) } @@ -1060,11 +1088,15 @@ impl ApiServerInMemoryStorage { token_id: TokenId, block_height: BlockHeight, issuance: NftIssuance, + owner: &Destination, ) -> Result<(), ApiServerStorageError> { - self.nft_token_issuances - .entry(token_id) - .or_default() - .insert(block_height, issuance); + self.nft_token_issuances.entry(token_id).or_default().insert( + block_height, + NftWithOwner { + nft: issuance, + owner: Some(owner.clone()), + }, + ); Ok(()) } diff --git a/api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs b/api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs index 314b3752d2..e65b85b340 100644 --- a/api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs +++ b/api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs @@ -17,9 +17,8 @@ use std::collections::BTreeMap; use common::{ chain::{ - block::timestamp::BlockTimestamp, - tokens::{NftIssuance, TokenId}, - Block, DelegationId, Destination, OrderId, PoolId, Transaction, UtxoOutPoint, + block::timestamp::BlockTimestamp, tokens::TokenId, Block, DelegationId, Destination, + OrderId, PoolId, Transaction, UtxoOutPoint, }, primitives::{Amount, BlockHeight, CoinOrTokenId, Id}, }; @@ -27,8 +26,8 @@ use pos_accounting::PoolData; use crate::storage::storage_api::{ block_aux_data::BlockAuxData, ApiServerStorageError, ApiServerStorageRead, BlockInfo, - CoinOrTokenStatistic, Delegation, FungibleTokenData, Order, PoolBlockStats, TransactionInfo, - Utxo, UtxoWithExtraInfo, + CoinOrTokenStatistic, Delegation, FungibleTokenData, NftWithOwner, Order, PoolBlockStats, + TransactionInfo, Utxo, UtxoWithExtraInfo, }; use super::ApiServerInMemoryStorageTransactionalRo; @@ -217,7 +216,7 @@ impl ApiServerStorageRead for ApiServerInMemoryStorageTransactionalRo<'_> { async fn get_nft_token_issuance( &self, token_id: TokenId, - ) -> Result, ApiServerStorageError> { + ) -> Result, ApiServerStorageError> { self.transaction.get_nft_token_issuance(token_id) } diff --git a/api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs b/api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs index 2876ff87b3..b4e601aae4 100644 --- a/api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs +++ b/api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs @@ -16,6 +16,7 @@ use std::collections::{BTreeMap, BTreeSet}; use common::{ + address::Address, chain::{ block::timestamp::BlockTimestamp, tokens::{NftIssuance, TokenId}, @@ -28,8 +29,8 @@ use pos_accounting::PoolData; use crate::storage::storage_api::{ block_aux_data::{BlockAuxData, BlockWithExtraData}, ApiServerStorageError, ApiServerStorageRead, ApiServerStorageWrite, BlockInfo, - CoinOrTokenStatistic, Delegation, FungibleTokenData, LockedUtxo, Order, PoolBlockStats, - TransactionInfo, Utxo, UtxoWithExtraInfo, + CoinOrTokenStatistic, Delegation, FungibleTokenData, LockedUtxo, NftWithOwner, Order, + PoolBlockStats, TransactionInfo, Utxo, UtxoWithExtraInfo, }; use super::ApiServerInMemoryStorageTransactionalRw; @@ -66,7 +67,7 @@ impl ApiServerStorageWrite for ApiServerInMemoryStorageTransactionalRw<'_> { async fn set_address_balance_at_height( &mut self, - address: &str, + address: &Address, amount: Amount, coin_or_token_id: CoinOrTokenId, block_height: BlockHeight, @@ -81,7 +82,7 @@ impl ApiServerStorageWrite for ApiServerInMemoryStorageTransactionalRw<'_> { async fn set_address_locked_balance_at_height( &mut self, - address: &str, + address: &Address, amount: Amount, coin_or_token_id: CoinOrTokenId, block_height: BlockHeight, @@ -219,8 +220,9 @@ impl ApiServerStorageWrite for ApiServerInMemoryStorageTransactionalRw<'_> { token_id: TokenId, block_height: BlockHeight, issuance: NftIssuance, + owner: &Destination, ) -> Result<(), ApiServerStorageError> { - self.transaction.set_nft_token_issuance(token_id, block_height, issuance) + self.transaction.set_nft_token_issuance(token_id, block_height, issuance, owner) } async fn del_token_issuance_above_height( @@ -456,7 +458,7 @@ impl ApiServerStorageRead for ApiServerInMemoryStorageTransactionalRw<'_> { async fn get_nft_token_issuance( &self, token_id: TokenId, - ) -> Result, ApiServerStorageError> { + ) -> Result, ApiServerStorageError> { self.transaction.get_nft_token_issuance(token_id) } diff --git a/api-server/api-server-common/src/storage/impls/postgres/queries.rs b/api-server/api-server-common/src/storage/impls/postgres/queries.rs index e220f55061..d19b675671 100644 --- a/api-server/api-server-common/src/storage/impls/postgres/queries.rs +++ b/api-server/api-server-common/src/storage/impls/postgres/queries.rs @@ -39,7 +39,7 @@ use crate::storage::{ storage_api::{ block_aux_data::{BlockAuxData, BlockWithExtraData}, ApiServerStorageError, BlockInfo, CoinOrTokenStatistic, Delegation, FungibleTokenData, - LockedUtxo, Order, PoolBlockStats, TransactionInfo, Utxo, UtxoWithExtraInfo, + LockedUtxo, NftWithOwner, Order, PoolBlockStats, TransactionInfo, Utxo, UtxoWithExtraInfo, }, }; @@ -240,7 +240,7 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { pub async fn set_address_balance_at_height( &mut self, - address: &str, + address: &Address, amount: Amount, coin_or_token_id: CoinOrTokenId, block_height: BlockHeight, @@ -260,12 +260,62 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { .await .map_err(|e| ApiServerStorageError::LowLevelStorageError(e.to_string()))?; + let CoinOrTokenId::TokenId(token_id) = coin_or_token_id else { + return Ok(()); + }; + + self.update_nft_owner(token_id, height, (amount > Amount::ZERO).then_some(address)) + .await?; + + Ok(()) + } + + async fn update_nft_owner( + &mut self, + token_id: TokenId, + height: i64, + owner: Option<&Address>, + ) -> Result<(), ApiServerStorageError> { + self.tx + .execute( + r#" + WITH LastRow AS ( + SELECT + nft_id, + block_height, + ticker, + issuance + FROM + ml.nft_issuance + WHERE + nft_id = $1 + ORDER BY + block_height DESC + LIMIT 1 + ) + INSERT INTO ml.nft_issuance (nft_id, block_height, ticker, issuance, owner) + SELECT + lr.nft_id, + $2, + lr.ticker, + lr.issuance, + $3 + FROM + LastRow lr + ON CONFLICT (nft_id, block_height) DO UPDATE + SET + ticker = EXCLUDED.ticker, + owner = EXCLUDED.owner;"#, + &[&token_id.encode(), &height, &owner.map(|o| o.as_object().encode())], + ) + .await + .map_err(|e| ApiServerStorageError::LowLevelStorageError(e.to_string()))?; Ok(()) } pub async fn set_address_locked_balance_at_height( &mut self, - address: &str, + address: &Address, amount: Amount, coin_or_token_id: CoinOrTokenId, block_height: BlockHeight, @@ -285,6 +335,13 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { .await .map_err(|e| ApiServerStorageError::LowLevelStorageError(e.to_string()))?; + let CoinOrTokenId::TokenId(token_id) = coin_or_token_id else { + return Ok(()); + }; + + self.update_nft_owner(token_id, height, (amount > Amount::ZERO).then_some(address)) + .await?; + Ok(()) } @@ -631,7 +688,8 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { block_height bigint NOT NULL, ticker bytea NOT NULL, issuance bytea NOT NULL, - PRIMARY KEY (nft_id) + owner bytea, + PRIMARY KEY (nft_id, block_height) );", ) .await?; @@ -2030,11 +2088,11 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { pub async fn get_nft_token_issuance( &self, token_id: TokenId, - ) -> Result, ApiServerStorageError> { + ) -> Result, ApiServerStorageError> { let row = self .tx .query_opt( - "SELECT issuance FROM ml.nft_issuance WHERE nft_id = $1 + "SELECT issuance, owner FROM ml.nft_issuance WHERE nft_id = $1 ORDER BY block_height DESC LIMIT 1;", &[&token_id.encode()], @@ -2048,15 +2106,26 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { }; let serialized_data: Vec = row.get(0); + let owner: Option> = row.get(1); - let issuance = NftIssuance::decode_all(&mut serialized_data.as_slice()).map_err(|e| { + let nft = NftIssuance::decode_all(&mut serialized_data.as_slice()).map_err(|e| { ApiServerStorageError::DeserializationError(format!( "Nft issuance data for nft id {} deserialization failed: {}", token_id, e )) })?; - Ok(Some(issuance)) + let owner = owner + .map(|owner| { + Destination::decode_all(&mut owner.as_slice()).map_err(|e| { + ApiServerStorageError::DeserializationError(format!( + "Deserialization failed for nft owner {token_id}: {e}" + )) + }) + }) + .transpose()?; + + Ok(Some(NftWithOwner { nft, owner })) } pub async fn set_nft_token_issuance( @@ -2064,6 +2133,7 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { token_id: TokenId, block_height: BlockHeight, issuance: NftIssuance, + owner: &Destination, ) -> Result<(), ApiServerStorageError> { let height = Self::block_height_to_postgres_friendly(block_height); @@ -2073,8 +2143,8 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { self.tx .execute( - "INSERT INTO ml.nft_issuance (nft_id, block_height, issuance, ticker) VALUES ($1, $2, $3, $4);", - &[&token_id.encode(), &height, &issuance.encode(), ticker], + "INSERT INTO ml.nft_issuance (nft_id, block_height, issuance, ticker, owner) VALUES ($1, $2, $3, $4, $5);", + &[&token_id.encode(), &height, &issuance.encode(), ticker, &owner.encode()], ) .await .map_err(|e| ApiServerStorageError::LowLevelStorageError(e.to_string()))?; diff --git a/api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs b/api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs index 88a7400714..f0a281a84a 100644 --- a/api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs +++ b/api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs @@ -15,9 +15,8 @@ use common::{ chain::{ - block::timestamp::BlockTimestamp, - tokens::{NftIssuance, TokenId}, - DelegationId, Destination, OrderId, PoolId, + block::timestamp::BlockTimestamp, tokens::TokenId, DelegationId, Destination, OrderId, + PoolId, }, primitives::{Amount, BlockHeight, CoinOrTokenId, Id}, }; @@ -26,7 +25,7 @@ use crate::storage::{ impls::postgres::queries::QueryFromConnection, storage_api::{ block_aux_data::BlockAuxData, ApiServerStorageError, ApiServerStorageRead, BlockInfo, - CoinOrTokenStatistic, Delegation, FungibleTokenData, Order, PoolBlockStats, + CoinOrTokenStatistic, Delegation, FungibleTokenData, NftWithOwner, Order, PoolBlockStats, TransactionInfo, Utxo, UtxoWithExtraInfo, }, }; @@ -304,7 +303,7 @@ impl ApiServerStorageRead for ApiServerPostgresTransactionalRo<'_> { async fn get_nft_token_issuance( &self, token_id: TokenId, - ) -> Result, ApiServerStorageError> { + ) -> Result, ApiServerStorageError> { let conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR)); let res = conn.get_nft_token_issuance(token_id).await?; diff --git a/api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs b/api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs index bb0a2b8dc8..bf5d7145f9 100644 --- a/api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs +++ b/api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs @@ -16,6 +16,7 @@ use std::collections::{BTreeMap, BTreeSet}; use common::{ + address::Address, chain::{ block::timestamp::BlockTimestamp, tokens::{NftIssuance, TokenId}, @@ -30,8 +31,8 @@ use crate::storage::{ storage_api::{ block_aux_data::{BlockAuxData, BlockWithExtraData}, ApiServerStorageError, ApiServerStorageRead, ApiServerStorageWrite, BlockInfo, - CoinOrTokenStatistic, Delegation, FungibleTokenData, LockedUtxo, Order, PoolBlockStats, - TransactionInfo, Utxo, UtxoWithExtraInfo, + CoinOrTokenStatistic, Delegation, FungibleTokenData, LockedUtxo, NftWithOwner, Order, + PoolBlockStats, TransactionInfo, Utxo, UtxoWithExtraInfo, }, }; @@ -81,7 +82,7 @@ impl ApiServerStorageWrite for ApiServerPostgresTransactionalRw<'_> { async fn set_address_balance_at_height( &mut self, - address: &str, + address: &Address, amount: Amount, coin_or_token_id: CoinOrTokenId, block_height: BlockHeight, @@ -95,7 +96,7 @@ impl ApiServerStorageWrite for ApiServerPostgresTransactionalRw<'_> { async fn set_address_locked_balance_at_height( &mut self, - address: &str, + address: &Address, amount: Amount, coin_or_token_id: CoinOrTokenId, block_height: BlockHeight, @@ -274,9 +275,10 @@ impl ApiServerStorageWrite for ApiServerPostgresTransactionalRw<'_> { token_id: TokenId, block_height: BlockHeight, issuance: NftIssuance, + owner: &Destination, ) -> Result<(), ApiServerStorageError> { let mut conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR)); - conn.set_nft_token_issuance(token_id, block_height, issuance).await?; + conn.set_nft_token_issuance(token_id, block_height, issuance, owner).await?; Ok(()) } @@ -612,7 +614,7 @@ impl ApiServerStorageRead for ApiServerPostgresTransactionalRw<'_> { async fn get_nft_token_issuance( &self, token_id: TokenId, - ) -> Result, ApiServerStorageError> { + ) -> Result, ApiServerStorageError> { let conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR)); let res = conn.get_nft_token_issuance(token_id).await?; diff --git a/api-server/api-server-common/src/storage/storage_api/mod.rs b/api-server/api-server-common/src/storage/storage_api/mod.rs index f291d5a0cc..adf9ceadde 100644 --- a/api-server/api-server-common/src/storage/storage_api/mod.rs +++ b/api-server/api-server-common/src/storage/storage_api/mod.rs @@ -20,6 +20,7 @@ use std::{ }; use common::{ + address::Address, chain::{ block::timestamp::BlockTimestamp, timelock::OutputTimeLock, @@ -415,6 +416,12 @@ impl FungibleTokenData { } } +#[derive(Debug, Clone)] +pub struct NftWithOwner { + pub nft: NftIssuance, + pub owner: Option, +} + #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct TxAdditionalInfo { pub fee: Amount, @@ -570,7 +577,7 @@ pub trait ApiServerStorageRead: Sync { async fn get_nft_token_issuance( &self, token_id: TokenId, - ) -> Result, ApiServerStorageError>; + ) -> Result, ApiServerStorageError>; async fn get_token_num_decimals( &self, @@ -641,7 +648,7 @@ pub trait ApiServerStorageWrite: ApiServerStorageRead { async fn set_address_balance_at_height( &mut self, - address: &str, + address: &Address, amount: Amount, coin_or_token_id: CoinOrTokenId, block_height: BlockHeight, @@ -649,7 +656,7 @@ pub trait ApiServerStorageWrite: ApiServerStorageRead { async fn set_address_locked_balance_at_height( &mut self, - address: &str, + address: &Address, amount: Amount, coin_or_token_id: CoinOrTokenId, block_height: BlockHeight, @@ -749,6 +756,7 @@ pub trait ApiServerStorageWrite: ApiServerStorageRead { token_id: TokenId, block_height: BlockHeight, issuance: NftIssuance, + owner: &Destination, ) -> Result<(), ApiServerStorageError>; async fn del_token_issuance_above_height( diff --git a/api-server/scanner-lib/src/blockchain_state/mod.rs b/api-server/scanner-lib/src/blockchain_state/mod.rs index d2348aba0e..bb0bf3aabd 100644 --- a/api-server/scanner-lib/src/blockchain_state/mod.rs +++ b/api-server/scanner-lib/src/blockchain_state/mod.rs @@ -1584,7 +1584,9 @@ async fn update_tables_from_transaction_outputs( ) .await; - db_tx.set_nft_token_issuance(*token_id, block_height, *issuance.clone()).await?; + db_tx + .set_nft_token_issuance(*token_id, block_height, *issuance.clone(), destination) + .await?; set_utxo( outpoint, output, @@ -1936,7 +1938,7 @@ async fn increase_address_amount( let new_amount = current_balance.add(*amount).expect("Balance should not overflow"); db_tx - .set_address_balance_at_height(address.as_str(), new_amount, coin_or_token_id, block_height) + .set_address_balance_at_height(address, new_amount, coin_or_token_id, block_height) .await .expect("Unable to update balance") } @@ -1957,12 +1959,7 @@ async fn increase_locked_address_amount( let new_amount = current_balance.add(*amount).expect("Balance should not overflow"); db_tx - .set_address_locked_balance_at_height( - address.as_str(), - new_amount, - coin_or_token_id, - block_height, - ) + .set_address_locked_balance_at_height(address, new_amount, coin_or_token_id, block_height) .await .expect("Unable to update balance") } @@ -1988,7 +1985,7 @@ async fn decrease_address_amount( }); db_tx - .set_address_balance_at_height(address.as_str(), new_amount, coin_or_token_id, block_height) + .set_address_balance_at_height(&address, new_amount, coin_or_token_id, block_height) .await .expect("Unable to update balance") } @@ -2014,12 +2011,7 @@ async fn decrease_address_locked_amount( }); db_tx - .set_address_locked_balance_at_height( - address.as_str(), - new_amount, - coin_or_token_id, - block_height, - ) + .set_address_locked_balance_at_height(&address, new_amount, coin_or_token_id, block_height) .await .expect("Unable to update balance") } diff --git a/api-server/scanner-lib/src/sync/tests/simulation.rs b/api-server/scanner-lib/src/sync/tests/simulation.rs index 2202aae095..4c6f4b35de 100644 --- a/api-server/scanner-lib/src/sync/tests/simulation.rs +++ b/api-server/scanner-lib/src/sync/tests/simulation.rs @@ -954,7 +954,7 @@ async fn check_token( RPCTokenInfo::NonFungibleToken(node_data) => { let scanner_data = tx.get_nft_token_issuance(token_id).await.unwrap().unwrap(); - match scanner_data { + match scanner_data.nft { NftIssuance::V0(scanner_data) => { let scanner_metadata: RPCNonFungibleTokenMetadata = (&scanner_data.metadata).into(); diff --git a/api-server/stack-test-suite/tests/v2/nft.rs b/api-server/stack-test-suite/tests/v2/nft.rs index 0f60f07634..e2bf228d4b 100644 --- a/api-server/stack-test-suite/tests/v2/nft.rs +++ b/api-server/stack-test-suite/tests/v2/nft.rs @@ -13,7 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use api_web_server::api::json_helpers::nft_issuance_data_to_json; +use api_server_common::storage::storage_api::NftWithOwner; +use api_web_server::api::json_helpers::nft_with_owner_to_json; use common::{ chain::tokens::{make_token_id, NftIssuance, TokenId}, primitives::H256, @@ -124,7 +125,7 @@ async fn ok(#[case] seed: Seed) { StandardInputSignature::produce_uniparty_signature_for_input( &alice_sk, SigHashType::try_from(SigHashType::ALL).unwrap(), - alice_destination, + alice_destination.clone(), &tx, &[issue_nft_tx.outputs().first()], 0, @@ -145,7 +146,13 @@ async fn ok(#[case] seed: Seed) { _ = tx.send([( token_id, - nft_issuance_data_to_json(&NftIssuance::V0(nft), &chain_config), + nft_with_owner_to_json( + &NftWithOwner { + nft: NftIssuance::V0(nft), + owner: Some(alice_destination), + }, + &chain_config, + ), )]); chainstate_block_ids diff --git a/api-server/storage-test-suite/src/basic.rs b/api-server/storage-test-suite/src/basic.rs index 302311ec50..2d4e60cae3 100644 --- a/api-server/storage-test-suite/src/basic.rs +++ b/api-server/storage-test-suite/src/basic.rs @@ -1071,6 +1071,8 @@ where drop(db_tx); + let (_, pk) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + let random_owner = Destination::PublicKeyHash(PublicKeyHash::from(&pk)); let nft = NftIssuance::V0(NftIssuanceV0 { metadata: common::chain::tokens::Metadata { creator: None, @@ -1088,13 +1090,60 @@ where let block_height = BlockHeight::new(rng.gen_range(1..100)); db_tx - .set_nft_token_issuance(random_token_id, block_height, nft.clone()) + .set_nft_token_issuance(random_token_id, block_height, nft.clone(), &random_owner) .await .unwrap(); let returned_nft = db_tx.get_nft_token_issuance(random_token_id).await.unwrap().unwrap(); - assert_eq!(returned_nft, nft); + assert_eq!(returned_nft.nft, nft); + assert_eq!(returned_nft.owner.unwrap(), random_owner); + + // Spend the nft + let address = Address::new(&chain_config, random_owner).unwrap(); + db_tx + .set_address_balance_at_height( + &address, + Amount::ZERO, + CoinOrTokenId::TokenId(random_token_id), + block_height.next_height(), + ) + .await + .unwrap(); + + let returned_nft = db_tx.get_nft_token_issuance(random_token_id).await.unwrap().unwrap(); + + assert_eq!(returned_nft.nft, nft); + // now owner is None + assert_eq!(returned_nft.owner, None); + + // Send the nft to a new owner + let (_, pk) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + let random_owner2 = Destination::PublicKeyHash(PublicKeyHash::from(&pk)); + let address2 = Address::new(&chain_config, random_owner2).unwrap(); + db_tx + .set_address_balance_at_height( + &address2, + Amount::from_atoms(1), + CoinOrTokenId::TokenId(random_token_id), + block_height.next_height(), + ) + .await + .unwrap(); + + let returned_nft = db_tx.get_nft_token_issuance(random_token_id).await.unwrap().unwrap(); + + assert_eq!(returned_nft.nft, nft); + // now owner is the new owner + assert_eq!(&returned_nft.owner.unwrap(), address2.as_object()); + + // delete the latest block height + db_tx.del_nft_issuance_above_height(block_height).await.unwrap(); + + let returned_nft = db_tx.get_nft_token_issuance(random_token_id).await.unwrap().unwrap(); + assert_eq!(returned_nft.nft, nft); + // now owner is the old owner again + assert_eq!(&returned_nft.owner.unwrap(), address.as_object()); db_tx .del_nft_issuance_above_height(block_height.prev_height().unwrap()) @@ -1232,19 +1281,21 @@ where }, }); + let (_, pk) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + let random_owner = Destination::PublicKeyHash(PublicKeyHash::from(&pk)); let random_token_id4 = TokenId::new(H256::random_using(&mut rng)); db_tx - .set_nft_token_issuance(random_token_id4, block_height, nft.clone()) + .set_nft_token_issuance(random_token_id4, block_height, nft.clone(), &random_owner) .await .unwrap(); let random_token_id5 = TokenId::new(H256::random_using(&mut rng)); db_tx - .set_nft_token_issuance(random_token_id5, block_height, nft.clone()) + .set_nft_token_issuance(random_token_id5, block_height, nft.clone(), &random_owner) .await .unwrap(); let random_token_id6 = TokenId::new(H256::random_using(&mut rng)); db_tx - .set_nft_token_issuance(random_token_id6, block_height, nft.clone()) + .set_nft_token_issuance(random_token_id6, block_height, nft.clone(), &random_owner) .await .unwrap(); diff --git a/api-server/web-server/src/api/json_helpers.rs b/api-server/web-server/src/api/json_helpers.rs index d0bcc2c114..2df1ebc21a 100644 --- a/api-server/web-server/src/api/json_helpers.rs +++ b/api-server/web-server/src/api/json_helpers.rs @@ -16,7 +16,7 @@ use std::{collections::BTreeMap, ops::Sub}; use api_server_common::storage::storage_api::{ - block_aux_data::BlockAuxData, Order, TransactionInfo, TxAdditionalInfo, + block_aux_data::BlockAuxData, NftWithOwner, Order, TransactionInfo, TxAdditionalInfo, }; use common::{ address::Address, @@ -224,6 +224,23 @@ pub fn txoutput_to_json( } } +pub fn nft_with_owner_to_json( + data: &NftWithOwner, + chain_config: &ChainConfig, +) -> serde_json::Value { + let mut json = nft_issuance_data_to_json(&data.nft, chain_config); + let obj = json.as_object_mut().expect("object"); + obj.insert( + "owner".into(), + data.owner + .as_ref() + .map(|d| Address::new(chain_config, d.clone()).expect("addressable").to_string()) + .into(), + ); + + json +} + pub fn nft_issuance_data_to_json( data: &NftIssuance, chain_config: &ChainConfig, diff --git a/api-server/web-server/src/api/v2.rs b/api-server/web-server/src/api/v2.rs index ee0bbbde3d..0b90c642a9 100644 --- a/api-server/web-server/src/api/v2.rs +++ b/api-server/web-server/src/api/v2.rs @@ -58,7 +58,7 @@ use utils::ensure; use crate::ApiServerWebServerState; -use super::json_helpers::{nft_issuance_data_to_json, to_json_string}; +use super::json_helpers::{nft_with_owner_to_json, to_json_string}; pub const API_VERSION: &str = "2.0.0"; @@ -1092,7 +1092,7 @@ pub async fn nft( ApiServerWebServerNotFoundError::NftNotFound, ))?; - Ok(Json(nft_issuance_data_to_json(&nft, &state.chain_config))) + Ok(Json(nft_with_owner_to_json(&nft, &state.chain_config))) } pub async fn coin_statistics(