Remote Coffee Order System

Overview

As technology develops, ordering a cup of coffee is now possible without visiting a café. However, smartphone and web-based ordering systems require order processing systems and database operations, demanding considerable manpower and high costs for development and operation. As such, these online coffee ordering systems are run only by large coffee franchises with enough money and manpower for convenience. What if a high-performance public blockchain replaces this ordering system? Then, the online coffee ordering system will be available to anyone, including small cafés.

These are the steps for using the Klaytn blockchain and KAS to build an ordering system for online coffee orders and providing KLAY as an incentive to customers. With this, the order details will be stored in the Klaytn blockchain, and KLAY, NFT, and NT can be automatically transferred as rewards for the order.

The remote ordering system for cafés consists of the following three applications.

  1. “customer-front” - Mobile web app where café enthusiasts can purchase coffee

  2. “store-front” - Mobile web app for café owners to register locations, menus, and reward policies

  3. “service-backend” - A server, which is also a host using the KAS API, operated by a service host of the remote ordering platform for cafés that transfers requests from the customer-front and store-front users to the blockchain

The contents of this page include examples to foster the development of blockchain applications using KAS. The source code and other contents may be applied differently according to the user development environment. The user is solely responsible for using the source code and other contents.

For inquires about this document or KAS, please visit KAS Developer forum.

Getting Started

Installation

To install the 3 applications above, please visit the following link:

https://github.com/ground-x/kas-bapp-klaybucks.git

Build and Run

You can build and run each application using the commands introduced below under the kas-bapp-klaybucks directory.

customer-front

cd customer
npm install
npm start

store-front

cd store
npm install
npm start

service-backend

To build and run service-backend, you need to configure it first. Copy backend/config.template.json under the same directory and modify its name to config.json, and set the configuration details as below:

  • authorization: Enter KAS API Auth Key here.

  • kas_account: Enter Klaytn Account you created by KAS Wallet API here.

  • master_contract: (Optional) Enter your own master contract address here if you wish to use own Klaybucks Master Contract. To deploy Master Contract, please use backend/storeList.sol.

After finishing the configuration, you can run service-backend by running the commands below.

cd backend
go run .

Demo Screen

For inquires about this document or KAS, please visit KAS Developer forum.

Application Workflow

Features

Klaybucks records locations, menus, and trade histories in Klaytn. Therefore, café owners can continuously search and modify the required data for remote services, even if the service is interrupted. Here are the essential functions of Klaytn.

  1. Café owners may record locations, menu information, and reward policies in a contract.

  2. Customer accounts are created using the KAS Wallet API.

  3. Customers can check locations of nearby cafés registered to the Klaybucks service in the contract.

  4. Customers can select cafés to order from and search menus and reward policies in the contract.

  5. Customers can pay through KakaoPay or other payment systems and record the details in the contract.

  6. Café owners can check payment details registered in the contract and accept or reject orders.

These functions use Klayt as one of the application storages and can also read and write data. Therefore, it is crucial to create a contract to store data and define a proper data structure.

In the store-front, café owners can deploy individual store contracts for storing café menus and order details. Store contracts provide functions to store and search for values in the following data structures, and café owners own store contracts accordingly.

enum MenuStatus {activated, deactivated}
enum OrderStatus {ordered, approved, denied}
enum RewardPolicyStatus {activated, deactivated}

struct Menu {
    string name;
    uint32 price;
    uint256 reward; // digit: peb
    MenuStatus status;
}

struct Order {
    address payable customer;
    string content;
    uint32 price;
    OrderStatus status;
}

event OrderReciept(
    uint32 indexed _id,
    address indexed _from,
    string _paymentTxId,
    string _content,
    uint32 _price
);

Individual store contracts are deployed through the process above and then registered to a master contract. A Klaybucks service owner becomes the owner of the master contract that stores locations of cafés and individual store contract addresses in the following formats. Store data is collected and managed in a map or a list so that customers can search for registered cafés at once.

enum Status {activated, deactivated}
struct Store {
    string name;
    string locationX;
    string locationY;
    address storeOwner;
    address storeContract;
    Status status;
}

Klaybucks uses the KAS API to utilize the implemented contracts easily and quickly. Here are descriptions and code samples of the important functions of the service (detailed implementation code can be found at GitHub).

