Klaytn에서 Truffle을 이용하여 NFT 발행하기

Tech at Klaytn
28 min readApr 16, 2021

--

전체 포스팅 목록은 여기에서 확인하세요.

안녕하세요, 이 포스트에서는 Klaytn에서 Truffle을 이용하여 NFT를 발행하는 방법에 대해서 설명하고자 합니다.

1. 클레이튼(Klaytn)?

먼저 클레이튼이 무엇인지 알아보도록 하겠습니다. 클레이튼은 카카오의 자회사인 GroundX에서 개발한 블록체인 플랫폼입니다. 더 자세한 정보는 클레이튼 홈페이지, Position Paper, Klaytn Docs 등에서 확인하시기 바랍니다.

2. NFT?

NFT는 Non-Fungible Token의 약자로, 그대로 번역하면 대체 불가능한 토큰입니다. 이더리움의 ERC-721에서부터 시작되었으며, 하나하나가 각자 다른 가치를 가지는 토큰입니다. 예를 들면 연예인 포토 카드, 스포츠 선수 카드, 포켓몬이 하나의 토큰으로 표현된다고 생각하시면 됩니다. 클레이튼에서의 NFT는 KIP-17을 통해 표준으로 정의되었습니다. KIP-17에 대한 자세한 설명은 KIPs에서 확인하실 수 있습니다.

이 문서는 NFT에 대해 자세히 설명하기보다는 클레이튼에서 NFT를 배포하고 발행하는 방법에 대해 설명하려고 합니다. NFT에 대한 자세한 설명은 다른 문서들을 참고하시기 바랍니다.

3. NFT 컨트랙트 배포? 토큰 전송?

먼저, NFT에 대한 기술적인 설명을 조금 해보겠습니다. NFT를 발급하기 위해서는 먼저 NFT 스마트 컨트랙트가 클레이튼 상에 우선 배포되어 있어야 하는데요, 여기서 스마트 컨트랙트는 NFT를 보관하는 저장소라고 생각하시면 됩니다. 저장소를 하나 만들고 그 안에 새로운 NFT를 계속 생성할 수 있습니다. 따라서, 아래와 같이 용어를 정리할 수 있습니다.

  • NFT: 대체 불가능한 토큰(Non-fungible token)
  • NFT 컨트랙트(Contract): NFT를 저장할 수 있는 저장소 컨트랙트
  • NFT 발행(Minting): NFT 컨트랙트에 NFT를 새로 생성(발행)하는 행위
  • NFT 전송(Transfer): 이미 생성된 NFT를 사용자 간 주고받는 행위
  • NFT 소각(Burn): 이미 생성된 NFT를 폐기(소각)하는 행위

4. 스마트 컨트랙트를 이용한 NFT 컨트랙트 배포

사용자가 KIP-17을 만족하는 스마트 컨트랙트를 직접 구현하여 Klaytn에 배포할 수도 있습니다만, 이 문서에서는 이미 제작된 KIP-17 코드를 활용하여 컨트랙트를 배포하는 방법에 대해 설명하고자 합니다. 이미 제작된 KIP-17 코드는 이 링크에서 확인하실 수 있습니다.

4.1. 배포 환경 설치

가장 쉽게 NFT 컨트랙트를 가장 쉬운 방법으로 배포해보려면, Github repository인 klaytn-contracts를 참고하시면 됩니다. 이 문서에서는 KAS의 Node API를 활용해 KIP-17 contract를 배포해보고자 합니다. KAS는 Ground X에서 개발한 API Service로 Klaytn의 다양한 기능들을 쉽게 사용할 수 있습니다. KAS는 하루 최대 10,000 API 요청까지 무료로 제공되므로 간단히 회원가입 후 이용해 보시기 바랍니다.

4.2. Truffle 설치

Truffle은 아래 명령어를 통해 설치할 수 있습니다.

$ npm install -g truffle@v5.1.61

4.3. klaytn-contract 코드 복사

아래 명령어를 통해 klaytn-contract repository를 내 컴퓨터로 복제할 수 있습니다.

$ git clone https://github.com/klaytn/klaytn-contracts.git

4.4. truffle-config.js 업데이트

KAS의 access key와 secret access key를 아래 코드 부분에 수정하여 추가합니다. Access key와 secret key 발급 방법은 KAS Docs를 참고하시기 바랍니다.

