LevyTransferTransactionを作ってみる

※完成版作成しました

https://qiita.com/Toshi_ma/items/7bf994ca7a288be0efc7

:::note info
ジャガーさんにフィードバックをもらった
https://catnotes.xyz/symbol/q-a/toshi-mosaic-levy-plugin

とってもシンプルになりそう。また、この方法でプラグインを作成し、記事を書く。Cacheについて学ぶ必要があるので時間はかかりそう。（ジャガーさんがCacheについての記事を書いてくれるそう、優しすぎ）とは言え、この記事は軌跡ということでおいておきます。
:::
課題はいくつもあるけど雰囲気がLevyTransactionのたたき台が出来たので記事にしておく

# 今回のLevyTransactionについて
モザイク定義のときにLeviableフラグを用意し、trueにするとそのモザイクは通常のTransferでは送信できなくなります（オーナーであっても！（課題３））
その代わりに、MosaicLevyTransferTransactionV1を使うと、送信が可能になる。ただし、送信時にCurrencyMosaic(つまりXYM)を10000000μXYMがモザイクオーナーに支払われる。（課題２）というもの。

### 課題1: 
今回はmosaic配下に作成したがtransfer配下のほうが適切な気がする。
が、mosaic cacheを取ると、mosaicプラグイン側も色々とパスを調整しなければいけないのが、どうも気に食わない。あと、なぜか`./catapult.server`でエラーが出た。ビルドはこけておらず、observerの追加をコメントアウトすると問題ないのでそこがおそらくおかしい

### 課題2: 
とても大きな問題だけれども現在はLevyのモザイクIDとAmountがハードコーディングされている。当初MosicDefinitionでlevyMosaicを定義しようと思ったが、これを変更しようとするとかなり大幅な変更、Nemesisにまで影響を及ぼしかねないのでこれは駄目。
次に考えたのはLevyDefinitionTransactionのようなものを作成し、オーナーが定義したモザイクに対してさらにLevyを定義するようなこと。おそらくこれが正解だが、キャッシュの使い方がまだちゃんと分かってないので、今後の課題にしておく

### 課題3: 
なんとオーナーまでも送信時にLevyが発生している。これはちょっと頑張ればいけそうだけど疲れたのでそのうち

### 課題4: 
Mongoでエラーが正常に表示されない。ただのエラーコードになる。調べる。

### 課題5: 
これは課題というか色々とバリデーションが適当なのでバグだらけだし実践には使えない。とは言えたたき台としては悪くない感触

# MosaicDefinitionTransaction

## MosaicFlags
まずは、モザイクFlagにLeviableを追加する
``` /symbol/client/catapult/plugins/txes/mosaic/src/model/MosaicFlags.h
		/// Mosaic supports revocation of tokens by creator.
		Revokable = 0x08,

        + /// Mosaic supports levy.
		+ Leviable = 0x10,

        /// All flags.
		- All = 0x0F
		+ All = 0x1F
```

## LeviedMosaicValidator

BalanceTransferNotification通知時にLeviableフラグが立ってるモザイクは送信できないようにする。

:::note info
<b>ジャガーさんに教えてもらった後のコメント</b>
このバリデータは不要です。通常のTransferTransactionに追加でLevyを徴収するので。
:::

``` /symbol/client/catapult/plugins/txes/mosaic/src/validators/LeviedMosaicValidator.cpp
#include "Validators.h"
#include "ActiveMosaicView.h"
#include "src/cache/MosaicCache.h"
#include "catapult/cache/ReadOnlyCatapultCache.h"
#include "catapult/cache_core/AccountStateCache.h"
#include "catapult/validators/ValidatorContext.h"

namespace catapult { namespace validators {

	using Notification = model::BalanceTransferNotification;

	DEFINE_STATEFUL_VALIDATOR(LeviedMosaic, [](const Notification& notification, const ValidatorContext& context) {
		// モザイクIDが存在するか
		ActiveMosaicView::FindIterator mosaicIter;
		ActiveMosaicView activeMosaicView(context.Cache);
		auto result = activeMosaicView.tryGet(context.Resolvers.resolve(notification.MosaicId), context.Height, mosaicIter);
		if (!IsValidationResultSuccess(result))
			return result;

		// モザイクがLeviableか
		const auto& mosaicEntry = mosaicIter.get();
		if (mosaicEntry.definition().properties().is(model::MosaicFlags::Leviable))
			return Failure_Mosaic_Levied;

		return ValidationResult::Success;
	})
}}
```

