Create Publication

We are looking for publications that demonstrate building dApps or smart contracts!
See the full list of Gitcoin bounties that are eligible for rewards.

Solution Thumbnail

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

EditorImages/2021/03/26 15:44/social_sign_in.jpeg

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