*While the service-backend calls the KAS API, the customer-front or store-front receives the required parameter values from users and transfers them to the service-backend. Such a design prevents API keys from being exposed to the public using KAS.

For inquires about this document or KAS, please visit KAS Developer forum.

Create Contracts for Cafe

Klaybucks uses two types of contracts. The first is a master contract deployed once at the beginning of service and stores locations and names of Klaybucks cafés. The second is a store contract, where individual cafés can deploy unique store contracts when registering to Klaybucks. This contract stores menus, reward policies, and trade details of the corresponding café.

To deploy contracts, Klaybucks uses Deploy Smart Contract: Transaction Fee Delegation by User of the Wallet API. The following code is an example of deploying a store contract in service-backend implemented with golang (actual code can be found at GitHub). The example assumes that the bytecode and ABI of the store contract required for using KAS API are already defined in string formats. The store contract is implemented to receive the store name and owner as input parameters when the constructor function is executed. In this example, the values are received from users and converted to appropriate forms for using the KAS API.

// DeployStoreContract handles Store Contract deploying requests with the given store name and the owner address.
func DeployStoreContract(c *gin.Context) {
    url := "https://wallet-api.klaytnapi.com/v2/tx/fd/contract/deploy"
    params := struct {
        Name string `form:"name" json:"name"`
        Owner string `form:"owner" json:"owner"`
    }{}

    // Step 1. Get user inputs from the request
    if err := c.ShouldBindJSON(&params); err != nil {
        c.String(-1, "invalid input data" + err.Error())
        return
    }

    // Step 2. Prepare field values of contract deploy transaction.
    // STORE_CONTRACT_ABI and STORE_CONTRACT_CODE are generated from the solidity code of Store Contract.
    _abi , err := abi.JSON(bytes.NewBufferString(STORE_CONTRACT_ABI))
    if err != nil {
        c.String(-1, err.Error())
        return
    }

    // Pack function convert user inputs to the rlp-encoded constructor parameter.
    inputParam, err := _abi.Constructor.Inputs.Pack(params.Name, common.HexToAddress(params.Owner))
    if err != nil {
        c.String(-1, err.Error())
        return
    }

    // input format of the KAS API, /v2/tx/fd/contract/deploy
    bodyData, err := json.Marshal(&kasDeployTxFD{
        From: KAS_ACCOUNT,
        Value: "0x0",
        GasLimit: 8000000,
        Input: STORE_CONTRACT_CODE + Encode(inputParam),
        Submit: true,
    })
    if err != nil {
        c.String(-1, err.Error())
        return
    }

    // Step 3. Call a KAS API with parameters and HTTP headers.
    // "Authorization" header should be set with your KAS API Auth Key (e.g., "Basic xxxx...") .
    // "x-krn" header should indicate your KAS Wallet resource (e.g., "krn:1001:wallet:80:account:default").
    req, err := http.NewRequest("POST", url, bytes.NewBuffer(bodyData))
    if err != nil {
        c.String(-1, err.Error())
        return
    }
    req.Header.Add("Content-Type", "application/json")
    req.Header.Add("Authorization", KAS_API_KEY)
    req.Header.Add("x-krn", KAS_WALLET_KRN)

    res, err := httpClient.Do(req)
    if err != nil {
        c.String(-1, err.Error())
        return
    }

    // Step 4. Read and return back to the user the response of KAS.
    bodyContents, err := ioutil.ReadAll(res.Body)
    if err != nil {
        c.String(-1, err.Error())
        return
    }
    _ = res.Body.Close()

    c.String(200, string(bodyContents))
}

For inquires about this document or KAS, please visit KAS Developer forum.

Register Cafe Menu/Reward and Klaybucks Service

The café owner can register the address and location information of the contract he deployed to the master contract to be registered to the Klaybucks service. Customers search for this contract first when looking up the registered cafés of Klaybucks.

To execute contracts, Klaybucks uses Execute Smart Contract: Transaction Fee Delegation by User of the Wallet API. The following code is an example of executing a master contract in service-backend implemented with golang (actual code can be found at GitHub). The example assumes that the ABI of the master contract required for using KAS the API is already defined in string format.

