Skip to content

Latest commit

 

History

History
932 lines (766 loc) · 46.9 KB

File metadata and controls

932 lines (766 loc) · 46.9 KB

Token SDK, Fungible Tokens, The Basics

In this Section, we will see examples of how to perform basic token operations like issue, transfer, swap, and so on, on fungible tokens.

We will consider the following business parties:

  • Issuer: The entity that creates/mints/issues the tokens.
  • Alice, Bob, and Charlie: Each of these parties is a fungible token holder.
  • Auditor: The entity that is auditing the token transactions.

Each party is running a Smart Fabric Client node with the Token SDK enabled. The parties are connected in a peer-to-peer network that is established and manteined by the nodes.

Let us then describe each token operation with examples:

Issuance

Issuance is a business interactive protocol among two parties: an issuer of a given token type and a recipient that will become the owner of the freshly created token.

Here is an example of a view representing the issuer's operations in the issuance process:
This view is executed by the Issuer's FSC node.

// IssueCash contains the input information to issue a token
type IssueCash struct {
	// IssuerWallet is the issuer's wallet to use
	IssuerWallet string
	// TokenType is the type of token to issue
	TokenType string
	// Quantity represent the number of units of a certain token type stored in the token
	Quantity uint64
	// Recipient is an identifier of the recipient identity
	Recipient string
}

type IssueCashView struct {
	*IssueCash
}

func (p *IssueCashView) Call(context view.Context) (interface{}, error) {
	// As a first step operation, the issuer contacts the recipient's FSC node
	// to ask for the identity to use to assign ownership of the freshly created token.
	// Notice that, this step would not be required if the issuer knew already which
	// identity the recipient wants to use.
	recipient, err := ttxcc.RequestRecipientIdentity(context, view.Identity(p.Recipient))
	assert.NoError(err, "failed getting recipient identity")

	// Before assembling the transaction, the issuer can perform any activity that best fits the business process.
	// In this example, if the token type is USD, the issuer checks that no more than 230 units of USD
	// have been issued already including the current request.
	// No check is performed for other types.
	wallet := ttxcc.GetIssuerWallet(context, p.IssuerWallet)
	assert.NotNil(wallet, "issuer wallet [%s] not found", p.IssuerWallet)
	if p.TokenType == "USD" {
		// Retrieve the list of issued tokens using a specific wallet for a given token type.
		history, err := wallet.ListIssuedTokens(ttxcc.WithType(p.TokenType))
		assert.NoError(err, "failed getting history for token type [%s]", p.TokenType)
		fmt.Printf("History [%s,%s]<[230]?\n", history.Sum(64).ToBigInt().Text(10), p.TokenType)

		// Fail if the sum of the issued tokens and the current quest is larger than 230
		assert.True(history.Sum(64).Add(token2.NewQuantityFromUInt64(p.Quantity)).Cmp(token2.NewQuantityFromUInt64(230)) <= 0)
	}

	// At this point, the issuer is ready to prepare the token transaction.
	// The issuer creates an anonymous transaction (this means that the result Fabric transaction will be signed using idemix),
	// and specify the auditor that must be contacted to approve the operation
	tx, err := ttxcc.NewAnonymousTransaction(
		context,
		ttxcc.WithAuditor(
			fabric.GetDefaultIdentityProvider(context).Identity("auditor"), // Retrieve the auditor's FSC node identity
		),
	)
	tx.SetApplicationMetadata("github.com/hyperledger-labs/fabric-token-sdk/integration/token/tcc/basic/issue", []byte("issue"))
	tx.SetApplicationMetadata("github.com/hyperledger-labs/fabric-token-sdk/integration/token/tcc/basic/meta", []byte("meta"))
	assert.NoError(err, "failed creating issue transaction")

	// The issuer adds a new issue operation to the transaction following the instruction received
	err = tx.Issue(
		wallet,
		recipient,
		p.TokenType,
		p.Quantity,
	)
	assert.NoError(err, "failed adding new issued token")

	// The issuer is ready to collect all the required signatures.
	// In this case, the issuer's and the auditor's signatures.
	// Invoke the Token Chaincode to collect endorsements on the Token Request and prepare the relative Fabric transaction.
	// This is all done in one shot running the following view.
	// Before completing, all recipients receive the approved Fabric transaction.
	// Depending on the token driver implementation, the recipient's signature might or might not be needed to make
	// the token transaction valid.
	_, err = context.RunView(ttxcc.NewCollectEndorsementsView(tx))
	assert.NoError(err, "failed to sign issue transaction")

	// Last but not least, the issuer sends the transaction for ordering and waits for transaction finality.
	_, err = context.RunView(ttxcc.NewOrderingAndFinalityView(tx))
	assert.NoError(err, "failed to commit issue transaction")

	return tx.ID(), nil
}

Here is the view representing the recipient's operations, instead.
This view is execute by the recipient's FSC node upon a message received from the issuer.

type AcceptCashView struct{}

func (a *AcceptCashView) Call(context view.Context) (interface{}, error) {
	// The recipient of a token (issued or transfer) responds, as first operation,
	// to a request for a recipient.
	// The recipient can do that by using the following code.
	// The recipient identity will be taken from the default wallet (ttxcc.MyWallet(context)), if not otherwise specified.
	id, err := ttxcc.RespondRequestRecipientIdentity(context)
	assert.NoError(err, "failed to respond to identity request")

	// At some point, the recipient receives the token transaction that in the mean time has been assembled
	tx, err := ttxcc.ReceiveTransaction(context)
	assert.NoError(err, "failed to receive tokens")

	// The recipient can perform any check on the transaction as required by the business process
	// In particular, here, the recipient checks that the transaction contains at least one output, and
	// that there is at least one output that names the recipient. (The recipient is receiving something.
	outputs, err := tx.Outputs()
	assert.NoError(err, "failed getting outputs")
	assert.True(outputs.Count() > 0)
	assert.True(outputs.ByRecipient(id).Count() > 0)

	// The recipient here is checking that, for each type of token she is receiving,
	// she does not hold already more than 3000 units of that type.
	// Just a fancy query to show the capabilities of the services we are using.
	for _, output := range outputs.ByRecipient(id).Outputs() {
		unspentTokens, err := ttxcc.MyWallet(context).ListUnspentTokens(ttxcc.WithType(output.Type))
		assert.NoError(err, "failed retrieving the unspent tokens for type [%s]", output.Type)
		assert.True(
			unspentTokens.Sum(64).Cmp(token2.NewQuantityFromUInt64(3000)) <= 0,
			"cannot have more than 3000 unspent quantity for type [%s]", output.Type,
		)
	}

	// If everything is fine, the recipient accepts and sends back her signature.
	// Notice that, a signature from the recipient might or might not be required to make the transaction valid.
	// This depends on the driver implementation.
	_, err = context.RunView(ttxcc.NewAcceptView(tx))
	assert.NoError(err, "failed to accept new tokens")

	// Before completing, the recipient waits for finality of the transaction
	_, err = context.RunView(ttxcc.NewFinalityView(tx))
	assert.NoError(err, "new tokens were not committed")

	return nil, nil
}

Thanks to the interaction between the issuer and the recipient, the recipient becomes aware that some tokens have been issued to her. Once the transaction is final, this is what the vault of each party will contain:

  • The issuer's vault will contain a reference to the issued tokens.
  • The recipient's vault will contain a reference to the same tokens. The recipient can query the vault, or the wallet used to derive the recipient identity. We will see examples in the coming sections.

Transfer

Transfer is a business interactive protocol among at least two parties: a sender and one or more recipients.

Here is an example of a view representing the sender's operations in the transfer process:
This view is execute by the sender's FSC node.

// Transfer contains the input information for a transfer
type Transfer struct {
	// Wallet is the identifier of the wallet that owns the tokens to transfer
	Wallet string
	// TokenIDs contains a list of token ids to transfer. If empty, tokens are selected on the spot.
	TokenIDs []*token.ID
	// TokenType of tokens to transfer
	TokenType string
	// Quantity to transfer
	Quantity uint64
	// Recipient is the identity of the recipient's FSC node
	Recipient string
	// Retry tells if a retry must happen in case of a failure
	Retry bool
}

