add token sdk sample application

Signed-off-by: Arne Rutjes <arne123@gmail.com>
This commit is contained in:
Arne Rutjes 2023-10-09 21:56:28 +02:00 committed by Dave Enyeart
parent 62f304a98f
commit 99a1f49da0
62 changed files with 14174 additions and 0 deletions

View file

@ -49,6 +49,7 @@ Additional samples demonstrate various Fabric use cases and application patterns
| **Sample** | **Description** | **Documentation** |
| -------------|------------------------------|------------------|
| [Off chain data](off_chain_data) | Learn how to use block events to build an off-chain database for reporting and analytics. | [Peer channel-based event services](https://hyperledger-fabric.readthedocs.io/en/latest/peer_event_services.html) |
| [Token SDK](token-sdk) | Sample REST API around the Hyperledger Labs [Token SDK](https://github.com/hyperledger-labs/fabric-token-sdk) for privacy friendly (zero knowledge proof) UTXO transactions. | [README](token-sdk/README.md) |
| [Token ERC-20](token-erc-20) | Smart contract demonstrating how to create and transfer fungible tokens using an account-based model. | [README](token-erc-20/README.md) |
| [Token UTXO](token-utxo) | Smart contract demonstrating how to create and transfer fungible tokens using a UTXO (unspent transaction output) model. | [README](token-utxo/README.md) |
| [Token ERC-1155](token-erc-1155) | Smart contract demonstrating how to create and transfer multiple tokens (both fungible and non-fungible) using an account based model. | [README](token-erc-1155/README.md) |

1
token-sdk/.dockerignore Normal file
View file

@ -0,0 +1 @@
oapi-server.yaml

8
token-sdk/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
bin/
keys/
data/
tokenchaincode/zkatdlog_pp.json
auditor/auditor
issuer/issuer
owner/owner

19
token-sdk/Dockerfile Normal file
View file

@ -0,0 +1,19 @@
#build stage
FROM golang:1.20.7-bookworm AS builder
WORKDIR /go/src/app
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . .
RUN go build -o /go/bin/app
#final stage
FROM golang:1.20.7-bookworm
COPY --from=builder /go/bin/app /app
ENTRYPOINT /app
LABEL Name=tokens Version=0.1.0
ENV PORT=9000
ENV CONF_DIR=/conf
EXPOSE 9000
EXPOSE 9001

413
token-sdk/README.md Normal file
View file

@ -0,0 +1,413 @@
# Token SDK Sample API
This is a service with a REST API that wraps the [Token SDK](https://github.com/hyperledger-labs/fabric-token-sdk) to issue, transfer and redeem tokens backed by a Hyperledger Fabric network for validation and settlement.
Several instances of this service form a Layer 2 network that can transact amongst each other, with an (optional but currently configured to be required) auditor role who has to approve every transaction. The ledger data does not reveal balances, transaction amounts and identities of transaction parties. UTXO Tokens are owned by pseudonymous keys and other details are obscured with Zero Knowledge Proofs.
This sample is intended to get familiar with the features of the Token SDK and as a starting point for a proof of concept. The sample contains a basic development setup with:
- An issuer service
- An auditor service
- Two owner services, with wallets for Alice and Bob (on Owner 1), and Carlos and Dan (on Owner 2)
- A Certificate Authority
- Configuration to use a Fabric test network.
From now on we'll call the services for the issuer, auditor and owners 'nodes' (not to be confused with Hyperledger Fabric peer nodes). Each of them runs as a separate application containing a REST API, the Fabric Smart Client and the Token SDK. The nodes talk to each other via a protocol called libp2p to create token transactions, and each of them also has a Hyperledger Fabric user to be able to submit the transaction to the settlement layer. The settlement layer is just any Fabric network that runs the Token Chaincode, which is configured with the identities of the issuer, auditor and CA to be able to validate transactions.
![components](./components.png)
[plantuml source](https://www.plantuml.com/plantuml/uml/ZPB1IiD048RlUOgXteHM4nIXbD0O4RnO3mKllKmtssR9PYRiRYWYlhlPNPMsIkrjcFs_d-LZvjQXSNsh4ziewj1W2wsYKgErhwfoDQGtrqdIeMXmAs6qv4OIF4ktOzEC02quRk0z0H3STaoI78nMT123iWX9WJ2RmGRNHecnm6awkPtSGPuVmqLVASScCDXN7ffSOLmESRWekauhWKun7RDFrlOoeihQY2g_-vTSx6W8fIkwX6B8I3_SypfKSHgRU4Vd5cMUBz5ejdvwG8fDsQccZptJZy4JBALr1xvhlVdj-qNwluVtRXXJs3Fj5rEDpXVb-PzazaDcPuCBKqdpfPhZlCV6pGayNaXPeoB1bOod94Iqu_oZ-7uBcfPIrCIQjs_UaZ-wyJZtCfAvfAflzIS0)
# Table of Contents
- [Token SDK Sample API](#token-sdk-sample-api)
- [Table of Contents](#table-of-contents)
- [Features](#features)
- [Getting started](#getting-started)
- [Install dependencies](#install-dependencies)
- [Quick start](#quick-start)
- [Using the application](#using-the-application)
- [Deep dive: what happens when doing a transfer?](#deep-dive-what-happens-when-doing-a-transfer)
- [Alternative: manual start](#alternative-manual-start)
- [Generate crypto material](#generate-crypto-material)
- [Start Fabric and install the chaincode](#start-fabric-and-install-the-chaincode)
- [Start the Token network](#start-the-token-network)
- [View the blockchain explorer](#view-the-blockchain-explorer)
- [Development](#development)
- [End to end tests](#end-to-end-tests)
- [Code structure](#code-structure)
- [Add or change a REST API endpoint](#add-or-change-a-rest-api-endpoint)
- [Upgrade the Token SDK and Fabric Smart Client versions](#upgrade-the-token-sdk-and-fabric-smart-client-versions)
- [Use another Fabric network](#use-another-fabric-network)
- [Add a user / account](#add-a-user--account)
- [Run the service directly (instead of with docker-compose)](#run-the-service-directly-instead-of-with-docker-compose)
## Features
Main flows:
- [X] issue token
- [X] transfer
- [X] redeem / burn
- [X] owner get balances
- [X] owner transaction history
- [ ] auditor get balances
- [X] auditor transaction history
- [ ] issuer transaction history
- [ ] swap
Additional features:
- [X] Documented REST API
- [X] Basic end to end tests
- [X] Support for multiple token types
- [X] Multiple accounts per node
- [X] Use Idemix (privacy preserving) accounts created by a Fabric CA
- [X] Pre-configured and easy to start for development
Out of scope for now:
- HTLC locks (hashed timelock contracts)
- Register/enroll new token accounts on a running network
- Business flows for redemption or issuance
- Advanced transaction history (queries, rolling balance, pagination, etc)
- Denylist / revocation and other business logic for auditor
- Idemix users to submit the transactions to Fabric anonymously
- Production configuration (e.g. deployment, networking, security, resilience, key management)
## Getting started
Prerequisites:
- bash
- golang 1.20+
- git
- docker
- docker-compose
### Install dependencies
Download the Fabric docker images and binaries. The code only works with Fabric CA 1.5.7+, so even if you cloned the fabric-samples repo before, you may have to re-run it to get the latest versions.
From the fabric-samples directory:
```bash
curl -sSLO https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh && chmod +x install-fabric.sh
./install-fabric.sh docker binary
```
Make sure that the new binaries are in your path. Change the following line (replace `<your/path/to/>` with the actual path) and add it to your `~/.bashrc` or `~/.zshrc` file. Restart your terminal or `source` the edited file.
```bash
export PATH=</your/path/to/>fabric-samples/bin:$PATH
```
Validate that the CA is at 1.5.7 by executing `fabric-ca-client version`.
> Note: you can run this code from anywhere. If you are *not* running it from the fabric-samples/token-sdk folder, also set the following environment variable:
> ```bash
> export TEST_NETWORK_HOME=</your/path/to>/fabric-samples/test-network
> ```
>
> See the bottom of this readme for instructions to use another Fabric network than the test network.
Install tokengen. Tokengen is a tool to create the configuration file for the token chaincode (once, when deploying the chaincode). It generates the public parameters that the network participants will use to generate their proofs, and it specifies the public identities of the issuer, auditor and CA for signature validation.
```bash
go install github.com/hyperledger-labs/fabric-token-sdk/cmd/tokengen@v0.3.0
```
### Quick start
The quickest way to get going is to run:
```bash
./scripts/up.sh
```
This generates the crypto material, starts Fabric, deploys the chaincode, and starts the token nodes.
When you're done and want to delete everything:
```bash
./scripts/down.sh
```
#### Using the application
The services are accessible on the following ports:
| port | service |
|------|--------------------------|
| 8080 | API documentation (web) |
| 9000 | auditor |
| 9100 | issuer |
| 9200 | owner 1 (alice and bob) |
| 9300 | owner 2 (carlos and dan) |
Besides that, the nodes communicate with each other via 9001, 9101, 9201 and 9301 respectively.
Now let's issue and transfer some tokens! View the API documentation and try some actions at [http://localhost:8080](http://localhost:8080). Or, directly from the commandline:
```bash
curl -X POST http://localhost:9100/api/v1/issuer/issue -H 'Content-Type: application/json' -d '{
"amount": {"code": "TOK","value": 1000},
"counterparty": {"node": "owner1","account": "alice"},
"message": "hello world!"
}'
curl -X GET http://localhost:9200/api/v1/owner/accounts
curl -X GET http://localhost:9300/api/v1/owner/accounts
curl -X POST http://localhost:9200/api/v1/owner/accounts/alice/transfer -H 'Content-Type: application/json' -d '{
"amount": {"code": "TOK","value": 100},
"counterparty": {"node": "owner2","account": "dan"},
"message": "hello dan!"
}'
curl -X GET http://localhost:9300/api/v1/owner/accounts/dan/transactions
curl -X GET http://localhost:9200/api/v1/owner/accounts/alice/transactions
```
Notice that the transaction overview uses the UTXO model (like bitcoin). The issuer created a new TOK token of 1000 and assigned its ownership to alice. When alice transfered 100 TOK to dan, she used the token of 1000 as **input** for her transaction. As **output**, she creates two new tokens:
1. one for 100 TOK with dan as the owner
2. one with _herself_ as the owner for the remaining 900 TOK.
This way, each transaction can have multiple inputs and multiple outputs. Their sum should always be the same, and every new transfer must be based on previously created outputs.
#### Deep dive: what happens when doing a transfer?
It may look simple from the outside, but there's a lot going on to securely and privately transfer tokens. Let's take the example of alice (on the Owner 1 node) transfering 100 TOK to dan (on the Owner 2 node).
1. **Create Transaction**: Alice requests an anonymous key from dan that will own the tokens. She then creates the transaction, with commitments that can be verified by anyone, but _only_ be opened (read) by dan and the auditor. The commitments contain the value, sender and recipient of each of the in- and output tokens.
2. **Get Endorsements**: Alice (or more precisely the TransferView in the Owner 1 node) now submits the transaction to the auditor, who validates and stores it. The auditor _may_ enforce any specific business logic that is needed for this token in this ecosystem (for instance a transaction or holding limit).
Alice then submits the transaction (which is now also signed by the auditor) to the Token Chaincode which is running on the Fabric peers. The chaincode verifies that all the proofs are valid and all the necessary signatures are there. Note that the peer and token chaincode cannot see what is transferred between who thanks to the zero knowledge proofs.
3. **Commit Transaction**: Alice submits the endorsed Fabric transaction to the ordering service. Alice (Owner 1), dan (Owner 2) and the Auditor nodes have been listening for Fabric events involving this transaction. When receiving the 'commit' event, they change the status of the stored transaction to 'Confirmed'. The transaction is now final; dan owns the 100 TOK.
The names of the Views below correspond to the code in `owner/service/transfer.go`, `owner/service/accept.go` and `auditor/service/audit.go`.
![transfer](transfer.png)
[plantuml source](http://www.plantuml.com/plantuml/uml/TLD1JoCz3BtdLrZb0X98yEaxhTGLRA5xu50EQ0-hNZA92r6dpcpY3Eg_tsHcYDmoUvb9hFUUxHVxFh8Ed0wjOiSjmclG57SOWCj16tQUbCf_7s3nq3g32z0HjEeopHdNQM9OR3ueK-wsjALFWLyEFmOezt2n-l6qNZ_ESVuhd0TZiEELZk-LrRXvraEoZdqOMELO2JhPob18xFW8YxLkWZFmWXZYWEhA2IuU_ry_hfxEOPjWCNmYVRb8i5ekOHLGCqflOBbK6cw-bpQ_0N-wTtTx2w-Rvosn1wi9Cd3gLnLUhnc5CJKcs-Q-o3OkomRyap1o_cSR71A3isFjIiefgQCQL_bcB5kJf-F1fxYbIqzum-w0DodY5UpnAF5lI1WA8sAcCkoAuR-VNy3umy7n0OcZ2iWf47IfQPqf2jSJN5ayAOhxwa_45Ws3eouniDyZHTb0XTQQHv0qFDS-qA_19nx-NV1-5tDszqQQKy0hwLryrm6bmBzCyXsIRF1wIpq6jpkEoldBFk2MNf2iepSfENanuD12mDXvYWZdJkG9-eaCMS27Y4EMF3zJjJfPyTJvvbXwKyydarxEbTlhrjcC6AFLyzEYncpZ8jHyiYQHzNHRnflWEkhzVdeYywuT6M-nZ7ojPCwaAPE5ox6oAnZNJs9dZ5iDBoD1rRgwgwNRr6JOdEISbr-tl0PEPST9a7fvFAOHRLflze8e76g2rzReo43uCG4jVicUhPsOvqimD3r4SaZdoERvr9kpcr2IqrsL1Bfn4gsJbSCqHoWGTOzaqw7z2m00)
### Alternative: manual start
To get a better view or have more control on the different layers of the network, you can also start the services manually. If you want to do that, first bring down everything with `./scripts/down.sh`.
#### Generate crypto material
In this step, we create all the identities which are used by the Token network. We use a normal Fabric CA for this. Technically, only the Owner identities (the wallets that will hold the tokens) need some form of hierarchy; they use Idemix credentials which must be issued by a single, known issuer (see [Fabric documentation](https://hyperledger-fabric.readthedocs.io/en/latest/idemix.html) for more info about idemix). To keep things simple, we use the same CA for the other identities too. The Token SDK expects the folders for the identities to be in Fabric's 'msp' structure.
The following crypto will be generated:
- Fabric Smart Client node identities, used by the nodes to authenticate each other
- Token Issuer identity (x509 certificate and private key)
- Token Auditor identity (x509 certificate and private key)
- Owner identities (idemix credentials)
```bash
mkdir -p keys/ca
docker-compose -f compose-ca.yaml up -d
./scripts/enroll-users.sh
```
> If you want, you can stop the CA now. You don't need it unless you want to register more users.
>
> ```bash
> docker-compose -f compose-ca.yaml down
> ```
The Issuer and Auditor identities are used by the Token Chaincode to validate token transactions. It also needs the identity of the CA that issues the Idemix credentials to the Owner wallets. The tokengen command generates the configuration that contains these identities and the cryptographic parameters for the proofs. We store it in the `tokenchaincode` folder, so that it will be baked into the chaincode docker image later.
```bash
tokengen gen dlog --base 300 --exponent 5 --issuers keys/issuer/iss/msp --idemix keys/owner1/wallet/alice --auditors keys/auditor/aud/msp --output tokenchaincode
```
> You only have to do this once. But if for any reason you want to re-generate the material: `rm -rf keys; rm tokenchaincode/zkatdlog_pp.json` and execute the steps above again. If any owner has existing tokens, they will now be invalid because the old proofs can not be verified with the new parameters.
#### Start Fabric and install the chaincode
For simplicity, in this sample all nodes use the credentials of User1 from Org1MSP and have Peer1 as a trusted peer. In a more serious setup, each instance would have its own (idemix) Fabric user and _may_ have it's own MSP and peers, depending on the network topology and trust relationships.
Start a Fabric sample network and deploy the Token Chaincode as a service:
```bash
../test-network/network.sh up createChannel
INIT_REQUIRED="--init-required" ../test-network/network.sh deployCCAAS -ccn tokenchaincode -ccp $(pwd)/tokenchaincode -cci "init" -verbose -ccs 1
mkdir -p keys/fabric && cp -r ../test-network/organizations keys/fabric/
```
> To fully remove the whole network:
> ```bash
> docker stop peer0org1_tokenchaincode_ccaas peer0org2_tokenchaincode_ccaas
> ../test-network/network.sh" down
> rm -rf keys/fabric
> ```
#### Start the Token network
> On the bottom of this document you'll find instructions to run the nodes as golang binaries natively instead of with docker compose.
```bash
rm -rf data/auditor data/issuer data/owner1 data/owner2
mkdir -p data/auditor data/issuer data/owner1 data/owner2
docker-compose up -d
```
Visit [http://localhost:8080](http://localhost:8080) to view the API documentation and execute some transactions.
### View the blockchain explorer
As a bonus, this sample contains configuration to connect the [blockchain explorer](https://github.com/hyperledger-labs/blockchain-explorer/) with the fabric-samples network. It allows you to inspect the transactions which are committed to the ledger. It shows more of what the Token SDK does under the covers.
Start it as follows:
```bash
cd explorer
docker-compose up -d
```
And visit it in the browser on [localhost:8081](http://localhost:8081).
To tear it down, do this (the -v is important; it removes the volumes that contain the identities and blocks):
```bash
docker-compose down -v
```
## Development
### End to end tests
See the `e2e` folder for some tests that exercise the APIs. The end to end tests require the services to be running. They create new transactions, so don't run them on a deployment you want to keep clean.
```bash
go test ./e2e -count=1 -v
```
### Code structure
This repo contains 3 different, isolated golang applications, one for each of the roles: *issuer*, *auditor*, and *owner*. They are maintained separately and each have their own dependencies. In a production scenario these would have their own lifecycle, and most likely be maintained and deployed by different organizations.
The code structure of each of the roles is the same. There is overlap between the roles; each has the boilerplate code to start the Fabric Smart Client and Token SDK. The main.go is almost identical; the only difference is which 'responders' the application registers. Also the contents of the routes and the services will depend on the features that a role needs:
- Issuers can issue funds
- Auditors see and sign every transaction
- Owners can transfer funds.
Here's an example of the code structure for the auditor:
```
auditor
├── main.go
├── oapi-server.yaml
├── conf
│ └── core.yaml
├── routes
│ ├── operations.go
│ ├── routes.gen.go
│ ├── routes.go
│ └── server.go
└── service
├── audit.go
├── balance.go
└── history.go
```
As you can see, the business logic is all in the 'service' directory. The 'routes' are purely the code needed for the REST API. We chose to use *openapi-codegen* to generate the code for the routes, and *echo* as the server. The 'routes' package is just the presentation layer; you could easily replace it and call the code from the 'service' package from somewhere else. For instance if you wanted to create a CLI application for the issuer!
![dependencies](./dependencies.png)
[plantuml](http://www.plantuml.com/plantuml/uml/RP71QlCm3CVlVWhHxnpw1X_jeR32e6FhRVIWEafgubX6ThgMqNTVQZjqAOD0PFd7pt_Pgn1Huj1RrGZs18lTboE19Mn365An7ceJMHRmhG36ht1R5qaSMl2eEsmfB00369ymWCyUZJlSM_S2_gszjqPZDEpoll0GAIGYbtymWUHiD2KedFKpSLEGxLLT_Ry3itMsAfZq1NbCy7Br99RgbWHUyHZcav2FqoWD7iNeAlGeiTBMa8ifKXF6I7lI9yUMs-iCZjoHgqBT9NByFv7pxEIZWZHYMJnIxkA91EYIRxj4uocQxzebYR24GvPcINQo-kNPN9xU2neMUDzyx67zjYrUdBoCtbIQQsh97NAhCwvYJsxSAPrn7fwEBPTSJaPrKojozT3R7m00)
For more information about how we interact with the Token SDK, check out an example on the [Token SDK GitHub](https://github.com/hyperledger-labs/fabric-token-sdk/blob/main/samples/fungible/README.md).
### Add or change a REST API endpoint
We generate the API based on `swagger.yaml`. To keep things a bit simple, we have only one definition which includes all of the roles (even though they are separate applications, running on different ports!) Any changes should be made in this file first. Then generate the code with:
```bash
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest
oapi-codegen -config auditor/oapi-server.yaml swagger.yaml
oapi-codegen -config issuer/oapi-server.yaml swagger.yaml
oapi-codegen -config owner/oapi-server.yaml swagger.yaml
oapi-codegen -config e2e/oapi-client.yaml swagger.yaml
```
### Upgrade the Token SDK and Fabric Smart Client versions
Token SDK and Fabric Smart Client are under active development. To upgrade to the latest versions:
- change the commit hash of fabric-smart-client and fabric-token-sdk in {auditor,issuer,owner}/go.mod (and do `go mod tidy`)
- change the commit hash in tokenchaincode/Dockerfile
- install tokengen with the new commit hash
- update the readme
### Use another Fabric network
Of course you're not tied to the Fabric samples testnetwork. If you want to anchor your Token services to another blockchain, you have to:
1. Deploy the token chaincode with the generated parameters to the Fabric network
2. Configure the token services with the correct channel, peer and orderer addresses and certs, MSP configuration, and a user (see the `core.yaml` files in the `conf` dir).
### Add a user / account
To add another user, simply register and enroll it at the Token CA (see `scripts/enroll-users.sh`), and configure it at one of the owner nodes (see `conf` dir).
### Run the service directly (instead of with docker-compose)
For a faster development cycle, you may choose to run the services outside of docker. It requires some adjustments to your environment to make the paths and routes work.
Add the following to your `/etc/hosts`:
```
127.0.0.1 peer0.org1.example.com
127.0.0.1 peer0.org2.example.com
127.0.0.1 orderer.example.com
127.0.0.1 owner1.example.com
127.0.0.1 owner2.example.com
127.0.0.1 auditor.example.com
127.0.0.1 issuer.example.com
```
> The Token SDK discovers the peer addresses from the channel config (after connecting to a configured trusted peer).
For the paths you have two options:
1. Find/replace all instances of /var/fsc in the conf directory with the path to this repo. **Or**
2. Create a symlink to this folder to make the configuration files work (they don't play nice with relative paths):
```bash
sudo ln -s "${PWD}" /var/fsc
```
The advantage of this approach is that the configuration is portable across developer laptops and works with docker-compose as well as without.
Start the blockchain and deploy the chaincode (see above).
Instead of doing docker-compose up, start the token services with (each in their own terminal):
```bash
mkdir bin
go build -o bin/auditor ./auditor
go build -o bin/issuer ./issuer
go build -o bin/owner ./owner
PORT=9000 CONF_DIR=./auditor/conf ./bin/auditor
PORT=9100 CONF_DIR=./issuer/conf ./bin/issuer
PORT=9200 CONF_DIR=./owner/conf/owner1 ./bin/owner
PORT=9300 CONF_DIR=./owner/conf/owner2 ./bin/owner
```
Now you can use the REST APIs to control the services (see the swagger definition).
When you made changes in the code, stop a service with CTRL+C, `go build -o bin/owner ./owner` and start it again.
If you want to reset the transaction history:
```bash
rm -rf data/auditor data/issuer data/owner1 data/owner2 && mkdir data/auditor data/issuer data/owner1 data/owner2
```

View file

@ -0,0 +1,112 @@
logging:
spec: info
format: '%{color}%{time:2006-01-02 15:04:05.000 MST} [%{module}] %{shortfunc} -> %{level:.4s} %{id:03x}%{color:reset} %{message}'
# ------------------- FSC Node Configuration -------------------------
# The FSC node is responsible for the peer to peer communication with other token services.
fsc:
identity:
cert:
file: /var/fsc/keys/auditor/fsc/msp/signcerts/cert.pem
key:
file: /var/fsc/keys/auditor/fsc/msp/keystore/priv_sk
tls:
enabled: false # TODO
p2p:
listenAddress: /ip4/0.0.0.0/tcp/9001
# 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:
kvs: # key-value-store
persistence:
type: badger # badger or memory
opts:
path: /var/fsc/data/auditor/kvs
# 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
identity:
id: issuer
path: /var/fsc/keys/issuer/fsc/msp/signcerts/cert.pem
addresses:
P2P: issuer.example.com:9101
- name: owner1
identity:
id: owner1
path: /var/fsc/keys/owner1/fsc/msp/signcerts/cert.pem
addresses:
P2P: owner1.example.com:9201
- name: owner2
identity:
id: owner2
path: /var/fsc/keys/owner2/fsc/msp/signcerts/cert.pem
addresses:
P2P: owner2.example.com:9301
# ------------------- Fabric Configuration -------------------------
fabric:
enabled: true
mynetwork:
default: true
mspConfigPath: /var/fsc/keys/fabric/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp
defaultMSP: Org1MSP
msps:
- id: Org1MSP
mspType: bccsp
mspID: Org1MSP
path: /var/fsc/keys/fabric/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp
tls:
enabled: true
# If the keepalive values are too low, Fabric peers will complain with: ENHANCE_YOUR_CALM, debug data: "too_many_pings"
keepalive:
interval: 300s
timeout: 600s
# List of orderer nodes this node can connect to. There must be at least one orderer node. Others are discovered.
orderers:
- address: orderer.example.com:7050
connectionTimeout: 10s
tlsEnabled: true
tlsRootCertFile: /var/fsc/keys/fabric/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/ca.crt
serverNameOverride: orderer.example.com
# List of trusted peers this node can connect to. There must be at least one trusted peer. Others are discovered.
peers:
- address: peer0.org1.example.com:7051
connectionTimeout: 10s
tlsEnabled: true
tlsRootCertFile: /var/fsc/keys/fabric/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
serverNameOverride: peer0.org1.example.com
# Channel where the token chaincode is deployed
channels:
- name: mychannel
default: true
# Configuration of the vault used to store the RW sets assembled by this node
vault:
persistence:
type: badger
opts:
path: /var/fsc/data/auditor/vault
# ------------------- Token SDK Configuration -------------------------
token:
enabled: true
tms:
mytms: # unique name of this token management system
network: mynetwork # the name of the fabric network as configured above
channel: mychannel # the name of the network's channel this TMS refers to, if applicable
namespace: tokenchaincode # chaincode name
driver: zkatdlog # privacy preserving driver (zero knowledge asset transfer)
wallets:
auditors:
- id: auditor # the unique identifier of this wallet. Here is an example of use: `ttx.GetIssuerWallet(context, "issuer)`
default: true # is this the default issuer wallet
path: /var/fsc/keys/auditor/aud/msp
# Internal database to keep track of token transactions.
# It is used by auditors and token owners to track history
ttxdb:
persistence:
type: badger
opts:
path: /var/fsc/data/auditor/txdb

245
token-sdk/auditor/go.mod Normal file
View file

@ -0,0 +1,245 @@
module github.com/hyperledger/fabric-samples/token-sdk/auditor
go 1.20
replace github.com/ugorji/go v1.1.4 => github.com/ugorji/go/codec v1.2.9
require (
github.com/deepmap/oapi-codegen v1.15.0
github.com/getkin/kin-openapi v0.120.0
github.com/hyperledger-labs/fabric-smart-client v0.3.0
github.com/hyperledger-labs/fabric-token-sdk v0.3.0
github.com/labstack/echo/v4 v4.11.1
github.com/pkg/errors v0.9.1
)
require (
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
github.com/CloudyKit/jet/v6 v6.2.0 // indirect
github.com/IBM/idemix v0.0.2-0.20230831093709-b7a940638990 // indirect
github.com/IBM/idemix/bccsp/schemes/aries v0.0.0-20230831093709-b7a940638990 // indirect
github.com/IBM/idemix/bccsp/schemes/weak-bb v0.0.0-20230831093709-b7a940638990 // indirect
github.com/IBM/idemix/bccsp/types v0.0.0-20230831093709-b7a940638990 // indirect
github.com/IBM/mathlib v0.0.3-0.20230831091907-c532c4d3b65c // indirect
github.com/Joker/jade v1.1.3 // indirect
github.com/ReneKroon/ttlcache/v2 v2.11.0 // indirect
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect
github.com/ale-linux/aries-framework-go/component/kmscrypto v0.0.0-20230817163708-4b3de6d91874 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/consensys/bavard v0.1.13 // indirect
github.com/consensys/gnark-crypto v0.9.1 // indirect
github.com/containerd/cgroups v1.1.0 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/dgraph-io/badger/v3 v3.2103.2 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/elastic/gosigar v0.14.2 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/flosch/pongo2/v4 v4.0.2 // indirect
github.com/flynn/noise v1.0.0 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/go-kit/kit v0.10.0 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 // indirect
github.com/google/flatbuffers v1.12.1 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-uuid v1.0.2 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huin/goupnp v1.2.0 // indirect
github.com/hyperledger-labs/orion-sdk-go v0.2.5 // indirect
github.com/hyperledger-labs/orion-server v0.2.5 // indirect
github.com/hyperledger-labs/weaver-dlt-interoperability/common/protos-go v1.2.3-alpha.1 // indirect
github.com/hyperledger-labs/weaver-dlt-interoperability/sdks/fabric/go-sdk v1.2.3-alpha.1.0.20210812140206-37f430515b8c // indirect
github.com/hyperledger/fabric v1.4.0-rc1.0.20230401164317-bd8e24856939 // indirect
github.com/hyperledger/fabric-amcl v0.0.0-20230602173724-9e02669dceb2 // indirect
github.com/hyperledger/fabric-chaincode-go v0.0.0-20220920210243-7bc6fa0dd58b // indirect
github.com/hyperledger/fabric-lib-go v1.0.0 // indirect
github.com/hyperledger/fabric-private-chaincode v0.0.0-20210907122433-d56466264e4d // indirect
github.com/hyperledger/fabric-protos-go v0.2.0 // indirect
github.com/invopop/yaml v0.2.0 // indirect
github.com/ipfs/boxo v0.8.0-rc1 // indirect
github.com/ipfs/go-cid v0.4.1 // indirect
github.com/ipfs/go-datastore v0.6.0 // indirect
github.com/ipfs/go-ipfs-util v0.0.2 // indirect
github.com/ipfs/go-log v1.0.5 // indirect
github.com/ipfs/go-log/v2 v2.5.1 // indirect
github.com/ipld/go-ipld-prime v0.20.0 // indirect
github.com/iris-contrib/schema v0.0.6 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
github.com/jbenet/goprocess v0.1.4 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kataras/blocks v0.0.7 // indirect
github.com/kataras/golog v0.1.9 // indirect
github.com/kataras/iris/v12 v12.2.6-0.20230908161203-24ba4e8933b9 // indirect
github.com/kataras/pio v0.0.12 // indirect
github.com/kataras/sitemap v0.0.6 // indirect
github.com/kataras/tunnel v0.0.4 // indirect
github.com/kilic/bls12-381 v0.1.0 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/koron/go-ssdp v0.0.4 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/libp2p/go-cidranger v1.1.0 // indirect
github.com/libp2p/go-flow-metrics v0.1.0 // indirect
github.com/libp2p/go-libp2p v0.31.0 // indirect
github.com/libp2p/go-libp2p-asn-util v0.3.0 // indirect
github.com/libp2p/go-libp2p-kad-dht v0.22.0 // indirect
github.com/libp2p/go-libp2p-kbucket v0.5.0 // indirect
github.com/libp2p/go-libp2p-record v0.2.0 // indirect
github.com/libp2p/go-msgio v0.3.0 // indirect
github.com/libp2p/go-nat v0.2.0 // indirect
github.com/libp2p/go-netroute v0.2.1 // indirect
github.com/libp2p/go-reuseport v0.4.0 // indirect
github.com/libp2p/go-yamux/v4 v4.0.1 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mailgun/raymond/v2 v2.0.48 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/microcosm-cc/bluemonday v1.0.25 // indirect
github.com/miekg/dns v1.1.55 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/mmcloughlin/addchain v0.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/multiformats/go-base32 v0.1.0 // indirect
github.com/multiformats/go-base36 v0.2.0 // indirect
github.com/multiformats/go-multiaddr v0.11.0 // indirect
github.com/multiformats/go-multiaddr-dns v0.3.1 // indirect
github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect
github.com/multiformats/go-multibase v0.2.0 // indirect
github.com/multiformats/go-multicodec v0.9.0 // indirect
github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-multistream v0.4.1 // indirect
github.com/multiformats/go-varint v0.0.7 // indirect
github.com/onsi/ginkgo/v2 v2.11.0 // indirect
github.com/opencontainers/runtime-spec v1.1.0 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/polydawn/refmt v0.89.0 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.3.3 // indirect
github.com/quic-go/quic-go v0.38.1 // indirect
github.com/quic-go/webtransport-go v0.5.3 // indirect
github.com/raulk/go-watchdog v1.3.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/schollz/closestmatch v2.1.0+incompatible // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.10.1 // indirect
github.com/stretchr/testify v1.8.4 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/sykesm/zap-logfmt v0.0.4 // indirect
github.com/tdewolff/minify/v2 v2.12.9 // indirect
github.com/tdewolff/parse/v2 v2.6.8 // indirect
github.com/test-go/testify v1.1.4 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect
github.com/yosssi/ace v0.0.5 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/otel v1.14.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.13.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.13.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.13.0 // indirect
go.opentelemetry.io/otel/sdk v1.13.0 // indirect
go.opentelemetry.io/otel/trace v1.14.0 // indirect
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/dig v1.17.0 // indirect
go.uber.org/fx v1.20.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.25.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
google.golang.org/grpc v1.53.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.2.1 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
)

1978
token-sdk/auditor/go.sum Normal file

File diff suppressed because it is too large Load diff

114
token-sdk/auditor/main.go Normal file
View file

@ -0,0 +1,114 @@
package main
import (
"net/http"
"os"
"os/signal"
"syscall"
"github.com/hyperledger/fabric-samples/token-sdk/auditor/routes"
"github.com/hyperledger/fabric-samples/token-sdk/auditor/service"
"github.com/hyperledger-labs/fabric-smart-client/pkg/api"
"github.com/hyperledger-labs/fabric-smart-client/pkg/node"
fabric "github.com/hyperledger-labs/fabric-smart-client/platform/fabric/sdk"
viewregistry "github.com/hyperledger-labs/fabric-smart-client/platform/view"
"github.com/hyperledger-labs/fabric-smart-client/platform/view/services/flogging"
tokensdk "github.com/hyperledger-labs/fabric-token-sdk/token/sdk"
"github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx"
)
var logger = flogging.MustGetLogger("main")
func main() {
dir := getEnv("CONF_DIR", "./conf")
port := getEnv("PORT", "9000")
fsc := startFabricSmartClient(dir)
// Tell the service how to respond to other nodes when they initiate an action
registry := viewregistry.GetRegistry(fsc)
succeedOrPanic(registry.RegisterResponder(&service.AuditView{}, &ttx.AuditingViewInitiator{}))
controller := routes.Controller{Service: service.TokenService{FSC: fsc}}
err := routes.StartWebServer(port, controller, logger)
if err != nil {
if err == http.ErrServerClosed {
logger.Infof("Webserver closing, exiting...", err.Error())
fsc.Stop()
} else {
logger.Fatalf("echo error - %s", err.Error())
fsc.Stop()
os.Exit(1)
}
}
}
type Node interface {
api.ServiceProvider
Stop()
}
func startFabricSmartClient(confDir string) Node {
logger.Infof("Initializing Fabric Smart Client and Token SDK...")
fsc := node.NewFromConfPath(confDir)
succeedOrPanic(fsc.InstallSDK(fabric.NewSDK(fsc)))
succeedOrPanic(fsc.InstallSDK(tokensdk.NewSDK(fsc)))
succeedOrPanic(fsc.Start())
// Stop gracefully
go handleSignals((map[os.Signal]func(){
syscall.SIGINT: func() {
logger.Info("Stopping FSC node...")
fsc.Stop()
os.Exit(130)
},
syscall.SIGTERM: func() {
logger.Info("Stopping FSC node...")
fsc.Stop()
os.Exit(143)
},
syscall.SIGSTOP: func() {
logger.Info("Stopping FSC node...")
fsc.Stop()
os.Exit(145)
},
syscall.SIGHUP: func() {
logger.Info("Stopping FSC node...")
fsc.Stop()
os.Exit(129)
},
}))
logger.Infof("FSC node is ready!")
return fsc
}
// getEnv returns an environment variable or the fallback
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
func succeedOrPanic(err error) {
if err != nil {
logger.Fatalf("Failed initializing Token SDK - %s", err.Error())
os.Exit(1)
}
}
func handleSignals(handlers map[os.Signal]func()) {
var signals []os.Signal
for sig := range handlers {
signals = append(signals, sig)
}
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, signals...)
for sig := range signalChan {
logger.Infof("Received signal: %d (%s)", sig, sig)
handlers[sig]()
}
}

View file

@ -0,0 +1,11 @@
package: routes
generate:
echo-server: true
strict-server: true
models: true
embedded-spec: true
output-options:
include-tags:
- operations
- auditor
output: auditor/routes/routes.gen.go

View file

@ -0,0 +1,31 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package routes
import (
"context"
)
// (GET /readyz)
func (c Controller) Readyz(ctx context.Context, request ReadyzRequestObject) (ReadyzResponseObject, error) {
// TODO: what defines readiness if the REST API is available after FSC?
return Readyz200JSONResponse{
HealthSuccessJSONResponse: HealthSuccessJSONResponse{
Message: "ok",
},
}, nil
}
// (GET /healthz)
func (c Controller) Healthz(ctx context.Context, request HealthzRequestObject) (HealthzResponseObject, error) {
// TODO: how to determine health?
return Healthz200JSONResponse{
HealthSuccessJSONResponse: HealthSuccessJSONResponse{
Message: "ok",
},
}, nil
}

View file

@ -0,0 +1,579 @@
// Package auditor provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/deepmap/oapi-codegen version v1.13.4 DO NOT EDIT.
package routes
import (
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"path"
"strings"
"time"
"github.com/deepmap/oapi-codegen/pkg/runtime"
"github.com/getkin/kin-openapi/openapi3"
"github.com/labstack/echo/v4"
)
// Account Information about an account and its balance
type Account struct {
// Balance balance in base units for each currency
Balance []Amount `json:"balance"`
// Id account id as registered at the Certificate Authority
Id string `json:"id"`
}
// Amount The amount to issue, transfer or redeem.
type Amount struct {
// Code the code of the token
Code string `json:"code"`
// Value value in base units (usually cents)
Value int64 `json:"value"`
}
// Error defines model for Error.
type Error struct {
// Message High level error message
Message string `json:"message"`
// Payload Details about the error
Payload string `json:"payload"`
}
// TransactionRecord A transaction
type TransactionRecord struct {
// Amount The amount to issue, transfer or redeem.
Amount Amount `json:"amount"`
// Id transaction id
Id string `json:"id"`
// Message user provided message
Message string `json:"message"`
// Recipient the recipient of the transaction
Recipient string `json:"recipient"`
// Sender the sender of the transaction
Sender string `json:"sender"`
// Status Unknown | Pending | Confirmed | Deleted
Status string `json:"status"`
// Timestamp timestamp in the format: "2018-03-20T09:12:28Z"
Timestamp time.Time `json:"timestamp"`
}
// Code The token code to filter on
type Code = string
// Id account id as registered at the Certificate Authority
type Id = string
// AccountSuccess defines model for AccountSuccess.
type AccountSuccess struct {
Message string `json:"message"`
// Payload Information about an account and its balance
Payload Account `json:"payload"`
}
// ErrorResponse defines model for ErrorResponse.
type ErrorResponse = Error
// HealthSuccess defines model for HealthSuccess.
type HealthSuccess struct {
// Message ok
Message string `json:"message"`
}
// TransactionsSuccess defines model for TransactionsSuccess.
type TransactionsSuccess struct {
Message string `json:"message"`
Payload []TransactionRecord `json:"payload"`
}
// AuditorAccountParams defines parameters for AuditorAccount.
type AuditorAccountParams struct {
Code *Code `form:"code,omitempty" json:"code,omitempty"`
}
// ServerInterface represents all server handlers.
type ServerInterface interface {
// Get an account and their balance of a certain type
// (GET /auditor/accounts/{id})
AuditorAccount(ctx echo.Context, id Id, params AuditorAccountParams) error
// Get all transactions for an account
// (GET /auditor/accounts/{id}/transactions)
AuditorTransactions(ctx echo.Context, id Id) error
// (GET /healthz)
Healthz(ctx echo.Context) error
// (GET /readyz)
Readyz(ctx echo.Context) error
}
// ServerInterfaceWrapper converts echo contexts to parameters.
type ServerInterfaceWrapper struct {
Handler ServerInterface
}
// AuditorAccount converts echo context to params.
func (w *ServerInterfaceWrapper) AuditorAccount(ctx echo.Context) error {
var err error
// ------------- Path parameter "id" -------------
var id Id
err = runtime.BindStyledParameterWithLocation("simple", false, "id", runtime.ParamLocationPath, ctx.Param("id"), &id)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err))
}
// Parameter object where we will unmarshal all parameters from the context
var params AuditorAccountParams
// ------------- Optional query parameter "code" -------------
err = runtime.BindQueryParameter("form", true, false, "code", ctx.QueryParams(), &params.Code)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter code: %s", err))
}
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.AuditorAccount(ctx, id, params)
return err
}
// AuditorTransactions converts echo context to params.
func (w *ServerInterfaceWrapper) AuditorTransactions(ctx echo.Context) error {
var err error
// ------------- Path parameter "id" -------------
var id Id
err = runtime.BindStyledParameterWithLocation("simple", false, "id", runtime.ParamLocationPath, ctx.Param("id"), &id)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err))
}
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.AuditorTransactions(ctx, id)
return err
}
// Healthz converts echo context to params.
func (w *ServerInterfaceWrapper) Healthz(ctx echo.Context) error {
var err error
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.Healthz(ctx)
return err
}
// Readyz converts echo context to params.
func (w *ServerInterfaceWrapper) Readyz(ctx echo.Context) error {
var err error
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.Readyz(ctx)
return err
}
// This is a simple interface which specifies echo.Route addition functions which
// are present on both echo.Echo and echo.Group, since we want to allow using
// either of them for path registration
type EchoRouter interface {
CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
}
// RegisterHandlers adds each server route to the EchoRouter.
func RegisterHandlers(router EchoRouter, si ServerInterface) {
RegisterHandlersWithBaseURL(router, si, "")
}
// Registers handlers, and prepends BaseURL to the paths, so that the paths
// can be served under a prefix.
func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) {
wrapper := ServerInterfaceWrapper{
Handler: si,
}
router.GET(baseURL+"/auditor/accounts/:id", wrapper.AuditorAccount)
router.GET(baseURL+"/auditor/accounts/:id/transactions", wrapper.AuditorTransactions)
router.GET(baseURL+"/healthz", wrapper.Healthz)
router.GET(baseURL+"/readyz", wrapper.Readyz)
}
type AccountSuccessJSONResponse struct {
Message string `json:"message"`
// Payload Information about an account and its balance
Payload Account `json:"payload"`
}
type ErrorResponseJSONResponse Error
type HealthSuccessJSONResponse struct {
// Message ok
Message string `json:"message"`
}
type TransactionsSuccessJSONResponse struct {
Message string `json:"message"`
Payload []TransactionRecord `json:"payload"`
}
type AuditorAccountRequestObject struct {
Id Id `json:"id"`
Params AuditorAccountParams
}
type AuditorAccountResponseObject interface {
VisitAuditorAccountResponse(w http.ResponseWriter) error
}
type AuditorAccount200JSONResponse struct{ AccountSuccessJSONResponse }
func (response AuditorAccount200JSONResponse) VisitAuditorAccountResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type AuditorAccountdefaultJSONResponse struct {
Body Error
StatusCode int
}
func (response AuditorAccountdefaultJSONResponse) VisitAuditorAccountResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(response.StatusCode)
return json.NewEncoder(w).Encode(response.Body)
}
type AuditorTransactionsRequestObject struct {
Id Id `json:"id"`
}
type AuditorTransactionsResponseObject interface {
VisitAuditorTransactionsResponse(w http.ResponseWriter) error
}
type AuditorTransactions200JSONResponse struct {
TransactionsSuccessJSONResponse
}
func (response AuditorTransactions200JSONResponse) VisitAuditorTransactionsResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type AuditorTransactionsdefaultJSONResponse struct {
Body Error
StatusCode int
}
func (response AuditorTransactionsdefaultJSONResponse) VisitAuditorTransactionsResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(response.StatusCode)
return json.NewEncoder(w).Encode(response.Body)
}
type HealthzRequestObject struct {
}
type HealthzResponseObject interface {
VisitHealthzResponse(w http.ResponseWriter) error
}
type Healthz200JSONResponse struct{ HealthSuccessJSONResponse }
func (response Healthz200JSONResponse) VisitHealthzResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type Healthz503JSONResponse struct{ ErrorResponseJSONResponse }
func (response Healthz503JSONResponse) VisitHealthzResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(503)
return json.NewEncoder(w).Encode(response)
}
type ReadyzRequestObject struct {
}
type ReadyzResponseObject interface {
VisitReadyzResponse(w http.ResponseWriter) error
}
type Readyz200JSONResponse struct{ HealthSuccessJSONResponse }
func (response Readyz200JSONResponse) VisitReadyzResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type Readyz503JSONResponse struct{ ErrorResponseJSONResponse }
func (response Readyz503JSONResponse) VisitReadyzResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(503)
return json.NewEncoder(w).Encode(response)
}
// StrictServerInterface represents all server handlers.
type StrictServerInterface interface {
// Get an account and their balance of a certain type
// (GET /auditor/accounts/{id})
AuditorAccount(ctx context.Context, request AuditorAccountRequestObject) (AuditorAccountResponseObject, error)
// Get all transactions for an account
// (GET /auditor/accounts/{id}/transactions)
AuditorTransactions(ctx context.Context, request AuditorTransactionsRequestObject) (AuditorTransactionsResponseObject, error)
// (GET /healthz)
Healthz(ctx context.Context, request HealthzRequestObject) (HealthzResponseObject, error)
// (GET /readyz)
Readyz(ctx context.Context, request ReadyzRequestObject) (ReadyzResponseObject, error)
}
type StrictHandlerFunc = runtime.StrictEchoHandlerFunc
type StrictMiddlewareFunc = runtime.StrictEchoMiddlewareFunc
func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface {
return &strictHandler{ssi: ssi, middlewares: middlewares}
}
type strictHandler struct {
ssi StrictServerInterface
middlewares []StrictMiddlewareFunc
}
// AuditorAccount operation middleware
func (sh *strictHandler) AuditorAccount(ctx echo.Context, id Id, params AuditorAccountParams) error {
var request AuditorAccountRequestObject
request.Id = id
request.Params = params
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
return sh.ssi.AuditorAccount(ctx.Request().Context(), request.(AuditorAccountRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "AuditorAccount")
}
response, err := handler(ctx, request)
if err != nil {
return err
} else if validResponse, ok := response.(AuditorAccountResponseObject); ok {
return validResponse.VisitAuditorAccountResponse(ctx.Response())
} else if response != nil {
return fmt.Errorf("Unexpected response type: %T", response)
}
return nil
}
// AuditorTransactions operation middleware
func (sh *strictHandler) AuditorTransactions(ctx echo.Context, id Id) error {
var request AuditorTransactionsRequestObject
request.Id = id
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
return sh.ssi.AuditorTransactions(ctx.Request().Context(), request.(AuditorTransactionsRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "AuditorTransactions")
}
response, err := handler(ctx, request)
if err != nil {
return err
} else if validResponse, ok := response.(AuditorTransactionsResponseObject); ok {
return validResponse.VisitAuditorTransactionsResponse(ctx.Response())
} else if response != nil {
return fmt.Errorf("Unexpected response type: %T", response)
}
return nil
}
// Healthz operation middleware
func (sh *strictHandler) Healthz(ctx echo.Context) error {
var request HealthzRequestObject
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
return sh.ssi.Healthz(ctx.Request().Context(), request.(HealthzRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "Healthz")
}
response, err := handler(ctx, request)
if err != nil {
return err
} else if validResponse, ok := response.(HealthzResponseObject); ok {
return validResponse.VisitHealthzResponse(ctx.Response())
} else if response != nil {
return fmt.Errorf("Unexpected response type: %T", response)
}
return nil
}
// Readyz operation middleware
func (sh *strictHandler) Readyz(ctx echo.Context) error {
var request ReadyzRequestObject
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
return sh.ssi.Readyz(ctx.Request().Context(), request.(ReadyzRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "Readyz")
}
response, err := handler(ctx, request)
if err != nil {
return err
} else if validResponse, ok := response.(ReadyzResponseObject); ok {
return validResponse.VisitReadyzResponse(ctx.Response())
} else if response != nil {
return fmt.Errorf("Unexpected response type: %T", response)
}
return nil
}
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"H4sIAAAAAAAC/9RZUW/bNhD+KwduDy2gRrKzDa3esrZYi70UaQoMbfNwls4WG4pUScqZl/q/DyQlWbJk",
"J826onlKJFLH7767++4I37BMlZWSJK1h6Q2rUGNJlrR/ylRO7i+XLGWfa9IbFjGJJbE0rEXMZAWV6Dbl",
"ZDLNK8uV231REFh1RRLcRrAKllxY0qAkixj9jWUlnJmX787/YhGzm8o9Gau5XLHtNmI8706u0Ba7g3nO",
"Iqbpc8015Sy1uqbDMDDLVC0t8BzQgKYVN5Y05YAWbEHwnLTlS56hJTirbaE0t5sBQBQ8owmEWwfCVEoa",
"8lydhZPe1llGpmFPWpLW/YtVJdwhXMn4k3HIbnqQK60qhyMYKskYXHne986MWIUbodAz87OmJUvZT/Eu",
"gHEwaeIGCwsgW6Y+dKZ3hi47x9TiE2U2ODbksHEJWncdkJdaK33evvgaZ4/h9lanIPiFAYBXhMIW92G7",
"C22PaqauPL2HAjFEo64mM3aK6fvye6FRGszcDvNNU2qX2LZ3BGiymtOa8rFng6zjlkpzWxh74M8pUzp3",
"RhqrqDVu/rfE3LZK0C/JcQBfy6XSpecOcKFqCyihlQqUOXBrYIECpS/9Xsa0L9MPrTq2CrZGURNLZ0mS",
"JNuoW3339kVvdZ4k28ugbY2wjLKuO2EfdLMAXMICDUEtHcql0kCYFZDVWpPMnHjdKUhnZZCI/ci0yvu9",
"dHSYCF7cWwrGORCxBvZkv0G/5noNN6amCHyKL13TceKRE5Unw3AeCuEoKm0nzGmJtbC7b4YoHBW+36ml",
"p8V3wKmSao7a98K/3ovwo9rUKMQGMhe/xyxiIXldK5T2t19YxEoueVmXLE26o7i0tCI9Irhp2+H8KYKD",
"Bh/SSfJCPC7XlH2Ffr7iqwIErUnAvr1j2jM08oIscmGa+nVke1t3VuZjUjPQ30bCRgDOoKegw7TCLkmP",
"JJgrs9n8NOqx6webjFfcazxbqIWbsEjmpHsVZCza2rCUPVdyyXUZRJuXZCyWFUvZPJk9fZKcPpknF8mz",
"dDZP50/fj8OzA3k3mZiShR4DwCd7x8EkqA1pqLRa85zyYxnQY+Rmoty65a7mBlEZmWvpnLIV1u5qqAnD",
"vqF38kqqawlf4A3JnMsVfIEuUvAFXpAgO91oe0EcwWuXnDo4dEEEUvg4Ge6PrK8TOVp64izcTX8bivrU",
"R2269EF2HES3zDtcLtVEFw4iXSiR96Tatd+g1UE9zQn8jtkV5bDYAELOHfJFbSkHQfmKdPRRVpoM6bXj",
"utJ8jdkGauOe3pNW8KdU134rvNFKLc2Jd8L6rnThj3ClSdoEWLOTxMVCVSSx4ixlpyfJyanXC1v4eMdY",
"59wqHTdd0cQ3PN+6lRX5LHVl5qeL104Zz8LudhqJBpesD9Plt9sSczc+3brLC42bLQaXknmSHCrwbl+8",
"d3PxY1bT5W77dHgN8OMX6XXr2N4IEWhgEau1YCkrrK3SOBYqQ1EoY9NnSZLEWPF4PYu9K6YuS9QblrI/",
"aDSi2YK4boc0V7IIGWmLrjxcBkbM4srh6A6+/KbwttGBPIj7g/VtSdGf8++VGfeK+NTt4ocNuxAwuKq4",
"iXeXC98lzoW/bP5zMJivmvX7xGJ4kd1G7Nfk9D4RaFnokJlAREfmOdlaSwPzJAEeOpwXTXelMBBc9PN/",
"7KdnHf74a+VBMsPOI1zODsbWq38j8W5k74XUYVDXknaFdRyFH4y8KISJ6RCYeR9MtG8lQy2U8WZylEfM",
"nI7yYwi26wYPC3Ecuu4PDHyYzX5EeLSotXzcpBE75Nm+Ij+wwLSz0QMJzUU7yvWLW9nCDXe9CteE+eaw",
"pp6H5Qcsqd5B732WUWUhQyHMt+2W0X8T5OgHy6GG8NFNu+n8uEYucCH8zwk7pprfBdoXYzyT33dMtT8r",
"hOc7fu3LFK5RCLJmZ8S/nrDxtsmK0GWbOw7mXLoE3X29y7Pt5fbfAAAA///PKdGnmxkAAA==",
}
// GetSwagger returns the content of the embedded swagger specification file
// or error if failed to decode
func decodeSpec() ([]byte, error) {
zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, ""))
if err != nil {
return nil, fmt.Errorf("error base64 decoding spec: %w", err)
}
zr, err := gzip.NewReader(bytes.NewReader(zipped))
if err != nil {
return nil, fmt.Errorf("error decompressing spec: %w", err)
}
var buf bytes.Buffer
_, err = buf.ReadFrom(zr)
if err != nil {
return nil, fmt.Errorf("error decompressing spec: %w", err)
}
return buf.Bytes(), nil
}
var rawSpec = decodeSpecCached()
// a naive cached of a decoded swagger spec
func decodeSpecCached() func() ([]byte, error) {
data, err := decodeSpec()
return func() ([]byte, error) {
return data, err
}
}
// Constructs a synthetic filesystem for resolving external references when loading openapi specifications.
func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) {
res := make(map[string]func() ([]byte, error))
if len(pathToFile) > 0 {
res[pathToFile] = rawSpec
}
return res
}
// GetSwagger returns the Swagger specification corresponding to the generated code
// in this file. The external references of Swagger specification are resolved.
// The logic of resolving external references is tightly connected to "import-mapping" feature.
// Externally referenced files must be embedded in the corresponding golang packages.
// Urls can be supported but this task was out of the scope.
func GetSwagger() (swagger *openapi3.T, err error) {
resolvePath := PathToRawSpec("")
loader := openapi3.NewLoader()
loader.IsExternalRefsAllowed = true
loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) {
pathToFile := url.String()
pathToFile = path.Clean(pathToFile)
getSpec, ok := resolvePath[pathToFile]
if !ok {
err1 := fmt.Errorf("path not found: %s", pathToFile)
return nil, err1
}
return getSpec()
}
var specData []byte
specData, err = rawSpec()
if err != nil {
return
}
swagger, err = loader.LoadFromData(specData)
if err != nil {
return
}
return
}

View file

@ -0,0 +1,100 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package routes
import (
"context"
"fmt"
"github.com/hyperledger/fabric-samples/token-sdk/auditor/service"
)
type Controller struct {
Service service.TokenService
}
// Get an account and their balance of a certain type
// (GET /auditor/accounts/{id})
func (c Controller) AuditorAccount(ctx context.Context, request AuditorAccountRequestObject) (AuditorAccountResponseObject, error) {
if request.Params.Code == nil {
return AuditorAccountdefaultJSONResponse{
Body: Error{
Message: "code is required",
Payload: "",
},
StatusCode: 400,
}, nil
}
balance, err := c.Service.GetBalance(request.Id, *request.Params.Code)
if err != nil {
return AuditorAccountdefaultJSONResponse{
Body: Error{
Message: "can't get account",
Payload: err.Error(),
},
StatusCode: 500,
}, nil
}
amounts := []Amount{}
for typ, val := range balance {
amounts = append(amounts, Amount{
Code: typ,
Value: val,
})
}
return AuditorAccount200JSONResponse{
AccountSuccessJSONResponse: AccountSuccessJSONResponse{
Message: fmt.Sprintf("got %s's %s", request.Id, *request.Params.Code),
Payload: Account{
Id: request.Id,
Balance: amounts,
},
},
}, nil
}
// Get all transactions for an account
// (GET /owner/accounts/{id}/transactions)
func (c Controller) AuditorTransactions(ctx context.Context, request AuditorTransactionsRequestObject) (AuditorTransactionsResponseObject, error) {
var history []service.TransactionHistoryItem
var err error
history, err = c.Service.GetHistory(request.Id)
if err != nil {
return AuditorTransactionsdefaultJSONResponse{
Body: Error{
Message: "can't get history",
Payload: err.Error(),
},
StatusCode: 500,
}, nil
}
pl := []TransactionRecord{}
for _, tx := range history {
pl = append(pl, TransactionRecord{
Amount: Amount{
Code: tx.TokenType,
Value: tx.Amount,
},
Id: tx.TxID,
Recipient: tx.Recipient,
Sender: tx.Sender,
Status: tx.Status,
Timestamp: tx.Timestamp,
Message: tx.Message,
})
}
return AuditorTransactions200JSONResponse{
TransactionsSuccessJSONResponse: TransactionsSuccessJSONResponse{
Message: fmt.Sprintf("got %d transactions for %s", len(pl), request.Id),
Payload: pl,
},
}, nil
}

View file

@ -0,0 +1,65 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package routes
import (
"fmt"
"log"
"os"
oapimiddleware "github.com/deepmap/oapi-codegen/pkg/middleware"
"github.com/labstack/echo/v4"
middleware "github.com/labstack/echo/v4/middleware"
)
type Logger interface {
Infof(template string, args ...interface{})
Debugf(template string, args ...interface{})
Warnf(template string, args ...interface{})
Errorf(template string, args ...interface{})
Fatalf(template string, args ...interface{})
}
// Start web server on the main thread. It exits the application if it fails setting up.
func StartWebServer(port string, routesImplementation StrictServerInterface, logger Logger) error {
e := echo.New()
baseURL := "/api/v1"
handler := NewStrictHandler(routesImplementation, nil)
RegisterHandlersWithBaseURL(e, handler, baseURL)
// Request validator
swagger, err := GetSwagger()
if err != nil {
log.Fatalf("Error loading swagger spec\n: %s", err)
os.Exit(1)
}
swagger.Servers = nil
e.Group(baseURL).Use(oapimiddleware.OapiRequestValidator(swagger))
e.Use(middleware.CORS())
e.Use(middleware.RequestID())
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
Skipper: func(c echo.Context) bool {
return c.Path() == "/api/v1/healthz" || c.Path() == "/api/v1/readyz"
},
LogRequestID: true, LogMethod: true, LogURI: true, LogStatus: true, LogLatency: true,
LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
if v.Status < 400 {
logger.Infof("%d %s %s %s [%s]", v.Status, v.Method, v.URI, v.Latency.String(), v.RequestID)
} else if v.Status >= 400 && v.Status < 500 {
logger.Warnf("%d %s %s %s [%s]", v.Status, v.Method, v.URI, v.Latency.String(), v.RequestID)
} else {
logger.Errorf("%d %s %s %s [%s]", v.Status, v.Method, v.URI, v.Latency.String(), v.RequestID)
}
return nil
},
}))
// Start REST API server
return e.Start(fmt.Sprintf("0.0.0.0:%s", port))
}

View file

@ -0,0 +1,68 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package service
import (
"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/ttx"
"github.com/pkg/errors"
)
var logger = flogging.MustGetLogger("service")
// VIEW
// Auditing is initiated as a response to an audit request from another
// FSC node (not via an internal service or API).
type AuditView struct{}
func (v *AuditView) Call(context view.Context) (interface{}, error) {
logger.Infof("incoming session from [%s]", context.Session().Info().Endpoint)
tx, err := ttx.ReceiveTransaction(context)
if err != nil {
err = errors.Wrap(err, "failed receiving transaction")
logger.Error(err.Error())
return "", err
}
// get auditor wallet
w := ttx.MyAuditorWallet(context)
if w == nil {
err = errors.New("failed getting default auditor wallet")
logger.Error(err.Error())
return "", err
}
auditor := ttx.NewAuditor(context, w)
// Validate
err = auditor.Validate(tx)
if err != nil {
err = errors.Wrapf(err, "transaction invalid: [%s]", tx.ID())
logger.Error(err.Error())
return "", err
}
// See https://github.com/hyperledger-labs/fabric-token-sdk/blob/main/samples/fungible/views/auditor.go for examples of auditor checks
logger.Infof("transaction valid: [%s]", tx.ID())
res, err := context.RunView(ttx.NewAuditApproveView(w, tx))
if err != nil {
logger.Error(err.Error())
return "", err
}
logger.Infof("transaction committed: [%s]", tx.ID())
return res, err
}
type RegisterAuditorView struct{}
func (r *RegisterAuditorView) Call(context view.Context) (interface{}, error) {
return context.RunView(ttx.NewRegisterAuditorView(
&AuditView{},
))
}

View file

@ -0,0 +1,51 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package service
import (
"github.com/hyperledger-labs/fabric-smart-client/pkg/api"
"github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx"
"github.com/pkg/errors"
)
type TokenService struct {
FSC api.ServiceProvider
}
// SERVICE
type ValueByTokenType map[string]int64
// GetBalance returns the balances per token type of a wallet
func (s TokenService) GetBalance(wallet string, tokenType string) (typeVal ValueByTokenType, err error) {
typeVal = make(ValueByTokenType)
// get auditor wallet
w := ttx.MyAuditorWallet(s.FSC)
if w == nil {
err = errors.New("failed getting default auditor wallet")
logger.Error(err.Error())
return
}
auditor := ttx.NewAuditor(s.FSC, w)
aqe := auditor.NewQueryExecutor()
defer aqe.Done()
// TODO: how to get all TokenTypes separately?
filter, err := aqe.NewHoldingsFilter().ByEnrollmentId(wallet).ByType(tokenType).Execute()
if err != nil {
err = errors.Wrapf(err, "failed retrieving holding for [%s][%s]", wallet, tokenType)
logger.Error(err.Error())
return
}
currentHolding := filter.Sum()
typeVal[tokenType] = currentHolding.Int64()
logger.Debugf("Current Holding: [%s][%s][%d]", wallet, tokenType, typeVal[tokenType])
return
}

View file

@ -0,0 +1,105 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package service
import (
"time"
"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/services/ttxdb"
"github.com/pkg/errors"
)
// SERVICE
type TransactionHistoryItem struct {
// TxID is the transaction ID
TxID string
// ActionType is the type of action performed by this transaction record
ActionType int
// SenderEID is the enrollment ID of the account that is sending tokens
Sender string
// RecipientEID is the enrollment ID of the account that is receiving tokens
Recipient string
// TokenType is the type of token
TokenType string
// Amount is positive if tokens are received. Negative otherwise
Amount int64
// Timestamp is the time the transaction was submitted to the db
Timestamp time.Time
// Status is the status of the transaction
Status string
// Message is the user message sent with the transaction. It comes from
// the ApplicationMetadata and is sent in the transient field
Message string
}
// GetHistory returns the full transaction history for an auditor.
func (s TokenService) GetHistory(wallet string) (txs []TransactionHistoryItem, err error) {
// get auditor wallet
w := ttx.MyAuditorWallet(s.FSC)
if w == nil {
err = errors.New("failed getting default auditor wallet")
logger.Error(err.Error())
return txs, err
}
auditor := ttx.NewAuditor(s.FSC, w)
// Get query executor
aqe := auditor.NewQueryExecutor()
defer aqe.Done()
// This retrieves all transactions to *or* from the provided wallet.
// See QueryTransactionsParams interface for additional filters.
it, err := aqe.Transactions(ttxdb.QueryTransactionsParams{
SenderWallet: wallet,
RecipientWallet: wallet,
})
if err != nil {
return txs, errors.New("failed querying transactions")
}
defer it.Close()
// we need transaction info to get the transient field (application metadata)
tip := ttx.NewTransactionInfoProvider(s.FSC, token.GetManagementService(s.FSC))
if tip == nil {
return txs, errors.New("failed to get transactionInfoProvider")
}
// Return the list of audited transactions
for {
tx, err := it.Next()
if tx == nil {
break
}
transaction := TransactionHistoryItem{
TxID: tx.TxID,
ActionType: int(tx.ActionType),
Sender: tx.SenderEID,
Recipient: tx.RecipientEID,
TokenType: tx.TokenType,
Amount: tx.Amount.Int64(),
Timestamp: tx.Timestamp.UTC(),
Status: string(tx.Status),
}
if err != nil {
return txs, errors.New("failed iterating over transactions")
}
// set user provided message from transient field
ti, err := tip.TransactionInfo(transaction.TxID)
if err != nil {
return txs, err
}
if ti.ApplicationMetadata != nil && string(ti.ApplicationMetadata["message"]) != "" {
transaction.Message = string(ti.ApplicationMetadata["message"])
}
txs = append(txs, transaction)
}
return
}

BIN
token-sdk/components.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

23
token-sdk/compose-ca.yaml Normal file
View file

@ -0,0 +1,23 @@
# Copyright IBM Corp. All Rights Reserved.
#
# SPDX-License-Identifier: Apache-2.0
#
version: '3.7'
services:
ca_token_network:
image: hyperledger/fabric-ca:1.5.7
labels:
service: hyperledger-fabric
environment:
- FABRIC_CA_HOME=/etc/hyperledger/fabric-ca-server
- FABRIC_CA_SERVER_CA_NAME=ca-token-network
- FABRIC_CA_SERVER_TLS_ENABLED=false
- FABRIC_CA_SERVER_PORT=27054
ports:
- "27054:27054"
command: sh -c 'fabric-ca-server start -b admin:adminpw --idemix.curve gurvy.Bn254 -d'
volumes:
- ${PWD}/keys/ca:/etc/hyperledger/fabric-ca-server
container_name: ca_token_network

BIN
token-sdk/dependencies.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View file

@ -0,0 +1,101 @@
version: '3.7'
# fabric_test is the name of the fabric-samples test network.
# By connecting to it, we can reach the peers at their DNS names
# (e.g. peer0.org1.example.com).
networks:
test:
name: fabric_test
external: true
services:
auditor:
hostname: auditor.example.com
restart: always
build:
context: ./auditor
dockerfile: ../Dockerfile
volumes:
- ./data/auditor:/var/fsc/data/auditor
- ./auditor/conf:/conf:ro
- ./keys:/var/fsc/keys:ro
ports:
- 9000:9000
expose:
- 9001
networks:
- test
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/api/v1/readyz"]
interval: "5s"
timeout: "1s"
retries: 20
issuer:
hostname: issuer.example.com
restart: always
build:
context: ./issuer
dockerfile: ../Dockerfile
volumes:
- ./data/issuer:/var/fsc/data/issuer
- ./issuer/conf:/conf:ro
- ./keys:/var/fsc/keys:ro
ports:
- 9100:9000
expose:
- 9101
networks:
- test
depends_on:
auditor:
condition: service_healthy
owner1:
hostname: owner1.example.com
restart: always
build:
context: ./owner
dockerfile: ../Dockerfile
volumes:
- ./data/owner1:/var/fsc/data/owner1
- ./owner/conf/owner1:/conf:ro
- ./keys:/var/fsc/keys:ro
ports:
- 9200:9000
expose:
- 9201
networks:
- test
depends_on:
auditor:
condition: service_healthy
owner2:
hostname: owner2.example.com
restart: always
build:
context: ./owner
dockerfile: ../Dockerfile
volumes:
- ./data/owner2:/var/fsc/data/owner2
- ./owner/conf/owner2:/conf:ro
- ./keys:/var/fsc/keys:ro
ports:
- 9300:9000
expose:
- 9301
networks:
- test
depends_on:
auditor:
condition: service_healthy
swagger-ui:
image: swaggerapi/swagger-ui
ports:
- '8080:8080'
environment:
- URL=/swagger.yaml
volumes:
- ./swagger.yaml:/usr/share/nginx/html/swagger.yaml

1591
token-sdk/e2e/client.gen.go Normal file

File diff suppressed because it is too large Load diff

249
token-sdk/e2e/e2e_test.go Normal file
View file

@ -0,0 +1,249 @@
package e2e
import (
"context"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
var auditor *ClientWithResponses
var issuer *ClientWithResponses
var err error
var CODE string = "TEST"
var alice = Counterparty{
Account: "alice",
Node: "owner1",
}
var bob = Counterparty{
Account: "bob",
Node: "owner1",
}
var dan = Counterparty{
Account: "dan",
Node: "owner2",
}
type ownerAPI struct {
client *ClientWithResponses
}
var owner1 ownerAPI
var owner2 ownerAPI
func TestMain(t *testing.T) {
auditor, err = NewClientWithResponses(getEnv("AUDITOR_URL", "http://localhost:9000/api/v1"))
assert.NoError(t, err, "failed creating client")
issuer, err = NewClientWithResponses(getEnv("ISSUER_URL", "http://localhost:9100/api/v1"))
assert.NoError(t, err, "failed creating client")
client1, err := NewClientWithResponses(getEnv("OWNER1_URL", "http://localhost:9200/api/v1"))
assert.NoError(t, err, "failed creating client")
owner1 = ownerAPI{client: client1}
client2, err := NewClientWithResponses(getEnv("OWNER2_URL", "http://localhost:9300/api/v1"))
assert.NoError(t, err, "failed creating client")
owner2 = ownerAPI{client: client2}
// we have to issue funds to alice first to be able to do the other tests
testIssuance(t)
}
func testIssuance(t *testing.T) {
accBefore := owner1.getAccounts(t)
txBefore := owner1.getTransactions(t, "alice")
id := issue(t, alice, 1000)
acc2 := owner1.getAccounts(t)
txAfter := owner1.getTransactions(t, "alice")
assert.Equal(t, len(txBefore)+1, len(txAfter), "should have 1 issue transaction more", txAfter)
assert.Equal(t, getValue(t, accBefore, "alice")+1000, getValue(t, acc2, "alice"), acc2)
lastTx := txAfter[len(txAfter)-1]
assert.Equal(t, id, lastTx.Id)
assert.Equal(t, int64(1000), lastTx.Amount.Value)
assert.Equal(t, "alice", lastTx.Recipient)
}
func TestTransfer(t *testing.T) {
accBefore := owner1.getAccounts(t)
txBefore := owner1.getTransactions(t, "alice")
id := owner1.transfer(t, "alice", bob, 100)
accAfter := owner1.getAccounts(t)
txAfter := owner1.getTransactions(t, "alice")
assert.Equal(t, getValue(t, accBefore, "alice")-100, getValue(t, accAfter, "alice"), accAfter)
assert.Equal(t, getValue(t, accBefore, "bob")+100, getValue(t, accAfter, "bob"), accAfter)
assert.Greater(t, len(txAfter), len(txBefore))
// on the sender side there may be several transactions, so we check the recipient
txBob := owner1.getTransactions(t, "bob")
lastTx := txBob[len(txBob)-1]
assert.Equal(t, id, lastTx.Id, txBob)
assert.Equal(t, lastTx.Amount.Value, int64(100))
}
func TestTransferToSecondNode(t *testing.T) {
// current state
acc1Before := owner1.getAccounts(t)
tx1Before := owner1.getTransactions(t, "alice")
acc2Before := owner2.getAccounts(t)
tx2Before := owner2.getTransactions(t, "dan")
// transfer 100 from alice to dan
id := owner1.transfer(t, "alice", dan, 100)
// after: alice
acc1After := owner1.getAccounts(t)
assert.Equal(t, getValue(t, acc1Before, "alice")-100, getValue(t, acc1After, "alice"), acc1After) // -100 TEST
tx1After := owner1.getTransactions(t, "alice")
assert.Greater(t, len(tx1After), len(tx1Before)) // +1 tx
// after: dan
acc2After := owner2.getAccounts(t)
assert.Equal(t, getValue(t, acc2Before, "dan")+100, getValue(t, acc2After, "dan"), acc2After) // +100 TEST
tx2After := owner2.getTransactions(t, "dan")
assert.Greater(t, len(tx2After), len(tx2Before)) // + 1 tx
// on the sender side there may be several transactions, so we check the recipient
txDan := owner2.getTransactions(t, "dan")
lastTx := txDan[len(txDan)-1]
assert.Equal(t, id, lastTx.Id, txDan)
assert.Equal(t, lastTx.Amount.Value, int64(100))
owner2.testIfAuditorMatchesOwnerHistory(t, []string{"dan"})
}
func TestRedeem(t *testing.T) {
accBefore := owner1.getAccounts(t)
id := owner1.redeem(t, "alice", 10, "test redeem")
accAfter := owner1.getAccounts(t)
transactions := owner1.getTransactions(t, "alice")
assert.Equal(t, getValue(t, accBefore, "alice")-10, getValue(t, accAfter, "alice"), accAfter)
lastTx := transactions[len(transactions)-1]
assert.Equal(t, id, lastTx.Id, transactions)
assert.Equal(t, lastTx.Amount.Value, int64(10))
assert.Equal(t, "alice", lastTx.Sender)
assert.Equal(t, "test redeem", lastTx.Message)
}
func TestIfAuditorMatchesOwnerHistory(t *testing.T) {
owner1.testIfAuditorMatchesOwnerHistory(t, []string{"alice", "bob"})
owner2.testIfAuditorMatchesOwnerHistory(t, []string{"carlos", "dan"})
}
func issue(t *testing.T, counterparty Counterparty, value int64) string {
res, err := issuer.IssueWithResponse(context.TODO(), IssueJSONRequestBody{
Amount: Amount{
Code: CODE,
Value: value,
},
Counterparty: alice,
Message: new(string),
})
assert.NoError(t, err)
assert.Nil(t, res.JSONDefault)
assert.NotNil(t, res.JSON200)
t.Logf(res.JSON200.Message)
return res.JSON200.Payload
}
func getAuditorTransactions(t *testing.T, wallet string) []TransactionRecord {
res, err := auditor.AuditorTransactionsWithResponse(context.TODO(), wallet)
assert.NoError(t, err)
assert.Nil(t, res.JSONDefault)
assert.NotNil(t, res.JSON200)
t.Logf(res.JSON200.Message)
return res.JSON200.Payload
}
func (o *ownerAPI) testIfAuditorMatchesOwnerHistory(t *testing.T, accounts []string) {
for _, w := range accounts {
tx := o.getTransactions(t, w)
audittx := getAuditorTransactions(t, w)
assert.Equal(t, len(tx), len(audittx), w)
// Timestamp is the time of storing the tx in the database
// so it's not the same on both sides.
for i := 0; i < len(tx); i++ {
tx[i].Timestamp = time.Time{}
audittx[i].Timestamp = time.Time{}
}
assert.Equal(t, tx, audittx)
}
}
func (o *ownerAPI) getTransactions(t *testing.T, wallet string) []TransactionRecord {
res, err := o.client.OwnerTransactionsWithResponse(context.TODO(), wallet)
assert.NoError(t, err)
assert.Nil(t, res.JSONDefault)
assert.NotNil(t, res.JSON200)
t.Logf(res.JSON200.Message)
return res.JSON200.Payload
}
func (o *ownerAPI) transfer(t *testing.T, sender string, counterparty Counterparty, value int64) string {
res, err := o.client.TransferWithResponse(context.TODO(), sender, TransferJSONRequestBody{
Amount: Amount{
Code: CODE,
Value: value,
},
Counterparty: counterparty,
Message: new(string),
})
assert.NoError(t, err)
assert.Nil(t, res.JSONDefault)
assert.NotNil(t, res.JSON200)
t.Logf(res.JSON200.Message)
return res.JSON200.Payload
}
func (o *ownerAPI) redeem(t *testing.T, wallet string, value int64, message string) string {
res, err := o.client.RedeemWithResponse(context.TODO(), wallet, RedeemJSONRequestBody{
Amount: Amount{
Code: CODE,
Value: value,
},
Message: &message,
})
assert.NoError(t, err)
assert.Nil(t, res.JSONDefault)
assert.NotNil(t, res.JSON200)
t.Logf(res.JSON200.Message)
return res.JSON200.Payload
}
func (o *ownerAPI) getAccounts(t *testing.T) []Account {
res, err := o.client.OwnerAccountsWithResponse(context.TODO())
assert.NoError(t, err)
assert.Nil(t, res.JSONDefault)
assert.NotNil(t, res.JSON200)
t.Logf(res.JSON200.Message)
return res.JSON200.Payload
}
func getValue(t *testing.T, acc []Account, wallet string) int64 {
for _, a := range acc {
if a.Id == wallet {
for _, b := range a.Balance {
if b.Code == CODE {
return b.Value
}
}
}
}
t.Logf("%s value not found for wallet %s in %v", CODE, wallet, acc)
return 0
}
// getEnv returns an environment variable or the fallback
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}

3
token-sdk/e2e/go.mod Normal file
View file

@ -0,0 +1,3 @@
module github.com/hyperledger/fabric-samples/token-sdk/e2e
go 1.20

View file

@ -0,0 +1,5 @@
package: e2e
generate:
client: true
models: true
output: e2e/client.gen.go

4
token-sdk/explorer/.env Normal file
View file

@ -0,0 +1,4 @@
PORT=8081
EXPLORER_CONFIG_FILE_PATH=./config.json
EXPLORER_PROFILE_DIR_PATH=./connection-profile
FABRIC_CRYPTO_PATH=../../test-network/organizations

View file

@ -0,0 +1,9 @@
{
"network-configs": {
"test-network": {
"name": "Test Network",
"profile": "./connection-profile/test-network.json"
}
},
"license": "Apache-2.0"
}

View file

@ -0,0 +1,48 @@
{
"name": "test-network",
"version": "1.0.0",
"client": {
"tlsEnable": true,
"adminCredential": {
"id": "exploreradmin",
"password": "exploreradminpw"
},
"enableAuthentication": true,
"organization": "Org1MSP",
"connection": {
"timeout": {
"peer": {
"endorser": "300"
},
"orderer": "300"
}
}
},
"channels": {
"mychannel": {
"peers": {
"peer0.org1.example.com": {}
}
}
},
"organizations": {
"Org1MSP": {
"mspid": "Org1MSP",
"adminPrivateKey": {
"path": "/tmp/crypto/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/keystore/priv_sk"
},
"peers": ["peer0.org1.example.com"],
"signedCert": {
"path": "/tmp/crypto/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/signcerts/User1@org1.example.com-cert.pem"
}
}
},
"peers": {
"peer0.org1.example.com": {
"tlsCACerts": {
"path": "/tmp/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt"
},
"url": "grpcs://peer0.org1.example.com:7051"
}
}
}

View file

@ -0,0 +1,63 @@
# SPDX-License-Identifier: Apache-2.0
# see https://github.com/hyperledger-labs/blockchain-explorer
version: '2.1'
volumes:
pgdata:
walletstore:
networks:
test:
name: fabric_test
external: true
services:
explorerdb.mynetwork.com:
image: ghcr.io/hyperledger-labs/explorer-db:latest
container_name: explorerdb.mynetwork.com
hostname: explorerdb.mynetwork.com
environment:
- DATABASE_DATABASE=fabricexplorer
- DATABASE_USERNAME=hppoc
- DATABASE_PASSWORD=password
healthcheck:
test: "pg_isready -h localhost -p 5432 -q -U postgres"
interval: 30s
timeout: 10s
retries: 5
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- test
explorer.mynetwork.com:
image: ghcr.io/hyperledger-labs/explorer:latest
container_name: explorer.mynetwork.com
hostname: explorer.mynetwork.com
environment:
- DATABASE_HOST=explorerdb.mynetwork.com
- DATABASE_DATABASE=fabricexplorer
- DATABASE_USERNAME=hppoc
- DATABASE_PASSWD=password
- LOG_LEVEL_APP=info
- LOG_LEVEL_DB=info
- LOG_LEVEL_CONSOLE=debug
- LOG_CONSOLE_STDOUT=true
- DISCOVERY_AS_LOCALHOST=false
- PORT=${PORT:-8080}
volumes:
- ${EXPLORER_CONFIG_FILE_PATH}:/opt/explorer/app/platform/fabric/config.json
- ${EXPLORER_PROFILE_DIR_PATH}:/opt/explorer/app/platform/fabric/connection-profile
- ${FABRIC_CRYPTO_PATH}:/tmp/crypto
- walletstore:/opt/explorer/wallet
ports:
- ${PORT:-8080}:${PORT:-8080}
depends_on:
explorerdb.mynetwork.com:
condition: service_healthy
networks:
- test

6
token-sdk/go.work Normal file
View file

@ -0,0 +1,6 @@
go 1.20
use ./auditor
use ./issuer
use ./owner
use ./e2e

7
token-sdk/go.work.sum Normal file
View file

@ -0,0 +1,7 @@
github.com/bits-and-blooms/bitset v1.2.1 h1:M+/hrU9xlMp7t4TyTDQW97d3tRPVuKFC6zBEK16QnXY=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8=
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=

View file

@ -0,0 +1,116 @@
logging:
spec: info
format: '%{color}%{time:2006-01-02 15:04:05.000 MST} [%{module}] %{shortfunc} -> %{level:.4s} %{id:03x}%{color:reset} %{message}'
# ------------------- FSC Node Configuration -------------------------
# The FSC node is responsible for the peer to peer communication with other token services.
fsc:
identity:
cert:
file: /var/fsc/keys/issuer/fsc/msp/signcerts/cert.pem
key:
file: /var/fsc/keys/issuer/fsc/msp/keystore/priv_sk
tls:
enabled: false # TODO
p2p:
listenAddress: /ip4/0.0.0.0/tcp/9101
# 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: auditor
kvs: # key-value-store
persistence:
type: badger # badger or memory
opts:
path: /var/fsc/data/issuer/kvs
# 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: auditor
identity:
id: auditor
path: /var/fsc/keys/auditor/fsc/msp/signcerts/cert.pem
addresses:
P2P: auditor.example.com:9001
- name: owner1
identity:
id: owner1
path: /var/fsc/keys/owner1/fsc/msp/signcerts/cert.pem
addresses:
P2P: owner1.example.com:9201
aliases:
- owner1
- name: owner2
identity:
id: owner2
path: /var/fsc/keys/owner2/fsc/msp/signcerts/cert.pem
addresses:
P2P: owner2.example.com:9301
aliases:
- owner2
# ------------------- Fabric Configuration -------------------------
fabric:
enabled: true
mynetwork:
default: true
mspConfigPath: /var/fsc/keys/fabric/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp
defaultMSP: Org1MSP
msps:
- id: Org1MSP
mspType: bccsp
mspID: Org1MSP
path: /var/fsc/keys/fabric/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp
tls:
enabled: true
# If the keepalive values are too low, Fabric peers will complain with: ENHANCE_YOUR_CALM, debug data: "too_many_pings"
keepalive:
interval: 300s
timeout: 600s
# List of orderer nodes this node can connect to. There must be at least one orderer node. Others are discovered.
orderers:
- address: orderer.example.com:7050
connectionTimeout: 10s
tlsEnabled: true
tlsRootCertFile: /var/fsc/keys/fabric/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/ca.crt
serverNameOverride: orderer.example.com
# List of trusted peers this node can connect to. There must be at least one trusted peer. Others are discovered.
peers:
- address: peer0.org1.example.com:7051
connectionTimeout: 10s
tlsEnabled: true
tlsRootCertFile: /var/fsc/keys/fabric/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
serverNameOverride: peer0.org1.example.com
# Channel where the token chaincode is deployed
channels:
- name: mychannel
default: true
# Configuration of the vault used to store the RW sets assembled by this node
vault:
persistence:
type: badger
opts:
path: /var/fsc/data/issuer/vault
# ------------------- Token SDK Configuration -------------------------
token:
enabled: true
tms:
mytms: # unique name of this token management system
network: mynetwork # the name of the fabric network as configured above
channel: mychannel # the name of the network's channel this TMS refers to, if applicable
namespace: tokenchaincode # chaincode name
driver: zkatdlog # privacy preserving driver (zero knowledge asset transfer)
wallets:
issuers:
- id: issuer # the unique identifier of this wallet. Here is an example of use: `ttx.GetIssuerWallet(context, "issuer)`
default: true # is this the default issuer wallet
path: /var/fsc/keys/issuer/iss/msp
# Internal database to keep track of token transactions.
# It is used by auditors and token owners to track history
ttxdb:
persistence:
type: badger
opts:
path: /var/fsc/data/issuer/txdb

245
token-sdk/issuer/go.mod Normal file
View file

@ -0,0 +1,245 @@
module github.com/hyperledger/fabric-samples/token-sdk/issuer
go 1.20
replace github.com/ugorji/go v1.1.4 => github.com/ugorji/go/codec v1.2.9
require (
github.com/deepmap/oapi-codegen v1.15.0
github.com/getkin/kin-openapi v0.120.0
github.com/hyperledger-labs/fabric-smart-client v0.3.0
github.com/hyperledger-labs/fabric-token-sdk v0.3.0
github.com/labstack/echo/v4 v4.11.1
github.com/pkg/errors v0.9.1
)
require (
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
github.com/CloudyKit/jet/v6 v6.2.0 // indirect
github.com/IBM/idemix v0.0.2-0.20230831093709-b7a940638990 // indirect
github.com/IBM/idemix/bccsp/schemes/aries v0.0.0-20230831093709-b7a940638990 // indirect
github.com/IBM/idemix/bccsp/schemes/weak-bb v0.0.0-20230831093709-b7a940638990 // indirect
github.com/IBM/idemix/bccsp/types v0.0.0-20230831093709-b7a940638990 // indirect
github.com/IBM/mathlib v0.0.3-0.20230831091907-c532c4d3b65c // indirect
github.com/Joker/jade v1.1.3 // indirect
github.com/ReneKroon/ttlcache/v2 v2.11.0 // indirect
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect
github.com/ale-linux/aries-framework-go/component/kmscrypto v0.0.0-20230817163708-4b3de6d91874 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/consensys/bavard v0.1.13 // indirect
github.com/consensys/gnark-crypto v0.9.1 // indirect
github.com/containerd/cgroups v1.1.0 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/dgraph-io/badger/v3 v3.2103.2 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/elastic/gosigar v0.14.2 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/flosch/pongo2/v4 v4.0.2 // indirect
github.com/flynn/noise v1.0.0 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/go-kit/kit v0.10.0 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 // indirect
github.com/google/flatbuffers v1.12.1 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-uuid v1.0.2 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huin/goupnp v1.2.0 // indirect
github.com/hyperledger-labs/orion-sdk-go v0.2.5 // indirect
github.com/hyperledger-labs/orion-server v0.2.5 // indirect
github.com/hyperledger-labs/weaver-dlt-interoperability/common/protos-go v1.2.3-alpha.1 // indirect
github.com/hyperledger-labs/weaver-dlt-interoperability/sdks/fabric/go-sdk v1.2.3-alpha.1.0.20210812140206-37f430515b8c // indirect
github.com/hyperledger/fabric v1.4.0-rc1.0.20230401164317-bd8e24856939 // indirect
github.com/hyperledger/fabric-amcl v0.0.0-20230602173724-9e02669dceb2 // indirect
github.com/hyperledger/fabric-chaincode-go v0.0.0-20220920210243-7bc6fa0dd58b // indirect
github.com/hyperledger/fabric-lib-go v1.0.0 // indirect
github.com/hyperledger/fabric-private-chaincode v0.0.0-20210907122433-d56466264e4d // indirect
github.com/hyperledger/fabric-protos-go v0.2.0 // indirect
github.com/invopop/yaml v0.2.0 // indirect
github.com/ipfs/boxo v0.8.0-rc1 // indirect
github.com/ipfs/go-cid v0.4.1 // indirect
github.com/ipfs/go-datastore v0.6.0 // indirect
github.com/ipfs/go-ipfs-util v0.0.2 // indirect
github.com/ipfs/go-log v1.0.5 // indirect
github.com/ipfs/go-log/v2 v2.5.1 // indirect
github.com/ipld/go-ipld-prime v0.20.0 // indirect
github.com/iris-contrib/schema v0.0.6 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
github.com/jbenet/goprocess v0.1.4 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kataras/blocks v0.0.7 // indirect
github.com/kataras/golog v0.1.9 // indirect
github.com/kataras/iris/v12 v12.2.6-0.20230908161203-24ba4e8933b9 // indirect
github.com/kataras/pio v0.0.12 // indirect
github.com/kataras/sitemap v0.0.6 // indirect
github.com/kataras/tunnel v0.0.4 // indirect
github.com/kilic/bls12-381 v0.1.0 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/koron/go-ssdp v0.0.4 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/libp2p/go-cidranger v1.1.0 // indirect
github.com/libp2p/go-flow-metrics v0.1.0 // indirect
github.com/libp2p/go-libp2p v0.31.0 // indirect
github.com/libp2p/go-libp2p-asn-util v0.3.0 // indirect
github.com/libp2p/go-libp2p-kad-dht v0.22.0 // indirect
github.com/libp2p/go-libp2p-kbucket v0.5.0 // indirect
github.com/libp2p/go-libp2p-record v0.2.0 // indirect
github.com/libp2p/go-msgio v0.3.0 // indirect
github.com/libp2p/go-nat v0.2.0 // indirect
github.com/libp2p/go-netroute v0.2.1 // indirect
github.com/libp2p/go-reuseport v0.4.0 // indirect
github.com/libp2p/go-yamux/v4 v4.0.1 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mailgun/raymond/v2 v2.0.48 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/microcosm-cc/bluemonday v1.0.25 // indirect
github.com/miekg/dns v1.1.55 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/mmcloughlin/addchain v0.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/multiformats/go-base32 v0.1.0 // indirect
github.com/multiformats/go-base36 v0.2.0 // indirect
github.com/multiformats/go-multiaddr v0.11.0 // indirect
github.com/multiformats/go-multiaddr-dns v0.3.1 // indirect
github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect
github.com/multiformats/go-multibase v0.2.0 // indirect
github.com/multiformats/go-multicodec v0.9.0 // indirect
github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-multistream v0.4.1 // indirect
github.com/multiformats/go-varint v0.0.7 // indirect
github.com/onsi/ginkgo/v2 v2.11.0 // indirect
github.com/opencontainers/runtime-spec v1.1.0 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/polydawn/refmt v0.89.0 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.3.3 // indirect
github.com/quic-go/quic-go v0.38.1 // indirect
github.com/quic-go/webtransport-go v0.5.3 // indirect
github.com/raulk/go-watchdog v1.3.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/schollz/closestmatch v2.1.0+incompatible // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.10.1 // indirect
github.com/stretchr/testify v1.8.4 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/sykesm/zap-logfmt v0.0.4 // indirect
github.com/tdewolff/minify/v2 v2.12.9 // indirect
github.com/tdewolff/parse/v2 v2.6.8 // indirect
github.com/test-go/testify v1.1.4 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect
github.com/yosssi/ace v0.0.5 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/otel v1.14.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.13.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.13.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.13.0 // indirect
go.opentelemetry.io/otel/sdk v1.13.0 // indirect
go.opentelemetry.io/otel/trace v1.14.0 // indirect
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/dig v1.17.0 // indirect
go.uber.org/fx v1.20.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.25.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
google.golang.org/grpc v1.53.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.2.1 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
)

1978
token-sdk/issuer/go.sum Normal file

File diff suppressed because it is too large Load diff

108
token-sdk/issuer/main.go Normal file
View file

@ -0,0 +1,108 @@
package main
import (
"net/http"
"os"
"os/signal"
"syscall"
"github.com/hyperledger/fabric-samples/token-sdk/issuer/routes"
"github.com/hyperledger/fabric-samples/token-sdk/issuer/service"
"github.com/hyperledger-labs/fabric-smart-client/pkg/api"
"github.com/hyperledger-labs/fabric-smart-client/pkg/node"
fabric "github.com/hyperledger-labs/fabric-smart-client/platform/fabric/sdk"
"github.com/hyperledger-labs/fabric-smart-client/platform/view/services/flogging"
tokensdk "github.com/hyperledger-labs/fabric-token-sdk/token/sdk"
)
var logger = flogging.MustGetLogger("main")
func main() {
dir := getEnv("CONF_DIR", "./conf")
port := getEnv("PORT", "9100")
fsc := startFabricSmartClient(dir)
controller := routes.Controller{Service: service.TokenService{FSC: fsc}}
err := routes.StartWebServer(port, controller, logger)
if err != nil {
if err == http.ErrServerClosed {
logger.Infof("Webserver closing, exiting...", err.Error())
fsc.Stop()
} else {
logger.Fatalf("echo error - %s", err.Error())
fsc.Stop()
os.Exit(1)
}
}
}
type Node interface {
api.ServiceProvider
Stop()
}
func startFabricSmartClient(confDir string) Node {
logger.Infof("Initializing Fabric Smart Client and Token SDK...")
fsc := node.NewFromConfPath(confDir)
succeedOrPanic(fsc.InstallSDK(fabric.NewSDK(fsc)))
succeedOrPanic(fsc.InstallSDK(tokensdk.NewSDK(fsc)))
succeedOrPanic(fsc.Start())
// Stop gracefully
go handleSignals((map[os.Signal]func(){
syscall.SIGINT: func() {
logger.Info("Stopping FSC node...")
fsc.Stop()
os.Exit(130)
},
syscall.SIGTERM: func() {
logger.Info("Stopping FSC node...")
fsc.Stop()
os.Exit(143)
},
syscall.SIGSTOP: func() {
logger.Info("Stopping FSC node...")
fsc.Stop()
os.Exit(145)
},
syscall.SIGHUP: func() {
logger.Info("Stopping FSC node...")
fsc.Stop()
os.Exit(129)
},
}))
logger.Infof("FSC node is ready!")
return fsc
}
// getEnv returns an environment variable or the fallback
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
func succeedOrPanic(err error) {
if err != nil {
logger.Fatalf("Failed initializing Token SDK - %s", err.Error())
os.Exit(1)
}
}
func handleSignals(handlers map[os.Signal]func()) {
var signals []os.Signal
for sig := range handlers {
signals = append(signals, sig)
}
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, signals...)
for sig := range signalChan {
logger.Infof("Received signal: %d (%s)", sig, sig)
handlers[sig]()
}
}

View file

@ -0,0 +1,11 @@
package: routes
generate:
echo-server: true
strict-server: true
models: true
embedded-spec: true
output-options:
include-tags:
- operations
- issuer
output: issuer/routes/routes.gen.go

View file

@ -0,0 +1,31 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package routes
import (
"context"
)
// (GET /readyz)
func (c Controller) Readyz(ctx context.Context, request ReadyzRequestObject) (ReadyzResponseObject, error) {
// TODO: what defines readiness if the REST API is available after FSC?
return Readyz200JSONResponse{
HealthSuccessJSONResponse: HealthSuccessJSONResponse{
Message: "ok",
},
}, nil
}
// (GET /healthz)
func (c Controller) Healthz(ctx context.Context, request HealthzRequestObject) (HealthzResponseObject, error) {
// TODO: how to determine health?
return Healthz200JSONResponse{
HealthSuccessJSONResponse: HealthSuccessJSONResponse{
Message: "ok",
},
}, nil
}

View file

@ -0,0 +1,448 @@
// Package routes provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/deepmap/oapi-codegen version v1.13.4 DO NOT EDIT.
package routes
import (
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"path"
"strings"
"github.com/deepmap/oapi-codegen/pkg/runtime"
"github.com/getkin/kin-openapi/openapi3"
"github.com/labstack/echo/v4"
)
// Amount The amount to issue, transfer or redeem.
type Amount struct {
// Code the code of the token
Code string `json:"code"`
// Value value in base units (usually cents)
Value int64 `json:"value"`
}
// Counterparty The counterparty in a Transfer or Issuance transaction.
type Counterparty struct {
Account string `json:"account"`
// Node The node that holds the recipient account
Node string `json:"node"`
}
// Error defines model for Error.
type Error struct {
// Message High level error message
Message string `json:"message"`
// Payload Details about the error
Payload string `json:"payload"`
}
// TransferRequest Instructions to issue or transfer tokens to an account
type TransferRequest struct {
// Amount The amount to issue, transfer or redeem.
Amount Amount `json:"amount"`
// Counterparty The counterparty in a Transfer or Issuance transaction.
Counterparty Counterparty `json:"counterparty"`
// Message optional message that will be sent and stored with the transfer transaction
Message *string `json:"message,omitempty"`
}
// ErrorResponse defines model for ErrorResponse.
type ErrorResponse = Error
// HealthSuccess defines model for HealthSuccess.
type HealthSuccess struct {
// Message ok
Message string `json:"message"`
}
// IssueSuccess defines model for IssueSuccess.
type IssueSuccess struct {
Message string `json:"message"`
// Payload Transaction id
Payload string `json:"payload"`
}
// IssueJSONRequestBody defines body for Issue for application/json ContentType.
type IssueJSONRequestBody = TransferRequest
// ServerInterface represents all server handlers.
type ServerInterface interface {
// (GET /healthz)
Healthz(ctx echo.Context) error
// Issue tokens to an account
// (POST /issuer/issue)
Issue(ctx echo.Context) error
// (GET /readyz)
Readyz(ctx echo.Context) error
}
// ServerInterfaceWrapper converts echo contexts to parameters.
type ServerInterfaceWrapper struct {
Handler ServerInterface
}
// Healthz converts echo context to params.
func (w *ServerInterfaceWrapper) Healthz(ctx echo.Context) error {
var err error
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.Healthz(ctx)
return err
}
// Issue converts echo context to params.
func (w *ServerInterfaceWrapper) Issue(ctx echo.Context) error {
var err error
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.Issue(ctx)
return err
}
// Readyz converts echo context to params.
func (w *ServerInterfaceWrapper) Readyz(ctx echo.Context) error {
var err error
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.Readyz(ctx)
return err
}
// This is a simple interface which specifies echo.Route addition functions which
// are present on both echo.Echo and echo.Group, since we want to allow using
// either of them for path registration
type EchoRouter interface {
CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
}
// RegisterHandlers adds each server route to the EchoRouter.
func RegisterHandlers(router EchoRouter, si ServerInterface) {
RegisterHandlersWithBaseURL(router, si, "")
}
// Registers handlers, and prepends BaseURL to the paths, so that the paths
// can be served under a prefix.
func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) {
wrapper := ServerInterfaceWrapper{
Handler: si,
}
router.GET(baseURL+"/healthz", wrapper.Healthz)
router.POST(baseURL+"/issuer/issue", wrapper.Issue)
router.GET(baseURL+"/readyz", wrapper.Readyz)
}
type ErrorResponseJSONResponse Error
type HealthSuccessJSONResponse struct {
// Message ok
Message string `json:"message"`
}
type IssueSuccessJSONResponse struct {
Message string `json:"message"`
// Payload Transaction id
Payload string `json:"payload"`
}
type HealthzRequestObject struct {
}
type HealthzResponseObject interface {
VisitHealthzResponse(w http.ResponseWriter) error
}
type Healthz200JSONResponse struct{ HealthSuccessJSONResponse }
func (response Healthz200JSONResponse) VisitHealthzResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type Healthz503JSONResponse struct{ ErrorResponseJSONResponse }
func (response Healthz503JSONResponse) VisitHealthzResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(503)
return json.NewEncoder(w).Encode(response)
}
type IssueRequestObject struct {
Body *IssueJSONRequestBody
}
type IssueResponseObject interface {
VisitIssueResponse(w http.ResponseWriter) error
}
type Issue200JSONResponse struct{ IssueSuccessJSONResponse }
func (response Issue200JSONResponse) VisitIssueResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type IssuedefaultJSONResponse struct {
Body Error
StatusCode int
}
func (response IssuedefaultJSONResponse) VisitIssueResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(response.StatusCode)
return json.NewEncoder(w).Encode(response.Body)
}
type ReadyzRequestObject struct {
}
type ReadyzResponseObject interface {
VisitReadyzResponse(w http.ResponseWriter) error
}
type Readyz200JSONResponse struct{ HealthSuccessJSONResponse }
func (response Readyz200JSONResponse) VisitReadyzResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type Readyz503JSONResponse struct{ ErrorResponseJSONResponse }
func (response Readyz503JSONResponse) VisitReadyzResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(503)
return json.NewEncoder(w).Encode(response)
}
// StrictServerInterface represents all server handlers.
type StrictServerInterface interface {
// (GET /healthz)
Healthz(ctx context.Context, request HealthzRequestObject) (HealthzResponseObject, error)
// Issue tokens to an account
// (POST /issuer/issue)
Issue(ctx context.Context, request IssueRequestObject) (IssueResponseObject, error)
// (GET /readyz)
Readyz(ctx context.Context, request ReadyzRequestObject) (ReadyzResponseObject, error)
}
type StrictHandlerFunc = runtime.StrictEchoHandlerFunc
type StrictMiddlewareFunc = runtime.StrictEchoMiddlewareFunc
func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface {
return &strictHandler{ssi: ssi, middlewares: middlewares}
}
type strictHandler struct {
ssi StrictServerInterface
middlewares []StrictMiddlewareFunc
}
// Healthz operation middleware
func (sh *strictHandler) Healthz(ctx echo.Context) error {
var request HealthzRequestObject
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
return sh.ssi.Healthz(ctx.Request().Context(), request.(HealthzRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "Healthz")
}
response, err := handler(ctx, request)
if err != nil {
return err
} else if validResponse, ok := response.(HealthzResponseObject); ok {
return validResponse.VisitHealthzResponse(ctx.Response())
} else if response != nil {
return fmt.Errorf("Unexpected response type: %T", response)
}
return nil
}
// Issue operation middleware
func (sh *strictHandler) Issue(ctx echo.Context) error {
var request IssueRequestObject
var body IssueJSONRequestBody
if err := ctx.Bind(&body); err != nil {
return err
}
request.Body = &body
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
return sh.ssi.Issue(ctx.Request().Context(), request.(IssueRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "Issue")
}
response, err := handler(ctx, request)
if err != nil {
return err
} else if validResponse, ok := response.(IssueResponseObject); ok {
return validResponse.VisitIssueResponse(ctx.Response())
} else if response != nil {
return fmt.Errorf("Unexpected response type: %T", response)
}
return nil
}
// Readyz operation middleware
func (sh *strictHandler) Readyz(ctx echo.Context) error {
var request ReadyzRequestObject
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
return sh.ssi.Readyz(ctx.Request().Context(), request.(ReadyzRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "Readyz")
}
response, err := handler(ctx, request)
if err != nil {
return err
} else if validResponse, ok := response.(ReadyzResponseObject); ok {
return validResponse.VisitReadyzResponse(ctx.Response())
} else if response != nil {
return fmt.Errorf("Unexpected response type: %T", response)
}
return nil
}
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"H4sIAAAAAAAC/9RWQW/jNhP9K8R832EXICIlaQtUt912gQS9FNkUKLrNYUyNLW4oUktSTt1A/70gKcu2",
"LDvxNkCzJ1sSOXxv5s3jPIIwdWM0ae+geARLrjHaUXz4YK2xN/2b8EIY7Un78BebRkmBXhqdfXZGh3dO",
"VFRj+Pd/S3Mo4H/ZJnqWvrosRoWu6ziU5ISVTQgCRTqOrRFAx+GKUPnqYysEOXcSAPoL60ZF0DU5hwuC",
"Asx9CNpY05D1MnEcvj6O0Jh74OBXTdjovJV6AQGypS+ttFRC8WnYezcsNLPPJPwUuZ7EDr1r51r6GnYH",
"KYzwcmhwpQyW+/RuLWqHIjwxWT6b6ibiKaSNZTQqbsd7PpHCu9q0ifgIZUUM4zfmDZMhX5z5AH1OlsWA",
"JVF9Bny74sKUAdeH325+Bw5LVC1BcZ7ne8VPC8Ohc2yV3+zZReErYmEpM3MW/ntzT3o/ZcNRYxbxNZOa",
"zdARa7X0jr1pXYtKrZgIzfEWOMyNrTFgkNr/8B1wqKWWdVtDkQ9HSe1pQXavPJHI+vz9ynD4KeSQbIPW",
"r6bTLLZWBKzIbrfyHKSKWlBKftLNKOsoRCoioJIiwNGpDuZBkz3fb71hw4Ru9VCZMc7whfkKPauMKl0s",
"iCUhG0nas3XMp/SsU8LWy6dSlnzqkJckPe+3RQEneMyVXFRM0ZIUG8d7fif/TB6lcgxnpvUxHTHWi7Q0",
"h7UIbuhLS26iRa+187aNgnBDkwbJDG0a2yV+Q71VoJEYBgc4dnf0PtFxECNBH9u1I/6OH/H8+AfVug5J",
"Zw9SKTYj5qLAdMmcN5ZK9iB9lfxgYLppjifTv0OAr/lPu6rUczOR+WSHoQ22TDEATK7YJ/6MvUdxTyWb",
"rRiyUgY8s9ZTyRSVC7L8T91YcmSXUi9YY+USxYq1Ljz9QdawX7R5iEvZr9aYuQt976UPHQG38YhgPWRd",
"gnV+lockm4Y0NhIKuDzLzy6jzHwVa51hW0pvbNaLwWWPsuziFUc2BILi05hsvwU4tFZBAZX3TZFlyghU",
"lXG++DHP8wwbmS3PM+juOn7gmGyrSO7lz6ziyPJ3CLygqOeg8niRXwd3uOq/891R6yLPD6l4WJftjkMd",
"h+/zy6d37U5xQU4eF4HuBpmDgN21dY12BQXckG+tduwiz5lMd17UhyAmHUsUYydlsdtt+omTiXETpKNS",
"IemfnH9vytWLjZNjg5oYQ6Ytat7qct+VNi3qbUvd15RpZ6yLYPrp4vRKHdZmSvwRaZ7vSHO7uhHfIVde",
"a6OPf/cfgAi6ijPD0LhPtGkYN6LvzczsCJiLbTB8HEWgVcbFMCXqI2Eu93p+F+xzzOwVIs7SpfGKge86",
"VLzh3sxaq9/2MoJDzE5w/NdYmPXV/o2U5nZq7jO+CrPJVodbwnJ1+J68SZ+/4WsyEozshaDGM4FKuSdc",
"/cSJg/87Q+avTEN9wsfx3vU3Ny5RKpwpikkdMqWxpq3U7eOZ3D9kqt/ePz9zd2xT9oBKkXebIPH1RIyP",
"vSrS5NSP6FhKHQS62b3RWXfX/RMAAP//uoh3hJoTAAA=",
}
// GetSwagger returns the content of the embedded swagger specification file
// or error if failed to decode
func decodeSpec() ([]byte, error) {
zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, ""))
if err != nil {
return nil, fmt.Errorf("error base64 decoding spec: %w", err)
}
zr, err := gzip.NewReader(bytes.NewReader(zipped))
if err != nil {
return nil, fmt.Errorf("error decompressing spec: %w", err)
}
var buf bytes.Buffer
_, err = buf.ReadFrom(zr)
if err != nil {
return nil, fmt.Errorf("error decompressing spec: %w", err)
}
return buf.Bytes(), nil
}
var rawSpec = decodeSpecCached()
// a naive cached of a decoded swagger spec
func decodeSpecCached() func() ([]byte, error) {
data, err := decodeSpec()
return func() ([]byte, error) {
return data, err
}
}
// Constructs a synthetic filesystem for resolving external references when loading openapi specifications.
func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) {
res := make(map[string]func() ([]byte, error))
if len(pathToFile) > 0 {
res[pathToFile] = rawSpec
}
return res
}
// GetSwagger returns the Swagger specification corresponding to the generated code
// in this file. The external references of Swagger specification are resolved.
// The logic of resolving external references is tightly connected to "import-mapping" feature.
// Externally referenced files must be embedded in the corresponding golang packages.
// Urls can be supported but this task was out of the scope.
func GetSwagger() (swagger *openapi3.T, err error) {
resolvePath := PathToRawSpec("")
loader := openapi3.NewLoader()
loader.IsExternalRefsAllowed = true
loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) {
pathToFile := url.String()
pathToFile = path.Clean(pathToFile)
getSpec, ok := resolvePath[pathToFile]
if !ok {
err1 := fmt.Errorf("path not found: %s", pathToFile)
return nil, err1
}
return getSpec()
}
var specData []byte
specData, err = rawSpec()
if err != nil {
return
}
swagger, err = loader.LoadFromData(specData)
if err != nil {
return
}
return
}

View file

@ -0,0 +1,49 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package routes
import (
"context"
"fmt"
"github.com/hyperledger/fabric-samples/token-sdk/issuer/service"
)
type Controller struct {
Service service.TokenService
}
// Issue tokens to an account
// (POST /issue)
func (c Controller) Issue(ctx context.Context, request IssueRequestObject) (IssueResponseObject, error) {
code := request.Body.Amount.Code
value := uint64(request.Body.Amount.Value)
recipient := request.Body.Counterparty.Account
recipientNode := request.Body.Counterparty.Node
var message string
if request.Body.Message != nil {
message = *request.Body.Message
}
txID, err := c.Service.Issue(code, value, recipient, recipientNode, message)
if err != nil {
return IssuedefaultJSONResponse{
Body: Error{
Message: "can't issue tokens",
Payload: err.Error(),
},
StatusCode: 500,
}, nil
}
return Issue200JSONResponse{
IssueSuccessJSONResponse: IssueSuccessJSONResponse{
Message: fmt.Sprintf("issued %d %s to %s on %s", value, code, recipient, recipientNode),
Payload: txID,
},
}, nil
}

View file

@ -0,0 +1,65 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package routes
import (
"fmt"
"log"
"os"
oapimiddleware "github.com/deepmap/oapi-codegen/pkg/middleware"
"github.com/labstack/echo/v4"
middleware "github.com/labstack/echo/v4/middleware"
)
type Logger interface {
Infof(template string, args ...interface{})
Debugf(template string, args ...interface{})
Warnf(template string, args ...interface{})
Errorf(template string, args ...interface{})
Fatalf(template string, args ...interface{})
}
// Start web server on the main thread. It exits the application if it fails setting up.
func StartWebServer(port string, routesImplementation StrictServerInterface, logger Logger) error {
e := echo.New()
baseURL := "/api/v1"
handler := NewStrictHandler(routesImplementation, nil)
RegisterHandlersWithBaseURL(e, handler, baseURL)
// Request validator
swagger, err := GetSwagger()
if err != nil {
log.Fatalf("Error loading swagger spec\n: %s", err)
os.Exit(1)
}
swagger.Servers = nil
e.Group(baseURL).Use(oapimiddleware.OapiRequestValidator(swagger))
e.Use(middleware.CORS())
e.Use(middleware.RequestID())
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
Skipper: func(c echo.Context) bool {
return c.Path() == "/api/v1/healthz" || c.Path() == "/api/v1/readyz"
},
LogRequestID: true, LogMethod: true, LogURI: true, LogStatus: true, LogLatency: true,
LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
if v.Status < 400 {
logger.Infof("%d %s %s %s [%s]", v.Status, v.Method, v.URI, v.Latency.String(), v.RequestID)
} else if v.Status >= 400 && v.Status < 500 {
logger.Warnf("%d %s %s %s [%s]", v.Status, v.Method, v.URI, v.Latency.String(), v.RequestID)
} else {
logger.Errorf("%d %s %s %s [%s]", v.Status, v.Method, v.URI, v.Latency.String(), v.RequestID)
}
return nil
},
}))
// Start REST API server
return e.Start(fmt.Sprintf("0.0.0.0:%s", port))
}

