SymbolにLevyを実装する - 完成版 -

前回以下のように実装しました。が、これではLevy額をハードコーディングしなければいけませんでした。

https://qiita.com/Toshi_ma/items/d22d7664c0bd04181b6c

これを解消するために今回はcacheを用いてみました。

:::note info
ジャガーさんにフィードバックをもらい
https://catnotes.xyz/symbol/q-a/toshi-mosaic-levy-plugin-2
Recieptも使えるようになりました。MongoやRESTもいじってるので、おそらくほぼ完璧に活用できるはずです。
:::

# 今回のLevyの仕様

モザイク定義は通常通り行います。その後、`MosaicLevyChangeTransaction`によってLevyMosaicIdとLevy額を決定します。

~~ただし徴収できるモザイクは`CurrencyMosaic`(いわゆるXYM)のみです。これは将来的に変更可能ではありますが、考えることが増えるのでやめました。（複数Levyを設定やIncrease, Decreaseが面倒、徴収という意味では貨幣価値があるもので良いだろうという判断）~~


TransferTransactionで送信する際に送信するモザイクが`MosaicLevyChangeTransaction`によってLevyMosaicIdとLevy額が設定されていれば、自動で設定した額を徴収しモザイクオーナーに送信します。

# 注意点
~~今のところ、外側からはどのモザイクがLevy設定されているか分かりません。つまり知らないうちに徴収されるカオスなトランザクションになっています。これはRESTをいじれば可視化は可能です。ただ、いずれにせよ実際に送金する際に徴収額が変更されている可能性もあります。なので本番使用には向かないでしょう。
これを改善する方法はいくつか考えられますが、主題とそれ長くなるので今回はスルーします。~~

RESTからmoasicIdを指定するとlevyMoasicIdとlevy額が分かります。事前に設定されているか確認は必要になります。

:::note alert
MosaicLevyChangeTransactionによりLevy額が増加された場合、その増加後の額が徴収されるので本番では使えない可能性が高いです。例えば、もし同一ブロックで`MosaicLevyChangeTransaction`と該当モザイクを含む`TransferTransaction`があった場合に前者が先に処理されると増加後の額が徴収されます。
:::

<details><summary>興味ある人向け、cacheの話を少しだけ</summary>

数日前よりcacheについて研究を始めました。一つだけ記事にしましたが、この続きは言語化するのがなかなか大変で書けそうにありません。

https://qiita.com/Toshi_ma/items/bfd02b16d7c3624a5186

cacheは基本的にはプラグインフォルダ（ファシリティコード）と1対1になっているように見受けられます。つまりMosaicフォルダならMosaicCacheがひとつ。通常MosaicCacheはモザイク定義に関する物です。

ただし、のちほど新たに追加することも可能です。今回のlevyはそのようにしています。前者のように新たにプラグインを作成する場合は、いくつかの構造体やクラスを作成する必要があります。

こちらに関してはジャガーさんが記事にしてくれたので必読

https://catnotes.xyz/symbol/architecture/understanding-caches#cache-container

今回はlevyMosaicIdとlevyを追加しそれを変更するようにしました。

</details>

# MosaicEntry

ここは、何をcacheに残すのかを書かれていて今回はlevyMosaiciIdやlevy額や増減の関数を追加することにしました。

``` /symbol/client/catapult/plugins/txes/mosaic/src/state/MosaicEntry.h
...
// region LeviedMosaicMixin
class MosaicEntryLevyMixin {
public:

    bool isLevied() const;

    /// Gets the mosaic levy mosaic id.
	UnresolvedMosaicId levyMosaicId() const;

    /// Sets the mosaic levy mosaic id to \a levyMosaicId.
	void setLevyMosaicId (const UnresolvedMosaicId& levyMosaicId);

    /// Gets the mosaic supply.
    Amount levy() const;

public:
    /// Increases the supply by \a delta.
    void increaseLevy(Amount delta);

    /// Decreases the supply by \a delta.
    void decreaseLevy(Amount delta);

private:
    Amount m_levy;
    UnresolvedMosaicId m_levyMosaicId;
    bool m_isLevied;
};
// endregion
...
class PLUGIN_API_DEPENDENCY MosaicEntry : public MosaicEntrySupplyMixin, public MosaicEntryLevyMixin {
```