// AddStoreInfo registers a new store to Master Contract
func AddStoreInfo(c *gin.Context) {
    url := "https://wallet-api.klaytnapi.com/v2/tx/fd/contract/execute"
    params := struct {
        Name string `json:"name"`
        X string `json:"x"`
        Y string `json:"y"`
        Owner string `json:"owner"`
        ContractAddr string `json:"contract_addr"`
    }{}

    // Step 1. Get user inputs from the request
    if err := c.ShouldBindJSON(&params); err != nil {
        c.String(-1, "invalid input data" + err.Error())
        return
    }

    // Step 2. Prepare field values of contract execution transaction.
    // MASTER_CONTRACT_ABI is generated from the solidity code of Store Contract.
    _abi , err := abi.JSON(bytes.NewBufferString(MASTER_CONTRACT_ABI))
    if err != nil {
        c.String(-1, err.Error())
        return
    }

    // Pack function convert user inputs to the rlp-encoded parameter.
    // "addStore" is a function of Master Contract.
    inputParam, err := _abi.Pack("addStore", params.Name, params.X, params.Y, common.HexToAddress(params.Owner), common.HexToAddress(params.ContractAddr))
    if err != nil {
        c.String(-1, err.Error())
        return
    }

    // input format of the KAS API, /v2/tx/fd/contract/execute
    bodyData, err := json.Marshal(&kasExecuteTxFD{
        From: KAS_ACCOUNT,
        To: MasterContract,
        Value: "0x0",
        GasLimit: 8000000,
        Input: Encode(inputParam),
        Submit: true,
    })
    if err != nil {
        c.String(-1, err.Error())
        return
    }

    // Step 3. Call a KAS API with parameters and HTTP headers.
    // "Authorization" header should be set with your KAS API Auth Key (e.g., "Basic xxxx...") .
    // "x-krn" header should indicate your KAS Wallet resource (e.g., "krn:1001:wallet:80:account:default").
    req, err := http.NewRequest("POST", url, bytes.NewBuffer(bodyData))
    if err != nil {
        c.String(-1, err.Error())
        return
    }
    req.Header.Add("Content-Type", "application/json")
    req.Header.Add("Authorization", KAS_API_KEY)
    req.Header.Add("x-krn", KAS_WALLET_KRN)

    res, err := httpClient.Do(req)
    if err != nil {
        c.String(-1, err.Error())
        return
    }

    // Step 4. Read and return back to the user the response of KAS.
    bodyContents, err := ioutil.ReadAll(res.Body)
    if err != nil {
        c.String(-1, err.Error())
        return
    }
    _ = res.Body.Close()

    c.String(200, string(bodyContents))
}

With the same API, café owners can register or modify menus and reward policies in their respective contracts. Similarly, they can update the operation state of their cafés anytime.

For inquires about this document or KAS, please visit KAS Developer forum.

Check Transaction Receipt

Once a created Klaytn transaction is entered into a block, a transaction receipt is produced and processed instantly. Therefore, it is required to check transaction receipts and processing results after deploying transactions.

Klaybucks uses the KAS Node API to check transaction receipts. With the KAS Node API, APIs of the Klaytn Node can be used in the same ways. Here is an example of using the “klay_getTransactionReceipt” method to check transaction receipts in the service-backend implemented with golang.

func GetTxReceipt(c *gin.Context) {
    url := "https://node-api.klaytnapi.com/v2/klaytn"
    params := struct {
        TxHash string `form:"tx_hash" json:"tx_hash"`
    }{}

    // Step 1. Get user inputs from the request
    if err := c.ShouldBindQuery(&params); err != nil {
        c.String(-1, "invalid input data" + err.Error())
        return
    }

    // Step 2. Prepare a Klaytn node method and parameters to use KAS Node API.
    // You can see the detailed information of node methods from
    // https://docs.klaytn.com/bapp/json-rpc/api-references
    bodyData, _ := json.Marshal(&kasKlaytnRPC{
        JsonRpc: "2.0",
        Method: "klay_getTransactionReceipt",
        Params: []string{params.TxHash},
        Id: 1,
    })

    // Step 3. Call a KAS API with parameters and HTTP headers.
    // "Authorization" header should be set with your KAS API Auth Key (e.g., "Basic xxxx...") .
    // "x-krn" header should indicate your KAS Node resource (e.g., "krn:1001:node").
    req, err := http.NewRequest("POST", url, bytes.NewBuffer(bodyData))
    if err != nil {
        c.String(-1, err.Error())
        return
    }
    req.Header.Add("Content-Type", "application/json")
    req.Header.Add("Authorization", KAS_API_KEY)
    req.Header.Add("x-krn", KAS_NODE_KRN)

    res, err := httpClient.Do(req)
    if err != nil {
        c.String(-1, err.Error())
        return
    }

    // Step 4. Read and return back to the user the response of KAS.
    bodyContents, err := ioutil.ReadAll(res.Body)
    if err != nil {
        c.String(-1, err.Error())
        return
    }
    _ = res.Body.Close()

    c.String(200, string(bodyContents))
}