View file

@ -0,0 +1,158 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package service
import (
"github.com/hyperledger-labs/fabric-smart-client/pkg/api"
viewregistry "github.com/hyperledger-labs/fabric-smart-client/platform/view"
"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/ttx"
"github.com/pkg/errors"
)
var logger = flogging.MustGetLogger("service")
// SERVICE
type TokenService struct {
FSC api.ServiceProvider
}
// Issue issues an amount of tokens to a wallet. It connects to the other node, prepares the transaction,
// gets it approved by the auditor and sends it to the blockchain for endorsement and commit.
func (s TokenService) Issue(tokenType string, quantity uint64, recipient string, recipientNode string, message string) (txID string, err error) {
logger.Infof("going to issue %d %s to [%s] on [%s] with message [%s]", quantity, tokenType, recipient, recipientNode, message)
res, err := viewregistry.GetManager(s.FSC).InitiateView(&IssueCashView{
IssueCash: &IssueCash{
TokenType: tokenType,
Quantity: quantity,
Recipient: recipient,
RecipientNode: recipientNode,
Message: message,
},
})
if err != nil {
logger.Errorf("error issuing: %s", err.Error())
return
}
txID, ok := res.(string)
if !ok {
return "", errors.New("cannot parse issue response")
}
logger.Infof("issued %d %s to [%s] on [%s] with message [%s]. ID: [%s]", quantity, tokenType, recipient, recipientNode, message, txID)
return
}
// VIEW
// IssueCash contains the input information to issue a token
type IssueCash struct {
// 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
// RecipientNode is the identifier of the node of the recipient
RecipientNode string
// Message is the message that will be visible to the recipient and the auditor
Message string
}
type IssueCashView struct {
*IssueCash
}
func (v *IssueCashView) Call(context view.Context) (interface{}, error) {
// Is the wallet on our node?
// tms := token.GetManagementService(context)
// if w := tms.WalletManager().OwnerWalletByIdentity(view.Identity(v.Recipient)); w != nil {
// logger.Infof("%s", v.Recipient)
// }
node := view.Identity(v.RecipientNode)
rec := view.Identity(v.Recipient)
eps := viewregistry.GetEndpointService(context)
if !eps.IsBoundTo(node, rec) {
logger.Infof("binding [%s] to node [%s]", v.Recipient, v.RecipientNode)
eps.Bind(node, rec) // TODO: it doesn't forget a wrong binding
}
// // Debug information
// epr, err := eps.Endpoint(rec)
// if err != nil {
// logger.Errorf(err.Error())
// }
// logger.Infof("recipient node: %s", epr["P2P"])
// 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.
logger.Infof("requesting [%s] identity from [%s]", v.Recipient, v.RecipientNode)
recipient, err := ttx.RequestRecipientIdentity(context, rec)
if err != nil {
return "", errors.Wrapf(err, "failed getting recipient identity from %s", v.RecipientNode)
}
// Prepare the transaction and specify the auditor that will approve it.
logger.Debug("getting identity of auditor")
auditor := viewregistry.GetIdentityProvider(context).Identity("auditor")
if auditor == nil {
return "", errors.New("auditor identity not found")
}
tx, err := ttx.NewTransaction(context, nil, ttx.WithAuditor(auditor))
if err != nil {
return "", errors.Wrap(err, "failed creating transaction")
}
// You can set any metadata you want. It is shared with the recipient and
// auditor but not committed to the ledger. We used 'message' here to let
// the user share messages that will be shown in the transaction history.
if v.Message != "" {
tx.SetApplicationMetadata("message", []byte(v.Message))
}
// Get issuer wallet
logger.Debug("loading issuer wallet")
wallet := ttx.MyIssuerWallet(context)
if wallet == nil {
return "", errors.Errorf("issuer wallet not found")
}
// The issuer adds a new issue operation to the transaction to issue
// the amount to the recipient id recieved from the owner's node.
err = tx.Issue(
wallet,
recipient,
v.TokenType,
v.Quantity,
)
if err != nil {
return "", errors.Wrap(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.
logger.Infof("collecting signatures and submitting transaction to chaincode: [%s]", tx.ID())
_, err = context.RunView(ttx.NewCollectEndorsementsView(tx))
if err != nil {
return "", errors.Wrap(err, "failed to sign transaction")
}
// Last but not least, the issuer sends the transaction for ordering and waits for transaction finality.
logger.Infof("submitting fabric transaction to orderer for final settlemement: [%s]", tx.ID())
_, err = context.RunView(ttx.NewOrderingAndFinalityView(tx))
if err != nil {
return "", errors.Wrap(err, "failed to order or commit transaction")
}
return tx.ID(), nil
}

View file

@ -0,0 +1,119 @@
logging:
spec: info
format: '%{color}%{time:2006-01-02 15:04:05.000 MST} [%{module}] %{shortfunc} -> %{level:.4s} %{id:03x}%{color:reset} %{message}'
# ------------------- FSC Node Configuration -------------------------
# The FSC node is responsible for the peer to peer communication with other token services.
fsc:
identity:
cert:
file: /var/fsc/keys/owner1/fsc/msp/signcerts/cert.pem
key:
file: /var/fsc/keys/owner1/fsc/msp/keystore/priv_sk
tls:
enabled: false # TODO
p2p:
listenAddress: /ip4/0.0.0.0/tcp/9201
# 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: auditor
kvs: # key-value-store
persistence:
type: badger # badger or memory
opts:
path: /var/fsc/data/owner1/kvs
# 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: auditor
identity:
id: auditor
path: /var/fsc/keys/auditor/fsc/msp/signcerts/cert.pem
addresses:
P2P: auditor.example.com:9001
- name: issuer
identity:
id: issuer
path: /var/fsc/keys/issuer/fsc/msp/signcerts/cert.pem
addresses:
P2P: issuer.example.com:9101
- name: owner2
identity:
id: owner2
path: /var/fsc/keys/owner2/fsc/msp/signcerts/cert.pem
addresses:
P2P: owner2.example.com:9201
aliases:
- owner2
# ------------------- Fabric Configuration -------------------------
fabric:
enabled: true
mynetwork:
default: true
mspConfigPath: /var/fsc/keys/fabric/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp
defaultMSP: Org1MSP
msps:
- id: Org1MSP
mspType: bccsp
mspID: Org1MSP
path: /var/fsc/keys/fabric/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp
tls:
enabled: true
# If the keepalive values are too low, Fabric peers will complain with: ENHANCE_YOUR_CALM, debug data: "too_many_pings"
keepalive:
interval: 300s
timeout: 600s
# List of orderer nodes this node can connect to. There must be at least one orderer node. Others are discovered.
orderers:
- address: orderer.example.com:7050
connectionTimeout: 10s
tlsEnabled: true
tlsRootCertFile: /var/fsc/keys/fabric/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/ca.crt
serverNameOverride: orderer.example.com
# List of trusted peers this node can connect to. There must be at least one trusted peer. Others are discovered.
peers:
- address: peer0.org1.example.com:7051
connectionTimeout: 10s
tlsEnabled: true
tlsRootCertFile: /var/fsc/keys/fabric/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
serverNameOverride: peer0.org1.example.com
# Channel where the token chaincode is deployed
channels:
- name: mychannel
default: true
# Configuration of the vault used to store the RW sets assembled by this node
vault:
persistence:
type: badger
opts:
path: /var/fsc/data/owner1/vault
# ------------------- Token SDK Configuration -------------------------
token:
enabled: true
tms:
mytms: # unique name of this token management system
network: mynetwork # the name of the fabric network as configured above
channel: mychannel # the name of the network's channel this TMS refers to, if applicable
namespace: tokenchaincode # chaincode name
driver: zkatdlog # privacy preserving driver (zero knowledge asset transfer)
wallets:
defaultCacheSize: 3 # how many idemix keys to pre-generate
owners:
- id: alice # the unique identifier of this wallet. Here is an example of use: `ttx.GetWallet(context, "alice")`
# default: true # is this the default owner wallet
path: /var/fsc/keys/owner1/wallet/alice/msp
- id: bob
path: /var/fsc/keys/owner1/wallet/bob/msp
# Internal database to keep track of token transactions.
# It is used by auditors and token owners to track history
ttxdb:
persistence:
# type can be badger (disk) or memory
type: badger
opts:
path: /var/fsc/data/owner1/txdb

View file

@ -0,0 +1,119 @@
logging:
spec: info
format: '%{color}%{time:2006-01-02 15:04:05.000 MST} [%{module}] %{shortfunc} -> %{level:.4s} %{id:03x}%{color:reset} %{message}'
# ------------------- FSC Node Configuration -------------------------
# The FSC node is responsible for the peer to peer communication with other token services.
fsc:
identity:
cert:
file: /var/fsc/keys/owner2/fsc/msp/signcerts/cert.pem
key:
file: /var/fsc/keys/owner2/fsc/msp/keystore/priv_sk
tls:
enabled: false # TODO
p2p:
listenAddress: /ip4/0.0.0.0/tcp/9301
# 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: auditor
kvs: # key-value-store
persistence:
type: badger # badger or memory
opts:
path: /var/fsc/data/owner2/kvs
# 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: auditor
identity:
id: auditor
path: /var/fsc/keys/auditor/fsc/msp/signcerts/cert.pem
addresses:
P2P: auditor.example.com:9001
- name: issuer
identity:
id: issuer
path: /var/fsc/keys/issuer/fsc/msp/signcerts/cert.pem
addresses:
P2P: issuer.example.com:9101
- name: owner1
identity:
id: owner1
path: /var/fsc/keys/owner1/fsc/msp/signcerts/cert.pem
addresses:
P2P: owner1.example.com:9201
aliases:
- owner1
# ------------------- Fabric Configuration -------------------------
fabric:
enabled: true
mynetwork:
default: true
mspConfigPath: /var/fsc/keys/fabric/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp
defaultMSP: Org1MSP
msps:
- id: Org1MSP
mspType: bccsp
mspID: Org1MSP
path: /var/fsc/keys/fabric/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp
tls:
enabled: true
# If the keepalive values are too low, Fabric peers will complain with: ENHANCE_YOUR_CALM, debug data: "too_many_pings"
keepalive:
interval: 300s
timeout: 600s
# List of orderer nodes this node can connect to. There must be at least one orderer node. Others are discovered.
orderers:
- address: orderer.example.com:7050
connectionTimeout: 10s
tlsEnabled: true
tlsRootCertFile: /var/fsc/keys/fabric/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/ca.crt
serverNameOverride: orderer.example.com
# List of trusted peers this node can connect to. There must be at least one trusted peer. Others are discovered.
peers:
- address: peer0.org1.example.com:7051
connectionTimeout: 10s
tlsEnabled: true
tlsRootCertFile: /var/fsc/keys/fabric/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
serverNameOverride: peer0.org1.example.com
# Channel where the token chaincode is deployed
channels:
- name: mychannel
default: true
# Configuration of the vault used to store the RW sets assembled by this node
vault:
persistence:
type: badger
opts:
path: /var/fsc/data/owner2/vault
# ------------------- Token SDK Configuration -------------------------
token:
enabled: true
tms:
mytms: # unique name of this token management system
network: mynetwork # the name of the fabric network as configured above
channel: mychannel # the name of the network's channel this TMS refers to, if applicable
namespace: tokenchaincode # chaincode name
driver: zkatdlog # privacy preserving driver (zero knowledge asset transfer)
wallets:
defaultCacheSize: 3 # how many idemix keys to pre-generate
owners:
- id: carlos # the unique identifier of this wallet. Here is an example of use: `ttx.GetWallet(context, "alice")`
# default: true # is this the default owner wallet
path: /var/fsc/keys/owner2/wallet/carlos/msp
- id: dan
path: /var/fsc/keys/owner2/wallet/dan/msp
# Internal database to keep track of token transactions.
# It is used by auditors and token owners to track history
ttxdb:
persistence:
# type can be badger (disk) or memory
type: badger
opts:
path: /var/fsc/data/owner2/txdb

245
token-sdk/owner/go.mod Normal file
View file

@ -0,0 +1,245 @@
module github.com/hyperledger/fabric-samples/token-sdk/owner
go 1.20
replace github.com/ugorji/go v1.1.4 => github.com/ugorji/go/codec v1.2.9
require (
github.com/deepmap/oapi-codegen v1.15.0
github.com/getkin/kin-openapi v0.120.0
github.com/hyperledger-labs/fabric-smart-client v0.3.0
github.com/hyperledger-labs/fabric-token-sdk v0.3.0
github.com/labstack/echo/v4 v4.11.1
github.com/pkg/errors v0.9.1
)
require (
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
github.com/CloudyKit/jet/v6 v6.2.0 // indirect
github.com/IBM/idemix v0.0.2-0.20230831093709-b7a940638990 // indirect
github.com/IBM/idemix/bccsp/schemes/aries v0.0.0-20230831093709-b7a940638990 // indirect
github.com/IBM/idemix/bccsp/schemes/weak-bb v0.0.0-20230831093709-b7a940638990 // indirect
github.com/IBM/idemix/bccsp/types v0.0.0-20230831093709-b7a940638990 // indirect
github.com/IBM/mathlib v0.0.3-0.20230831091907-c532c4d3b65c // indirect
github.com/Joker/jade v1.1.3 // indirect
github.com/ReneKroon/ttlcache/v2 v2.11.0 // indirect
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect
github.com/ale-linux/aries-framework-go/component/kmscrypto v0.0.0-20230817163708-4b3de6d91874 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/consensys/bavard v0.1.13 // indirect
github.com/consensys/gnark-crypto v0.9.1 // indirect
github.com/containerd/cgroups v1.1.0 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/dgraph-io/badger/v3 v3.2103.2 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/elastic/gosigar v0.14.2 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/flosch/pongo2/v4 v4.0.2 // indirect
github.com/flynn/noise v1.0.0 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/go-kit/kit v0.10.0 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 // indirect
github.com/google/flatbuffers v1.12.1 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-uuid v1.0.2 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huin/goupnp v1.2.0 // indirect
github.com/hyperledger-labs/orion-sdk-go v0.2.5 // indirect
github.com/hyperledger-labs/orion-server v0.2.5 // indirect
github.com/hyperledger-labs/weaver-dlt-interoperability/common/protos-go v1.2.3-alpha.1 // indirect
github.com/hyperledger-labs/weaver-dlt-interoperability/sdks/fabric/go-sdk v1.2.3-alpha.1.0.20210812140206-37f430515b8c // indirect
github.com/hyperledger/fabric v1.4.0-rc1.0.20230401164317-bd8e24856939 // indirect
github.com/hyperledger/fabric-amcl v0.0.0-20230602173724-9e02669dceb2 // indirect
github.com/hyperledger/fabric-chaincode-go v0.0.0-20220920210243-7bc6fa0dd58b // indirect
github.com/hyperledger/fabric-lib-go v1.0.0 // indirect
github.com/hyperledger/fabric-private-chaincode v0.0.0-20210907122433-d56466264e4d // indirect
github.com/hyperledger/fabric-protos-go v0.2.0 // indirect
github.com/invopop/yaml v0.2.0 // indirect
github.com/ipfs/boxo v0.8.0-rc1 // indirect
github.com/ipfs/go-cid v0.4.1 // indirect
github.com/ipfs/go-datastore v0.6.0 // indirect
github.com/ipfs/go-ipfs-util v0.0.2 // indirect
github.com/ipfs/go-log v1.0.5 // indirect
github.com/ipfs/go-log/v2 v2.5.1 // indirect
github.com/ipld/go-ipld-prime v0.20.0 // indirect
github.com/iris-contrib/schema v0.0.6 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
github.com/jbenet/goprocess v0.1.4 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kataras/blocks v0.0.7 // indirect
github.com/kataras/golog v0.1.9 // indirect
github.com/kataras/iris/v12 v12.2.6-0.20230908161203-24ba4e8933b9 // indirect
github.com/kataras/pio v0.0.12 // indirect
github.com/kataras/sitemap v0.0.6 // indirect
github.com/kataras/tunnel v0.0.4 // indirect
github.com/kilic/bls12-381 v0.1.0 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/koron/go-ssdp v0.0.4 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/libp2p/go-cidranger v1.1.0 // indirect
github.com/libp2p/go-flow-metrics v0.1.0 // indirect
github.com/libp2p/go-libp2p v0.31.0 // indirect
github.com/libp2p/go-libp2p-asn-util v0.3.0 // indirect
github.com/libp2p/go-libp2p-kad-dht v0.22.0 // indirect
github.com/libp2p/go-libp2p-kbucket v0.5.0 // indirect
github.com/libp2p/go-libp2p-record v0.2.0 // indirect
github.com/libp2p/go-msgio v0.3.0 // indirect
github.com/libp2p/go-nat v0.2.0 // indirect
github.com/libp2p/go-netroute v0.2.1 // indirect
github.com/libp2p/go-reuseport v0.4.0 // indirect
github.com/libp2p/go-yamux/v4 v4.0.1 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mailgun/raymond/v2 v2.0.48 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/microcosm-cc/bluemonday v1.0.25 // indirect
github.com/miekg/dns v1.1.55 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/mmcloughlin/addchain v0.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/multiformats/go-base32 v0.1.0 // indirect
github.com/multiformats/go-base36 v0.2.0 // indirect
github.com/multiformats/go-multiaddr v0.11.0 // indirect
github.com/multiformats/go-multiaddr-dns v0.3.1 // indirect
github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect
github.com/multiformats/go-multibase v0.2.0 // indirect
github.com/multiformats/go-multicodec v0.9.0 // indirect
github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-multistream v0.4.1 // indirect
github.com/multiformats/go-varint v0.0.7 // indirect
github.com/onsi/ginkgo/v2 v2.11.0 // indirect
github.com/opencontainers/runtime-spec v1.1.0 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/polydawn/refmt v0.89.0 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.3.3 // indirect
github.com/quic-go/quic-go v0.38.1 // indirect
github.com/quic-go/webtransport-go v0.5.3 // indirect
github.com/raulk/go-watchdog v1.3.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/schollz/closestmatch v2.1.0+incompatible // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.10.1 // indirect
github.com/stretchr/testify v1.8.4 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/sykesm/zap-logfmt v0.0.4 // indirect
github.com/tdewolff/minify/v2 v2.12.9 // indirect
github.com/tdewolff/parse/v2 v2.6.8 // indirect
github.com/test-go/testify v1.1.4 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect
github.com/yosssi/ace v0.0.5 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/otel v1.14.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.13.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.13.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.13.0 // indirect
go.opentelemetry.io/otel/sdk v1.13.0 // indirect
go.opentelemetry.io/otel/trace v1.14.0 // indirect
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/dig v1.17.0 // indirect
go.uber.org/fx v1.20.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.25.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
google.golang.org/grpc v1.53.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.2.1 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
)

1978
token-sdk/owner/go.sum Normal file

File diff suppressed because it is too large Load diff

114
token-sdk/owner/main.go Normal file
View file

@ -0,0 +1,114 @@
package main
import (
"net/http"
"os"
"os/signal"
"syscall"
"github.com/hyperledger/fabric-samples/token-sdk/owner/routes"
"github.com/hyperledger/fabric-samples/token-sdk/owner/service"
"github.com/hyperledger-labs/fabric-smart-client/pkg/api"
"github.com/hyperledger-labs/fabric-smart-client/pkg/node"
fabric "github.com/hyperledger-labs/fabric-smart-client/platform/fabric/sdk"
viewregistry "github.com/hyperledger-labs/fabric-smart-client/platform/view"
"github.com/hyperledger-labs/fabric-smart-client/platform/view/services/flogging"
tokensdk "github.com/hyperledger-labs/fabric-token-sdk/token/sdk"
)
var logger = flogging.MustGetLogger("main")
func main() {
dir := getEnv("CONF_DIR", "./conf/owner1")
port := getEnv("PORT", "9200")
fsc := startFabricSmartClient(dir)
// Tell the service how to respond to other nodes when they initiate an action
registry := viewregistry.GetRegistry(fsc)
succeedOrPanic(registry.RegisterResponder(&service.AcceptCashView{}, "github.com/hyperledger/fabric-samples/token-sdk/issuer/service/IssueCashView"))
succeedOrPanic(registry.RegisterResponder(&service.AcceptCashView{}, &service.TransferView{}))
controller := routes.Controller{Service: service.TokenService{FSC: fsc}}
err := routes.StartWebServer(port, controller, logger)
if err != nil {
if err == http.ErrServerClosed {
logger.Infof("Webserver closing, exiting...", err.Error())
fsc.Stop()
} else {
logger.Fatalf("echo error - %s", err.Error())
fsc.Stop()
os.Exit(1)
}
}
}
type Node interface {
api.ServiceProvider
Stop()
}
func startFabricSmartClient(confDir string) Node {
logger.Infof("Initializing Fabric Smart Client and Token SDK...")
fsc := node.NewFromConfPath(confDir)
succeedOrPanic(fsc.InstallSDK(fabric.NewSDK(fsc)))
succeedOrPanic(fsc.InstallSDK(tokensdk.NewSDK(fsc)))
succeedOrPanic(fsc.Start())
// Stop gracefully
go handleSignals((map[os.Signal]func(){
syscall.SIGINT: func() {
logger.Info("Stopping FSC node...")
fsc.Stop()
os.Exit(130)
},
syscall.SIGTERM: func() {
logger.Info("Stopping FSC node...")
fsc.Stop()
os.Exit(143)
},
syscall.SIGSTOP: func() {
logger.Info("Stopping FSC node...")
fsc.Stop()
os.Exit(145)
},
syscall.SIGHUP: func() {
logger.Info("Stopping FSC node...")
fsc.Stop()
os.Exit(129)
},
}))
logger.Infof("FSC node is ready!")
return fsc
}
// getEnv returns an environment variable or the fallback
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
func succeedOrPanic(err error) {
if err != nil {
logger.Fatalf("Failed initializing Token SDK - %s", err.Error())
os.Exit(1)
}
}
func handleSignals(handlers map[os.Signal]func()) {
var signals []os.Signal
for sig := range handlers {
signals = append(signals, sig)
}
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, signals...)
for sig := range signalChan {
logger.Infof("Received signal: %d (%s)", sig, sig)
handlers[sig]()
}
}

View file

@ -0,0 +1,11 @@
package: routes
generate:
echo-server: true
strict-server: true
models: true
embedded-spec: true
output-options:
include-tags:
- operations
- owner
output: owner/routes/routes.gen.go

View file

@ -0,0 +1,33 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package routes
import (
"context"
)
type OperationsAPI struct{}
// (GET /readyz)
func (c Controller) Readyz(ctx context.Context, request ReadyzRequestObject) (ReadyzResponseObject, error) {
// TODO: what defines readiness if the REST API is available after FSC?
return Readyz200JSONResponse{
HealthSuccessJSONResponse: HealthSuccessJSONResponse{
Message: "ok",
},
}, nil
}
// (GET /healthz)
func (c Controller) Healthz(ctx context.Context, request HealthzRequestObject) (HealthzResponseObject, error) {
// TODO: how to determine health?
return Healthz200JSONResponse{
HealthSuccessJSONResponse: HealthSuccessJSONResponse{
Message: "ok",
},
}, nil
}

View file

@ -0,0 +1,897 @@
// Package routes provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/deepmap/oapi-codegen version v1.13.4 DO NOT EDIT.
package routes
import (
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"path"
"strings"
"time"
"github.com/deepmap/oapi-codegen/pkg/runtime"
"github.com/getkin/kin-openapi/openapi3"
"github.com/labstack/echo/v4"
)
// Account Information about an account and its balance
type Account struct {
// Balance balance in base units for each currency
Balance []Amount `json:"balance"`
// Id account id as registered at the Certificate Authority
Id string `json:"id"`
}
// Amount The amount to issue, transfer or redeem.
type Amount struct {
// Code the code of the token
Code string `json:"code"`
// Value value in base units (usually cents)
Value int64 `json:"value"`
}
// Counterparty The counterparty in a Transfer or Issuance transaction.
type Counterparty struct {
Account string `json:"account"`
// Node The node that holds the recipient account
Node string `json:"node"`
}
// Error defines model for Error.
type Error struct {
// Message High level error message
Message string `json:"message"`
// Payload Details about the error
Payload string `json:"payload"`
}
// RedeemRequest Instructions to redeem tokens from an account
type RedeemRequest struct {
// Amount The amount to issue, transfer or redeem.
Amount Amount `json:"amount"`
// Message optional message that will be visible to the auditor
Message *string `json:"message,omitempty"`
}
// TransactionRecord A transaction
type TransactionRecord struct {
// Amount The amount to issue, transfer or redeem.
Amount Amount `json:"amount"`
// Id transaction id
Id string `json:"id"`
// Message user provided message
Message string `json:"message"`
// Recipient the recipient of the transaction
Recipient string `json:"recipient"`
// Sender the sender of the transaction
Sender string `json:"sender"`
// Status Unknown | Pending | Confirmed | Deleted
Status string `json:"status"`
// Timestamp timestamp in the format: "2018-03-20T09:12:28Z"
Timestamp time.Time `json:"timestamp"`
}
// TransferRequest Instructions to issue or transfer tokens to an account
type TransferRequest struct {
// Amount The amount to issue, transfer or redeem.
Amount Amount `json:"amount"`
// Counterparty The counterparty in a Transfer or Issuance transaction.
Counterparty Counterparty `json:"counterparty"`
// Message optional message that will be sent and stored with the transfer transaction
Message *string `json:"message,omitempty"`
}
// Code The token code to filter on
type Code = string
// Id account id as registered at the Certificate Authority
type Id = string
// AccountSuccess defines model for AccountSuccess.
type AccountSuccess struct {
Message string `json:"message"`
// Payload Information about an account and its balance
Payload Account `json:"payload"`
}
// AccountsSuccess defines model for AccountsSuccess.
type AccountsSuccess struct {
Message string `json:"message"`
Payload []Account `json:"payload"`
}
// ErrorResponse defines model for ErrorResponse.
type ErrorResponse = Error
// HealthSuccess defines model for HealthSuccess.
type HealthSuccess struct {
// Message ok
Message string `json:"message"`
}
// RedeemSuccess defines model for RedeemSuccess.
type RedeemSuccess struct {
Message string `json:"message"`
// Payload Transaction id
Payload string `json:"payload"`
}
// TransactionsSuccess defines model for TransactionsSuccess.
type TransactionsSuccess struct {
Message string `json:"message"`
Payload []TransactionRecord `json:"payload"`
}
// TransferSuccess defines model for TransferSuccess.
type TransferSuccess struct {
Message string `json:"message"`
// Payload Transaction id
Payload string `json:"payload"`
}
// OwnerAccountParams defines parameters for OwnerAccount.
type OwnerAccountParams struct {
Code *Code `form:"code,omitempty" json:"code,omitempty"`
}
// RedeemJSONRequestBody defines body for Redeem for application/json ContentType.
type RedeemJSONRequestBody = RedeemRequest
// TransferJSONRequestBody defines body for Transfer for application/json ContentType.
type TransferJSONRequestBody = TransferRequest
// ServerInterface represents all server handlers.
type ServerInterface interface {
// (GET /healthz)
Healthz(ctx echo.Context) error
// Get all accounts on this node and their balances
// (GET /owner/accounts)
OwnerAccounts(ctx echo.Context) error
// Get an account and their balances
// (GET /owner/accounts/{id})
OwnerAccount(ctx echo.Context, id Id, params OwnerAccountParams) error
// Redeem (burn) tokens
// (POST /owner/accounts/{id}/redeem)
Redeem(ctx echo.Context, id Id) error
// Get all transactions for an account
// (GET /owner/accounts/{id}/transactions)
OwnerTransactions(ctx echo.Context, id Id) error
// Transfer tokens to another account
// (POST /owner/accounts/{id}/transfer)
Transfer(ctx echo.Context, id Id) error
// (GET /readyz)
Readyz(ctx echo.Context) error
}
// ServerInterfaceWrapper converts echo contexts to parameters.
type ServerInterfaceWrapper struct {
Handler ServerInterface
}
// Healthz converts echo context to params.
func (w *ServerInterfaceWrapper) Healthz(ctx echo.Context) error {
var err error
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.Healthz(ctx)
return err
}
// OwnerAccounts converts echo context to params.
func (w *ServerInterfaceWrapper) OwnerAccounts(ctx echo.Context) error {
var err error
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.OwnerAccounts(ctx)
return err
}
// OwnerAccount converts echo context to params.
func (w *ServerInterfaceWrapper) OwnerAccount(ctx echo.Context) error {
var err error
// ------------- Path parameter "id" -------------
var id Id
err = runtime.BindStyledParameterWithLocation("simple", false, "id", runtime.ParamLocationPath, ctx.Param("id"), &id)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err))
}
// Parameter object where we will unmarshal all parameters from the context
var params OwnerAccountParams
// ------------- Optional query parameter "code" -------------
err = runtime.BindQueryParameter("form", true, false, "code", ctx.QueryParams(), &params.Code)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter code: %s", err))
}
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.OwnerAccount(ctx, id, params)
return err
}
// Redeem converts echo context to params.
func (w *ServerInterfaceWrapper) Redeem(ctx echo.Context) error {
var err error
// ------------- Path parameter "id" -------------
var id Id
err = runtime.BindStyledParameterWithLocation("simple", false, "id", runtime.ParamLocationPath, ctx.Param("id"), &id)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err))
}
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.Redeem(ctx, id)
return err
}
// OwnerTransactions converts echo context to params.
func (w *ServerInterfaceWrapper) OwnerTransactions(ctx echo.Context) error {
var err error
// ------------- Path parameter "id" -------------
var id Id
err = runtime.BindStyledParameterWithLocation("simple", false, "id", runtime.ParamLocationPath, ctx.Param("id"), &id)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err))
}
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.OwnerTransactions(ctx, id)
return err
}
// Transfer converts echo context to params.
func (w *ServerInterfaceWrapper) Transfer(ctx echo.Context) error {
var err error
// ------------- Path parameter "id" -------------
var id Id
err = runtime.BindStyledParameterWithLocation("simple", false, "id", runtime.ParamLocationPath, ctx.Param("id"), &id)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err))
}
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.Transfer(ctx, id)
return err
}
// Readyz converts echo context to params.
func (w *ServerInterfaceWrapper) Readyz(ctx echo.Context) error {
var err error
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.Readyz(ctx)
return err
}
// This is a simple interface which specifies echo.Route addition functions which
// are present on both echo.Echo and echo.Group, since we want to allow using
// either of them for path registration
type EchoRouter interface {
CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
}
// RegisterHandlers adds each server route to the EchoRouter.
func RegisterHandlers(router EchoRouter, si ServerInterface) {
RegisterHandlersWithBaseURL(router, si, "")
}
// Registers handlers, and prepends BaseURL to the paths, so that the paths
// can be served under a prefix.
func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) {
wrapper := ServerInterfaceWrapper{
Handler: si,
}
router.GET(baseURL+"/healthz", wrapper.Healthz)
router.GET(baseURL+"/owner/accounts", wrapper.OwnerAccounts)
router.GET(baseURL+"/owner/accounts/:id", wrapper.OwnerAccount)
router.POST(baseURL+"/owner/accounts/:id/redeem", wrapper.Redeem)
router.GET(baseURL+"/owner/accounts/:id/transactions", wrapper.OwnerTransactions)
router.POST(baseURL+"/owner/accounts/:id/transfer", wrapper.Transfer)
router.GET(baseURL+"/readyz", wrapper.Readyz)
}
type AccountSuccessJSONResponse struct {
Message string `json:"message"`
// Payload Information about an account and its balance
Payload Account `json:"payload"`
}
type AccountsSuccessJSONResponse struct {
Message string `json:"message"`
Payload []Account `json:"payload"`
}
type ErrorResponseJSONResponse Error
type HealthSuccessJSONResponse struct {
// Message ok
Message string `json:"message"`
}
type RedeemSuccessJSONResponse struct {
Message string `json:"message"`
// Payload Transaction id
Payload string `json:"payload"`
}
type TransactionsSuccessJSONResponse struct {
Message string `json:"message"`
Payload []TransactionRecord `json:"payload"`
}
type TransferSuccessJSONResponse struct {
Message string `json:"message"`
// Payload Transaction id
Payload string `json:"payload"`
}
type HealthzRequestObject struct {
}
type HealthzResponseObject interface {
VisitHealthzResponse(w http.ResponseWriter) error
}
type Healthz200JSONResponse struct{ HealthSuccessJSONResponse }
func (response Healthz200JSONResponse) VisitHealthzResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type Healthz503JSONResponse struct{ ErrorResponseJSONResponse }
func (response Healthz503JSONResponse) VisitHealthzResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(503)
return json.NewEncoder(w).Encode(response)
}
type OwnerAccountsRequestObject struct {
}
type OwnerAccountsResponseObject interface {
VisitOwnerAccountsResponse(w http.ResponseWriter) error
}
type OwnerAccounts200JSONResponse struct{ AccountsSuccessJSONResponse }
func (response OwnerAccounts200JSONResponse) VisitOwnerAccountsResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type OwnerAccountsdefaultJSONResponse struct {
Body Error
StatusCode int
}
func (response OwnerAccountsdefaultJSONResponse) VisitOwnerAccountsResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(response.StatusCode)
return json.NewEncoder(w).Encode(response.Body)
}
type OwnerAccountRequestObject struct {
Id Id `json:"id"`
Params OwnerAccountParams
}
type OwnerAccountResponseObject interface {
VisitOwnerAccountResponse(w http.ResponseWriter) error
}
type OwnerAccount200JSONResponse struct{ AccountSuccessJSONResponse }
func (response OwnerAccount200JSONResponse) VisitOwnerAccountResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type OwnerAccountdefaultJSONResponse struct {
Body Error
StatusCode int
}
func (response OwnerAccountdefaultJSONResponse) VisitOwnerAccountResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(response.StatusCode)
return json.NewEncoder(w).Encode(response.Body)
}
type RedeemRequestObject struct {
Id Id `json:"id"`
Body *RedeemJSONRequestBody
}
type RedeemResponseObject interface {
VisitRedeemResponse(w http.ResponseWriter) error
}
type Redeem200JSONResponse struct{ RedeemSuccessJSONResponse }
func (response Redeem200JSONResponse) VisitRedeemResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type RedeemdefaultJSONResponse struct {
Body Error
StatusCode int
}
func (response RedeemdefaultJSONResponse) VisitRedeemResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(response.StatusCode)
return json.NewEncoder(w).Encode(response.Body)
}
type OwnerTransactionsRequestObject struct {
Id Id `json:"id"`
}
type OwnerTransactionsResponseObject interface {
VisitOwnerTransactionsResponse(w http.ResponseWriter) error
}
type OwnerTransactions200JSONResponse struct {
TransactionsSuccessJSONResponse
}
func (response OwnerTransactions200JSONResponse) VisitOwnerTransactionsResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type OwnerTransactionsdefaultJSONResponse struct {
Body Error
StatusCode int
}
func (response OwnerTransactionsdefaultJSONResponse) VisitOwnerTransactionsResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(response.StatusCode)
return json.NewEncoder(w).Encode(response.Body)
}
type TransferRequestObject struct {
Id Id `json:"id"`
Body *TransferJSONRequestBody
}
type TransferResponseObject interface {
VisitTransferResponse(w http.ResponseWriter) error
}
type Transfer200JSONResponse struct{ TransferSuccessJSONResponse }
func (response Transfer200JSONResponse) VisitTransferResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type TransferdefaultJSONResponse struct {
Body Error
StatusCode int
}
func (response TransferdefaultJSONResponse) VisitTransferResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(response.StatusCode)
return json.NewEncoder(w).Encode(response.Body)
}
type ReadyzRequestObject struct {
}
type ReadyzResponseObject interface {
VisitReadyzResponse(w http.ResponseWriter) error
}
type Readyz200JSONResponse struct{ HealthSuccessJSONResponse }
func (response Readyz200JSONResponse) VisitReadyzResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type Readyz503JSONResponse struct{ ErrorResponseJSONResponse }
func (response Readyz503JSONResponse) VisitReadyzResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(503)
return json.NewEncoder(w).Encode(response)
}
// StrictServerInterface represents all server handlers.
type StrictServerInterface interface {
// (GET /healthz)
Healthz(ctx context.Context, request HealthzRequestObject) (HealthzResponseObject, error)
// Get all accounts on this node and their balances
// (GET /owner/accounts)
OwnerAccounts(ctx context.Context, request OwnerAccountsRequestObject) (OwnerAccountsResponseObject, error)
// Get an account and their balances
// (GET /owner/accounts/{id})
OwnerAccount(ctx context.Context, request OwnerAccountRequestObject) (OwnerAccountResponseObject, error)
// Redeem (burn) tokens
// (POST /owner/accounts/{id}/redeem)
Redeem(ctx context.Context, request RedeemRequestObject) (RedeemResponseObject, error)
// Get all transactions for an account
// (GET /owner/accounts/{id}/transactions)
OwnerTransactions(ctx context.Context, request OwnerTransactionsRequestObject) (OwnerTransactionsResponseObject, error)
// Transfer tokens to another account
// (POST /owner/accounts/{id}/transfer)
Transfer(ctx context.Context, request TransferRequestObject) (TransferResponseObject, error)
// (GET /readyz)
Readyz(ctx context.Context, request ReadyzRequestObject) (ReadyzResponseObject, error)
}
type StrictHandlerFunc = runtime.StrictEchoHandlerFunc
type StrictMiddlewareFunc = runtime.StrictEchoMiddlewareFunc
func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface {
return &strictHandler{ssi: ssi, middlewares: middlewares}
}
type strictHandler struct {
ssi StrictServerInterface
middlewares []StrictMiddlewareFunc
}
// Healthz operation middleware
func (sh *strictHandler) Healthz(ctx echo.Context) error {
var request HealthzRequestObject
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
return sh.ssi.Healthz(ctx.Request().Context(), request.(HealthzRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "Healthz")
}
response, err := handler(ctx, request)
if err != nil {
return err
} else if validResponse, ok := response.(HealthzResponseObject); ok {
return validResponse.VisitHealthzResponse(ctx.Response())
} else if response != nil {
return fmt.Errorf("Unexpected response type: %T", response)
}
return nil
}
// OwnerAccounts operation middleware
func (sh *strictHandler) OwnerAccounts(ctx echo.Context) error {
var request OwnerAccountsRequestObject
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
return sh.ssi.OwnerAccounts(ctx.Request().Context(), request.(OwnerAccountsRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "OwnerAccounts")
}
response, err := handler(ctx, request)
if err != nil {
return err
} else if validResponse, ok := response.(OwnerAccountsResponseObject); ok {
return validResponse.VisitOwnerAccountsResponse(ctx.Response())
} else if response != nil {
return fmt.Errorf("Unexpected response type: %T", response)
}
return nil
}
// OwnerAccount operation middleware
func (sh *strictHandler) OwnerAccount(ctx echo.Context, id Id, params OwnerAccountParams) error {
var request OwnerAccountRequestObject
request.Id = id
request.Params = params
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
return sh.ssi.OwnerAccount(ctx.Request().Context(), request.(OwnerAccountRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "OwnerAccount")
}
response, err := handler(ctx, request)
if err != nil {
return err
} else if validResponse, ok := response.(OwnerAccountResponseObject); ok {
return validResponse.VisitOwnerAccountResponse(ctx.Response())
} else if response != nil {
return fmt.Errorf("Unexpected response type: %T", response)
}
return nil
}
// Redeem operation middleware
func (sh *strictHandler) Redeem(ctx echo.Context, id Id) error {
var request RedeemRequestObject
request.Id = id
var body RedeemJSONRequestBody
if err := ctx.Bind(&body); err != nil {
return err
}
request.Body = &body
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
return sh.ssi.Redeem(ctx.Request().Context(), request.(RedeemRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "Redeem")
}
response, err := handler(ctx, request)
if err != nil {
return err
} else if validResponse, ok := response.(RedeemResponseObject); ok {
return validResponse.VisitRedeemResponse(ctx.Response())
} else if response != nil {
return fmt.Errorf("Unexpected response type: %T", response)
}
return nil
}
// OwnerTransactions operation middleware
func (sh *strictHandler) OwnerTransactions(ctx echo.Context, id Id) error {
var request OwnerTransactionsRequestObject
request.Id = id
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
return sh.ssi.OwnerTransactions(ctx.Request().Context(), request.(OwnerTransactionsRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "OwnerTransactions")
}
response, err := handler(ctx, request)
if err != nil {
return err
} else if validResponse, ok := response.(OwnerTransactionsResponseObject); ok {
return validResponse.VisitOwnerTransactionsResponse(ctx.Response())
} else if response != nil {
return fmt.Errorf("Unexpected response type: %T", response)
}
return nil
}
// Transfer operation middleware
func (sh *strictHandler) Transfer(ctx echo.Context, id Id) error {
var request TransferRequestObject
request.Id = id
var body TransferJSONRequestBody
if err := ctx.Bind(&body); err != nil {
return err
}
request.Body = &body
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
return sh.ssi.Transfer(ctx.Request().Context(), request.(TransferRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "Transfer")
}
response, err := handler(ctx, request)
if err != nil {
return err
} else if validResponse, ok := response.(TransferResponseObject); ok {
return validResponse.VisitTransferResponse(ctx.Response())
} else if response != nil {
return fmt.Errorf("Unexpected response type: %T", response)
}
return nil
}
// Readyz operation middleware
func (sh *strictHandler) Readyz(ctx echo.Context) error {
var request ReadyzRequestObject
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
return sh.ssi.Readyz(ctx.Request().Context(), request.(ReadyzRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "Readyz")
}
response, err := handler(ctx, request)
if err != nil {
return err
} else if validResponse, ok := response.(ReadyzResponseObject); ok {
return validResponse.VisitReadyzResponse(ctx.Response())
} else if response != nil {
return fmt.Errorf("Unexpected response type: %T", response)
}
return nil
}
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"H4sIAAAAAAAC/+RaW2/bOBb+KwR3H2YANZKdzU1vnXawLfZhB2kKLKbNAy0eW5xQpEpSznpT//cFL7rL",
"l6SZou08JTapw3O+850b5QecyaKUAoTROH3AJVGkAAPKfcokBfuXCZziTxWoDY6wIAXg1K9FWGc5FMRu",
"oqAzxUrDpN19kwMy8g4EshuRkWjJuAGFpMARhv+SouRWzK/vr/+DI2w2pf2kjWJihbfbCDPanFwSk7cH",
"M4ojrOBTxRRQnBpVwW41SJbJShjEKCIaKVgxbUABRcQgkwN6BcqwJcuIAfSyMrlUzGx6ChLOMpjQcGuV",
"0KUUGhxWL/1J76osAx3QEwaEsf+SsuT2ECZF/Ie2mj10VC6VLK0eXlABWpOVw31wZoRLsuGSOGT+rmCJ",
"U/y3uHVg7EXqOOiCvZI1Uh8a0a2g28YwufgDMuMN62MYTEK1uVaRcIL+auYyA4U+2u7GKqIU2fyJOPyq",
"lFTX9RePQWGfHU7qlApuoafAGyDc5E9xQ0Pxjg+wvHO47/JQXxt5Nxm5U0g/Fd9roADFs7KsDW2Xn+x5",
"9gygY2N6DBzkN0WEJpn9hFxGasVeZPNzoOdwBsnZ8oLC5Yxczs+vZovz88UsOb+is7Pkgl4BXZ5mF4uL",
"SyCzJbm6PDu7ADi7WPzjaFC/nL4dK/SfBXLnCKTAKAbrg1gfFe0d5a8hk4p+xbh3Zy9BHQ/aZLiZIMZW",
"JM/Gjm7PwKRt9KV59wDrvw5RGzO6lXas3VuxlKpwsCOykJVBRKC6AyCCImY0WhBOhKvoHY/UX6Yf6qan",
"bkzWhFeA01mSJMk2albfv3vdWZ0nyfbWtyyhXxgl0eaEodJhATGBFkQDqoTVcikVApLlKKuUApHZnuS4",
"ElhMV8C6ofpa7VGfCI4rNQRjDkQ4qD3ZRhK3ZltIpnUFEaqjBrlaaJP3Sd+du1w48krd4FJYkoqb9pm+",
"FhYK18bKpYPFhepUCgtHDa1wXw88/FOlK8L5BmXWfz/jCHvy2g5XmHNbBQomWFEVOE2ao5gwsAI1Ajh0",
"4/78KYBfWQxBlUSZzTTMWWeH1ZWgmw7Ob7WuHE87+XyAOqnjsiGF8H6Q9wLUbBwTpA3kEZKi8cxQT+Hm",
"iZwYlEtOtXOIgoyVDGyYB5mHGCk8YPX2Kch8F7YrdYNrxcYZLsWP6KDesFWOOKyBo6G845PyazCEcR1S",
"noXDyXqW7Fx3YNfwqQI9mXO1UVUo70aGeAzFDC2VLDpJGI8Y0IT9cTltdyvq/iG8BtAT5J5xjhaA1kyz",
"BXdTqIWHVJSZIwAK2k2hMu49Riq97IbKIFIas/dkKpuvZ/PTjtHYDb6B6bZ0yIWdwEFQUJ2o04aYSuMU",
"v5JiyVTobFkB2pCixCmeJ7PLF8npi3lyk1yls3k6v/x9Ijof6Zup+mIO9At7PFppUKhUcs0o0H1x0UHk",
"YSJvt6mhTt49r4zE1XBOyfJrxwoKbhgKei/uhLwX6DP6DQRlYoU+o8ZT6DN6DRzMdIfcceJIvXrJpm6r",
"na8mKfo46e6PuFtwKDHwwko4rpAHiLrQRzVduko2GER75sC2lz46x7g2wBalphEI6cbI50w22aBk7nuq",
"V16fnqg0hE5VG2nbsHtm8pZrztJ9pBs1BR2lot3pzF23iaWcQN43XLbQdtouq2Avz5+gX0h2BxQtNogg",
"yqw+i8oARRzoClT0UZQKNKi1pXup2JpkG1Rp++l3UBL9S8h7txX9pqRc6hPHI+M6zJt6LlqD0l6t2Uli",
"QZYlCFIynOLTk+Tk1BUykztfxyHHx4EMOn5gdOuGV1Brd735YdQMN2WhUhynODemTOOYy4zwXGqTXiVJ",
"EpOSxetZjLe322jHMXF35n3+M3N35fM/K3gFjs+W5W7seWv7jzdhfXBBOU+SXSxu9sX966RthM+S08NP",
"9W/BLJ0MWVlzW800trrrqiiI2uAUX4OplNBoniSI+Xzq+GEnIY28iS6SYhftyv/ZD6bfuQfLWQ/LrjqO",
"6NNpxOrgOtjGyTuR/7fdVl+NPgn/4b2qG47DbPJ4L+zhne0VXCD7JmIXYvMuYtFQSkYUl9qJoUTsEXO6",
"E/h/gkGE8xptjaQtX0z7Nt9KNjkwVY/tFtWGWxZtT6tv084xc5osdJA+Lpm172I+TDu/3RIz6hQ/sMv1",
"m9YDT6bmX4uZ/fujH46KsS/j7nZQ6glK+unvSWS89c0IaPOLpJtnezfSn0e3/Z7HqAq2TyF3/z3DD89t",
"by76aVEp8XN78/zd0PmgLbv4PuzMdufh7quRp/P/sTyceiHzl+kBeq+KllL1h7nvPtfW89PubFtPwt9S",
"vh1O58+VcYdv0H54lt9MXVZIk9uB+vsj+SMtszGhgNDN7rH12i9/x1OrM9BZn2VQGpQRzvUhej7uAiD6",
"spk3+sY4FAAfXZ2HGkDWhHESbu5bpMIPweovxvpMPt8gVf+OzH8+8mkXkOiecA5utA9CfJyOZbwLrPAX",
"GeHGjFAmLEHbp1uebW+3/w8AAP//KMjhw4wnAAA=",
}
// GetSwagger returns the content of the embedded swagger specification file
// or error if failed to decode
func decodeSpec() ([]byte, error) {
zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, ""))
if err != nil {
return nil, fmt.Errorf("error base64 decoding spec: %w", err)
}
zr, err := gzip.NewReader(bytes.NewReader(zipped))
if err != nil {
return nil, fmt.Errorf("error decompressing spec: %w", err)
}
var buf bytes.Buffer
_, err = buf.ReadFrom(zr)
if err != nil {
return nil, fmt.Errorf("error decompressing spec: %w", err)
}
return buf.Bytes(), nil
}
var rawSpec = decodeSpecCached()
// a naive cached of a decoded swagger spec
func decodeSpecCached() func() ([]byte, error) {
data, err := decodeSpec()
return func() ([]byte, error) {
return data, err
}
}
// Constructs a synthetic filesystem for resolving external references when loading openapi specifications.
func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) {
res := make(map[string]func() ([]byte, error))
if len(pathToFile) > 0 {
res[pathToFile] = rawSpec
}
return res
}
// GetSwagger returns the Swagger specification corresponding to the generated code
// in this file. The external references of Swagger specification are resolved.
// The logic of resolving external references is tightly connected to "import-mapping" feature.
// Externally referenced files must be embedded in the corresponding golang packages.
// Urls can be supported but this task was out of the scope.
func GetSwagger() (swagger *openapi3.T, err error) {
resolvePath := PathToRawSpec("")
loader := openapi3.NewLoader()
loader.IsExternalRefsAllowed = true
loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) {
pathToFile := url.String()
pathToFile = path.Clean(pathToFile)
getSpec, ok := resolvePath[pathToFile]
if !ok {
err1 := fmt.Errorf("path not found: %s", pathToFile)
return nil, err1
}
return getSpec()
}
var specData []byte
specData, err = rawSpec()
if err != nil {
return
}
swagger, err = loader.LoadFromData(specData)
if err != nil {
return
}
return
}

