Tutorial

This tutorial will walk you through building a loans application from scratch using the QED private asset transfers. In the end, you will have in your hands a fully functional application in Go.

To get access to a testing environment, please see the Request an API key page!

Prelude

The QED solution is operated using an HTTP API. This API allows you to transfer, issue and view assets you own. Additionally, this API allows you to manage your wallets.

We provide ready-made SDKs to integrate into your application, as described in the SDKs page.

Create the skeleton

The easiest way to get started is to use Go 1.11 and create a Go module.

To do this, create a new directory anywhere and then run go mod init github.com/bank/loans, declaring your module URL (obviously, modify bank/loans to your actual URL).

Next, create a main.go file importing the SDK:

package main

import (
  "fmt"
  "github.com/QED-it/asset_transfers_dev_guide/go/examples/util"
)

func main() {
  _, _, err := util.InitAPIClient()
  if err != nil {
    util.HandleErrorAndExit(fmt.Errorf("couldn't get a valid config"))
  }
}

When building the application, Go will automatically detect the import to the SDK and add it to go.mod:

module github.com/bank/loans

require (
  github.com/QED-it/asset_transfers_dev_guide/go SOME_COMMIT
)

You may notice we also use the optional util package from the repository to parse flags and handle errors.

You can now run the application using go run main.go. It doesn’t do much, but we have our skeleton ready!

Initialize the API client

The first thing you will notice is that the application complains it requires an asset transfers node url. You should have received this from us when you requested an API key.

Supply the URL and API key values as parameters: go run main.go -url http://localhost:12052 -key API_KEY.

It still doesn’t do much, but at least it doesn’t complain!

Generate a wallet

At this point, your instance doesn’t contain wallets. When you received an API key, you should also have received a wallet private key loaded with some assets. Let’s ignore that for now and generate a new wallet, which will be used, for example, to cater your potential bank users.

To do that, you use the /node/generate_wallet endpoint.

The endpoint requires two parameters: wallet_label and authorization. The wallet labels will be used in subsequent calls to identify the wallet storing your assets and the authroziation is the password encrypting it, since the wallet is never in plain-text besides after being decrypted in-memory.

Let’s generate a wallet for Jane. To do that, we modify our application to receive the client object and use that:

func main() {
     client, _, err := util.InitAPIClient()
     if err != nil {
             util.HandleErrorAndExit(fmt.Errorf("couldn't get a valid config"))
     }
     generateWalletRequest := openapi.GenerateWalletRequest{
             WalletLabel:   "Jane",
             Authorization: "123456",
     }

     ctx := context.Background()

     _, err = client.NodeApi.NodeGenerateWalletPost(ctx, generateWalletRequest)
     if err != nil {
             util.HandleErrorAndExit(fmt.Errorf("couldn't generate wallet: %v", err))
     }
}

This wallet is now accessible using the wallet label Jane in the different API endpoints.

Import an issuer wallet

Now that we have a wallet for Jane, let’s make sure she has some assets.

To do that, you can use the special private key you’ve received, bank, which has issuance permissions. The different kinds of issuance permissions are described in the Concepts page.

To use this wallet, you have to import the private key using the /node/import_wallet endpoint:

importWalletRequest := openapi.ImportWalletRequest{
       WalletLabel:   "bank",
       EncryptedSk:   "YOUR_PRIVATE_KEY",
       Authorization: "123456",
       Salt:          "bed104e2358a36053fb3a5dacb4ed34a2a612a2e10aa521dd166032e9b3343d5",
}

_, err = client.NodeApi.NodeImportWalletPost(ctx, importWalletRequest)
if err != nil {
       util.HandleErrorAndExit(fmt.Errorf("couldn't import wallet: %v", err))
}

In this case, the imported bank wallet has the ability to issue assets of a specific range (let’s assume you were given the range 100-200) or the ability to issue any asset confidentially. Assigning meaning to these numbers, let’s decide that each asset ID refers to a different loan that the bank can give. This is essentially a non-fungible token.

Get a new address

To issue assets to Jane, we need to have the address of her destination wallet. Since we are issuing assets to Jane, let’s request a new address for her wallet:

getNewAddressRequest := openapi.GetNewAddressRequest{
  WalletLabel: "Jane",
  Diversifier: "69be9d33a15535a59dd111",
}

getNewAddressResponse, _, err := client.WalletApi.WalletGetNewAddressPost(ctx, getNewAddressRequest)
if err != nil {
  util.HandleErrorAndExit(fmt.Errorf("couldn't generate address: %v", err))
}

fmt.Printf("new address details: %v", getNewAddressResponse)

You may have noticed the diversifier in the request. The diversifier is a random 11-byte string that allows you to create unlinkable addresses for a single wallet. This means that in order to receive assets you can dispense as many addresses as you’d like with different diversifiers, and nobody will know they’re related to the same wallet.

Issue some assets

We are now ready to issue assets to Jane! We will use the address we received from the previous request (which we printed out). Let’s say we received the following:

{
  "recipient_address": {
    "d": "69be9d33a15535a59dd111",
    "pkd": "bed104e2358a36053fb3a5dacb4ed34a2a612a2e10aa521dd166032e9b3343d5"
  },
}