// klaytn-contracts/truffle-config.js:29
const accessKeyId = “ACCESS_KEY”;
const secretAccessKey = “SECRET_KEY”;

4.5. dependency package 설치

npm install을 이용하여 실행할 때 필요한 패키지들을 설치합니다.

klaytn-contracts$ npm install

4.6. migrations/2_contract_migration.js 업데이트

Truffle에서 배포할 컨트랙트는 2_contract_migration.js 파일을 통해 설정할 수 있습니다. 해당 파일을 아래와 같이 수정하시면 됩니다.

// migrations/2_contract_migration.js
var kip17 = artifacts.require(‘KIP17Token’);
module.exports = function(deployer) {
deployer.deploy(kip17, “Test NFT”, “TN”)
};

4.7. Baobab에 테스트 배포

Baobab은 클레이튼의 테스트넷입니다. 바오밥의 테스트 KLAY를 받으려면 Baobab faucet을 이용하시면 됩니다. KlaytnWallet에서 새로운 계정을 생성한 후 faucet을 이용하시면 테스트 KLAY를 받을 수 있습니다. 생성한 계정의 private key를 truffle-config.js에 아래와 같이 추가합니다.

// klaytn-contracts/truffle-config.js:40
const privateKey = “0x123 …”;

truffle-config.js에 access key, secret key, private key까지 모두 작성했다면 이제 배포 준비가 완료되었습니다. 아래 명령을 통해 배포 단계를 진행해봅시다. 실행하면서 출력되는 내용은 다음과 같습니다. 아래 내용을 참고하여 문제없이 실행되는지 확인하시기 바랍니다.