type TransferView struct {
	*Transfer
}

func (t *TransferView) Call(context view.Context) (interface{}, error) {
	// As a first step operation, the sender contacts the recipient's FSC node
	// to ask for the identity to use to assign ownership of the freshly created token.
	// Notice that, this step would not be required if the sender knew already which
	// identity the recipient wants to use.
	recipient, err := ttxcc.RequestRecipientIdentity(context, view.Identity(t.Recipient))
	assert.NoError(err, "failed getting recipient")

	// At this point, the sender is ready to prepare the token transaction.
	// The sender creates an anonymous transaction (this means that the result Fabric transaction will be signed using idemix),
	// and specify the auditor that must be contacted to approve the operation.
	tx, err := ttxcc.NewAnonymousTransaction(
		context,
		ttxcc.WithAuditor(fabric.GetDefaultIdentityProvider(context).Identity("auditor")),
	)
	assert.NoError(err, "failed creating transaction")

	// The sender will select tokens owned by this wallet
	senderWallet := ttxcc.GetWallet(context, t.Wallet)
	assert.NotNil(senderWallet, "sender wallet [%s] not found", t.Wallet)

	// The sender adds a new transfer operation to the transaction following the instruction received.
	// Notice the use of `token2.WithTokenIDs(t.TokenIDs...)`. If t.TokenIDs is not empty, the Transfer
	// function uses those tokens, otherwise the tokens will be selected on the spot.
	// Token selection happens internally by invoking the default token selector:
	// selector, err := tx.TokenService().SelectorManager().NewSelector(tx.ID())
	// assert.NoError(err, "failed getting selector")
	// selector.Select(wallet, amount, tokenType)
	// It is also possible to pass a custom token selector to the Transfer function by using the relative opt:
	// token2.WithTokenSelector(selector).
	err = tx.Transfer(
		senderWallet,
		t.TokenType,
		[]uint64{t.Quantity},
		[]view.Identity{recipient},
		token2.WithTokenIDs(t.TokenIDs...),
	)
	assert.NoError(err, "failed adding new tokens")

	// The sender is ready to collect all the required signatures.
	// In this case, the sender's and the auditor's signatures.
	// Invoke the Token Chaincode to collect endorsements on the Token Request and prepare the relative Fabric transaction.
	// This is all done in one shot running the following view.
	// Before completing, all recipients receive the approved Fabric transaction.
	// Depending on the token driver implementation, the recipient's signature might or might not be needed to make
	// the token transaction valid.
	_, err = context.RunView(ttxcc.NewCollectEndorsementsView(tx))
	assert.NoError(err, "failed to sign transaction")

	// Send to the ordering service and wait for finality
	_, err = context.RunView(ttxcc.NewOrderingAndFinalityView(tx))
	assert.NoError(err, "failed asking ordering")

	return tx.ID(), nil
}

The view representing the recipient's operations can be exactly the same of that used for the issuance, or different It depends on the specific business process.

Thanks to the interaction between the sender and the recipient, the recipient becomes aware that some tokens have been transfer to her. Once the transaction is final, the is what the vault of each party will contain:

  • The token spent will disappear form the sender's vault.
  • The recipient's vault will contain a reference to the freshly created tokens originated from the transfer. (Don't forget, we use the UTXO model here) The recipient can query the vault, or the wallet used to derive the recipient identity. We will see examples in the coming sections.

Redeem

Depending on the driver used, a sender can redeem tokens directly or by requesting redeem to an authorized redeemer. In the following, we assume that the sender redeems directly. This view is execute by the Sender's FSC node.

// Redeem contains the input information for a redeem operation
type Redeem struct {
	// Wallet is the identifier of the wallet that owns the tokens to redeem
	Wallet string
	// TokenIDs contains a list of token ids to redeem. If empty, tokens are selected on the spot.
	TokenIDs []*token.ID
	// TokenType of tokens to redeem
	TokenType string
	// Quantity to redeem
	Quantity uint64
}

type RedeemView struct {
	*Redeem
}

func (t *RedeemView) Call(context view.Context) (interface{}, error) {
	// The sender directly prepare the token transaction.
	// The sender creates an anonymous transaction (this means that the result Fabric transaction will be signed using idemix),
	// and specify the auditor that must be contacted to approve the operation.
	tx, err := ttxcc.NewAnonymousTransaction(
		context,
		ttxcc.WithAuditor(fabric.GetDefaultIdentityProvider(context).Identity("auditor")),
	)
	assert.NoError(err, "failed creating transaction")

	// The sender will select tokens owned by this wallet
	senderWallet := ttxcc.GetWallet(context, t.Wallet)
	assert.NotNil(senderWallet, "sender wallet [%s] not found", t.Wallet)

	// The sender adds a new redeem operation to the transaction following the instruction received.
	// Notice the use of `token2.WithTokenIDs(t.TokenIDs...)`. If t.TokenIDs is not empty, the Redeem
	// function uses those tokens, otherwise the tokens will be selected on the spot.
	// Token selection happens internally by invoking the default token selector:
	// selector, err := tx.TokenService().SelectorManager().NewSelector(tx.ID())
	// assert.NoError(err, "failed getting selector")
	// selector.Select(wallet, amount, tokenType)
	// It is also possible to pass a custom token selector to the Redeem function by using the relative opt:
	// token2.WithTokenSelector(selector).
	err = tx.Redeem(
		senderWallet,
		t.TokenType,
		t.Quantity,
		token2.WithTokenIDs(t.TokenIDs...),
	)
	assert.NoError(err, "failed adding new tokens")

	// The sender is ready to collect all the required signatures.
	// In this case, the sender's and the auditor's signatures.
	// Invoke the Token Chaincode to collect endorsements on the Token Request and prepare the relative Fabric transaction.
	// This is all done in one shot running the following view.
	// Before completing, all recipients receive the approved Fabric transaction.
	// Depending on the token driver implementation, the recipient's signature might or might not be needed to make
	// the token transaction valid.
	_, err = context.RunView(ttxcc.NewCollectEndorsementsView(tx))
	assert.NoError(err, "failed to sign transaction")

	// Send to the ordering service and wait for finality
	_, err = context.RunView(ttxcc.NewOrderingAndFinalityView(tx))
	assert.NoError(err, "failed asking ordering")

	return tx.ID(), nil
}

Swap

Let us now consider a more complex scenario. Alice and Bob are two business parties that want to swap some of their assets/tokens. For example, Alice sends 1 TOK to Bob in exchange for 1 KOT. The swap of these assets must happen automatically.

This view is execute by Alice's FSC node to initiate the swap.

// Swap contains the input information for a swap
type Swap struct {
	// FromWallet is the wallet A will use
	FromWallet string
	// FromType is the token type A will transfer
	FromType string
	// FromQuantity is the amount A will transfer
	FromQuantity uint64
	// ToType is the token type To will transfer
	ToType string
	// ToQuantity is the amount To will transfer
	ToQuantity uint64
	// To is the identity of the To's FSC node
	To string
}

type SwapInitiatorView struct {
	*Swap
}

