Blockchain

ethersJS로 블록체인과 상호작용

Sila 2023. 3. 19. 18:30

우리 직장 상사는 hardhat같은 툴을 사용하거나 하지 않고 모조리 깡 코드를 써서 tx도 보내고 배포도 하고 nft도 만들고 다 하시더라..

 

ethersJS 라이브러리를 이용해 다른 어플리케이션을 사용하지 않고 코드만으로 블록체인 네트워크와 상호작용을 하는 방법을 알아보자.

 

hardhat, ethersjs를 사용한다.

 

1. 네트워크와 연결

새로운 hardhat 프로젝트를 생성하고, 다음과 같이 하드햇에서 제공하는 네트워크를 구동한다.

npx hardhat node

디폴트로 rpc 주소는 http://127.0.0.1:8545로 지정된다.

 

이제 js 파일을 하나 만든다. 여기에 블록체인과 상호작용할 코드를 쭉 작성할 것이다.

 

2. pk > wallet, signer 생성

하드햇 네트워크를 구동하면 1000 ETH가 있는 pk, account 주소 20개를 제공해줄 것이다.

 

그 중 0번째 지갑의 개인키를 이용해 지갑, signer 인스턴스를 만들자.

 

// ethers_practice.js
const ethers = require('ethers')

// 하드햇 네트워크 구동시 터미널에 출력되는 값을 쓰면 된다. 디폴트는 내 아이피 8545번 포트를 사용한다.
const chainRPC = "http://127.0.0.1:8545"

// 블록체인과의 상호작용을 해줄 provider 인스턴스 생성
const provider = new ethers.providers.JsonRpcProvider(chainRPC)

// 하드햇에서 주는 0번 지갑을 사용했다.
const pk = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"

// pk를 통해 wallet, 서명을 처리해줄 signer 인스턴스를 만든다.
const wallet = new ethers.Wallet(pk. provider)
const signer = wallet.connect(provider)

 

3. 특정 지갑의 잔고 확인

가장 간단하게 다른 지갑의 잔고를 확인하는 것부터 해보자.

 

provider 인스턴스의 getBalance 메서드를 이용해 원하는 지갑의 잔고를 알아볼 수 있다.

 

나는 1번 지갑 주소의 잔고를 조회해보았다.

 

const getBalance = async () => {
    const balance = await provider.getBalance("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")
    console.log(ethers.utils.formatEther(balance))
}

const main = async() => {
	getBalance()
}

 

4. 트랜잭션 보내기

0번 지갑에서 1번 지갑으로 이더를 전송해보자. tx를 발생시키는 것은 돈이 필요하므로 여기서부터는 signer 인스턴스를 사용하게 된다.

 

4.1 tx 객체 만들기

우선 블록체인에 내가 원하는 작업을 수행하도록 해줄 명령어를 객체로 만들어줘야 한다.

 

const sendTx = async () => {
	const Tx = {
    	
        to : "<1번 지갑 주소>",
        value : ethers.utils.parseEther('100'),
        gasLimit : 21000,
        gasPrice : ethers.utils.parseUnits('20', 'gwei')
        chainId : 31337
    }
}

 

기본적으로 어디로 얼마나 보낼지, 가스비는 얼마나 사용할지, (실제 네트워크에서는 이를 적절히 조절해줘야 함), chainId는 뭔지를 지정해준 tx 객체를 만들어 주었다.

참고로 hardhat 네트워크의 체인 아이디는 31337이다.

 

여기서 서명을 해서 블록체인으로 보내면 된다.

 

const sendTx = async () => {
	const tx = {
    	...
    }
    
    // tx에 서명을 추가한 signedTx
    const signedTx = await signer.signTransaction(tx)
    
    // 블록체인에 서명된 tx를 전송
	const txResponse = await provider.sendTransaction(signedTx)
    
    // txHash를 출력
    console.log(txResponse.hash)
}

 

4.2 tx receipt 