``` /symbol/client/catapult/plugins/txes/mosaic/src/state/MosaicEntry.cpp
// region MosaicEntryLevyMixin

bool MosaicEntryLevyMixin::isLevied() const {
    return Amount(0) != m_levy;
}

Amount MosaicEntryLevyMixin::levy() const {
    return m_levy;
}

UnresolvedMosaicId MosaicEntryLevyMixin::levyMosaicId() const {
    return m_levyMosaicId;
}

void MosaicEntryLevyMixin::setLevyMosaicId(const UnresolvedMosaicId& levyMosaicId) {
    m_levyMosaicId = levyMosaicId;
}

void MosaicEntryLevyMixin::increaseLevy(Amount delta) {
    if (!utils::CheckedAdd(m_levy, delta))
        CATAPULT_THROW_INVALID_ARGUMENT_2("cannot increase mosaic levy above max (levy, delta)", m_levy, delta);
}

void MosaicEntryLevyMixin::decreaseLevy(Amount delta) {
    if (delta > m_levy)
        CATAPULT_THROW_INVALID_ARGUMENT_2("cannot decrease mosaic levy below zero (levy, delta)", m_levy, delta);

    m_levy = m_levy - delta;
}

// endregion
```

``` /symbol/client/catapult/extensions/mongo/plugins/mosaic/src/mappers/MosaicEntryMapper.cpp
<< "supply" << ToInt64(mosaicEntry.supply())
// 追加
<< "levyMosaicId" << ToInt64(mosaicEntry.levyMosaicId())
<< "levy" << ToInt64(mosaicEntry.levy())
// ここまで
<< "startHeight" << ToInt64(definition.startHeight())
```

# MosaicEntrySerializer
ここではcacheへの読み書きを行います。うまく表現できないのですがトランザクション等でコンテナに保存されたデータをノード本体へ記述する箇所です。

変更点が多いので全て貼り付けます。

``` /symbol/client/catapult/plugins/txes/mosaic/src/state/MosaicEntrySerializer.cpp
#include "MosaicEntrySerializer.h"
#include "catapult/io/PodIoUtils.h"

namespace catapult { namespace state {

	namespace {
		void SaveProperties(io::OutputStream& output, const MosaicEntry& entry) {
			const auto& properties = entry.definition().properties();
			io::Write8(output, static_cast<uint8_t>((entry.isLevied() ? 0x80 : 0x00) | utils::to_underlying_type(properties.flags())));
			io::Write8(output, properties.divisibility());
			io::Write(output, properties.duration());
			if (entry.isLevied()) {
				io::Write(output, entry.levyMosaicId());
				io::Write(output, entry.levy());
			}
		}

		void SaveDefinition(io::OutputStream& output, const MosaicEntry& entry) {
			auto definition = entry.definition();
			io::Write(output, definition.startHeight());
			output.write(definition.ownerAddress());
			io::Write32(output, definition.revision());
			SaveProperties(output, entry);
		}
	}

	void MosaicEntrySerializer::Save(const MosaicEntry& entry, io::OutputStream& output) {
		io::Write(output, entry.mosaicId());
		io::Write(output, entry.supply());
		SaveDefinition(output, entry);
	}

	namespace {
		model::MosaicProperties LoadProperties(io::InputStream& input, bool& hasLevy) {
			auto flags = static_cast<model::MosaicFlags>(io::Read8(input));
			auto divisibility = io::Read8(input);
			auto duration = io::Read<BlockDuration>(input);
			hasLevy = static_cast<int>(flags) & 0x80;
			auto flagsInt = static_cast<int>(flags);
			flagsInt &= 0x7F;
			flags = static_cast<model::MosaicFlags>(flagsInt);
			return model::MosaicProperties(flags, divisibility, duration);
		}

		MosaicDefinition LoadDefinition(io::InputStream& input, bool& hasLevy) {
			Address owner;
			auto height = io::Read<Height>(input);
			input.read(owner);
			auto revision = io::Read32(input);

			auto properties = LoadProperties(input, hasLevy);
			return MosaicDefinition(height, owner, revision, properties);
		}
	}

	MosaicEntry MosaicEntrySerializer::Load(io::InputStream& input) {
		auto mosaicId = io::Read<MosaicId>(input);
		auto supply = io::Read<Amount>(input);
		bool hasLevy;
		auto definition = LoadDefinition(input, hasLevy);

		auto entry = MosaicEntry(mosaicId, definition);
		entry.increaseSupply(supply);

		if (hasLevy) {
			auto levyMosaicId = io::Read<UnresolvedMosaicId>(input);
			auto levy = io::Read<Amount>(input);
			entry.setLevyMosaicId(levyMosaicId);
			entry.increaseLevy(levy);
		}
		return entry;
	}
}}
```