func (t *SwapInitiatorView) Call(context view.Context) (interface{}, error) {
	// As a first step operation, A contacts the recipient's FSC node
	// to exchange identities to use to assign ownership of the transferred tokens.
	me, other, err := ttxcc.ExchangeRecipientIdentities(context, t.FromWallet, view.Identity(t.To))
	assert.NoError(err, "failed exchanging identities")

	// At this point, A is ready to prepare the token transaction.
	// A creates an anonymous transaction (this means that the result Fabric transaction will be signed using idemix),
	// and specify the auditor that must be contacted to approve the operation.
	tx, err := ttxcc.NewAnonymousTransaction(
		context,
		ttxcc.WithAuditor(fabric.GetDefaultIdentityProvider(context).Identity("auditor")),
	)
	assert.NoError(err, "failed creating transaction")

	// A will select tokens owned by this wallet
	senderWallet := ttxcc.GetWallet(context, t.FromWallet)
	assert.NotNil(senderWallet, "sender wallet [%s] not found", t.FromWallet)

	// A adds a new transfer operation to the transaction following the instruction received.
	err = tx.Transfer(
		senderWallet,
		t.FromType,
		[]uint64{t.FromQuantity},
		[]view.Identity{other},
	)
	assert.NoError(err, "failed adding output")

	// At this point, A is ready to collect To's transfer.
	// She does that by using the CollectActionsView.
	// A specifies the actions that she is expecting to be added to the transaction.
	// For each action, A contacts the recipient sending the transaction and the expected action.
	// At the end of the view, tx contains the collected actions
	_, err = context.RunView(ttxcc.NewCollectActionsView(tx,
		&ttxcc.ActionTransfer{
			From:      other,
			Type:      t.ToType,
			Amount:    t.ToQuantity,
			Recipient: me,
		},
	))
	assert.NoError(err, "failed collecting actions")

	// A doubles check that the content of the transaction is the one expected.
	assert.NoError(tx.Verify(), "failed verifying transaction")

	outputs, err := tx.Outputs()
	assert.NoError(err, "failed getting outputs")
	os := outputs.ByRecipient(other)
	assert.Equal(0, os.Sum().Cmp(token2.NewQuantityFromUInt64(t.FromQuantity)))
	assert.Equal(os.Count(), os.ByType(t.FromType).Count())

	os = outputs.ByRecipient(me)
	assert.Equal(0, os.Sum().Cmp(token2.NewQuantityFromUInt64(t.ToQuantity)))
	assert.Equal(os.Count(), os.ByType(t.ToType).Count())

	// A is ready to collect all the required signatures and form the Fabric Transaction.
	_, err = context.RunView(ttxcc.NewCollectEndorsementsView(tx))
	assert.NoError(err, "failed to sign transaction")

	// Send to the ordering service and wait for finality
	_, err = context.RunView(ttxcc.NewOrderingAndFinalityView(tx))
	assert.NoError(err, "failed asking ordering")

	return tx.ID(), nil
}

Here is the view representing Bob's side of the swap process,
This view is execute by Bob's FSC node upon a message received from Alice.

type SwapResponderView struct{}

func (t *SwapResponderView) Call(context view.Context) (interface{}, error) {
	// As a first step, To responds to the request to exchange token recipient identities.
	// To takes his token recipient identity from the default wallet (ttxcc.MyWallet(context)),
	// if not otherwise specified.
	_, _, err := ttxcc.RespondExchangeRecipientIdentities(context)
	assert.NoError(err, "failed getting identity")

	// To respond to a call from the CollectActionsView, the first thing to do is to receive
	// the transaction and the requested action.
	// This could happen multiple times, depending on the use-case.
	tx, action, err := ttxcc.ReceiveAction(context)
	assert.NoError(err, "failed receiving action")

	// Depending on the use case, To can further analyse the requested action, before proceeding. It depends on the use-case.
	// If everything is fine, To adds his transfer to A as requested.
	// To will select tokens from his default wallet matching the transaction
	bobWallet := ttxcc.MyWalletFromTx(context, tx)
	assert.NotNil(bobWallet, "To's default wallet not found")
	err = tx.Transfer(
		bobWallet,
		action.Type,
		[]uint64{action.Amount},
		[]view.Identity{action.Recipient},
	)
	assert.NoError(err, "failed appending transfer")

	// Once To finishes the preparation of his part, he can send Back the transaction
	// calling the CollectActionsResponderView
	_, err = context.RunView(ttxcc.NewCollectActionsResponderView(tx, action))
	assert.NoError(err, "failed responding to action collect")

	// If everything is fine, To endorses and sends back his signature.
	_, err = context.RunView(ttxcc.NewEndorseView(tx))
	assert.NoError(err, "failed endorsing transaction")

	// Before completing, the recipient waits for finality of the transaction
	_, err = context.RunView(ttxcc.NewFinalityView(tx))
	assert.NoError(err, "new tokens were not committed")

	return tx.ID(), nil
}

Queries

Here are two examples of view to list tokens.

The following view returns the list of unspent tokens:

// ListUnspentTokens contains the input to query the list of unspent tokens
type ListUnspentTokens struct {
	// Wallet whose identities own the token
	Wallet string
	// TokenType is the token type to select
	TokenType string
}

type ListUnspentTokensView struct {
	*ListUnspentTokens
}

func (p *ListUnspentTokensView) Call(context view.Context) (interface{}, error) {
	// Tokens owner by identities in this wallet will be listed
	wallet := ttxcc.GetWallet(context, p.Wallet)
	assert.NotNil(wallet, "wallet [%s] not found", p.Wallet)

	// Return the list of unspent tokens by type
	return wallet.ListUnspentTokens(ttxcc.WithType(p.TokenType))
}

This other view returns the list of issued tokens:

// ListIssuedTokens contains the input to query the list of issued tokens
type ListIssuedTokens struct {
	// Wallet whose identities own the token
	Wallet string
	// TokenType is the token type to select
	TokenType string
}

type ListIssuedTokensView struct {
	*ListIssuedTokens
}

func (p *ListIssuedTokensView) Call(context view.Context) (interface{}, error) {
	// Tokens issued by identities in this wallet will be listed
	wallet := ttxcc.GetIssuerWallet(context, p.Wallet)
	assert.NotNil(wallet, "wallet [%s] not found", p.Wallet)

	// Return the list of issued tokens by type
	return wallet.ListIssuedTokens(ttxcc.WithType(p.TokenType))
}

Testing

To run the Fungible Tokens sample, one needs first to deploy the Fabric Smart Client and the Fabric networks. Once these networks are deployed, one can invoke views on the smart client nodes to test the Fungible Tokens sample.

So, first step is to describe the topology of the networks we need.

Describe the topology of the networks

To test the above views, we have to first clarify the topology of the networks we need. Namely, Fabric and FSC networks.

For Fabric, we will use a simple topology with:

  1. Two organization: Org1 and Org2;
  2. Single channel;
  3. Org1 runs/endorse the Token Chaincode.

For the FSC network, we have a topology with a node for each business party.

  1. Issuer and Auditor have an Org1 Fabric Identity;
  2. Alice, Bob, and Charlie have an Org2 Fabric Identity.

We can describe the network topology programmatically as follows:

