投稿日
Cordaを用いたNonFungibleTokenの実装
もくじ
はじめに
こんにちは。ブロックチェーン推進室の鈴木です。
Cordaは世界で350社を超える金融機関、規制当局、中央銀行、業界団体、システム・インテグレーターやソフトウェアベンダーにより構成されるR3エコシステムから、エンドユーザー目線で設計・開発されています。 また、4半期に1度のバージョンアップにより機能追加がされていますが、その機能がお客様に活用できるものなのか、ドキュメントだけではわからないことも多く、ブロックチェーン推進室独自で調査を進めています。
このブログでは、そういったCordaの機能調査をした結果を機能の使い方含めて紹介していきます。
Cordaの導入方法についてはをご参照ください。
今後以下の内容で紹介しようと思います。
-
-
-
-
-
-
-
-
-
-
-
-
Cordaを用いたNonFungibleTokenの実装(←今回はこちらの記事)
-
(以降、バージョンアップの都度、新機能を紹介予定)
第12回目は「Cordaを用いたNonFungibleTokenの実装」について紹介します。
概要
今回の記事はCordaのTokens SDKのNonFungibleToken(NFT)について紹介します。
昨今注目されているNFTはCordaにおいてもTokens SDKを用いて実装することができます。
本記事ではTokens SDKを使ったNFTの実装方法について紹介します。
Tokens SDKについて
Tokens SDKはCorda4.3で導入された、ネットワーク上のあらゆる種類の資産を表すトークンをCordaにおいて、簡単に作成できるライブラリです。
アップグレードも幾度とされており、現在(2022年6月)ではV1.2.2が最新で、Corda4.6以降と互換性があります。
Tokens SDKを使うことで、次のことが可能になります。
-
トークンとその属性を定義
-
Cordappにトークンの発行、移転、償却機能を追加
Tokens SDKでは代替可能なFungibleTokenと非代替可能なNonFungibleTokenの2種類扱うことができ、トークンを作成する際には後述のTokenTypeを指定する必要があります。
TokenTypeの指定
Tokens SDKでトークンを発行する際に2種類のタイプから1つ選択する必要があり、状態が変化しない不変のTokenTypeと時間とともに状態が変化するEvolvableTokenTypeがあります。
TokenType
TokenTypeは状態が変わらないものを表現します。 TokenTypeには以下の2つを指定する必要があります。
-
tokenIdentifier:「USD」や「XRP」などの識別子を設定します。
-
fractionDigits:トークンの価値を小数点第何位まで扱うかの桁数を設定します。
後述のEvolvableTokenTypeに対し、TokenTypeを「FixedTokenType」と呼ぶ場合もあります。
EvolvableTokenType
EvolvableTokenTypeは状態が変わっていくものを表現します。 EvolvableTokenTypeには以下の3つを指定する必要があります。
-
maintainers:このStateの管理者を設定します。
-
fractionDigits:トークンの価値を小数点第何位まで扱うかの桁数を設定します。
-
その他、自分で属性を自由に設定できます。
トークン作成の組み合わせ
前述のTokenTypeと代替性の有無を考慮すると、トークン作成の組み合わせは以下の4種類になります。
-
FungibleToken × (Fixed)TokenType
-
FungibleToken × EvolvableTokenType
-
NonFungibleToken × (Fixed)TokenType
-
NonFungibleToken × EvolvableTokenType
今回はNonFungibleToken × EvolvableTokenTypeについてを解説します。
NonFungible Token × EvolvableTokenType
NonFungibleTokenには所有者やTokenTypeなど基本的な情報しか持っていません。
例えば、家のNonFungibleTokenを作成したとしても、何階建てかや、建築年数などの情報をNonFungibleTokenには持たせることはできません。
このような家の情報をもたせるにはEvolvableTokenTypeのStateを作成して、そのStateに情報をもたせNonFungibleTokenに結びつけます。
以下の図のような関係で2つは結びつきます。
図1:NonFungibleTokenとEvolvableTokenType
ここで家の築年数が更新され、EvolvableTokenTypeの情報が変更されたとします。
古いEvolvableTokenTypeのStateが消費済みになり、NonFungibleTokenは更新されたEvolvableTokenTypeを参照するようになります。
図2:NonFungibleTokenとEvolvableTokenTypeの更新
また、NonFungibleTokenの所有者が変更になったとしてもEvolvableTokenTypeの方は変わりません。
図3:NonFungibleToken移転とEvolvableTokenType
NonFungibleTokenとEvolvableTokenTypeはそれぞれlinearIdを持っているため情報が変わっても一意に特定でき、常に最新情報を取得できます。 このようにNonFungibleTokenとEvolvableTokenTypeは常に一対一でそれぞれの最新の状態と結びついています。
実装方法
今回は家をNonFungibleTokenに例え、基本的なTokenの流れである発行、移転、償還とトークン情報(EvolvableTokenType)の更新を実装したsampleを作成しています。
それをもとにEvolvableTokenTypeの定義から各Flowの実装を紹介します。
EvolvableTokenTypeを定義
EvolvableTokenTypeでは発行するNonFungibleTokenに付帯して持たせたい情報を定義します。
EvolvableTokenTypeを継承したStateクラスとEvolvableTokenContractを継承したContractクラスを作成します。
このContractはEvolvableTokenTypeの作成時と更新時にチェックを行います。
HouseTokenState.kt
今回の場合、家の情報をカスタムプロパティとして階数を表すFloorと築年数を表すageOfBuildingを定義しています。
上記に加え、EvolvableTokenTypeに必要なlinearId、fractionDigits、maintainersもオーバーライドします。
@BelongsToContract(HouseTokenStateContract::class)
data class HouseTokenState(
val floor: Int, //階数
val ageOfBuilding: Int, //築年数
val maintainer: Party,
override val linearId: UniqueIdentifier,
override val fractionDigits: Int,
override val maintainers: List<Party> = listOf(maintainer)
) : EvolvableTokenType()
HouseTokenStateContract.kt
ContractにはEvolvableTokenContractを継承し、additionalCreateChecksとadditionalUpdateChecksをオーバーライドします。
これは、EvolvableTokenContractの標準のチェックに加え、作成時と更新時に独自で追加のチェックを行うことができます。
なお、EvolvableTokenContractはverifyが既に定義されているため、自分で実装はしません。
このverifyによってadditionalCreateChecksやadditionalUpdateChecksが自動的に呼び出されます。
class HouseTokenStateContract : EvolvableTokenContract(),Contract {
companion object {
const val CONTRACT_ID = "~~.contracts.HouseTokenStateContract"
}
override fun additionalCreateChecks(tx: LedgerTransaction) {
// EvolvableTokenTypeを作成時に追加でチェックするものがあればここに記述する
}
override fun additionalUpdateChecks(tx: LedgerTransaction) {
// EvolvableTokenTypeを更新時に追加でチェックするものがあればここに記述する
}
NonFungibleTokenの発行とEvolvableTokenTypeの作成
NonFungibleTokenの発行時にEvolvableTokenTypeとの結びつけを行うので、発行と作成を同時にするのが望ましいです。
-
NonFungibleTokenの発行
IssueTokensを呼び出すことでTokenを発行することができます。これはFungibleTokenの発行でも同様です。 -
EvolvableTokenTypeの作成
CreateEvolvableTokensを呼び出すことでEvolvableTokenTypeを作成することができます。
これらはどちらもそれぞれラップされたFlowのため、内部で使用しているFlowなど直接呼び出して使用することもできます。
EvolvableTokenTypeを使用しており、NonFungibleTokenを発行する時と、移転する時にはUpdateDistributionListFlowを呼び出す必要があります。
これは誰にEvolvableTokenTypeの更新情報を反映するかを、maintainersのノードのDISTRIBUTION_RECORDテーブルに記録します。
CreateHouseTokenStateAndIssueHouseToken.kt
このFlowではNonFungibleTokenの発行とEvolvableTokenTypeの作成の2つを行います。
EvolvableTokenTypeのtoPointer関数とissuer(発行者)を用いてIssuedTokenTypeを作成します。
これをNonFungibleTokenのインスタンスを作成をするときにパラメータとして渡すことで、EvolvableTokenTypeとNonFungibleTokenの紐づけができます。
IssueTokensを呼び出してNonFungibleTokenを発行します。
UpdateDistributionListFlowはIssueTokensの内部で呼び出されるため、明示的に呼び出す必要はありません。
@StartableByRPC
class CreateHouseTokenStateAndIssueHouseToken(
val floor: Int,
val ageOfBuilding: Int
) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val notary = serviceHub.networkMapCache.notaryIdentities.single()
val issuer = ourIdentity
//EvolvableTokenTypeの作成
val houseTokenState = HouseTokenState(floor,ageOfBuilding,issuer,UniqueIdentifier(),0)
// houseTokenStateとnotaryを使ってTransactionStateを作成
val transactionState = houseTokenState withNotary notary
// このサブフローを呼ぶことでEvolvableTokenTypeを登録
subFlow(CreateEvolvableTokens(transactionState))
// issuedTokenType(= tokenPointer)は、発行者情報とLinearStateのラッパー
val houseTokenStatePointer = houseTokenState.toPointer(houseTokenState.javaClass) issuedBy issuer
// NFTを生成し、houseTokenStateへのポインタを参照させる
val houseToken = NonFungibleToken(houseTokenStatePointer, issuer, UniqueIdentifier())
// NFTの発行
subFlow(IssueTokens(listOf(houseToken)))
}
}
NonFungibleTokenを移転
NonFungibleTokenはMoveNonFungibleTokensを呼び出すことで他の人に移転することができます。
NonFungibleTokenの移転だけではなく、FungibleTokenのやり取りなど他のTokenやStateをTransactionに含めるのであればMoveNonFungibleTokensの内部で使用されているaddMoveNonFungibleTokensを使用して、自身でTransactionを作成します。
MoveHouseToken.kt
このFlowではNonFungibleTokenの移転を行います。
linearIdをもとに、EvolvableTokenTypeを検索してきます。
検索してきたEvolvableTokenTypeと移転先のPartyを指定して、MoveNonFungibleTokensを呼び出してNonFungibleTokenを移転します。
UpdateDistributionListFlowはMoveNonFungibleTokensの内部で呼びされるため、明示的に呼び出す必要はありません。
@StartableByRPC
class MoveHouseToken(
val linearId: String,
val buyer: Party
) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
// 対象のEvolvableTokenType(HouseTokenState)を検索
val houseTokenStateAndRef = getHouseTokenStateAndRefFromLinearId(linearId, serviceHub)
val houseTokenState = houseTokenStateAndRef.state.data
// NFTの移転
subFlow(MoveNonFungibleTokens(PartyAndToken(buyer,houseTokenState.toPointer(houseTokenState.javaClass))))
}
}
なお、このsampleではlinearIdから対象のEvolvableTokenTypeを検索する関数を以下のように定義しています。
//linearIdをもとにVaultからHouseTokenStateを検索する
fun getHouseTokenStateAndRefFromLinearId(
linearId: String,
serviceHub:ServiceHub)
:StateAndRef<HouseTokenState>{
val inputCriteria =
QueryCriteria.LinearStateQueryCriteria(linearId = listOf(UniqueIdentifier.fromString(linearId)))
return serviceHub.vaultService.queryBy<HouseTokenState>(criteria = inputCriteria).states.single()
}
NonFungibleTokenを償還
NonFungibleTokenはRedeemNonFungibleTokensを呼び出すことで償還できます。
NonFungibleTokenの移転同様に、償還するNonFungibleToken以外のStateをTransactionに含める場合、addNonFungibleTokensToRedeemを使用して、自身でTransactionを作成します。
RedeemHouseToken.kt
このFlowではNonFungibleTokenの償還を行います。
linearIdをもとに、EvolvableTokenTypeを検索します。
次にEvolvableTokenTypeをもとにvaultからNonFungibleTokenを検索します。
検索してきたEvolvableTokenTypeとNonFungibleTokenのIssuerを指定して、RedeemNonFungibleTokensを呼び出してNonFungibleTokenを償還します。
@StartableByRPC
class RedeemHouseToken(
val linearId: String
) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
// 対象のEvolvableTokenType(HouseTokenState)を検索
val houseTokenStateAndRef = getHouseTokenStateAndRefFromLinearId(linearId, serviceHub)
val houseTokenState = houseTokenStateAndRef.state.data
val houseTokenPointer = houseTokenState.toPointer(houseTokenState.javaClass)
// NFTの検索条件の作成
val nonFungibleTokensCriteria = heldTokenCriteria(houseTokenPointer)
// NFTの検索
val nonFungibleTokenStateAndRef =
serviceHub.vaultService
.queryBy<NonFungibleToken>(criteria = nonFungibleTokensCriteria).states.single()
val nonFungibleToken = nonFungibleTokenStateAndRef.state.data
// NFTの償還
subFlow(RedeemNonFungibleTokens(houseTokenPointer, nonFungibleToken.issuer))
}
}
EvolvableTokenTypeの更新
EvolvableTokenTypeはUpdateEvolvableTokenを呼び出すことで更新することができます。
この更新を行うノードはmaintainersに含まれていることが条件になっているので注意が必要です。
UpdateHouseTokenState.kt
このFlowではEvolvableTokenTypeの更新を行います。
linearIdをもとに、EvolvableTokenTypeを検索します。
検索したEvolvableTokenTypeと更新するために受け取った家の情報から、新しいEvolvableTokenTypeのインスタンスを作成します。
この更新Flowを実施するノードがmaintainersではなくTokenのHolderの場合、maintainersのノードにEvolvableTokenの情報をsendしUpdateEvolvableTokenを実行してもらいます。
このFlowを実施するノードがmaintainersかつTokenのHolderの場合はUpdateEvolvableTokenを呼び出すだけになります。
@InitiatingFlow
@StartableByRPC
class UpdateHouseTokenState(
val linearId: String,
val floor: Int,
val ageOfBuilding: Int
) : FlowLogic<Unit>() {
@Suspendable
override fun call(){
// 対象のEvolvableTokenType(HouseTokenState)を検索
val houseTokenStateAndRef = getHouseTokenStateAndRefFromLinearId(linearId, serviceHub)
val houseTokenState = houseTokenStateAndRef.state.data
// 更新するEvolvableTokenType(HouseTokenState)の作成
val newHouseTokenState = HouseTokenState(
floor,
ageOfBuilding,
houseTokenState.maintainer,
houseTokenState.linearId,
houseTokenState.fractionDigits
)
// maintainerが自分でなければmaintainerに実行してもらう
if (houseTokenState.maintainer != ourIdentity) {
val maintainerSession = initiateFlow(houseTokenState.maintainer)
maintainerSession.send(houseTokenStateAndRef)
maintainerSession.send(newHouseTokenState)
}else{
subFlow(UpdateEvolvableToken(houseTokenStateAndRef,newHouseTokenState))
}
}
}
@InitiatedBy(UpdateHouseTokenState::class)
class HouseUpdateResponder(
val counterpartySession: FlowSession
) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val houseTokenStateAndRef = counterpartySession.receive<StateAndRef<HouseTokenState>>().unwrap{ it }
val newHouseTokenState = counterpartySession.receive<HouseTokenState>().unwrap{ it }
val stx = subFlow(UpdateEvolvableToken(houseTokenStateAndRef,newHouseTokenState))
}
}
注意点
上記で説明したことに加え、そのほかに特筆すべき注意点を以下にまとめておきます。
-
UpdateEvolvableToken
UpdateEvolvableTokenは更新するEvolvableTokenTypeのmaintainersしか実行することができません。
また、更新するTransactionに対象のEvolvableTokenType以外のStateをinputやoutputに入れることができません。 -
UpdateDistributionListFlow
上記で紹介したコードではラップされているFlowを呼んでいるため、明示的に呼び出す記述はしていませんが、直接内部のFlowを使用する場合にはUpdateDistributionListFlowをトークンの発行、移転時に呼び出す必要があります。
これはNonFungibleTokenの発行でも説明しましたがこのUpdateDistributionListFlowを呼び出すことで、誰にEvolvableTokenTypeの更新情報を反映するかを、maintainersのノードのDISTRIBUTION_RECORDテーブルに記録します。
呼び出さなかった場合、どのようになるか以下に例を示します。
1.NonFungibleTokenの移転
以下の図のようにAliceが持っているNonFungibleTokenをBobに移転します。
移転が成功するとNFTとそのNFTに紐づくEvolvableTokenTypeが移転先のvaultに登録されます。
図4:NonFungibleTokenの移転
2.EvolvableTokenTypeの更新
Bobは築年数を更新します。
しかし、EvolvableTokenTypeの更新はmaintainersしか行うことができないので、AliceにUpdateEvolvableTokenを実行してもらいます。この時、更新は成功してもBobのvaultには更新されたEvolvableTokenTypeは登録されません。
図5:UpdateDistributionListFlowを呼ばなかった時のEvolvableTokenTypeの更新◎UpdateDistributionListFlowの仕組み
UpdateDistributionListFlowを呼ぶとmaintainersのDISTRIBUTION_RECORDテーブルにTokenの移転先のHolder情報とそのTokenに紐づくEvolvableTokenTypeのlinearIdを記録します。
図6:Tokenの移転時のUpdateDistributionListFlowの処理
その後、maintainersがUpdateEvolvableTokenを呼ぶときにこのテーブルを確認し、対象のEvolvableTokenTypeのlinearIdを持つPartyに更新をするような仕組みになっています。
図7:UpdateEvolvableToken時のDISTRIBUTION_RECORDの確認
おわりに
いかがでしたでしょうか。 今回はTokens SDKを使ったNFTの実装方法について紹介しました。
NonFungibleTokenの基本的な発行や移転を行うのはTokens SDKを使うことで自分で実装するよりも容易に実装することができます。
しかしEvolvableTokenTypeと一緒に利用すると様々な注意点や制約があるので少し実装の難易度が上がると思います。
そのため、NonFungibleTokenの発行時や移転時、Transactionの中にFungibleTokenやその他Stateを共に含めることがなければ、今回紹介したsampleのようにラップされたFlowを使うことをおすすめします。
また次回は、今回の記事の応用編として「