~~もしかしたらもっとうまく書けるのかもしれませんが、`MosaicLevyChangeTransaction`が設定されたモザイクはisLeviedフラグをtrueにしているので、（後述）その場合のみ読み書きをします。この判定が無ければbrokerがエラーを吐きます。~~

ジャガーさんにもらったフィードバックをもとにかなりいい感じになりました。ビットでフラグ判定などの理解が深まりました。本当にありがたいです。

https://catnotes.xyz/symbol/q-a/toshi-mosaic-levy-plugin-2

# MosaicLevyChangeTransaction
``` /symbol/client/catapult/plugins/txes/mosaic/src/model/MosaicLevyChangeTransaction.h
#pragma once
#include "MosaicEntityType.h"
#include "MosaicTypes.h"
#include "catapult/model/Transaction.h"

namespace catapult { namespace model {

#pragma pack(push, 1)

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

public:
    DEFINE_TRANSACTION_CONSTANTS(Entity_Type_Mosaic_Levy_Change, 1)

public:
    /// Affected mosaic identifier.
    UnresolvedMosaicId MosaicId;

    /// Levy moasic identifier.
	UnresolvedMosaicId LevyMosaicId;

    /// Change amount.
    Amount Delta;

    /// Levy change action.
    MosaicLevyChangeAction Action;

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

DEFINE_EMBEDDABLE_TRANSACTION(MosaicLevyChange)

#pragma pack(pop)
}}
```
MosaicLevyChangeTransactionの構造です。
Levyを設定するモザイクID、徴収するモザイクID、増減を判定するActionとその量Deltaです。

``` /symbol/client/catapult/plugins/txes/mosaic/src/model/MosaicTypes.h
/// Mosaic levy change actions.
enum class MosaicLevyChangeAction : uint8_t {
    /// Decreases the levy.
    Decrease,

    /// Increases the levy.
    Increase
};
```
`MosaicSupplyChangeAction`を使っても良かったのですが名前的に気持ち悪いので新たに専用のenumを作成しました。

``` /symbol/client/catapult/plugins/txes/mosaic/src/model/MosaicEntityType.h
/// Mosaic levy change transaction.
DEFINE_TRANSACTION_TYPE(Mosaic, Mosaic_Levy_Change, 0x4);
```
modelにMosaicLevyChangeTransactionを定義します。

# MosaicLevyChangeNotification
新たに通知を作成します。通知する内容は先程定義したもの＆オーナー（トランザクションの署名者）アドレスです。
通知先は、オブザーバーとバリデータの両方なのでALLです。

