Overview
It is common for sellers to show receipts when selling used luxury goods as it proves the authenticity of the product. On the other hand, it is more difficult to make fake receipts than find imitations. As such, the original receipt must be kept for selling used products in the future. However, holding or storing these receipts may be difficult because they are small, thin, and fragile. Moreover, the content on the receipt’s thermal paper can be easily damaged by heat or light.
If the receipt of a luxury item is kept in the blockchain, it will be kept safe permanently for future use. This tutorial describes the decentralized application for maintaining and sending receipts on the blockchain platform, Kaia. This allows users to issue electronic receipts using the platform Non-fungible Token (Non-fungible Token, NFT), KIP-17 so that they can be used to sell used products. The receipt is deployed to Kaia for prolonged storage as long as the receipt owner keeps the private key. Therefore, the receipt can be safely and efficiently delivered to the new owner of the used luxury item, and, in turn, the new owner can also give the receipt when he sells the product.
One luxury item means one NFT (KIP-17) smart contract, and one receipt equals one NFT token. The management function can be added to each premium brand or store that sells the brand, but this tutorial assumes that there is only one store.
Here are the key functions of this application.
- Deployment of a new NFT smart contract = The prestige store registers a new luxury item (on the blockchain).
- Issuance of a new NFT token = The store sells a luxury item and issues an electronic receipt to the customer (on the blockchain).
- NFT token transmission = The receipt is sent (on the blockchain) when the customer sells his used product to another person.
The following system has been configured to implement the above functions.
- Frontend: React
-
Backend: Node.js,
caver-js
- caver-js: KIP-17 Token Contract Deployment, Issuing Tokens, Sending Tokens
-
DB: MySQL
- Save goods and receipts information for premium shops
-
KAS: Node API, Token History API
- Use Kaia Endpoint Node by Node API
- Get Token Issuing History by Token History API
warning
The contents of this page include examples to foster the development of decentralized 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 Developers Forum.
Getting Started
Installation
- Node.js & yarn
-
MySQL
- In the code, both the ID and the password should be "root."
- There are tables in the "test" DB. As such, create a "test" database if there is no existing DB.
For inquires about this document or KAS, please visit KAS Developers Forum.
Setting up Database
- Two (2) tables (product and receipt) are required.
- The "product" is a table for managing registered luxury goods.
CREATE TABLE `product` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(64) NOT NULL,
`image` VARCHAR(128) NOT NULL,
`contractAddr` VARCHAR(256) DEFAULT NULL,
`registeredDate` DATETIME NOT NULL,
`isDeleted` INT NOT NULL,
`symbol` VARCHAR(64) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = INNODB AUTO_INCREMENT = 22 DEFAULT CHARSET = utf8;
- The "receipt" is a table for managing issued receipts.
CREATE TABLE `receipt` (
`id` INT NOT NULL AUTO_INCREMENT,
`sellerID` INT NOT NULL,
`productID` INT NOT NULL,
`tokenID` BIGINT NOT NULL,
`tokenURI` VARCHAR(256) NOT NULL,
`contractAddr` VARCHAR(256) NOT NULL,
`fromAddr` VARCHAR(256) NOT NULL,
`toAddr` VARCHAR(256) NOT NULL,
`registeredDate` DATETIME NOT NULL,
`lastUpdatedDate` DATETIME NOT NULL,
`isDeleted` INT NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = INNODB AUTO_INCREMENT = 29 DEFAULT CHARSET = utf8;
For inquires about this document or KAS, please visit KAS Developers Forum.
Copy Github Repository
- Acquire the clone code from http://github.com/ground-x/kas-bapp-luxurytracker .
git clone git@github.com:ground-x/kas-bapp-luxurytracker.git
For inquires about this document or KAS, please visit KAS Developers Forum.
NPM Install
- The "server.js" file on the root directory is for the back-end server function, while the "client" directory is for the reaction-based front-end function. This tutorial uses a JavaScript-based package manager for both front and back ends. As such, the "npm install" is required twice for the "root" and "client" directories, respectively.
npm install
cd client
npm install
For inquires about this document or KAS, please visit KAS Developers Forum.
Setting up KAS
-
Enter accessKeyId and secretAccessKey issued by KAS Console to
client/src/Config.js
.
const accessKeyId = "YOUR_ACCESS_KEY_ID_FROM_KAS_CONSOLE";
const secretAccessKey = "YOUR_SECRET_ACCESS_KEY_FROM_KAS_CONSOLE";
Loading Your Account for the Tutorial
- Since the transactions are directly submitted by the buyer and seller accounts, not having sufficient KAIA balance required for the transaction may result in an error on node.js as shown below. If you are running this tutorial on Kairos, you can obtain test KAIA at Faucet . If you are using the Mainnet network, make sure that the seller and buyer accounts have sufficient KAIA.
[0] (node:115456) UnhandledPromiseRejectionWarning: Error: Returned error: insufficient funds of the sender for value
Run
- Execute the following commands in the root directory to carry out the back-end and front-end servers simultaneously. Executed programs can be checked at http://localhost:3000 (use another port number if the port is already in use).
yarn dev
Demo
For inquires about this document or KAS, please visit KAS Developers Forum.
Application Workflow
As described above, the key functions of DApp (example) are as follows.
- Deployment of a new NFT smart contract = The prestige store registers a new luxury item (on the blockchain).
- Issuance of a new NFT token = The store sells a luxury item and issues an electronic receipt to the customer (on the blockchain).
- NFT token transmission = The receipt is sent (on the blockchain) when the customer sells his used product to another person.
The front end sends the requests of application users (owners of premium stores and customers buying luxury goods) to the back-end server and shows the responses of the back-end server. For the front-end code, refer to code in Repository.
For inquires about this document or KAS, please visit KAS Developers Forum.
Register new luxury goods to a store’s product list
This code is used for registering new luxury goods (or deploying new contracts). In this tutorial, the owner of the prestige store registers new luxury goods directly to the blockchain. The owner of a prestige store needs his/her private key to deploy new smart contracts Deploy KIP-17 Contract with caver-js. In this example, the store owner’s private key is in the "req" object sent from a client to a server for simple implementation. In actual service development, it is recommended to keep the private key in the server.
The "./server.js" receives the request when "./client/src/components/ProductAdd.js" sends a request for sending a KIP-17 contract to Kaia.
- Deploys (registers) a new KIP-17 contract (=new luxury item to sell)
- Stores the information of the deployed KIP-17 contract on the "product" table
This is the overall workflow.
- Register the store owner’s private key to the caver wallet (skip if already registered). - Front end
- Enter the information of the luxury item to register, and send the information to a back-end server. - Front end
- With the premium product’s information from the front end, deploy the KIP-17 contract. - Back end
- Store the information of the deployed KIP-17 contract (=new luxury item information) on the "product" table. - Back end
/////////////////
// ./server.js //
/////////////////
// Contract (luxury item) deploy code
app.post("/api/products", upload.single("image"), async (req, res) => {
// Code for checking data from the front end to the back-end server.
console.log(
"name", req.body.name,
"symbol", req.body.symbol,
"sellerPrivateKey", req.body.sellerPrivateKey
);
// Create a key ring with the private key.
// For more details on key ring, refer to https://docs.klaytn.com/dapp/sdk/caver-js/api-references/caver.wallet/keyring
const keyring = caver.wallet.keyring.createFromPrivateKey(
req.body.sellerPrivateKey
);
// Add the key ring only if it is not added to the wallet.
if (!caver.wallet.getKeyring(keyring.address)) {
const singleKeyRing = caver.wallet.keyring.createFromPrivateKey(
req.body.sellerPrivateKey
);
caver.wallet.add(singleKeyRing);
}
// With the delivered data, deploy the new KIP-17 (=register a new luxury item).
const kip17 = await caver.kct.kip17.deploy(
{
name: req.body.name,
symbol: req.body.symbol,
},
keyring.address
);
// Check the contract address of the deployed KIP-17.
console.log(KIP-17.options.address);
// Insert the information of the deployed KIP-17 on the "TEST.PRODUCT" table.
let sql =
"INSERT INTO TEST.PRODUCT (name, symbol, contractAddr, image, registeredDate, isDeleted) VALUES (?, ?, ?, ?, now(), 0)";
console.log(req.file);
let image = "/image/" + req.file.filename;
let name = req.body.name;
let symbol = req.body.symbol;
let params = [name, symbol, KIP-17.options.address, image];
connection.query(sql, params, (err, rows, fields) => {
res.send(rows);
console.log(err);
});
console.log("end of /api/products post");
});
For inquires about this document or KAS, please visit KAS Developers Forum.
Issue a receipt to a customer when a luxury item is sold
The owner of a prestige store starts to sell the luxury item if a new luxury item is registered to the blockchain by deploying a new smart contract. Once a customer buys a luxury item at the prestige store, a receipt will be issued to him/her. In this section, an electronic receipt will be issued to the customer on the blockchain.
Issuing the receipt to a customer on the blockchain means issuing a new token to the EOA of the customer in the KIP-17 smart contract created above. The store owner’s private key is required to Issue KIP-17 Token with caver-js. In this example, the store owner’s private key is in the "req" object sent from a client to a server for simple implementation. In the actual service development, it is recommended to keep the private key in the server.
The "./client/src/components/ReceiptAdd.js" sends a request, while "./server.js" receives the request.
- Issues a new KIP-17 token (=electronic receipt) to the customer
- Stores the information of the issued KIP-17 token on the "receipt" table
Here is the overall flow.
- Register the store owner’s private key to the caver wallet (skip if already registered). - Front end
- Enter the information of receipt to issue, and send the information of this receipt to the back-end server. - Front end
- Deploy the new KIP-17 token with the receipt information from the front end. - Back end
- Store the information of the issued KIP-17 token on the "receipt" table. - Back end
/////////////////
// ./server.js //
/////////////////
/*
Token (=receipt) issuance code:
"fromAddr" is the Kaia EOA address of the prestige store owner;
"toAddr" is the Kaia EOA address of the customer to buy the luxury item;
"tokenURI" is the token information; and
"contractAddr" is a smart contract address used for registering the new luxury item to be sold in the blockchain.
*/
app.post("/api/receipts", async (req, res) => {
// Code for checking data from the front end to the back-end server
console.log("req.body.sellerPrivateKey: " + req.body.sellerPrivateKey);
console.log("req.body.productID: " + req.body.productID);
console.log("req.body.toAddr: " + req.body.toAddr);
// Create a key ring with the private key.
// For more details on key ring, refer to https://docs.klaytn.com/dapp/sdk/caver-js/api-references/caver.wallet/keyring
const keyring = caver.wallet.keyring.createFromPrivateKey(
req.body.sellerPrivateKey
);
// Add the key ring only if it is not added to the wallet.
if (!caver.wallet.getKeyring(keyring.address)) {
const singleKeyRing = caver.wallet.keyring.createFromPrivateKey(
req.body.sellerPrivateKey
);
caver.wallet.add(singleKeyRing);
}
/// Search if a luxury item that matches the received productID is registered.
connection.query(
"SELECT * FROM TEST.PRODUCT WHERE isDeleted = 0 AND id = ?",
req.body.productID,
async (err, rows, fields) => {
// product.id is primary key so that it should return only 1 row.
console.log("keyring.address", keyring.address);
console.log("contractAddr", rows[0]["contractAddr"]);
// Contract address for the received productID
contractAddr = rows[0]["contractAddr"];
// Create the KIP-17 object with the contract address.
const kip17 = new caver.kct.kip17(contractAddr);
// Use "Math.random" to assign a random tokenID to a newly issued token, and check if it is a duplicate.
minted = false;
while (true) {
randomTokenID = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
console.log("randomTokenID", randomTokenID);
try {
owner = await KIP-17.ownerOf(randomTokenID);
} catch (e) {
// An error will be received if the owner does not exist (=non-existent tokenID).
// Once the error is received, a token can be created using the tokenID.
console.log("we can mint");
// Random information can be inserted into the tokenURI.
// This example stores the random sellerID and productID in JSON formats.
// The token image URL or other information can be stored in the tokenURI.
tokenURI = JSON.stringify({
sellerID: 0,
productID: req.body.productID,
});
// Issue a new token with the KIP-17.mintWithTokenURI.
// For details, please visit https://docs.klaytn.com/dapp/sdk/caver-js/api-references/caver.kct/KIP-17#KIP-17-mintwithtokenuri
mintResult = await KIP-17.mintWithTokenURI(
req.body.toAddr,
randomTokenID,
tokenURI,
{ from: keyring.address }
);
// After the token is issued on the Kaia Network, store the created token in the "receipt" database.
let sql =
"INSERT INTO TEST.RECEIPT " +
"(sellerID, productID, tokenID, tokenURI, contractAddr, fromAddr, toAddr, registeredDate, lastUpdatedDate, isDeleted)" +
" VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW(), 0)";
let params = [
0,
req.body.productID,
randomTokenID,
tokenURI,
contractAddr,
"0x0000000000000000000000000000000000000000",
req.body.toAddr,
];
connection.query(sql, params, (err, rows, fields) => {
if (!err) {
console.log("error while inserting a new receipt", "err", err);
}
});
minted = true;
}
// Only break if a new token is issued because there is no token with a randomTokenID.
// Retry using the new randomTokenID if it is not issued.
if (minted) {
break;
}
}
res.send(rows);
}
);
console.log("end of /api/products post");
});
For inquires about this document or KAS, please visit KAS Developers Forum.
The seller of the used product sends a receipt that was issued by the store to the buyer
The customer has used a purchased luxury item for a while and wants to sell it as a secondhand good this time. In this case, he also wants to send the receipt of the used product that was issued by the prestige store to the buyer. In this example of DApp, the seller of the used luxury item sends the original receipt to the buyer by sending the KIP-17 token to him.
In this example, the private key of the seller of the used luxury item is in the "req" object sent from a client to a server for simple implementation. In actual service development, it is recommended to keep the private key in the server.
The "./client/src/components/ReceiptSend.js" sends a request, while "./server.js" receives the request.
- The seller of a used luxury item sends a KIP-17 token (=receipt) to the buyer of the luxury item.
- The seller updates the information of the sent KIP-17 token on the "receipt" table.
This is the overall workflow.
- Register the private key of the seller of the luxury item to the caver wallet (skip if already registered). - Front end
- Send the buyer’s EOA to the back-end server when the seller of the used luxury item selects the receipt, and then enter the buyer’s EOA address. - Front end
- Send the KIP-17 token (receipt) to EOA of the buyer with the information from the front end. - Back end
- Update the information of the sent KIP-17 token on the "receipt" table. - Back end
Here is a detailed description of the receipt transmission process.
- The seller of the used luxury item selects the Kaia account address (=EOA) of the buyer and clicks the "Sell" button in DApp.
- The "ReceiptSend.js" requests the back-end server to transfer the receipt from the seller to the buyer.
- The back-end server (server.js) receives the request and updates the changed receipt ownership in the DB.
- After updating the DB, the "server.js" requests a transaction from KAS to transfer the receipt from the seller to the buyer.
- KAS executes this transaction, and the buyer receives a message for the receipt in his DApp.
- The buyer can check if the receipt was successfully transferred (if receipt ownership was successfully updated on the blockchain) in Klaytnscope(=Kaiascope) .
- The buyer can check the transferred receipt and the ownership information anytime via the DApp.
/////////////////
// ./server.js //
/////////////////
// 토큰(=영수증) 전송 코드
app.post("/api/receipts/send", async (req, res) => {
// Code for checking the data from the front end to the back-end server.
console.log("post /api/receipts");
console.log("req.body.customerPrivateKey: " + req.body.customerPrivateKey);
console.log("req.body.contractAddr: " + req.body.contractAddr);
console.log("req.body.tokenId: " + req.body.tokenId);
console.log("receiverAddr", req.body.receiverAddr);
// Create a key ring with the private key.
// For more details on key ring, refer to https://docs.klaytn.com/dapp/sdk/caver-js/api-references/caver.wallet/keyring
let senderPrivateKey = req.body.customerPrivateKey;
const senderKeyring = caver.wallet.keyring.createFromPrivateKey(
senderPrivateKey
);
// Add the key ring only if it is not added to the wallet.
if (!caver.wallet.getKeyring(senderKeyring.address)) {
const singleKeyRing = caver.wallet.keyring.createFromPrivateKey(
senderPrivateKey
);
caver.wallet.add(singleKeyRing);
}
let contractAddr = req.body.contractAddr;
// Update the new values of "fromAddr" and "toAddr" on the "receipt" table.
connection.query(
"UPDATE TEST.RECEIPT SET fromAddr = ?, toAddr = ?, lastUpdatedDate=NOW() WHERE contractAddr=? AND tokenID=?",
[
senderKeyring.address,
req.body.receiverAddr,
contractAddr,
req.body.tokenId,
],
// After updating the "receipt" table, send the KIP-17 to the new owner.
async (err, rows, fields) => {
const kip17 = new caver.kct.kip17(contractAddr);
console.log(`senderKeyring.address: ${senderKeyring.address}`);
console.log(`req.body.receiverAddr: ${req.body.receiverAddr}`);
console.log(`req.body.tokenId: ${typeof req.body.tokenId}`);
console.log(caver.currentProvider);
transferResult = await KIP-17.transferFrom(
senderKeyring.address,
req.body.receiverAddr,
req.body.tokenId,
{ from: senderKeyring.address, gas: 200000 }
);
console.log(transferResult);
res.send(transferResult);
}
);
});
For inquires about this document or KAS, please visit KAS Developers Forum.
Additional functions: luxury item sales history search
The prestige store owner can load a list of issued receipts for one luxury item that he/she has sold, similar to loading a list of tokens issued by one KIP-17 contract. The list of receipts can be loaded using NFT Search Function of KAS Token History API using "contractAddr" in the "product" table. Only the REST API is used instead of "caver-js" following other sample codes above.
This is the overall workflow.
- Send the information of a luxury item to search for the list of receipts to a back-end server. - Front end
- Search for a corresponding contract address in the "product" table using the information of the luxury item (=luxury item ID) from the front end. - Back end
- Use NFT Search Function of the KAS Token History API with the searched contract address. - Back end
- Return the searched NFT token list to the front end. - Back end
/////////////////
// ./server.js //
/////////////////
// Search for the token code (=receipt) issued for one luxury item.
app.get("/api/receipts/:id", (req, res) => {
// Get `contractAddr` that matches the given id from `product` table.
connection.query(
"SELECT * FROM TEST.PRODUCT WHERE isDeleted = 0 AND id = ?",
req.params.id,
async (err, rows, fields) => {
contractAddr = rows[0]["contractAddr"];
// Create data for calling the KAS Token History API.
var options = {
method: "GET",
url:
"https://th-api.klaytnapi.com/v2/kct/nft/" + contractAddr + "/token",
headers: {
"x-krn": "krn:1001:th",
"Content-Type": "application/json",
Authorization:
// Insert let credential = btoa(`${accessKey}:${secretKey}`)). See https://docs.klaytnapi.com/getting-started
"Basic MjAxMzczYTQ3...",
},
};
request(options, function (error, response) {
if (error) throw new Error(error);
console.log(response.body);
items = response.body["items"];
if (!items) {
res.send(response.body);
return;
}
items.map((contract) => {});
});
}
);
});
For inquires about this document or KAS, please visit KAS Developers Forum.