To do that as the bank, we use the /wallet/issue_asset endpoint:

issueAssetRequest := openapi.IssueAssetRequest{
     WalletLabel: "bank",
     RecipientAddress: openapi.TransferAssetRequestRecipientAddress{
             D:   "69be9d33a15535a59dd111",
             Pkd: "bed104e2358a36053fb3a5dacb4ed34a2a612a2e10aa521dd166032e9b3343d5",
     },
     AssetId:    200,
     Amount:     1,
     ClearValue: true,
     Memo:       "this is for you, Jane",
}

_, err = client.WalletApi.WalletIssueAssetPost(ctx, issueAssetRequest)
if err != nil {
     util.HandleErrorAndExit(fmt.Errorf("couldn't issue asset: %v", err))
}

Many details, so let’s break them down one by one:

  • WalletLabel: The wallet issuing the asset. In our case it’s bank.
  • Authoriztaion: The password for the wallet, which we chose at the time of import.
  • RecipientAddress: Jane’s address, which we generated before.
  • AssetId: The self-chosen asset ID. The bank is the one assigning meaning to it.
  • Amount: The amount of the asset we’re issuing. Since it’s a non-fungible asset, it should have amount 1, so there can only be one of it.
  • ClearValue: Does this issuance expose the value to the public? In some cases it makes sense - for example, when issuing publicly traded stocks. In our case, it’s a private loan, so probably not. Let’s put it as false. Since our wallet is allowed to issue assets confidentially, it will succeed.
  • Memo: An arbitrary message, encrypted to the recipient.

Congratulations! You have issued an asset as the bank to Jane.

Viewing your balance

Jane, after a block has been mined, can now check that she received the asset using the /wallet/get_wallet_balances:

time.Sleep(20)

getWalletBalancesRequest := openapi.GetWalletBalanceRequest{
     WalletLabel: "Jane",
}

getWalletBalancesResponse, _, err := client.WalletApi.WalletGetWalletBalancesPost(ctx, getWalletBalancesRequest)
if err != nil {
     util.HandleErrorAndExit(fmt.Errorf("couldn't get wallet balances: %v", err))
}

fmt.Printf("wallet balances: %v", getWalletBalancesResponse)

Additionally, Jane will be able to examine her transaction history, containing all the assets she has sent and received, using the /analytics/get_transactions endpoint:

getTransactionsRequest := openapi.GetTransactionsRequest{
  WalletLabel:     "Jane",
  StartIndex:      0,
  NumberOfResults: 10,
}

getTransactionsResponse, _, err := client.AnalyticsApi.AnalyticsGetTransactionsPost(ctx, getTransactionsRequest)
if err != nil {
  util.HandleErrorAndExit(fmt.Errorf("couldn't get transactions: %v", err))
}

fmt.Printf("transactions: %v", getTransactionsResponse)

Transfer an asset

Finally, Jane owns some assets and she can transfer them to John (who gave her some recipient_address)! To do that, she uses the /wallet/transfer_asset endpoint:

getTransactionsRequest := openapi.GetTransactionsRequest{
     WalletLabel:     "Jane",
     StartIndex:      0,
     NumberOfResults: 10,
}

getTransactionsResponse, _, err := client.AnalyticsApi.AnalyticsGetTransactionsPost(ctx, getTransactionsRequest)
if err != nil {
     util.HandleErrorAndExit(fmt.Errorf("couldn't get transactions: %v", err))
}

fmt.Printf("transactions: %v", getTransactionsResponse)

transferAssetRequest := openapi.TransferAssetRequest{
     WalletLabel: "bank",
     RecipientAddress: openapi.TransferAssetRequestRecipientAddress{
             D:   "69be9d33a15535a59dd111",
             Pkd: "bed104e2358a36053fb3a5dacb4ed34a2a612a2e10aa521dd166032e9b3343d5",
     },
     AssetId: 200,
     Amount:  1,
     Memo:    "Getting rid of my asset",
}

_, err = client.WalletApi.WalletTransferAssetPost(ctx, transferAssetRequest)
if err != nil {
     util.HandleErrorAndExit(fmt.Errorf("couldn't transfer asset: %v", err))
}

time.Sleep(20)

After a block is mined, Jane will no longer the owner of the asset, and this fact can be verified by another call to the /analytics/get_transactions endpoint.

Did we miss anything?

Some notable features that we didn’t cover are:

  • Unlocking wallets - an intensive operation that stores the wallet in-memory, when in situations where there is a high volume of transfers from the same wallet.
  • Configuration transactions - A method of updating the allowed issuers and their permission to issue different kinds of assets.
  • Getting blocks - raw analytics data about transactions that have occured in the network, not only your own. Obviously, you will not be able to see any private transfer deatils there.

Additionally, we’ve done all of the requests to one node in the network. In a real deployment, you would have different nodes for the bank and Jane. It would all work the same.

Conclusion

The code presented here is available in the asset_transfers_dev_guide repository.

That’s it! This has been a walkthrough of some the features in the private asset transfers solution and how you can model it around loans.