View file

@ -0,0 +1,191 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package routes
import (
"context"
"fmt"
"github.com/hyperledger/fabric-samples/token-sdk/owner/service"
)
type Controller struct {
Service service.TokenService
}
// Transfer tokens to another account
// (POST /owner/accounts/{id}/transfer)
func (c Controller) Transfer(ctx context.Context, request TransferRequestObject) (TransferResponseObject, error) {
code := request.Body.Amount.Code
value := uint64(request.Body.Amount.Value)
sender := request.Id
recipient := request.Body.Counterparty.Account
recipientNode := request.Body.Counterparty.Node
var message string
if request.Body.Message != nil {
message = *request.Body.Message
}
txID, err := c.Service.TransferTokens(code, value, sender, recipient, recipientNode, message)
if err != nil {
return TransferdefaultJSONResponse{
Body: Error{
Message: "can't transfer funds",
Payload: err.Error(),
},
StatusCode: 500,
}, nil
}
return Transfer200JSONResponse{
TransferSuccessJSONResponse: TransferSuccessJSONResponse{
Message: fmt.Sprintf("%s transferred %d %s to %s", sender, value, code, recipient),
Payload: txID,
},
}, nil
}
// Get all accounts on this node and their balances
// (GET /owner/accounts)
func (c Controller) OwnerAccounts(ctx context.Context, request OwnerAccountsRequestObject) (OwnerAccountsResponseObject, error) {
balances, err := c.Service.GetAllBalances()
if err != nil {
return OwnerAccountsdefaultJSONResponse{
Body: Error{
Message: "can't get accounts",
Payload: err.Error(),
},
StatusCode: 500,
}, nil
}
acc := []Account{}
for wallet, balance := range balances {
amounts := []Amount{}
for typ, val := range balance {
amounts = append(amounts, Amount{
Code: typ,
Value: val,
})
}
acc = append(acc, Account{
Id: wallet,
Balance: amounts,
})
}
return OwnerAccounts200JSONResponse{
AccountsSuccessJSONResponse: AccountsSuccessJSONResponse{
Message: fmt.Sprintf("got %d accounts", len(acc)),
Payload: acc,
},
}, err
}
// Get an account and their balances
// (GET /owner/accounts/{id})
func (c Controller) OwnerAccount(ctx context.Context, request OwnerAccountRequestObject) (OwnerAccountResponseObject, error) {
var code string
if request.Params.Code != nil {
code = *request.Params.Code
}
balance, err := c.Service.GetBalance(request.Id, code)
if err != nil {
return OwnerAccountdefaultJSONResponse{
Body: Error{
Message: "can't get account",
Payload: err.Error(),
},
StatusCode: 500,
}, nil
}
amounts := []Amount{}
for typ, val := range balance {
amounts = append(amounts, Amount{
Code: typ,
Value: val,
})
}
return OwnerAccount200JSONResponse{
AccountSuccessJSONResponse: AccountSuccessJSONResponse{
Message: fmt.Sprintf("got balances for %s", request.Id),
Payload: Account{
Id: request.Id,
Balance: amounts,
},
},
}, nil
}
// Get all transactions for an account
// (GET /owner/accounts/{id}/transactions)
func (c Controller) OwnerTransactions(ctx context.Context, request OwnerTransactionsRequestObject) (OwnerTransactionsResponseObject, error) {
var history []service.TransactionHistoryItem
var err error
history, err = c.Service.GetHistory(request.Id)
if err != nil {
return OwnerTransactionsdefaultJSONResponse{
Body: Error{
Message: "can't get history",
Payload: err.Error(),
},
StatusCode: 500,
}, nil
}
pl := []TransactionRecord{}
for _, tx := range history {
pl = append(pl, TransactionRecord{
Amount: Amount{
Code: tx.TokenType,
Value: tx.Amount,
},
Id: tx.TxID,
Recipient: tx.Recipient,
Sender: tx.Sender,
Status: tx.Status,
Timestamp: tx.Timestamp,
Message: tx.Message,
})
}
return OwnerTransactions200JSONResponse{
TransactionsSuccessJSONResponse: TransactionsSuccessJSONResponse{
Message: fmt.Sprintf("got %d transactions for %s", len(pl), request.Id),
Payload: pl,
},
}, nil
}
// Redeem (burn) tokens
// (POST /owner/accounts/{id}/redeem)
func (c Controller) Redeem(ctx context.Context, request RedeemRequestObject) (RedeemResponseObject, error) {
code := request.Body.Amount.Code
value := uint64(request.Body.Amount.Value)
account := request.Id
var message string
if request.Body.Message != nil {
message = *request.Body.Message
}
txID, err := c.Service.RedeemTokens(code, value, account, message)
if err != nil {
return RedeemdefaultJSONResponse{
Body: Error{
Message: "can't redeem tokens",
Payload: err.Error(),
},
StatusCode: 500,
}, nil
}
return Redeem200JSONResponse{
RedeemSuccessJSONResponse: RedeemSuccessJSONResponse{
Message: fmt.Sprintf("%s redeemed %d %s", account, value, code),
Payload: txID,
},
}, nil
}