klaytn-contracts$ truffle deploy --network kasBaobabCompiling your contracts…
===========================
> Compiling ./contracts/GSN/Context.sol
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/access/Roles.sol
> Compiling ./contracts/access/roles/MinterRole.sol
> Compiling ./contracts/access/roles/PauserRole.sol
> Compiling ./contracts/drafts/Counters.sol
> Compiling ./contracts/introspection/IKIP13.sol
> Compiling ./contracts/introspection/KIP13.sol
> Compiling ./contracts/introspection/KIP13Checker.sol
> Compiling ./contracts/lifecycle/Pausable.sol
> Compiling ./contracts/math/SafeMath.sol
> Compiling ./contracts/mocks/ERC1155ReceiverMock.sol
> Compiling ./contracts/mocks/ERC721ReceiverMock.sol
> Compiling ./contracts/mocks/KIP13/KIP13InterfacesSupported.sol
> Compiling ./contracts/mocks/KIP13/KIP13NotSupported.sol
> Compiling ./contracts/mocks/KIP13CheckerMock.sol
> Compiling ./contracts/mocks/KIP13Mock.sol
> Compiling ./contracts/mocks/KIP17FullMock.sol
> Compiling ./contracts/mocks/KIP17MintableBurnableImpl.sol
> Compiling ./contracts/mocks/KIP17Mock.sol
> Compiling ./contracts/mocks/KIP17PausableMock.sol
> Compiling ./contracts/mocks/KIP17ReceiverMock.sol
> Compiling ./contracts/mocks/KIP37BurnableMock.sol
> Compiling ./contracts/mocks/KIP37Mock.sol
> Compiling ./contracts/mocks/KIP37PausableMock.sol
> Compiling ./contracts/mocks/KIP37ReceiverMock.sol
> Compiling ./contracts/mocks/KIP7BurnableMock.sol
> Compiling ./contracts/mocks/KIP7MetadataMock.sol
> Compiling ./contracts/mocks/KIP7MintableMock.sol
> Compiling ./contracts/mocks/KIP7Mock.sol
> Compiling ./contracts/mocks/KIP7PausableMock.sol
> Compiling ./contracts/mocks/MinterRoleMock.sol
> Compiling ./contracts/mocks/PauserRoleMock.sol
> Compiling ./contracts/token/KIP17/IERC721Receiver.sol
> Compiling ./contracts/token/KIP17/IKIP17.sol
> Compiling ./contracts/token/KIP17/IKIP17Enumerable.sol
> Compiling ./contracts/token/KIP17/IKIP17Full.sol
> Compiling ./contracts/token/KIP17/IKIP17Metadata.sol
> Compiling ./contracts/token/KIP17/IKIP17Receiver.sol
> Compiling ./contracts/token/KIP17/KIP17.sol
> Compiling ./contracts/token/KIP17/KIP17Burnable.sol
> Compiling ./contracts/token/KIP17/KIP17Enumerable.sol
> Compiling ./contracts/token/KIP17/KIP17Full.sol
> Compiling ./contracts/token/KIP17/KIP17Metadata.sol
> Compiling ./contracts/token/KIP17/KIP17MetadataMintable.sol
> Compiling ./contracts/token/KIP17/KIP17Mintable.sol
> Compiling ./contracts/token/KIP17/KIP17Pausable.sol
> Compiling ./contracts/token/KIP17/KIP17Token.sol
> Compiling ./contracts/token/KIP37/IERC1155Receiver.sol
> Compiling ./contracts/token/KIP37/IKIP37.sol
> Compiling ./contracts/token/KIP37/IKIP37MetadataURI.sol
> Compiling ./contracts/token/KIP37/IKIP37Receiver.sol
> Compiling ./contracts/token/KIP37/KIP37.sol
> Compiling ./contracts/token/KIP37/KIP37Burnable.sol
> Compiling ./contracts/token/KIP37/KIP37Holder.sol
> Compiling ./contracts/token/KIP37/KIP37Mintable.sol
> Compiling ./contracts/token/KIP37/KIP37Pausable.sol
> Compiling ./contracts/token/KIP37/KIP37Receiver.sol
> Compiling ./contracts/token/KIP37/KIP37Token.sol
> Compiling ./contracts/token/KIP7/IKIP7.sol
> Compiling ./contracts/token/KIP7/IKIP7Receiver.sol
> Compiling ./contracts/token/KIP7/KIP7.sol
> Compiling ./contracts/token/KIP7/KIP7Burnable.sol
> Compiling ./contracts/token/KIP7/KIP7Metadata.sol
> Compiling ./contracts/token/KIP7/KIP7Mintable.sol
> Compiling ./contracts/token/KIP7/KIP7Pausable.sol
> Compiling ./contracts/token/KIP7/KIP7Token.sol
> Compiling ./contracts/utils/Address.sol
> Compilation warnings encountered:
/Users/kjhman21/Sources/github.com/klaytn/klaytn-contracts/contracts/token/KIP37/KIP37Pausable.sol:79:9: Warning: Unused function parameter. Remove or comment out the variable name to silence this warning.
address operator,
^ — — — — — — — ^
,/Users/kjhman21/Sources/github.com/klaytn/klaytn-contracts/contracts/token/KIP37/KIP37Pausable.sol:80:9: Warning: Unused function parameter. Remove or comment out the variable name to silence this warning.
address from,
^ — — — — — ^
,/Users/kjhman21/Sources/github.com/klaytn/klaytn-contracts/contracts/token/KIP37/KIP37Pausable.sol:81:9: Warning: Unused function parameter. Remove or comment out the variable name to silence this warning.
address to,
^ — — — — ^
,/Users/kjhman21/Sources/github.com/klaytn/klaytn-contracts/contracts/token/KIP37/KIP37Pausable.sol:83:9: Warning: Unused function parameter. Remove or comment out the variable name to silence this warning.
uint256[] memory amounts,
^ — — — — — — — — — — — ^
,/Users/kjhman21/Sources/github.com/klaytn/klaytn-contracts/contracts/token/KIP37/KIP37Pausable.sol:84:9: Warning: Unused function parameter. Remove or comment out the variable name to silence this warning.
bytes memory data
^ — — — — — — — -^
> Artifacts written to /Users/kjhman21/Sources/github.com/klaytn/klaytn-contracts/build/contracts
> Compiled successfully using:
- solc: 0.5.6+commit.b259423e.Emscripten.clang
Starting migrations…
======================
> Network name: ‘kasBaobab’
> Network id: 1001
> Block gas limit: 0 (0x0)

1_initial_migration.js
======================
Deploying ‘Migrations’
— — — — — — — — — — —
> transaction hash: 0xb1c4474dd0819f0a92c7a2510acb76c45b704d56b536230ea9809be33a4b2602
> Blocks: 0 Seconds: 0
> contract address: 0x69E2622287EaBDF8FeDC6D1465626A55e9744777
> block number: 54229441
> block timestamp: 1615785044
> account: 0xEB5cCFa5C1750a2d075Df8F7a0f34490d4930b8b
> balance: 78.856641625
> gas used: 140894 (0x2265e)
> gas price: 25 gwei
> value sent: 0 ETH
> total cost: 0.00352235 ETH
> Saving migration to chain.
> Saving artifacts
— — — — — — — — — — — — — — — — — — -
> Total cost: 0.00352235 ETH