``` /symbol/client/catapult/plugins/txes/mosaic/src/model/MosaicNotifications.h
...
/// Mosaic levy was changed.
DEFINE_MOSAIC_NOTIFICATION(Levy_Change, 0x0006, All);
...
// region MosaicLevyChangeNotification
struct MosaicLevyChangeNotification : public Notification {
    public:
        /// Matching notification type.
        static constexpr auto Notification_Type = Mosaic_Levy_Change_Notification;

    public:
    /// Creates a notification around \a owner, \a mosaicId, \a action and \a delta.
    MosaicLevyChangeNotification(const Address& owner, UnresolvedMosaicId mosaicId, UnresolvedMosaicId levyMosaicId, MosaicLevyChangeAction action, Amount delta)
            : Notification(Notification_Type, sizeof(MosaicLevyChangeNotification))
            , Owner(owner)
            , MosaicId(mosaicId)
            , LevyMosaicId(levyMosaicId)
            , Action(action)
            , Delta(delta)
    {}

public:
    /// Mosaic owner.
    Address Owner;

    /// Id of the affected mosaic.
    UnresolvedMosaicId MosaicId;

    /// Id of the levy mosaic.
	UnresolvedMosaicId LevyMosaicId;

    /// Supply change action.
    MosaicLevyChangeAction Action;

    /// Amount of the change.
    Amount Delta;
};
// endregion
```

# MosaicLevyChangeTransactionPlugin
``` /symbol/client/catapult/plugins/txes/mosaic/src/plugins/MosaicLevyChangeTransactionPlugin.h
#pragma once
#include "catapult/plugins.h"
#include <memory>

namespace catapult { namespace model { class TransactionPlugin; } }

namespace catapult { namespace plugins {
	/// Creates a mosaic levy change transaction plugin.
	PLUGIN_API
	std::unique_ptr<model::TransactionPlugin> CreateMosaicLevyChangeTransactionPlugin();
}}
```

``` /symbol/client/catapult/plugins/txes/mosaic/src/plugins/MosaicLevyChangeTransactionPlugin.cpp
#include "MosaicLevyChangeTransactionPlugin.h"
#include "src/model/MosaicNotifications.h"
#include "src/model/MosaicLevyChangeTransaction.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) {
			sub.notify(MosaicRequiredNotification(context.SignerAddress, transaction.MosaicId));
			sub.notify(MosaicLevyChangeNotification(context.SignerAddress, transaction.MosaicId, transaction.LevyMosaicId, transaction.Action, transaction.Delta));
		}
	}

	DEFINE_TRANSACTION_PLUGIN_FACTORY(MosaicLevyChange, Default, Publish)
}}
```
`MosaicLevyChangeTransactionPlugin`の本体です。ここではMosaicRequiredNotificationとMosaicLevyChangeNotificationを通知します。前者は既存のものでMosaicオーナーかどうかの判定などを行います。
後者は先程作成したものです。

# MosaicLevyChangeValidator
``` /symbol/client/catapult/plugins/txes/mosaic/src/validators/MosaicLevyChangeValidator.cpp
#include "Validators.h"
#include "src/cache/MosaicCache.h"

namespace catapult { namespace validators {

	using Notification = model::MosaicLevyChangeNotification;

	namespace {
		constexpr bool IsValidAction(model::MosaicLevyChangeAction action) {
			return action <= model::MosaicLevyChangeAction::Increase;
		}
	}

	DECLARE_STATEFUL_VALIDATOR(MosaicLevyChange, Notification)() {
		return MAKE_STATEFUL_VALIDATOR(MosaicLevyChange, [](
				const Notification& notification,
				const ValidatorContext& context) {
			if (!IsValidAction(notification.Action))
			return Failure_Mosaic_Invalid_Levy_Change_Action;

			if(Amount() == notification.Delta)
				return Failure_Mosaic_Invalid_Levy_Change_Amount;

			const auto& mosaicCache = context.Cache.sub<cache::MosaicCache>();
			
			auto mosaicId = context.Resolvers.resolve(notification.MosaicId);
			auto mosaicIter = mosaicCache.find(mosaicId);
			if(mosaicIter.get().isLevied() == false)
				return ValidationResult::Success;

			auto newLevyMosaicId = context.Resolvers.resolve(notification.LevyMosaicId);
			auto previousMosaicId = context.Resolvers.resolve(mosaicIter.get().levyMosaicId());

			if(newLevyMosaicId != previousMosaicId)
				return Failure_Mosaic_Levy_Change_Not_Allowed;

			return ValidationResult::Success;
		});
	}
}}
```
このバリデータではまず、SupplyChangeと同じようにActionやDeltaが適切かどうか判定しています。
次にisLevied()によりlevyが設定されているか確認し設定されていなければSuccessを返します
設定されている場合でもLevyMosaicと違うモザイクを設定する場合はエラーを返します。つまりLevyMosaicIdを変更したい場合は必ずLevyをゼロにしておく必要があります。なお、ゼロにした時点でオブザーバがLevyMosaicIDを空（0）にします。