Remember that receipts are not determinately created when checking transaction receipts. The Klaytn Network can create blocks quickly so that a transaction can generally be processed within a second, but the duration depends on network latency or transaction traffic. As such, a transaction may not have been processed when trying to check a receipt. In this case, it is recommended to make repetitive requests for the receipt until the transaction is processed.

In Klaybucks, the front code makes repetitive requests for a receipt. Adding a repetition loop in the service-backend implemented with golang is also possible, but user requests may be blocked in this case. To avoid it, Klaybucks provides a logic for the front code implemented with javascript to check for the receipt and make repetitive requests if it does not exist.

function CheckTransactionReceipt(txHash) {
  let self = this;
  function getReceipt(txHash) {
    axios
      .get(klaybucks_backend + "/klaytn/receipt?tx_hash=" + txHash)
      .then((response) => {
        let result = response.data["result"];
        if (result !== null) {
          clearInterval(self.intervalId2);
          // check the result and process next steps
        }
      })
      .catch((error) => {
        console.log(error);
      });
  }

  self.intervalId2 = setInterval(getReceipt, 1000, txHash);
  setTimeout(clearInterval, 10000, self.intervalId2);
}

For inquires about this document or KAS, please visit KAS Developer forum.

Create Customer's Klaytn account

In Klaybucks, a Klaytn account must be created to use the customer-front for café customers. This account makes a transaction when a customer places an order, and any KLAY rewards from the café will be received through this account address. With that, customer behaviors in Klaybucks belong to and are stored in this account.

A burden is placed on the user for managing the private key if a Klaybucks service account is replaced with a Klaytn account. The user must store and manage the complicated private key, so there is no way to retrieve the key if it is lost. To reduce inconvenience in using the blockchain, Klaybucks uses the KAS Wallet API to entrust the management of private keys of café customers. In the following code, Create Account of the KAS Wallet API is used to create a customer account in the service-backend implemented with golang.

func CreateAccount(c *gin.Context){
    url := "https://wallet-api.klaytnapi.com/v2/account"

    // Step 1. Call a KAS API with parameters and HTTP headers.
    // "Authorization" header should be set with your KAS API Auth Key (e.g., "Basic xxxx...") .
    // "x-krn" header should indicate your KAS Wallet resource (e.g., "krn:1001:wallet:80:account:default").
    req, err := http.NewRequest("POST", url, nil)
    if err != nil {
        c.String(-1, err.Error())
        return
    }
    req.Header.Add("Content-Type", "application/json")
    req.Header.Add("Authorization", KAS_API_KEY)
    req.Header.Add("x-krn", KAS_NODE_KRN)

    res, err := httpClient.Do(req)
    if err != nil {
        c.String(-1, err.Error())
        return
    }

    // Step 2. Read and return back to the user the response of KAS.
    bodyContents, err := ioutil.ReadAll(res.Body)
    if err != nil {
        c.String(-1, err.Error())
        return
    }
    _ = res.Body.Close()

    c.String(200, string(bodyContents))
}

For inquires about this document or KAS, please visit KAS Developer forum.

Get Cafe Information

Once the account is created, the customer loads data from the master contact to search for registered Klaybucks cafés. The menu information is additionally loaded in the same way when the customer selects a café.

Moreover, Klaybucks uses the Node API to execute a function for searching data in the contract. The following code is an example of executing a master contract in the service-backend implemented with golang (actual code can be found at GitHub). It assumes that the ABI of the master contract required for using the KAS API is already defined in string format.