2_contract_migration.js
=======================
Deploying ‘KIP17Token’
— — — — — — — — — — —
> transaction hash: 0x55d5f332e7eb86e12d9688acb53fab8fe4c62c4e86eda7ea5ee46f6580b864d9
> Blocks: 0 Seconds: 0
> contract address: 0xc609eB0eE11D4246AC6d0c070c0D2374B53d7BA8
> block number: 54229446
> block timestamp: 1615785049
> account: 0xEB5cCFa5C1750a2d075Df8F7a0f34490d4930b8b
> balance: 78.780642175
> gas used: 2998052 (0x2dbf24)
> gas price: 25 gwei
> value sent: 0 ETH
> total cost: 0.0749513 ETH

> Saving migration to chain.
> Saving artifacts
— — — — — — — — — — — — — — — — — — -
> Total cost: 0.0749513 ETH

Summary
=======
> Total deployments: 2
> Final cost: 0.07847365 ETH

위와 같이 Summary까지 출력되면 성공적으로 배포된 것입니다. 배포된 컨트랙트 주소를 잘 기억하시기 바랍니다. 위에서는 0xc609eB0eE11D4246AC6d0c070c0D2374B53d7BA8 이 NFT 컨트랙트가 배포된 주소입니다.

4.8. Cypress에 배포

Cypress에 배포하기 위해서는 위에서 사용한 0.07847365 KLAY 이상의 잔고를 가지고 있는 클레이튼 계정이 필요합니다. KLAY를 얻기 위해서는 거래소 혹은 KLAY 거래를 지원하는 지갑 앱을 이용하시면 됩니다.

truffle-config.js의 private key 변수에 KLAY 잔고가 있는 계정의 private key를 입력한 후 아래와 같이 실행합니다.

klaytn-contracts$ truffle deploy --network kasCypress

만약 컨트랙트의 일부를 수정하고 싶다면 klaytn-contracts/contracts/token/KIP17에서 원하는 부분을 수정하고 다시 배포 가능합니다.

4.9. Caver를 이용한 토큰 발행/전송/소각

Truffle은 배포에 이용하기 좋은 툴이지만 스마트 컨트랙트를 실행하기에는 조금 무거운 툴입니다. 토큰 발행/전송/소각에 대한 실행 예제는 caver를 이용하여 설명하도록 하겠습니다.

4.9.1. caver-js-ext-kas를 이용한 토큰 발행/전송/소각

caver-js-ext-kas를 이용하면 토큰 발행/전송/소각이 어렵지 않습니다. 아래 예제 코드를 이용하여 토큰 발행/전송/소각을 실행해 보시기 바랍니다.

const CaverExtKAS = require('caver-js-ext-kas')// Configuration Part
// Set your KAS access key and secretAccessKey.
const accessKeyId = '{your_accessKeyId}'
const secretAccessKey = '{your_secretAccessKey}'
// const CHAIN_ID_BAOBOB = '1001'
// const CHAIN_ID_CYPRESS = '8217'
const chainId = '1001'
const contractAddress = '{contractAddress}'const caver = new CaverExtKAS(chainId, accessKeyId, secretAccessKey)test()async function test () {
const privateKey = '0x{private key}'
// Create a KeyringContainer instance
const keyringContainer = new caver.keyringContainer()
// Add keyring to in-memory wallet
const keyring = keyringContainer.keyring.createFromPrivateKey(privateKey)
keyringContainer.add(keyring)
// Create a KIP17 instance
const kip17 = new caver.kct.kip17(contractAddress)

// Call `kip17.setWallet(keyringContainer)` to use KeyringContainer instead of KAS Wallet API
kip17.setWallet(keyringContainer)
const tokenId = '1'
const uri = 'http://test.url'
const mintReceipt = await kip17.mintWithTokenURI(keyring.address, tokenId, uri, { from:keyring.address })
console.log(`mint receipt: `)
console.log(mintReceipt)
const transferReceipt = await kip17.transferFrom(keyring.address, keyring.address, tokenId, { from:keyring.address })
console.log(`transfer receipt: `)
console.log(transferReceipt)
const burnReceipt = await kip17.burn(tokenId, { from:keyring.address })
console.log(`burn receipt: `)
console.log(burnReceipt)
}

4.9.2. caver-java-ext-kas를 이용한 토큰 발행/전송/소각