以下も必要です。

``` /symbol/client/catapult/plugins/txes/mosaic/src/validators/Validators.h
/// Validator that applies to all mosaicLevy change notifications and validates that:
DECLARE_STATEFUL_VALIDATOR(MosaicLevyChange, model::MosaicLevyChangeNotification)();
```

``` /symbol/client/catapult/plugins/txes/mosaic/src/validators/Results.h
/// Validation failed because the mosaic levy change action is invalid.
DEFINE_MOSAIC_RESULT(Invalid_Levy_Change_Action, 116);

/// Validation failed because the mosaic levy change amount is invalid.
DEFINE_MOSAIC_RESULT(Invalid_Levy_Change_Amount, 117);

/// Validation failed because the mosaic levy change is not allowed.
DEFINE_MOSAIC_RESULT(Levy_Change_Not_Allowed, 118);
```

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

namespace catapult { namespace observers {

	namespace {
		constexpr bool ShouldIncrease(NotifyMode mode, model::MosaicLevyChangeAction action) {
			return
					(NotifyMode::Commit == mode && model::MosaicLevyChangeAction::Increase == action) ||
					(NotifyMode::Rollback == mode && model::MosaicLevyChangeAction::Decrease == action);
		}
	}

	DEFINE_OBSERVER(MosaicLevyChange, model::MosaicLevyChangeNotification, [](
			const model::MosaicLevyChangeNotification& notification,
			const ObserverContext& context) {

		auto mosaicId = context.Resolvers.resolve(notification.MosaicId);
		auto levyMosaicId = notification.LevyMosaicId;
		auto& mosaicCache = context.Cache.sub<cache::MosaicCache>();
		auto mosaicIter = mosaicCache.find(mosaicId);
		auto& mosaicEntry = mosaicIter.get();

		ShouldIncrease(context.Mode, notification.Action) 
			? mosaicEntry.increaseLevy(notification.Delta) 
			: mosaicEntry.decreaseLevy(notification.Delta);

		mosaicEntry.levy() == Amount() 
			? mosaicEntry.setLevyMosaicId(UnresolvedMosaicId()) 
			: mosaicEntry.setLevyMosaicId(levyMosaicId);
	})
}}
```
このオブザーバでは通知を受けたらまずlevy額を増減しています。
levy額がゼロの場合は空のlevyMosaicIdを設定しゼロでない場合、つまりisLeviedの場合は指定されたMosaicIDをLevyMosaicIdとして設定します

observersネームスペースに定義します
``` /symbol/client/catapult/plugins/txes/mosaic/src/observers/Observers.h
/// Observes changes triggered by mosaic levy change notifications and:
DECLARE_OBSERVER(MosaicLevyChange, model::MosaicLevyChangeNotification)();
```

これでMosaicLevyChangeTransactionについては完成です。

# Receipt
levyを徴収した際にレシートを発行します
```/symbol/client/catapult/plugins/txes/mosaic/src/model/MosaicReceiptType.h
...
/// Mosaic levy fee.
DEFINE_RECEIPT_TYPE(BalanceTransfer, Mosaic, Mosaic_Levy_Fee, 3);
...
```

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

namespace catapult { namespace mongo { namespace plugins {

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

``` /symbol/client/catapult/extensions/mongo/plugins/mosaic/src/MosaicLevyChangeMapper.cpp
#include "MosaicLevyChangeMapper.h"
#include "mongo/src/MongoTransactionPluginFactory.h"
#include "mongo/src/mappers/MapperUtils.h"
#include "plugins/txes/mosaic/src/model/MosaicLevyChangeTransaction.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
                << "mosaicId" << ToInt64(transaction.MosaicId)
                << "action" << utils::to_underlying_type(transaction.Action)
                << "delta" << ToInt64(transaction.Delta);
		}
	}

	DEFINE_MONGO_TRANSACTION_PLUGIN_FACTORY(MosaicLevyChange, StreamTransaction)
}}}
```

``` /symbol/client/catapult/extensions/mongo/plugins/mosaic/src/MongoMosaicPlugin.cpp
#include "MosaicLevyChangeMapper.h"
...
manager.addTransactionSupport(mongo::plugins::CreateMosaicLevyChangeTransactionMongoPlugin());
...
manager.addReceiptSupport(mongo::CreateBalanceTransferReceiptMongoPlugin(model::Receipt_Type_Mosaic_Levy_Fee));
```
# MosaicLevyTransferValidator
levyのついたモザイクを判定します。

``` /symbol/client/catapult/plugins/txes/mosaic/src/validators/MosaicLevyTransferValidator.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::BalanceTransferNotification;

	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;
	}

	DECLARE_STATEFUL_VALIDATOR(MosaicLevyTransfer, Notification)() {
		return MAKE_STATEFUL_VALIDATOR(MosaicLevyTransfer, [](
				const Notification& notification,
				const ValidatorContext& context) {
			const auto& cache = context.Cache.sub<cache::AccountStateCache>();
			const auto& mosaicCache = context.Cache.sub<cache::MosaicCache>();
			auto mosaicId = context.Resolvers.resolve(notification.MosaicId);
			auto mosaicIter = mosaicCache.find(mosaicId);
			if(mosaicIter.get().isLevied() == false)
				return ValidationResult::Success;
			
			auto levyMosaicId = context.Resolvers.resolve(mosaicIter.get().levyMosaicId());
			auto effectiveAmount = mosaicIter.get().levy();
			Amount mosaicAmount;
			bool hasMosaic = FindAccountBalance(cache, notification.Sender.resolved(context.Resolvers), levyMosaicId, mosaicAmount);
			if(!hasMosaic || mosaicAmount < effectiveAmount) {
				return Failure_Mosaic_Levy_Insufficient_Balance;
			}
			return ValidationResult::Success;
		});
	}
}}
```

通知元は`BalanceTransferNotification`つまりこの通知が使われている、例えばTransferTransaction内のモザイクがisLevyか判定し、もし設定されていたらLevyMosaicIDのAmountが足りているか判定し足りていればSuccessを返します。

``` /symbol/client/catapult/plugins/txes/mosaic/src/validators/Validators.h
/// Validator that applies to all balance transfer notifications and validates that:
DECLARE_STATEFUL_VALIDATOR(MosaicLevyTransfer, model::BalanceTransferNotification)();
```

``` /symbol/client/catapult/plugins/txes/mosaic/src/validators/Results.h
/// Validation failed because the mosaic has insufficient balance.
DEFINE_MOSAIC_RESULT(Levy_Insufficient_Balance, 115);
```
先程と同じく定義しておきます

# MosaicLevyTransferObserver

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

namespace catapult { namespace observers {

	using Notification = model::BalanceTransferNotification;

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

	DECLARE_OBSERVER(MosaicLevyTransfer, Notification)() {
		return MAKE_OBSERVER(MosaicLevyTransfer, Notification, ([](const Notification& notification, ObserverContext& context) {
			auto& cache = context.Cache.sub<cache::AccountStateCache>();
			auto senderAddress = notification.Sender.resolved(context.Resolvers);
			auto senderIter = cache.find(senderAddress);
			auto& senderState = senderIter.get();

			const auto& mosaicCache = context.Cache.sub<cache::MosaicCache>();

			auto mosaicId = context.Resolvers.resolve(notification.MosaicId);
			auto mosaicIter = mosaicCache.find(mosaicId);

			if(mosaicIter.get().isLevied() == false)
				return;

			auto ownerAddress = mosaicIter.get().definition().ownerAddress();

			if(senderAddress == ownerAddress)
				return;

			auto ownerIter = cache.find(ownerAddress);
			auto& ownerState = ownerIter.get();

			auto receiptType = model::Receipt_Type_Mosaic_Levy_Fee;
			auto effectiveAmount = mosaicIter.get().levy();
			auto levyMosaicId = context.Resolvers.resolve(mosaicIter.get().levyMosaicId());
			if (NotifyMode::Commit == context.Mode) {
				Transfer(senderState, ownerState, levyMosaicId, effectiveAmount);
				model::BalanceTransferReceipt receipt(receiptType, senderAddress, ownerAddress, levyMosaicId, effectiveAmount);
				context.StatementBuilder().addReceipt(receipt);
			} else {
				Transfer(ownerState, senderState, levyMosaicId, effectiveAmount);
			}
		}));
	}
}}
```
`BalanceTransferNotification`を監視します。isLevied()がtrueであればモザイクオーナーに対して設定されたモザイクIDをlevy額分送信します