바로 tx의 receipt를 확인하고 싶다면 두 가지 메서드를 사용할 수 있다.

 

i) getTransactionReceipt

const txReceipt = await provider.getTransactionReceipt(txResponse.hash)
console.log(txReceipt)

 

이 메서드는 tx의 블록체인에서의 컨펌 여부에 관계없이 즉, 영수증이 나왔건 말건 신경쓰지 않고 응답을 받아온다.

 

그래서 이 메서드를 사용하면 종종 receipt 값으로 null이 출력 된다.

 

따라서 tx send 직후 receipt를 보고 싶다면 이 메서드보다는 waitForTransaction 메서드가 더 유리하다.

 

ii) waitForTransaction

const txReceipt = wawit provider.waitFortransaction(txResponse.hash)
console.log(txReceipt)

이 메서드는 tx가 컨펌되지 않았다면 컨펌될 때까지 기다렸다가 receipt를 받아온다.

 

정리해보면 tx의 전송은 다음과 같이 할 수 있다.

 

const sendTx = async () => {
   
    // 다음과 같이 tx 객체를 만들어 서명 후, 보내면 된다.
    const tx = {
        to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
        value: ethers.utils.parseEther('100'),
        gasLimit: 21000,
        gasPrice: ethers.utils.parseUnits('30', 'gwei'),
        chainId : 31337
    }
   
    const signedTx = await signer.signTransaction(tx)
    const txResponse = await provider.sendTransaction(signedTx);
    console.log('Transaction sent:', txResponse.hash);
   
    const txReceipt = await provider.waitForTransaction(txResponse.hash)

    console.log(txReceipt)

}

const main = async() => {
	sendTx()
}

 

4.3 nonce error

그런데 tx 전송이 될 때도 있지만, 에러를 출력하면서 실패할 때도 있을 것이다.

 

나는 다음과 같은 에러를 계속해서 만나고 있는데, 

 

data: { message: 'Nonce too low. Expected nonce to be 1 but got 0.' }

 

hardhat을 사용해도, go-ethereum을 사용해도 nonce 값의 조절이 자동으로 되지 않는다.

 

이더리움에서 nonce 값은 해당 지갑이 네트워크에 tx를 몇 번 보냈는지 그 횟수를 의미한다.

 

따라서 하나의 지갑으로부터 만들어진 tx는 전부 nonce 값이 다르고, 이는 0부터 시작해서 1씩 증가해야 한다.

 

만약 tx를 처음 한 번 보내면 그 tx의 nonce는 0 이고, 다음 내가 보낼 tx의 nonce 값은 1이어야 한다.

 

나는 이게 자동으로 조절되는 줄 알고 있었는데 그렇지 않으니, 다음과 같이 수동으로 nonce값을 변경해주자.

 

const tx = {
	nonce : 1,
     	to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
     	value: ethers.utils.parseEther('100'),
     	gasLimit: 21000,
     	gasPrice: ethers.utils.parseUnits('30', 'gwei'),
     	chainId : 31337
}

 

다행히도 nonce 값은 어렵지 않게 바꿔줄 수 있으므로 일단 여기서는  특정 지갑 주소의 nonce 값을 확인하는 방법만 알아보고 넘어가자.

 

const getNonce = async () => {
	const nonce = await provider.getTransactionCount("<nonce 값을 알고싶은 지갑주소>")
	console.log(getNonce)
}

 

4.4 여러 개의 tx 보내기

앞서 말한 nonce 에러를 만난 후 생각해본건데, 똑같은 tx를 여러개 한 번에 보내고 싶다면 nonce 값과 가스비를 조절해주면 된다.

 

단 이런 경우 tx receipt를 받으려고 하면

 

tx 전송 > 대기해 receipt receive > 다음 tx > ...

 

이런 식으로 하나의 tx confirm 전까지 다음 tx를 보내지 못해 의미가 없으니 receipt 관련 코드는 제외하고 hash값만 확인하는게 좋다.

 