View file

@ -0,0 +1,65 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package routes
import (
"fmt"
"log"
"os"
oapimiddleware "github.com/deepmap/oapi-codegen/pkg/middleware"
"github.com/labstack/echo/v4"
middleware "github.com/labstack/echo/v4/middleware"
)
type Logger interface {
Infof(template string, args ...interface{})
Debugf(template string, args ...interface{})
Warnf(template string, args ...interface{})
Errorf(template string, args ...interface{})
Fatalf(template string, args ...interface{})
}
// Start web server on the main thread. It exits the application if it fails setting up.
func StartWebServer(port string, routesImplementation StrictServerInterface, logger Logger) error {
e := echo.New()
baseURL := "/api/v1"
handler := NewStrictHandler(routesImplementation, nil)
RegisterHandlersWithBaseURL(e, handler, baseURL)
// Request validator
swagger, err := GetSwagger()
if err != nil {
log.Fatalf("Error loading swagger spec\n: %s", err)
os.Exit(1)
}
swagger.Servers = nil
e.Group(baseURL).Use(oapimiddleware.OapiRequestValidator(swagger))
e.Use(middleware.CORS())
e.Use(middleware.RequestID())
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
Skipper: func(c echo.Context) bool {
return c.Path() == "/api/v1/healthz" || c.Path() == "/api/v1/readyz"
},
LogRequestID: true, LogMethod: true, LogURI: true, LogStatus: true, LogLatency: true,
LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
if v.Status < 400 {
logger.Infof("%d %s %s %s [%s]", v.Status, v.Method, v.URI, v.Latency.String(), v.RequestID)
} else if v.Status >= 400 && v.Status < 500 {
logger.Warnf("%d %s %s %s [%s]", v.Status, v.Method, v.URI, v.Latency.String(), v.RequestID)
} else {
logger.Errorf("%d %s %s %s [%s]", v.Status, v.Method, v.URI, v.Latency.String(), v.RequestID)
}
return nil
},
}))
// Start REST API server
return e.Start(fmt.Sprintf("0.0.0.0:%s", port))
}