``` /symbol/client/catapult/plugins/txes/mosaic/src/observers/Observers.h
/// Observes changes triggered by balance transfer notifications and:
DECLARE_OBSERVER(MosaicLevyTransfer, model::BalanceTransferNotification)();
```
先程と同じく定義します

# MosaicPlugin.cpp
それぞれ登録します
``` /symbol/client/catapult/plugins/txes/mosaic/src/plugins/MosaicPlugin.cpp
...
#include "MosaicLevyChangeTransactionPlugin.h"
...
manager.addTransactionSupport(CreateMosaicLevyChangeTransactionPlugin());
...
manager.addStatefulValidatorHook([config, unresolvedCurrencyMosaicId, &networkConfig = manager.config()](auto& builder) {
    builder
        .add(validators::CreateMosaicFlagsValidator(networkConfig.ForkHeights.TreasuryReissuance))
...
        .add(validators::CreateMosaicLevyChangeValidator())
        .add(validators::CreateMosaicLevyTransferValidator());
});
...
manager.addObserverHook([currencyMosaicId, calculator, maxRollbackBlocks](auto& builder) {
    auto rentalFeeReceiptType = model::Receipt_Type_Mosaic_Rental_Fee;
    auto expiryReceiptType = model::Receipt_Type_Mosaic_Expired;
    builder
        .add(observers::CreateMosaicDefinitionObserver())
...
        .add(observers::CreateMosaicLevyChangeObserver())
        .add(observers::CreateMosaicLevyTransferObserver());
});
```