func GetStores(c *gin.Context) {
    url := "https://node-api.klaytnapi.com/v2/klaytn"

    // Step 1. Prepare "klay_call" API among Node APIs
    bodyData, _ := json.Marshal(&kasKlaytnRPC{
        JsonRpc: "2.0",
        Method: "klay_call",
        Params: []interface{}{
            structCall{
                From: KASAccount,
                To: MASTER_CONTRACT,
                Gas: "0x7A1200", // 8000000
                Data: MASTER_CONTRACT_ABI_GETSTORE,
            },
            "latest",
        },
        Id: 1,
    })

    // Step 2. Call a KAS API with parameters and HTTP headers.
    // "Authorization" header should be set with your KAS API Auth Key (e.g., "Basic xxxx...") .
    // "x-krn" header should indicate your KAS Node resource (e.g., "krn:1001:node").
    req, err := http.NewRequest("POST", url, bytes.NewBuffer(bodyData))
    if err != nil {
        c.String(-1, err.Error())
    }
    req.Header.Add("Content-Type", "application/json")
    req.Header.Add("Authorization", KAS_API_KEY)
    req.Header.Add("x-krn", KAS_NODE_KRN)

    res, err := httpClient.Do(req)
    if err != nil {
        c.String(-1, err.Error())
        return
    }

    bodyContents, err := ioutil.ReadAll(res.Body)
    if err != nil {
        c.String(-1, err.Error())
        return
    }
    res.Body.Close()

    // Step 3. Parse the return of Node API
    var rpcRet struct {
        JsonRPC string `json:"jsonrpc"`
        Id int `json:"id"`
        Result string `json:"result"`
    }

    if err := json.Unmarshal(bodyContents, &rpcRet); err != nil {
        c.String(-1, err.Error())
        return
    }

    // Step 4. Parse the store information which is stored as an array in the Contract
    _abi , err := abi.JSON(bytes.NewBufferString(MASTER_CONTRACT_ABI))
    if err != nil {
        c.String(-1, err.Error())
        return
    }

    var storeArray []struct {
        Name string `json:"name"`
        X string `json:"x"`
        Y string `json:"y"`
        StoreOwner common.Address `json:"store_owner"`
        StoreContract common.Address `json:"store_contract"`
        Status uint8 `json:"status"`
    }

    if err := _abi.Unpack(&storeArray, "getStores", hexutil.MustDecode(rpcRet.Result)); err != nil {
        c.String(-1, err.Error())
        return
    }

    c.JSON(200, storeArray)
}

After searching for the registered cafés as above, the customer uses the same API to look for other cafés. Once the customer selects a menu and places an order, the order details are registered to the contract, in the same way, using the API used in “2. Register Cafe Menu/Reward and Klaybucks Service.” It is also required to perform “3. Check Transaction Receipt” to check receipts of registered transactions.

For inquires about this document or KAS, please visit KAS Developer forum.

Get Order History

Once the customer registers the order details to the store contract of a specific café, the café owner can then search and check the receipt in the Klaytn block.

Therefore, café owners are required to continuously check if there are receipts, which are created through their store contracts, in every block.

// Retrieves the receipt logs of the given contract address
RetrieveLogs = (address) => {
  let self = this;
  function getReceipt(address) {
    let hexBlockNumber = caver.utils.toHex(self.state.blockNumber);
    axios
      .get(
        klaybucks_backend +
          "getReceipts?from_block=" +
          hexBlockNumber +
          "&to_block=" +
          hexBlockNumber +
          "&contract=" +
          address
      )
      .then((response) => {
        self.setState((current) => ({
          blockNumber: self.state.blockNumber + 1,
        }));
        if (response.data !== null) {
          let current_receipts = response.data;
          // Add validation logic for the receipts
          current_receipts.map((item) => {
            let order = qs.parse(item.content);
            let receipt = {
              index: item.index,
              order: JSON.stringify(order),
              price: item.price,
              reward: self.CalculateReward(order),
              tid: item.payment_tx_id,
              status: "ordered",
            };
            self.setState((current) => ({
              receipts: self.state.receipts.concat(receipt),
            }));
            return item;
          });
        }
      })
      .catch((error) => {
        console.log(error);
      });
  }
  self.intervalId = setInterval(getReceipt, 1000, address);
};

Once the order details are confirmed through the process, update the order status value to “Accepted” or “Rejected” using APIs cited in “2. Register Cafe Menu / Reward and Klaybucks Service” and “3. Check Transaction Receipt.” The customer can also check if the order was accepted by looking up the updated order status through the same process.

For inquires about this document or KAS, please visit KAS Developer forum.

Last updated