View file

@ -0,0 +1,83 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package service
import (
"github.com/hyperledger-labs/fabric-smart-client/pkg/api"
"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/ttx"
"github.com/pkg/errors"
)
var logger = flogging.MustGetLogger("owner")
type TokenService struct {
FSC api.ServiceProvider
}
type AcceptCashView struct{}
func (v *AcceptCashView) Call(context view.Context) (interface{}, error) {
logger.Infof("incoming session from [%s]", context.Session().Info().Endpoint)
// The recipient of a token (issued or transfer) responds, as first operation,
// to a request for a recipient. The recipient identity is a new nym key and not the main key of the wallet.
id, err := ttx.RespondRequestRecipientIdentity(context)
if err != nil {
return "", errors.Wrap(err, "failed to respond to identity request")
}
logger.Infof("shared recipient id: [%s]", id.UniqueID())
// After we responded with the recipient identity, the counterparty assembles
// the transaction that assigns the tokens to the recipient id and sends it to us.
tx, err := ttx.ReceiveTransaction(context)
if err != nil {
err = errors.Wrap(err, "failed to receive tokens")
logger.Error(err.Error())
return "", err
}
logger.Infof("transaction received: [%s]", tx.ID())
// 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()
if err != nil {
err = errors.Wrap(err, "failed getting outputs")
logger.Error(err.Error())
return "", err
}
if outputs.Count() <= 0 {
err = errors.New("outputs should be more than zero")
logger.Error(err.Error())
return "", err
}
if outputs.ByRecipient(id).Count() <= 0 {
err = errors.New("outputs to me should be more than zero")
logger.Error(err.Error())
return "", err
}
// 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))
if err != nil {
return "", errors.Wrap(err, "failed to accept new tokens")
}
logger.Infof("transaction accepted: [%s]", tx.ID())
// Before completing, the recipient waits for finality of the transaction
_, err = context.RunView(ttx.NewFinalityView(tx))
if err != nil {
return "", errors.Wrap(err, "new tokens were not committed")
}
logger.Infof("transaction committed: [%s]", tx.ID())
return nil, nil
}