``` /symbol/client/catapult/plugins/txes/mosaic/src/validators/Validators.h
	/// Validator that applies to all balance transfer notifications and validates that:
	/// - the recipient does not exceed the maximum number of mosaics (\a maxMosaics) an account is allowed to own
	DECLARE_STATEFUL_VALIDATOR(MaxMosaicsBalanceTransfer, model::BalanceTransferNotification)(uint16_t maxMosaics);

	+ // Validator that applies to all balance transfer notifications and validates that:
	+ DECLARE_STATEFUL_VALIDATOR(LeviedMosaic, model::BalanceTransferNotification)();
```

``` /symbol/client/catapult/plugins/txes/mosaic/src/validators/Results.h
	/// Validation failed because the mosaic has at least one required property flag unset.
	DEFINE_MOSAIC_RESULT(Required_Property_Flag_Unset, 114);

	+ /// Validation failed because the mosaic has a levy.
	+ DEFINE_MOSAIC_RESULT(Levied, 115);
```

``` /symbol/client/catapult/plugins/txes/mosaic/src/plugins/MosaicPlugin.cpp
+ #include "MosaicLevyTransferTransactionPlugin.h"
...
    .add(validators::CreateMaxMosaicsSupplyChangeValidator(config.MaxMosaicsPerAccount))
    // note that the following validator depends on RequiredMosaicValidator
    - .add(validators::CreateMosaicSupplyChangeAllowedValidator(networkConfig.MaxMosaicAtomicUnits));
    + .add(validators::CreateMosaicSupplyChangeAllowedValidator(networkConfig.MaxMosaicAtomicUnits))
    + // LeviedMosaicValidator is added in RegisterSubsystem
    + .add(validators::CreateLeviedMosaicValidator());
});
```
これで通常のTransferTransactionのときにモザイクにLeviableフラグが立っていたら送信不可になる。

## MosaicLevyTransferTransaction

:::note info
<b>ジャガーさんに教えてもらった後のコメント</b>
このTransactionも不要です。その代わりに、`MosaicLevyChangeTransaction`を作成し、Levyの設定を可変で行うトランザクションが必要になる。
:::

### model作成
``` /symbol/client/catapult/plugins/txes/mosaic/src/model/MosaicLevyTransferTransaction.h
#pragma once
#include "MosaicConstants.h"
#include "MosaicEntityType.h"
#include "MosaicProperties.h"
#include "catapult/model/Transaction.h"

namespace catapult { namespace model {

#pragma pack(push, 1)

	/// Binary layout for a mosaic definition transaction body.
	template<typename THeader>
	struct MosaicLevyTransferTransactionBody : public THeader {
	private:
		using TransactionType = MosaicLevyTransferTransactionBody<THeader>;

	public:
		DEFINE_TRANSACTION_CONSTANTS(Entity_Type_Mosaic_Levy_Transfer, 1)

	public:
		/// Recipient.
		UnresolvedAddress RecipientAddress;

		/// Mosaic.
		UnresolvedMosaic Mosaic;

        // 8byte のreserved アドレスが24byteだから。これが必要なのかはまだ分かってない
        uint8_t MosaicLevyTransferTransactionBody_Reserved;

	public:
		/// Calculates the real size of mosaic definition \a transaction.
		static constexpr uint64_t CalculateRealSize(const TransactionType&) noexcept {
			return sizeof(TransactionType);
		}
	};

	DEFINE_EMBEDDABLE_TRANSACTION(MosaicLevyTransfer)

#pragma pack(pop)
}}
```
ここでは受信者とモザイクの指定のみ、とりあえずメッセージは無し

### MosaicNotifications
続いて通知を作成

``` /symbol/client/catapult/plugins/txes/mosaic/src/model/MosaicNotifications.h
	/// Mosaic rental fee has been sent.
	DEFINE_MOSAIC_NOTIFICATION(Rental_Fee, 0x0005, Observer);

	+ /// Mosaic levy transfer has been sent.
	+ DEFINE_MOSAIC_NOTIFICATION(Levy_Transfer, 0x0006, All);
...
    // 以下最下部に追加
    struct MosaicLevyTransferNotification : public Notification {
    	public:
    		/// Matching notification type.
    		static constexpr auto Notification_Type = Mosaic_Levy_Transfer_Notification;
    
    	public:
    		MosaicLevyTransferNotification(
    			const ResolvableAddress& sender,
    			const ResolvableAddress& recipient,
    			UnresolvedMosaic mosaic)
    			: Notification(Notification_Type, sizeof(MosaicLevyTransferNotification))
    			, Sender(sender)
    			, Recipient(recipient)
    			, Mosaic(mosaic)
    		{}
    
    	public:
    		/// Sender.
    		ResolvableAddress Sender;
    
    		/// Recipient.
    		ResolvableAddress Recipient;
    
    		/// Mosaic.
    		UnresolvedMosaic Mosaic;
    	};
```
### MosaicLevyTransferTransactionPlugin
プラグイン作成