あとはビルドしてオリジナルネットワークで起動すれば使えるはずです。が、実際に使用するにはSDKを使うと思うのでその方法も。

# SDK
``` /catbuffer/schemas/symbol/mosaic/mosaic_types.cats
// 追加
enum MosaicLevyChangeAction : uint8
	# DECREASE the levy.
	DECREASE = 0x00

	# Increases the levy.
	INCREASE = 0x01
```
``` /catbuffer/schemas/symbol/mosaic/mosaic_levy_change.cats
import "mosaic/mosaic_types.cats"
import "transaction.cats"

# Shared content between MosaicLevyChangeTransaction and Embedded MosaicLevyChangeTransaction.
inline struct MosaicLevyChangeTransactionBody	
	mosaic_id = UnresolvedMosaicId

    levy_mosaic_id = UnresolvedMosaicId

	delta = Amount
    
    action = MosaicLevyChangeAction
	
# Create a new  [mosaic](/concepts/mosaic.html) (V1, latest).
struct MosaicLevyChangeTransactionV1
	TRANSACTION_VERSION = make_const(uint8, 1)
	TRANSACTION_TYPE = make_const(TransactionType, MOSAIC_LEVY_CHANGE)

	inline Transaction
	inline MosaicLevyChangeTransactionBody

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

	inline EmbeddedTransaction
	inline MosaicLevyChangeTransactionBody
```