View file

@ -0,0 +1,69 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package service
import (
"strconv"
"github.com/hyperledger-labs/fabric-token-sdk/token"
"github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx"
"github.com/pkg/errors"
)
// SERVICE
type BalanceByWallet map[string]ValueByTokenType
type ValueByTokenType map[string]int64
// GetAllBalances returns a map of all wallets with their balances per token type
func (s TokenService) GetAllBalances() (walletBalance BalanceByWallet, err error) {
walletBalance = make(BalanceByWallet)
wm := token.GetManagementService(s.FSC).WalletManager()
wallets, err := wm.OwnerWalletIDs()
if err != nil {
return walletBalance, errors.Wrap(err, "can't get list of wallets")
}
logger.Infof("getting balances for %v", wallets)
for _, w := range wallets {
b, err := s.GetBalance(w, "")
if err != nil {
logger.Error(err)
return walletBalance, err
}
walletBalance[w] = b
}
return
}
// GetBalance returns the balances per token type of a wallet
func (s TokenService) GetBalance(wallet string, tokenType string) (typeVal ValueByTokenType, err error) {
typeVal = make(ValueByTokenType)
// Tokens owned by identities in this wallet will be listed
if wallet == "" {
return typeVal, errors.New("no wallet id provided")
}
w := ttx.GetWallet(s.FSC, wallet)
if w == nil {
return nil, errors.Errorf("wallet not found: %s", wallet)
}
unspentTokens, err := w.ListUnspentTokens(ttx.WithType(tokenType))
if len(unspentTokens.Tokens) == 0 {
return typeVal, nil
}
// Add the value of all unspent tokens in the wallet
for _, token := range unspentTokens.Tokens {
val, err := strconv.ParseInt(token.Quantity, 0, 64)
if err != nil {
return typeVal, errors.Wrap(err, "Error parsing token "+token.Id.String())
}
typeVal[token.Type] += val
}
return
}