(하드햇 네트워크는 블록 생성 자체를 내 tx에 맞춰서 바로 해주니 모르겠는데, go-ethereum은 블록 생성까지 기다려야 했고

 

일반적으로도 이게 맞을 듯)

 

const sendTxs = async () => {
	// nonce 값을 가져온다.
	const nonce = await provider.getTransactionCount("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
    console.log(`nonce : ${nonce}`)

    // 연속적으로 tx를 보내고 싶다면 다음과 같이 nonce값, gassprice를 증가시키며 반복
    for (let i = nonce; i < nonce + 10; i++) {
        const tx = {
            nonce : i,
            to: '0x1d61b265007c71bde64ea2858bc31ece265c1e42',
            value: ethers.utils.parseEther('10'),
            gasLimit: 21000,
            
            // gasPrice가 0이 되면 tx 처리를 안하므로 1 이상은 줘야한다.
            gasPrice: ethers.utils.parseUnits(`${i+1}`, 'gwei'),
            chainId : 31337
        }
       
        const signedTx = await signer.signTransaction(tx)
        const txResponse = await provider.sendTransaction(signedTx);
        console.log('Transaction sent:', txResponse.hash);
    }
}

 

tx hash를 쭉 띄워주면 성공이다.

 

5. Contract

5.1 contract deploy

https://liferesetbutton.tistory.com/100 에서 사용한 Counter contract를 다시 사용한다.

 

compile까지만 해주자.

 

그러면 폴더에 Counter.json 파일이 생성될 것이고, 이 안에 abi, bytecode가 있을 것이다.

 

다음과 같이 deploy할 함수를 만들어줄 수 있다.

 

const deployContract = async () => {
	// contract를 compile한 json 경로
    const contractJSON = require('./artifacts/contracts/Counter/Counter.json')
    const abi = contractJSON.abi
    const bytecode = contractJSON.bytecode
	
    // 
    const contractFactory = new ethers.ContractFactory(abi, bytecode, signer)
    const contract = await contractFactory.deploy(0)
    await contract.deployed()
   
    console.log(`ca : ${contract.address}` )
}

 

ethers는 contract를 배포할때 contractFactory 인스턴스를 사용한다.

 

우선 abi, bytecode, signer를 넣어 contractFactory 인스턴스를 만든 후,

 

deploy() 메서드에 초기값들을 넣어 배포하면 된다.

 

ca는 기록해두자.

 

5.2 contract와 interact

이제 배포한 컨트랙트의 상태를 불러오고 수정도해보자.

 

우선 counter의 현재 숫자를 확인해보자.

 

알다시피, 컨트랙트와 상호작용하려면 해당 컨트랙트의 abi, ca를 알아야 한다.

 

const showNumber = async() => {
    const contractJSON = require('./artifacts/contracts/Counter/Counter.json')
    const abi = contractJSON.abi

    const contractInstance = new ethers.Contract("<배포된 contract의 address>", abi, provider);
    
    // 컨트랙트 인스턴스를 통해 컨트랙트의 함수를 호출할 수 있다.
    const currentNum = await contractInstance.showNum()
    console.log(`cueernt Number : ${currentNum}`)

}

 

여러 번 사용되는 contractJSON이나 abi, ca는 전역 변수로 만들어줘도 좋다.

 

 

5.2 contract의 state 변경

이제 컨트랙트의 state를 바꿔보자. increase() 함수를 사용할 것이다.

 

우선 필요한 정보들을 나열해보자.

 

const increaseNumber = async() => {
	const nonce = await provider.getTransactionCount("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
    
	const contractJSON = require('./artifacts/contracts/Counter/Counter.json')
	const abi = contractJSON.abi
	const ca = "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0"
    
	const contractInstance = new ethers.Contract(ca, abi, provider);
}

 

state를 바꾸려는 건 tx에 속하므로 전송인의 nonce값이 필요하고, contract와 상호작용하기 위해 abi, ca가 필요하다.

 

이제 어떤 함수를 실행시킬지는 다음과 같이 contractInstance의 interface의 encodefunctionData 메서드를 사용한다.

 

const increaseNumber = async() => {
	// ...
    
    // increase 함수를 실행하겠다는 코드
    const data = contractInstance.interface.encodeFunctionData("increase")
    
    const tx = {
    	nonce : nonce,
        // to의 value는 상호작용할 contract를 넣는다.
        to : ca,
        gasLimit : 210000,
        gasPrice: ethers.utils.parseUnits('30', 'gwei'),
        chainId : 31337,
        
        // tx에 이 data를 넣어준다.
        data : data
    }
}

 

지금은 실행하는 함수의 매개변수가 없어 약간 더 간단하지만, 매개변수를 넣어주고 싶다면 data 변수를 수정하면 된다.

 

한 예시로 ethereum의 deposit contract의 deposit 함수는 4개의 매개변수를 받는데, 다음과 같이 적어줄 수 있다.

 

const deposit_data = require(./deposit_data-1670987626.json)
const dd = deposit_data[0]

const bufferHex = (x) => Buffer.from(x, "hex")

const params = [
    bufferHex(dd.pubkey),
    bufferHex(dd.withdrawal_credentials),
    bufferHex(dd.signature),
    bufferHex(dd.deposit_data_root)
]

// 매개변수를 담은 배열을 두 번째 인수로 준다.
const data = contractInstance.interface.encodeFunctionData("deposit", params)

 

 

다시 돌아와서, 이후로는 아까 tx를 실행했을 때와 동일하게 서명해서 보내고, 원한다면 기다렸다가 receipt를 받으면 된다.

 

const increaseNumber = async() => {
    const contractJSON = require('./artifacts/contracts/Counter/Counter.json')
    const abi = contractJSON.abi
    const ca = "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0"
    const contractInstance = new ethers.Contract("0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", abi, provider);

    const nonce = await provider.getTransactionCount("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");

    const data = contractInstance.interface.encodeFunctionData("increase")

    const tx = {
        nonce : nonce,
        to : ca,
        gasLimit: 210000,
        gasPrice: ethers.utils.parseUnits('30', 'gwei'),
        chainId : 31337,
        data : data
    }

    const signedTx = await signer.signTransaction(tx)
    const txResponse = await provider.sendTransaction(signedTx);
    console.log('Transaction sent:', txResponse.hash);
   
    const txReceipt = await provider.waitForTransaction(txResponse.hash)
    console.log(txReceipt)
}

 

 

 

6. Summary

const ethers = require('ethers')

const chainRPC = "http://127.0.0.1:8545"
const provider = new ethers.providers.JsonRpcProvider(chainRPC);

const pk = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
const wallet = new ethers.Wallet(pk, provider)
const signer = wallet.connect(provider)

const getBalance = async () => {
    const balance = await provider.getBalance("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")
    console.log(ethers.utils.formatEther(balance))
}

const sendTx = async () => {
   
    // 다음과 같이 tx 객체를 만들어 서명 후, 보내면 된다.
    const tx = {
        nonce : 1,
        to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
        value: ethers.utils.parseEther('100'),
        gasLimit: 21000,
        gasPrice: ethers.utils.parseUnits('30', 'gwei'),
        chainId : 31337
    }
   
    const signedTx = await signer.signTransaction(tx)
    const txResponse = await provider.sendTransaction(signedTx);
    console.log('Transaction sent:', txResponse.hash);
   
    const txReceipt = await provider.waitForTransaction(txResponse.hash)

    console.log(txReceipt)


   
    // const txReceipt = await provider.getTransactionReceipt(txResponse.hash)
    // console.log(txReceipt)
   
    // 이건 tx가 컨펌되건 말건 상관없이 요청을 보내 응답을 가져오므로
    // 영수증이 null이 뜰 수가 있다.
    // 방금 보낸 tx를 받고 싶다면 위의 waitForTransaction method를 사용할 것.
}

const getNonce = async() => {
    const nonce = await provider.getTransactionCount("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")
    console.log(nonce)
}

const sendTxs = async() => {
   
    // nonce 값은 tx 보내고 confirm되기 전까지 바뀌지 않는다.
    // 값이 업데이트 되기 전까지 시간이 걸리는데 연속적으로 tx를 보내려면
    const nonce = await provider.getTransactionCount("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
    console.log(`nonce : ${nonce}`)

    // 연속적으로 tx를 보내고 싶다면 다음과 같이 nonce값, gassprice를 증가시키며 반복
    for (let i = nonce; i < nonce + 10; i++) {
        const tx = {
            nonce : i,
            to: '0x1d61b265007c71bde64ea2858bc31ece265c1e42',
            value: ethers.utils.parseEther('10'),
            gasLimit: 21000,
            gasPrice: ethers.utils.parseUnits(`${i}`, 'gwei'),
            chainId : 31337
        }
       
        const signedTx = await signer.signTransaction(tx)
        const txResponse = await provider.sendTransaction(signedTx);
        console.log('Transaction sent:', txResponse.hash);
    }
}

const deployContract = async () => {
    const contractJSON = require('./artifacts/contracts/Counter/Counter.json')
    const abi = contractJSON.abi
    const bytecode = contractJSON.bytecode

   const contractFactory = new ethers.ContractFactory(abi, bytecode, signer)
   const contract = await contractFactory.deploy(0)
   await contract.deployed()
   
   console.log(`ca : ${contract.address}` )
   
   // 이 안에 있는 함수를 사용할 수 있다. contractInstance.deposit 처럼..
   const contractInstance = new ethers.Contract(contract.address, abi, provider);
   const currentNum = await contractInstance.showNum()
   console.log(`cueernt Number : ${currentNum}`)
}

const showNumber = async() => {
    const contractJSON = require('./artifacts/contracts/Counter/Counter.json')
    const abi = contractJSON.abi

    const contractInstance = new ethers.Contract("0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", abi, provider);
    const currentNum = await contractInstance.showNum()
    console.log(`cueernt Number : ${currentNum}`)

}

const increaseNumber = async() => {
    const contractJSON = require('./artifacts/contracts/Counter/Counter.json')
    const abi = contractJSON.abi
    const ca = "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0"
    const contractInstance = new ethers.Contract("0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", abi, provider);

    const nonce = await provider.getTransactionCount("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");

    const data = contractInstance.interface.encodeFunctionData("increase")

    const tx = {
        nonce : nonce,
        to : ca,
        gasLimit: 210000,
        gasPrice: ethers.utils.parseUnits('30', 'gwei'),
        chainId : 31337,
        data : data
    }

    const signedTx = await signer.signTransaction(tx)
    const txResponse = await provider.sendTransaction(signedTx);
    console.log('Transaction sent:', txResponse.hash);
   
    const txReceipt = await provider.waitForTransaction(txResponse.hash)
    console.log(txReceipt)
}


const main = async () => {
    // getBalance()
    // sendTx()
    // getNonce()
    // sendTxs()
    // deployContract()
    // showNumber()
    increaseNumber()
}

main()

 

nonce 값이나 tx confirm 등의 이유로 한 번에 다 실행하려고 하면 에러가 날 가능성이 높으니 몇 개씩 끊어서 테스트해보자.

'Blockchain' 카테고리의 다른 글

hardhat network config 설정 (alchemy)  (0) 2023.04.01
Hardhat  (1) 2022.11.02
#33 NFT minting dApp 만들기 ch3  (0) 2022.08.05
#32 NFT Minting dApp 만들기 ch2  (0) 2022.08.05
#31 NFT minting dApp 만들기 ch1  (0) 2022.08.04