【本文】
コンソーシアム型DLT(Distributed Ledger Techonology)
DLT(Distributed Ledger Techonology)の基盤を提供するR3 Cordaでは、Token SDKを通じてトークン発行が可能となっている。
Cordaでのトークンとは、端的にデジタルアセットとしての証券(など、コモディティ含めた金融商品、及び法定通貨にペッグしたステーブルコイン)を指す。
Token SDKでは、Ethreumでよく聞くfungible token(ERC20)とnon-fungible token(ERC721)の両タイプが発行可能であり、またEvolvable type(デジタルアセットのライフサイクルの中で、例えば株式分割や要綱変更によって発行時より何らかの変更が発生する。)、もしくはfixed type(法定通貨のように原則として変更が加わらない。)の2種類のタイプが存在する。
詳細は、以下参照。
Corda State Hierarchy Design : Corda Token SDK
https://github.com/corda/token-sdk/blob/master/design/design.md
今回、GitHubのテンプレートをベースにToken SDKを利用したCordaでのトークンの要綱作成・発行・移転の簡易的な実装例を紹介する。
(注:Javaで実装。本来なら、トークンを制御する為のcontractも定義するべきだが、今回は無風。)
【簡単なフロー】
トークンの要綱作成(Create)、発行(Issue)、移転(Move)のイメージとしては、以下の流れとなる。
(注:今回は償還(Redeem)は割愛。)
なお、実際のコードは次章に例示しているが、CorDappとしてデプロイ後にRPCコマンドで叩くコマンドと実行結果と合わせ、簡単なフローを紹介。
(1)
FlowのCreateを呼び出す
start CreateTestTokenFlow importantInformationThatMayChange : xxxx
↓
(2)
Evolvable Token Typeを通じてトークン要綱が生成される。この時、evolvable IDが発行される。
(以降、発行や移転時にはこのevolvable ID(uniqueIdentifier
)を指定する。)
Vault検索:実行結果例(抜粋)
states:
- state:
data: !<com.template.states.TestTokenType>
importantInformationThatMayChange: "20200105-Test"
uniqueIdentifier:
externalId: null
id: "23183716-8400-4f42-a5b3-38e8aa9693b2"
fractionDigits: 2
maintainer: "O=Sec-A, L=Tokyo, C=JP"
contract: "com.template.TestTokenTypeContract"
notary: "O=Notary, L=Tokyo, C=JP"
ref:
txhash: "2DC31421ED1349A26F1545AE09C3C0C4FC23D502B9489FF912EC2055BA51CDFA"
index: 0
↓
(3)
トークンを発行する。引数はトークン名や発行数量、発行者等を定義しておく。発行時には、evolvable IDを指定する。
start IssueTestTokenFlow evolvableTokenId : xx-xx-xx-xx-xx, tokenname : XXXX , quantity : nnnn , issuer : "O=AAAA, L=XXX, C=XX"
Vault検索:実行結果例(抜粋)
states:
- state:
data: !<com.r3.corda.lib.tokens.contracts.states.FungibleToken>
amount: "500.00 TokenPointer(class com.template.states.TestTokenType, 23183716-8400-4f42-a5b3-38e8aa9693b2)\
\ issued by Sec-A"
holder: "O=Sec-A, L=Tokyo, C=JP"
tokenTypeJarHash: null
contract: "com.r3.corda.lib.tokens.contracts.FungibleTokenContract"
notary: "O=Notary, L=Tokyo, C=JP"
ref:
txhash: "4FFD2908FAE545B22427A23C8AAF02BB1F148A003E06A662B8AFACCF3E275B4C"
index: 0
↓
(4)
トークンを移転する。引数はトークン名や発行数量、移転先等を定義しておく。移転時には、evolvable IDを指定する。
start MoveTestTokenFlow evolvableTokenId : xx-xx-xx-xx-xx, tokenname : XXXX, quantity : nnnn, recipient : "O=BBBB, L=XXXX, C=XX"
Vault検索:実行結果例(抜粋)「移転先」
amountが80に増加。
states:
- state:
data: !<com.r3.corda.lib.tokens.contracts.states.FungibleToken>
amount: "80.00 TokenPointer(class com.template.states.TestTokenType, 23183716-8400-4f42-a5b3-38e8aa9693b2)\
\ issued by Sec-A"
holder: "O=Sec-B, L=Osaka, C=JP"
tokenTypeJarHash: null
contract: "com.r3.corda.lib.tokens.contracts.FungibleTokenContract"
notary: "O=Notary, L=Tokyo, C=JP"
ref:
txhash: "5DE519CE4101EFECFBEB242E283476D35D56C2D1F0E77549F03F10D6F27B37DA"
index: 0
Vault検索:実行結果例(抜粋)「移転元」
amountが500->420に減少。
- state:
data: !<com.r3.corda.lib.tokens.contracts.states.FungibleToken>
amount: "420.00 TokenPointer(class com.template.states.TestTokenType, 23183716-8400-4f42-a5b3-38e8aa9693b2)\
\ issued by Sec-A"
holder: "O=Sec-A, L=Tokyo, C=JP"
tokenTypeJarHash: null
contract: "com.r3.corda.lib.tokens.contracts.FungibleTokenContract"
notary: "O=Notary, L=Tokyo, C=JP"
ref:
txhash: "5DE519CE4101EFECFBEB242E283476D35D56C2D1F0E77549F03F10D6F27B37DA"
index: 1
(以降、トークンの移転を複数所有者で実行)
以上の流れとなる。
【コード例】
Evolvable Token Type
(トークン・タイプ(ContractState)を定義:今回はEvolvable Type。)
package com.template.states;
import com.google.common.collect.ImmutableList;
import com.r3.corda.lib.tokens.contracts.states.EvolvableTokenType;
import com.template.TestTokenTypeContract;
import net.corda.core.contracts.BelongsToContract;
import net.corda.core.contracts.UniqueIdentifier;
import net.corda.core.identity.Party;
import java.util.List;
import java.util.Objects;
@BelongsToContract(TestTokenTypeContract.class)
public class TestTokenType extends EvolvableTokenType {
private final String importantInformationThatMayChange;
private final UniqueIdentifier uniqueIdentifier;
private final int fractionDigits;
private final Party maintainer;
public TestTokenType(
String importantInformationThatMayChange,
UniqueIdentifier uniqueIdentifier,
Party maintainer,
int fractionDigits
) {
this.importantInformationThatMayChange = importantInformationThatMayChange;
this.uniqueIdentifier = uniqueIdentifier;
this.fractionDigits = fractionDigits;
this.maintainer = maintainer;
}
@Override
public List<Party> getMaintainers() {
return ImmutableList.of(maintainer);
}
@Override
public int getFractionDigits() {
return this.fractionDigits;
}
@Override
public UniqueIdentifier getLinearId() {
return this.uniqueIdentifier;
}
public String getImportantInformationThatMayChange() {
return importantInformationThatMayChange;
}
public Party getMaintainer() {
return maintainer;
}
public UniqueIdentifier getUniqueIdentifier() {
return uniqueIdentifier;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestTokenType that = (TestTokenType) o;
return getFractionDigits() == that.getFractionDigits() &&
getImportantInformationThatMayChange().equals(that.getImportantInformationThatMayChange()) &&
getMaintainer().equals(that.getMaintainer()) &&
getUniqueIdentifier().equals(that.getUniqueIdentifier());
}
@Override
public int hashCode() {
return Objects.hash(
getImportantInformationThatMayChange(),
getMaintainer(),
getUniqueIdentifier(),
getFractionDigits());
}
}
Create Evolvable Token
(トークン発行の要綱作成のイメージ)
/**
* Create Evolvable Token using CreateEvolvableTokens
*/
@StartableByRPC
public static class CreateTestTokenFlow extends FlowLogic<SignedTransaction> {
private final String importantInformationThatMayChange;
public CreateTestTokenFlow(
String importantInformationThatMayChange
) {
this.importantInformationThatMayChange = importantInformationThatMayChange;
}
@Override
@Suspendable
public SignedTransaction call() throws FlowException {
//grab the notary
Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);
//create token type
TestTokenType evolvableTokenType = new TestTokenType(
importantInformationThatMayChange,
new UniqueIdentifier(),
getOurIdentity(),
2
);
//warp it with transaction state specifying the notary
TransactionState transactionState = new TransactionState(evolvableTokenType, notary);
//call built in sub flow CreateEvolvableTokens. This can be called via rpc or in unit testing
return subFlow(new CreateEvolvableTokens(transactionState));
}
}
Issue Fungible Token
(トークン発行のイメージ)
/**
* Issue Fungible Token using IssueTokens flow
*/
@StartableByRPC
public static class IssueTestTokenFlow extends FlowLogic<SignedTransaction> {
private final String evolvableTokenId;
private final String tokenname;
private final Long quantity;
private final Party issuer;
public IssueTestTokenFlow(
String evolvableTokenId,
String tokenname,
Long quantity,
Party issuer) {
this.evolvableTokenId = evolvableTokenId;
this.issuer = issuer;
this.tokenname = tokenname;
this.quantity = quantity;
}
@Override
@Suspendable
public SignedTransaction call() throws FlowException {
UUID uuid = UUID.fromString(evolvableTokenId);
//construct the query criteria
QueryCriteria queryCriteria = new QueryCriteria.LinearStateQueryCriteria(null, ImmutableList.of(uuid), null,
Vault.StateStatus.UNCONSUMED);
// grab the token type off the ledger which was created using CreateEvolvableTokens flow
StateAndRef<TestTokenType> stateAndRef = getServiceHub().getVaultService().
queryBy(TestTokenType.class, queryCriteria).getStates().get(0);
TestTokenType evolvableTokenType = stateAndRef.getState().getData();
//get the pointer pointer to the evolvable token type
TokenPointer tokenPointer = evolvableTokenType.toPointer(evolvableTokenType.getClass());
//assign the issuer who will be issuing the tokens
IssuedTokenType issuedTokenType = new IssuedTokenType(getOurIdentity(),tokenPointer);
//specify how much amount to issue to holder
Amount<IssuedTokenType> amount = new Amount(quantity, issuedTokenType);
//create fungible amount specifying the new owner
FungibleToken fungibleToken = new FungibleToken(amount, issuer, TransactionUtilitiesKt.getAttachmentIdForGenericParam(tokenPointer));
//use built in flow for issuing tokens on ledger
return subFlow(new IssueTokens(ImmutableList.of(fungibleToken)));
}
}
Move Fungible Token
(トークン移転のイメージ)
/**
* Move Fungible Token using MoveFungibleTokens flow
*/
@StartableByRPC
public static class MoveTestTokenFlow extends FlowLogic<SignedTransaction> {
private final String evolvableTokenId;
private final String tokenname;
private final Long quantity;
private final Party recipient;
public MoveTestTokenFlow(
String evolvableTokenId,
String tokenname,
Long quantity,
Party recipient) {
this.evolvableTokenId = evolvableTokenId;
this.recipient = recipient;
this.tokenname = tokenname;
this.quantity = quantity;
}
@Override
@Suspendable
public SignedTransaction call() throws FlowException {
UUID uuid = UUID.fromString(evolvableTokenId);
//construct the query criteria
QueryCriteria queryCriteria = new QueryCriteria.LinearStateQueryCriteria(null, ImmutableList.of(uuid), null,
Vault.StateStatus.UNCONSUMED);
// grab the token type off the ledger which was created using CreateEvolvableTokens flow
StateAndRef<TestTokenType> stateAndRef = getServiceHub().getVaultService().
queryBy(TestTokenType.class, queryCriteria).getStates().get(0);
TestTokenType evolvableTokenType = stateAndRef.getState().getData();
//get the pointer pointer to the evolvable token type
TokenPointer tokenPointer = evolvableTokenType.toPointer(evolvableTokenType.getClass());
//specify how much amount to issue to holder
Amount amount = new Amount<>(quantity, tokenPointer);
PartyAndAmount partyAndAmount = new PartyAndAmount(recipient,amount);
//use built in flow for moveing issued tokens on ledger
return subFlow(new MoveFungibleTokens(partyAndAmount));
}
}
完成版: FlowWithTestToken
(トークンの発行や移転等のライフサイクル全体の各フローを定義)
package com.template.flows;
import co.paralleluniverse.fibers.Suspendable;
import com.google.common.collect.ImmutableList;
import com.r3.corda.lib.tokens.contracts.states.FungibleToken;
import com.r3.corda.lib.tokens.contracts.types.IssuedTokenType;
import com.r3.corda.lib.tokens.contracts.types.TokenPointer;
import com.r3.corda.lib.tokens.contracts.utilities.TransactionUtilitiesKt;
import com.r3.corda.lib.tokens.workflows.flows.rpc.CreateEvolvableTokens;
import com.r3.corda.lib.tokens.workflows.flows.rpc.IssueTokens;
import com.r3.corda.lib.tokens.workflows.flows.rpc.MoveFungibleTokens;
import com.r3.corda.lib.tokens.workflows.types.PartyAndAmount;
import com.template.states.TestTokenType;
import net.corda.core.contracts.Amount;
import net.corda.core.contracts.StateAndRef;
import net.corda.core.contracts.TransactionState;
import net.corda.core.contracts.UniqueIdentifier;
import net.corda.core.flows.FlowException;
import net.corda.core.flows.FlowLogic;
import net.corda.core.flows.StartableByRPC;
import net.corda.core.identity.Party;
import net.corda.core.node.services.Vault;
import net.corda.core.node.services.vault.QueryCriteria;
import net.corda.core.transactions.SignedTransaction;
import java.util.UUID;
public class FlowWithTestToken {
private FlowWithTestToken() {
//Instantiation not allowed
}
/**
* Create Evolvable Token using CreateEvolvableTokens
*/
@StartableByRPC
public static class CreateTestTokenFlow extends FlowLogic<SignedTransaction> {
private final String importantInformationThatMayChange;
public CreateTestTokenFlow(
String importantInformationThatMayChange
) {
this.importantInformationThatMayChange = importantInformationThatMayChange;
}
@Override
@Suspendable
public SignedTransaction call() throws FlowException {
//grab the notary
Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);
//create token type
TestTokenType evolvableTokenType = new TestTokenType(
importantInformationThatMayChange,
new UniqueIdentifier(),
getOurIdentity(),
2
);
//warp it with transaction state specifying the notary
TransactionState transactionState = new TransactionState(evolvableTokenType, notary);
//call built in sub flow CreateEvolvableTokens. This can be called via rpc or in unit testing
return subFlow(new CreateEvolvableTokens(transactionState));
}
}
/**
* Issue Fungible Token using IssueTokens flow
*/
@StartableByRPC
public static class IssueTestTokenFlow extends FlowLogic<SignedTransaction> {
private final String evolvableTokenId;
private final String tokenname;
private final Long quantity;
private final Party issuer;
public IssueTestTokenFlow(
String evolvableTokenId,
String tokenname,
Long quantity,
Party issuer) {
this.evolvableTokenId = evolvableTokenId;
this.issuer = issuer;
this.tokenname = tokenname;
this.quantity = quantity;
}
@Override
@Suspendable
public SignedTransaction call() throws FlowException {
UUID uuid = UUID.fromString(evolvableTokenId);
//construct the query criteria
QueryCriteria queryCriteria = new QueryCriteria.LinearStateQueryCriteria(null, ImmutableList.of(uuid), null,
Vault.StateStatus.UNCONSUMED);
// grab the token type off the ledger which was created using CreateEvolvableTokens flow
StateAndRef<TestTokenType> stateAndRef = getServiceHub().getVaultService().
queryBy(TestTokenType.class, queryCriteria).getStates().get(0);
TestTokenType evolvableTokenType = stateAndRef.getState().getData();
//get the pointer pointer to the evolvable token type
TokenPointer tokenPointer = evolvableTokenType.toPointer(evolvableTokenType.getClass());
//assign the issuer who will be issuing the tokens
IssuedTokenType issuedTokenType = new IssuedTokenType(getOurIdentity(),tokenPointer);
//specify how much amount to issue to holder
Amount<IssuedTokenType> amount = new Amount(quantity, issuedTokenType);
//create fungible amount specifying the new owner
FungibleToken fungibleToken = new FungibleToken(amount, issuer, TransactionUtilitiesKt.getAttachmentIdForGenericParam(tokenPointer));
//use built in flow for issuing tokens on ledger
return subFlow(new IssueTokens(ImmutableList.of(fungibleToken)));
}
}
/**
* Move Fungible Token using MoveFungibleTokens flow
*/
@StartableByRPC
public static class MoveTestTokenFlow extends FlowLogic<SignedTransaction> {
private final String evolvableTokenId;
private final String tokenname;
private final Long quantity;
private final Party recipient;
public MoveTestTokenFlow(
String evolvableTokenId,
String tokenname,
Long quantity,
Party recipient) {
this.evolvableTokenId = evolvableTokenId;
this.recipient = recipient;
this.tokenname = tokenname;
this.quantity = quantity;
}
@Override
@Suspendable
public SignedTransaction call() throws FlowException {
UUID uuid = UUID.fromString(evolvableTokenId);
//construct the query criteria
QueryCriteria queryCriteria = new QueryCriteria.LinearStateQueryCriteria(null, ImmutableList.of(uuid), null,
Vault.StateStatus.UNCONSUMED);
// grab the token type off the ledger which was created using CreateEvolvableTokens flow
StateAndRef<TestTokenType> stateAndRef = getServiceHub().getVaultService().
queryBy(TestTokenType.class, queryCriteria).getStates().get(0);
TestTokenType evolvableTokenType = stateAndRef.getState().getData();
//get the pointer pointer to the evolvable token type
TokenPointer tokenPointer = evolvableTokenType.toPointer(evolvableTokenType.getClass());
//specify how much amount to issue to holder
Amount amount = new Amount<>(quantity, tokenPointer);
PartyAndAmount partyAndAmount = new PartyAndAmount(recipient,amount);
//use built in flow for moveing issued tokens on ledger
return subFlow(new MoveFungibleTokens(partyAndAmount));
}
}
}
以上。
【参考文献】
- Introduction to Token SDK in Corda (Medium)
- Let’s create some tokens (Medium)
- How do I? (GitHub)
- Corda State Hierarchy Design
備考:その他、日本語でも実装時にいくつか参考になる。