Java 환경에서는 caver-java-ext-kas를 이용해 보다 쉽게 토큰 발행/전송/소각할 수 있습니다. 아래 예제 코드를 이용하여 토큰 발행/전송/소각을 실행해 보시기 바랍니다.

package com.klaytn.caver.boilerplate;import com.klaytn.caver.Caver;
import com.klaytn.caver.account.Account;
import com.klaytn.caver.contract.SendOptions;
import com.klaytn.caver.kct.kip17.KIP17;
import com.klaytn.caver.kct.kip7.KIP7;
import com.klaytn.caver.methods.response.AccountKey;
import com.klaytn.caver.methods.response.Bytes32;
import com.klaytn.caver.methods.response.TransactionReceipt;
import com.klaytn.caver.transaction.response.PollingTransactionReceiptProcessor;
import com.klaytn.caver.transaction.response.TransactionReceiptProcessor;
import com.klaytn.caver.transaction.type.AccountUpdate;
import com.klaytn.caver.transaction.type.ValueTransfer;
import com.klaytn.caver.wallet.keyring.KeyringFactory;
import com.klaytn.caver.wallet.keyring.SingleKeyring;
import okhttp3.Credentials;
import org.web3j.protocol.exceptions.TransactionException;
import org.web3j.protocol.http.HttpService;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.math.BigInteger;
public class BoilerPlate {
private static final String URL_NODE_API = "https://node-api.klaytnapi.com/v1/klaytn";
// Configuration Part
// Set your KAS access key and secretAccessKey.
static String accessKey = "{your_accessKeyId}";
static String secretAccessKey = "{your_secretAccessKey}";
// static String CHAIN_ID_BAOBOB = "1001";
// static String CHAIN_ID_CYPRESS = "8217";
static String chainId = "1001";
static String contractAddress = "{contractAddress}";
public static void main(String[] args) {
// Build a Caver instance.
Caver caver = setCaver(accessKey, secretAccessKey, chainId);
// Run a test.
test(caver);
}
public static void test(Caver caver) {
String testPrivateKey = "0x{private key}";
SingleKeyring deployerKeyring = KeyringFactory.createFromPrivateKey(testPrivateKey);
caver.wallet.add(deployerKeyring);
try {
// Create a KIP17 instance
KIP17 kip17 = new KIP17(caver, contractAddress);
//Mint a NFT token
BigInteger tokenId = BigInteger.ONE;
String uri = "http://test.url";
TransactionReceipt.TransactionReceiptData mintReceiptData = kip17.mintWithTokenURI(deployerKeyring.getAddress(), tokenId, uri, new SendOptions(deployerKeyring.getAddress()));
System.out.println("NFT mint transaction hash : " + mintReceiptData.getTransactionHash());
//Transfer a NFT token
TransactionReceipt.TransactionReceiptData transferReceiptData = kip17.transferFrom(deployerKeyring.getAddress(), deployerKeyring.getAddress(), tokenId, new SendOptions(deployerKeyring.getAddress()));
System.out.println("NFT transfer transaction hash : " + transferReceiptData.getTransactionHash());
//Burn a NFT token
TransactionReceipt.TransactionReceiptData burnReceiptData = kip17.burn(tokenId, new SendOptions(deployerKeyring.getAddress()));
System.out.println("NFT burn transaction hash : " + burnReceiptData.getTransactionHash());
} catch (NoSuchMethodException | IOException | InstantiationException | ClassNotFoundException | IllegalAccessException | InvocationTargetException | TransactionException e) {
e.printStackTrace();
}
}

private static Caver setCaver(String accessKey, String secretAccessKey, String chainID) {
HttpService httpService = new HttpService(URL_NODE_API);
httpService.addHeader("Authorization", Credentials.basic(accessKey, secretAccessKey));
httpService.addHeader("x-chain-id", chainID);
return new Caver(httpService);
}
}

여기까지 Klaytn에서 Truffle을 사용하여 KIP-17 컨트랙트를 배포하는 방법에 대해 알아보았습니다. 여러분들ㅇ도 카카오의 자회사 GroundX가 개발한 빠르고 편리한 블록체인 Klaytn에서 NFT를 배포하고 전송해보시기 바랍니다.

궁금한 점은 Klaytn 개발자 포럼에 언제든 남겨주세요.

감사합니다.

--

--

Tech at Klaytn
Tech at Klaytn

Responses (1)