``` /symbol/client/catapult/plugins/txes/mosaic/src/plugins/MosaicLevyTransferTransactionPlugin.h
#include "MosaicLevyTransferTransactionPlugin.h"
#include "src/model/MosaicNotifications.h"
#include "src/model/MosaicLevyTransferTransaction.h"
#include "catapult/model/NotificationSubscriber.h"
#include "catapult/model/TransactionPluginFactory.h"

using namespace catapult::model;

namespace catapult { namespace plugins {

	namespace {
		template<typename TTransaction>
		void Publish(const TTransaction& transaction, const PublishContext& context, NotificationSubscriber& sub) {
			auto padding = transaction.MosaicLevyTransferTransactionBody_Reserved;
			sub.notify(InternalPaddingNotification(padding));
			sub.notify(MosaicLevyTransferNotification(context.SignerAddress, transaction.RecipientAddress, transaction.Mosaic));
		}
	}

	DEFINE_TRANSACTION_PLUGIN_FACTORY(MosaicLevyTransfer, Default, Publish)
}}
```

### BalanceLevyTransferValidator
:::note info
<b>ジャガーさんに教えてもらった後のコメント</b>
これは`BalanceTransferNotification`にフックしLeviableなモザイクの場合にLevyモザイク残高があるかどうかだけのチェックになるはず。（そもそもLeviableフラグが必要か？
:::
適切な残高があるかどうか、Levyで支払うモザイクと送信したいモザイクの両方
``` /symbol/client/catapult/plugins/txes/mosaic/src/validators/Validators.h
+ /// Validator that applies to all balance transfer notifications and validates that:
+ DECLARE_STATEFUL_VALIDATOR(BalanceLevyTransfer, model::MosaicLevyTransferNotification)();
```

``` /symbol/client/catapult/plugins/txes/mosaic/src/validators/Results.h
+ /// Validation failed because the mosaic has insufficient balance.
+ DEFINE_MOSAIC_RESULT(Insufficient_Balance, 116);
```

``` /symbol/client/catapult/plugins/txes/mosaic/src/validators/BalanceLevyTransferValidator.cpp
#include "Validators.h"
#include "ActiveMosaicView.h"
#include "catapult/cache_core/AccountStateCache.h"
#include "catapult/state/CatapultState.h"
#include "catapult/validators/ValidatorContext.h"

namespace catapult { namespace validators {

	using Notification = model::MosaicLevyTransferNotification;

	bool FindAccountBalance(const cache::ReadOnlyAccountStateCache& cache, const Address& address, MosaicId mosaicId, Amount& amount) {
		auto accountStateAddressIter = cache.find(address);
		if (accountStateAddressIter.tryGet()) {
			amount = accountStateAddressIter.get().Balances.get(mosaicId);
			return true;
		}

		return false;
	}

	DEFINE_STATEFUL_VALIDATOR(BalanceLevyTransfer, [](const Notification& notification, const ValidatorContext& context) {
		const auto& cache = context.Cache.sub<cache::AccountStateCache>();

		auto effectiveMosaicAmount = notification.Mosaic.Amount;
		Amount mosaicAmount;
		auto mosaicId = context.Resolvers.resolve(notification.Mosaic.MosaicId);
		bool hasMosaic = FindAccountBalance(cache, notification.Sender.resolved(context.Resolvers), mosaicId, mosaicAmount);

		auto leviedMosaicEffectiveAmount = Amount(1000000);
        // currency mosaic id
        auto leviedMosaicId = MosaicId(0x1154AC64240B225F);
		Amount leviedMosaicAmount;
		bool hasLeviedMosaic = FindAccountBalance(cache, notification.Sender.resolved(context.Resolvers), leviedMosaicId, leviedMosaicAmount);

		if(hasMosaic && hasLeviedMosaic && mosaicAmount >= effectiveMosaicAmount && leviedMosaicAmount >= leviedMosaicEffectiveAmount) {
			return ValidationResult::Success;
		}
		
		return Failure_Mosaic_Insufficient_Balance;
	})
}}
```

### MosaicLevyTransferObserver
:::note info
<b>ジャガーさんに教えてもらった後のコメント</b>
これも`BalanceTransferNotification`にフックしLeviableなモザイクの場合にLevyを徴収するオブザーバーにする
:::
ここで送信の処理、今のところIDとAmountはそのままコーディングしてる
``` /symbol/client/catapult/plugins/txes/mosaic/src/observers/Observers.h
+ /// Observes changes triggered by mosaic levy transfer notifications and:
+ DECLARE_OBSERVER(MosaicLevyTransfer, model::MosaicLevyTransferNotification)();
```

``` /symbol/client/catapult/plugins/txes/mosaic/src/observers/MosaicLevyTransferObserver.cpp
#include "Observers.h"
#include "catapult/cache_core/AccountStateCache.h"

namespace catapult { namespace observers {

	namespace {
		void Transfer(state::AccountState& debitState, state::AccountState& creditState, MosaicId mosaicId, Amount amount) {
			debitState.Balances.debit(mosaicId, amount);
			creditState.Balances.credit(mosaicId, amount);
		}
	}

	DEFINE_OBSERVER(MosaicLevyTransfer, model::MosaicLevyTransferNotification, [](
			const model::MosaicLevyTransferNotification& notification,
			const ObserverContext& context) {
		auto& cache = context.Cache.sub<cache::AccountStateCache>();
		auto senderIter = cache.find(notification.Sender.resolved(context.Resolvers));
		auto recipientIter = cache.find(notification.Recipient.resolved(context.Resolvers));

		auto& senderState = senderIter.get();
		auto& recipientState = recipientIter.get();

		auto effectiveAmount = notification.Mosaic.Amount;
		auto mosaicId = context.Resolvers.resolve(notification.Mosaic.MosaicId);

		const auto& mosaicCache = context.Cache.sub<cache::MosaicCache>();
		auto mosaicIter = mosaicCache.find(mosaicId);
		auto ownerIter = cache.find(mosaicIter.get().definition().ownerAddress());
		auto& ownerState = ownerIter.get();
		// auto properties = iter.get().definition().properties();
		// auto leviedMosaic = properties.leviedMosaic();
		// auto leviedMosaicEffectiveAmount = leviedMosaic.Amount;
		// auto leviedMosaicId = leviedMosaic.MosaicId;

		auto leviedMosaicEffectiveAmount = Amount(10000000);
		auto leviedMosaicId = MosaicId(0x1154AC64240B225F);

		if (NotifyMode::Commit == context.Mode) {
			Transfer(senderState, recipientState, mosaicId, effectiveAmount);
			Transfer(senderState, ownerState, leviedMosaicId, leviedMosaicEffectiveAmount);
		}
		else {
			Transfer(recipientState, senderState, mosaicId, effectiveAmount);
			Transfer(ownerState, senderState, leviedMosaicId, leviedMosaicEffectiveAmount);
		}
	})
}}
```

### 登録
``` /symbol/client/catapult/plugins/txes/mosaic/src/plugins/MosaicPlugin.cpp
manager.addTransactionSupport(CreateMosaicSupplyChangeTransactionPlugin());
- manager.addTransactionSupport(CreateMosaicSupplyRevocationTransactionPlugin(
        model::GetNemesisSignerAddress(manager.config().Network)));
+ manager.addTransactionSupport(CreateMosaicSupplyRevocationTransactionPlugin(
        model::GetNemesisSignerAddress(manager.config().Network)))
// MosiacLevyTransferTransactionPlugin is added in RegisterSubsystem
+ manager.addTransactionSupport(CreateMosaicLevyTransferTransactionPlugin());
...
// note that the following validator depends on RequiredMosaicValidator
.add(validators::CreateMosaicSupplyChangeAllowedValidator(networkConfig.MaxMosaicAtomicUnits))
// LeviedMosaicValidator is added in RegisterSubsystem
.add(validators::CreateLeviedMosaicValidator()) // さっき登録したやつ
+ // BalanceTransferValidator is added in RegisterSubsystem
+ .add(validators::CreateBalanceLevyTransferValidator())
...
- .add(observers::CreateCacheBlockTouchObserver<cache::MosaicCache>("Mosaic", 
expiryReceiptType));
+ .add(observers::CreateCacheBlockTouchObserver<cache::MosaicCache>("Mosaic", expiryReceiptType))
+ // BalanceTransferObserver is added in RegisterSubsystem
+ .add(observers::CreateBalanceLevyTransferObserver());
```

### Mongo
``` /symbol/client/catapult/extensions/mongo/plugins/mosaic/src/MongoMosaicPlugin.cpp
manager.addTransactionSupport(mongo::plugins::CreateMosaicSupplyRevocationTransactionMongoPlugin());
+ manager.addTransactionSupport(mongo::plugins::CreateMosaicLevyTransferTransactionMongoPlugin());
```

``` /symbol/client/catapult/extensions/mongo/plugins/mosaic/src/MosaicLevyTransferMapper.h
#pragma once
#include "mongo/src/MongoTransactionPlugin.h"

namespace catapult { namespace mongo { namespace plugins {

	/// Creates a mongo mosaic levy transfer transaction plugin.
	PLUGIN_API
	std::unique_ptr<MongoTransactionPlugin> CreateMosaicLevyTransferTransactionMongoPlugin();
}}}
```

``` /symbol/client/catapult/extensions/mongo/plugins/mosaic/src/MosaicLevyTransferMapper.cpp
#include "MosaicLevyTransferMapper.h"
#include "mongo/src/MongoTransactionPluginFactory.h"
#include "mongo/src/mappers/MapperUtils.h"
#include "plugins/txes/mosaic/src/model/MosaicLevyTransferTransaction.h"

using namespace catapult::mongo::mappers;

namespace catapult { namespace mongo { namespace plugins {

	namespace {
		template<typename TTransaction>
		void StreamTransaction(bson_stream::document& builder, const TTransaction& transaction) {
			builder
					<< "recipientAddress" << ToBinary(transaction.RecipientAddress);
					<< "mosaicId" << ToInt64(transaction.Mosaic.MosaicId)
					<< "amount" << ToInt64(transaction.Mosaic.Amount)
		}
	}

	DEFINE_MONGO_TRANSACTION_PLUGIN_FACTORY(MosaicLevyTransfer, StreamTransaction)
}}}
```

### SDK
``` catbuffer/schemas/symbol/all_transactions.cats
+ import "mosaic/mosaic_levy_transfer.cats"
```

``` catbuffer/schemas/symbol/transaction_type.cats
+ MOSAIC_LEVY_TRANSFER = 0x444D
```

``` catbuffer/schemas/symbol/mosaic/mosaic_types.cats
# Mosaic supports revocation of tokens by the mosaic creator.
REVOKABLE = 0x08

+ LEVIABLE = 0x10
```

``` catbuffer/schemas/symbol/mosaic/mosaic_levy_transfer.cats
import "mosaic/mosaic_types.cats"
import "transaction.cats"

# Shared content between MosaicLevyTransferTransaction and Embedded MosaicLevyTransferTransaction.
inline struct MosaicLevyTransferTransactionBody
	recipient_address = UnresolvedAddress

	mosaic = UnresolvedMosaic
	
	# reserved padding to align mosaics on 8-byte boundary
    mosaic_levy_transfer_transaction_body_reserved = make_reserved(uint8, 0)

# Create a new  [mosaic](/concepts/mosaic.html) (V1, latest).
struct MosaicLevyTransferTransactionV1
	TRANSACTION_VERSION = make_const(uint8, 1)
	TRANSACTION_TYPE = make_const(TransactionType, MOSAIC_LEVY_TRANSFER)

	inline Transaction
	inline MosaicLevyTransferTransactionBody

# Embedded version of MosaicLevyTransferTransaction (V1, latest).
struct EmbeddedMosaicLevyTransferTransactionV1
	TRANSACTION_VERSION = make_const(uint8, 1)
	TRANSACTION_TYPE = make_const(TransactionType, MOSAIC_LEVY_TRANSFER)

	inline EmbeddedTransaction
	inline MosaicLevyTransferTransactionBody
```

### SDKビルド
```
./scripts/run_catbuffer_generator.sh
```

# おわり
ほぼコードだけですが、、、キャッシュをマスターするともっと色々できそうな気がするのでがんばろう

:::note info
ジャガーさんにフィードバックをもらった
https://catnotes.xyz/symbol/q-a/toshi-mosaic-levy-plugin

とってもシンプルになりそう。また、この方法でプラグインを作成し、記事を書く。Cacheについて学ぶ必要があるので時間はかかりそう。（ジャガーさんがCacheについての記事を書いてくれるそう、優しすぎ）とは言え、この記事は軌跡ということでおいておきます。
:::

※完成版作成しました

https://qiita.com/Toshi_ma/items/7bf994ca7a288be0efc7