Klaytn Blockchain 에서 사용하는 Caver SDK인 caver-js에 대해서 공부해보고 다양한 use cases들을 소개하겠습니다.
caver-js는 개발자가 HTTP 또는 웹소켓 연결을 사용하여 Klaytn 노드와 상호작용할 수 있도록 하는 자바스크립트 API 라이브러리입니다. npm 이용이 가능합니다.
아래에 있는 Klaytn Reference를 참고하면서 정리했습니다.
https://docs.klaytn.com/bapp/sdk/caver-js
caver-js를 공부하면서 자주 등장하는 헷갈리기 쉬운 용어부터 먼저 정리하겠습니다.
Account
Klaytn의 Account는 Person Balance와 Smart Contract까지 포함하는 Data Structure입니다.
크게 두 종류가 있습니다.
EOA (Externally Owned Accounts)란 무엇인가?
SCAs (Smart Contract Accounts)란 무엇인가?
AccountKey
Account는 다양한 Key와 연결해서 사용할 수 있습니다.
트랜잭션 검증에 사용됩니다.
- ecrecover(txhash, txsig)로부터 파생된 공개키를 얻습니다.
- 파생된 공개키가 해당 계정의 공개키와 같은지 확인합니다.
AccountKeyNil
AccountKeyLegacy
AccountKeyPublic
AccountKeyFail
AccountKeyWeightedMultiSig
AccountKeyRoleBased
$ npm install caver-js
or
$ yarn add caver-js
아래와 같이 임의의 값을 사용해 SingleKeyring을 생성할 수 있습니다.
const createKeyring = () => {
const keyring = caver.wallet.keyring.generate()
console.log(keyring);
}
createKeyring();
// expected output
SingleKeyring {
_address: '0x44303b192b8ecf9239b7246bab627f0b43f6426b',
_key: PrivateKey {
_privateKey: '0xb633f5ce69aecaa92bac53ccceaadd7d5346c6a517f565989d08c2397e5e00c2'
}
}
caver를 통해서 MultipleKeyring을 만들 수 있습니다.
const address1 = '0xd8919593e0736e953bc7c416fb2dc71fa70a15ab';
const privateKey1 = '0x43e38109a5d25cd79fc6928a60e4a8749120915e7e2df2d2971ca0b571e98fbb';
const address2 = '0xe443ff0c67ef9e2e44fe758592a8e5b46708dd0f';
const privateKey2 = '0xd678d994df0b8ecd94903b20b3a70e473830c5d43dbb9d58ada0a751f50bee00';
const createMultipleKeyringTest = async () => {
// Create a keyring with an address and private keys
const keyring = caver.wallet.keyring.createWithMultipleKey(address1, [ privateKey1, privateKey2]);
console.log(keyring)
}
createMultipleKeyringTest();
// expected output
MultipleKeyring {
_address: '0xd8919593e0736e953bc7c416fb2dc71fa70a15ab',
_keys: [
PrivateKey {
_privateKey: '0x43e38109a5d25cd79fc6928a60e4a8749120915e7e2df2d2971ca0b571e98fbb'
},
PrivateKey {
_privateKey: '0xd678d994df0b8ecd94903b20b3a70e473830c5d43dbb9d58ada0a751f50bee00'
}
]
}
궁금한게 caver.wallet.keyring.createWithMultipleKey을 실행 후 실제 내 계정은 AccountKeyWeightedMultiSig가 된건가 궁금했다.
static createWithMultipleKey(address, keyArray) {
if (!isMultipleKeysFormat(keyArray))
throw new Error(`Invalid format of parameter. 'keyArray' should be an array of private key strings.`)
return new MultipleKeyring(address, keyArray)
}
실제 코드를 보니 데이터 형태만 Multisig Account 처럼 된 거고 내 계정이 Multisig Account로 업데이트 하는 트랜잭션을 발생 시키진 않는다.
const TransactionMultipleKeyringTest = async () => {
// Create a keyring with an address and private keys
const keyring = caver.wallet.keyring.createWithMultipleKey(address1, [ privateKey1, privateKey2]);
caver.wallet.add(keyring)
// Create value transfer transaction
const vt = new caver.transaction.valueTransfer({
from: keyring.address,
to: '0x8084fed6b1847448c24692470fc3b2ed87f9eb47',
value: caver.utils.toPeb(1, 'KLAY'),
gas: 25000,
})
// Sign to the transaction
const signed = await caver.wallet.sign(keyring.address, vt)
// Send transaction to the Klaytn blockchain platform (Klaytn)
const receipt = await caver.rpc.klay.sendRawTransaction(signed)
console.log(receipt);
}
TransactionMultipleKeyringTest();
// expected output
UnhandledPromiseRejectionWarning: Error: Returned error: invalid transaction v, r, s values of the sender
caver-js를 사용하여 KLAY를 보내는 방법입니다.
개인키 또는 키스토어 파일을 사용하여 Klaytn Wallet에 로그인하고 테스트를 위해 faucet을 통해 Baobab 테스트넷 KLAY를 받습니다.
https://ko.docs.klaytn.com/bapp/developer-tools/klaytn-wallet#how-to-receive-baobab-testnet-klay
트랜잭션 서명은 caver-js 지갑을 통해 할 수 있습니다. 트랜잭션을 네트워크에 보내려면 아래와 같이 2단계를 거쳐야합니다.
const sendTransactionTest = async () => {
// Add a keyring to caver.wallet
const keyring = caver.wallet.keyring.createFromPrivateKey(privateKey1);
caver.wallet.add(keyring)
// Create a value transfer transaction
const valueTransfer = new caver.transaction.valueTransfer({
from: keyring.address,
to: '0x176ff0344de49c04be577a3512b6991507647f72',
value: 1,
gas: 30000,
})
// Sign the transaction via caver.wallet.sign
await caver.wallet.sign(keyring.address, valueTransfer)
const rlpEncoded = valueTransfer.getRLPEncoding()
console.log(`RLP-encoded string: ${rlpEncoded}`)
}
sendTransactionTest();
// expected output
RLP-encoded string: 0x08f87e028505d21dba0082753094176ff0344de49c04be577a3512b6991507647f720194d8919593e0736e953bc7c416fb2dc71fa70a15abf847f8458207f5a02647b0c3d2834f63088b0dc4af54a151c6a7e3c0b89493032ecd2a2668722c39a07a563036c3c0356df7a8de33e7a1793cc78166fea487a2f4c30eef4c086d6496
Transaction을 네트워크에 보낸 후 영수증을 받는 과정입니다.
// Using a promise - async/await
const receipt = await caver.rpc.klay.sendRawTransaction(rawTransaction)
console.log(receipt)
// Using a promise
caver.rpc.klay.sendRawTransaction(rawTransaction).then(console.log)
// expected output
{
blockHash: '0xcd9d7c70fbaf27dd408b2a704b5fd8d0ec2a26ce29ba05a5a5409eb258433e4a',
blockNumber: '0x29b46d2',
contractAddress: null,
from: '0xd8919593e0736e953bc7c416fb2dc71fa70a15ab',
gas: '0x7530',
gasPrice: '0x5d21dba00',
gasUsed: '0x5208',
logs: [],
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
nonce: '0x2',
senderTxHash: '0x59572311b816ca99996f6ab165e17b1f4921d74ea14549fc8304e4ba79413f04', // 발신자만 서명한 트랜잭션 해시 입니다
signatures: [
{
V: '0x7f5',
R: '0x2647b0c3d2834f63088b0dc4af54a151c6a7e3c0b89493032ecd2a2668722c39',
S: '0x7a563036c3c0356df7a8de33e7a1793cc78166fea487a2f4c30eef4c086d6496'
}
],
status: '0x1', // 트랜잭션이 성공적으로 실행됐다면 0x1을 반환하고 아니라면 0x0을 반환합니다.
to: '0x176ff0344de49c04be577a3512b6991507647f72',
transactionHash: '0x59572311b816ca99996f6ab165e17b1f4921d74ea14549fc8304e4ba79413f04', // Fee delegation이 아니라면 senderTxHash와 항상 동일합니다.
transactionIndex: '0x0',
type: 'TxTypeValueTransfer', // 트랜잭션의 유형을 나타내는 문자열입니다
typeInt: 8, // 트랜잭션의 유형을 나타내는 정수입니다.
value: '0x1' // peb로 전송된 값입니다
}
트랜잭션이 실패했다면 txError 필드가 추가되고 에러 메시지가 포함됩니다.
Klaytn은 확장성과 성능을 위한 다양한 트랜잭션을 제공합니다.
트랜잭션 전송자일 때 RLP 인코딩된 트랜잭션을 만드는 예시입니다.
const createSenderRlpEncoded = async () => {
const sender = caver.wallet.keyring.createFromPrivateKey(privateKey1);
caver.wallet.add(sender)
const feeDelegatedTx = new caver.transaction.feeDelegatedValueTransfer({
from: sender.address,
to: '0x176ff0344de49c04be577a3512b6991507647f72',
value: 5,
gas: 50000,
})
await caver.wallet.sign(sender.address, feeDelegatedTx)
const rlpEncoded = feeDelegatedTx.getRLPEncoding()
console.log(rlpEncoded)
return rlpEncoded;
}
createSenderRlpEncoded();
// expected output
0x09f884038505d21dba0082c35094176ff0344de49c04be577a3512b6991507647f720594d8919593e0736e953bc7c416fb2dc71fa70a15abf847f8458207f5a095853fe30d984e1d7b68abfd6defa701c40ce744b9e2b9d9d1197dfbdc32f8c1a071befa60a3b9c346d86570f1e1d8a3066ffd5c19766271901111cdf3908b923f80c4c3018080
Fee Deleagation Transaction은 RLP 인코딩 된 Raw Transaction에 feePayerSignatures를 첨부한 후 클레이튼으로 Transaction을 전파할 수 있습니다.
const FeeDelegationTransactionTest = async (RLPEncoded) => {
const feePayer = caver.wallet.keyring.createFromPrivateKey(privateKey2)
caver.wallet.add(feePayer)
const rlpEncoded = RLPEncoded;
const feeDelegateTxFromRLPEncoding = new caver.transaction.feeDelegatedValueTransfer(rlpEncoded)
// Set the fee payer address.
feeDelegateTxFromRLPEncoding.feePayer = feePayer.address
await caver.wallet.signAsFeePayer(feePayer.address, feeDelegateTxFromRLPEncoding)
const receipt = await caver.rpc.klay.sendRawTransaction(feeDelegateTxFromRLPEncoding.getRLPEncoding())
console.log(receipt)
}
// 위에 있는 RLP 인코딩 된 값을 가지고옵니다.
FeeDelegationTransactionTest('0x09f884038505d21dba0082c35094176ff0344de49c04be577a3512b6991507647f720594d8919593e0736e953bc7c416fb2dc71fa70a15abf847f8458207f5a095853fe30d984e1d7b68abfd6defa701c40ce744b9e2b9d9d1197dfbdc32f8c1a071befa60a3b9c346d86570f1e1d8a3066ffd5c19766271901111cdf3908b923f80c4c3018080');
// expected output
{
blockHash: '0xfaad1e118efec913f0d60d127cd6850d0bcdb043e84736e0ab48eefe85a2ecff',
blockNumber: '0x29b55d6',
contractAddress: null,
feePayer: '0xe443ff0c67ef9e2e44fe758592a8e5b46708dd0f',
feePayerSignatures: [
{
V: '0x7f5',
R: '0xddaec07c471ba3e626ba371adb06f0fea73248e603ae9971fba6fee23f0a2ca3',
S: '0x65527a6de68d0e26b88ade532eb6dfd86cdff69f40ab04c87f9cce13628097f8'
}
],
from: '0xd8919593e0736e953bc7c416fb2dc71fa70a15ab',
gas: '0xc350',
gasPrice: '0x5d21dba00',
gasUsed: '0x7918',
logs: [],
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
nonce: '0x3',
senderTxHash: '0x8887d18368e77558bea5454c49c835a3e2102d4314223d440be2dd282fb8d595',
signatures: [
{
V: '0x7f5',
R: '0x95853fe30d984e1d7b68abfd6defa701c40ce744b9e2b9d9d1197dfbdc32f8c1',
S: '0x71befa60a3b9c346d86570f1e1d8a3066ffd5c19766271901111cdf3908b923f'
}
],
status: '0x1',
to: '0x176ff0344de49c04be577a3512b6991507647f72',
transactionHash: '0xad9ddb142128a384ceb92dcd7108ad1cce4e3159cedd4e7905d2ab1cc9c2c0ac',
transactionIndex: '0x0',
type: 'TxTypeFeeDelegatedValueTransfer',
typeInt: 9,
value: '0x5'
}
Klaytn Account에 있는 privateKey를 새로운 privateKey로 바꿀 수 있습니다.
주의할 점은 바꿀 때 새로운 privateKey를 준비해야 합니다.
const changeKeyringTest = async () => {
let senderKeyring = caver.wallet.keyring.createFromPrivateKey(privateKey1)
caver.wallet.add(senderKeyring)
const newPrivateKey = caver.wallet.keyring.generateSingleKey()
console.log(senderKeyring);
console.log(`new private key string: ${newPrivateKey}`)
const newKeyring = caver.wallet.keyring.createWithSingleKey(senderKeyring.address, newPrivateKey)
console.log(newKeyring);
}
changeKeyringTest();
// expected output
SingleKeyring {
_address: '0xd8919593e0736e953bc7c416fb2dc71fa70a15ab',
_key: PrivateKey {
_privateKey: '0x43e38109a5d25cd79fc6928a60e4a8749120915e7e2df2d2971ca0b571e98fbb'
}
}
new private key string: 0x93bf6e02927da67b4c2f562a970d17905becf086de119f9765c01620dd682f6e
SingleKeyring {
_address: '0xd8919593e0736e953bc7c416fb2dc71fa70a15ab',
_key: PrivateKey {
_privateKey: '0x93bf6e02927da67b4c2f562a970d17905becf086de119f9765c01620dd682f6e'
}
}
새로 만들어진 Keyring을 통해서 Account Update Transaction을 낸다면 AccountKey는 새로운 privateKey로 바껴집니다.
const AccountUpdateTransactionTest = async () => {
let sender = caver.wallet.keyring.createFromPrivateKey(privateKey1)
caver.wallet.add(sender)
const newPrivateKey = caver.wallet.keyring.generateSingleKey()
console.log(sender);
console.log(`new private key string: ${newPrivateKey}`)
const newKeyring = caver.wallet.keyring.createWithSingleKey(sender.address, newPrivateKey)
console.log(newKeyring);
const account = newKeyring.toAccount()
const updateTx = new caver.transaction.accountUpdate({
from: sender.address,
account: account,
gas: 50000,
})
await caver.wallet.sign(sender.address, updateTx)
const receipt = await caver.rpc.klay.sendRawTransaction(updateTx)
console.log(receipt)
// Update the keyring in caver.wallet for signing afterward.
sender = caver.wallet.updateKeyring(newKeyring)
console.log(sender);
}
AccountUpdateTransactionTest();
// expected output
SingleKeyring {
_address: '0xd8919593e0736e953bc7c416fb2dc71fa70a15ab',
_key: PrivateKey {
_privateKey: '0x43e38109a5d25cd79fc6928a60e4a8749120915e7e2df2d2971ca0b571e98fbb'
}
}
new private key string: 0x8e9028ca822129454cada528e70dbb8dad1093da3e9211ab836a02890e571335
SingleKeyring {
_address: '0xd8919593e0736e953bc7c416fb2dc71fa70a15ab',
_key: PrivateKey {
_privateKey: '0x8e9028ca822129454cada528e70dbb8dad1093da3e9211ab836a02890e571335'
}
}
{
blockHash: '0xf0c4cbe5563105dc40a4887559fdde94b4ac2e5b7f416a3c5461b850481d720b',
blockNumber: '0x29b598f',
contractAddress: null,
from: '0xd8919593e0736e953bc7c416fb2dc71fa70a15ab',
gas: '0xc350',
gasPrice: '0x5d21dba00',
gasUsed: '0xa028',
key: '0x02a103dcaf378c3709441439238bc17509a5299a1a3d7ab7680c6a1a24020ced40c5e1',
logs: [],
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
nonce: '0x4',
senderTxHash: '0x9154b367ace2d3cce7182844a391b685f98f80acce24f7466c74a038b51a48ea',
signatures: [
{
V: '0x7f6',
R: '0x8d2e928d0983bcadbec10f9b9a11527be300d1170053697c7d77ce923e5395ca',
S: '0x446d5db22a159d751e18c6705bbef4a63e2f1775dc44607a931d6c6d94ebdd6a'
}
],
status: '0x1',
transactionHash: '0x9154b367ace2d3cce7182844a391b685f98f80acce24f7466c74a038b51a48ea',
transactionIndex: '0x0',
type: 'TxTypeAccountUpdate',
typeInt: 32
}
SingleKeyring {
_address: '0xd8919593e0736e953bc7c416fb2dc71fa70a15ab',
_key: PrivateKey {
_privateKey: '0x8e9028ca822129454cada528e70dbb8dad1093da3e9211ab836a02890e571335'
}
}
Caver를 이용하면 스마트 컨트랙트의 모든 메소드를 자동으로 자바스크립트 호출로 변환합니다. 이를 통해 스마트 컨트랙트가 마치 자바스크립트 객체인 것처럼 스마트 컨트랙트와 상호작용할 수 있습니다
Klaytn IDE로 가서 솔리디티 언어로 스마트 컨트랙트를 개발 한 후 배포합니다.
// Klaytn IDE uses solidity 0.4.24, 0.5.6 versions.
pragma solidity >=0.4.24 <=0.5.6;
contract Count {
// Storage variable `count` (type: uint256)
uint256 public count = 0;
// Get current node's block number.
function getBlockNumber() public view returns (uint256) {
return block.number;
}
// Set value of storage variable `count`.
function setCount(uint256 _count) public {
count = _count;
}
}
Smart Contract를 배포하고 난 뒤 contract Address와 ABI를 가지고 옵니다.
const contractAddress = '0x4ed05eb7ea2e2a0a8569b7d532beb2425b8c63cb';
const ABI = [
{
"constant": false,
"inputs": [
{
"name": "_count",
"type": "uint256"
}
],
"name": "setCount",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "count",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getBlockNumber",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]
const ContractInstanceTest = async () => {
const keyring = caver.wallet.keyring.createFromPrivateKey(privateKey2);
caver.wallet.add(keyring)
const contractInstance = new caver.contract(ABI,contractAddress);
const receipt = await contractInstance.methods.setCount(12).send({from:keyring.address,gas:'0x4bfd200'});
const result = await contractInstance.methods.getBlockNumber().call({from:keyring.address,gas:'0x4bfd200'});
console.log(receipt);
console.log(result);
}
ContractInstanceTest();
// expected output
{
blockHash: '0xc2c4add9a70027aee50c52508860c0e96d611591e1aac6e0d24f506a12189e0e',
blockNumber: 43737004,
contractAddress: null,
from: '0xd2f15aacd423fb5311583f2c5f63183c5c6f3f62',
gas: '0x4bfd200',
gasPrice: '0x5d21dba00',
gasUsed: 44864,
input: '0xd14e62b8000000000000000000000000000000000000000000000000000000000000000c',
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
nonce: '0x0',
senderTxHash: '0x9af270e1985c51a3d4d3e5679d60c937bcecc59dc61d7d2bb8206e27aad8b68a',
signatures: [
{
V: '0x7f6',
R: '0x518d1e9ca79be895bfb394166e2fc86efae8d626194561b44e712885c8799a11',
S: '0x4590ba16b5af25f4bf14dd31748734b613ab75521939d42f289e899f9e022ba6'
}
],
status: true,
to: '0x4ed05eb7ea2e2a0a8569b7d532beb2425b8c63cb',
transactionHash: '0x9af270e1985c51a3d4d3e5679d60c937bcecc59dc61d7d2bb8206e27aad8b68a',
transactionIndex: 0,
type: 'TxTypeSmartContractExecution',
typeInt: 48,
value: '0x0',
events: {}
}
// console.log(result);
43737211