func Topology(tokenSDKDriver string) []api.Topology {
	// Fabric
	fabricTopology := fabric.NewDefaultTopology()
	fabricTopology.EnableIdemix()
	fabricTopology.AddOrganizationsByName("Org1", "Org2")
	fabricTopology.SetNamespaceApproverOrgs("Org1")

	// FSC
	fscTopology := fsc.NewTopology()
	// fscTopology.SetLogging("grpc=error:debug", "")

	// issuer
	issuer := fscTopology.AddNodeByName("issuer").AddOptions(
		fabric.WithOrganization("Org1"),
		fabric.WithAnonymousIdentity(),
		token.WithDefaultIssuerIdentity(),
		token.WithIssuerIdentity("issuer.id1"),
	)
	issuer.RegisterViewFactory("issue", &views.IssueCashViewFactory{})
	issuer.RegisterViewFactory("issued", &views.ListIssuedTokensViewFactory{})

	// auditor
	auditor := fscTopology.AddNodeByName("auditor").AddOptions(
		fabric.WithOrganization("Org1"),
		fabric.WithAnonymousIdentity(),
		token.WithAuditorIdentity(),
	)
	auditor.RegisterViewFactory("register", &views.RegisterAuditorViewFactory{})

	// alice
	alice := fscTopology.AddNodeByName("alice").AddOptions(
		fabric.WithOrganization("Org2"),
		fabric.WithAnonymousIdentity(),
		token.WithDefaultOwnerIdentity(tokenSDKDriver),
		token.WithOwnerIdentity(tokenSDKDriver, "alice.id1"),
	)
	alice.RegisterResponder(&views.AcceptCashView{}, &views.IssueCashView{})
	alice.RegisterResponder(&views.AcceptCashView{}, &views.TransferView{})
	alice.RegisterViewFactory("transfer", &views.TransferViewFactory{})
	alice.RegisterViewFactory("redeem", &views.RedeemViewFactory{})
	alice.RegisterViewFactory("swap", &views.SwapInitiatorViewFactory{})
	alice.RegisterViewFactory("unspent", &views.ListUnspentTokensViewFactory{})

	// bob
	bob := fscTopology.AddNodeByName("bob").AddOptions(
		fabric.WithOrganization("Org2"),
		fabric.WithAnonymousIdentity(),
		token.WithDefaultOwnerIdentity(tokenSDKDriver),
		token.WithOwnerIdentity(tokenSDKDriver, "bob.id1"),
	)
	bob.RegisterResponder(&views.AcceptCashView{}, &views.IssueCashView{})
	bob.RegisterResponder(&views.AcceptCashView{}, &views.TransferView{})
	bob.RegisterResponder(&views.SwapResponderView{}, &views.SwapInitiatorView{})
	bob.RegisterViewFactory("transfer", &views.TransferViewFactory{})
	bob.RegisterViewFactory("redeem", &views.RedeemViewFactory{})
	bob.RegisterViewFactory("swap", &views.SwapInitiatorViewFactory{})
	bob.RegisterViewFactory("unspent", &views.ListUnspentTokensViewFactory{})

	// charlie
	charlie := fscTopology.AddNodeByName("charlie").AddOptions(
		fabric.WithOrganization("Org2"),
		fabric.WithAnonymousIdentity(),
		token.WithDefaultOwnerIdentity(tokenSDKDriver),
		token.WithOwnerIdentity(tokenSDKDriver, "charlie.id1"),
	)
	charlie.RegisterResponder(&views.AcceptCashView{}, &views.IssueCashView{})
	charlie.RegisterResponder(&views.AcceptCashView{}, &views.TransferView{})
	charlie.RegisterResponder(&views.SwapResponderView{}, &views.SwapInitiatorView{})
	charlie.RegisterViewFactory("transfer", &views.TransferViewFactory{})
	charlie.RegisterViewFactory("redeem", &views.RedeemViewFactory{})
	charlie.RegisterViewFactory("swap", &views.SwapInitiatorViewFactory{})
	charlie.RegisterViewFactory("unspent", &views.ListUnspentTokensViewFactory{})

	tokenTopology := token.NewTopology()
	tokenTopology.SetDefaultSDK(fscTopology)
	tms := tokenTopology.AddTMS(fabricTopology, tokenSDKDriver)
	tms.SetNamespace([]string{"Org1"}, "100", "2")

	return []api.Topology{fabricTopology, tokenTopology, fscTopology}
}

The above topology takes in input the token driver name.

Boostrap the networks

To help us bootstrap the networks and then invoke the business views, the fungible command line tool is provided. To build it, we need to run the following command from the folder $GOPATH/src/github.com/hyperledger-labs/fabric-token-sdk/samples/fabric/fungible.

go build -o fungible

If the compilation is successful, we can run the fungible command line tool as follows:

./fungible network start --path ./testdata

The above command will start the Fabric network and the FSC network, and store all configuration files under the ./testdata directory. The CLI will also create the folder ./cmd that contains a go main file for each FSC node. The CLI compiles these go main files and then runs them.

If everything is successful, you will see something like the following:

2022-02-09 14:17:06.705 UTC [nwo.network] Start -> INFO 032  _____   _   _   ____
2022-02-09 14:17:06.705 UTC [nwo.network] Start -> INFO 033 | ____| | \ | | |  _ \
2022-02-09 14:17:06.705 UTC [nwo.network] Start -> INFO 034 |  _|   |  \| | | | | |
2022-02-09 14:17:06.705 UTC [nwo.network] Start -> INFO 035 | |___  | |\  | | |_| |
2022-02-09 14:17:06.705 UTC [nwo.network] Start -> INFO 036 |_____| |_| \_| |____/
2022-02-09 14:17:06.705 UTC [fsc.integration] Serve -> INFO 037 All GOOD, networks up and running...
2022-02-09 14:17:06.705 UTC [fsc.integration] Serve -> INFO 038 If you want to shut down the networks, press CTRL+C
2022-02-09 14:17:06.705 UTC [fsc.integration] Serve -> INFO 039 Open another terminal to interact with the networks

To shut down the networks, just press CTRL-C.

If you want to restart the networks after the shutdown, you can just re-run the above command. If you don't delete the ./testdata directory, the network will be started from the previous state.

Before restarting the networks, one can modify the business views to add new functionalities, to fix bugs, and so on. Upon restarting the networks, the new business views will be available. Later on, we will see an example of this.

To clean up all artifacts, we can run the following command:

./fungible network clean --path ./testdata

The ./testdata and ./cmd folders will be deleted.

Invoke the business views

If you reached this point, you can now invoke the business views on the FSC nodes.

To issue a fungible token, we can run the following command:

./fungible view -c ~/testdata/fsc/nodes/issuer/client-config.yaml -f issue -i "{\"TokenType\":\"TOK\", \"Quantity\":10, \"Recipient\":\"alice\"}"

The above command invoke the issue view on the issuer's FSC node. The -c option specifies the client configuration file. The -f option specifies the view name. The -i option specifies the input data. In the specific case, we are asking the issuer to issue a fungible token whose type is TOK and quantity is 10, and the recipient is alice. If everything is successful, you will see something like the following:

"594fbed71f03879b95463d9f68bc6af2f221207840c4a943faa054f729e0752e"

The above is the transaction id of the transaction that issued the fungible token.

Indeed, once the token is issued, the recipient can query its wallet to see the token.

./fungible view -c ~/testdata/fsc/nodes/alice/client-config.yaml -f unspent -i "{\"TokenType\":\"TOK\"}"

The above command will query Alice's wallet to get a list of unspent tokens whose type TOK. You can expect to see an output like this (beautified):

{
  "tokens": [
    {
      "Id": {
        "tx_id": "594fbed71f03879b95463d9f68bc6af2f221207840c4a943faa054f729e0752e"
      },
      "Owner": {
        "raw": "MIIEmxMCc2kEggSTCgxJZGVtaXhPcmdNU1ASggkKIAHCT4uQAPEP1883dXxsJYXshm+r/8Sl+KWKQBPtsDMtEiAN1qnd8QKF2YSv6Bftzt1v5XqH30yzJqlzp777FoBRsxpZCgxJZGVtaXhPcmdNU1ASJ2RlZmF1bHQtdGVzdGNoYW5uZWwtZGVmYXVsdC5leGFtcGxlLmNvbRogAvYocoRRzxnU/3a5uT0Lo/0S+q6nBi5/kLf96ay1gywiEAoMSWRlbWl4T3JnTVNQEAEqzgcKRAogDCvVIZKf3BjWSglYHs9hpmIYFoivU2Ny/ikuMz55VmkSICGZg9JMY+E2eGDpXMowMIW/q6qr5f6ruBHewxDeonVhEkQKICqW2eFFYce3rL+W+vu4YEW83SBWpBUae3ZXdtpytC/BEiAO7VqjyDybt5G4yIkRnZt0MaizCkXEIrEcTF5GhEdYdBpECiAAboYv9DUo8rfC2Nn9tF2YoyVh35YrYX60mjfMS/ugGxIgBRL1EIkE2PWm/prA1S4hAwrmHRnnF2FyrKkK1HJVU3siIBB/5FVWOQnly0LQYeW8Xhm1Y1zNFU4YAZ3n4Pn+W1B6KiAlZdXIel/1SQMDjOwUTKJCenIa8gl2Tyg2p3jWtNX/AjIgELICUsvWTp74zgOVGpCdVpYllbLF19jTPup0sDEQHRc6IB5z48Dokk3HGmfT555wnvwLELoid1lmmRlOmlhSfo3rQiANhc+0gpNXAS2XVoZ+fWcROim/cCK1b/f25/YJp9+42EogEsE+x5fHs+4n4+ve8Lnz8UBdvdEYTdfHNQfjETofOr1SIA6Fc43xTun9EYUKuDIpPdVqLli9BilsmHmhhyKv2U6HUiAUYOPpVIbr4PwDpEd5QdET9bu+qIZTx1SQEsq3Ky8TTlogIDYq3UNHAY1tdC0+fiVUM5c2WWBcehqcavMlTDuQxWpiRAogAcJPi5AA8Q/Xzzd1fGwlheyGb6v/xKX4pYpAE+2wMy0SIA3Wqd3xAoXZhK/oF+3O3W/leoffTLMmqXOnvvsWgFGzaiAl/5bWO/zuNCcqEUqLQC7L8ySTQSg5e7w9jP+IZsNc23KIAQogGY6Tk5INSDpyYL+3MftdJfGqSTM1qecSl+SFt67zEsISIBgA3u8SHx52QmoAZl5cRHlnQyLU917a3UbevVzZkvbtGiAJBonQWF/wdeyema1pDDOVvEsxM3CzjvNVrNrc0SKXWyIgEshepduMbetKq3GAjctAj+PR52kMQ9N7TObMAWb6fap6ZzBlAjEA3YJerRXWIkSqyNJj9GWNV1iGDgRaR9F8uSP1ogYdmY0ORg8zIgwntDg41rS7E+JSAjAQowDJ4JD5jNx2O4AFd8JqBQd0BcrEfw+QdlFVhqiHG8wjX67/cT369jPpZtajxdCKAQCSAWgKRAogB3S14S2mIIQOadbFeGXNFOJjxTYcMdMDgO0wxm6M6IISIBp8N5qfXX941p2f4jklZ9lOe07TaD/XvPq9MgljWUikEiAKUJC0MZGIHvJf9lkIHgaZlS00Qi5S30vWvId1rLeqeg=="
      },
      "Type": "TOK",
      "DecimalQuantity": "10"
    }
  ]
}

