Channel Manager for NFTs
Overview
Non-Fungible Tokens (NFTs) are becoming a craze these days as they provide the benefit of authority and authenticity of issuance and lifecycle of an asset. Issuing these NFTs on top of Algorand provides the higher speed, low cost and easy traceability for the parties involved with the digital asset.
In this solution, we are bringing Channel Manager, as a medium to issue and digitize NFTs, provide custodial solution by mapping each user to a mobile number, perform commerce like Buy, Sell, Re-sell and Send. For custodial solution, we are using Hashicorp Vault, which provide management of secrets on Amazon Web Service.
The functionality of channel manager can be decentralized by creating a governance protocol and giving control to multiple authorities depending on the use-case. In this example, we have created a channel manager for ticketing system, where multiple marketplaces can connect to the channel manager and digitize their tickets as an NFT and allow commerce using these digital tickets on top of Algorand blockchain.
For example, event organised in United Kingdom in hosting Katy Perry’s concert and wish to sell these tickets on multiple marketplaces like Ticketmaster, Eventbrite and multiple other local and self-hosted platforms. Event Organizer can connect to the channel manager and issue the tickets as NFTs and marketplace can make use of the same channel manager to perform commerce based on the conditions set by the event organizer. This way interoperability and transparency can be brought into the system. Reselling or sending tickets can be made possible with the low cost and scalability of Algorand, as this problem solves the ticket touting and ticket authenticity issue.
Custody
Blockchain can have immense advantages to consumers, however usability is the key issue. Here we are mapping each consumer’s mobile number to account address and private key on Algorand blockchain. This mapping is stores in Hashicorp vault, leveraging the safety and security of a software based security module. Channel Manager has the capability to provide different marketplaces with the facility to map Algorand address of their user with human understandable mobile number, and port their users on blockchain with ease.
func saveAddress(ctx context.Context, v vault.Vault, algo algorand.Algo,
addressPath string) error {
a, err := algo.GenerateAccount()
if err != nil {
return fmt.Errorf("saveAddress: error generating address:
%w", err)
}
path := fmt.Sprintf("%s/%s", v.UserPath, addressPath)
data := map[string]interface{}{
constants.AccountAddress: a.AccountAddress,
constants.PrivateKey: a.PrivateKey,
constants.SecurityPassphrase: a.SecurityPassphrase,
}
_, err = v.Logical().Write(path, data)
if err != nil {
return fmt.Errorf("saveAddress: unable to write to vault:
%w", err)
}
err = algo.Send(ctx, a, 5)
if err != nil {
return fmt.Errorf("saveAddress: error sending algos
to: %+v: err: %w", a, err)
}
return nil
}
func (u *User) userAddress(addressPath string) (*algorand.Account,
bool, error) {
path := fmt.Sprintf("%s/%s", u.Vault.UserPath, addressPath)
secret, err := u.Vault.Logical().Read(path)
if err != nil {
return nil, false, fmt.Errorf("userAddress: could not
get account of user: %d", addressPath)
}
accountAddress, accountAddressOK := secret.Data[constants.AccountAddress]
if !accountAddressOK {
return nil, false, fmt.Errorf("userAddress: account address not found")
}
privateKey, privateKeyOK := secret.Data[constants.PrivateKey]
if !privateKeyOK {
return nil, false, fmt.Errorf("userAddress: private key not found")
}
securityPassphrase, securityPassphraseOK :=
secret.Data[constants.SecurityPassphrase]
if !securityPassphraseOK {
return nil, false, fmt.Errorf("userAddress: security
passphrase not found")
}
ua := algorand.Account{
AccountAddress: accountAddress.(string),
PrivateKey: privateKey.(string),
SecurityPassphrase: securityPassphrase.(string),
}
return &ua, true, nil
}
Algorand provides various SDK support for generating address and performing various operations on Algorand blockchain.
type algo struct {
from *Account
apiAddress string
apiKey string
amountFactor uint64
minFee uint64
seedAlgo uint64
}
func (a *algo) GenerateAccount() (*Account, error) {
account := crypto.GenerateAccount()
paraphrase, err := mnemonic.FromPrivateKey(account.PrivateKey)
if err != nil {
return nil, fmt.Errorf("generateAccount: error generating account: %w", err)
}
return &Account{
AccountAddress: account.Address.String(),
PrivateKey: string(account.PrivateKey),
SecurityPassphrase: paraphrase,
}, nil
}
Digitisation
NFTs are represented as an Algorand Standard Asset with issuance parameter set as 1. This uniquely creates an NFT and here is our structure to represent
func (a *algo) CreateNFT(ctx context.Context, ac *Account) {
var headers []*algod.Header
headers = append(headers, &algod.Header{Key: "X-API-Key", Value: a.apiKey})
algodClient, err := algod.MakeClientWithHeaders(a.apiAddress, "", headers)
if err != nil {
return 0, fmt.Errorf("createAsset: error connecting to algo: %w", err)
}
txParams, err := algodClient.SuggestedParams()
if err != nil {
return 0, fmt.Errorf("createAsset: error getting suggested tx params: %w", err)
}
genID := txParams.GenesisID
genHash := txParams.GenesisHash
firstValidRound := txParams.LastRound
lastValidRound := firstValidRound + 1000
// Create an asset
// Set parameters for asset creation transaction
creator := ac.AccountAddress
assetName := "NAME"
unitName := "tickets"
assetURL := "URL"
assetMetadataHash := "thisIsSomeLength32HashCommitment"
defaultFrozen := false
decimals := uint32(0)
totalIssuance := uint64(1) //unique NFTs
manager := a.from.AccountAddress
reserve := a.from.AccountAddress
freeze := ""
clawback := a.from.AccountAddress
note := []byte(nil)
txn, err := transaction.MakeAssetCreateTxn(creator, a.minFee, firstValidRound,
lastValidRound, note,
genID, base64.StdEncoding.EncodeToString(genHash), totalIssuance, decimals,
defaultFrozen, manager, reserve, freeze, clawback,
unitName, assetName, assetURL, assetMetadataHash)
if err != nil {
return 0, fmt.Errorf("createAsset: failed to make asset: %w", err)
}
fmt.Printf("Asset created AssetName: %s\n", txn.AssetConfigTxnFields.AssetParams.AssetName)
privateKey, err := mnemonic.ToPrivateKey(ac.SecurityPassphrase)
if err != nil {
return 0, fmt.Errorf("createAsset: error getting private key from mnemonic: %w", err)
}
txid, stx, err := crypto.SignTransaction(privateKey, txn)
if err != nil {
return 0, fmt.Errorf("createAsset: failed to sign transaction: %w", err)
}
logger.Infof(ctx, "Signed txid: %s", txid)
// Broadcast the transaction to the network
txHeaders := append([]*algod.Header{}, &algod.Header{Key: "Content-Type", Value: "application/x-binary"})
sendResponse, err := algodClient.SendRawTransaction(stx, txHeaders...)
if err != nil {
return 0, fmt.Errorf("createAsset: failed to send transaction: %w", err)
}
}
Commerce operations like buy, sell, re-sell and resend are performed using various transaction mechanism using Purestake API for connecting to the Algorand blockchain.
func (a *algo) Send(ctx context.Context, to *Account, noOfAlgos uint64) error {
var headers []*algod.Header
headers = append(headers, &algod.Header{Key: "X-API-Key", Value: a.apiKey})
algodClient, err := algod.MakeClientWithHeaders(a.apiAddress, "", headers)
if err != nil {
return fmt.Errorf("send: error connecting to algo: %w", err)
}
txParams, err := algodClient.SuggestedParams()
if err != nil {
return fmt.Errorf("send: error getting suggested tx params: %w", err)
}
fromAddr := a.from.AccountAddress
toAddr := to.AccountAddress
amount := noOfAlgos * a.amountFactor
note := []byte(fmt.Sprintf("Transferring %d algos from %s", a.seedAlgo, a.from))
genID := txParams.GenesisID
genHash := txParams.GenesisHash
firstValidRound := txParams.LastRound
lastValidRound := firstValidRound + 1000
txn, err := transaction.MakePaymentTxnWithFlatFee(fromAddr, toAddr,
a.minFee, amount, firstValidRound,
lastValidRound, note, "", genID, genHash)
if err != nil {
return fmt.Errorf("send: error creating transaction: %w", err)
}
privateKey, err := mnemonic.ToPrivateKey(a.from.SecurityPassphrase)
if err != nil {
return fmt.Errorf("send: error getting private key from mnemonic: %w", err)
}
txId, bytes, err := crypto.SignTransaction(privateKey, txn)
if err != nil {
return fmt.Errorf("send: failed to sign transaction: %w", err)
}
logger.Infof(ctx, "Signed txid: %s", txId)
txHeaders := append([]*algod.Header{}, &algod.Header{Key: "Content-Type", Value: "application/x-binary"})
sendResponse, err := algodClient.SendRawTransaction(bytes, txHeaders...)
if err != nil {
return fmt.Errorf("send: failed to send transaction: %w", err)
}
logger.Infof(ctx, "send: submitted transaction %s", sendResponse.TxID)
return nil
}
Demo
Here is the demonstration of an event app connecting to the channel manager and solving the issues pertaining to ticketing world.
The app provides various use case implementation in Java and Kotlin. Rekey functionality and non-custodial features are also implemented as part of the android app code.
References
Android App : https://play.google.com/store/apps/details?id=com.eventersapp.marketplace
Channel Manager Code (in Golang) : https://github.com/eventers/Eventers-Marketplace-Backend
Android Code (in Kotlin + Java) : https://github.com/eventers/Eventers-Marketplace-Android