From 2dbeecc3a224544688d5f69cd84aec241076cd69 Mon Sep 17 00:00:00 2001 From: Gevorg Voskanyan Date: Fri, 14 Feb 2025 19:12:48 +0400 Subject: [PATCH] Implemented asset modification --- src/primitives/transaction.cpp | 10 +- src/primitives/transaction.h | 1 + src/qt/myownspats.cpp | 23 ++++ src/qt/myownspats.h | 1 + src/qt/sparkassetdialog.cpp | 27 ++-- src/script/script.cpp | 6 + src/script/script.h | 8 +- src/spark/state.cpp | 38 ++++++ src/spats/CMakeLists.txt | 2 + src/spats/actions.hpp | 52 ++++++- src/spats/modification.hpp | 231 ++++++++++++++++++++++++++++++++ src/spats/registry.cpp | 154 ++++++++++++++++++++- src/spats/registry.hpp | 82 ++++++++++-- src/spats/spark_asset.hpp | 44 ++++-- src/spats/wallet.cpp | 38 ++++++ src/spats/wallet.hpp | 2 + src/test/data/script_tests.json | 1 - src/utils/constrained_value.hpp | 39 +++++- src/utils/empty_class.hpp | 14 ++ src/utils/scaled_amount.hpp | 30 ++++- src/wallet/wallet.cpp | 33 +++++ src/wallet/wallet.h | 2 + 22 files changed, 784 insertions(+), 54 deletions(-) create mode 100644 src/spats/modification.hpp create mode 100644 src/utils/empty_class.hpp diff --git a/src/primitives/transaction.cpp b/src/primitives/transaction.cpp index af3616f9c0..247f45576b 100644 --- a/src/primitives/transaction.cpp +++ b/src/primitives/transaction.cpp @@ -231,9 +231,17 @@ bool CTransaction::IsSpatsUnregister() const return false; } +bool CTransaction::IsSpatsModify() const +{ + for (const CTxOut &txout: vout) + if (txout.scriptPubKey.IsSpatsModify()) + return true; + return false; +} + bool CTransaction::IsSpatsTransaction() const { - return IsSpatsCreate() || IsSpatsUnregister(); // TODO more + return IsSpatsCreate() || IsSpatsUnregister() || IsSpatsModify(); // TODO more } bool CTransaction::IsSparkSpend() const diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h index 84b8d095a4..f1808e338e 100644 --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -460,6 +460,7 @@ class CTransaction bool IsSpatsTransaction() const; bool IsSpatsCreate() const; bool IsSpatsUnregister() const; + bool IsSpatsModify() const; bool HasNoRegularInputs() const; bool HasPrivateInputs() const; diff --git a/src/qt/myownspats.cpp b/src/qt/myownspats.cpp index 0ba500dc13..1a8e3fad47 100644 --- a/src/qt/myownspats.cpp +++ b/src/qt/myownspats.cpp @@ -22,6 +22,7 @@ MyOwnSpats::MyOwnSpats( const PlatformStyle *platform_style, QWidget *parent ) ui_->tableWidgetMyOwnSpats->setSelectionBehavior( QAbstractItemView::SelectRows ); ui_->tableWidgetMyOwnSpats->setSelectionMode( QAbstractItemView::SingleSelection ); connect( ui_->create_spark_asset, &QPushButton::clicked, this, &MyOwnSpats::onCreateButtonClicked ); + connect( ui_->modify_spark_asset, &QPushButton::clicked, this, &MyOwnSpats::onModifyButtonClicked ); connect( ui_->unregister_spark_asset, &QPushButton::clicked, this, &MyOwnSpats::onUnregisterButtonClicked ); connect( this, &MyOwnSpats::displayMyOwnSpatsSignal, this, &MyOwnSpats::handleDisplayMyOwnSpatsSignal ); connect( ui_->tableWidgetMyOwnSpats->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MyOwnSpats::updateButtonStates ); @@ -145,6 +146,28 @@ void MyOwnSpats::onCreateButtonClicked() } } +void MyOwnSpats::onModifyButtonClicked() +{ + assert( wallet_model_ ); + if ( const auto row = get_the_selected_row() ) { + try { + const spats::asset_type_t asset_type{ ui_->tableWidgetMyOwnSpats->item( *row, 0 )->text().toULongLong() }; + spats::identifier_t identifier{ 0 }; + if ( !is_fungible_asset_type( asset_type ) ) + identifier = spats::identifier_t{ ui_->tableWidgetMyOwnSpats->item( *row, 1 )->text().toULongLong() }; + const auto &existing_asset = my_own_assets_map_.at( spats::universal_asset_id_t{ asset_type, identifier } ); + SparkAssetDialog dialog( platform_style_, existing_asset, this ); + if ( dialog.exec() == QDialog::Accepted ) + wallet_model_->getWallet()->ModifySparkAsset( existing_asset, *dialog.getResultAsset() ); // TODO user confirm callback + } + catch ( const std::exception &e ) { + QMessageBox::critical( this, tr( "Error" ), tr( "An error occurred: %1" ).arg( e.what() ) ); + } + } + else + QMessageBox::critical( this, tr( "Error" ), tr( "Please select an asset to modify." ) ); +} + void MyOwnSpats::onUnregisterButtonClicked() { assert( wallet_model_ ); diff --git a/src/qt/myownspats.h b/src/qt/myownspats.h index f679542293..a03d33f878 100644 --- a/src/qt/myownspats.h +++ b/src/qt/myownspats.h @@ -40,6 +40,7 @@ class MyOwnSpats : public QWidget, public spats::UpdatesObserver { private Q_SLOTS: void onCreateButtonClicked(); + void onModifyButtonClicked(); void onUnregisterButtonClicked(); void handleDisplayMyOwnSpatsSignal() { display_my_own_spats(); } void updateButtonStates(); diff --git a/src/qt/sparkassetdialog.cpp b/src/qt/sparkassetdialog.cpp index 83a3adc505..7441e25d93 100644 --- a/src/qt/sparkassetdialog.cpp +++ b/src/qt/sparkassetdialog.cpp @@ -57,16 +57,13 @@ SparkAssetDialog::SparkAssetDialog( const PlatformStyle *platform_style, dialog_ // Example: Apply styles (if applicable to your PlatformStyle) } + std::visit( [ this ]( auto &&arg ) { set_fields( arg ); }, context_ ); + // Connect UI elements to actions connect( ui_->fungibilityCheckBox, &QCheckBox::stateChanged, this, &SparkAssetDialog::onFungibilityChanged ); connect( ui_->assetTypeSpinBox, &QUInt64SpinBox::valueChanged, this, &SparkAssetDialog::onAssetTypeChanged ); connect( ui_->saveButton, &QPushButton::clicked, this, &SparkAssetDialog::onSave ); connect( ui_->cancelButton, &QPushButton::clicked, this, &QDialog::reject ); - - std::visit( [ this ]( auto &&arg ) { set_fields( arg ); }, context_ ); - - // Update field visibility/state based on fungibility checkbox state - onFungibilityChanged( ui_->fungibilityCheckBox->isChecked() ? Qt::Checked : Qt::Unchecked ); } SparkAssetDialog::~SparkAssetDialog() {} @@ -113,17 +110,17 @@ void SparkAssetDialog::onSave() void SparkAssetDialog::onFungibilityChanged( int state ) { - const bool is_fungible = ( state == Qt::Checked ); + if ( const auto *const context = std::get_if< NewSparkAssetCreationContext >( &context_ ) ) { + const bool is_fungible = ( state == Qt::Checked ); - // Enable or disable fungible-related fields - ui_->totalSupplySpin->setEnabled( is_fungible ); - ui_->resupplyableCheckBox->setEnabled( is_fungible ); - ui_->precisionSpinBox->setEnabled( is_fungible ); + // Enable or disable fungible-related fields + ui_->totalSupplySpin->setEnabled( is_fungible ); + ui_->resupplyableCheckBox->setEnabled( is_fungible ); + ui_->precisionSpinBox->setEnabled( is_fungible ); - // Non-Fungible-specific fields - ui_->identifierSpinBox->setEnabled( !is_fungible ); + // Non-Fungible-specific fields + ui_->identifierSpinBox->setEnabled( !is_fungible ); - if ( const auto *const context = std::get_if< NewSparkAssetCreationContext >( &context_ ) ) { if ( is_fungible ) { ui_->assetTypeSpinBox->setValue( context->lowest_available_asset_type_for_new_fungible_asset ); } @@ -132,6 +129,8 @@ void SparkAssetDialog::onFungibilityChanged( int state ) } onAssetTypeChanged( ui_->assetTypeSpinBox->value() ); } + else + assert( !"Not allowed to modify the fungibility of an existing asset" ); } void SparkAssetDialog::onAssetTypeChanged( int asset_type_value ) @@ -191,8 +190,6 @@ void SparkAssetDialog::set_fields( const spats::SparkAsset &existing_asset ) } }, existing_asset ); - onFungibilityChanged( ui_->fungibilityCheckBox->checkState() ); - ui_->fungibilityCheckBox->setEnabled( false ); ui_->assetTypeSpinBox->setEnabled( false ); ui_->identifierSpinBox->setEnabled( false ); diff --git a/src/script/script.cpp b/src/script/script.cpp index a7b7681c39..dadadd38ff 100644 --- a/src/script/script.cpp +++ b/src/script/script.cpp @@ -158,6 +158,7 @@ const char* GetOpName(opcodetype opcode) case OP_SPARKSPEND : return "OP_SPARKSPEND"; case OP_SPATSCREATE : return "OP_SPATSCREATE"; case OP_SPATSUNREGISTER : return "OP_SPATSUNREGISTER"; + case OP_SPATSMODIFY : return "OP_SPATSMODIFY"; // Super transparent txout script prefix case OP_EXCHANGEADDR : return "OP_EXCHANGEADDR"; @@ -371,6 +372,11 @@ bool CScript::IsSpatsUnregister() const return this->size() > 0 && (*this)[0] == OP_SPATSUNREGISTER; } +bool CScript::IsSpatsModify() const +{ + return this->size() > 0 && (*this)[0] == OP_SPATSMODIFY; +} + bool CScript::IsSpats() const { return this->size() > 0 && IsSpatsOp(static_cast((*this)[0])); diff --git a/src/script/script.h b/src/script/script.h index fd97ae0c39..2d6f56fa74 100644 --- a/src/script/script.h +++ b/src/script/script.h @@ -209,12 +209,16 @@ enum opcodetype OP_SPARKSPEND = 0xd3, OP_SPATSCREATE = 0xd4, OP_SPATSUNREGISTER = 0xd5, + OP_SPATSMODIFY = 0xd6, // TODO when adding a new spats opcode, update this below, and keep all spats ops values consecutive if possible, otherwise change IsSpatsOp() implementation - OP_SPATSLAST = OP_SPATSUNREGISTER, + OP_SPATSLAST = OP_SPATSMODIFY, // basically NOP but identifies that subsequent txout script contains super transparent address OP_EXCHANGEADDR = 0xe0 }; +// ATTENTION: When adding a new enumerator to the above, make sure to update the src/test/data/script_tests.json file accordingly, e.g. by deleting the corresponding +// ["0", "IF 0xNN ELSE 1 ENDIF", "P2SH,STRICTENC", "OK"], +// line... const char* GetOpName(opcodetype opcode); @@ -702,6 +706,8 @@ class CScript : public CScriptBase bool IsSpatsUnregister() const; + bool IsSpatsModify() const; + bool IsSpats() const; bool IsZerocoinRemint() const; diff --git a/src/spark/state.cpp b/src/spark/state.cpp index ae865e78b3..41e422a642 100644 --- a/src/spark/state.cpp +++ b/src/spark/state.cpp @@ -261,6 +261,42 @@ static spats::UnregisterAssetAction ParseSpatsUnregisterTransaction(const CTrans } } +static spats::ModifyAssetAction ParseSpatsModifyTransaction(const CTransaction &tx) +{ + assert(tx.IsSpatsModify()); + if (tx.vout.size() < 1) + throw CBadTxIn(); + const CScript& modification_script = tx.vout.front().scriptPubKey; + if (!modification_script.IsSpatsModify()) + throw CBadTxIn(); + + try{ + std::vector serialized(modification_script.begin() + 1, modification_script.end()); + auto action_serialization_size = serialized.size(); + const void* const action_serialization_start_address = serialized.data(); + CDataStream stream(serialized, SER_NETWORK, PROTOCOL_VERSION ); + assert(stream.size() == action_serialization_size); + spats::ModifyAssetAction action(deserialize, stream); + const auto &asset_modification = action.get(); + action_serialization_size -= stream.size(); + spark::OwnershipProof proof; + stream >> proof; + LogPrintf("ParseSpatsModifyTransaction address ownership proof: %s\n", proof); + const auto& b = get_base(asset_modification); + Address a(spark::Params::get_default()); + a.decode(b.initiator_public_address()); + const auto scalar_of_proof = spats::Wallet::compute_modify_spark_asset_serialization_scalar( + b, {static_cast(action_serialization_start_address), action_serialization_size}); + LogPrintf("ParseSpatsModifyTransaction scalar_of_proof: %s\n", scalar_of_proof); + if (!a.verify_own(scalar_of_proof, proof)) + throw CBadTxIn(); + return action; + } + catch (...) { + throw CBadTxIn(); + } +} + static spats::Action ParseSpatsTransaction(const CTransaction &tx) { assert(tx.IsSpatsTransaction()); @@ -268,6 +304,8 @@ static spats::Action ParseSpatsTransaction(const CTransaction &tx) return ParseSpatsCreateTransaction(tx); if (tx.IsSpatsUnregister()) return ParseSpatsUnregisterTransaction(tx); + if (tx.IsSpatsModify()) + return ParseSpatsModifyTransaction(tx); // TODO more throw CBadTxIn(); } diff --git a/src/spats/CMakeLists.txt b/src/spats/CMakeLists.txt index 2849563aca..565e782e5e 100644 --- a/src/spats/CMakeLists.txt +++ b/src/spats/CMakeLists.txt @@ -30,7 +30,9 @@ add_library(spats STATIC actions.hpp manager.cpp manager.hpp + modification.hpp ../utils/constrained_value.hpp + ../utils/empty_class.hpp ../utils/enum.hpp ../utils/math.hpp ../utils/scaled_amount.hpp diff --git a/src/spats/actions.hpp b/src/spats/actions.hpp index 21e479f321..1ea9fc1bb7 100644 --- a/src/spats/actions.hpp +++ b/src/spats/actions.hpp @@ -13,6 +13,7 @@ #include "identification.hpp" #include "base_asset.hpp" +#include "modification.hpp" #include "spark_asset.hpp" namespace spats { @@ -82,7 +83,9 @@ class CreateAssetAction { {} template < typename Stream > - CreateAssetAction( deserialize_type, Stream &is ); + CreateAssetAction( deserialize_type, Stream &is ) + : asset_( Unserialize( is ) ) + {} template < typename Stream > void Serialize( Stream &os ) const; @@ -98,11 +101,6 @@ class CreateAssetAction { static SparkAsset Unserialize( Stream &is ); }; -template < typename Stream > -CreateAssetAction::CreateAssetAction( deserialize_type, Stream &is ) - : asset_( Unserialize( is ) ) -{} - template < typename Stream > void CreateAssetAction::Serialize( Stream &os ) const { @@ -152,7 +150,47 @@ class UnregisterAssetAction { } }; -using Action = std::variant< CreateAssetAction, UnregisterAssetAction >; // TODO more +class ModifyAssetAction { +public: + explicit ModifyAssetAction( AssetModification asset_modification ) + : asset_modification_( std::move( asset_modification ) ) + {} + + template < typename Stream > + ModifyAssetAction( deserialize_type, Stream &is ) + : asset_modification_( Unserialize( is ) ) + {} + + template < typename Stream > + void Serialize( Stream &os ) const; + + const AssetModification &get() const & noexcept { return asset_modification_; } + AssetModification &&get() && noexcept { return std::move( asset_modification_ ); } + +private: + AssetModification asset_modification_; + static constexpr std::uint8_t serialization_version = 1; + + template < typename Stream > + static AssetModification Unserialize( Stream &is ); +}; + +template < typename Stream > +void ModifyAssetAction::Serialize( Stream &os ) const +{ + os << serialization_version; + ::Serialize( os, asset_modification_ ); +} + +template < typename Stream > +AssetModification ModifyAssetAction::Unserialize( Stream &is ) +{ + std::uint8_t version; + is >> version; + return UnserializeVariant< AssetModification >( is ); +} + +using Action = std::variant< CreateAssetAction, UnregisterAssetAction, ModifyAssetAction >; // TODO more using ActionSequence = std::vector< Action >; diff --git a/src/spats/modification.hpp b/src/spats/modification.hpp new file mode 100644 index 0000000000..93c69af99f --- /dev/null +++ b/src/spats/modification.hpp @@ -0,0 +1,231 @@ +// +// Created by Gevorg Voskanyan +// + +#ifndef FIRO_SPATS_MODIFICATION_HPP_INCLUDED +#define FIRO_SPATS_MODIFICATION_HPP_INCLUDED + +#include "spark_asset.hpp" + +namespace spats { + +template < typename T > +void verify_modification_validity( const T & /*old_value*/, const T & /*new_value*/ ) noexcept +{ + // no-op by default, with any modifications considered OK +} + +inline void verify_modification_validity( const AssetNaming &old_value, const AssetNaming &new_value ) +{ + // TODO allow symbol length changes where the fee doesn't actually change + if ( old_value.symbol.get().length() != new_value.symbol.get().length() ) + throw std::domain_error( "Spark asset's symbol length cannot be modified" ); // because the creation fee depended on that! +} + +template < typename T > +class AttributeModification { +public: + AttributeModification( T old_value, T new_value ) + : old_( std::move( old_value ) ) + , new_( std::move( new_value ) ) + { + verify_modification_validity( old_, new_ ); // may throw + } + + template < typename Stream > + requires( std::is_default_constructible_v< T > ) + AttributeModification( deserialize_type, Stream &is ) + { + is >> old_ >> new_; + } + + template < typename Stream > + requires( !std::is_default_constructible_v< T > ) + AttributeModification( deserialize_type d, Stream &is ) + : old_( d, is ) + , new_( d, is ) + {} + + template < typename Stream > + void Serialize( Stream &os ) const + { + os << old_ << new_; + } + + const T &old_value() const noexcept { return old_; } + const T &new_value() const noexcept { return new_; } + + explicit operator bool() const noexcept( noexcept( old_ != new_ ) ) { return old_ != new_; } + +private: + T old_, new_; +}; + +class AssetModificationBase { +protected: + AssetModificationBase( const SparkAssetBase &old_asset_base, const SparkAssetBase &new_asset_base, public_address_t initiator_public_address ) + : asset_type_( old_asset_base.asset_type() ) + , initiator_public_address_( std::move( initiator_public_address ) ) + , asset_naming_change_( old_asset_base.naming(), new_asset_base.naming() ) + , metadata_change_( old_asset_base.metadata(), new_asset_base.metadata() ) + { + if ( old_asset_base.asset_type() != new_asset_base.asset_type() ) + throw std::domain_error( "Spats asset type cannot be modified" ); + if ( old_asset_base.admin_public_address() != new_asset_base.admin_public_address() ) + throw std::domain_error( + "Spark asset's admin public address cannot be modified via a regular modification operation - use Admin Control Transfer operation instead" ); // TODO + } + + template < typename Stream > + AssetModificationBase( deserialize_type d, Stream &is ) + : asset_naming_change_( d, is ) + , metadata_change_( d, is ) + { + is >> asset_type_ >> initiator_public_address_; + } + + ~AssetModificationBase() = default; + + template < typename Stream > + void Serialize( Stream &os ) const + { + os << asset_naming_change_ << metadata_change_ << asset_type_ << initiator_public_address_; + } + +public: + asset_type_t asset_type() const noexcept { return asset_type_; } + const public_address_t &initiator_public_address() const noexcept { return initiator_public_address_; } + +protected: + bool any_changes() const noexcept { return asset_naming_change_ || metadata_change_; } + + void apply_on( SparkAssetBase &b ) const + { + if ( b.naming() != asset_naming_change_.old_value() ) + throw std::domain_error( "Spark asset's naming modification no longer applicable" ); + if ( b.metadata() != metadata_change_.old_value() ) + throw std::domain_error( "Spark asset's metadata modification no longer applicable" ); + b.naming( asset_naming_change_.new_value() ); + b.metadata( metadata_change_.new_value() ); + } + +private: + asset_type_t asset_type_; + public_address_t initiator_public_address_; + AttributeModification< AssetNaming > asset_naming_change_; + AttributeModification< std::string > metadata_change_; +}; + +template < bool Fungible > +class BasicAssetModification : public AssetModificationBase { +public: + BasicAssetModification( const FungibleSparkAsset &old_asset, const FungibleSparkAsset &new_asset, public_address_t initiator_public_address ) + requires( Fungible ) + : AssetModificationBase( old_asset, new_asset, std::move( initiator_public_address ) ) + , old_asset_( old_asset ) + { + if ( old_asset.total_supply().precision() != new_asset.total_supply().precision() ) + throw std::domain_error( "Spark asset's precision cannot be modified" ); + if ( old_asset.total_supply() != new_asset.total_supply() ) + throw std::domain_error( "Spark asset's total supply cannot be modified via a regular modification operation - use Mint or Burn operations instead" ); + if ( old_asset.resupplyable() != new_asset.resupplyable() ) + throw std::domain_error( "Spark asset's resuppliability cannot be modified" ); + assert( apply_on( FungibleSparkAsset( old_asset ) ) == new_asset ); + assert( this->new_asset() == new_asset ); + } + + BasicAssetModification( const NonfungibleSparkAsset &old_asset, const NonfungibleSparkAsset &new_asset, public_address_t initiator_public_address ) + requires( !Fungible ) + : AssetModificationBase( old_asset, new_asset, std::move( initiator_public_address ) ) + , old_asset_( old_asset ) + { + if ( old_asset.identifier() != new_asset.identifier() ) + throw std::domain_error( "Spark asset's identifier cannot be modified" ); + assert( apply_on( NonfungibleSparkAsset( old_asset ) ) == new_asset ); + assert( this->new_asset() == new_asset ); + } + + template < typename Stream > + BasicAssetModification( deserialize_type d, Stream &is ) + : AssetModificationBase( d, is ) + , old_asset_( d, is ) + {} + + template < typename Stream > + void Serialize( Stream &os ) const + { + AssetModificationBase::Serialize( os ); + os << old_asset_; + } + + const BasicSparkAsset< Fungible > &old_asset() const noexcept { return old_asset_; } + + BasicSparkAsset< Fungible > new_asset() const + { + auto a = old_asset_; + apply_on( a ); + return a; + } + + void apply_on( BasicSparkAsset< Fungible > &a ) const + { + this->AssetModificationBase::apply_on( a ); + // TODO apply Fungible-ity specific attribute changes from *this, if there ever are any + } + + explicit operator bool() const noexcept + { + return AssetModificationBase::any_changes(); // TODO or any (Fungible-ity specific) data members in *this, if any ever + } + +private: + BasicSparkAsset< Fungible > old_asset_; + // ATTENTION: right now there are no fungibility-specific attributes that can be modified, but if there are in the future, add them here, as an object of a class that + // is specialized based on Fungible-ity. + + BasicSparkAsset< Fungible > apply_on( BasicSparkAsset< Fungible > &&a ) const + { + apply_on( a ); // just calling the lvalue-ref overload + return a; + } +}; + +using FungibleAssetModification = BasicAssetModification< true >; +using NonfungibleAssetModification = BasicAssetModification< false >; +using AssetModification = std::variant< FungibleAssetModification, NonfungibleAssetModification >; + +// Function that returns the base of an AssetModification +inline const AssetModificationBase &get_base( const AssetModification &modif ) noexcept +{ + return std::visit( []( const auto &m ) -> const AssetModificationBase & { return m; }, modif ); +} + +inline AssetModification make_asset_modification( const SparkAsset &old_asset, const SparkAsset &new_asset, public_address_t initiator_public_address ) +{ + if ( old_asset.index() != new_asset.index() ) + throw std::domain_error( "Cannot modify the fungibility of an asset" ); + return std::visit( + [ & ]< bool Fungible >( const BasicSparkAsset< Fungible > &old ) -> AssetModification { + return BasicAssetModification< Fungible >( old, std::get< BasicSparkAsset< Fungible > >( new_asset ), std::move( initiator_public_address ) ); + }, + old_asset ); +} + +inline SparkAsset get_old_asset( const AssetModification &modif ) +{ + return std::visit( []( const auto &m ) -> SparkAsset { return m.old_asset(); }, modif ); +} + +inline SparkAsset get_new_asset( const AssetModification &modif ) +{ + return std::visit( []( const auto &m ) -> SparkAsset { return m.new_asset(); }, modif ); +} + +inline bool has_any_modifications( const AssetModification &modif ) noexcept +{ + return std::visit( []( const auto &m ) { return !!m; }, modif ); +} + +} // namespace spats + +#endif // FIRO_SPATS_MODIFICATION_HPP_INCLUDED diff --git a/src/spats/registry.cpp b/src/spats/registry.cpp index bcf0420246..fc4265d496 100644 --- a/src/spats/registry.cpp +++ b/src/spats/registry.cpp @@ -82,7 +82,7 @@ bool Registry::has_nonfungible_asset( asset_type_t asset_type, identifier_t iden return it != nft_lines_.end() && it->second.contains( identifier ); } -bool Registry::process( const SparkAsset &a, [[maybe_unused]] int block_height, const std::optional< block_hash_t > &block_hash, write_lock_proof wlp ) +bool Registry::process( const SparkAsset &a, int /*block_height*/, const std::optional< block_hash_t > &block_hash, write_lock_proof wlp ) { std::visit( [ this, &block_hash, wlp ]( auto &&x ) { add( x, block_hash, wlp ); }, a ); return true; @@ -146,6 +146,50 @@ bool Registry::process( const UnregisterAssetParameters &p, int block_height, co return true; } +void Registry::validate( const AssetModification &m, read_lock_proof rlp ) const +{ + std::visit( [ & ]( const auto &x ) { internal_validate( x, rlp ); }, m ); +} + +void Registry::internal_validate( const FungibleAssetModification &m, read_lock_proof ) const +{ + const auto &existing_asset = m.old_asset(); + const auto asset_type = existing_asset.asset_type(); + assert( is_fungible_asset_type( asset_type ) ); + const auto it = fungible_assets_.find( asset_type ); + if ( it == fungible_assets_.end() ) + throw std::invalid_argument( "Asset to modify not found" ); + if ( it->second != existing_asset ) + throw std::domain_error( "Asset to modify has different data than what was expected" ); + if ( it->second.admin_public_address() != m.initiator_public_address() ) + throw std::domain_error( "No permission to modify the given asset" ); +} + +void Registry::internal_validate( const NonfungibleAssetModification &m, read_lock_proof ) const +{ + const auto &existing_asset = m.old_asset(); + const auto asset_type = existing_asset.asset_type(); + assert( !is_fungible_asset_type( asset_type ) ); + const auto it = nft_lines_.find( asset_type ); + if ( it != nft_lines_.end() ) { + const auto nft_it = it->second.find( existing_asset.identifier() ); + if ( nft_it != it->second.end() ) { + if ( nft_it->second != existing_asset ) + throw std::domain_error( "NFT to modify has different data than what was expected" ); + if ( nft_it->second.admin_public_address() != m.initiator_public_address() ) + throw std::domain_error( "No permission to modify the given NFT" ); + return; // all ok with the modification if it reaches here + } + } + throw std::invalid_argument( "No such NFT found to unregister" ); +} + +bool Registry::process( + const AssetModification &m, int block_height, const std::optional< block_hash_t > &block_hash, write_lock_proof wlp, BlockAnnotation **out_block_annotation_ptr ) +{ + return std::visit( [ &, wlp ]( const auto &x ) { return modify( x, block_height, block_hash, wlp, out_block_annotation_ptr ); }, m ); +} + bool Registry::unprocess( const SparkAsset &a, [[maybe_unused]] int block_height, write_lock_proof wlp ) { const auto &b = get_base( a ); @@ -175,6 +219,26 @@ bool Registry::unprocess( const UnregisterAssetParameters &p, int block_height, return any_changes; } +bool Registry::unprocess( const AssetModification &m, int block_height, write_lock_proof wlp ) +{ + if ( !has_any_modifications( m ) ) + return false; + assert( block_height >= 0 ); + const auto &b = get_base( m ); + BlockAnnotation *block_annotation = nullptr; + // just applying the modification in reverse + [[maybe_unused]] const bool modified_back = + process( make_asset_modification( get_new_asset( m ), get_old_asset( m ), b.initiator_public_address() ), -1, {}, wlp, &block_annotation ); + assert( modified_back ); + assert( block_annotation ); + // and restoring the block hash + restore_block_annotation_before_modification( { b.asset_type(), get_identifier( get_old_asset( m ) ).value_or( identifier_t{} ) }, + *block_annotation, + block_height, + wlp ); + return true; +} + std::optional< asset_type_t > Registry::get_lowest_available_asset_type_for_new_fungible_asset() const noexcept { // TODO Performance: retrieve from an interval_set maintained along @@ -293,6 +357,24 @@ std::optional< Registry::LocatedAsset > Registry::get_asset( asset_type_t asset_ return LocatedAsset{ nft_it->second.block_hash, nft_it->second }; } +void Registry::restore_block_annotation_before_modification( const universal_asset_id_t modified_asset_id, + BlockAnnotation &block_annotation, + const int block_height_modified_at, + write_lock_proof ) +{ + assert( block_height_modified_at >= 0 ); + const auto it = modification_history_blocks_by_asset_.find( modified_asset_id ); + assert( it != modification_history_blocks_by_asset_.end() ); + auto &bookkeepings = it->second; + assert( !bookkeepings.empty() ); + auto &bk = bookkeepings.back(); + assert( bk.block_height_modification_applied_at == block_height_modified_at ); + block_annotation.block_hash = std::move( bk.block_hash_before_modification ); + bookkeepings.pop_back(); + if ( bookkeepings.empty() ) + modification_history_blocks_by_asset_.erase( it ); +} + std::vector< Nft > Registry::get_nfts_administered_by( const public_address_t &public_address ) const { std::shared_lock lock( mutex_ ); @@ -398,12 +480,82 @@ void Registry::internal_add( const NonfungibleSparkAsset &a, std::optional< bloc assert( has_nonfungible_asset( asset_type, identifier, wlp ) ); } +bool Registry::internal_modify( + const FungibleAssetModification &m, int block_height, std::optional< block_hash_t > block_hash, write_lock_proof, BlockAnnotation **out_block_annotation_ptr ) +{ + assert( !out_block_annotation_ptr || !block_hash && block_height == -1 ); + if ( !m ) + return false; + const auto asset_type = m.old_asset().asset_type(); + const auto it = fungible_assets_.find( asset_type ); + assert( it != fungible_assets_.end() ); + auto &a = it->second; + assert( a == m.old_asset() ); + assert( a.admin_public_address() == m.initiator_public_address() ); + m.apply_on( a ); + assert( a == m.new_asset() ); + assert( a.admin_public_address() == m.initiator_public_address() ); + if ( block_hash ) { + auto &effective_block_hash = it->second.block_hash; + if ( block_height >= 0 ) + modification_history_blocks_by_asset_[ { asset_type, identifier_t{} } ].emplace_back( std::move( effective_block_hash ), block_height ); + effective_block_hash = std::move( block_hash ); + } + else if ( out_block_annotation_ptr ) { + assert( block_height == -1 ); + *out_block_annotation_ptr = &a; + } + return true; +} + +bool Registry::internal_modify( + const NonfungibleAssetModification &m, int block_height, std::optional< block_hash_t > block_hash, write_lock_proof, BlockAnnotation **out_block_annotation_ptr ) +{ + assert( !out_block_annotation_ptr || !block_hash && block_height == -1 ); + if ( !m ) + return false; + const auto asset_type = m.old_asset().asset_type(); + const auto it = nft_lines_.find( asset_type ); + assert( it != nft_lines_.end() ); + assert( it->second.contains( m.old_asset().identifier() ) ); + assert( it->second.begin()->second.admin_public_address() == m.initiator_public_address() ); + const auto identifier = m.old_asset().identifier(); + auto &a = it->second.at( identifier ); + assert( a == m.old_asset() ); + assert( a.admin_public_address() == m.initiator_public_address() ); + m.apply_on( a ); + assert( a == m.new_asset() ); + assert( a.admin_public_address() == m.initiator_public_address() ); + if ( block_hash ) { + auto &effective_block_hash = a.block_hash; + if ( block_height >= 0 ) + modification_history_blocks_by_asset_[ { asset_type, identifier } ].emplace_back( std::move( effective_block_hash ), block_height ); + effective_block_hash = std::move( block_hash ); + } + else if ( out_block_annotation_ptr ) { + assert( block_height == -1 ); + *out_block_annotation_ptr = &a; + } + return true; +} + void Registry::cleanup_old_blocks_bookkeeping( int block_height, write_lock_proof ) { const int cleanup_threshold = 2000; if ( block_height >= cleanup_threshold ) { const int remove_earlier_than_block_number = block_height - cleanup_threshold; + std::erase_if( unregistered_assets_, [ = ]( const auto &u ) { return u.block_height_unregistered_at < remove_earlier_than_block_number; } ); + + for ( auto it = modification_history_blocks_by_asset_.begin(); it != modification_history_blocks_by_asset_.end(); ) { + auto &bookkeepings = it->second; + while ( !bookkeepings.empty() && bookkeepings.front().block_height_modification_applied_at < remove_earlier_than_block_number ) + bookkeepings.pop_front(); + if ( bookkeepings.empty() ) + modification_history_blocks_by_asset_.erase( it++ ); + else + ++it; + } } } diff --git a/src/spats/registry.hpp b/src/spats/registry.hpp index 55cbf77e46..58d47fe9ac 100644 --- a/src/spats/registry.hpp +++ b/src/spats/registry.hpp @@ -10,8 +10,10 @@ #include #include #include +#include #include "../uint256.h" +#include "../utils/empty_class.hpp" #include "../utils/lock_proof.hpp" #include "spark_asset.hpp" @@ -52,30 +54,45 @@ class Registry { void clear(); private: + struct BlockAnnotation { + std::optional< block_hash_t > block_hash; +#if 0 // not sure yet whether this will be needed here or not + // if ever added, then add a block_height_before_modification to AssetModificationBlockBookkeeping too + // as well as to BlockAnnotated constructor too, of course + int block_height; +#endif + }; + struct UnregisteredAsset { int block_height_unregistered_at; SparkAsset asset; + // BlockAnnotation block_annotation;//TODO }; template < typename T > - struct BlockAnnotated : T { - std::optional< block_hash_t > block_hash; -#if 0 // not sure yet whether this will be needed here or not - int block_height; -#endif - - BlockAnnotated( T t, std::optional< block_hash_t > block_hash ) + struct BlockAnnotated : T, BlockAnnotation { + BlockAnnotated( T t, std::optional< block_hash_t > blockhash ) : T( std::move( t ) ) - , block_hash( std::move( block_hash ) ) + , BlockAnnotation{ std::move( blockhash ) } {} }; mutable std::shared_mutex mutex_; // TODO Performance: a more efficient storage type for both, using contiguous memory to the extent possible - // All below data members are protected by mutex_ + // All below data members, till the end of this class, are protected by mutex_ std::unordered_map< asset_type_t, BlockAnnotated< FungibleSparkAsset > > fungible_assets_; std::unordered_map< asset_type_t, std::unordered_map< identifier_t, BlockAnnotated< NonfungibleSparkAsset > > > nft_lines_; std::list< UnregisteredAsset > unregistered_assets_; + + struct AssetModificationBlockBookkeeping { + std::optional< block_hash_t > block_hash_before_modification; + int block_height_modification_applied_at; + }; + + // The deque here is intended to be used like a stack, except during the very old blocks history cleanup, where elements may be removed from its front + using asset_modification_blocks_history_t = std::deque< AssetModificationBlockBookkeeping >; + std::map< universal_asset_id_t, asset_modification_blocks_history_t > modification_history_blocks_by_asset_; + int last_block_height_processed_ = -1; using read_lock_proof = utils::read_lock_proof< &Registry::mutex_ >; @@ -108,15 +125,56 @@ class Registry { // addition (creation) bool process( const SparkAsset &a, int block_height, const std::optional< block_hash_t > &block_hash, write_lock_proof wlp ); + // unregistration void validate( const UnregisterAssetParameters &p, read_lock_proof ) const; - bool process( const UnregisterAssetParameters &p, int block_height, const std::optional< block_hash_t > &block_hash, write_lock_proof ); + // modification + void internal_validate( const FungibleAssetModification &m, read_lock_proof ) const; + void internal_validate( const NonfungibleAssetModification &m, read_lock_proof ) const; + void validate( const AssetModification &m, read_lock_proof ) const; + bool process( const AssetModification &m, + int block_height, + const std::optional< block_hash_t > &block_hash, + write_lock_proof, + BlockAnnotation **out_block_annotation_ptr = nullptr ); + + bool modify( const FungibleAssetModification &m, + int block_height, + std::optional< block_hash_t > block_hash, + write_lock_proof wlp, + BlockAnnotation **out_block_annotation_ptr = nullptr ) + { + internal_validate( m, wlp ); // will throw if invalid + return internal_modify( m, block_height, std::move( block_hash ), wlp, out_block_annotation_ptr ); + } + + bool modify( const NonfungibleAssetModification &m, + int block_height, + std::optional< block_hash_t > block_hash, + write_lock_proof wlp, + BlockAnnotation **out_block_annotation_ptr = nullptr ) + { + internal_validate( m, wlp ); // will throw if invalid + return internal_modify( m, block_height, std::move( block_hash ), wlp, out_block_annotation_ptr ); + } + bool unprocess( const SparkAsset &a, int block_height, write_lock_proof wlp ); bool unprocess( const UnregisterAssetParameters &p, int block_height, write_lock_proof ); + bool unprocess( const AssetModification &m, int block_height, write_lock_proof ); void internal_add( const FungibleSparkAsset &a, std::optional< block_hash_t > block_hash, write_lock_proof ); void internal_add( const NonfungibleSparkAsset &a, std::optional< block_hash_t > block_hash, write_lock_proof ); + bool internal_modify( const FungibleAssetModification &m, + int block_height, + std::optional< block_hash_t > block_hash, + write_lock_proof, + BlockAnnotation **out_block_annotation_ptr = nullptr ); + bool internal_modify( const NonfungibleAssetModification &m, + int block_height, + std::optional< block_hash_t > block_hash, + write_lock_proof, + BlockAnnotation **out_block_annotation_ptr = nullptr ); void add_the_base_asset( write_lock_proof ); bool has_nonfungible_asset( asset_type_t asset_type, identifier_t identifier, read_lock_proof ) const noexcept; @@ -127,6 +185,10 @@ class Registry { std::optional< LocatedAsset > get_asset( asset_type_t asset_type, std::optional< identifier_t > identifier, read_lock_proof ) const; + void restore_block_annotation_before_modification( universal_asset_id_t modified_asset_id, + BlockAnnotation &block_annotation, + int block_height_modified_at, + write_lock_proof wlp ); void cleanup_old_blocks_bookkeeping( int block_height, write_lock_proof ); }; diff --git a/src/spats/spark_asset.hpp b/src/spats/spark_asset.hpp index 69ae4a4155..47570d33f6 100644 --- a/src/spats/spark_asset.hpp +++ b/src/spats/spark_asset.hpp @@ -43,33 +43,45 @@ struct AssetNaming { nonempty_trimmed_uppercase_string symbol; std::string description; + AssetNaming( nonempty_trimmed_string n, nonempty_trimmed_uppercase_string s, std::string desc ) noexcept + : name( std::move( n ) ) + , symbol( std::move( s ) ) + , description( std::move( desc ) ) + {} + template < typename Stream > - void Serialize( Stream &os ) const + AssetNaming( deserialize_type d, Stream &is ) + : name( d, is ) + , symbol( d, is ) { - os << name.get() << symbol.get() << description; + is >> description; } template < typename Stream > - void Unserialize( Stream &is ) + void Serialize( Stream &os ) const { - std::string s; - is >> s; - name = std::move( s ); - is >> s; - symbol = std::move( s ); - is >> description; + os << name << symbol << description; } + + bool operator==( const AssetNaming &rhs ) const noexcept = default; }; using supply_amount_t = utils::scaled_amount<>; class SparkAssetBase { public: + // getters [[nodiscard]] asset_type_t asset_type() const noexcept { return asset_type_; } [[nodiscard]] const AssetNaming &naming() const noexcept { return asset_naming_; } [[nodiscard]] const std::string &metadata() const noexcept { return metadata_; } [[nodiscard]] const public_address_t &admin_public_address() const noexcept { return admin_public_address_; } + // setters + void naming( AssetNaming naming ) { asset_naming_ = std::move( naming ); } + void metadata( std::string metadata ) { metadata_ = std::move( metadata ); } + + bool operator==( const SparkAssetBase &rhs ) const noexcept = default; + protected: // not meant to be constructed/destroyed by itself - only objects of derived classes are meant to be created SparkAssetBase( asset_type_t asset_type, AssetNaming asset_naming, std::string metadata, public_address_t admin_public_address ) @@ -83,10 +95,10 @@ class SparkAssetBase { } template < typename Stream > - SparkAssetBase( deserialize_type, Stream &is ) - : asset_naming_( "x"s, "X"s, "desc"s ) // dummy values that will soon get overwritten + SparkAssetBase( deserialize_type d, Stream &is ) + : asset_naming_( d, is ) { - is >> asset_type_ >> asset_naming_ >> metadata_ >> admin_public_address_; + is >> asset_type_ >> metadata_ >> admin_public_address_; } ~SparkAssetBase() = default; @@ -94,7 +106,7 @@ class SparkAssetBase { template < typename Stream > void Serialize( Stream &os ) const { - os << asset_type_ << asset_naming_ << metadata_ << admin_public_address_; + os << asset_naming_ << asset_type_ << metadata_ << admin_public_address_; } private: @@ -144,7 +156,10 @@ class BasicSparkAsset : public SparkAssetBase { [[nodiscard]] supply_amount_t total_supply() const noexcept { return total_supply_; } [[nodiscard]] bool resupplyable() const noexcept { return resupplyable_; } + bool operator==( const BasicSparkAsset &rhs ) const noexcept = default; + explicit operator SparkAssetDisplayAttributes() const; + explicit operator universal_asset_id_t() const noexcept { return { asset_type(), identifier_t{} }; } private: supply_amount_t total_supply_; // Precision of the asset is included within this too @@ -180,7 +195,10 @@ class BasicSparkAsset< false > : public SparkAssetBase { [[nodiscard]] identifier_t identifier() const noexcept { return identifier_; } + bool operator==( const BasicSparkAsset &rhs ) const noexcept = default; + explicit operator SparkAssetDisplayAttributes() const; + explicit operator universal_asset_id_t() const noexcept { return { asset_type(), identifier() }; } private: identifier_t identifier_; diff --git a/src/spats/wallet.cpp b/src/spats/wallet.cpp index 375abfe24c..ff23639723 100644 --- a/src/spats/wallet.cpp +++ b/src/spats/wallet.cpp @@ -60,6 +60,15 @@ Scalar Wallet::compute_unregister_spark_asset_serialization_scalar( const Unregi return ret; } +Scalar Wallet::compute_modify_spark_asset_serialization_scalar( const AssetModificationBase &b, std::span< const unsigned char > modification_serialization_bytes ) +{ + spark::Hash hash( std::format( "spatsmodify_{}_from_{}", utils::to_underlying( b.asset_type() ), b.initiator_public_address() ) ); + hash.include( modification_serialization_bytes ); + auto ret = hash.finalize_scalar(); + LogPrintf( "Modify spark asset serialization scalar (hex): %s\n", ret.GetHex() ); + return ret; +} + Wallet::Wallet( CSparkWallet &spark_wallet ) noexcept : spark_wallet_( spark_wallet ) , registry_( spark::CSparkState::GetState()->GetSpatsManager().registry() ) @@ -153,6 +162,35 @@ CWalletTx Wallet::create_unregister_spark_asset_transaction( asset_type_t asset_ return tx; } +CWalletTx Wallet::create_modify_spark_asset_transaction( const SparkAsset &old_asset, const SparkAsset &new_asset, CAmount &standard_fee ) const +{ + const auto &admin_public_address = my_public_address_as_admin(); + const auto m = make_asset_modification( old_asset, new_asset, admin_public_address ); + const auto &b = get_base( m ); + CScript script; + script << OP_SPATSMODIFY; + assert( script.IsSpatsModify() ); + CDataStream serialized( SER_NETWORK, PROTOCOL_VERSION ); + serialized << ModifyAssetAction( m ); + // TODO instead of how it is being done now, put ownership proof into script default-constructed first, then compute ownership proof from the whole tx, and overwrite + // the ownership proof in the script then + const auto scalar_of_proof = compute_modify_spark_asset_serialization_scalar( b, serialized.as_bytes_span() ); + const spark::OwnershipProof proof = spark_wallet_.makeDefaultAddressOwnershipProof( scalar_of_proof ); + LogPrintf( "Ownership proof for modify spark asset (hex): %s\n", proof ); + CDataStream proof_serialized( SER_NETWORK, PROTOCOL_VERSION ); + proof_serialized << proof; + script.insert( script.end(), serialized.begin(), serialized.end() ); + assert( script.IsSpatsModify() ); + script.insert( script.end(), proof_serialized.begin(), proof_serialized.end() ); + assert( script.IsSpatsModify() ); + auto tx = spark_wallet_.CreateSparkSpendTransaction( { CRecipient( std::move( script ), {}, false, admin_public_address ) }, + {}, + standard_fee, + nullptr ); // may throw + assert( tx.tx->IsSpatsModify() ); + return tx; +} + void Wallet::notify_registry_changed() { // TODO diff --git a/src/spats/wallet.hpp b/src/spats/wallet.hpp index ab5cf6c320..4ef7a9bf0c 100644 --- a/src/spats/wallet.hpp +++ b/src/spats/wallet.hpp @@ -39,6 +39,7 @@ class Wallet { static Scalar compute_new_spark_asset_serialization_scalar( const SparkAssetBase &b, std::span< const unsigned char > asset_serialization_bytes ); static Scalar compute_unregister_spark_asset_serialization_scalar( const UnregisterAssetParameters &p, std::span< const unsigned char > unreg_asset_serialization_bytes ); + static Scalar compute_modify_spark_asset_serialization_scalar( const AssetModificationBase &b, std::span< const unsigned char > modification_serialization_bytes ); const std::string &my_public_address_as_admin() const; @@ -47,6 +48,7 @@ class Wallet { CAmount &new_asset_fee, const public_address_t &destination_public_address = {} ) const; CWalletTx create_unregister_spark_asset_transaction( asset_type_t asset_type, std::optional< identifier_t > identifier, CAmount &standard_fee ) const; + CWalletTx create_modify_spark_asset_transaction( const SparkAsset &old_asset, const SparkAsset &new_asset, CAmount &standard_fee ) const; void notify_registry_changed(); diff --git a/src/test/data/script_tests.json b/src/test/data/script_tests.json index f4401c570f..788f5bb166 100644 --- a/src/test/data/script_tests.json +++ b/src/test/data/script_tests.json @@ -258,7 +258,6 @@ ["0", "IF 0xce ELSE 1 ENDIF", "P2SH,STRICTENC", "OK"], ["0", "IF 0xcf ELSE 1 ENDIF", "P2SH,STRICTENC", "OK"], ["0", "IF 0xd0 ELSE 1 ENDIF", "P2SH,STRICTENC", "OK"], -["0", "IF 0xd6 ELSE 1 ENDIF", "P2SH,STRICTENC", "OK"], ["0", "IF 0xd7 ELSE 1 ENDIF", "P2SH,STRICTENC", "OK"], ["0", "IF 0xd8 ELSE 1 ENDIF", "P2SH,STRICTENC", "OK"], ["0", "IF 0xd9 ELSE 1 ENDIF", "P2SH,STRICTENC", "OK"], diff --git a/src/utils/constrained_value.hpp b/src/utils/constrained_value.hpp index 1f96142919..262d8a90f4 100644 --- a/src/utils/constrained_value.hpp +++ b/src/utils/constrained_value.hpp @@ -33,11 +33,30 @@ class constrained_value : detail::validity_ensurer< T, ValidityPredicate > { using base_type = detail::validity_ensurer< T, ValidityPredicate >; public: +#if 0 // TODO contemplate if/how to support this + constrained_value() + requires( std::is_default_constructible_v< T > && ValidityPredicate( T() ) ) + : base_type( T() ) + , value_() + {} +#endif + constrained_value( T &&value ) : base_type( value ) , value_( std::move( value ) ) {} + template < typename Stream > + constrained_value( deserialize_type, Stream &is ) + : constrained_value( deserialize_from( is ) ) + {} + + template < typename Stream > + void Serialize( Stream &os ) const + { + os << value_; + } + constrained_value &operator=( T &&value ) { base_type::ensure_validity( value ); @@ -50,11 +69,27 @@ class constrained_value : detail::validity_ensurer< T, ValidityPredicate > { // deliberately implicit operator const T &() const noexcept { return get(); } + friend bool operator==( const constrained_value &a, const constrained_value &b ) noexcept( noexcept( a.get() == b.get() ) ) + requires( std::equality_comparable< T > ) + { + return a.get() == b.get(); + } + private: T value_; -}; -// TODO operators like == if and when it is supported by T + template < typename Stream > + static T deserialize_from( Stream &is ) + { + if constexpr ( std::is_default_constructible_v< T > ) { + T t; + is >> t; + return t; + } + else + return T( deserialize, is ); + } +}; } // namespace utils diff --git a/src/utils/empty_class.hpp b/src/utils/empty_class.hpp new file mode 100644 index 0000000000..7cc07007d4 --- /dev/null +++ b/src/utils/empty_class.hpp @@ -0,0 +1,14 @@ +// +// Created by Gevorg Voskanyan +// + +#ifndef FIRO_EMPTY_CLASS_HPP_INCLUDED +#define FIRO_EMPTY_CLASS_HPP_INCLUDED + +namespace utils { + +class empty_class {}; + +} // namespace utils + +#endif // FIRO_EMPTY_CLASS_HPP_INCLUDED diff --git a/src/utils/scaled_amount.hpp b/src/utils/scaled_amount.hpp index c6400e467b..5b22701ca9 100644 --- a/src/utils/scaled_amount.hpp +++ b/src/utils/scaled_amount.hpp @@ -11,6 +11,7 @@ #include #include #include +#include // TODO remove #include @@ -79,25 +80,48 @@ class scaled_amount { explicit constexpr operator bool() const noexcept { return !!raw_amount_; } + bool operator==( scaled_amount const &rhs ) const noexcept + { + if ( precision() == rhs.precision() ) + return raw() == rhs.raw(); + try { + auto lhs = *this; + lhs.set_precision( rhs.precision() ); + return lhs.raw() == rhs.raw(); + } + catch ( ... ) { + return false; + } + } + private: // the true mathematical amount is raw_amount_ / 10^precision_ raw_amount_type raw_amount_{}; precision_type precision_{}; - // For now, not allowing precision to change after construction, hence this is private. - // If we ever do, then need to readjust the raw value such that as_double() would return the same value before & after. constexpr void set_precision( precision_type const precision ) { // though theoretically possible, choosing not to support here precisions which would equal or exceed the number of digits of the max raw value if ( precision >= std::numeric_limits< raw_amount_type >::digits10 ) throw std::invalid_argument( "precision is too high, not supported" ); // TODO add details + if ( precision_ == precision ) + return; + // TODO remove cout stuff + std::cout << "set_precision() raw_ = " << raw_amount_ << " precision_ " << precision_ << " precision " << precision << "\n"; + std::cout << "as_double() before = " << as_double() << "\n"; + // TODO overflow/underflow checks + if ( precision_ > precision ) + raw_amount_ *= math::integral_power( std::uintmax_t( 10 ), precision_ - precision ); + else + raw_amount_ /= math::integral_power( std::uintmax_t( 10 ), precision - precision_ ); precision_ = precision; + std::cout << "as_double() after = " << as_double() << "\n"; } constexpr auto decimal_factor() const noexcept { return math::integral_power( std::uintmax_t( 10 ), precision() ); } }; -// TODO comparison operators +// TODO more comparison operators template < typename CharT, typename Traits, typename RawAmountType > std::basic_ostream< CharT, Traits > &operator<<( std::basic_ostream< CharT, Traits > &os, const scaled_amount< RawAmountType > &amount ) diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 17e6692a99..cfc250341d 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -5970,6 +5970,39 @@ std::optional CWallet::UnregisterSparkAsset(spats::asset_type_t asset return wtx; } +std::optional CWallet::ModifySparkAsset(const spats::SparkAsset& old_asset, const spats::SparkAsset& new_asset, + const std::function& user_confirmation_callback) +{ + // create transaction + CAmount standard_fee; + auto wtx = sparkWallet->getSpatsWallet().create_modify_spark_asset_transaction(old_asset, new_asset, standard_fee); + + // give the user a chance to confirm/cancel + if (user_confirmation_callback && !user_confirmation_callback(standard_fee)) { + // TODO log cancellation by user + return {}; + } + + // commit + try { + CValidationState state; + CReserveKey reserveKey(this); + if (!CommitTransaction(wtx, reserveKey, g_connman.get(), state)) + throw std::runtime_error("Failed to commit modify spark asset transaction"); + } catch (const std::exception &) { + auto error = _( + "Error: The ModifySparkAsset transaction was rejected! This might happen e.g. if some of " + "the coins in your wallet were already spent, such as if you used " + "a copy of wallet.dat and coins were spent in the copy but not " + "marked as spent here." + ); + + std::throw_with_nested(std::runtime_error(error)); + } + + return wtx; +} + bool CWallet::LelantusToSpark(std::string& strFailReason) { std::list coins = GetAvailableLelantusCoins(); std::list sigmaCoins = GetAvailableCoins(); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 0a1c54afb6..74daf91e0d 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -1115,6 +1115,8 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface const std::function& user_confirmation_callback = {}); std::optional UnregisterSparkAsset(spats::asset_type_t asset_type, std::optional identifier, const std::function& user_confirmation_callback = {}); + std::optional ModifySparkAsset(const spats::SparkAsset& old_asset, const spats::SparkAsset& new_asset, + const std::function& user_confirmation_callback = {}); bool LelantusToSpark(std::string& strFailReason);