From ebb36bc423f07e8d9550621783de4a8a358c739a Mon Sep 17 00:00:00 2001 From: Angelo De Caro Date: Thu, 17 Jun 2021 16:14:40 +0200 Subject: [PATCH] - additional documentation - integration test, basics: issuance process Signed-off-by: Angelo De Caro --- README.md | 8 +- docs/design.md | 24 +-- docs/driver-api.md | 7 +- docs/drivers.md | 9 +- docs/fabtoken.md | 4 +- docs/imgs/token_request_translator.png | Bin 0 -> 28146 bytes docs/token-api.md | 54 +++++-- docs/zkat-dlog.md | 4 +- go.mod | 2 + go.sum | 4 + integration/README.md | 2 +- integration/token/dvp/README.md | 13 +- integration/token/dvp/{ => diagrams}/dvp.png | Bin integration/token/dvp/{ => diagrams}/dvp.puml | 0 integration/token/tcc/basic/README.md | 144 +++++++++++++++++- integration/token/tcc/basic/tests.go | 8 +- integration/token/tcc/basic/views/accept.go | 33 ++-- integration/token/tcc/basic/views/issue.go | 44 ++++-- token/stream.go | 5 + 19 files changed, 300 insertions(+), 65 deletions(-) create mode 100644 docs/imgs/token_request_translator.png rename integration/token/dvp/{ => diagrams}/dvp.png (100%) rename integration/token/dvp/{ => diagrams}/dvp.puml (100%) diff --git a/README.md b/README.md index e9e5ec04f..02a98a18b 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ token-based distributed application on Hyperledger Fabric. - Submit your issues [`here`][`fabric-token-sdk` Issues]. - If you have any questions, queries or comments, find us on [GitHub discussions]. -- [`Fabric Smart Client`](https://github.com/hyperledger-labs/fabric-client-sdk): The Token SDK leverages the - [`Fabric Smart Client`](https://github.com/hyperledger-labs/fabric-smart-client) for transaction orchestration, check it out. +- [`Fabric Smart Client`](https://github.com/hyperledger-labs/fabric-smart-client): The Token SDK leverages the + `Fabric Smart Client` for transaction orchestration, storing tokens and wallets, and more. Check it out. # Disclaimer @@ -44,10 +44,6 @@ What would happen if the developers could use a `Fabric Token SDK` that let: Developing Enterprise Token-based distributed applications would become simpler and more secure. -# Issues - -The `Fabric Token SDK` issues are tracked in the GitHub issues tab. - # Use the Fabric Token SDK ## Install diff --git a/docs/design.md b/docs/design.md index b32a1f005..44643e59f 100644 --- a/docs/design.md +++ b/docs/design.md @@ -1,11 +1,15 @@ # The Fabric Token SDK -The scope of the Fabric Token SDK is to deliver a set of API and services that let developers create token-based distributed application on Hyperledger Fabric. +The scope of the `Fabric Token SDK` is to deliver a set of API and services that let developers create token-based +distributed application on Hyperledger Fabric. The `Fabric Token SDK` has the following characteristics; -- It adopts the UTXO model. In the UTXO model, a direct acyclic graph reflects the movements of the assets. Nodes are token transactions. Edges are transaction outputs. Each new token transaction consumes some the UTXOs and create new ones. +- It adopts the UTXO model. In the UTXO model, a direct acyclic graph reflects the movements of the assets. + Nodes are token transactions. Edges are transaction outputs. Each new token transaction consumes some the + UTXOs and create new ones. - Wallets contain a set of `secret keys` and keep track of the list of unspent outputs `owned` those keys. -- It supports different privacy levels: from a plain instantiation, where everything is in the clear on the ledger, to Zero Knowledge-based instantiations that will obfuscate the ledger while enforcing the required invariants. -- It allows the developers to write their own services on top of the Token SDK to deliver customised services +- It supports different privacy levels: from a `plain` instantiation, where everything is in the clear on the ledger, + to `Zero Knowledge-based` instantiations that will obfuscate the ledger while enforcing the required invariants. +- It allows the developers to write their own `services` on top of the Token SDK to deliver customised compoenents for their token-based applications. ## The Token SDK Stack @@ -16,18 +20,18 @@ This is the Fabric Token SDK stack: It consists of the following layers: - `Services` (light-blue boxes): Services offer pre-packaged token-related functionalities, -like `Token Transaction` assembling, Token Selectors, and so on. +like `Token Transaction` assembling, `Token Selectors`, and so on. They are built of top of the `Token API` abstraction. Therefore, they are independent of the underlying token technology. -- `Token API`: This API offers a useful abstraction to deal with tokens in an implementation-independent way. -- `Driver API`: This API takes the burden of translating calls of the Token API into API calls that are implementation-specific. +- `Token API`: This API offers a useful abstraction to deal with tokens in an implementation and blockchain independent way. +- `Driver API`: This API takes the burden of translating calls to the Token API into API calls that are implementation-specific. - `Driver Implementations`: This is the lowest level of the Token SDK. A driver implementation is responsible for defining the representation of tokens on the ledger, what it means to perform certain token actions, and when a token transaction is valid, among other things. -The Fabric Token SDK is built on top of the Fabric Smart Client stack. -The Smart Client allows the Token SDK to: +The `Fabric Token SDK` is built on top of the `Fabric Smart Client` stack. +The `Smart Client` allows the `Token SDK` to: - Orchestrate very complex token-dependent business processes; -- Store the tokens inside the Vault to easy lookup and manipulation; +- Store the tokens inside the Vault for easy lookup and manipulation; - To listen to events from Fabric related to token transaction, and more. Le us explore in more details each layer of the Token SDK stack. diff --git a/docs/driver-api.md b/docs/driver-api.md index 2154a7ec7..7ab55c21a 100644 --- a/docs/driver-api.md +++ b/docs/driver-api.md @@ -1,6 +1,9 @@ # Driver API The Driver API defines the contracts any implementation should respect to be compatible with the Token API. -It has a finer granularity than the Token API to accommodate better the difference between the various token technologies. +It has a finer granularity than the Token API to accommodate better the difference between +the various token technologies. -![img_2.png](imgs/driver_api.png) \ No newline at end of file +Here is a pictorial representation of the dependencies among the various building blocks. + +![driver_api.png](imgs/driver_api.png) \ No newline at end of file diff --git a/docs/drivers.md b/docs/drivers.md index 9b12d10ec..ccfa52849 100644 --- a/docs/drivers.md +++ b/docs/drivers.md @@ -1 +1,8 @@ -# Drivers \ No newline at end of file +# Drivers + +The Token SDK comes equipped with two driver implementations: +- [`FabToken`](./fabtoken.md): This is a simple implementation of the Driver API that does not support privacy. +- [`ZKAT DLog`](./zkat-dlog.md): This driver supports privacy via Zero Knowledge. We follow + a simplified version of the blueprint described in the paper + [`Privacy-preserving auditable token payments in a permissioned blockchain system`]('https://eprint.iacr.org/2019/1058.pdf') + by Androulaki et al. \ No newline at end of file diff --git a/docs/fabtoken.md b/docs/fabtoken.md index 9b23a490f..d1a307e8d 100644 --- a/docs/fabtoken.md +++ b/docs/fabtoken.md @@ -1 +1,3 @@ -# FabToken \ No newline at end of file +# FabToken + +To Be Continued... \ No newline at end of file diff --git a/docs/imgs/token_request_translator.png b/docs/imgs/token_request_translator.png new file mode 100644 index 0000000000000000000000000000000000000000..d8061da213eac0a2f5eae56226995fb2746a2865 GIT binary patch literal 28146 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 CvGR8S literal 0 HcmV?d00001 diff --git a/docs/token-api.md b/docs/token-api.md index 5fdb850d1..6700ddc8d 100644 --- a/docs/token-api.md +++ b/docs/token-api.md @@ -1,7 +1,6 @@ # Token API -The `Token API` offers a useful abstraction to deal with tokens in an -implementation and blockchain independent way. +The `Token API` offers a useful abstraction to deal with tokens in an implementation and blockchain independent way. Let us start with defining the tokens that the Token SDK handles. A token consists of the following triplet: @@ -16,9 +15,23 @@ A token consists of the following triplet: These tokens are `fungible` with the respect to the same type. In particular, tokens with the same denomination can be merged and split, if not otherwise forbidden. -Let us now focus on the building blocks the Token API consists of: +A token can be spent only by the `rightful owner`. This concept is implementation dependant. For example, +if the `Owner` field contains a public-key, then a valid signature under public key must be presented to spend the token. +If the `Owner` field contains a script, then an input that satisfies the script must be presented to spend the token. -![img.png](imgs/token_api.png) +The Token SDK supports the following basic operations: +- The `Issue` operation creates new tokens. `Issuers` are in charge of issuing new tokens. Depending on the driver + implementation, an issuing policy can be used to identify the authorized issuers for a given type. +- The `Transfer` operation transfers the ownership of a given token. A transfer operation must refer to tokens of the same +type. +- The `Redeem` operation deletes tokens. Depending on the driver implementation, either the rightful owner or special +parties, called `redeemers` can invoke this operation. + +A `Token Request` aggregates token operations that must be performed atomically. + +Let us now focus on the building blocks the `Token API` consists of: + +![token_api.png](imgs/token_api.png) - `Token Management Service`: The Token Management Service (TMS, for short) is the entry point of the Token SDK and gives access to the other building blocks. @@ -60,27 +73,27 @@ Let us now focus on the building blocks the Token API consists of: A token request should: - Well-formed, and - Satisfies the constraints of the payment system. Namely: - - Only the rightful owner can transfer a token, + - Only the `rightful owner` can transfer a token, - No token can be created out of the blue, - Audited(able) - Etc. (Each implementation can enforce additional requirements, if needed) The tuple `(network, channel, namespace, public parameters)` uniquely identifies a TMS, where: -- `network` is the identifier of the Fabric network of reference; -- `channel` is the channel inside the Fabric network; -- `namespace` is the namespace inside the channel where the tokens are stored. +- `network` is the identifier of the network of reference; +- `channel` is the channel inside the network, if available; +- `namespace` is the namespace inside the channel that stores the tokens. - `public parameters` contain all information needed to operate the specific token infrastructure. ## Token Request -Let us spend a few more words on the Token Request that is the core of the Token API. +Let us spend a few more words on the `Token Request` that is the core of the Token API. The Token Request is a `container` of token actions (issue, transfer, and redeem) that must be performed atomically. -Looking ahead, parties interacting to assemble a token transaction are, under the hood, assembling a Token Request that it is +Looking ahead, parties interacting to assemble a token transaction are, under the hood, assembling a `Token Request` that it is later marshalled into the format required by the target Blockchain. This is the anatomy of a Token Request: -![img_2.png](imgs/token_request.png) +![token_request.png](imgs/token_request.png) It consists of three parts: - `Anchor`: It is used to bind the Actions to a given Transaction. In Fabric, the anchor is the Transaction ID. @@ -88,16 +101,25 @@ It consists of three parts: - `Issues`, to create new Tokens; - `Transfers`, to manipulate Tokens (e.g., transfer ownership or redeem) - The actions in the collection are independent. - In addition, actions comes with a set of `Witnesses` that attest the will of the `Issuers` and/or the `Token Owners` - to perform a certain operation. + The actions in the collection are independent. An action cannot spend tokens created by another action in the same Token Request. + In addition, actions comes with a set of `Witnesses` to verify the `right to spend` or the `right to issue` a given token - `Metadata`: It is a collection of `Token Metadata`, one entry for each Token Action. Parties, assembling a token request, exchange metadata that contain secret information used by the parties to check the content of the token actions. This is particularly relevant when using ZK-based drivers. - Notice that, no metadata is stored on the ledger. + Notice that, the ledger does not store any metadata. As we mentioned earlier, a Token Request is itself agnostic to the details of the specific Blockchain. Indeed, a Token Request must be translated to the Transaction format of the target Blockchain to become meaningful. A service called `Token Request Translator` translates the token requests. -The Token Request Translator does not belong to the Token API. It is offered as a service on top of the Token API. \ No newline at end of file +The `Token Request Translator` does not belong to the Token API. It is offered as a service on top of the `Token API` +because it is blockchain dependant. + +Here is a pictorial representation of the translation process. + +![token_request_translator.png](imgs/token_request_translator.png) + +In the section dedicated to [`What you need to build Token-Based Applications on top of Fabric`](./services.md), +we will learn that a special chaincode called `Token Chaincode` performs +1. The `validation` of the Token Request, and +2. The `translation` to the RWSet. \ No newline at end of file diff --git a/docs/zkat-dlog.md b/docs/zkat-dlog.md index 0cdafca29..f1e331922 100644 --- a/docs/zkat-dlog.md +++ b/docs/zkat-dlog.md @@ -1 +1,3 @@ -# ZKAT DLog \ No newline at end of file +# ZKAT DLog + +To Be Continued... \ No newline at end of file diff --git a/go.mod b/go.mod index f4e44149c..a298f6a6e 100755 --- a/go.mod +++ b/go.mod @@ -30,6 +30,8 @@ require ( github.com/stretchr/testify v1.7.0 github.com/tedsuo/ifrit v0.0.0-20191009134036-9a97d0632f00 go.uber.org/atomic v1.7.0 + golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect + golang.org/x/tools v0.1.3 // indirect google.golang.org/grpc v1.36.1 // indirect google.golang.org/protobuf v1.26.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 5e4487084..608042403 100644 --- a/go.sum +++ b/go.sum @@ -901,6 +901,8 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 h1:C+AwYEtBp/VQwoLntUmQ/yx3MS9vmZaKNdw5eOpoQe8= golang.org/x/sys v0.0.0-20210608053332-aa57babbf139/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -933,6 +935,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3 h1:L69ShwSZEyCsLKoAxDKeMvLDZkumEe8gXUZAjab0tX8= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/integration/README.md b/integration/README.md index 2c860c442..e255b0fa4 100644 --- a/integration/README.md +++ b/integration/README.md @@ -10,5 +10,5 @@ business processes by invoking the `initiator view` on the specific FSC nodes. Here is a list of available examples: - [`Tha Basics`](./token/tcc/basic/README.md): A showcase of all possibility that the Token SDK offers. -- [`I Owe You`](./token/dvp/README.md): In this example, we see how to orchestrate a Delivery vs Payment use-case +- [`DvP`](./token/dvp/README.md): In this example, we see how to orchestrate a Delivery vs Payment use-case diff --git a/integration/token/dvp/README.md b/integration/token/dvp/README.md index d4d780a36..0dd0bacbf 100644 --- a/integration/token/dvp/README.md +++ b/integration/token/dvp/README.md @@ -1,3 +1,14 @@ # Delivery vs Payment -To be continued... \ No newline at end of file +In this section, we will address a more complex scenario. +Let us start by describing it first. +There are two parties: a `seller` and a `buyer`. +- The `seller` owns a house that she wants to sell for an amount of money corresponding +to the house's valuation. +- The `buyer` owns some cash in the form of digital tokens and wants to buy the house +the seller is offering. + +This can be seen as a classical `delivery versus payment`, (DVP, for short). + +[comment]: <> (The following diagram gives a pictorial representation of the business interactions) +[comment]: <> (that allow the parties to track the exchange they are performing on a blockchain.) diff --git a/integration/token/dvp/dvp.png b/integration/token/dvp/diagrams/dvp.png similarity index 100% rename from integration/token/dvp/dvp.png rename to integration/token/dvp/diagrams/dvp.png diff --git a/integration/token/dvp/dvp.puml b/integration/token/dvp/diagrams/dvp.puml similarity index 100% rename from integration/token/dvp/dvp.puml rename to integration/token/dvp/diagrams/dvp.puml diff --git a/integration/token/tcc/basic/README.md b/integration/token/tcc/basic/README.md index 93a4c2bc1..aaa699903 100644 --- a/integration/token/tcc/basic/README.md +++ b/integration/token/tcc/basic/README.md @@ -1,3 +1,145 @@ # Token SDK, The Basics -To be continued... \ No newline at end of file +In this Section, we will see examples of how to perform basic token operations like `issue`, `transfer`, `swap`, +and so on. + +Each operation will always involve two parties. An issuer and a recipient, a sender and a recipient, and so on. + +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. +We assume that both issuer and recipient are running a `Fabric Smart Client` node. + +Here is an example of a `view` representing the issuer's operations in the `issuance process`: +This view is execute by the issuer's FSC node. + +```go + +// 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 the identity of the recipient's FSC node + Recipient view.Identity +} + +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. + recipient, err := ttxcc.RequestRecipientIdentity(context, 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) + if p.TokenType == "USD" { + // Retrieve the list of issued tokens using a specific wallet for a given token type. + history, err := wallet.HistoryTokens(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.GetIdentityProvider(context).Identity("auditor"), // Retrieve the auditor's FSC node identity + ), + ) + assert.NoError(err, "failed creating issue transaction") + + // The issuer add 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. + _, 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.NewOrderingView(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. + +```go + +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).ListTokens(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 +} +``` diff --git a/integration/token/tcc/basic/tests.go b/integration/token/tcc/basic/tests.go index 3e269f2b9..5fddc74e1 100644 --- a/integration/token/tcc/basic/tests.go +++ b/integration/token/tcc/basic/tests.go @@ -296,10 +296,10 @@ func registerCertifier(network *integration.Infrastructure) { func issueCash(network *integration.Infrastructure, wallet string, typ string, amount uint64, receiver string) string { txid, err := network.Client("issuer").CallView("issue", common.JSONMarshall(&views.IssueCash{ - Wallet: wallet, - TokenType: typ, - Quantity: amount, - Recipient: network.Identity(receiver), + IssuerWallet: wallet, + TokenType: typ, + Quantity: amount, + Recipient: network.Identity(receiver), })) Expect(err).NotTo(HaveOccurred()) Expect(network.Client(receiver).IsTxFinal(common.JSONUnmarshalString(txid))).NotTo(HaveOccurred()) diff --git a/integration/token/tcc/basic/views/accept.go b/integration/token/tcc/basic/views/accept.go index 57bccfca2..d5bdbba81 100644 --- a/integration/token/tcc/basic/views/accept.go +++ b/integration/token/tcc/basic/views/accept.go @@ -16,29 +16,44 @@ import ( type AcceptCashView struct{} func (a *AcceptCashView) Call(context view.Context) (interface{}, error) { - // Respond to a request for an identity + // 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") - // Expect a transaction + // 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") - // Check that the transaction is as expected + // 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) - unpsentTokens, err := ttxcc.MyWallet(context).ListTokens(ttxcc.WithType(outputs.At(0).Type)) - assert.NoError(err, "failed retrieving the unspent tokens for type [%s]", outputs.At(0).Type) - assert.True(unpsentTokens.Sum(64).Cmp(token2.NewQuantityFromUInt64(3000)) <= 0, "cannot have more than 3000 unspent quantity for type [%s]", outputs.At(0).Type) - - // Accept and send back + // 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).ListTokens(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") - // Wait for finality + // Before completing, the recipient waits for finality of the transaction _, err = context.RunView(ttxcc.NewFinalityView(tx)) assert.NoError(err, "new tokens were not committed") diff --git a/integration/token/tcc/basic/views/issue.go b/integration/token/tcc/basic/views/issue.go index 3bd3d826b..8d48c366b 100644 --- a/integration/token/tcc/basic/views/issue.go +++ b/integration/token/tcc/basic/views/issue.go @@ -46,10 +46,15 @@ func (p *RegisterIssuerViewFactory) NewView(in []byte) (view.View, error) { return f, nil } +// IssueCash contains the input information to issue a token type IssueCash struct { - Wallet string + // IssuerWallet is the issuer's wallet to use + IssuerWallet string + // TokenType is the type of token to issue TokenType string - Quantity uint64 + // Quantity represent the number of units of a certain token type stored in the token + Quantity uint64 + // Recipient is the identity of the recipient's FSC node Recipient view.Identity } @@ -58,28 +63,38 @@ type IssueCashView struct { } 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. recipient, err := ttxcc.RequestRecipientIdentity(context, p.Recipient) assert.NoError(err, "failed getting recipient identity") - // Limits - wallet := ttxcc.GetIssuerWallet(context, p.Wallet) - + // 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) if p.TokenType == "USD" { - // check limit + // Retrieve the list of issued tokens using a specific wallet for a given token type. history, err := wallet.HistoryTokens(ttxcc.WithType(p.TokenType)) assert.NoError(err, "failed getting history for token type [%s]", p.TokenType) - fmt.Printf("History [%s,%s]<[220]?\n", history.Sum(64).ToBigInt().Text(10), 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) } - // Prepare transaction - tx, err := ttxcc.NewTransaction( + // 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, - fabric.GetIdentityProvider(context).DefaultIdentity(), - ttxcc.WithAuditor(fabric.GetIdentityProvider(context).Identity("auditor")), + ttxcc.WithAuditor( + fabric.GetIdentityProvider(context).Identity("auditor"), // Retrieve the auditor's FSC node identity + ), ) assert.NoError(err, "failed creating issue transaction") + // The issuer add a new issue operation to the transaction following the instruction received err = tx.Issue( wallet, recipient, @@ -88,10 +103,15 @@ func (p *IssueCashView) Call(context view.Context) (interface{}, error) { ) 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. _, err = context.RunView(ttxcc.NewCollectEndorsementsView(tx)) assert.NoError(err, "failed to sign issue transaction") - // Send to the ordering service and wait for confirmation + // Last but not least, the issuer sends the transaction for ordering and waits for transaction finality. _, err = context.RunView(ttxcc.NewOrderingView(tx)) assert.NoError(err, "failed to commit issue transaction") diff --git a/token/stream.go b/token/stream.go index 13520a6dd..c23837ad8 100644 --- a/token/stream.go +++ b/token/stream.go @@ -9,6 +9,7 @@ import ( "fmt" "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" + token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" ) @@ -63,6 +64,10 @@ func (o *OutputStream) ByType(typ string) *OutputStream { }) } +func (o *OutputStream) Outputs() []*Output { + return o.outputs +} + func (o *OutputStream) Count() int { return len(o.outputs) }