Alice can now transfer some of her tokens to other parties. For example:

./fungible view -c ~/testdata/fsc/nodes/alice/client-config.yaml -f transfer -i "{\"TokenType\":\"TOK\", \"Quantity\":6, \"Recipient\":\"bob\"}"

The above command instructs Alice's node to perform a transfer of 6 units of tokens TOK to bob. If everything is successful, you will see something like the following:

"26e1546970b96299680040b3409cfcd486854cbbf13e1e46d272cf85127b271c"

The above is the transaction id of the transaction that transferred the tokens.

Now, we check again Alice and Bob's wallets to see if they are up-to-date.

Alice:

./fungible view -c ~/testdata/fsc/nodes/alice/client-config.yaml -f unspent -i "{\"TokenType\":\"TOK\"}"

You can expect to see an output like this (beautified):

{
  "tokens": [
    {
      "Id": {
        "tx_id": "26e1546970b96299680040b3409cfcd486854cbbf13e1e46d272cf85127b271c",
        "index": 1
      },
      "Owner": {
        "raw": "MIIEmxMCc2kEggSTCgxJZGVtaXhPcmdNU1ASggkKIBcbZLS9FhXl09IqczHADl22FHW6oSFxpFVy1lzjnvKwEiAAE7YWWVG3xkUt4Hj37t3maDh35u3GQSgS4Xe7p0seUxpZCgxJZGVtaXhPcmdNU1ASJ2RlZmF1bHQtdGVzdGNoYW5uZWwtZGVmYXVsdC5leGFtcGxlLmNvbRogAvYocoRRzxnU/3a5uT0Lo/0S+q6nBi5/kLf96ay1gywiEAoMSWRlbWl4T3JnTVNQEAEqzgcKRAogGi67Lnc5ponluEm4YB/ZtnYDNOn9rRwgirw7KwDAuvASICLCJCB3zoQYnt76MTVjKwrEPTW9ShADObpX8RGGo1WmEkQKIA3npyjuwVfxyCJ8RZCl4Q3lZ7BbRZW6skkVV4uJ9LDaEiAHO2Vh4HSjO1ct44w5Evp1YdjGC0l2N+EoGGg1jjRKzhpECiAl+Wmp34OubzO64TyntSBoAjCPCc1gfBXB2oyQMnuwMxIgE5W0f0qayux2/lJmbs4IDSgVxwHdwFtfDcadrMoqHBAiIAVigpyA1/M9bEZX9ihbgKRFHVlGq5gFrTUF/7JMc39wKiAdp6WB7NGy+kpaBNMpquYGhNijjmbkArgLzLxx0jdefDIgHqYoHP33vdOi+/Hwec1nIpC3b5KMto4oVQlcjwKIfRs6ICoNL+Sm/a6BqOAoju1UoCCxHnVkROk9HCl7UQR42lIuQiAmJEQkJAHGriYhh+6mcrsf/uH6p3JSSzBGDOSuw1qNGEogGnFtcE6Hde4P7knQppY9148A7WB03x22DhETnPEoM4xSIB/bPQ1oL1DZURI34B7HeRNwueR/kTti2S7FYXRjLq15UiAprNE30EAxMFQ9juO5FdSpWrOi1Wn2yHrG30G+5+9x8logFYxXD3llanJ4SCKAx0dJ9bFsQPtplbE2Ad3UdOD99qliRAogFxtktL0WFeXT0ipzMcAOXbYUdbqhIXGkVXLWXOOe8rASIAATthZZUbfGRS3gePfu3eZoOHfm7cZBKBLhd7unSx5TaiAFPZYpfR8yNh2lfOonbXZKWDu6A9nn1k93XdTKbBE2MnKIAQogGY6Tk5INSDpyYL+3MftdJfGqSTM1qecSl+SFt67zEsISIBgA3u8SHx52QmoAZl5cRHlnQyLU917a3UbevVzZkvbtGiAJBonQWF/wdeyema1pDDOVvEsxM3CzjvNVrNrc0SKXWyIgEshepduMbetKq3GAjctAj+PR52kMQ9N7TObMAWb6fap6ZzBlAjEA3YJerRXWIkSqyNJj9GWNV1iGDgRaR9F8uSP1ogYdmY0ORg8zIgwntDg41rS7E+JSAjAQowDJ4JD5jNx2O4AFd8JqBQd0BcrEfw+QdlFVhqiHG8wjX67/cT369jPpZtajxdCKAQCSAWgKRAogKthwRsZE5VxE3m6IHJ5ifN0E9fWuAPoOhnOlkhsFvPQSIAbTbuvi6fuccEzywmq6nIO8zgFW1YbkFevHggIDeZjqEiAdSXW+RnXJkI/uN7QPlcrmvAxmF4qH7v9wAy0QKTceaA=="
      },
      "Type": "TOK",
      "DecimalQuantity": "4"
    }
  ]
}

Then, Bob:

./fungible view -c ~/testdata/fsc/nodes/bob/client-config.yaml -f unspent -i "{\"TokenType\":\"TOK\"}"

You can expect to see an output like this (beautified):

{
  "tokens": [
    {
      "Id": {
        "tx_id": "26e1546970b96299680040b3409cfcd486854cbbf13e1e46d272cf85127b271c"
      },
      "Owner": {
        "raw": "MIIEmxMCc2kEggSTCgxJZGVtaXhPcmdNU1ASggkKICUQcEN/52Caweb0KpnfO/ThAFKHOv4a3c8Tdmyd4nXdEiAedZFC0VgWtS41eoFPbyW5HCt32JecChY7VSkqSj58bhpZCgxJZGVtaXhPcmdNU1ASJ2RlZmF1bHQtdGVzdGNoYW5uZWwtZGVmYXVsdC5leGFtcGxlLmNvbRogAvYocoRRzxnU/3a5uT0Lo/0S+q6nBi5/kLf96ay1gywiEAoMSWRlbWl4T3JnTVNQEAEqzgcKRAogCPq27TzRNd5juExU3tG8Gz093rrctGSDw8j70UtVHKoSIBoTzD15AU+k3dK+mpEOh15tmreZUDFPjTGpWJnoT5SMEkQKIAzXax75L8lZifdm2Mw3mOZP9BOl0iTbHC+sJ9u6tgcDEiAiMwrR2zsUowm2O4kUj6wQBVoWRfKahQgLIdXoQBjlnxpECiAQOAyPj4KAG4wgHxj1ZzwlVu4V2VOCmaU+ORTMBucINxIgIlcGWlHAMn8iLVHIj64B0IvGCayFtSEb5oicruv2llciIBjCsbLVbhQm0EABn8fqycPmiOnJwBRkLEIcVoE6iJ8GKiAYAEyH+ZCg6nrZvLbM8IgYdvyPYHDFD4WGfcfMOyQcuzIgHRSPOHxlcneVnn+IY28Rrd6qsfTpNPPrfk12jAwo5BA6IBNB3AoffpT1iDS05wEKAV1khK3r+6aa/1aOZWM8RWkhQiAq2gHduDKhSbTO3lIw0Nv+u7popduVFHBub/yBw1yN+kogAhg82iwgvf+ymaRNO8XXAq/XFzt6ZHJKsAD0q/Uz6cdSIBcOaU4ttkLHTJT9WrKLoA1L++1fB9XEpB1IC2q4w/cjUiAik8nVmX7OKKR8vF6IovWKVfn4EB1J2a3japRFpHYIqVogK+PzUp1i3gp4QKycTbvMGz7kYRBnan012GUEA+UzfxhiRAogJRBwQ3/nYJrB5vQqmd879OEAUoc6/hrdzxN2bJ3idd0SIB51kULRWBa1LjV6gU9vJbkcK3fYl5wKFjtVKSpKPnxuaiAgqHc732XZd7yfKCpnk9XH4toLpFu/odPT5LrZSgUeWnKIAQogGY6Tk5INSDpyYL+3MftdJfGqSTM1qecSl+SFt67zEsISIBgA3u8SHx52QmoAZl5cRHlnQyLU917a3UbevVzZkvbtGiAJBonQWF/wdeyema1pDDOVvEsxM3CzjvNVrNrc0SKXWyIgEshepduMbetKq3GAjctAj+PR52kMQ9N7TObMAWb6fap6ZzBlAjEAyA9+362MWyeF8QllJYEba/zs0x2wTF60ZtteERimKRQQLVdJzBpx9MgyonUNIy4CAjAdyMpi+0ki6re3HW+uma/qN2qr2WMVJA4PgIhdBmR+b8zb6+NyvNJ/t8waF3ZqUhqKAQCSAWgKRAogG9kaVKu1CdP8L+1n0juQyQJaVjn5fXpKMjHymrpAlrsSIAbB6cz91ElZIYuJJGCCIBHYJiEkvrWAudqTPfjF9iuSEiAkoA9Pt5cG3xyzGyhN37nGRo33VWXkYcQYaHqoXwin6w=="
      },
      "Type": "TOK",
      "DecimalQuantity": "6"
    }
  ]
}

Let us try something more complex: A swap. Let us consider the following scenario: Alice wants to swap 1 unit of TOK tokes with 1 unit of KOT tokens owned by Charlie.

Because KOT tokens don't exist yet, let us issue them first. The following command instructs the issuer to issue KOT tokens to Charlie.

./fungible view -c ~/testdata/fsc/nodes/issuer/client-config.yaml -f issue -i "{\"TokenType\":\"KOT\", \"Quantity\":10, \"Recipient\":\"charlie\"}"

This is transaction id of the transaction that issues KOT tokens

"ca195ab8072f12d6e0ed78d977404668e0523dbd45b72991cb2cd853de58e5e8"

Let us query Charlie's wallet

./fungible view -c ~/testdata/fsc/nodes/charlie/client-config.yaml -f unspent -i "{\"TokenType\":\"KOT\"}"

You can expect to see an output like this (beautified):

{
  "tokens": [
    {
      "Id": {
        "tx_id": "ca195ab8072f12d6e0ed78d977404668e0523dbd45b72991cb2cd853de58e5e8"
      },
      "Owner": {
        "raw": "MIIEnBMCc2kEggSUCgxJZGVtaXhPcmdNU1ASgwkKIAdT9TwkNH+V6BQ+/lI5b4Am76lF0Vk8ZpQEYTWZ6cGBEiAV8SYHdCviYcgBXzePH4rckaBMIXR0T3jbTn/rORABtBpZCgxJZGVtaXhPcmdNU1ASJ2RlZmF1bHQtdGVzdGNoYW5uZWwtZGVmYXVsdC5leGFtcGxlLmNvbRogAvYocoRRzxnU/3a5uT0Lo/0S+q6nBi5/kLf96ay1gywiEAoMSWRlbWl4T3JnTVNQEAEqzwcKRAogFFpI5IVoV2oIhJnl477WAWeTYiDrHnr/V6QodyFyycESIBxPmg2eZO6wtiSt1nbmpo1yZIxIUNyiKy8XpsYc5cJcEkQKICT9MLpXc0pfYY6KGsvGcvl16gPrg+BdRl7WV0CgUmnoEiAcGBrN3/5wX+eWG31xX69gzM+c9Nk1imtyLG4qoqF25xpECiAjslZCXcCHHUaPv00mVW4RKPwRxMI9gZUSCyWc2Q8byxIgBk7LJ8vpIn85VpE4ZVPgUypb4P3SjPhXln2u07fPNrIiIBOxJvXSouyqljfNJb8S5ThNFO7U46N216c4f9sAjfP0KiAkozy8lbwlKhuPiPA0jqqLGAt0X1mxQKy8ZgTnZ43HRDIgAozvYrwYm7cYuU085+C7uvP7KTMHO9luNv8n1vHzf0I6IBM2SDoCyhitQCDrkki0e+HvbNjQ1EWVy7Dbd74G8EgBQiAeJV6NSR4/l9gfFD3zxpeBNtPrgSYqPSN19I19XcsmfEogK8NJLi+4gaRdALNAOsQddfynlKHlQAYV8YOXuaIInzNSIBVfum33qgBFZYCWFKLRN3/WJj1yg7d802tHZewoy4glUiAb8O+SUBKtG6SQxcFijrRUw6D6jWBg9xj0PlSpB/JZLFogI1U7eydWvNENNmIQlgaIs+5oFwz2gyfcaYOdR6clbPtiRAogB1P1PCQ0f5XoFD7+UjlvgCbvqUXRWTxmlARhNZnpwYESIBXxJgd0K+JhyAFfN48fityRoEwhdHRPeNtOf+s5EAG0aiAglY3WmDBF0eg5FHCVfVCt3wD6NSkyFUzFzcbesfYNGHKIAQogGY6Tk5INSDpyYL+3MftdJfGqSTM1qecSl+SFt67zEsISIBgA3u8SHx52QmoAZl5cRHlnQyLU917a3UbevVzZkvbtGiAJBonQWF/wdeyema1pDDOVvEsxM3CzjvNVrNrc0SKXWyIgEshepduMbetKq3GAjctAj+PR52kMQ9N7TObMAWb6fap6aDBmAjEAzjgDxfFwSzA2VLKaQofXmbQCsPM3CBvzU64G/Yem38fybHlkSnnyv/zGRv275Fj7AjEAldpJLRUrK6TPxxOhMZsxeklROHRM+RZOe+9B7UBuojYA8RJXRRALE2rqB+EiyCXiigEAkgFoCkQKIBWjarOnKvfmEIXEfpIymPYlwoa5GwamwkQLU+dDIHLMEiAOfw/mXGo3WcuamtnWab6JRu6NjgwvQWSrkgwtLu/rOhIgAHCrCKb8BvO46JAJkgZGBy+44K1ljJD6VMER0O4/H4k="
      },
      "Type": "KOT",
      "DecimalQuantity": "10"
    }
  ]
}

Now, we are ready to orchestrate the swap. The above command instructs Alice's node to start the swap we described above whose business process was described in the previous sections.

./fungible view -c ~/testdata/fsc/nodes/alice/client-config.yaml -f swap -i "{\"FromType\":\"TOK\", \"FromQuantity\":1,\"ToType\":\"KOT\", \"ToQuantity\":1, \"To\":\"charlie\"}"

If everything is successfully, you can expect to see the transaction id of the transaction that performed the swap.

"790098ae67dc5157c7d679b8def39983adb6e9d7070209f5ab12938d45e1630d"

We can now query the wallets of Alice and Charlie to confirm the swap happened.

Alice:

./fungible view -c ~/testdata/fsc/nodes/alice/client-config.yaml -f unspent -i "{\"TokenType\":\"\"}"

You can expect to see that Alice has 3 units of TOK tokens, and 1 unit of KOT tokens.

{
  "tokens": [
    {
      "Id": {
        "tx_id": "790098ae67dc5157c7d679b8def39983adb6e9d7070209f5ab12938d45e1630d",
        "index": 1
      },
      "Owner": {
        "raw": "MIIEmxMCc2kEggSTCgxJZGVtaXhPcmdNU1ASggkKICJZpYJkYEt1+0bTwp9yIsbIwT+9uTyDkCvaBEZQINZMEiAINuE8/cVzKaQUKcbUX31hMkKuDAk9AQ7780C0ZVXRTRpZCgxJZGVtaXhPcmdNU1ASJ2RlZmF1bHQtdGVzdGNoYW5uZWwtZGVmYXVsdC5leGFtcGxlLmNvbRogAvYocoRRzxnU/3a5uT0Lo/0S+q6nBi5/kLf96ay1gywiEAoMSWRlbWl4T3JnTVNQEAEqzgcKRAogLttMr+WJ06CTka5Kel024/b0IQTMzgoRxq3bs6aTZ18SIA2DpE4uY2foJ8GkPy1z6wmGCPjMGYJjnaZ+96crIwqNEkQKICrCRwCQIDQong3+z2emWZJC6MGnEO9DaYb3+fjKmTfSEiAtCGFNA8z1rhgQjxdVUFNvrk3f/gb8H/E9e+tOu888pxpECiAoBB0GmanqV+SEkP/u3sJiI/LllZ13gtiiZzfqO9JKoBIgF90F29Rc3fXp/fKRjIY7Dcg+bWN4EV8B+tw1ft9196QiIAjDN69DMy27cR5ykFsYsRJey0/xpSqtZwdEpy71TsGYKiAPR/JgslMlgkzGuhp+ZH3PL7Kapl4VzdexwfL8rZDppDIgI02k6BlrsT2Vh2nIXH7NzZGhqs91Xgdlzc8CIjjzskM6IBBqjy4rKIh8MrWHyyXZ4UNWUC4ddCYCJsbeO8dqt+RlQiALPa15oWp++3RPMM5GMX5yUTHCMjH+BGzV8jIBjmJhlkogEjsFfo7PPEV/IAWDNDirnEiXLhhXNXWrp+75/IHzyAlSICMml9G4B2Ci6dvkCU60tXBjsjMOhnWrOElU1c/AG6g5UiARiUDjF9zP5Awdm1hQN2rLCyGqbEOhvxO6HAN9sw+p7FogCCnGnwolxd6rwE6onuOgmm6X4L9mSxmI21q2Of8Ki+RiRAogIlmlgmRgS3X7RtPCn3IixsjBP725PIOQK9oERlAg1kwSIAg24Tz9xXMppBQpxtRffWEyQq4MCT0BDvvzQLRlVdFNaiAFcdKLBbkxlV+8B7Db3LwdHBKPJ58HiMgMMkv1NA3xKXKIAQogGY6Tk5INSDpyYL+3MftdJfGqSTM1qecSl+SFt67zEsISIBgA3u8SHx52QmoAZl5cRHlnQyLU917a3UbevVzZkvbtGiAJBonQWF/wdeyema1pDDOVvEsxM3CzjvNVrNrc0SKXWyIgEshepduMbetKq3GAjctAj+PR52kMQ9N7TObMAWb6fap6ZzBlAjEA3YJerRXWIkSqyNJj9GWNV1iGDgRaR9F8uSP1ogYdmY0ORg8zIgwntDg41rS7E+JSAjAQowDJ4JD5jNx2O4AFd8JqBQd0BcrEfw+QdlFVhqiHG8wjX67/cT369jPpZtajxdCKAQCSAWgKRAogFllIt96ke1j9jVuzhdAQz+KRlOtM6SKFR6rwVukkOakSICKsh2fdSfpzP0r7tNUfoOCTw9KQKkq11WwHYN6RmtlsEiAs7EYkgGcoKDHky5yXN7qQjr/FFQfEKkG8KgEbAWy5Og=="
      },
      "Type": "TOK",
      "DecimalQuantity": "3"
    },
    {
      "Id": {
        "tx_id": "790098ae67dc5157c7d679b8def39983adb6e9d7070209f5ab12938d45e1630d",
        "index": 2
      },
      "Owner": {
        "raw": "MIIEmxMCc2kEggSTCgxJZGVtaXhPcmdNU1ASggkKIAVafcxfRK0H0QWSQM8oy2p+CIWRfDxHGxIVdQwMiZvUEiAN310C96rxXnLZ0xbR1TFj7uWYyXwgXJYy6DR4vOgbTRpZCgxJZGVtaXhPcmdNU1ASJ2RlZmF1bHQtdGVzdGNoYW5uZWwtZGVmYXVsdC5leGFtcGxlLmNvbRogAvYocoRRzxnU/3a5uT0Lo/0S+q6nBi5/kLf96ay1gywiEAoMSWRlbWl4T3JnTVNQEAEqzgcKRAogCuypu8bCCgJz8jeQuLeMfAUpt57B8r91GKuBxy0EDxUSIASP2yYOOTaLvle/TfM79FCsWYnmsjjy4OokZ2MfwbsREkQKICTppfd2awQh4bXRuHuSYyaoBKVDl/KOpw5RTeYZ6eebEiAIruuLKQwpSo6iHrV8ui/JiKPPEX3/8AAvvJJJlc6lzxpECiAnDCyWWnk3T7r515qQM+Jp0gKYMSSJ4W11+r7lEtrQsBIgDKnAphIt06U04MfGR8F7YRf1mVIi7Z+8aBV3VIK+WxIiIBjPazywKo/XaxorqU2Mnyz138W4zE7lfyYaF7b39ahtKiADlNiVapRCF7NiFjEE5yGRpRZFiRUYXwzw5rF3R4i05zIgJd2JbWMXIaIILplk3Ui9PiAMXYI6Q2uC/TVfU4j6H/M6ICMegnZ40/geKGj3+NAQO0ONkWK+GpyijyobmznG1uD0QiAZDctUyhkwccsEUSLkOdR6aR9yTAzcfalbBv5oItTuQkogCN+dLBPNx8IUPfwes3I2LvupSen6jCZhygLeTvREEy1SICJgsck+7Kdwt03svl5DPg95B9ST8MkrTCLWlGTkghHyUiATlAEj7R5uZy7wKqqJnoJWR9tPPaVdVsz3O3GuI2dXhFogCj9aW1AE+qVx+Q5G7/bvaMlaJaWjidMdeHU/CfKV5kZiRAogBVp9zF9ErQfRBZJAzyjLan4IhZF8PEcbEhV1DAyJm9QSIA3fXQL3qvFectnTFtHVMWPu5ZjJfCBcljLoNHi86BtNaiAQDx6X9QOy6aj9LV0FHoxdfLZrnelqR4BwGjNmdgEhG3KIAQogGY6Tk5INSDpyYL+3MftdJfGqSTM1qecSl+SFt67zEsISIBgA3u8SHx52QmoAZl5cRHlnQyLU917a3UbevVzZkvbtGiAJBonQWF/wdeyema1pDDOVvEsxM3CzjvNVrNrc0SKXWyIgEshepduMbetKq3GAjctAj+PR52kMQ9N7TObMAWb6fap6ZzBlAjEA3YJerRXWIkSqyNJj9GWNV1iGDgRaR9F8uSP1ogYdmY0ORg8zIgwntDg41rS7E+JSAjAQowDJ4JD5jNx2O4AFd8JqBQd0BcrEfw+QdlFVhqiHG8wjX67/cT369jPpZtajxdCKAQCSAWgKRAogFLurOOSmbEEk7yNEwRX9ghWk7IbWDnA8V9kLB6gEWyMSIBOjwYbM9HA30fr9usDn16m5chNpyDEgi6ktlhfRTQ4REiAwXYEeOISdw+4YSSVS9PeqZK5f5EqsV/6xwALU20XJ5w=="
      },
      "Type": "KOT",
      "DecimalQuantity": "1"
    }
  ]
}

Charlie:

./fungible view -c ~/testdata/fsc/nodes/charlie/client-config.yaml -f unspent -i "{\"TokenType\":\"\"}"

You can expect to see that Charlie has 1 unit of TOK tokens and still 9 units of KOT tokens.

{
  "tokens": [
    {
      "Id": {
        "tx_id": "790098ae67dc5157c7d679b8def39983adb6e9d7070209f5ab12938d45e1630d"
      },
      "Owner": {
        "raw": "MIIEnBMCc2kEggSUCgxJZGVtaXhPcmdNU1ASgwkKICGsY7CO3ltYugDI2GImFCD5WOCJq7HDI0I+Ki41nM/XEiAVb1ZOtv+Aod7xNOdpeTudN88+7sRSDTmx5yW4VukgJBpZCgxJZGVtaXhPcmdNU1ASJ2RlZmF1bHQtdGVzdGNoYW5uZWwtZGVmYXVsdC5leGFtcGxlLmNvbRogAvYocoRRzxnU/3a5uT0Lo/0S+q6nBi5/kLf96ay1gywiEAoMSWRlbWl4T3JnTVNQEAEqzwcKRAogGXnVxqgPuf0/pbEjKQV01cCFrWF9mCJBubPOSnpmrUgSIBr4WDNTvTmcuLYXDJc+5PVZgojMw1edFlxKdo/hJBAvEkQKIAoBWg7DYx1SrHiJu3okRrMRYidR39v9R/visJwLmifjEiAVQuPsDdTupMpuM65dT+mD+simDhQf/jmgWlFrhmzsVBpECiAb4Jz/xjedjxp4XvFz5I9xZDGN952T3yMv6viybDqo7hIgA8LebWf/Kr4o2lPHDS04m/hkviuKhfLZcmj/OpLDkuAiICWZHBevFDIGmSJ+3ibmH55zccGGNTFTPVAZ28JxSKASKiAZzuai4Qz2PxdEcALgVSgWox3Nslxi9uK6LkJ+lb61JjIgI3MPK5Abo+rU+zOlsVyQeQH4UyqLqWlidUGdAd6Ipf06IBt5IxBUiO29clqMNjFtjtN9ZcmY110CcADI7yu2SRbaQiAYFBpAre8HUOy/42h59HlwXdXlFFW6IvGBznpKkNsqgkogKxVRAtjKZkrpYFlgRvPoUgs/Rb2zhMwWrPO8iV0X2CRSICsmtLvRi+jZppqY5lWenFBVQ1rW9p9n4nrWngVJ92h9UiAYMdYcyGCOYDdckx8aEgV7Owo1uAcB8Gcpti1Uej0vwFogHYpzOrGr8h9+x/2Pz/GT6oDYOjosCY2tmeWdyzmLy3piRAogIaxjsI7eW1i6AMjYYiYUIPlY4ImrscMjQj4qLjWcz9cSIBVvVk62/4Ch3vE052l5O503zz7uxFINObHnJbhW6SAkaiABlLc54f1rvWjm+CpY+SJCJMDaSsHuDrpF+8+5pwT/VnKIAQogGY6Tk5INSDpyYL+3MftdJfGqSTM1qecSl+SFt67zEsISIBgA3u8SHx52QmoAZl5cRHlnQyLU917a3UbevVzZkvbtGiAJBonQWF/wdeyema1pDDOVvEsxM3CzjvNVrNrc0SKXWyIgEshepduMbetKq3GAjctAj+PR52kMQ9N7TObMAWb6fap6aDBmAjEAzjgDxfFwSzA2VLKaQofXmbQCsPM3CBvzU64G/Yem38fybHlkSnnyv/zGRv275Fj7AjEAldpJLRUrK6TPxxOhMZsxeklROHRM+RZOe+9B7UBuojYA8RJXRRALE2rqB+EiyCXiigEAkgFoCkQKICFpz7fe2B1/KuLDr2BSRVbABRQaXchNpuNJWT8iHXMuEiAgBnZ44Ww3M4VVUiZ59uqtFGLwJ/D3Da4CFyaGylWblhIgEeqORny6JkziE31kvUXv72SXOUlNZqAmQKMZaMF0NqI="
      },
      "Type": "TOK",
      "DecimalQuantity": "1"
    },
    {
      "Id": {
        "tx_id": "790098ae67dc5157c7d679b8def39983adb6e9d7070209f5ab12938d45e1630d",
        "index": 3
      },
      "Owner": {
        "raw": "MIIEnBMCc2kEggSUCgxJZGVtaXhPcmdNU1ASgwkKICVPsqioUyWm9q6xizxbHfgBSSUkg0s0b3hUhe4zbQQYEiAZ8BHcI3BBs0qtGX49E9lngYv4b0Qb4EhCnYAi2WGxqhpZCgxJZGVtaXhPcmdNU1ASJ2RlZmF1bHQtdGVzdGNoYW5uZWwtZGVmYXVsdC5leGFtcGxlLmNvbRogAvYocoRRzxnU/3a5uT0Lo/0S+q6nBi5/kLf96ay1gywiEAoMSWRlbWl4T3JnTVNQEAEqzwcKRAogBTwzc7U816TnMRwVxY8NFWV4IQ99jPAIG9Qnsn+ZPRsSIAdnZwExRnQZV5TiKRaqPmWpPJN/Nmwz6bg2vW0Dl28WEkQKIAE551T9rDBCUBwLQ8iEHneHiFTyDN1BI5bYKJ2E62c1EiAR6KLGNrx48iW5LZQF0QUWURJoxPuSmw3cYrXRsHKnfBpECiArBYSRzJqPTiPLCc/4XUeFfEPSnfQxNgPEk0rWWGpQwxIgDo1QCcOIjH4BW4nM67UQU6LmaPIrZm5QPMkRm0zNqbEiICtIljP4XkWQWuhzzUWymOtYf+jEvxO5q0DjFDaXfNY7KiARo8TWfG8c8T16pOXOLcLPWAzd8i5v3STWyGV7f2NJSzIgFJGYzwy7WWuG/I+BpOgaI6W/W64naQNPy4LTFb8GHQ06IDBC9vD8nMII4b9z6Co76tPbYfsUPg1HGLBXZwtO59ySQiAXkDX1J4Ee6TMJ+XCTiqNGoBD/78zkKA4nkMfBnyuO6kogDxN/pIlqDccyiCFb3GYkIDAU1nAZBhWy3baIZA3uNzpSIAE/U5CQi0+2P3rxaBtMbuHztpyWZXmRxkwE5v8cN8h0UiAiEbRhFC3NzbqOI+F3WTRuRAaKILU4nJQLfnVn3TcgGlogLDfRJ099L1Z0tNnrymmAaOzR156wg8n85RygbfEAe4piRAogJU+yqKhTJab2rrGLPFsd+AFJJSSDSzRveFSF7jNtBBgSIBnwEdwjcEGzSq0Zfj0T2WeBi/hvRBvgSEKdgCLZYbGqaiAXF4wRyDbFe9/rAgUYfM49iI/HU6hb9WzM1syJXeJ1knKIAQogGY6Tk5INSDpyYL+3MftdJfGqSTM1qecSl+SFt67zEsISIBgA3u8SHx52QmoAZl5cRHlnQyLU917a3UbevVzZkvbtGiAJBonQWF/wdeyema1pDDOVvEsxM3CzjvNVrNrc0SKXWyIgEshepduMbetKq3GAjctAj+PR52kMQ9N7TObMAWb6fap6aDBmAjEAzjgDxfFwSzA2VLKaQofXmbQCsPM3CBvzU64G/Yem38fybHlkSnnyv/zGRv275Fj7AjEAldpJLRUrK6TPxxOhMZsxeklROHRM+RZOe+9B7UBuojYA8RJXRRALE2rqB+EiyCXiigEAkgFoCkQKICkeIT/6W6IQpr9QNROBSR69w5ynLg01vSqMZrqGP+oGEiANCVz7nY94sX1MxYATg0BWVz/j69lP2R06i7V5LFY7OBIgCMItl/7wSRXVWxYszLX7UgsKdBgDV6Bo7KTAnszIHmQ="
      },
      "Type": "KOT",
      "DecimalQuantity": "9"
    }
  ]
}