View file

@ -0,0 +1,93 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package service
import (
"time"
"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/services/ttxdb"
"github.com/pkg/errors"
)
// SERVICE
type TransactionHistoryItem struct {
// TxID is the transaction ID
TxID string
// ActionType is the type of action performed by this transaction record
ActionType int
// SenderEID is the enrollment ID of the account that is sending tokens
Sender string
// RecipientEID is the enrollment ID of the account that is receiving tokens
Recipient string
// TokenType is the type of token
TokenType string
// Amount is positive if tokens are received. Negative otherwise
Amount int64
// Timestamp is the time the transaction was submitted to the db
Timestamp time.Time
// Status is the status of the transaction
Status string
// Message is the user message sent with the transaction. It comes from
// the ApplicationMetadata and is sent in the transient field
Message string
}
// GetHistory returns the full transaction history for an owner.
func (s TokenService) GetHistory(wallet string) (txs []TransactionHistoryItem, err error) {
// Get query executor
owner := ttx.NewOwner(s.FSC, token.GetManagementService(s.FSC))
aqe := owner.NewQueryExecutor()
defer aqe.Done()
it, err := aqe.Transactions(ttxdb.QueryTransactionsParams{
SenderWallet: wallet,
RecipientWallet: wallet,
})
if err != nil {
return txs, errors.Wrap(err, "failed querying transactions from db")
}
defer it.Close()
// we need transaction info to get the transient field (application metadata)
tip := ttx.NewTransactionInfoProvider(s.FSC, token.GetManagementService(s.FSC))
if tip == nil {
return txs, errors.New("failed to get transactionInfoProvider")
}
// Return the list of accepted transactions
for {
tx, err := it.Next()
if err != nil {
return txs, errors.Wrap(err, "failed iterating over transactions")
}
if tx == nil {
break
}
transaction := TransactionHistoryItem{
TxID: tx.TxID,
ActionType: int(tx.ActionType),
Sender: tx.SenderEID,
Recipient: tx.RecipientEID,
TokenType: tx.TokenType,
Amount: tx.Amount.Int64(),
Timestamp: tx.Timestamp.UTC(),
Status: string(tx.Status),
}
// set user provided message from transient field
ti, err := tip.TransactionInfo(tx.TxID)
if err != nil {
return txs, errors.Wrapf(err, "cannot get transaction info for %s", tx.TxID)
}
if ti.ApplicationMetadata != nil && string(ti.ApplicationMetadata["message"]) != "" {
transaction.Message = string(ti.ApplicationMetadata["message"])
}
txs = append(txs, transaction)
}
return
}

