diff --git a/Makefile b/Makefile index 7c88d184e..d3daa2d60 100644 --- a/Makefile +++ b/Makefile @@ -117,8 +117,6 @@ clean: rm -rf ./integration/token/dvp/fabtoken/cmd/ rm -rf ./integration/token/interop/fabtoken/cmd/ rm -rf ./integration/token/interop/dlog/cmd/ - rm -rf ./samples/fungible/cmd - rm -rf ./samples/nft/cmd .PHONY: clean-fabric-peer-images clean-fabric-peer-images: diff --git a/README.md b/README.md index 6eca9ab37..fe06db2d6 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,7 @@ [![Go](https://github.com/hyperledger-labs/fabric-token-sdk/actions/workflows/tests.yml/badge.svg)](https://github.com/hyperledger-labs/fabric-token-sdk/actions/workflows/go.yml) [![CodeQL](https://github.com/hyperledger-labs/fabric-token-sdk/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/hyperledger-labs/fabric-token-sdk/actions/workflows/codeql-analysis.yml) -The `Fabric Token SDK` is a set of API and services that let developers create -token-based distributed application on Hyperledger Fabric. +The `Fabric Token SDK` provides a collection of APIs and services that streamline development for token-based distributed applications. # Disclaimer @@ -15,7 +14,8 @@ The project will be subject to rapid changes to complete the open-sourcing proce # Useful Links - [`Documentation`](./docs/design.md): Discover the design principles of the Fabric Token SDK. -- [`Samples`](./samples/README.md): A collection of sample applications that demonstrate the use of the Fabric Token SDK. +- [`Fabric Samples`](https://github.com/hyperledger/fabric-samples/tree/main/token-sdk) Token SDK sample application is the + quickest way to get a full network running with a REST API to issue, transfer and redeem tokens right away. - `Feedback`: Your help is the key to the success of the Fabric Token SDK. - Submit your issues [`here`][`fabric-token-sdk` Issues]. - Found a bug? Need help to fix an issue? You have a great idea for a new feature? Talk to us! You can reach us on @@ -23,9 +23,7 @@ The project will be subject to rapid changes to complete the open-sourcing proce - [`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. -- [Fabric Samples](https://github.com/hyperledger/fabric-samples/tree/main/token-sdk) Token SDK sample application is the - quickest way to get a full network running with a REST API to issue, transfer and redeem tokens right away. - +- # Getting started Clone the code and make sure it is on your `$GOPATH`. @@ -81,27 +79,33 @@ If you want to provide your own versions of the fabric binaries then just set `F In this comprehensive guide, we'll walk you through two essential aspects of the Fabric Token-SDK. Firstly, you'll learn how to develop a straightforward token application to manage a currency. You'll grasp the fundamentals of creating tokens, and implementing transaction logic using the Fabric Token-SDK. Once you've mastered the application development, we'll then show you how to effortlessly deploy it in your existing Fabric network, ensuring a seamless integration with your blockchain infrastructure. By the end of this tutorial, you'll be equipped with the skills to expand your blockchain capabilities and unleash the true potential of decentralized currency management. (Refers to [Fabric Samples](https://github.com/hyperledger/fabric-samples/tree/main/token-sdk)) - # Motivation -[Hyperledger Fabric]('https://wiki.hyperledger.org/display/fabric') is a permissioned, modular, and extensible open-source DLT platform. Fabric architecture follows a novel `execute-order-validate` paradigm that supports distributed execution of untrusted code in an untrusted environment. Indeed, Fabric-based distributed applications can be written in any general-purpose programming language. -Fabric does not depend on a native cryptocurrency as it happens for existing blockchain platforms that require “smart-contracts” to be written in domain-specific languages or rely on a cryptocurrency. +**Hyperledger Fabric: Blockchain Built for Business** + +Hyperledger Fabric ([https://hyperledger-fabric.readthedocs.io/](https://hyperledger-fabric.readthedocs.io/)) is an open-source platform designed for permissioned blockchain networks. It offers a modular and extensible architecture, allowing for customization and future growth. Unlike traditional blockchains, Fabric applications can be written in any general-purpose programming language, making them more accessible to developers. + +**Beyond Cryptocurrencies: Tokenizing the World** -Blockchain technologies are accelerating the shifting towards a decentralised economy. Cryptocurrencies are reshaping the financial landscape to the extent that even central banks are now testing the technology to propose what is known as the `central bank digital currency`. But it is more than this. Real-world assets are being tokenised as fungible or non-fungible assets represented by tokens on a blockchain. Thus enabling business opportunities to extract more value. +While blockchain is often associated with cryptocurrencies, its potential extends far beyond. Fabric allows for the creation of tokens that represent real-world assets, both fungible (like loyalty points) and non-fungible (like unique digital artwork). This opens doors for new business models and unlocks additional value from existing assets. -Developing token-based applications for Hyperledger Fabric is not easy. Fabric does not provide an out-of-the-box SDK that let developers create tokens that represents any kind of asset. Developers are left on their own and this exposes them to useless duplication of code and security vulnerabilities. +**The Challenge: Building Tokenized Applications** -What would happen if the developers could use a `Fabric Token SDK` that let: -- Create tokens that represents any kind of asset (baked by a real-world asset or virtual); -- Choose the privacy level that best fits the use-case without changing the application logic; -- Orchestrate token transaction in a peer-to-peer fashion; -- Perform atomic swaps; -- Audit transactions before they get committed; -- Interoperate with token systems in other blockchain networks; -- Add a token layer to existing Fabric distributed application? +Developing applications that leverage tokens on Hyperledger Fabric can be complex. Fabric lacked a built-in SDK for creating and managing tokens, forcing developers to build solutions from scratch. This not only led to wasted effort with duplicated code, but also exposed applications to potential security vulnerabilities. -Developing Enterprise Token-based distributed applications would become simpler and more secure. - +**Introducing the Fabric Token SDK: Streamlining Tokenized Development (and Beyond)** + +The Fabric Token SDK has evolved beyond its initial focus on Hyperledger Fabric. It now empowers developers with the following capabilities across various platforms, including permissioned blockchains like Fabric and even centralized systems like Orion: + +* **Tokenization Made Easy:** Create tokens representing any type of asset, be it physical or digital. +* **Privacy by Design:** Select the appropriate privacy level for your specific use case, without modifying your application logic. +* **Peer-to-Peer Transactions:** Orchestrate token transfers directly between users, streamlining the process. +* **Atomic Swaps:** Facilitate secure exchanges of different tokens without relying on intermediaries. +* **Transaction Auditing:** Review transactions before they are finalized, ensuring accuracy and compliance. +* **Interoperability:** Connect with token systems on other blockchain networks, fostering broader ecosystems. +* **Seamless Integration:** Add a token layer to existing applications, regardless of platform, with minimal effort. + +With a robust Fabric Token SDK, developing secure and efficient enterprise-grade tokenized applications becomes a reality, offering flexibility for developers to choose the platform that best suits their needs. # Testing Philosophy @@ -109,6 +113,8 @@ Developing Enterprise Token-based distributed applications would become simpler We also believe that when developing new functions running tests is preferable than running the application to verify the code is working as expected. +For more information about our integration tests, look [`here`](./docs/itests.md). + # Versioning We use [`SemVer`](https://semver.org/) for versioning. For the versions available, see the [`tags on this repository`](https://github.com/hyperledger-labs/fabric-token-sdk/tags). diff --git a/cmd/tokengen/samples/topology/fungible.go b/cmd/tokengen/samples/topology/fungible.go index 63ebb8656..d81a91822 100644 --- a/cmd/tokengen/samples/topology/fungible.go +++ b/cmd/tokengen/samples/topology/fungible.go @@ -14,7 +14,7 @@ import ( "github.com/hyperledger-labs/fabric-token-sdk/integration/nwo/artifactgen" "github.com/hyperledger-labs/fabric-token-sdk/integration/nwo/token" fabric2 "github.com/hyperledger-labs/fabric-token-sdk/integration/nwo/token/fabric" - "github.com/hyperledger-labs/fabric-token-sdk/samples/fungible/views" + "github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/views" tokenSDK "github.com/hyperledger-labs/fabric-token-sdk/token/sdk" ) diff --git a/cmd/tokengen/samples/topology/fungible.yaml b/cmd/tokengen/samples/topology/fungible.yaml index e57dec8de..9315152af 100755 --- a/cmd/tokengen/samples/topology/fungible.yaml +++ b/cmd/tokengen/samples/topology/fungible.yaml @@ -126,7 +126,7 @@ topologies: github.com/hyperledger-labs/fabric-smart-client/platform/fabric/sdk: original: sdk alias: sdk1 - github.com/hyperledger-labs/fabric-token-sdk/samples/fungible/views: + github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/views: original: views alias: views github.com/hyperledger-labs/fabric-token-sdk/token/sdk: @@ -134,7 +134,7 @@ topologies: alias: sdk Imports: - github.com/hyperledger-labs/fabric-smart-client/platform/fabric/sdk - - github.com/hyperledger-labs/fabric-token-sdk/samples/fungible/views + - github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/views - github.com/hyperledger-labs/fabric-token-sdk/token/sdk Factories: - id: issue @@ -168,7 +168,7 @@ topologies: github.com/hyperledger-labs/fabric-smart-client/platform/fabric/sdk: original: sdk alias: sdk1 - github.com/hyperledger-labs/fabric-token-sdk/samples/fungible/views: + github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/views: original: views alias: views github.com/hyperledger-labs/fabric-token-sdk/token/sdk: @@ -176,7 +176,7 @@ topologies: alias: sdk Imports: - github.com/hyperledger-labs/fabric-smart-client/platform/fabric/sdk - - github.com/hyperledger-labs/fabric-token-sdk/samples/fungible/views + - github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/views - github.com/hyperledger-labs/fabric-token-sdk/token/sdk Factories: - id: register @@ -203,7 +203,7 @@ topologies: github.com/hyperledger-labs/fabric-smart-client/platform/fabric/sdk: original: sdk alias: sdk1 - github.com/hyperledger-labs/fabric-token-sdk/samples/fungible/views: + github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/views: original: views alias: views github.com/hyperledger-labs/fabric-token-sdk/token/sdk: @@ -211,7 +211,7 @@ topologies: alias: sdk Imports: - github.com/hyperledger-labs/fabric-smart-client/platform/fabric/sdk - - github.com/hyperledger-labs/fabric-token-sdk/samples/fungible/views + - github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/views - github.com/hyperledger-labs/fabric-token-sdk/token/sdk Factories: - id: transfer @@ -253,7 +253,7 @@ topologies: github.com/hyperledger-labs/fabric-smart-client/platform/fabric/sdk: original: sdk alias: sdk1 - github.com/hyperledger-labs/fabric-token-sdk/samples/fungible/views: + github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/views: original: views alias: views github.com/hyperledger-labs/fabric-token-sdk/token/sdk: @@ -261,7 +261,7 @@ topologies: alias: sdk Imports: - github.com/hyperledger-labs/fabric-smart-client/platform/fabric/sdk - - github.com/hyperledger-labs/fabric-token-sdk/samples/fungible/views + - github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/views - github.com/hyperledger-labs/fabric-token-sdk/token/sdk Factories: - id: transfer @@ -305,7 +305,7 @@ topologies: github.com/hyperledger-labs/fabric-smart-client/platform/fabric/sdk: original: sdk alias: sdk1 - github.com/hyperledger-labs/fabric-token-sdk/samples/fungible/views: + github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/views: original: views alias: views github.com/hyperledger-labs/fabric-token-sdk/token/sdk: @@ -313,7 +313,7 @@ topologies: alias: sdk Imports: - github.com/hyperledger-labs/fabric-smart-client/platform/fabric/sdk - - github.com/hyperledger-labs/fabric-token-sdk/samples/fungible/views + - github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/views - github.com/hyperledger-labs/fabric-token-sdk/token/sdk Factories: - id: transfer diff --git a/docs/driver-api.md b/docs/apis/driver-api.md similarity index 100% rename from docs/driver-api.md rename to docs/apis/driver-api.md diff --git a/docs/apis/token-api.md b/docs/apis/token-api.md new file mode 100644 index 000000000..46e85c315 --- /dev/null +++ b/docs/apis/token-api.md @@ -0,0 +1,205 @@ +## Unleashing the Power of Tokens: A Look at the Token API + +The Token API, housed within the `token` and `token/token` packages, provides a powerful and versatile way to manage tokens across different implementations and backends. It acts as an abstraction layer, simplifying token interaction for developers. + +This API handles tokens defined by a three-part structure: + +* **Owner:** This identifies the token's rightful owner. Driver implementations can interpret this field based on their specific needs. It could represent a public key, a script, or anything the underlying driver supports. +* **Type:** Think of this as the token's denomination, a string value specific to your application. Examples include digital currency denominations or unique identifiers. +* **Quantity:** This represents the amount stored by the token. It's always a non-negative number encoded as a string in base 16, prefixed with "0x". + +Tokens of the same type are considered **fungible**. This means they can be merged or split (unless restricted), similar to how interchangeable units of currency behave. However, the API also allows for the creation of **non-fungible tokens**. These unique tokens have a quantity of 1 and a unique type. Drivers can further enhance non-fungible token functionality with additional features. + +Spending a token requires authorization from the rightful owner. This process depends on the implementation. For instance, if the Owner field holds a public key, a valid signature using that key is necessary. Script-based tokens require an input that satisfies the script's conditions. + +The Token API empowers developers with essential operations: + +* **Issue:** Creates new tokens. The designated issuers, determined by driver-specific issuing policies, control this operation. +* **Transfer:** Shifts ownership of a token. Transfers can only occur between tokens of the same type. +* **Redeem:** Deletes tokens. Depending on the driver, either the owner or designated redeemers can perform this action. + +**Token Requests** bundle these operations, ensuring they are executed atomically, meaning all operations succeed or fail together. + +Now, let's delve deeper into the core components that make up the Token API... + +![token_api.png](../imgs/token_api.png) + +## Diving into the Token Management Service (TMS) + +The Token Management Service (TMS), also known as `token.ManagementService`, acts as the central hub for the Token SDK. +It provides access to all the other APIs within the SDK. + +Imagine a TMS as being uniquely identified by a four-part address: + +1. **Network:** This identifies the underlying network or backend system. +2. **Channel (Optional):** This specifies the channel within the network. If not applicable, it remains empty. +3. **Namespace:** This defines the specific namespace within the channel where tokens are stored. +4. **Public Parameters:** This section holds all the crucial information needed to operate the particular token infrastructure. + +To interact with the TMS, developers can leverage the `GetManagementService` function: + +```go +tms := token.GetManagementService(context) +``` + +Here, `context` refers to an `FSC View Context` ([https://github.com/hyperledger-labs/fabric-smart-client](https://github.com/hyperledger-labs/fabric-smart-client)), which provides essential environmental details. + +For more granular control, developers can provide additional options with the function. For example: + +```go +tms := token.GetManagementService(context, token.WithNetwork("my-network")) +``` + +This allows you to specify a particular network named "my-network" for the TMS instance. + +## Unveiling the Public Parameters Manager + +Every Token Management Service (TMS) is linked to a set of public parameters. +This information, managed by the `Public Parameters Manager` (`token.PublicParametersManager`), holds the key to operating the token infrastructure effectively. + +While some parameters are specific to different drivers, some common details are included: + +* **Precision:** This dictates the level of detail used to represent the amount stored in a token. +* **MaxTokenValue:** This sets a limit on the maximum quantity a single token can hold. +* **Token Data Hiding:** When enabled (true), the content of the tokens is obscured. +* **Graph Hiding:** With this set to true, tokens become untraceable within the system. +* **Auditors:** This list identifies authorized auditors for the token system. + +To access the `Public Parameters Manager` associated with a specific TMS, developers can use the following code: + +```go +ppm := tms.PublicParametersManager() +``` + +This line retrieves the manager instance from the provided TMS object. + +## A Look Inside Wallets + +A Wallet acts like a digital identity vault, holding a long-term identity (think of it as a main key) and any credentials derived from it. +This identity can take different forms, such as an X509 Certificate for signing purposes or an [`Idemix Credential`](https://github.com/IBM/idemix) with its associated pseudonyms. +Ultimately, the specific driver you're using determines what constitutes a valid long-term identity. + +Wallets play a crucial role in signing and verifying operations. +Whenever a signature is needed, the system looks to the appropriate wallet within the Wallet Manager to locate the necessary keys. +This manager keeps track of wallets for different roles like Issuers, Owners, and Auditors. +Notably, Certifiers aren't supported because this driver doesn't handle a specific privacy feature called Graph Hiding. + +Depending on the type of wallet, you can extract additional information. +For instance, an Issuer Wallet lets you see a list of issued tokens, while an Owner Wallet shows you their unspent tokens. + +The Wallet Manager, conveniently referred to as `token.WalletManager` in code, serves as the central hub for managing all these wallets. + +Here's how developers can access the Wallet Manager of a specific TMS (Token Management System): + +```go +wm := tms.WalletManager() +``` + +## Building a Token Transaction + +Imagine a `Token Request` as a blueprint for a secure financial transaction. +It groups together different actions like issuing new tokens, transferring ownership, or redeeming existing ones. +These actions must happen all at once, ensuring everything goes smoothly. + +The `Token Request` offers a toolbox for developers to easily add or review the actions included in this blueprint. + +Here's a breakdown of its key components: + +* **Anchor:** This acts like a reference point, tying the actions to a specific transaction within the system. In Hyperledger Fabric, the anchor corresponds to the transaction ID. +* **Actions:** This is the heart of the blueprint, containing a set of specific token operations: + * **Issue:** Creates brand new tokens. + * **Transfer:** Manages existing tokens, allowing ownership changes or redemption. + +These actions within the request are independent. One action cannot utilize tokens created by another action within the same request. Additionally, each action comes with witnesses, which are essentially verifications. These witnesses confirm the "right to spend" or "right to issue" a particular token. In simpler scenarios, witnesses might be signatures from token owners or issuers. +* **Metadata:** This serves as a secure communication channel between involved parties. It holds secret information that allows them to verify the details of the token actions. This is especially important when using privacy-focused drivers based on Zero-Knowledge proofs. Importantly, the ledger itself doesn't store this metadata. + +Developers can create a new `Token Request` from scratch using this line of code: + +```go +tr, err := token.NewRequest() +``` + +Alternatively, they can bring a previously saved request back to life using this approach: + +```go +tr, err := token.NewRequestFromBytes(achor, actions, metadata) +``` + +Behind the scenes, when parties collaborate to create a token transaction, they're essentially building a `Token Request`. This request is then translated into a format that the specific ledger system (like Hyperledger Fabric) understands. Remember, a `Token Request` itself is independent of the underlying ledger. To be processed, it needs a translation service called the `Token Request Translator`. This translator converts the request into the transaction format specific to the chosen ledger backend. Since this translation depends on the ledger being used, the `Token Request Translator` is a separate service on top of the core `Token API`. + +Here's a visual representation of the translation process for Hyperledger Fabric (image not included, but the concept remains). + +In Fabric, a special component called the `Token Chaincode` is responsible for validating and translating these token requests. + +The Token SDK provides a handy service, the `ttx service`, to streamline working with token requests as transactions. +This service takes care of the entire process, from creation to completion. +For a deeper dive into this service, check out the dedicated page: [`ttx service`](../services/ttx.md). + +## Validator + +The `token.Validator` acts as the guardian of token requests, ensuring they adhere to specific rules. +These rules vary depending on the types of tokens supported (fungible or non-fungible) and the chosen driver implementation. + +The validator meticulously examines each token request against two key aspects: + +- The provided anchor (think of it as a reference point, like a transaction ID in Fabric). +- The target ledger (though some implementations might not require the ledger itself). + +While specific validation rules can differ based on the driver, some general principles hold true. A valid token request should: + +- Be structurally sound (well-formed). +- Align with the constraints of the payment system. This means: + - Only authorized owners can transfer tokens. + - Tokens can't be conjured out of thin air (they must be issued properly). + - The system should be auditable (transactions can be traced and verified). +- Additional requirements can be enforced by individual implementations as needed. + +## Manage all your tokens with ease using Token Vault + +Token Vault is your central hub for everything token-related. +It works seamlessly across different systems, offering a comprehensive toolkit to understand your token holdings. + +With Token Vault, you can: + +* **Gain instant insights:** See all your tokens in one place, check their status, and get detailed information about each one. +* **Track transactions:** Easily query if a transaction is pending or confirm ownership of a specific token. +* **Explore unspent tokens:** Utilize iterators to discover tokens you haven't used yet. +* **List all tokens:** Get a complete overview of all your tokens or those issued on the network. +* **Dive deeper:** Retrieve specific details about tokens and their outputs. (For certain systems) You can even find out who deleted a token, if applicable. + +**Easy access for developers:** Developers can effortlessly access the vault of a specific Token Management System (TMS) using a simple line of Go code: + +```go +vault := tms.Vault() +``` + +This vault instance is the same one provided by the dedicated [`Vault Service`](../services/vault.md). + +## Token Selector Manager + +The token SDK empowers developers to select specific tokens from the vault for transactions using the `token.Selector` function. +This selector acts like a refined filter, allowing you to specify conditions like token type, amount, and even ownership. +To safeguard against double-spending, any chosen token is automatically locked until the transaction's fate is sealed – be it commitment, rejection, timeout, or manual unlock. +This ensures developers leverage the appropriate tokens while eliminating double-spending woes. + +Let's delve deeper. +To get a selector for your specific needs, simply call the `SelectorManager()` method on your TMS instance. +This manager provides access to new selectors using the `NewSelector(tr.Anchor)` function. +Remember, the chosen tokens will be locked under the provided ID. + +Behind the scenes, these selectors are meticulously crafted by the dedicated [`Token Selector Service`](../services/selector.md). + +## Signature Service + +The `token.SignatureService` acts as your gateway to secure transactions. +It provides access to both signature verifiers and signers, all seamlessly linked to identities retrieved from the Wallet Manager. + +Getting started is a breeze. +Simply call the `SigService()` method on your existing TMS instance, and you'll be ready to leverage these powerful signing and verification tools. + +## Config Manager + +Unveil the inner workings of your TMS with the `token.ConfigManager`. +This component grants you access to the TMS's configuration, giving you full control over its behavior. + +To interact with the configuration manager for a specific TMS instance, simply use the `tms.ConfigManager()` method. It's that easy! diff --git a/docs/core-token.md b/docs/core-token.md index 591ad5e65..28d5b5113 100644 --- a/docs/core-token.md +++ b/docs/core-token.md @@ -1,6 +1,6 @@ # Example core.yaml section -The following example provides descriptions for the various keys required for a Fabric Smart Client node that uses the Token SDK. +The following example provides descriptions for the various keys required by the Token SDK. ```yaml # ------------------- Token SDK Configuration ------------------------- @@ -107,35 +107,4 @@ token: SW: Hash: SHA2 Security: 256 - # Internal database to keep track of token transactions. - # It is used by auditors and token owners to track history - ttxdb: - persistence: - # type can be one of badger, sql or memory. - type: badger - opts: - # persistence location - path: /some/path - - # The sql driver uses golangs database/sql package internally. - # In theory you can use any driver if you import it in your application; - # for instance `import _ "github.com/mattn/go-sqlite3"` for the cgo version of sqlite. - # See https://github.com/golang/go/wiki/SQLDrivers. We only tested with github.com/lib/pq - # and modernc.org/sqlite, and it's likely that other drivers don't work exactly the same. - # To try a new sql driver, add a test here: token/services/ttxdb/db/sql/sql_test.go. - # - # type: sql - # opts: - # createSchema: true # create tables programmatically - # tablePrefix: tsdk # optional - # driver: sqlite # in the application, `import _ "modernc.org/sqlite"` - # dataSource: /some/path/ttxdb.sqlite - # maxOpenConns: 10 # by default this is 0 (unlimited), sets the maximum number of open connections to the database - # - # Alternative (`import _ "github.com/lib/pq"`). - # The 'dataSource' field can be sensitive (contain a password). In that case, - # set it in the TTXDB_DATASOURCE environment variable instead of in this file. - # driver: postgres - # dataSource: host=localhost port=5432 user=postgres password=example dbname=tokendb sslmode=disable - ``` diff --git a/docs/design.md b/docs/design.md index 1f1ba5cfc..2d59c0c5f 100644 --- a/docs/design.md +++ b/docs/design.md @@ -1,42 +1,25 @@ # The Fabric Token SDK -The scope of the `Fabric Token SDK` is to deliver a set of `APIs` and `Services` that let developers create token-based -applications on Hyperledger Fabric, Orion, and potentially more. -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. -- Key-Management via `Wallets`. A Wallet contains a set of `secret keys` and keeps track of the list of unspent outputs `owned` by those keys. -- It supports `multiple privacy levels`: from a `plain` instantiation, where everything is in the clear on the ledger, - to `Zero Knowledge-based` instantiations that will obfuscate the content of the token transactions on the ledger while enforcing the required invariants - (see [drivers](./drivers.md) for more information). -- It allows developers to write their own `Services` on top of the Token SDK API to deliver customised components - for token-based applications. - -## The Token SDK Stack - -This is the Fabric Token SDK stack: - -![stack](imgs/stack.png) - -It consists of the following layers (from the top): -- [`Services`](./services.md): Services offer pre-packaged token-related functionalities, - like `Token Transaction` assembling, `Token Selectors` of unspent tokens, and so on. - They are built on top of the `Token API` abstraction. Therefore, they are independent of the underlying token technology. -- [`Token API`](./token-api.md): This API offers a useful abstraction to deal with tokens in an implementation and blockchain independent way. - Tokens and the related operations are represented in a meta-language that is then translated to a given backend (Fabric, Orion, etc...). -- [`Driver API`](./driver-api.md): This API takes the burden of translating calls to the Token API into API calls that are token implementation-specific. - Indeed, a transfer operation with privacy via Zero Knowledge is not the same as a transfer operation with privacy via plain instantiation. -- [`Drivers`](./drivers.md): This is the lowest level of the stack. A driver is responsible for - defining the representation of tokens, what it means to perform certain token operations, - 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: -- Orchestrate very complex token-dependent business processes via [`views`](https://github.com/hyperledger-labs/fabric-smart-client/blob/main/docs/view/api.md); -- Store the tokens inside the Vault for easy lookup and manipulation; -- To listen to events from the backends related to token transaction, and more. - -## Configuration - -You can find an example of the configuration required for the Token SDK in [Example Core File Section](./core-token.md) +**What it is:** + +* A set of APIs and services for building token-based applications on Hyperledger Fabric, Orion, and potentially other platforms. + +**Key Features:** + +* Uses the `Unspent Transaction Output` (UTXO) model for tracking token movements. +* Manages cryptographic keys through `Wallets`, keeping track of owned unspent outputs. +* Supports `various privacy levels`, from fully transparent to Zero-Knowledge Proofs for obfuscating transaction details. +* Allows developers to create `custom services` on top of the core API for specific application needs. + +**Architecture:** + +* The Fabric Token SDK stack consists of several layers: + * [`Services`](services/services.md): Pre-built functionalities like assembling transactions and selecting unspent tokens. + * [`Token API`](apis/token-api.md): Provides a common abstraction for interacting with tokens across different backends. + * [`Driver API`](apis/driver-api.md): Translates generic token operations into backend-specific calls (e.g., Fabric vs. Orion). + * [`Drivers`](drivers/drivers.md): Define token representation, operations, and validation rules for specific implementations. + +**Additional Information:** + +* The SDK leverages the Fabric Smart Client stack for complex workflows, secure storage, and event listening. +* Configuration examples can be found in the [`Example Core File Section`](./core-token.md) documentation. \ No newline at end of file diff --git a/docs/drivers.md b/docs/drivers/drivers.md similarity index 63% rename from docs/drivers.md rename to docs/drivers/drivers.md index f829cbfa4..ffeac546c 100644 --- a/docs/drivers.md +++ b/docs/drivers/drivers.md @@ -1,8 +1,8 @@ # 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 +- [`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/drivers/fabtoken.md b/docs/drivers/fabtoken.md new file mode 100644 index 000000000..5a545dedf --- /dev/null +++ b/docs/drivers/fabtoken.md @@ -0,0 +1,45 @@ +## FabToken: A Simple Token Driver + +FabToken is a straightforward implementation of the Driver API. +It prioritizes simplicity over privacy, storing all token transaction details openly on the ledger for anyone with access to view ownership and activity. + +### Configuration + +FabToken recognizes [`public parameters`](../../token/core/fabtoken/setup.go) containing the following information: + +* **Label:** A unique identifier associated with the configuration, often used for versioning. +* **Quantity Precision:** Defines the level of detail used to represent token amounts. +* **Auditor (Optional):** If set, specifies the identity of an authorized auditor who can approve token requests. +* **Issuers:** A list of authorized issuers who can create new tokens. +* **MaxToken:** The maximum quantity a token can hold. + +**Important:** The `Label` field must be set to `"fabtoken"`. This driver supports multiple issuers but only one auditor (if enabled). + +### Supported Identities + +FabToken exclusively supports long-term identities based on a standard X.509 certificate scheme. +These identities contain an X.509 certificate, which reveals the owner's enrollment ID in plain text. + +**Public Parameter Requirements:** Both `Auditor` (optional) and `Issuers` fields within the public parameters must contain serialized X.509-based identities. + +### Managing Wallets + +The wallet service provides access to a party's various token wallets. Each party can have multiple wallets for different purposes. However, each wallet is linked to a single X.509-based identity. + +### Tokens on the Ledger + +Tokens are directly represented on the ledger as JSON-formatted data based on the [`token.Token`](../../token/token/token.go) structure. +The `Owner` field of this structure stores the identity information. +The [`Identity Service`](../services/identity.md) handles the encoding/decoding of this field. + +### Ensuring Secure Transactions + +FabToken's validation process enforces several critical security measures: + +* **Authorized Issuance:** Only issuers whose identities are registered in the public parameter's `Issuers` field can create tokens. If this list is empty, anyone can issue tokens (not recommended for production). +* **Ownership Verification:** Only the legitimate owner of a token can transfer it. +* **Balanced Transfers:** In a transfer transaction, the total value of tokens being transferred in (inputs) must equal the total value being transferred out (outputs). +* **Redemption Control:** Only the owner of a token can redeem it. +* **Optional Auditing:** If an auditor is specified in the public parameters, their signature is required on all token requests for them to be valid. + +This revised version removes references to Fabric and emphasizes FabToken's compatibility with various blockchain backends. \ No newline at end of file diff --git a/docs/zkat-dlog.md b/docs/drivers/zkat-dlog.md similarity index 100% rename from docs/zkat-dlog.md rename to docs/drivers/zkat-dlog.md diff --git a/docs/fabtoken.md b/docs/fabtoken.md deleted file mode 100644 index b115a1908..000000000 --- a/docs/fabtoken.md +++ /dev/null @@ -1,56 +0,0 @@ -# FabToken - -The `FabToken` driver is a simple implementation of the Driver API that does not support privacy. -The ledger contains all token transaction details in clear and therefore everyone with access to the ledger can see who did what. - -## Public Params Manager - -`FabToken` understands the following public parameters: - -```go -// PublicParams is the public parameters for fabtoken -type PublicParams struct { - // Label is the label associated with the PublicParams. - // It can be used by the driver for versioning purpose. - Label string - // The precision of token quantities - QuantityPrecision uint64 - // This is set when audit is enabled - Auditor []byte - // This encodes the list of authorized issuers - Issuers [][]byte -} -``` - -The `Label` field must be set to `"fabtoken"`. -`FabToken` supports multiple issuers and a single auditor. - -## Identity Provider - -In `FabToken`, the only long-term identities supported are `X509-based Fabric MSP identities`. -Such an identity contains an X509 certificate and reveals in the clear the Enrollment ID of the certificate's owner. - -The `Auditor` and `Issuers` fields of the public parameters must contain serialized version of X509-based Fabric MSP identities. - -## Wallet Service - -The wallet services provides access to the wallets hold by the party. -A party can have multiple wallets for different purposes. -In any case, each wallet is bound to a single `X509-based Fabric MSP identities`. - -## Token Service - -A token is represented on the ledger directly as the `json` representation of the `token.Token` struct. -The `Owner` field of the `token.Token` struct is filled with the `asn1` representation of the `identity.RawOwner` struct -whose `Type` field is set to `identity.SerializedIdentityType` and whose `Raw` field is set to -a serialized X509-based Fabric MSP identity. - -## Validator - -`FabToken` validation process ensures the following: -- Only the issuers whose identities are registered in the public parameters (`Issuers` field) are allowed to issue tokens. - If the public parameters do not contain any issuer (`Issuers` field is empty), then anyone is allowed to issue tokens. -- Only the rightful owners of the tokens are allowed to transfer them. -- In a transfer operation, the sum of the inputs must be equal to the sum of the outputs. -- Only the owner of a token can redeem it. -- If the public parameters contain an auditor, then the auditor must sign the token request for it to be considered valid. \ No newline at end of file diff --git a/docs/interop.md b/docs/interop.md deleted file mode 100644 index d02805b9b..000000000 --- a/docs/interop.md +++ /dev/null @@ -1,72 +0,0 @@ -# Interoperability via Scripting - -Token SDK supports interoperability, cross-chain operations, via scripting. -It allows spending a token to a script by encoding the script in the `Owner` field of a `Token`, and the different drivers are capable of interpreting the owner as a script. -After the ownership is assigned to a script, the script is evaluated at the time of spending the token. -The right to spend the token is enforced according to the conditions within the script. - -## HTLC - -HTLC (Hash Time Locked Contract) is a token transfer that use hashlocks and timelocks to require that the recipient of a token either acknowledge receiving the token prior to a deadline by generating cryptographic proof or forfeit the ability to claim the token, returning it to the sender. -With this mechanism, the Token SDK supports atomic cross-chain swap of tokens. - -### HTLC script - -The HTLC script encodes the details of the HTLC, the identities of the sender and the recipient of the token, a deadline, and hashing information. -The hashing information includes the hash itself, the hash function used (e.g., SHA-256), and the encoding (e.g., Base64). -The hash is chosen by the sender and the recipient must provide the preimage for the transfer to happen. - -```go -// Script contains the details of an HTLC -type Script struct { - Sender view.Identity - Recipient view.Identity - Deadline time.Time - HashInfo HashInfo -} - -// HashInfo contains the information regarding the hash -type HashInfo struct { - Hash []byte - HashFunc crypto.Hash - HashEncoding encoding.Encoding -} -``` - -## Interoperability services - -The token transaction assembling service enables appending `Lock`, `Claim`, or `Reclaim` actions to the token request of the transaction. All of these actions translate into a transfer action. -`Lock` is the locking process, where the sender sets the details of the HTLC and transfers ownership of the token to a script. -`Claim` allows the recipient to gain ownership of the token by providing the preimage. -`Reclaim` returns the token to the sender. -Claim must happen before the deadline ends, while reclaim can only occur after the deadline has passed. - -```go -func (t *Transaction) Lock(wallet *token.OwnerWallet, sender view.Identity, typ string, value uint64, recipient view.Identity, deadline time.Duration, opts ...token.TransferOption) ([]byte, error) -func (t *Transaction) Claim(wallet *token.OwnerWallet, tok *token2.UnspentToken, preImage []byte) error -func (t *Transaction) Reclaim(wallet *token.OwnerWallet, tok *token2.UnspentToken) error -``` - -The interop `Wallet` service, located under `token/services/interop/`, supports listing tokens with a desired matching preimage, and listing expired tokens, whose deadline have passed. - -In addition, the interop `Signer` and `Verifier` services are script specific, for example in the HTLC case the preimage is part of the signed message. - -Finally, the interoperability services which are responsible for assembling the token transaction and managing its lifecycle are the same as the [`Token Transaction Services`](./services.md). -They are located in `token/services/interop`. - - -## Driver adjustments - -The `FabToken` and `ZKAT DLog` drivers support also interoperability, and more specifically, the drivers support HTLC. - -The validator in `FabToken` and `ZKAT DLog` can be enhanced with extra validators to accommodate additional validation rules. In particular, to support atomic swap, they take a validator that ensures HTLC conditions are met. That is, the deadline has not passed in the case of lock, that a claim was initiated by the recipient before the expiration of the deadline and carries the pre-image matching the hash, and that a reclaim is initiated by the sender after the deadline has passed. - -Their `TransferAction` carries the pre-image at time of transaction assembly to support HTLC. - -The `deserializer` in the interoperability case returns a specialized script owner verifier, that takes into account both the sender and the recipient as well as the deadline and the hash. - -The driver's `TransferService` also takes into account the presence of scripts, as `Transfer` returns `TransferMetadata` which includes information for both the sender and recipient of a script. - -Lastly, the `auditor` inspects the token ownership also in the interoperability case, and verifies that the audit info matches the script owner's, both the sender and the recipient. - -For more details on the drivers see [`FabToken`](./fabtoken.md) and [`ZKAT DLog`](./zkat-dlog.md). diff --git a/samples/README.md b/docs/itests.md similarity index 67% rename from samples/README.md rename to docs/itests.md index 1dbb56766..b973f589d 100644 --- a/samples/README.md +++ b/docs/itests.md @@ -1,15 +1,4 @@ -# Samples - -Samples are a collection of small and simple apps that demonstrate how to use the library. - -To run the samples, we recommend to use `go 1.20`. You will also need docker when using Fabric. -To make sure you have all the required docker images, you can run `make docker-images` in the -folder `$GOPATH/src/github.com/hyperledger-labs/fabric-token-sdk`. - -- [`Fungible Tokens, The Basics. On Fabric`](./fungible//README.md): How to handle `fungible tokens`. -- [`Non-Fungible Tokens, The Basics. On Fabric`](./nft//README.md): How to handle `non-fungible tokens`. - -## Additional Examples via Integration Tests +# Integration tests Integration tests are useful to show how multiple components work together. The Fabric Smart Client comes equipped with some of them to show the main features. diff --git a/docs/services.md b/docs/services.md deleted file mode 100644 index 8046ef47b..000000000 --- a/docs/services.md +++ /dev/null @@ -1,105 +0,0 @@ -# Services - -In this Section, we will see how to leverage the Token API to build a token-based application. -The Token SDK comes with pre-defined services that provide token specific functionality as we will see next. -Moreover, the Token SDK stack allows to build custom services as needed. - -## Token Transaction Service - -To assemble token transactions, the Token SDK comes equipped with a dedicated service located in the package `token/services/ttx`. -This service is responsible for assembling the token transaction, managing the transaction lifecycle, and so on. - -This service is ledger agnostic, meaning that it can be used to assemble token transactions for Fabric, for Orion, and so on. -This is made possible by the `token/services/network` service. This service abstracts away the complexities of the underlying -ledger technology. -More on this service in its dedicated section. - -The lifecycle of a Token Transaction consists of the following high-level steps: - -1. `Assembling the Token Transaction`. In this phase, the business parties decide on the token operations - that must happen atomically. The parties assemble the operations in a token transaction by following an interactive business process. - Under the hood, a token transaction contains a Token Request (defined by the Token API). - -2. `Collect Endorsements`. Once the Token Transaction is ready, meaning that the Token Request has been formed, - one of the business parties, let call it the `leader`, takes the charge of the following sub-steps: - - `Collect the relevant signatures for each action`: The leader contacts the relevant business parties to collect their signatures (endorsements). - - From the issuers of new tokens, if any; - - From the owners of the tokens spent, if any; - - `Request Audit`. The leader sends the token transaction to the auditor that checks it and signs it if all checks pass. - The auditor sends back the signature to the leader. - - `Request Approval`. At this point the token transaction can be validated and translated to a format - understood by the ledger backend. The leader sends the token transaction, stripped from all private data, - to the `Approvers` that validate it and translate it. - The approvers send back the translated token transaction signed. We call these `approvals`. - The leader attaches the approvals to the token transaction. - - `Distribute the Approvals`. The leader sends the full token transaction, containing also the approvals, to all involved business parties. - -3. `Commit`. The Token Transaction is fully formed and can be committed. The leader sends the token transaction to the ledger backend - (e.g., in case of Fabric - we send the transaction to the ordering service)) stripping out all private information. - The leader, and all other business parties, can now wait for finality if needed. A transaction is final when the ledger backend - says so and the transaction is committed to the local vault. - -## Token Vault Service - -The Token Vault service, located in `token/services/vault`, stores the available tokens owned by the wallets a party possess. -Tokens appear in the vault if an issuer issued them or a third-party transferred some tokens to one of the wallets the party possess. -The vault service is backend agnostic. It uses the network service to get access to the local vault instance of a specific ledger backend. - -## Network Service - -The `token/services/network` service is responsible for abstracting away the complexities of the underlying backend technology (e.g., Fabric, Orion, etc.).. -The service uses a driver-based design that let the developers implement new drivers for different ledgers. -Currently, the network service supports Fabric and Orion. - -### Fabric Driver - -The Fabric driver for the network service is located in the package `token/services/network/fabric`. -This driver assumes that the `Token Chaincode` (located in the package `token/services/network/fabric/tcc`) -is available in the Fabric network. This chaincode provides multiple functionalities that the Fabric driver uses. -Namely: -- `Public Parameters Lifecycle`. The public parameters govern the behaviour of the token chaincode. That is, how - token requests are processed and translated, who can issue tokens, who must audit before committing the token transaction, etc. -- `Approval`. This is one of the essential steps in the lifecycle of a token transaction, - as we have seen in the previous section. The Token Chaincode validates the received token request and, if valid, translates - it into the Read/Write Set (RW Set) format understood by Fabric. - Notice that the Token Chaincode can only check the validity of the transaction. Though, double spending can only verified - at committing time. To do so, the RW Set is generated in a way to trigger [`MVCC`](https://hyperledger-fabric.readthedocs.io/en/release-1.3/arch-deep-dive.html#the-endorsing-peer-simulates-a-transaction-and-produces-an-endorsement-signature) - conflicts in case of double spending. - An approval in this case is just the endorsement over the RW Set. - -To commit a token transaction, the Fabric network driver first derives a Fabric transaction with the RW Set -obtained from the approval phase. Then, this Fabric transaction is sent to the Fabric ordering service. - -The following figure illustrates the token transaction lifecycle for Fabric: - -![fabric_ttx_lifecycle.png](imgs/fabric_ttx_lifecycle.png) - -Let us explore now what happens when the Fabric transaction gets committed. -We expect to see the tokens created by the transaction appearing in the `Token Vault` of the owners. -The following picture shows how this happens: - -![commit_process.png](imgs/commit_process.png) - -In more details: -1. In background, each business party listens to the `Delivery Service` events generated by its Fabric peer partner. - When the Fabric peer commits a token transaction `Tx`, the FSC node gets informed. -2. The `Delivery Client` informs the `Committer` that `Tx` is available. -3. If `Tx` is deemed valid by Fabric, then the `Committer` further manipulates the RW Set by invoking all registered - `RW Set Processors` before the RW Set gets committed into the local Token Vault. If `Tx` is invalid, the transaction is - discarded. -4. The Token SDK installs one of these processors, the `Token RW Set Processor`. This processor extracts all the token - related information and augments the RW Set with additional information to speed up the token selection process, among other things. -5. Finally, the RW Set, after processing, is committed to the vault. - -Only at this point, the tokens created by the transaction become available via the `Token Vault Service` we have discussed above. - -### Orion Driver - -The Orion driver is similar to the Fabric driver because also Orion manages RW Sets. -Though, in Orion there is no concept of chaincode or stored routines. -To solve this problem, the Orion driver assumes the existence of a `Custodian` (another FSC node) that sits in front of Orion -and handles the `approval` and `commit` steps. - -Here is the pictorial representation of the lifecycle of a token transaction for Orion: - -![orion_ttx_lifecycle.png](imgs/orion_ttx_lifecycle.png) diff --git a/docs/services/auditor.md b/docs/services/auditor.md new file mode 100644 index 000000000..67d3d5979 --- /dev/null +++ b/docs/services/auditor.md @@ -0,0 +1,22 @@ +# Auditor + +The `auditor` service provides auditing capabilities for token transactions. +The service interacts with an audit database and a network provider to track and manage transaction status. + +Key features and components: + +- **Auditor:** The central service responsible for auditing transactions. + - Uses the `auditdb` service to store audit records. + - Relies on a `NetworkProvider` to interact with networks and channels. +- **Auditing flow:** + 1. **Validate**: Checks the validity of a token request using `request.AuditCheck()`. + 2. **Audit**: Extracts inputs and outputs from a transaction, locking enrollment IDs for safety. + 3. **Append**: Adds a transaction to the audit database and subscribes to transaction status changes on the network. + 4. **Release**: Releases locks acquired during auditing. +- **Querying and status management:** + - **NewQueryExecutor**: Creates a QueryExecutor for filtering and retrieving audit data. + - **SetStatus**: Sets the status of an audit record (Pending, Confirmed, Deleted). + - **GetStatus**: Retrieves the status of a transaction. + - **GetTokenRequest**: Retrieves the token request associated with a transaction ID. + +The auditor service is locate under [`token/services/auditor`](./../../token/services/auditor). \ No newline at end of file diff --git a/docs/services/identity.md b/docs/services/identity.md new file mode 100644 index 000000000..3797a65b5 --- /dev/null +++ b/docs/services/identity.md @@ -0,0 +1,94 @@ +# Identity Service: Who Can Do What in the Token-SDK? + +The Token-SDK uses identities to establish trust and control access within the system. +These identities are like digital passports that verify who a party is and what actions they're authorized to perform. + +**Think of Roles as Teams:** + +Within the identity service, long-term identities are grouped into roles. +Imagine these roles as teams with specific permissions. +Here are some key roles: + +* **Issuers:** Like a mint that creates coins, issuers have the power to create new tokens. +* **Owners:** Owners hold tokens, just like possessing a digital asset. +* **Auditors:** These act as financial inspectors, overseeing token requests and ensuring proper use. +* **Certifiers:** They verify the existence and legitimacy of specific tokens, similar to checking identification. +This role is used only by certain token drivers that support the so called `graph hiding`. + +**Identity Options: Passports and Beyond** + +Identities can come in different forms. +A common choice is an X.509 certificate, a secure electronic passport that links a real-world entity (website, organization, or person) to a public key using a digital signature. +This certificate uniquely identifies the entity it represents. + +Another option is anonymous credentials, which allow users to prove they have certain attributes (like age or qualifications) without revealing their entire identity. +Imagine showing an ID that only displays relevant information, not your full details. +This is particularly useful for protecting user privacy. + +**Roles and Wallets: Managing Access** + +Importantly, roles can contain different identity types. +These roles then act as the foundation for creating wallets. +A wallet is like a digital vault that stores a long-term identity (the main key) and any credentials derived from it. +Different wallet types provide different functionalities. +For example, an issuer wallet lets you see a list of issued tokens, while an owner wallet shows unspent tokens. + +To manage these identities and wallets, the identity service provides several tools: + +* **Identity Provider:** Manages roles and the identities within them. +* **Wallet Registry:** Uses the Identity Provider to manage all wallets associated with a specific role. +This registry relies on the token driver to implement the specific wallet functionalities defined by the Driver API. + +In essence, the Token-SDK identity service provides a secure and flexible framework for managing access control within your system. + +The identity service is locate under [`token/services/identity`](./../../token/services/identity). + +## Understanding Roles in More Detail + +Building on the concept of long-term identities, we'll now explore how they are grouped into roles within the identity service. + +Each role acts as a container for long-term identities, which are then used to create wallets. Here's the interface that defines a role: + +```go +// Role is a container of long-term identities. +// A long-term identity is then used to construct a wallet. +type Role interface { + // ID returns the identifier of this role + ID() driver.IdentityRole + // MapToID returns the long-term identity and its identifier for the given index. + // The index can be an identity or a label (string). + MapToID(v interface{}) (view.Identity, string, error) + // GetIdentityInfo returns the long-term identity info associated to the passed id + GetIdentityInfo(id string) (driver.IdentityInfo, error) + // RegisterIdentity registers the given identity + RegisterIdentity(id string, path string) error + // IdentityIDs returns the identifiers contained in this role + IdentityIDs() ([]string, error) + // Reload the roles with the respect to the passed public parameters + Reload(pp driver.PublicParameters) error +} +``` + +This interface offers functions for managing identities within the role. +You, as the developer, have the flexibility to implement a role using any identity representation that best fits your application's needs. +For example, a role could even encompass identities based on various cryptographic schemes. + +The identity service conveniently provides two built-in implementations of the Role interface. +Both implementations leverage the concept of Hyperledger Fabric MSP ([https://hyperledger-fabric.readthedocs.io/en/latest/msp.html](https://hyperledger-fabric.readthedocs.io/en/latest/msp.html)): + +* [**MSP X.509:**](./../../token/services/identity/msp/x509) This implementation retrieves long-term identities from local folders adhering to the X.509-based MSP format. +* [**MSP Idemix:**](./../../token/services/identity/msp/idemix) This implementation loads long-term identities from local folders that follow the Idemix-based MSP format. + +## Using the Identity Service in a Token Driver + +If you want to use the Identity Service in your Token Driver, then here is what you need to do. + +First, instantiate your roles. The identity service come equipped with two `Role` implementation. +One is based on X.509 certificates, the other one is based on Idemix. +Both requires the identities to be stored on the filesystem following the Hyperledger Fabric MSP prescriptions. + +High level, these are the steps to follow: +1. Instantiate the roles your driver will support. +2. Instantiate the Wallet Registry for each role. + +See an example taken from the [`fabtoken`](./../../token/core/fabtoken/driver) driver. \ No newline at end of file diff --git a/docs/services/interop.md b/docs/services/interop.md new file mode 100644 index 000000000..f5322cac9 --- /dev/null +++ b/docs/services/interop.md @@ -0,0 +1,55 @@ +# Spendable Scripts for Cross-Chain Swaps + +The Token SDK lets you create tokens that can be programmed! These "spendable scripts" allow for secure cross-chain operations. + +Here's how it works: + +* **Script as Owner:** Instead of a regular owner, you can encode a script as the owner of a token. Different drivers in the system can understand and execute these scripts. +* **Script Evaluation:** When you spend a token with a script as its owner, the script is evaluated at that moment. The script determines if the spending is allowed based on its programmed conditions. + +## Secure Swaps with HTLC + +One powerful use case for spendable scripts is Hash Time Locked Contracts (HTLC). This lets you securely swap tokens between different blockchains. + +* **Locking the Tokens:** To initiate a swap, you lock your token in a script. This script defines the recipient, a deadline, and a secret code (hashlock). +* **Recipient Claims the Token:** The recipient can only claim the token by providing the secret code (preimage) before the deadline. +* **Timelock Protection:** If the recipient doesn't claim the token on time, the script will allow you to get the token back. + +## Script Details + +The HTLC script stores all the necessary information: + +* Identities of both sender and recipient +* Deadline for claiming the token +* Hash information (including the hash itself, the hashing function used, and the encoding) + +Here's a glimpse of the code structure for the script and hash information: + +```go +// Script details for HTLC +type Script struct { + Sender string // Identity of the sender + Recipient string // Identity of the recipient + Deadline time.Time + HashInfo HashInfo +} + +// Information about the hashlock +type HashInfo struct { + Hash []byte + HashFunc string // Hashing function used (e.g., SHA-256) + HashEncoding string // Encoding format (e.g., Base64) +} +``` + +## Handy Services for Script Management + +The Token SDK provides several services, under [`token/services/interop`](./../../token/services/interop), to manage scripts and HTLC swaps: + +* **Building Transactions:** A service helps you build transactions with actions like locking (initiating a swap), claiming (recipient receiving the token), and reclaiming (sender getting the token back if unclaimed). +* **Wallet Interactions:** A separate wallet service lets you list tokens with specific preimages or find expired tokens (where the deadline has passed). +* **Script-Specific Services:** Additional services handle signing messages (including the preimage for HTLC) and verifying script ownership. +* **Driver Integration:** Existing drivers like FabToken and ZKAT DLog are already compatible with interoperability and HTLC functionality. These drivers have enhanced validation rules to ensure proper script execution and deadline adherence. + + +For a deeper dive into specific drivers, refer to the FabToken and ZKAT DLog documentation. diff --git a/docs/services/network.md b/docs/services/network.md new file mode 100644 index 000000000..00585aaa7 --- /dev/null +++ b/docs/services/network.md @@ -0,0 +1,58 @@ +# Network Service + +The [`token/services/network`](./../../token/services/network) service acts as a bridge, hiding the intricate details of the underlying ledger technology (like Fabric or Orion) from developers. +This service leverages a driver-based design, allowing developers to create new drivers for additional ledger platforms. +Currently, Fabric and Orion are supported out of the box. + +### Fabric Driver + +The Fabric driver for the network service is located in the package `token/services/network/fabric`. +This driver assumes that the `Token Chaincode` (located in the package `token/services/network/fabric/tcc`) +is available in the Fabric network. This chaincode provides multiple functionalities that the Fabric driver uses. +Namely: +- `Public Parameters Lifecycle`. The public parameters govern the behaviour of the token chaincode. That is, how + token requests are processed and translated, who can issue tokens, who must audit before committing the token transaction, etc. +- `Approval`. This is one of the essential steps in the lifecycle of a token transaction, + as we have seen in the previous section. The Token Chaincode validates the received token request and, if valid, translates + it into the Read/Write Set (RW Set) format understood by Fabric. + Notice that the Token Chaincode can only check the validity of the transaction. Though, double spending can only verified + at committing time. To do so, the RW Set is generated in a way to trigger [`MVCC`](https://hyperledger-fabric.readthedocs.io/en/release-1.3/arch-deep-dive.html#the-endorsing-peer-simulates-a-transaction-and-produces-an-endorsement-signature) + conflicts in case of double spending. + An approval in this case is just the endorsement over the RW Set. + +To commit a token transaction, the Fabric network driver first derives a Fabric transaction with the RW Set +obtained from the approval phase. Then, this Fabric transaction is sent to the Fabric ordering service. + +The following figure illustrates the token transaction lifecycle for Fabric: + +![fabric_ttx_lifecycle.png](./../imgs/fabric_ttx_lifecycle.png) + +Let us explore now what happens when the Fabric transaction gets committed. +We expect to see the tokens created by the transaction appearing in the `Token Vault` of the owners. +The following picture shows how this happens: + +![commit_process.png](./../imgs/commit_process.png) + +In more details: +1. In background, each business party listens to the `Delivery Service` events generated by its Fabric peer partner. + When the Fabric peer commits a token transaction `Tx`, the FSC node gets informed. +2. The `Delivery Client` informs the `Committer` that `Tx` is available. +3. If `Tx` is deemed valid by Fabric, then the `Committer` further manipulates the RW Set by invoking all registered + `RW Set Processors` before the RW Set gets committed into the local Token Vault. If `Tx` is invalid, the transaction is + discarded. +4. The Token SDK installs one of these processors, the `Token RW Set Processor`. This processor extracts all the token + related information and augments the RW Set with additional information to speed up the token selection process, among other things. +5. Finally, the RW Set, after processing, is committed to the vault. + +Only at this point, the tokens created by the transaction become available via the `Token Vault Service` we have discussed above. + +### Orion Driver + +The Orion driver is similar to the Fabric driver because also Orion manages RW Sets. +Though, in Orion there is no concept of chaincode or stored routines. +To solve this problem, the Orion driver assumes the existence of a `Custodian` (another FSC node) that sits in front of Orion +and handles the `approval` and `commit` steps. + +Here is the pictorial representation of the lifecycle of a token transaction for Orion: + +![orion_ttx_lifecycle.png](./../imgs/orion_ttx_lifecycle.png) diff --git a/docs/services/selector.md b/docs/services/selector.md new file mode 100644 index 000000000..3daf5f21c --- /dev/null +++ b/docs/services/selector.md @@ -0,0 +1,24 @@ +# Token Selector + +The Fabric Token SDK offers a powerful concept known as token selectors, empowering developers with granular control over token selection within the vault. +These selectors function as filters, allowing you to specify precise criteria for choosing the tokens you require for a particular transaction. + +Here's a breakdown of how token selectors work: + +* **Conditional Selection:** You can define a set of conditions to narrow down the pool of available tokens. + Common examples include selecting tokens based on: + * **Type:** Specify the desired token denomination (e.g., "USD Coin" or a unique identifier token). + * **Amount:** Define the exact quantity of tokens required for the transaction. + * **Ownership:** Select tokens held within a specific wallet. + +* **Preventing Double Spending:** To safeguard against the potential for double-spending (using the same token in multiple transactions), token selectors enforce a locking mechanism. + Once a token is selected, it becomes temporarily unavailable for other transactions until its fate is determined. + This locking remains in place until one of the following scenarios occurs: + * **Transaction Commitment:** If the transaction using the selected tokens is successfully committed to the backend, the lock is released. + * **Transaction Rejection:** If the transaction is rejected, the lock is lifted, and the tokens become available for selection again. + * **Timeout:** If a predefined period of inactivity elapses (timeout), the lock automatically expires, and the tokens are released. + * **Explicit Unlock:** Developers can also choose to explicitly unlock tokens before the transaction is completed. + +By leveraging token selectors, developers can ensure they are working with the appropriate tokens for their transactions while maintaining the integrity of the system and preventing fraudulent activities like double-spending. + +The selector service is locate under [`token/services/selector`](./../../token/services/selector). \ No newline at end of file diff --git a/docs/services/services.md b/docs/services/services.md new file mode 100644 index 000000000..0c78eddfe --- /dev/null +++ b/docs/services/services.md @@ -0,0 +1,17 @@ +# Services + +This section dives into using the Token API to build applications that rely on tokens. +The Token SDK provides pre-built services specifically designed for token functionality, which we'll explore next. +But that's not all! The SDK also empowers you to create custom services tailored to your unique needs. + +- [`Token Transaction Service`](ttx.md): Simplifies building and managing token transactions across different ledger platforms. +- [`Token Vault Service`](vault.md): Is a secure and adaptable personal vault for managing all your tokens with comprehensive query and retrieval functionalities. +- [`Storage`](storage.md): Fabric Token SDK uses secure databases to track transactions (ttxdb), manage tokens (tokendb), optionally store audit trails (auditdb), and manage user identities (identitydb). +It offers flexible deployment options for isolated or shared backend systems. +- [`Token Selector`](selector.md): Fabric Token SDK's token selectors allow developers to choose specific tokens (by type, amount, owner) from the vault for transactions. +They prevent double-spending by locking tokens until the transaction is completed, rejected, times out, or explicitly unlocked +- [`Network`](network.md): Network Service in Fabric Token SDK hides complexities of the ledger (Fabric or Orion) for developers. +It uses a driver-based design allowing for future support of additional platforms. +- [`Interoperability`](interop.md): Fabric Token SDK allows spending tokens based on conditions defined in scripts. +You encode the script within the token's owner field, and the backend interprets it during spending. +This enables interoperability and cross-chain operations. diff --git a/docs/services/storage.md b/docs/services/storage.md new file mode 100644 index 000000000..9b0bfc468 --- /dev/null +++ b/docs/services/storage.md @@ -0,0 +1,35 @@ +# Storage + +The Fabric Token SDK utilizes a robust data management system to ensure the secure and reliable tracking of all token-related activities. +This system leverages several databases, each with a specific purpose: + +* **Transaction Database (ttxdb)**: + This critical database serves as the central repository for all transaction records. + It captures every token issuance, transfer, or redemption, providing a complete historical record of token activity within the network. + The ttxdb service is locate under [`token/services/ttxdb`](./../../token/services/ttxdb). + +* **Token Database (tokendb)**: + The tokendb acts as the registry for all tokens within the system. + It stores detailed information about each token, including its unique identifier, denomination type (think currency or unique identifier), current ownership, and total quantity in circulation. + By referencing the tokendb, developers and network participants can obtain a clear picture of the token landscape. + The tokendb is used by the `Token Selector`, to select the tokens to use in each transaction, and by the `Token Vault Service` to provide its services. + The tokendb service is locate under [`token/services/tokendb`](./../../token/services/tokendb). + +* **Audit Database (auditdb)** (if applicable): + For applications requiring enhanced auditability, the auditdb provides an additional layer of transparency. + It meticulously stores audit records for transactions that have undergone the auditing process. + This functionality is particularly valuable for scenarios where regulatory compliance or tamper-proof records are essential. + The auditdb service is locate under [`token/services/auditdb`](./../../token/services/auditdb). + +* **Identity Database (identitydb)**: + The identitydb plays a crucial role in managing user identities and wallets within the network. + It securely stores walet configurations, identity-related audit information, and so on, enabling secure interactions with the token system. + The identitydb service is locate under [`token/services/identitydb`](./../../token/services/identitydb). + +The Fabric Token SDK offers flexibility in deploying these databases. Developers can choose to: + +* **Instantiate in Isolation:** Each database can operate independently, utilizing a distinct backend system for optimal performance and manageability. + +* **Shared Backend:** Alternatively, a single backend system can be shared by all databases, offering a more streamlined approach for deployments with simpler requirements. + +The specific driver used by the application will ultimately determine the available deployment options. diff --git a/docs/services/ttx.md b/docs/services/ttx.md new file mode 100644 index 000000000..f63b8d3d8 --- /dev/null +++ b/docs/services/ttx.md @@ -0,0 +1,30 @@ +# Token Transaction Service + +The Token SDK simplifies token transaction assembly with a dedicated service in the [`token/services/ttx`](./../../token/services/ttx) package. +This service handles the entire transaction lifecycle, from building the transaction to managing its various stages. + +Even better, this service is ledger-agnostic. +Whether you're using Fabric, Orion, or another platform, you can use the same service to assemble token transactions. +This flexibility is thanks to the `token/services/network` service, which acts as an abstraction layer, hiding the complexities of the underlying ledger technology. +We'll delve deeper into this service later. + +Now, let's break down the high-level steps involved in a token transaction lifecycle: + +1. **Assemble the Token Transaction:** + During this phase, the involved parties collaborate to define the token operations that need to occur atomically (all at once). + They achieve this by assembling these operations into a token transaction, following an interactive business process. + Behind the scenes, a token transaction includes a Token Request, as specified by the Token API. + +2. **Collect Endorsements:** + Once the token transaction is ready (meaning the Token Request is finalized), one party, designated as the leader, takes charge of the following steps: + + - **Gather Signatures:** The leader collects signatures (endorsements) from relevant parties for each action: + - Issuers of any new tokens (if applicable) + - Owners of any tokens being spent (if applicable) + - **Request Audit:** + The leader sends the token transaction to an auditor for verification. If all checks pass, the auditor signs the transaction and returns the signature to the leader. + This step is optional + - **Request Approval:** Now, the transaction needs to be validated and converted into a format compatible with the ledger backend. The leader strips all private data from the transaction and sends it to approvers for validation and translation. These approvers send back the translated transaction signed with their approvals. The leader then attaches these approvals to the original transaction. + - **Distribute Approvals:** Finally, the leader distributes the complete token transaction, including endorsements, to all participating parties. + +3. **Commit:** With everything in place, the transaction is ready to be committed. The leader sends the transaction to the ledger backend (e.g., the ordering service in Fabric), again removing any private information. The leader and all other parties can then wait for confirmation (finality) from the ledger backend, indicating that the transaction is committed to the local vault. diff --git a/docs/services/vault.md b/docs/services/vault.md new file mode 100644 index 000000000..1773414c6 --- /dev/null +++ b/docs/services/vault.md @@ -0,0 +1,36 @@ +# Token Vault Service + +Located within the [`token/services/vault`](./../../token/services/vault) package, the Token Vault service acts as your personal vault for all the tokens you hold. +It meticulously tracks every token currently residing in the wallets under your control, regardless of whether they were directly issued to you or transferred from external parties. + +One of the key strengths of the Token Vault service is its adaptability. +It leverages the network service to seamlessly connect with the specific ledger backend's local vault instance. +This translates to a technology-agnostic solution that functions flawlessly regardless of the underlying infrastructure. + +The Token Vault service empowers you with a comprehensive suite of functionalities for managing your tokens: + +* **Query Service:** This service provides insightful queries to help you navigate your token holdings effectively. + Here's a glimpse into some of the valuable functionalities offered: + + * **IsPending:** This function clarifies the status of a transaction by returning `true` if the transaction with the provided ID remains pending and `false` if it has been completed. + * **IsMine:** As the name suggests, this function verifies ownership. It returns `true` if the provided token ID belongs to any of your known wallets. + * **Unspent Token Iterators:** These iterators act as powerful tools for exploring your unspent tokens. They come in two flavors: + * **UnspentTokensIterator()**: This iterator efficiently traverses through all your unspent tokens. + * **UnspentTokensIteratorBy(id, typ string)**: This specialized iterator allows you to filter your unspent tokens based on ownership and type. You can specify a wallet ID (`id`) and an optional token type (`typ`). If no type is provided, the iterator returns tokens of any type owned by the specified wallet. + * **List Functions:** The Token Vault service offers several list functions to provide consolidated views of your tokens: + * **ListUnspentTokens()**: This function returns a comprehensive list of all your unspent tokens. + * **ListAuditTokens(ids ...*token.ID)**: This function retrieves a list of audited tokens associated with the provided token IDs. + * **ListHistoryIssuedTokens()**: This function delivers a detailed list of all tokens that have been issued within the network. + * **Public Parameters:** The PublicParams() function retrieves the public parameters associated with the token system. + * **Token Information Retrieval:** These functions provide mechanisms to access information about your tokens: + * **GetTokenInfos(ids []*token.ID)**: This function retrieves information for the provided token IDs. + * **GetTokenOutputs(ids []*token.ID)**: Similar to the previous function, this function retrieves the raw token outputs stored on the ledger for the provided IDs. + * **GetTokenInfoAndOutputs(ids []*token.ID)**: This function offers a combined approach, retrieving both the token information and their corresponding outputs for the provided IDs. + * **GetTokens:** This function retrieves a list of tokens along with their corresponding vault keys. + * **WhoDeletedTokens:** This function delves into the history of deleted tokens. It provides information about who deleted the specified tokens (if applicable) and returns a boolean array indicating whether each token at a given position has been deleted. + +* **Certification Service (if applicable):** + This service provides functionalities for managing token certifications, which are additional layers of validation used in certain token systems. + +The Token Vault service equips you with a robust and adaptable toolkit for managing your tokens. +Its rich set of functionalities empowers you to maintain a clear and secure grasp on your token holdings. diff --git a/docs/token-api.md b/docs/token-api.md deleted file mode 100644 index 6d24745d4..000000000 --- a/docs/token-api.md +++ /dev/null @@ -1,215 +0,0 @@ -# Token API - -The `Token API`, located in `token` and `token/token` packages, -offers a useful abstraction to deal with tokens in an implementation and backend independent way. - -The Token SDK handles tokens that consist of the following triplet: -- `Owner`: The owner of the token; Each driver implementation can interpreter this field as needed. It can be a public-key, a script, - anything the underlying specific driver supports. -- `Type`: The *denomination* of the token; - This is a string whose value can be application specific. Examples are: - The denomination of a digital currency or unique identifiers. -- `Quantity`: The amount stored by this token. It is a positive number, larger or equal to zero, - encoded as a string containing a number in base 16. The string starts with the prefix `0x`. - -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. - -The above definition allows the developers to define non-fungible tokens as well. -A non-fungible token is a token whose quantity is `1` and whose type is `unique`. -If uniqueness is guaranteed, then such a token is by all means a non-fungible token. -Drivers are free to implement additional semantics for non-fungible tokens. - -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 that 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. - -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 some of the main building blocks the `Token API` consists of: - -![token_api.png](imgs/token_api.png) - -## Token Management Service - -The `Token Management Service` (`token.ManagementService`) (TMS, for short) is the entry point of the Token SDK -and gives access to the rest of the APIs. -The tuple `(network, channel, namespace, public parameters)` uniquely identifies a TMS, where: -- `network` is the identifier of the network/backend of reference; -- `channel` is the channel inside the network. If not available, it is empty; -- `namespace` is the namespace, inside the channel, that stores the tokens. -- `public parameters` contain all information needed to operate the specific token infrastructure. - -Developers can get an instance of the `Token Management Service` by using the `GetManagementService` function: -```go - tms := token.GetManagementService(context) -``` -where `context` is an [`FSC View Context`](https://github.com/hyperledger-labs/fabric-smart-client/blob/main/docs/view/api.md). -Developers can pass additional options to request a specific TMS like: -```go - tms := token.GetManagementService(context, token.WithNetwork("my-network")) -``` - -## Public Parameters Manager - -Each TMS is associated to some public parameters that contain all information needed to operate the token infrastructure. -The `Public Parameters Manager` (`token.PublicParametersManager`) is responsible for handling the public parameters. -Even though, parts of the public parameters are driver-specific, we can identify the following common information: - - `Precision`: The precision used to represent the token quantity. - - `MaxTokenValue`: It is the maximum quantity that a token can contain. - - `TokenDataHiding`: When true it means that the content of the tokens is hidden. - - `GraphHiding`: When true it means that the tokens are untraceable. - - `Auditors`: A list of auditor identities. - -Developers can access the `Public Parameters Manager` of a give TMS as follows: -```go - ppm := tms.PublicParametersManager() -``` - -## Wallet Manager - -A Wallet consists of a long-term identity and all its derivation (if any). -Examples of long-term identities are: - - An `X509 Certificate` for an ECDSA signing public-key. - - An `Idemix Credential`. In this case, the wallet will contain also all pseudonyms derived from the credential. - -However, it is always the specific driver that dictates what a long-term identity is. - -All operations that require a signature refer to wallets to identify the signing and verification keys. -There are wallets for `Issuers`, `Owners`, and `Auditors`. Notice that `Certifiers` are not supported by this driver -because the driver does not support `Graph Hiding`. -Depending on the nature of the wallet, additional information can be extracted like: - - An Issuer Wallet gives access to the list of issued tokens; - - An Owner Wallet gives access to the list of owned unspent tokens; - -The Wallet Manager (`token.WalletManager`) helps to manage these wallets. - -Developers can access the Wallet Manager of a given TMS as follows: -```go - wm := tms.WalletManager() -``` - -## Token Request - -The Token Request (`token.Request`) is a container of token actions (issue, transfer, and redeem) that must be -performed atomically. The Token Request offers an API to add actions or inspect actions already present in the container. - -This is the anatomy of a Token Request: - -![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. -- `Actions`: It is a collection of `Token Action`: - - `Issues`, to create new Tokens; - - `Transfers`, to manipulate Tokens (e.g., transfer ownership or redeem) - - The actions in the collection are independent. An action cannot spend tokens created by another action in the same Token Request. - In addition, actions come with a set of `Witnesses` to verify the `right to spend` or the `right to issue` a given token. - In the simplest case, the witnesses are the signatures of the issuers and the token owners. - -- `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, the ledger does not store any metadata. - -Developers can create a new Token Request as follows: -```go - tr, err := token.NewRequest() -``` -or unmarshal a previously marshalled Token Request as follows: -```go - tr, err := token.NewRequestFromBytes(achor, actions, metadata) -``` - -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 ledger backend. -As we mentioned earlier, a Token Request is itself agnostic to the details of the specific ledger backend. -Indeed, a Token Request must be translated to the Transaction format of the target ledger backend 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` -because it is ledger dependant. - -Here is a pictorial representation of the translation process for Fabric: - -![token_request_translator.png](imgs/token_request_translator.png) - -In Fabric, it is the `Token Chaincode` that performs validation and translations of token requests. -More information [`here`](./services.md). - -## Validator - -The validator (`token.Validator`) is the component that sets the validation rules for a Token Request. The rules depend on the -type of tokens supported (fungible and non-fungible), and on the specific driver implementation. -A Validator validates Token Requests with the respect to: -- A given Anchor (e.g., Fabric TxID), and -- The Ledger. Notice that, in certain implementations, the ledger might not be needed. - -Even though, certain validation rules are driver specific, we can identify the following general validation rules. -A token request should: -- Be Well-formed, and -- Satisfies the constraints of the payment system. Namely: -- 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) - -## Token Vault - -The vault (`token.Vault`) gives access to the tokens that are owned by the wallets in the wallet manager. - -Developers can access the vault of a given TMS as follows: -```go - vault := tms.Vault() -``` - -## Token Selector Manager - -The Token Selector Manager (`token.SelectorManager`) provides instances of a token selector implementation. - -A token selector (`token.Selector`) allows developers to select tokens from the vault under certain conditions. -For example, a token selector can be used to select tokens of a given type and for a given amount that are owned by a specific wallet, -The selector must ensure that two different routines receives different tokens. This is important to avoid double-spending. -This means that selected tokens are locked until the transaction that uses them is committed or rejected, or a timeout occurs, -or an explicit unlock operation is performed. - -Developers can access the Selector Manager of a given TMS as follows: -```go - sm := tms.SelectorManager() -``` - -and get a new selector, for a given id, as follows: - -```go - selector := sm.NewSelector(tr.Anchor) -``` - -Notice that, the tokens selected by this selector will be locked under the passed id. - -## Signature Service - -The Signature Service (`token.SignatureService`) is the component that gives access to signature verifiers and signers -bound to identities obtained from the Wallet Manager. - -Developers can access the Signature Service of a given TMS as follows: -```go - sigService := tms.SignatureService() -``` - -## Config Manager - -The Config Manager (`token.ConfigManager`) is the component that gives access to the configuration of the TMS. - -Developers can access the Config Manager of a given TMS as follows: -```go - cm := tms.ConfigManager() -``` diff --git a/samples/fungible/.gitignore b/samples/fungible/.gitignore deleted file mode 100644 index f2cb467d3..000000000 --- a/samples/fungible/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -cmd/ -testdata/ -fungible \ No newline at end of file diff --git a/samples/fungible/README.md b/samples/fungible/README.md deleted file mode 100644 index 6fdcfb04e..000000000 --- a/samples/fungible/README.md +++ /dev/null @@ -1,1328 +0,0 @@ -# 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 maintained by the nodes. - -**Remark**: The Smart Fabric Client SDK and the Token SDK can be embedded in an already existing -Go-based application node by using the following code: -```go - // import - // fscnode "github.com/hyperledger-labs/fabric-smart-client/node" - // "github.com/hyperledger-labs/fabric-smart-client/platform/fabric/sdk" - // sdk "github.com/hyperledger-labs/fabric-token-sdk/token/sdk" - - // Instantiate a new Fabric Smart Client Node - n := fscnode.New() // Use NewFromConfPath() to load configuration from a file - // Install the required SDKs - n.InstallSDK(fabric.NewSDK(n)) - n.InstallSDK(sdk.NewSDK(n)) - - // Execute the stack - n.Execute(func() error { - // Your initialization code here - return nil - }) -``` -This is also the code that is executed by a standalone Fabric Smart Client node. - -Now, 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. - -```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 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 resulting transaction will be signed using idemix, for example), - // and specify the auditor that must be contacted to approve the operation - tx, err := ttxcc.NewAnonymousTransaction( - context, - ttxcc.WithAuditor( - view2.GetIdentityProvider(context).Identity("auditor"), // Retrieve the auditor's FSC node identity - ), - ) - tx.SetApplicationMetadata("github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/issue", []byte("issue")) - tx.SetApplicationMetadata("github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/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 transaction. - // This is all done in one shot running the following view. - // Before completing, all recipients receive the approved 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. - -```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 (ttx.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. - -```go -// 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 resulting transaction will be signed using idemix, for example), - // and specify the auditor that must be contacted to approve the operation. - tx, err := ttxcc.NewAnonymousTransaction( - context, - ttxcc.WithAuditor(view2.GetIdentityProvider(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 transaction. - // This is all done in one shot running the following view. - // Before completing, all recipients receive the approved 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. - -```go -// 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 resulting transaction will be signed using idemix, for example), - // and specify the auditor that must be contacted to approve the operation. - tx, err := ttxcc.NewAnonymousTransaction( - context, - ttxcc.WithAuditor(view2.GetIdentityProvider(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 transaction. - // This is all done in one shot running the following view. - // Before completing, all recipients receive the approved 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. - -```go -// 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 resulting transaction will be signed using idemix, for example), - // and specify the auditor that must be contacted to approve the operation. - tx, err := ttxcc.NewAnonymousTransaction( - context, - ttxcc.WithAuditor(view2.GetIdentityProvider(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 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. - -```go -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 (ttx.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: - -```go - -// 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: - -```go -// 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; -2. 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: - -```go -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("registerAuditor", &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.SetSDK(fscTopology, &sdk.SDK{}) - tms := tokenTopology.AddTMS(fabricTopology, tokenSDKDriver) - tms.SetNamespace([]string{"Org1"}, "16") - tms.AddAuditor(auditor) - - return []api.Topology{fabricTopology, tokenTopology, fscTopology} -} -``` - -The above topology takes in input the token driver name. - -### Boostrap the networks - -Bootstrap of the networks requires both Fabric Docker images and Fabric binaries. To ensure you have the required images you can use the following Makefile target in the project root directory: - -```shell -make fabric-docker-images -``` - -To ensure you have the required fabric binary files and set the `FAB_BINS` environment variable to the correct place you can do the following in the project root directory - -```shell -make download-fabric -export FAB_BINS=$PWD/../fabric/bin -``` - -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`. - -```shell -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. -These go main files are synthesized on the fly, and -the CLI compiles and then runs them. - -If everything is successful, you will see something like the following: - -```shell -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: - -```shell -./fungible network clean --path ./testdata -``` - -The `./testdata` and `./cmd` folders will be deleted. - -### Anatomy of the configuration file - -Each FSC node has a configuration file that contains the information about the FSC node itself, -the networks (Fabric, Orion, ...) that the FSC node is part of, and the token-related configuration. - -Here is how the configuration file looks like for Alice's node: - -```yaml -# Logging section -logging: - # Spec - spec: info - # Format - format: '%{color}%{time:2006-01-02 15:04:05.000 MST} [%{module}] %{shortfunc} -> %{level:.4s} %{id:03x}%{color:reset} %{message}' -# The fsc section is dedicated to the FSC node itself -fsc: - # The FSC id provides a name for this node instance and is used when - # naming docker resources. - id: fsc.alice - # The networkId allows for logical separation of networks and is used when - # naming docker resources. - networkId: u6l7g33mhjdn3ko7zrnzl5ftae - # This represents the endpoint to other FSC nodes in the same organization. - address: 127.0.0.1:20006 - # Whether the FSC node should programmatically determine its address - # This case is useful for docker containers. - # When set to true, will override FSC address. - addressAutoDetect: true - # GRPC Server listener address - listenAddress: 127.0.0.1:20006 - # Identity of this node, used to connect to other nodes - identity: - # X.509 certificate used as identity of this node - cert: - file: /home/vagrant/testdata/fsc/crypto/peerOrganizations/fsc.example.com/peers/alice.fsc.example.com/msp/signcerts/alice.fsc.example.com-cert.pem - # Private key matching the X.509 certificate - key: - file: /home/vagrant/testdata/fsc/crypto/peerOrganizations/fsc.example.com/peers/alice.fsc.example.com/msp/keystore/priv_sk - # Admin X.509 certificates - admin: - certs: - - /home/vagrant/testdata/fsc/crypto/peerOrganizations/fsc.example.com/users/Admin@fsc.example.com/msp/signcerts/Admin@fsc.example.com-cert.pem - # TLS Settings - # (We use here the same set of properties as Hyperledger Fabric) - tls: - # Require server-side TLS - enabled: true - # Require client certificates / mutual TLS for inbound connections. - # Note that clients that are not configured to use a certificate will - # fail to connect to the node. - clientAuthRequired: false - # X.509 certificate used for TLS server - cert: - file: /home/vagrant/testdata/fsc/crypto/peerOrganizations/fsc.example.com/peers/alice.fsc.example.com/tls/server.crt - # Private key used for TLS server - key: - file: /home/vagrant/testdata/fsc/crypto/peerOrganizations/fsc.example.com/peers/alice.fsc.example.com/tls/server.key - # X.509 certificate used for TLS when making client connections. - # If not set, fsc.tls.cert.file will be used instead - clientCert: - file: /home/vagrant/testdata/fsc/crypto/peerOrganizations/fsc.example.com/peers/alice.fsc.example.com/tls/server.crt - # Private key used for TLS when making client connections. - # If not set, fsc.tls.key.file will be used instead - clientKey: - file: /home/vagrant/testdata/fsc/crypto/peerOrganizations/fsc.example.com/peers/alice.fsc.example.com/tls/server.key - # rootcert.file represents the trusted root certificate chain used for verifying certificates - # of other nodes during outbound connections. - rootcert: - file: /home/vagrant/testdata/fsc/crypto/peerOrganizations/fsc.example.com/peers/alice.fsc.example.com/tls/ca.crt - # If mutual TLS is enabled, clientRootCAs.files contains a list of additional root certificates - # used for verifying certificates of client connections. - clientRootCAs: - files: - - /home/vagrant/testdata/fsc/crypto/peerOrganizations/fsc.example.com/peers/alice.fsc.example.com/tls/ca.crt - rootCertFile: /home/vagrant/testdata/fsc/crypto/ca-certs.pem - # Keepalive settings for node server and clients - keepalive: - # MinInterval is the minimum permitted time between client pings. - # If clients send pings more frequently, the peer server will - # disconnect them - minInterval: 60s - # Interval is the duration after which if the server does not see - # any activity from the client it pings the client to see if it's alive - interval: 300s - # Timeout is the duration the server waits for a response - # from the client after sending a ping before closing the connection - timeout: 600s - # P2P configuration - p2p: - # Listening address - listenAddress: /ip4/127.0.0.1/tcp/20007 - # If empty, this is a P2P boostrap node. Otherwise, it contains the name of the FSC node that is a bootstrap node. - # The name of the FSC node that is a bootstrap node must be set under fsc.endpoint.resolvers - bootstrapNode: issuer - # The Key-Value Store is used to store various information related to the FSC node - kvs: - persistence: - # Persistence type can be \'badger\' (on disk) or \'memory\' - type: badger - opts: - path: /home/vagrant/testdata/fsc/nodes/alice/kvs - # HTML Server configuration for REST calls - web: - # Enable the REST server - enabled: true - # HTTPS server listener address - address: 0.0.0.0:20008 - tls: - # Require server-side TLS - enabled: true - cert: - file: /home/vagrant/testdata/fsc/crypto/peerOrganizations/fsc.example.com/peers/alice.fsc.example.com/tls/server.crt - key: - file: /home/vagrant/testdata/fsc/crypto/peerOrganizations/fsc.example.com/peers/alice.fsc.example.com/tls/server.key - clientRootCAs: - files: - - /home/vagrant/testdata/fsc/crypto/peerOrganizations/fsc.example.com/peers/alice.fsc.example.com/tls/ca.crt - rootCertFile: /home/vagrant/testdata/fsc/crypto/ca-certs.pem - tracing: - provider: none - udp: - address: 127.0.0.1:8125 - metrics: - # metrics provider is one of statsd, prometheus, or disabled - provider: prometheus - # statsd configuration - statsd: - # network type: tcp or udp - network: udp - # statsd server address - address: 127.0.0.1:8125 - # the interval at which locally cached counters and gauges are pushed - # to statsd; timings are pushed immediately - writeInterval: 10s - # prefix is prepended to all emitted statsd metrics - prefix: - - # The endpoint section tells how to reach other FSC node in the network. - # For each node, the name, the domain, the identity of the node, and its addresses must be specified. - endpoint: - resolvers: - - name: issuer - domain: fsc.example.com - identity: - id: issuer - path: /home/vagrant/testdata/fsc/crypto/peerOrganizations/fsc.example.com/peers/issuer.fsc.example.com/msp/signcerts/issuer.fsc.example.com-cert.pem - addresses: - P2P: 127.0.0.1:20001 - aliases: - - issuer.id1 - - name: auditor - domain: fsc.example.com - identity: - id: auditor - path: /home/vagrant/testdata/fsc/crypto/peerOrganizations/fsc.example.com/peers/auditor.fsc.example.com/msp/signcerts/auditor.fsc.example.com-cert.pem - addresses: - aliases: - - name: bob - domain: fsc.example.com - identity: - id: bob - path: /home/vagrant/testdata/fsc/crypto/peerOrganizations/fsc.example.com/peers/bob.fsc.example.com/msp/signcerts/bob.fsc.example.com-cert.pem - addresses: - aliases: - - bob.id1 - - name: charlie - domain: fsc.example.com - identity: - id: charlie - path: /home/vagrant/testdata/fsc/crypto/peerOrganizations/fsc.example.com/peers/charlie.fsc.example.com/msp/signcerts/charlie.fsc.example.com-cert.pem - addresses: - aliases: - - charlie.id1 - - -# The fabric section defines the configuration of the fabric networks. -fabric: - enabled: true - # The name of the network - default: - # Is this the default network? - default: true - # BCCSP configuration for this fabric network. Similar to the equivalent section in Fabric peer configuration. - BCCSP: - Default: SW - SW: - Hash: SHA2 - Security: 256 - FileKeyStore: - KeyStore: - # The MSP config path of the default identity to connect to this network. - mspConfigPath: /home/vagrant/testdata/fabric.default/crypto/peerOrganizations/org2.example.com/peers/alice.org2.example.com/msp - # Local MSP ID of the default identity - localMspId: Org2MSP - # Cache size to use when handling idemix pseudonyms. If the value is larger than 0, the cache is enabled and - # pseudonyms are generated in batches of the given size to be ready to be used. - mspCacheSize: 500 - # Additional MSP identities that can be used to connect to this network. - msps: - - id: idemix # The id of the identity. - mspType: idemix # The type of the MSP. - mspID: IdemixOrgMSP # The MSP ID. - # The path to the MSP folder containing the cryptographic materials. - path: /home/vagrant/testdata/fabric.default/crypto/peerOrganizations/org2.example.com/peers/alice.org2.example.com/extraids/idemix - # TLS Settings - tls: - enabled: true - clientAuthRequired: false - cert: - file: /home/vagrant/testdata/fabric.default/crypto/peerOrganizations/org2.example.com/peers/alice.org2.example.com/tls/server.crt - key: - file: /home/vagrant/testdata/fabric.default/crypto/peerOrganizations/org2.example.com/peers/alice.org2.example.com/tls/server.key - clientCert: - file: /home/vagrant/testdata/fabric.default/crypto/peerOrganizations/org2.example.com/peers/alice.org2.example.com/tls/server.crt - clientKey: - file: /home/vagrant/testdata/fabric.default/crypto/peerOrganizations/org2.example.com/peers/alice.org2.example.com/tls/server.key - rootcert: - file: /home/vagrant/testdata/fabric.default/crypto/peerOrganizations/org2.example.com/peers/alice.org2.example.com/tls/ca.crt - clientRootCAs: - files: - - /home/vagrant/testdata/fabric.default/crypto/peerOrganizations/org2.example.com/peers/alice.org2.example.com/tls/ca.crt - rootCertFile: /home/vagrant/testdata/fabric.default/crypto/ca-certs.pem - # List of orderer nodes this node can connect to. There must be at least one orderer node. - orderers: - - address: 127.0.0.1:20015 - connectionTimeout: 10s - tlsEnabled: true - tlsRootCertFile: /home/vagrant/testdata/fabric.default/crypto/ca-certs.pem - serverNameOverride: - # List of trusted peers this node can connect to. There must be at least one trusted peer. - peers: - - address: 127.0.0.1:20026 - connectionTimeout: 10s - tlsEnabled: true - tlsRootCertFile: /home/vagrant/testdata/fabric.default/crypto/ca-certs.pem - serverNameOverride: - # List of channels this node is aware of - channels: - - name: testchannel - default: true - # Configuration of the vault used to store the RW sets assembled by this node - vault: - persistence: - type: file - opts: - path: /home/vagrant/testdata/fsc/nodes/alice/fabric.default/vault - # The endpoint section tells how to reach other Fabric nodes in the network. - # For each node, the name, the domain, the identity of the node, and its addresses must be specified. - endpoint: - resolvers: - - name: Org1_peer_0 - domain: org1.example.com - identity: - id: Org1_peer_0 - mspType: bccsp - mspID: Org1MSP - path: /home/vagrant/testdata/fabric.default/crypto/peerOrganizations/org1.example.com/peers/Org1_peer_0.org1.example.com/msp/signcerts/Org1_peer_0.org1.example.com-cert.pem - addresses: - Listen: 127.0.0.1:20019 - aliases: - - name: Org2_peer_0 - domain: org2.example.com - identity: - id: Org2_peer_0 - mspType: bccsp - mspID: Org2MSP - path: /home/vagrant/testdata/fabric.default/crypto/peerOrganizations/org2.example.com/peers/Org2_peer_0.org2.example.com/msp/signcerts/Org2_peer_0.org2.example.com-cert.pem - addresses: - Listen: 127.0.0.1:20026 - aliases: - - name: issuer - domain: org1.example.com - identity: - id: issuer - mspType: bccsp - mspID: Org1MSP - path: /home/vagrant/testdata/fabric.default/crypto/peerOrganizations/org1.example.com/peers/issuer.org1.example.com/msp/signcerts/issuer.org1.example.com-cert.pem - addresses: - aliases: - - issuer - - issuer.id1 - - name: auditor - domain: org1.example.com - identity: - id: auditor - mspType: bccsp - mspID: Org1MSP - path: /home/vagrant/testdata/fabric.default/crypto/peerOrganizations/org1.example.com/peers/auditor.org1.example.com/msp/signcerts/auditor.org1.example.com-cert.pem - addresses: - aliases: - - auditor - - name: alice - domain: org2.example.com - identity: - id: alice - mspType: bccsp - mspID: Org2MSP - path: /home/vagrant/testdata/fabric.default/crypto/peerOrganizations/org2.example.com/peers/alice.org2.example.com/msp/signcerts/alice.org2.example.com-cert.pem - addresses: - aliases: - - alice - - name: bob - domain: org2.example.com - identity: - id: bob - mspType: bccsp - mspID: Org2MSP - path: /home/vagrant/testdata/fabric.default/crypto/peerOrganizations/org2.example.com/peers/bob.org2.example.com/msp/signcerts/bob.org2.example.com-cert.pem - addresses: - aliases: - - bob - - name: charlie - domain: org2.example.com - identity: - id: charlie - mspType: bccsp - mspID: Org2MSP - path: /home/vagrant/testdata/fabric.default/crypto/peerOrganizations/org2.example.com/peers/charlie.org2.example.com/msp/signcerts/charlie.org2.example.com-cert.pem - addresses: - aliases: - - charlie - -token: - enabled: true - # The Token Transaction DB is a database of audited records. It is used to track the - # history of audited events. In particular, it is used to track payments, holdings, - # and transactions of any business party identified by a unique enrollment ID. - ttxdb: - persistence: - # The type of persistence mechanism used to store the Token Transaction DB. (memory, badger) - type: badger - opts: - # The path to the Token Transaction DB. - path: /home/vagrant/testdata/fsc/nodes/alice/kvs - # TMS stands for Token Management Service. A TMS is uniquely identified by a network, channel, and - # namespace identifiers. The network identifier should refer to a configured network (Fabric, Orion, and so on). - # The meaning of channel and namespace are network dependent. For Fabric, the meaning is clear. - # For Orion, channel is empty and namespace is the DB name to use. - tms: - - channel: testchannel # Channel identifier within the specified network - namespace: zkat # Namespace identifier within the specified channel - # Network identifier this TMS refers to. It must match the identifier of a Fabric or Orion network - network: default - # Wallets associated with this TMS - wallets: - # Owners wallets are used to own tokens - owners: - - default: true - # ID of the wallet - id: alice - # Path to folder containing the cryptographic material - path: /home/vagrant/testdata/token/crypto/default-testchannel-zkat/idemix/alice - # Type of the wallet in the form of :: - type: idemix:IdemixOrgMSP:BN254 - - default: false - id: alice.id1 - path: /home/vagrant/testdata/token/crypto/default-testchannel-zkat/idemix/alice.id1 - type: idemix:IdemixOrgMSP:BN254 -``` - -### 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: - -```shell -./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: - -```shell -"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. - -```shell -./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): -```shell -{ - "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: - -```shell -./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: - -```shell -"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: - -```shell -./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): - -```shell -{ - "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: - -```shell -./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): - -```shell -{ - "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. - -```shell -./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 -```shell -"ca195ab8072f12d6e0ed78d977404668e0523dbd45b72991cb2cd853de58e5e8" -``` - -Let us query Charlie's wallet - -````shell -./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): - - -```shell -{ - "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. - -```shell -./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. - -```shell -"790098ae67dc5157c7d679b8def39983adb6e9d7070209f5ab12938d45e1630d" -``` - -We can now query the wallets of Alice and Charlie to confirm the swap happened. - -Alice: - -```shell -./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. - -```shell -{ - "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: - -```shell -./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. - -```shell -{ - "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" - } - ] -} -``` \ No newline at end of file diff --git a/samples/fungible/fungible.go b/samples/fungible/fungible.go deleted file mode 100644 index 5e7031a15..000000000 --- a/samples/fungible/fungible.go +++ /dev/null @@ -1,42 +0,0 @@ -/* -Copyright IBM Corp All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package main - -import ( - "github.com/hyperledger-labs/fabric-smart-client/integration" - "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/api" - "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/cmd" - "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/cmd/network" - view "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/client/view/cmd" - "github.com/hyperledger-labs/fabric-token-sdk/integration/nwo/token" - "github.com/hyperledger-labs/fabric-token-sdk/samples/fungible/topology" - "github.com/pkg/errors" -) - -func main() { - m := cmd.NewMain("Fungible Tokens", "0.1") - mainCmd := m.Cmd() - - network.StartCMDPostNew = func(infrastructure *integration.Infrastructure) error { - infrastructure.RegisterPlatformFactory(token.NewPlatformFactory()) - return nil - } - network.StartCMDPostStart = func(infrastructure *integration.Infrastructure) error { - _, err := infrastructure.Client("auditor").CallView("registerAuditor", nil) - if err != nil { - return errors.WithMessage(err, "failed to register auditor") - } - return nil - } - mainCmd.AddCommand(network.NewCmdWithMultipleTopologies( - map[string][]api.Topology{ - "default": topology.Fabric("dlog"), - "orion": topology.Orion("dlog"), - })) - mainCmd.AddCommand(view.NewCmd()) - m.Execute() -} diff --git a/samples/fungible/topology/fabric.go b/samples/fungible/topology/fabric.go deleted file mode 100644 index 60d559c6d..000000000 --- a/samples/fungible/topology/fabric.go +++ /dev/null @@ -1,120 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package topology - -import ( - "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/api" - "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/fabric" - "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/fsc" - fabric3 "github.com/hyperledger-labs/fabric-smart-client/platform/fabric/sdk" - "github.com/hyperledger-labs/fabric-token-sdk/integration/nwo/token" - fabric2 "github.com/hyperledger-labs/fabric-token-sdk/integration/nwo/token/fabric" - views2 "github.com/hyperledger-labs/fabric-token-sdk/samples/fungible/views" - sdk "github.com/hyperledger-labs/fabric-token-sdk/token/sdk" -) - -func Fabric(tokenSDKDriver string) []api.Topology { - // Fabric - fabricTopology := fabric.NewDefaultTopology() - fabricTopology.EnableIdemix() - fabricTopology.AddOrganizationsByName("Org1", "Org2") - fabricTopology.SetNamespaceApproverOrgs("Org1") - fabricTopology.EnableGRPCLogging() - fabricTopology.EnableLogPeersToFile() - fabricTopology.EnableLogOrderersToFile() - fabricTopology.SetLogging("info", "") - - // FSC - fscTopology := fsc.NewTopology() - fscTopology.SetLogging("info", "") - fscTopology.EnableLogToFile() - fscTopology.EnablePrometheusMetrics() - - // issuer - issuer := fscTopology.AddNodeByName("issuer").AddOptions( - fabric.WithOrganization("Org1"), - fabric.WithAnonymousIdentity(), - token.WithDefaultIssuerIdentity(), - token.WithIssuerIdentity("issuer.id1"), - ) - issuer.RegisterViewFactory("issue", &views2.IssueCashViewFactory{}) - issuer.RegisterViewFactory("issued", &views2.ListIssuedTokensViewFactory{}) - - // auditor - auditor := fscTopology.AddNodeByName("auditor").AddOptions( - fabric.WithOrganization("Org1"), - fabric.WithAnonymousIdentity(), - token.WithAuditorIdentity(), - ) - auditor.RegisterViewFactory("registerAuditor", &views2.RegisterAuditorViewFactory{}) - - // alice - alice := fscTopology.AddNodeByName("alice").AddOptions( - fabric.WithOrganization("Org2"), - fabric.WithAnonymousIdentity(), - token.WithDefaultOwnerIdentity(), - token.WithOwnerIdentity("alice.id1"), - ) - alice.RegisterResponder(&views2.AcceptCashView{}, &views2.IssueCashView{}) - alice.RegisterResponder(&views2.AcceptCashView{}, &views2.TransferView{}) - alice.RegisterViewFactory("transfer", &views2.TransferViewFactory{}) - alice.RegisterViewFactory("redeem", &views2.RedeemViewFactory{}) - alice.RegisterViewFactory("swap", &views2.SwapInitiatorViewFactory{}) - alice.RegisterViewFactory("unspent", &views2.ListUnspentTokensViewFactory{}) - - // bob - bob := fscTopology.AddNodeByName("bob").AddOptions( - fabric.WithOrganization("Org2"), - fabric.WithAnonymousIdentity(), - token.WithDefaultOwnerIdentity(), - token.WithOwnerIdentity("bob.id1"), - ) - bob.RegisterResponder(&views2.AcceptCashView{}, &views2.IssueCashView{}) - bob.RegisterResponder(&views2.AcceptCashView{}, &views2.TransferView{}) - bob.RegisterResponder(&views2.SwapResponderView{}, &views2.SwapInitiatorView{}) - bob.RegisterViewFactory("transfer", &views2.TransferViewFactory{}) - bob.RegisterViewFactory("redeem", &views2.RedeemViewFactory{}) - bob.RegisterViewFactory("swap", &views2.SwapInitiatorViewFactory{}) - bob.RegisterViewFactory("unspent", &views2.ListUnspentTokensViewFactory{}) - - // charlie - charlie := fscTopology.AddNodeByName("charlie").AddOptions( - fabric.WithOrganization("Org2"), - fabric.WithAnonymousIdentity(), - token.WithDefaultOwnerIdentity(), - token.WithOwnerIdentity("charlie.id1"), - ) - charlie.RegisterResponder(&views2.AcceptCashView{}, &views2.IssueCashView{}) - charlie.RegisterResponder(&views2.AcceptCashView{}, &views2.TransferView{}) - charlie.RegisterResponder(&views2.SwapResponderView{}, &views2.SwapInitiatorView{}) - charlie.RegisterViewFactory("transfer", &views2.TransferViewFactory{}) - charlie.RegisterViewFactory("redeem", &views2.RedeemViewFactory{}) - charlie.RegisterViewFactory("swap", &views2.SwapInitiatorViewFactory{}) - charlie.RegisterViewFactory("unspent", &views2.ListUnspentTokensViewFactory{}) - - tokenTopology := token.NewTopology() - tokenTopology.SetSDK(fscTopology, &sdk.SDK{}) - tms := tokenTopology.AddTMS(fscTopology.ListNodes(), fabricTopology, fabricTopology.Channels[0].Name, tokenSDKDriver) - fabric2.SetOrgs(tms, "Org1") - tms.SetTokenGenPublicParams("16") - tms.AddAuditor(auditor) - - // Add Fabric SDK to FSC Nodes - fscTopology.AddSDK(&fabric3.SDK{}) - - // Monitoring - //monitoringTopology := monitoring.NewTopology() - //monitoringTopology.EnableHyperledgerExplorer() - //monitoringTopology.EnablePrometheusGrafana() - - return []api.Topology{ - fabricTopology, - tokenTopology, - fscTopology, - //monitoringTopology, - } -} diff --git a/samples/fungible/topology/orion.go b/samples/fungible/topology/orion.go deleted file mode 100644 index e07710af8..000000000 --- a/samples/fungible/topology/orion.go +++ /dev/null @@ -1,108 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package topology - -import ( - "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/api" - "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/fsc" - "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/orion" - "github.com/hyperledger-labs/fabric-token-sdk/integration/nwo/token" - orion2 "github.com/hyperledger-labs/fabric-token-sdk/integration/nwo/token/orion" - views2 "github.com/hyperledger-labs/fabric-token-sdk/samples/fungible/views" - sdk "github.com/hyperledger-labs/fabric-token-sdk/token/sdk" -) - -func Orion(tokenSDKDriver string) []api.Topology { - // Orion - orionTopology := orion.NewTopology() - - // FSC - fscTopology := fsc.NewTopology() - fscTopology.SetLogging("info", "") - fscTopology.EnableLogToFile() - fscTopology.EnablePrometheusMetrics() - - // issuer - issuer := fscTopology.AddNodeByName("issuer").AddOptions( - orion.WithRole("issuer"), - token.WithDefaultIssuerIdentity(), - token.WithIssuerIdentity("issuer.id1"), - ) - issuer.RegisterViewFactory("issue", &views2.IssueCashViewFactory{}) - issuer.RegisterViewFactory("issued", &views2.ListIssuedTokensViewFactory{}) - - // auditor - auditor := fscTopology.AddNodeByName("auditor").AddOptions( - orion.WithRole("auditor"), - token.WithAuditorIdentity(), - ) - auditor.RegisterViewFactory("registerAuditor", &views2.RegisterAuditorViewFactory{}) - - // alice - alice := fscTopology.AddNodeByName("alice").AddOptions( - orion.WithRole("alice"), - token.WithDefaultOwnerIdentity(), - token.WithOwnerIdentity("alice.id1"), - ) - alice.RegisterResponder(&views2.AcceptCashView{}, &views2.IssueCashView{}) - alice.RegisterResponder(&views2.AcceptCashView{}, &views2.TransferView{}) - alice.RegisterViewFactory("transfer", &views2.TransferViewFactory{}) - alice.RegisterViewFactory("redeem", &views2.RedeemViewFactory{}) - alice.RegisterViewFactory("swap", &views2.SwapInitiatorViewFactory{}) - alice.RegisterViewFactory("unspent", &views2.ListUnspentTokensViewFactory{}) - - // bob - bob := fscTopology.AddNodeByName("bob").AddOptions( - orion.WithRole("bob"), - token.WithDefaultOwnerIdentity(), - token.WithOwnerIdentity("bob.id1"), - ) - bob.RegisterResponder(&views2.AcceptCashView{}, &views2.IssueCashView{}) - bob.RegisterResponder(&views2.AcceptCashView{}, &views2.TransferView{}) - bob.RegisterResponder(&views2.SwapResponderView{}, &views2.SwapInitiatorView{}) - bob.RegisterViewFactory("transfer", &views2.TransferViewFactory{}) - bob.RegisterViewFactory("redeem", &views2.RedeemViewFactory{}) - bob.RegisterViewFactory("swap", &views2.SwapInitiatorViewFactory{}) - bob.RegisterViewFactory("unspent", &views2.ListUnspentTokensViewFactory{}) - - // charlie - charlie := fscTopology.AddNodeByName("charlie").AddOptions( - orion.WithRole("charlie"), - token.WithDefaultOwnerIdentity(), - token.WithOwnerIdentity("charlie.id1"), - ) - charlie.RegisterResponder(&views2.AcceptCashView{}, &views2.IssueCashView{}) - charlie.RegisterResponder(&views2.AcceptCashView{}, &views2.TransferView{}) - charlie.RegisterResponder(&views2.SwapResponderView{}, &views2.SwapInitiatorView{}) - charlie.RegisterViewFactory("transfer", &views2.TransferViewFactory{}) - charlie.RegisterViewFactory("redeem", &views2.RedeemViewFactory{}) - charlie.RegisterViewFactory("swap", &views2.SwapInitiatorViewFactory{}) - charlie.RegisterViewFactory("unspent", &views2.ListUnspentTokensViewFactory{}) - - // we need to define the custodian - custodian := fscTopology.AddNodeByName("custodian") - custodian.AddOptions(orion.WithRole("custodian")) - fscTopology.SetBootstrapNode(custodian) - - tokenTopology := token.NewTopology() - tms := tokenTopology.AddTMS(fscTopology.ListNodes(), orionTopology, "", tokenSDKDriver) - tms.SetTokenGenPublicParams("16") - orion2.SetCustodian(tms, custodian) - - // Enable orion sdk on each FSC node - orionTopology.AddDB(tms.Namespace, "custodian", "issuer", "auditor", "alice", "bob", "charlie") - orionTopology.SetDefaultSDK(fscTopology) - - tokenTopology.SetSDK(fscTopology, &sdk.SDK{}) - tms.AddAuditor(auditor) - - return []api.Topology{ - orionTopology, - tokenTopology, - fscTopology, - } -} diff --git a/samples/fungible/views/accept.go b/samples/fungible/views/accept.go deleted file mode 100644 index 2c735ae1e..000000000 --- a/samples/fungible/views/accept.go +++ /dev/null @@ -1,62 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package views - -import ( - "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/assert" - "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - - "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" - token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" -) - -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 (ttx.MyWallet(context)), if not otherwise specified. - id, err := ttx.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 := ttx.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. - precision := tx.TokenService().PublicParametersManager().PublicParameters().Precision() - for _, output := range outputs.ByRecipient(id).Outputs() { - unspentTokens, err := ttx.MyWallet(context).ListUnspentTokens(ttx.WithType(output.Type)) - assert.NoError(err, "failed retrieving the unspent tokens for type [%s]", output.Type) - upperLimit, err := token2.UInt64ToQuantity(3000, precision) - assert.NoError(err, "failed to convert to quantity") - assert.True(unspentTokens.Sum(precision).Cmp(upperLimit) <= 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(ttx.NewAcceptView(tx)) - assert.NoError(err, "failed to accept new tokens") - - // Before completing, the recipient waits for finality of the transaction - _, err = context.RunView(ttx.NewFinalityView(tx)) - assert.NoError(err, "new tokens were not committed") - - return nil, nil -} diff --git a/samples/fungible/views/auditor.go b/samples/fungible/views/auditor.go deleted file mode 100644 index 4df357991..000000000 --- a/samples/fungible/views/auditor.go +++ /dev/null @@ -1,146 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package views - -import ( - "fmt" - "math/big" - - "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/assert" - "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" -) - -type AuditView struct{} - -func (a *AuditView) Call(context view.Context) (interface{}, error) { - tx, err := ttx.ReceiveTransaction(context) - assert.NoError(err, "failed receiving transaction") - - w := ttx.MyAuditorWallet(context) - assert.NotNil(w, "failed getting default auditor wallet") - - // Validate - auditor, err := ttx.NewAuditor(context, w) - assert.NoError(err, "failed to get auditor instance") - assert.NoError(auditor.Validate(tx), "failed auditing verification") - - // Check ValidationRecords - opRaw := tx.ApplicationMetadata("github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/issue") - if len(opRaw) != 0 { - assert.Equal([]byte("issue"), opRaw, "expected 'issue' application metadata") - metaRaw := tx.ApplicationMetadata("github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/meta") - assert.Equal([]byte("meta"), metaRaw, "expected 'meta' application metadata") - } - - // Check limits - - inputs, outputs, err := auditor.Audit(tx) - assert.NoError(err, "failed retrieving inputs and outputs") - defer auditor.Release(tx) - - aqe := auditor.NewQueryExecutor() - defer aqe.Done() - - // R1: Default payment limit is set to 200. All payments of an amount less than or equal to Default Payment Limit is valid. - eIDs := inputs.EnrollmentIDs() - tokenTypes := inputs.TokenTypes() - fmt.Printf("Limits on inputs [%v][%v]\n", eIDs, tokenTypes) - for _, eID := range eIDs { - assert.NotEmpty(eID, "enrollment id should not be empty") - for _, tokenType := range tokenTypes { - // compute the payment done in the transaction - sent := inputs.ByEnrollmentID(eID).ByType(tokenType).Sum() - received := outputs.ByEnrollmentID(eID).ByType(tokenType).Sum() - fmt.Printf("Payment Limit: [%s] Sent [%d], Received [%d], type [%s]\n", eID, sent.Int64(), received.Int64(), tokenType) - - diff := big.NewInt(0).Sub(sent, received) - if diff.Cmp(big.NewInt(0)) <= 0 { - continue - } - fmt.Printf("Payment Limit: [%s] Diff [%d], type [%s]\n", eID, diff.Int64(), tokenType) - - assert.True(diff.Cmp(big.NewInt(200)) <= 0, "payment limit reached [%s][%s][%s]", eID, tokenType, diff.Text(10)) - // R3: The default configuration is customized by a specific organisation (Guarantor) - } - } - - // R2: Default cumulative payment limit is set to 2000. - for _, eID := range eIDs { - assert.NotEmpty(eID, "enrollment id should not be empty") - for _, tokenType := range tokenTypes { - // compute the payment done in the transaction - sent := inputs.ByEnrollmentID(eID).ByType(tokenType).Sum() - received := outputs.ByEnrollmentID(eID).ByType(tokenType).Sum() - fmt.Printf("Cumulative Limit: [%s] Sent [%d], Received [%d], type [%s]\n", eID, sent.Int64(), received.Int64(), tokenType) - - diff := sent.Sub(sent, received) - if diff.Cmp(big.NewInt(0)) <= 0 { - continue - } - fmt.Printf("Cumulative Limit: [%s] Diff [%d], type [%s]\n", eID, diff.Int64(), tokenType) - - // load last 10 payments, add diff, and check that it is below the threshold - filter, err := aqe.NewPaymentsFilter().ByEnrollmentId(eID).ByType(tokenType).Last(10).Execute() - assert.NoError(err, "failed retrieving last 10 payments") - sumLastPayments := filter.Sum() - fmt.Printf("Cumulative Limit: [%s] Last NewPaymentsFilter [%s], type [%s]\n", eID, sumLastPayments.Text(10), tokenType) - - // R3: The default configuration is customized by a specific organisation (Guarantor) - total := sumLastPayments.Add(sumLastPayments, diff) - assert.True(total.Cmp(big.NewInt(2000)) < 0, "cumulative payment limit reached [%s][%s][%s]", eID, tokenType, total.Text(10)) - } - } - - // R4: Default holding limit is set to 3000. - eIDs = outputs.EnrollmentIDs() - tokenTypes = outputs.TokenTypes() - for _, eID := range eIDs { - assert.NotEmpty(eID, "enrollment id should not be empty") - for _, tokenType := range tokenTypes { - // compute the amount received - received := outputs.ByEnrollmentID(eID).ByType(tokenType).Sum() - sent := inputs.ByEnrollmentID(eID).ByType(tokenType).Sum() - fmt.Printf("Holding Limit: [%s] Sent [%d], Received [%d], type [%s]\n", eID, sent.Int64(), received.Int64(), tokenType) - - diff := received.Sub(received, sent) - if diff.Cmp(big.NewInt(0)) <= 0 { - // Nothing received - continue - } - fmt.Printf("Holding Limit: [%s] Diff [%d], type [%s]\n", eID, diff.Int64(), tokenType) - - // load current holding, add diff, and check that it is below the threshold - filter, err := aqe.NewHoldingsFilter().ByEnrollmentId(eID).ByType(tokenType).Execute() - assert.NoError(err, "failed retrieving holding for [%s][%s]", eIDs, tokenTypes) - currentHolding := filter.Sum() - - fmt.Printf("Holding Limit: [%s] Current [%s], type [%s]\n", eID, currentHolding.Text(10), tokenType) - - total := currentHolding.Add(currentHolding, diff) - assert.True(total.Cmp(big.NewInt(3000)) < 0, "holding limit reached [%s][%s][%s]", eID, tokenType, total.Text(10)) - } - } - aqe.Done() - - return context.RunView(ttx.NewAuditApproveView(w, tx)) -} - -type RegisterAuditorView struct{} - -func (r *RegisterAuditorView) Call(context view.Context) (interface{}, error) { - return context.RunView(ttx.NewRegisterAuditorView( - &AuditView{}, - )) -} - -type RegisterAuditorViewFactory struct{} - -func (p *RegisterAuditorViewFactory) NewView(in []byte) (view.View, error) { - f := &RegisterAuditorView{} - return f, nil -} diff --git a/samples/fungible/views/history.go b/samples/fungible/views/history.go deleted file mode 100644 index 42ecad46a..000000000 --- a/samples/fungible/views/history.go +++ /dev/null @@ -1,45 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ -package views - -import ( - "encoding/json" - - "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/assert" - "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - - "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" -) - -// 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 := ttx.GetIssuerWallet(context, p.Wallet) - assert.NotNil(wallet, "wallet [%s] not found", p.Wallet) - - // Return the list of issued tokens by type - return wallet.ListIssuedTokens(ttx.WithType(p.TokenType)) -} - -type ListIssuedTokensViewFactory struct{} - -func (i *ListIssuedTokensViewFactory) NewView(in []byte) (view.View, error) { - f := &ListIssuedTokensView{ListIssuedTokens: &ListIssuedTokens{}} - err := json.Unmarshal(in, f.ListIssuedTokens) - assert.NoError(err, "failed unmarshalling input") - return f, nil -} diff --git a/samples/fungible/views/issue.go b/samples/fungible/views/issue.go deleted file mode 100644 index 6a546682b..000000000 --- a/samples/fungible/views/issue.go +++ /dev/null @@ -1,118 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package views - -import ( - "encoding/json" - "fmt" - - "github.com/hyperledger-labs/fabric-token-sdk/token" - - view2 "github.com/hyperledger-labs/fabric-smart-client/platform/view" - - token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token" - - "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/assert" - "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - - "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" -) - -// 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 := ttx.RequestRecipientIdentity(context, view2.GetIdentityProvider(context).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 := ttx.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. - precision := token.GetManagementService(context).PublicParametersManager().PublicParameters().Precision() - - history, err := wallet.ListIssuedTokens(ttx.WithType(p.TokenType)) - assert.NoError(err, "failed getting history for token type [%s]", p.TokenType) - fmt.Printf("History [%s,%s]<[230]?\n", history.Sum(precision).ToBigInt().Text(10), p.TokenType) - - // Fail if the sum of the issued tokens and the current quest is larger than 230 - q, err := token2.UInt64ToQuantity(p.Quantity, precision) - assert.NoError(err, "failed to convert quantity") - upperBound, err := token2.UInt64ToQuantity(230, precision) - assert.NoError(err, "failed to convert upper bound") - assert.True(history.Sum(precision).Add(q).Cmp(upperBound) <= 0) - } - - // At this point, the issuer is ready to prepare the token transaction. - // The issuer creates an anonymous transaction (this means that the resulting Fabric transaction will be signed using idemix, for example), - // and specify the auditor that must be contacted to approve the operation - tx, err := ttx.NewAnonymousTransaction( - context, - ttx.WithAuditor( - view2.GetIdentityProvider(context).Identity("auditor"), // Retrieve the auditor's FSC node identity - ), - ) - tx.SetApplicationMetadata("github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/issue", []byte("issue")) - tx.SetApplicationMetadata("github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible/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 transaction. - // This is all done in one shot running the following view. - // Before completing, all recipients receive the approved 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(ttx.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(ttx.NewOrderingAndFinalityView(tx)) - assert.NoError(err, "failed to commit issue transaction") - - return tx.ID(), nil -} - -type IssueCashViewFactory struct{} - -func (p *IssueCashViewFactory) NewView(in []byte) (view.View, error) { - f := &IssueCashView{IssueCash: &IssueCash{}} - err := json.Unmarshal(in, f.IssueCash) - assert.NoError(err, "failed unmarshalling input") - - return f, nil -} diff --git a/samples/fungible/views/list.go b/samples/fungible/views/list.go deleted file mode 100644 index 052e43d5b..000000000 --- a/samples/fungible/views/list.go +++ /dev/null @@ -1,45 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ -package views - -import ( - "encoding/json" - - "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/assert" - "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - - "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" -) - -// 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 := ttx.GetWallet(context, p.Wallet) - assert.NotNil(wallet, "wallet [%s] not found", p.Wallet) - - // Return the list of unspent tokens by type - return wallet.ListUnspentTokens(ttx.WithType(p.TokenType)) -} - -type ListUnspentTokensViewFactory struct{} - -func (i *ListUnspentTokensViewFactory) NewView(in []byte) (view.View, error) { - f := &ListUnspentTokensView{ListUnspentTokens: &ListUnspentTokens{}} - err := json.Unmarshal(in, f.ListUnspentTokens) - assert.NoError(err, "failed unmarshalling input") - return f, nil -} diff --git a/samples/fungible/views/redeem.go b/samples/fungible/views/redeem.go deleted file mode 100644 index 3795a5b13..000000000 --- a/samples/fungible/views/redeem.go +++ /dev/null @@ -1,93 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package views - -import ( - "encoding/json" - - view2 "github.com/hyperledger-labs/fabric-smart-client/platform/view" - - "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/assert" - "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - - token2 "github.com/hyperledger-labs/fabric-token-sdk/token" - "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" - "github.com/hyperledger-labs/fabric-token-sdk/token/token" -) - -// 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 resulting Fabric transaction will be signed using idemix, for example), - // and specify the auditor that must be contacted to approve the operation. - tx, err := ttx.NewAnonymousTransaction( - context, - ttx.WithAuditor(view2.GetIdentityProvider(context).Identity("auditor")), - ) - assert.NoError(err, "failed creating transaction") - - // The sender will select tokens owned by this wallet - senderWallet := ttx.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 transaction. - // This is all done in one shot running the following view. - // Before completing, all recipients receive the approved 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(ttx.NewCollectEndorsementsView(tx)) - assert.NoError(err, "failed to sign transaction") - - // Send to the ordering service and wait for finality - _, err = context.RunView(ttx.NewOrderingAndFinalityView(tx)) - assert.NoError(err, "failed asking ordering") - - return tx.ID(), nil -} - -type RedeemViewFactory struct{} - -func (p *RedeemViewFactory) NewView(in []byte) (view.View, error) { - f := &RedeemView{Redeem: &Redeem{}} - err := json.Unmarshal(in, f.Redeem) - assert.NoError(err, "failed unmarshalling input") - return f, nil -} diff --git a/samples/fungible/views/swap.go b/samples/fungible/views/swap.go deleted file mode 100644 index 370f77726..000000000 --- a/samples/fungible/views/swap.go +++ /dev/null @@ -1,157 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package views - -import ( - "encoding/json" - "math/big" - - view2 "github.com/hyperledger-labs/fabric-smart-client/platform/view" - "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/assert" - "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" -) - -// 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 := ttx.ExchangeRecipientIdentities(context, t.FromWallet, view2.GetIdentityProvider(context).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 resulting Fabric transaction will be signed using idemix, for example), - // and specify the auditor that must be contacted to approve the operation. - tx, err := ttx.NewAnonymousTransaction( - context, - ttx.WithAuditor(view2.GetIdentityProvider(context).Identity("auditor")), - ) - assert.NoError(err, "failed creating transaction") - - // A will select tokens owned by this wallet - senderWallet := ttx.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(ttx.NewCollectActionsView(tx, - &ttx.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.IsValid(), "failed verifying transaction") - - outputs, err := tx.Outputs() - assert.NoError(err, "failed getting outputs") - os := outputs.ByRecipient(other) - assert.Equal(0, os.Sum().Cmp(big.NewInt(int64(t.FromQuantity)))) - assert.Equal(os.Count(), os.ByType(t.FromType).Count()) - - os = outputs.ByRecipient(me) - assert.Equal(0, os.Sum().Cmp(big.NewInt(int64(t.ToQuantity)))) - assert.Equal(os.Count(), os.ByType(t.ToType).Count()) - - // A is ready to collect all the required signatures and form the Transaction. - _, err = context.RunView(ttx.NewCollectEndorsementsView(tx)) - assert.NoError(err, "failed to sign transaction") - - // Send to the ordering service and wait for finality - _, err = context.RunView(ttx.NewOrderingAndFinalityView(tx)) - assert.NoError(err, "failed asking ordering") - - return tx.ID(), nil -} - -type SwapInitiatorViewFactory struct{} - -func (p *SwapInitiatorViewFactory) NewView(in []byte) (view.View, error) { - f := &SwapInitiatorView{Swap: &Swap{}} - err := json.Unmarshal(in, f.Swap) - assert.NoError(err, "failed unmarshalling input") - return f, nil -} - -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 (ttx.MyWallet(context)), - // if not otherwise specified. - _, _, err := ttx.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 := ttx.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 := ttx.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(ttx.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(ttx.NewEndorseView(tx)) - assert.NoError(err, "failed endorsing transaction") - - // Before completing, the recipient waits for finality of the transaction - _, err = context.RunView(ttx.NewFinalityView(tx)) - assert.NoError(err, "new tokens were not committed") - - return tx.ID(), nil -} diff --git a/samples/fungible/views/transfer.go b/samples/fungible/views/transfer.go deleted file mode 100644 index 911e56f0a..000000000 --- a/samples/fungible/views/transfer.go +++ /dev/null @@ -1,103 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package views - -import ( - "encoding/json" - - view2 "github.com/hyperledger-labs/fabric-smart-client/platform/view" - "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/assert" - "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - token2 "github.com/hyperledger-labs/fabric-token-sdk/token" - "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" - "github.com/hyperledger-labs/fabric-token-sdk/token/token" -) - -// 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 := ttx.RequestRecipientIdentity(context, view2.GetIdentityProvider(context).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 resulting Fabric transaction will be signed using idemix, for example), - // and specify the auditor that must be contacted to approve the operation. - tx, err := ttx.NewAnonymousTransaction( - context, - ttx.WithAuditor(view2.GetIdentityProvider(context).Identity("auditor")), - ) - assert.NoError(err, "failed creating transaction") - - // The sender will select tokens owned by this wallet - senderWallet := ttx.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 transaction. - // This is all done in one shot running the following view. - // Before completing, all recipients receive the approved 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(ttx.NewCollectEndorsementsView(tx)) - assert.NoError(err, "failed to sign transaction") - - // Send to the ordering service and wait for finality - _, err = context.RunView(ttx.NewOrderingAndFinalityView(tx)) - assert.NoError(err, "failed asking ordering") - - return tx.ID(), nil -} - -type TransferViewFactory struct{} - -func (p *TransferViewFactory) NewView(in []byte) (view.View, error) { - f := &TransferView{Transfer: &Transfer{}} - err := json.Unmarshal(in, f.Transfer) - assert.NoError(err, "failed unmarshalling input") - return f, nil -} diff --git a/samples/nft/.gitignore b/samples/nft/.gitignore deleted file mode 100644 index ee451550c..000000000 --- a/samples/nft/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -cmd/ -testdata/ -nft \ No newline at end of file diff --git a/samples/nft/README.md b/samples/nft/README.md deleted file mode 100644 index 5bb4f8378..000000000 --- a/samples/nft/README.md +++ /dev/null @@ -1,462 +0,0 @@ -# Token SDK, Non Fungible Tokens, The Basics - -In this Section, we will see examples of how to perform basic token operations -like `issue` and `transfer` on `non-fungible tokens` (NFT, for short). - -We will consider the following business parties: -- `Issuer`: The entity that creates/mints/issues the tokens. -- `Alice` and `Bob`: Each of these parties is a `NFT` holder. -- `Auditor`: The entity that is auditing the token transactions. - -The NFT we use will model a `house` with an address and a valuation. - -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 maintained by the nodes. - -Let us then describe each token operation with examples: - -## Issuance - -Issuance is a business interactive protocol among two parties: an `issuer` -and a `recipient` that will become the owner of the freshly created NFT. - -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. - -```go -// IssueHouse contains the input information to issue a token -type IssueHouse struct { - // IssuerWallet is the issuer's wallet to use - IssuerWallet string - // Recipient is an identifier of the recipient identity - Recipient string - // Address is the address of the house to issue - Address string - // Valuation is the valuation of the house to issue - Valuation uint64 -} - -type IssueHouseView struct { - *IssueHouse -} - -func (p *IssueHouseView) 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 := nftcc.RequestRecipientIdentity(context, view2.GetIdentityProvider(context).Identity(p.Recipient)) - assert.NoError(err, "failed getting recipient identity") - - // 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 := nftcc.NewAnonymousTransaction( - context, - nftcc.WithAuditor( - view2.GetIdentityProvider(context).Identity("auditor"), // Retrieve the auditor's FSC node identity - ), - ) - assert.NoError(err, "failed creating issue transaction") - - // The issuer adds a new issue operation to the transaction following the instruction received - wallet := nftcc.GetIssuerWallet(context, p.IssuerWallet) - assert.NotNil(wallet, "issuer wallet [%s] not found", p.IssuerWallet) - h := &House{ - Address: p.Address, - Valuation: p.Valuation, - } - // The issuer enforce uniqueness of the token by computing a unique identifier for the passed house. - uniqueID, err := uniqueness.GetService(context).ComputeID(h.Address) - assert.NoError(err, "failed computing unique ID") - - err = tx.Issue(wallet, h, recipient, nftcc.WithUniqueID(uniqueID)) - 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(nftcc.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(nftcc.NewOrderingAndFinalityView(tx)) - assert.NoError(err, "failed to commit issue transaction") - - return h.LinearID, nil -} -``` - -The important point to highlight is that the issuer must ensure that the NFT is unique in the system. -There are many ways to do that. The simplest one is to compute a salted hash of the data structure (or part of it) one -wants to convert to an NFT. This is the approach followed by the `uniqueness` package. - -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 AcceptIssuedHouseView struct{} - -func (a *AcceptIssuedHouseView) 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 (ttx.MyWallet(context)), if not otherwise specified. - id, err := nftcc.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 := nftcc.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 one output that names the recipient. - // (The recipient is receiving something) - outputs, err := tx.Outputs() - assert.NoError(err, "failed getting outputs") - assert.NoError(outputs.Validate(), "failed validating outputs") - assert.True(outputs.Count() == 1, "the transaction must contain one output") - assert.True(outputs.ByRecipient(id).Count() == 1, "the transaction must contain one output that names the recipient") - house := &House{} - assert.NoError(outputs.StateAt(0, house), "failed to get house state") - assert.NotEmpty(house.LinearID, "the house must have a linear ID") - assert.True(house.Valuation > 0, "the house must have a valuation") - assert.NotEmpty(house.Address, "the house must have an address") - - // 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(nftcc.NewAcceptView(tx)) - assert.NoError(err, "failed to accept new tokens") - - // Before completing, the recipient waits for finality of the transaction - _, err = context.RunView(nftcc.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 a `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. - -```go -type TransferHouseView struct { - *Transfer -} - -func (d *TransferHouseView) Call(context view.Context) (interface{}, error) { - // Prepare a new token transaction. - tx, err := nftcc.NewAnonymousTransaction( - context, - nftcc.WithAuditor( - view2.GetIdentityProvider(context).Identity("auditor"), // Retrieve the auditor's FSC node identity - ), - ) - assert.NoError(err, "failed to create a new token transaction") - - buyer, err := nftcc.RequestRecipientIdentity(context, view2.GetIdentityProvider(context).Identity(d.Recipient)) - assert.NoError(err, "failed getting buyer identity") - - wallet := nftcc.MyWallet(context) - assert.NotNil(wallet, "failed getting default wallet") - - // Transfer ownership of the house to the buyer - house := &House{} - assert.NoError(wallet.QueryByKey(house, "LinearID", d.HouseID), "failed loading house with id %s", d.HouseID) - - assert.NoError(tx.Transfer(wallet, house, buyer), "failed transferring house") - - // Collect signature from the parties - _, err = context.RunView(nftcc.NewCollectEndorsementsView(tx)) - assert.NoError(err, "failed to collect endorsements") - - // Send to the ordering service and wait for confirmation - _, err = context.RunView(nftcc.NewOrderingAndFinalityView(tx)) - assert.NoError(err, "failed to order and finalize") - - 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. - -## Queries - -Here are two examples of view to list tokens. - -The following view returns the list of unspent tokens: - -```go -// GetHouse contains the input to query a house by id -type GetHouse struct { - HouseID string -} - -type GetHouseView struct { - *GetHouse -} - -func (p *GetHouseView) Call(context view.Context) (interface{}, error) { - house := &House{} - if err := nftcc.MyWallet(context).QueryByKey(house, "LinearID", p.HouseID); err != nil { - if err == nftcc.ErrNoResults { - return fmt.Sprintf("no house found with id [%s]", p.HouseID), nil - } - return nil, err - } - return house, nil -} -``` - -## 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 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; -2. 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 and Bob have an Org2 Fabric Identity. - -We can describe the network topology programmatically as follows: - -```go -func Fabric(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 := fscTopology.AddNodeByName("issuer").AddOptions( - fabric.WithOrganization("Org1"), - fabric.WithAnonymousIdentity(), - token.WithDefaultIssuerIdentity(), - ) - issuer.RegisterViewFactory("issue", &views.IssueHouseViewFactory{}) - - auditor := fscTopology.AddNodeByName("auditor").AddOptions( - fabric.WithOrganization("Org1"), - fabric.WithAnonymousIdentity(), - token.WithAuditorIdentity(), - ) - auditor.RegisterViewFactory("registerAuditor", &views.RegisterAuditorViewFactory{}) - - alice := fscTopology.AddNodeByName("alice").AddOptions( - fabric.WithOrganization("Org2"), - fabric.WithAnonymousIdentity(), - token.WithDefaultOwnerIdentity(tokenSDKDriver), - ) - alice.RegisterResponder(&views.AcceptIssuedHouseView{}, &views.IssueHouseView{}) - alice.RegisterResponder(&views.AcceptTransferHouseView{}, &views.TransferHouseView{}) - alice.RegisterViewFactory("transfer", &views.TransferHouseViewFactory{}) - alice.RegisterViewFactory("queryHouse", &views.GetHouseViewFactory{}) - - bob := fscTopology.AddNodeByName("bob").AddOptions( - fabric.WithOrganization("Org2"), - fabric.WithAnonymousIdentity(), - token.WithDefaultOwnerIdentity(tokenSDKDriver), - ) - bob.RegisterResponder(&views.AcceptIssuedHouseView{}, &views.IssueHouseView{}) - bob.RegisterResponder(&views.AcceptTransferHouseView{}, &views.TransferHouseView{}) - bob.RegisterViewFactory("transfer", &views.TransferHouseViewFactory{}) - bob.RegisterViewFactory("queryHouse", &views.GetHouseViewFactory{}) - - tokenTopology := token.NewTopology() - tokenTopology.SetSDK(fscTopology, &sdk.SDK{}) - tms := tokenTopology.AddTMS(fabricTopology, fabricTopology.Channels[0].Name, tokenSDKDriver) - tms.SetTokenGenPublicParams("16") - fabric2.SetOrgs(tms, "Org1") - tms.AddAuditor(auditor) - - return []api.Topology{fabricTopology, tokenTopology, fscTopology} -} -``` - -The above topology takes in input the token driver name. - -### Boostrap the networks - -Bootstrap of the networks requires both Fabric Docker images and Fabric binaries. To ensure you have the required images you can use the following Makefile target in the project root directory: - -```shell -make fabric-docker-images -``` - -To ensure you have the required fabric binary files and set the `FAB_BINS` environment variable to the correct place you can do the following in the project root directory - -```shell -make download-fabric -export FAB_BINS=$PWD/../fabric/bin -``` - -To help us bootstrap the networks and then invoke the business views, the `nft` 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/nft`. - -```shell -go build -o nft -``` - -If the compilation is successful, we can run the `nft` command line tool as follows: - -``` -./nft 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: - -```shell -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: - -```shell -./nft 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 nft token, we can run the following command: - -```shell -./nft view -c ./testdata/fsc/nodes/issuer/client-config.yaml -f issue -i "{\"Address\":\"5th Avenue\", \"Valuation\":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 nft token for a house whose address is `5th Avenue` and its valuation is 10. -Owner of the token is `alice`. -If everything is successful, you will see something like the following: - -```shell -"74f183f2-cbe1-4724-a6ea-4bbbc69bdc18" -``` -The above is the NFT unique identifier. - -Indeed, once the token is issued, the recipient can query its wallet to see the token. - -```shell -./nft view -c ./testdata/fsc/nodes/alice/client-config.yaml -f queryHouse -i "{\"HouseID\":\"74f183f2-cbe1-4724-a6ea-4bbbc69bdc18\"}" -``` - -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): -```shell -{ - "LinearID": "74f183f2-cbe1-4724-a6ea-4bbbc69bdc18", - "Address": "5th Avenue", - "Valuation": 10 -} -``` - -Alice can now transfer some of her tokens to other parties. For example: - -```shell -./nft view -c ./testdata/fsc/nodes/alice/client-config.yaml -f transfer -i "{\"HouseID\":\"74f183f2-cbe1-4724-a6ea-4bbbc69bdc18\", \"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: - -```shell -"d1db2f7a7bd73e8dc4bb4b7c6785595157a5dbb60f00b9eeaed53f2c9e270c0f" -``` - -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: - -```shell -./nft view -c ./testdata/fsc/nodes/alice/client-config.yaml -f queryHouse -i "{\"HouseID\":\"74f183f2-cbe1-4724-a6ea-4bbbc69bdc18\"}" -``` - -You can expect to see an output like this (beautified): - -```shell -no house found with id [74f183f2-cbe1-4724-a6ea-4bbbc69bdc18] -``` - -Then, Bob: - -```shell -./nft view -c ./testdata/fsc/nodes/bob/client-config.yaml -f queryHouse -i "{\"HouseID\":\"74f183f2-cbe1-4724-a6ea-4bbbc69bdc18\"}" -``` - -You can expect to see an output like this (beautified): - -```shell -{ - "LinearID": "74f183f2-cbe1-4724-a6ea-4bbbc69bdc18", - "Address": "5th Avenue", - "Valuation": 10 -} -``` \ No newline at end of file diff --git a/samples/nft/nft.go b/samples/nft/nft.go deleted file mode 100644 index 8f13bc49c..000000000 --- a/samples/nft/nft.go +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright IBM Corp All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package main - -import ( - "github.com/hyperledger-labs/fabric-smart-client/integration" - "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/api" - "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/cmd" - "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/cmd/network" - view "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/client/view/cmd" - "github.com/hyperledger-labs/fabric-token-sdk/integration/nwo/token" - "github.com/hyperledger-labs/fabric-token-sdk/samples/nft/topology" - "github.com/pkg/errors" -) - -func main() { - m := cmd.NewMain("NFT", "0.1") - mainCmd := m.Cmd() - - network.StartCMDPostNew = func(infrastructure *integration.Infrastructure) error { - infrastructure.RegisterPlatformFactory(token.NewPlatformFactory()) - return nil - } - network.StartCMDPostStart = func(infrastructure *integration.Infrastructure) error { - _, err := infrastructure.Client("auditor").CallView("registerAuditor", nil) - if err != nil { - return errors.WithMessage(err, "failed to register auditor") - } - return nil - } - mainCmd.AddCommand(network.NewCmdWithMultipleTopologies( - map[string][]api.Topology{ - "default": topology.Fabric("dlog"), - "orion": topology.Orion("dlog"), - }, - )) - mainCmd.AddCommand(view.NewCmd()) - m.Execute() -} diff --git a/samples/nft/topology/fabric.go b/samples/nft/topology/fabric.go deleted file mode 100644 index 1e6ad40a2..000000000 --- a/samples/nft/topology/fabric.go +++ /dev/null @@ -1,76 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package topology - -import ( - "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/api" - "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/fabric" - "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/fsc" - fabric3 "github.com/hyperledger-labs/fabric-smart-client/platform/fabric/sdk" - "github.com/hyperledger-labs/fabric-token-sdk/integration/nwo/token" - fabric2 "github.com/hyperledger-labs/fabric-token-sdk/integration/nwo/token/fabric" - "github.com/hyperledger-labs/fabric-token-sdk/samples/nft/views" - sdk "github.com/hyperledger-labs/fabric-token-sdk/token/sdk" -) - -func Fabric(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 := fscTopology.AddNodeByName("issuer").AddOptions( - fabric.WithOrganization("Org1"), - fabric.WithAnonymousIdentity(), - token.WithDefaultIssuerIdentity(), - ) - issuer.RegisterViewFactory("issue", &views.IssueHouseViewFactory{}) - - auditor := fscTopology.AddNodeByName("auditor").AddOptions( - fabric.WithOrganization("Org1"), - fabric.WithAnonymousIdentity(), - token.WithAuditorIdentity(), - ) - auditor.RegisterViewFactory("registerAuditor", &views.RegisterAuditorViewFactory{}) - - alice := fscTopology.AddNodeByName("alice").AddOptions( - fabric.WithOrganization("Org2"), - fabric.WithAnonymousIdentity(), - token.WithDefaultOwnerIdentity(), - ) - alice.RegisterResponder(&views.AcceptIssuedHouseView{}, &views.IssueHouseView{}) - alice.RegisterResponder(&views.AcceptTransferHouseView{}, &views.TransferHouseView{}) - alice.RegisterViewFactory("transfer", &views.TransferHouseViewFactory{}) - alice.RegisterViewFactory("queryHouse", &views.GetHouseViewFactory{}) - - bob := fscTopology.AddNodeByName("bob").AddOptions( - fabric.WithOrganization("Org2"), - fabric.WithAnonymousIdentity(), - token.WithDefaultOwnerIdentity(), - ) - bob.RegisterResponder(&views.AcceptIssuedHouseView{}, &views.IssueHouseView{}) - bob.RegisterResponder(&views.AcceptTransferHouseView{}, &views.TransferHouseView{}) - bob.RegisterViewFactory("transfer", &views.TransferHouseViewFactory{}) - bob.RegisterViewFactory("queryHouse", &views.GetHouseViewFactory{}) - - tokenTopology := token.NewTopology() - tokenTopology.SetSDK(fscTopology, &sdk.SDK{}) - tms := tokenTopology.AddTMS(fscTopology.ListNodes(), fabricTopology, fabricTopology.Channels[0].Name, tokenSDKDriver) - tms.SetTokenGenPublicParams("16") - fabric2.SetOrgs(tms, "Org1") - tms.AddAuditor(auditor) - - // Add Fabric SDK to FSC Nodes - fscTopology.AddSDK(&fabric3.SDK{}) - - return []api.Topology{fabricTopology, tokenTopology, fscTopology} -} diff --git a/samples/nft/topology/orion.go b/samples/nft/topology/orion.go deleted file mode 100644 index d3e5f3bf6..000000000 --- a/samples/nft/topology/orion.go +++ /dev/null @@ -1,72 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package topology - -import ( - "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/api" - "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/fsc" - "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/orion" - "github.com/hyperledger-labs/fabric-token-sdk/integration/nwo/token" - orion2 "github.com/hyperledger-labs/fabric-token-sdk/integration/nwo/token/orion" - "github.com/hyperledger-labs/fabric-token-sdk/samples/nft/views" - sdk "github.com/hyperledger-labs/fabric-token-sdk/token/sdk" -) - -func Orion(tokenSDKDriver string) []api.Topology { - // Orion - orionTopology := orion.NewTopology() - - // FSC - fscTopology := fsc.NewTopology() - //fscTopology.SetLogging("grpc=error:debug", "") - - issuer := fscTopology.AddNodeByName("issuer").AddOptions( - orion.WithRole("issuer"), - token.WithDefaultIssuerIdentity(), - ) - issuer.RegisterViewFactory("issue", &views.IssueHouseViewFactory{}) - - auditor := fscTopology.AddNodeByName("auditor").AddOptions( - orion.WithRole("auditor"), - token.WithAuditorIdentity(), - ) - auditor.RegisterViewFactory("registerAuditor", &views.RegisterAuditorViewFactory{}) - - alice := fscTopology.AddNodeByName("alice").AddOptions( - orion.WithRole("alice"), - token.WithDefaultOwnerIdentity(), - ) - alice.RegisterResponder(&views.AcceptIssuedHouseView{}, &views.IssueHouseView{}) - alice.RegisterResponder(&views.AcceptTransferHouseView{}, &views.TransferHouseView{}) - alice.RegisterViewFactory("transfer", &views.TransferHouseViewFactory{}) - alice.RegisterViewFactory("queryHouse", &views.GetHouseViewFactory{}) - - bob := fscTopology.AddNodeByName("bob").AddOptions( - orion.WithRole("bob"), - token.WithDefaultOwnerIdentity(), - ) - bob.RegisterResponder(&views.AcceptIssuedHouseView{}, &views.IssueHouseView{}) - bob.RegisterResponder(&views.AcceptTransferHouseView{}, &views.TransferHouseView{}) - bob.RegisterViewFactory("transfer", &views.TransferHouseViewFactory{}) - bob.RegisterViewFactory("queryHouse", &views.GetHouseViewFactory{}) - - // we need to define the custodian - custodian := fscTopology.AddNodeByName("custodian") - custodian.AddOptions(orion.WithRole("custodian")) - fscTopology.SetBootstrapNode(custodian) - - tokenTopology := token.NewTopology() - tokenTopology.SetSDK(fscTopology, &sdk.SDK{}) - tms := tokenTopology.AddTMS(fscTopology.ListNodes(), orionTopology, "", tokenSDKDriver) - tms.SetTokenGenPublicParams("16") - orion2.SetCustodian(tms, custodian) - orionTopology.AddDB(tms.Namespace, "custodian", "issuer", "auditor", "alice", "bob", "charlie", "manager") - orionTopology.SetDefaultSDK(fscTopology) - tms.AddAuditor(auditor) - - return []api.Topology{orionTopology, tokenTopology, fscTopology} -} diff --git a/samples/nft/views/accept.go b/samples/nft/views/accept.go deleted file mode 100644 index 2d3ea798b..000000000 --- a/samples/nft/views/accept.go +++ /dev/null @@ -1,88 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package views - -import ( - "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/assert" - "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/flogging" - "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - "github.com/hyperledger-labs/fabric-token-sdk/token/services/nfttx" -) - -var logger = flogging.MustGetLogger("token-sdk.sample.nft") - -type AcceptIssuedHouseView struct{} - -func (a *AcceptIssuedHouseView) 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 (ttx.MyWallet(context)), if not otherwise specified. - id, err := nfttx.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 := nfttx.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 one output that names the recipient. - // (The recipient is receiving something) - outputs, err := tx.Outputs() - assert.NoError(err, "failed getting outputs") - assert.NoError(outputs.Validate(), "failed validating outputs") - assert.True(outputs.Count() == 1, "the transaction must contain one output") - assert.True(outputs.ByRecipient(id).Count() == 1, "the transaction must contain one output that names the recipient") - house := &House{} - assert.NoError(outputs.StateAt(0, house), "failed to get house state") - assert.NotEmpty(house.LinearID, "the house must have a linear ID") - assert.True(house.Valuation > 0, "the house must have a valuation") - assert.NotEmpty(house.Address, "the house must have an address") - - // 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(nfttx.NewAcceptView(tx)) - assert.NoError(err, "failed to accept new tokens") - - // Before completing, the recipient waits for finality of the transaction - _, err = context.RunView(nfttx.NewFinalityView(tx)) - assert.NoError(err, "new tokens were not committed") - - return nil, nil -} - -type AcceptTransferHouseView struct{} - -func (a AcceptTransferHouseView) Call(context view.Context) (interface{}, error) { - logger.Infof("AcceptTransferHouseView, context: %s", context.ID()) - - // 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 (ttx.MyWallet(context)), if not otherwise specified. - _, err := nfttx.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 := nfttx.ReceiveTransaction(context) - assert.NoError(err, "failed to receive tokens") - - // 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(nfttx.NewAcceptView(tx)) - assert.NoError(err, "failed to accept new tokens") - - // Before completing, the recipient waits for finality of the transaction - _, err = context.RunView(nfttx.NewFinalityView(tx)) - assert.NoError(err, "new tokens were not committed") - - logger.Infof("AcceptTransferHouseView, context: %s, done", context.ID()) - - return nil, nil -} diff --git a/samples/nft/views/auditor.go b/samples/nft/views/auditor.go deleted file mode 100644 index f4e3db070..000000000 --- a/samples/nft/views/auditor.go +++ /dev/null @@ -1,45 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package views - -import ( - "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/assert" - "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - "github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx" -) - -type AuditView struct{} - -func (a *AuditView) Call(context view.Context) (interface{}, error) { - tx, err := ttx.ReceiveTransaction(context) - assert.NoError(err, "failed receiving transaction") - - w := ttx.MyAuditorWallet(context) - assert.NotNil(w, "failed getting default auditor wallet") - - // Validate - auditor, err := ttx.NewAuditor(context, w) - assert.NoError(err, "failed to get auditor instance") - assert.NoError(auditor.Validate(tx), "failed auditing verification") - - return context.RunView(ttx.NewAuditApproveView(w, tx)) -} - -type RegisterAuditorView struct{} - -func (r *RegisterAuditorView) Call(context view.Context) (interface{}, error) { - return context.RunView(ttx.NewRegisterAuditorView( - &AuditView{}, - )) -} - -type RegisterAuditorViewFactory struct{} - -func (p *RegisterAuditorViewFactory) NewView(in []byte) (view.View, error) { - f := &RegisterAuditorView{} - return f, nil -} diff --git a/samples/nft/views/house.go b/samples/nft/views/house.go deleted file mode 100644 index dfe74ad32..000000000 --- a/samples/nft/views/house.go +++ /dev/null @@ -1,22 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package views - -// House is a struct that contains a house -type House struct { - LinearID string - Address string - Valuation uint64 -} - -// SetLinearID sets the linear id of the house -func (h *House) SetLinearID(id string) string { - if len(h.LinearID) == 0 { - h.LinearID = id - } - return h.LinearID -} diff --git a/samples/nft/views/issue.go b/samples/nft/views/issue.go deleted file mode 100644 index f162cd7bc..000000000 --- a/samples/nft/views/issue.go +++ /dev/null @@ -1,93 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package views - -import ( - "encoding/json" - - view2 "github.com/hyperledger-labs/fabric-smart-client/platform/view" - "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/assert" - "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - "github.com/hyperledger-labs/fabric-token-sdk/token/services/nfttx" - "github.com/hyperledger-labs/fabric-token-sdk/token/services/nfttx/uniqueness" -) - -// IssueHouse contains the input information to issue a token -type IssueHouse struct { - // IssuerWallet is the issuer's wallet to use - IssuerWallet string - // Recipient is an identifier of the recipient identity - Recipient string - // Address is the address of the house to issue - Address string - // Valuation is the valuation of the house to issue - Valuation uint64 -} - -type IssueHouseView struct { - *IssueHouse -} - -func (p *IssueHouseView) 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 := nfttx.RequestRecipientIdentity(context, view2.GetIdentityProvider(context).Identity(p.Recipient)) - assert.NoError(err, "failed getting recipient identity") - - // 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 := nfttx.NewAnonymousTransaction( - context, - nfttx.WithAuditor( - view2.GetIdentityProvider(context).Identity("auditor"), // Retrieve the auditor's FSC node identity - ), - ) - assert.NoError(err, "failed creating issue transaction") - - // The issuer adds a new issue operation to the transaction following the instruction received - wallet := nfttx.GetIssuerWallet(context, p.IssuerWallet) - assert.NotNil(wallet, "issuer wallet [%s] not found", p.IssuerWallet) - h := &House{ - Address: p.Address, - Valuation: p.Valuation, - } - // The issuer enforce uniqueness of the token by computing a unique identifier for the passed house. - uniqueID, err := uniqueness.GetService(context).ComputeID(h.Address) - assert.NoError(err, "failed computing unique ID") - - err = tx.Issue(wallet, h, recipient, nfttx.WithUniqueID(uniqueID)) - 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(nfttx.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(nfttx.NewOrderingAndFinalityView(tx)) - assert.NoError(err, "failed to commit issue transaction") - - return h.LinearID, nil -} - -type IssueHouseViewFactory struct{} - -func (p *IssueHouseViewFactory) NewView(in []byte) (view.View, error) { - f := &IssueHouseView{IssueHouse: &IssueHouse{}} - err := json.Unmarshal(in, f.IssueHouse) - assert.NoError(err, "failed unmarshalling input") - - return f, nil -} diff --git a/samples/nft/views/list.go b/samples/nft/views/list.go deleted file mode 100644 index c6ac9c527..000000000 --- a/samples/nft/views/list.go +++ /dev/null @@ -1,45 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package views - -import ( - "encoding/json" - "fmt" - - "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/assert" - "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - "github.com/hyperledger-labs/fabric-token-sdk/token/services/nfttx" -) - -// GetHouse contains the input to query a house by id -type GetHouse struct { - HouseID string -} - -type GetHouseView struct { - *GetHouse -} - -func (p *GetHouseView) Call(context view.Context) (interface{}, error) { - house := &House{} - if err := nfttx.MyWallet(context).QueryByKey(house, "LinearID", p.HouseID); err != nil { - if err == nfttx.ErrNoResults { - return fmt.Sprintf("no house found with id [%s]", p.HouseID), nil - } - return nil, err - } - return house, nil -} - -type GetHouseViewFactory struct{} - -func (i *GetHouseViewFactory) NewView(in []byte) (view.View, error) { - f := &GetHouseView{GetHouse: &GetHouse{}} - err := json.Unmarshal(in, f.GetHouse) - assert.NoError(err, "failed unmarshalling input") - return f, nil -} diff --git a/samples/nft/views/transfer.go b/samples/nft/views/transfer.go deleted file mode 100644 index 931bab082..000000000 --- a/samples/nft/views/transfer.go +++ /dev/null @@ -1,72 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package views - -import ( - "encoding/json" - - view2 "github.com/hyperledger-labs/fabric-smart-client/platform/view" - "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/assert" - "github.com/hyperledger-labs/fabric-smart-client/platform/view/view" - "github.com/hyperledger-labs/fabric-token-sdk/token/services/nfttx" -) - -// Transfer contains the transfer instructions -type Transfer struct { - // Wallet is the wallet from which recipient identities must be derived - Wallet string - // HouseID is the house ID of the house to sell - HouseID string - // Recipient is the identity of the buyer (it is identifier as defined in the topology) - Recipient string -} - -type TransferHouseView struct { - *Transfer -} - -func (d *TransferHouseView) Call(context view.Context) (interface{}, error) { - // Prepare a new token transaction. - tx, err := nfttx.NewAnonymousTransaction( - context, - nfttx.WithAuditor( - view2.GetIdentityProvider(context).Identity("auditor"), // Retrieve the auditor's FSC node identity - ), - ) - assert.NoError(err, "failed to create a new token transaction") - - buyer, err := nfttx.RequestRecipientIdentity(context, view2.GetIdentityProvider(context).Identity(d.Recipient)) - assert.NoError(err, "failed getting buyer identity") - - wallet := nfttx.MyWallet(context) - assert.NotNil(wallet, "failed getting default wallet") - - // Transfer ownership of the house to the buyer - house := &House{} - assert.NoError(wallet.QueryByKey(house, "LinearID", d.HouseID), "failed loading house with id %s", d.HouseID) - - assert.NoError(tx.Transfer(wallet, house, buyer), "failed transferring house") - - // Collect signature from the parties - _, err = context.RunView(nfttx.NewCollectEndorsementsView(tx)) - assert.NoError(err, "failed to collect endorsements") - - // Send to the ordering service and wait for confirmation - _, err = context.RunView(nfttx.NewOrderingAndFinalityView(tx)) - assert.NoError(err, "failed to order and finalize") - - return tx.ID(), nil -} - -type TransferHouseViewFactory struct{} - -func (s TransferHouseViewFactory) NewView(in []byte) (view.View, error) { - f := &TransferHouseView{Transfer: &Transfer{}} - err := json.Unmarshal(in, f) - assert.NoError(err, "failed unmarshalling input") - return f, nil -} diff --git a/token/services/selector/README.md b/token/services/selector/README.md index 478603410..ee3d9381d 100644 --- a/token/services/selector/README.md +++ b/token/services/selector/README.md @@ -2,7 +2,7 @@ This package contains the token selector implementations. The token selector is responsible to select tokens of a given type and for a given amount from a wallet. -See more details in the [Token API documentation](../../../docs/token-api.md#token-selector-manager). +See more details in the [Token API documentation](../../../docs/apis/token-api.md#token-selector-manager). ## Available Implementations