``` /catbuffer/schemas/symbol/transaction_type.cats
...
# MosaicLevyChangeTransaction
MOSAIC_LEVY_CHANGE = 0x444D
...
```

``` /catbuffer/schemas/symbol/all_transactions.cats
...
import "mosaic/mosaic_levy_change.cats"
...
```
receiptも一応
``` /catbuffer/schemas/symbol/receipt_type.cats
# Mosaic levy fee receipt.
MOSAIC_LEVY_FEE = 0x134D
```

model作成します
```
./scripts/run_catbuffer_generator.sh
```

以下はC#ですがjsやpython版もなんとなく分かると思います
``` cs
var tx = new MosaicLevyChangeTransactionV1()
{
    Network = NetworkType.TESTNET,
    SignerPublicKey = alice.PublicKey,
    Deadline = new Timestamp(facade.Network.FromDatetime<NetworkTimestamp>(DateTime.UtcNow).AddHours(2).Timestamp),
    Action = MosaicLevyChangeAction.INCREASE,
    MosaicId = new UnresolvedMosaicId(0x5F66FAD1C87DE7EB),
    Delta = new Amount(2000000)
};
```

# REST

`MosaicLevyChangeTransaction`をRESTで正常に表示するためにrestも少し改造します
``` /symbol/client/rest/src/catapult-sdk/model/EntityType.js
/** Mosaic levy change transaction. */
mosaicLevyChange: 0x444D,
```
``` /symbol/client/rest/src/catapult-sdk/plugins/mosaic.js
...
builder.addTransactionSupport(EntityType.mosaicLevyChange, {
    mosaicId: ModelType.uint64HexIdentifier,
    levyMosaicId: ModelType.uint64HexIdentifier,
    delta: ModelType.uint64,
    action: ModelType.uint8
});
...
codecBuilder.addTransactionSupport(EntityType.mosaicLevyChange, {
    deserialize: parser => {
        const transaction = {};
        transaction.mosaicId = parser.uint64();
        transaction.levyMosaicId = parser.uint64();
        transaction.delta = parser.uint64();
        transaction.action = parser.uint8();
        return transaction;
    },

    serialize: (transaction, serializer) => {
        serializer.writeUint64(transaction.mosaicId);
        serializer.writeUint64(transaction.levyMosaicId);
        serializer.writeUint64(transaction.delta);
        serializer.writeUint8(transaction.action);
    }
});
```
これは今回追加したエラーを正常表示するために追加します
``` /symbol/client/rest/src/catapult-sdk/model/status.js
...
case 0x804D0073: return 'Failure_Mosaic_Levy_Insufficient_Balance';
case 0x804D0074: return 'Failure_Mosaic_Invalid_Levy_Change_Action';
case 0x804D0075: return 'Failure_Mosaic_Invalid_Levy_Change_Amount';
case 0x804D0076: return 'Failure_Mosaic_Levy_Change_Not_Allowed';
...
```

# 終わりに
Symbolは完成されているが、拡張性はとてつもなく高いブロックチェーンだと思いますし、それを前提で作られています。まだまだ始まったばかりで新たなトランザクションもRevokableしか作成されていません。

Symbol2.0があるとすればみんなで作り上げるものなのかもしれません。

これからが楽しみですね！