View file

@ -0,0 +1,130 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package service
import (
viewregistry "github.com/hyperledger-labs/fabric-smart-client/platform/view"
"github.com/hyperledger-labs/fabric-smart-client/platform/view/view"
"github.com/pkg/errors"
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"
)
// SERVICE
// Redeem burns an amount of a certain token. It prepares the transaction,
// gets it approved by the auditor and sends it to the blockchain for endorsement and commit.
func (s TokenService) RedeemTokens(tokenType string, quantity uint64, wallet string, message string) (txID string, err error) {
logger.Infof("redeeming %d %s from [%s] with message [%s]", quantity, tokenType, wallet, message)
res, err := viewregistry.GetManager(s.FSC).InitiateView(&RedeemView{
Redeem: &Redeem{
Wallet: wallet,
TokenType: tokenType,
Quantity: quantity,
Message: message,
},
})
if err != nil {
logger.Error(err)
return
}
txID, ok := res.(string)
if !ok {
err = errors.New("cannot parse redeem response")
logger.Error(err)
return
}
return
}
// VIEW
// 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
// Message is an optional user message sent with the transaction.
// It's stored in the ApplicationMetadata and is sent in the transient field.
Message string
}
type RedeemView struct {
*Redeem
}
func (v *RedeemView) Call(context view.Context) (interface{}, error) {
// specify the auditor and create the envelope for the transaction
logger.Debug("getting identity of auditor")
auditor := viewregistry.GetIdentityProvider(context).Identity("auditor")
if auditor == nil {
return "", errors.New("auditor identity not found")
}
tx, err := ttx.NewTransaction(context, nil, ttx.WithAuditor(auditor))
if err != nil {
return "", errors.Wrap(err, "failed creating transaction")
}
// The sender will select tokens owned by this wallet
logger.Debug("loading wallet [%s]", v.Wallet)
senderWallet := ttx.GetWallet(context, v.Wallet)
if senderWallet == nil {
return "", errors.Errorf("sender wallet [%s] not found", v.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())
// if err != nil {
// return "", errors.Wrap(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,
v.TokenType,
v.Quantity,
token2.WithTokenIDs(v.TokenIDs...),
)
if err != nil {
return "", errors.Wrap(err, "failed adding tokens to redeem")
}
// You can set any metadata you want. It is shared with the
// auditor but not committed to the ledger.
if v.Message != "" {
tx.SetApplicationMetadata("message", []byte(v.Message))
}
// 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.
logger.Infof("collecting signatures and submitting transaction to chaincode: [%s]", tx.ID())
_, err = context.RunView(ttx.NewCollectEndorsementsView(tx))
if err != nil {
return "", errors.Wrap(err, "failed to sign transaction")
}
// Send to the ordering service and wait for finality
logger.Infof("submitting fabric transaction to orderer for final settlemement: [%s]", tx.ID())
_, err = context.RunView(ttx.NewOrderingAndFinalityView(tx))
if err != nil {
return "", errors.Wrap(err, "failed to order or commit transaction")
}
return tx.ID(), nil
}

View file

@ -0,0 +1,159 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package service
import (
viewregistry "github.com/hyperledger-labs/fabric-smart-client/platform/view"
"github.com/hyperledger-labs/fabric-smart-client/platform/view/view"
"github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx"
"github.com/pkg/errors"
)
// SERVICE
// TransferTokens transfers an amount of a certain token. It connects to the other node, prepares the transaction,
// gets it approved by the auditor and sends it to the blockchain for endorsement and commit.
func (s TokenService) TransferTokens(tokenType string, quantity uint64, sender string, recipient string, recipientNode string, message string) (txID string, err error) {
logger.Infof("going to transfer %d %s from [%s] to [%s] on [%s] with message [%s]", quantity, tokenType, sender, recipient, recipientNode, message)
res, err := viewregistry.GetManager(s.FSC).InitiateView(&TransferView{
Transfer: &Transfer{
Wallet: sender,
TokenType: tokenType,
Quantity: quantity,
Recipient: recipient,
RecipientNode: recipientNode,
Message: message,
},
})
if err != nil {
logger.Error(err)
return
}
txID, ok := res.(string)
if !ok {
err = errors.New("cannot parse transfer response")
logger.Error(err)
return
}
return
}
// VIEW
// 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
// TokenType of tokens to transfer
TokenType string
// Quantity to transfer
Quantity uint64
// RecipientNode is the identity of the recipient's FSC node
RecipientNode string
// Recipient is the identity of the recipient's wallet
Recipient string
// Message is an optional user message sent with the transaction.
// It's stored in the ApplicationMetadata and is sent in the transient field.
Message string
}
type TransferView struct {
*Transfer
}
func (v *TransferView) Call(context view.Context) (interface{}, error) {
// As a first step operation, the sender tries its own node or contacts the recipients
// FSC node to ask for the identity to use to assign ownership of the freshly created token.
var recipient view.Identity
var err error
w := ttx.GetWallet(context, v.Recipient)
if w != nil {
// Get recipient identity from own wallet
logger.Infof("getting local identity for %s", v.Recipient)
recipient, err = w.GetRecipientIdentity()
if err != nil {
return nil, errors.Wrapf(err, "failed getting recipient identity from own node: %s", v.Recipient)
}
} else {
node := view.Identity(v.RecipientNode)
rec := view.Identity(v.Recipient)
eps := viewregistry.GetEndpointService(context)
if !eps.IsBoundTo(node, rec) {
logger.Infof("binding [%s] to node [%s]", v.Recipient, v.RecipientNode)
eps.Bind(node, rec) // TODO: it doesn't forget a wrong binding
}
// Request recipient identity from other node
logger.Infof("requesting [%s] identity from [%s]", v.Recipient, v.RecipientNode)
recipient, err = ttx.RequestRecipientIdentity(context, rec)
if err != nil {
return "", errors.Wrapf(err, "failed getting recipient identity from %s", v.RecipientNode)
}
}
// specify the auditor and create the envelope for the transaction
logger.Debug("getting identity of auditor")
auditor := viewregistry.GetIdentityProvider(context).Identity("auditor") // TODO: should not be hardcoded
if auditor == nil {
return "", errors.New("auditor identity not found")
}
tx, err := ttx.NewTransaction(context, nil, ttx.WithAuditor(auditor))
if err != nil {
return "", errors.Wrap(err, "failed creating transaction")
}
// The sender will select tokens owned by this wallet
senderWallet := ttx.GetWallet(context, v.Wallet)
if senderWallet == nil {
return "", errors.Errorf("sender wallet [%s] not found", v.Wallet)
}
// The sender adds a new transfer operation to the transaction following the instruction received.
// Notice the use of `token2.WithTokenIDs(v.TokenIDs...)`. If v.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,
v.TokenType,
[]uint64{v.Quantity},
[]view.Identity{recipient},
)
if err != nil {
return "", errors.Wrap(err, "failed adding new tokens")
}
// You can set any metadata you want. It is shared with the recipient and
// auditor but not committed to the ledger.
if v.Message != "" {
tx.SetApplicationMetadata("message", []byte(v.Message))
}
// 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.
logger.Infof("collecting signatures and submitting transaction to chaincode: [%s]", tx.ID())
_, err = context.RunView(ttx.NewCollectEndorsementsView(tx))
if err != nil {
return "", errors.Wrap(err, "failed to sign transaction")
}
// Send to the ordering service and wait for finality
logger.Infof("submitting fabric transaction to orderer for final settlemement: [%s]", tx.ID())
_, err = context.RunView(ttx.NewOrderingAndFinalityView(tx))
if err != nil {
return "", errors.Wrap(err, "failed to order or commit transaction")
}
return tx.ID(), nil
}

21
token-sdk/scripts/down.sh Executable file
View file

@ -0,0 +1,21 @@
#!/bin/bash
#
# This script fully tears down and deletes all artifacts from the sample network that was started with ./scripts/up.sh.
ls scripts/down.sh || { echo "run this script from the root directory: ./scripts/down.sh"; exit 1; }
TEST_NETWORK_HOME="${TEST_NETWORK_HOME:-$(pwd)/../test-network}"
ls "$TEST_NETWORK_HOME/network.sh" 1> /dev/null || { echo "Set the TEST_NETWORK_HOME environment variable to the directory of your fabric-samples/test-network; e.g.:
export TEST_NETWORK_HOME=\"$TEST_NETWORK_HOME\"
"; exit 1; }
docker-compose down
docker-compose -f compose-ca.yaml down
docker stop peer0org1_tokenchaincode_ccaas peer0org2_tokenchaincode_ccaas
bash "$TEST_NETWORK_HOME"/network.sh down
rm -rf keys
rm -rf data
rm tokenchaincode/zkatdlog_pp.json

View file

@ -0,0 +1,39 @@
#!/bin/bash
#
# Register and enroll all identities needed for the Token network.
set -Exeuo pipefail
# enroll admin
fabric-ca-client enroll -u http://admin:adminpw@localhost:27054
# Fabric Smart Client node identities (identity of the node, used when talking to other nodes)
for node in issuer auditor owner1 owner2
do
fabric-ca-client register -u http://localhost:27054 --id.name fsc${node} --id.secret password --id.type client
fabric-ca-client enroll -u http://fsc${node}:password@localhost:27054 -M "$(pwd)/keys/${node}/fsc/msp"
# make private key name predictable
mv "$(pwd)/keys/${node}/fsc/msp/keystore/"* "$(pwd)/keys/${node}/fsc/msp/keystore/priv_sk"
done
# Issuer and Auditor wallet users (non-anonymous)
fabric-ca-client register -u http://localhost:27054 --id.name auditor --id.secret password --id.type client
fabric-ca-client enroll -u http://auditor:password@localhost:27054 -M "$(pwd)/keys/auditor/aud/msp"
fabric-ca-client register -u http://localhost:27054 --id.name issuer --id.secret password --id.type client
fabric-ca-client enroll -u http://issuer:password@localhost:27054 -M "$(pwd)/keys/issuer/iss/msp"
# Owner wallet users (pseudonymous) on the owner1 node
fabric-ca-client register -u http://localhost:27054 --id.name alice --id.secret password --id.type client --enrollment.type idemix --idemix.curve gurvy.Bn254
fabric-ca-client enroll -u http://alice:password@localhost:27054 -M "$(pwd)/keys/owner1/wallet/alice/msp" --enrollment.type idemix --idemix.curve gurvy.Bn254
fabric-ca-client register -u http://localhost:27054 --id.name bob --id.secret password --id.type client --enrollment.type idemix --idemix.curve gurvy.Bn254
fabric-ca-client enroll -u http://bob:password@localhost:27054 -M "$(pwd)/keys/owner1/wallet/bob/msp" --enrollment.type idemix --idemix.curve gurvy.Bn254
# Owner wallet users (pseudonymous) on the owner2 node
fabric-ca-client register -u http://localhost:27054 --id.name carlos --id.secret password --id.type client --enrollment.type idemix --idemix.curve gurvy.Bn254
fabric-ca-client enroll -u http://carlos:password@localhost:27054 -M "$(pwd)/keys/owner2/wallet/carlos/msp" --enrollment.type idemix --idemix.curve gurvy.Bn254
fabric-ca-client register -u http://localhost:27054 --id.name dan --id.secret password --id.type client --enrollment.type idemix --idemix.curve gurvy.Bn254
fabric-ca-client enroll -u http://dan:password@localhost:27054 -M "$(pwd)/keys/owner2/wallet/dan/msp" --enrollment.type idemix --idemix.curve gurvy.Bn254

48
token-sdk/scripts/up.sh Executable file
View file

@ -0,0 +1,48 @@
#!/bin/bash
#
# This script generates crypto, starts Fabric, deploys the chaincode and starts the token nodes.
set -Eeuo pipefail
# Checks
ls scripts/up.sh || { echo "run this script from the root directory: ./scripts/up.sh"; exit 1; }
docker-compose version
git --version
go version
tokengen version || { echo "install tokengen (see readme)"; exit 1; }
[ ! -f data/auditor/kvs ] || { echo "delete existing crypto before running ./scripts/up.sh:
# ./scripts/down.sh"; exit 1; }
TEST_NETWORK_HOME="${TEST_NETWORK_HOME:-$(pwd)/../test-network}"
ls "$TEST_NETWORK_HOME/network.sh" 1> /dev/null || { echo "Set the TEST_NETWORK_HOME environment variable to the directory of your fabric-samples/test-network; e.g.:
export TEST_NETWORK_HOME=\"$TEST_NETWORK_HOME\"
"; exit 1; }
# Generate identities for the nodes, issuer, auditor and owner
mkdir -p keys/ca
docker-compose -f compose-ca.yaml up -d
while ! fabric-ca-client getcainfo -u localhost:27054 2>/dev/null; do echo "waiting for CA to start..." && sleep 1; done
./scripts/enroll-users.sh
# generate the parameters needed for the tokenchaincode
tokengen gen dlog --base 300 --exponent 5 --issuers keys/issuer/iss/msp --idemix keys/owner1/wallet/alice --auditors keys/auditor/aud/msp --output tokenchaincode
# Start Fabric network
bash "$TEST_NETWORK_HOME/network.sh" up createChannel
# copy the keys and certs of the peers, orderer and the client user
mkdir -p keys/fabric
cp -r "$TEST_NETWORK_HOME/organizations" keys/fabric/
# Install and start tokenchaincode as a service
INIT_REQUIRED="--init-required" "$TEST_NETWORK_HOME/network.sh" deployCCAAS -ccn tokenchaincode -ccp "$(pwd)/tokenchaincode" -cci "init" -ccs 1
# Start token nodes
mkdir -p data/auditor data/issuer data/owner1 data/owner2
docker-compose up -d
echo "
Ready!
Visit http://localhost:8080 in your browser to view the API documentation and try some transactions.
"

519
token-sdk/swagger.yaml Normal file
View file

@ -0,0 +1,519 @@
openapi: 3.0.3
info:
title: Tokens
version: "1.0"
description: |-
Issue, hold, transfer and redeem tokens. Backed by a distributed ledger,
preserving privacy using Zero Knowledge Proofs. This API definition fronts 4 different services, which each play a different
role in the ecosystem.
To try it out, let the Issuer issue some funds to one of the users. Watch the balances
get updated and see the transaction history. Transfer it to another account, or redeem
the tokens to destroy them.
servers:
- url: http://localhost:9000/api/v1/
description: auditor
- url: http://localhost:9100/api/v1/
description: issuer
- url: http://localhost:9200/api/v1/
description: alice and bob
- url: http://localhost:9300/api/v1/
description: carlos and dan
paths:
# Auditor
/auditor/accounts/{id}:
servers:
- url: http://localhost:9000/api/v1/
description: auditor
get:
tags:
- auditor
parameters:
- $ref: "#/components/parameters/id"
- $ref: "#/components/parameters/code"
required: true
responses:
"200":
$ref: "#/components/responses/AccountSuccess"
default:
$ref: "#/components/responses/ErrorResponse"
operationId: auditorAccount
summary: Get an account and their balance of a certain type
/auditor/accounts/{id}/transactions:
servers:
- url: http://localhost:9000/api/v1/
description: auditor
get:
tags:
- auditor
parameters:
- $ref: "#/components/parameters/id"
operationId: auditorTransactions
summary: Get all transactions for an account
responses:
"200":
$ref: "#/components/responses/TransactionsSuccess"
default:
$ref: "#/components/responses/ErrorResponse"
# Issuer
/issuer/issue:
servers:
- url: http://localhost:9100/api/v1/
description: issuer
summary: Issue tokens to an account
post:
tags:
- issuer
responses:
"200":
$ref: "#/components/responses/IssueSuccess"
default:
$ref: "#/components/responses/ErrorResponse"
requestBody:
description: Instructions to issue funds to an account
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/TransferRequest"
operationId: issue
summary: Issue tokens of any kind to an account
# Owner
/owner/accounts:
servers:
- url: http://localhost:9200/api/v1/
description: alice and bob
- url: http://localhost:9300/api/v1/
description: carlos and dan
get:
tags:
- owner
responses:
"200":
$ref: "#/components/responses/AccountsSuccess"
default:
$ref: "#/components/responses/ErrorResponse"
operationId: ownerAccounts
summary: Get all accounts on this node and their balances of each type
/owner/accounts/{id}:
servers:
- url: http://localhost:9200/api/v1/
description: alice and bob
- url: http://localhost:9300/api/v1/
description: carlos and dan
get:
tags:
- owner
parameters:
- $ref: "#/components/parameters/id"
- $ref: "#/components/parameters/code"
responses:
"200":
$ref: "#/components/responses/AccountSuccess"
default:
$ref: "#/components/responses/ErrorResponse"
operationId: ownerAccount
summary: Get an account and its balances of each token type
/owner/accounts/{id}/transactions:
servers:
- url: http://localhost:9200/api/v1/
description: alice and bob
- url: http://localhost:9300/api/v1/
description: carlos and dan
get:
tags:
- owner
parameters:
- $ref: "#/components/parameters/id"
operationId: ownerTransactions
summary: Get all transactions for an account
description: |
Note that the system uses Unspent Transaction Outputs (UTXO).
For a transfer, you'll often see two transactions with the same id. The user specified amount goes
to the counterparty, and some other amount (the remaining part of the input token) goes back to the
sender!
This could be confusing for a user. A simple solution is to filter out the transactions with the same
sender and recipient.
responses:
"200":
$ref: "#/components/responses/TransactionsSuccess"
default:
$ref: "#/components/responses/ErrorResponse"
/owner/accounts/{id}/transfer:
servers:
- url: http://localhost:9200/api/v1/
description: alice and bob
- url: http://localhost:9300/api/v1/
description: carlos and dan
summary: Transfer tokens to another account
post:
tags:
- owner
parameters:
- $ref: "#/components/parameters/id"
name: id
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/TransferRequest"
responses:
"200":
$ref: "#/components/responses/TransferSuccess"
default:
$ref: "#/components/responses/ErrorResponse"
operationId: transfer
summary: Transfer tokens to another account
/owner/accounts/{id}/redeem:
servers:
- url: http://localhost:9200/api/v1/
description: alice and bob
- url: http://localhost:9300/api/v1/
description: carlos and dan
summary: Redeem (burn) tokens
post:
tags:
- owner
parameters:
- $ref: "#/components/parameters/id"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RedeemRequest"
operationId: redeem
summary: Redeem (burn) tokens
responses:
"200":
$ref: "#/components/responses/RedeemSuccess"
default:
$ref: "#/components/responses/ErrorResponse"
# Operations
/healthz:
get:
tags:
- operations
operationId: healthz
summary: Returns 200 if the service is healthy
responses:
"200":
$ref: "#/components/responses/HealthSuccess"
"503":
$ref: "#/components/responses/ErrorResponse"
/readyz:
get:
tags:
- operations
operationId: readyz
summary: Returns 200 if the service is ready to accept calls
responses:
"200":
$ref: "#/components/responses/HealthSuccess"
"503":
$ref: "#/components/responses/ErrorResponse"
components:
responses:
AccountSuccess:
description: Success response
content:
application/json:
schema:
type: object
required:
- message
- payload
properties:
message:
type: string
payload:
$ref: "#/components/schemas/Account"
AccountsSuccess:
description: Success response
content:
application/json:
schema:
type: object
required:
- message
- payload
properties:
message:
type: string
payload:
type: array
items:
$ref: "#/components/schemas/Account"
TransactionsSuccess:
description: Success response
content:
application/json:
schema:
type: object
required:
- message
- payload
properties:
message:
type: string
example: "transactions retrieved"
payload:
type: array
items:
$ref: "#/components/schemas/TransactionRecord"
TransferSuccess:
description: Success response
content:
application/json:
schema:
type: object
required:
- message
- payload
properties:
message:
type: string
payload:
type: string
description: Transaction id
example:
message: transferred tokens
payload: 7c26ed6e5e05f7de81a82691b66b1069d1507d9edf3c7b78ea1fa98557ee57b4
RedeemSuccess:
description: Success response
content:
application/json:
schema:
type: object
required:
- message
- payload
properties:
message:
type: string
example: tokens redeemed
payload:
type: string
description: Transaction id
example: 7c26ed6e5e05f7de81a82691b66b1069d1507d9edf3c7b78ea1fa98557ee57b4
IssueSuccess:
description: Success or error response
content:
application/json:
schema:
type: object
required:
- message
- payload
properties:
message:
type: string
payload:
type: string
description: Transaction id
HealthSuccess:
description: Success response
content:
application/json:
schema:
type: object
required:
- message
properties:
message:
type: string
description: ok
example:
message: ok
ErrorResponse:
description: Error response
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
schemas:
# Common
Amount:
description: The amount to issue, transfer or redeem.
type: object
required:
- code
- value
properties:
code:
description: the code of the token
default: EURX
type: string
value:
description: value in base units (usually cents)
type: integer
format: int64
minimum: 0
example:
code: EURX
value: 100
Account:
type: object
description: Information about an account and its balance
required:
- id
- balance
properties:
id:
type: string
description: account id as registered at the Certificate Authority
example: alice
balance:
type: array
description: balance in base units for each currency
items:
$ref: "#/components/schemas/Amount"
example:
id: alice
balance:
- code: EURX
value: 10000
- code: USDX
value: 200
Counterparty:
description: The counterparty in a Transfer or Issuance transaction.
required:
- node
- account
type: object
properties:
node:
description: The node that holds the recipient account
type: string
account:
type: string
example:
node: owner1
account: alice
TransactionRecord:
type: object
description: A transaction
required:
- id
- sender
- recipient
- amount
- timestamp
- status
- message
properties:
id:
type: string
description: transaction id
sender:
type: string
description: the sender of the transaction
recipient:
type: string
description: the recipient of the transaction
amount:
$ref: "#/components/schemas/Amount"
timestamp:
type: string
format: date-time
description: 'timestamp in the format: "2018-03-20T09:12:28Z"'
status:
type: string
description: Unknown | Pending | Confirmed | Deleted
message:
type: string
description: user provided message
example:
id: 123
sender: alice
recipient: bob
amount:
code: EURX
value: 100
timestamp: "2018-03-20T09:12:28Z"
status: Confirmed
message: ""
# Owner
TransferRequest:
description: Instructions to issue or transfer tokens to an account
required:
- counterparty
- amount
type: object
properties:
amount:
$ref: "#/components/schemas/Amount"
counterparty:
$ref: "#/components/schemas/Counterparty"
message:
description: optional message that will be sent and stored with the transfer transaction
type: string
RedeemRequest:
description: Instructions to redeem tokens from an account
required:
- amount
type: object
properties:
amount:
$ref: "#/components/schemas/Amount"
message:
description: optional message that will be visible to the auditor
type: string
Error:
required:
- message
- payload
type: object
properties:
message:
description: High level error message
type: string
payload:
description: Details about the error
type: string
example:
message: error message
payload: ""
parameters:
id:
name: id
schema:
example: alice
description: account id as registered at the Certificate Authority
type: string
in: path
required: true
code:
name: code
in: query
schema:
example: EURX
description: The token code to filter on
type: string
securitySchemes: {}
headers: {}
tags:
- name: issuer
description: can create tokens of any kind, and send them to owner accounts
- name: owner
description: is the service that hosts the keys and accounts of end users
- name: auditor
description: gets to see every transaction in the clear, and keeps a history
- name: operations
description: Service health and readiness
security: []

View file

@ -0,0 +1,22 @@
FROM golang:1.20 as builder
RUN git clone https://github.com/hyperledger-labs/fabric-token-sdk.git
WORKDIR fabric-token-sdk
# Change the hash to checkout a different commit / version. It should be the same as in app/go.mod.
RUN git checkout v0.3.0 && go mod download
RUN CGO_ENABLED=1 go build -buildvcs=false -o /tcc token/services/network/fabric/tcc/main/main.go && chmod +x /tcc
# Final image
FROM golang:1.20
COPY --from=builder /tcc .
EXPOSE 9999
# zkatdlog is the output of the tokengen command. It contains the certificates
# of the issuer and auditor and the CA that issues owner account credentials,
# As well as cryptographic curves needed by the chaincode to verify proofs.
# It is generated once to initialize the network, when the 'init' function is
# invoked on the chaincode.
ENV PUBLIC_PARAMS_FILE_PATH=/zkatdlog_pp.json
ADD zkatdlog_pp.json /zkatdlog_pp.json
CMD [ "./tcc"]

BIN
token-sdk/transfer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB