[Solidity+Go] geth로 스마트 컨트랙트 배포하기 - 4. 배포된 스마트 컨트랙트와 상호작용

geth로 스마트 컨트랙트 배포하기 · 4/7
  1. 1. 초기 구성
  2. 2. 스마트 컨트랙트로 Go 코드 생성
  3. 3. 생성된 Go 코드로 스마트 컨트랙트 배포
  4. 4. 배포된 스마트 컨트랙트와 상호작용
  5. 5. 이벤트 구독
  6. 6. 메타데이터를 사용한 standard json input 생성
  7. 7. standard json input을 사용해 배포한 컨트랙트 검증

1. 코드 작성

cmd/interact/main.go

package main

import (
	"context"
	"crypto/ecdsa"
	"fmt"
	token "go-ethereum-example/gen"
	"math/big"
	"os"

	_ "github.com/joho/godotenv/autoload"

	"github.com/ethereum/go-ethereum"
	"github.com/ethereum/go-ethereum/accounts/abi/bind"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/ethclient"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Connect to Ethereum client with RPC endpoint
	client, err := ethclient.DialContext(ctx, os.Getenv("RPC_ENDPOINT"))
	handleError(err)

	defer client.Close()

	fmt.Println("Successfully connected to Ethereum client")

	// Change these addresses to match your contract!
	contractAddress := common.HexToAddress("0x5FbDB2315678afecb367f032d93F642f64180aa3")
	toAddress := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")

	// Create an instance of the contract, specifying its address
	tokenInstance, err := token.NewToken(contractAddress, client)
	handleError(err)

	// Parse wallet private key
	privateKey := mustParsePrivateKey()
	address := crypto.PubkeyToAddress(privateKey.PublicKey)

	// Get nonce, gas price and chain ID
	nonce, err := client.PendingNonceAt(context.Background(), address)
	handleError(err)

	gasPrice, err := client.SuggestGasPrice(context.Background())
	handleError(err)

	fmt.Printf("Suggested gas price: %s\n", gasPrice)

	chainID, err := client.NetworkID(context.Background())
	handleError(err)

	fmt.Printf("Chain ID: %d\n", chainID)

	// Create an transactor with the private key, chain ID and nonce
	signer, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID)
	handleError(err)

	signer.GasPrice = gasPrice
	signer.GasLimit = 3000000
	signer.Nonce = big.NewInt(int64(nonce))

	// Call transfer method (state-changing)
	tx, err := tokenInstance.Transfer(signer, toAddress, big.NewInt(1000000))
	handleError(err)

	fmt.Printf("Transaction hash: %s\n", tx.Hash().Hex())

	// Wait for the transaction to be mined
	receipt, err := bind.WaitMined(context.Background(), client, tx)
	handleError(err)

	fmt.Printf("Transaction receipt status %d\n", receipt.Status)

	// If the transaction was reverted by the EVM, we can see the reason
	if receipt.Status == 0 {
		msg := ethereum.CallMsg{
			From:     address,
			To:       tx.To(),
			Gas:      tx.Gas(),
			GasPrice: tx.GasPrice(),
			Value:    tx.Value(),
			Data:     tx.Data(),
		}

		_, err = client.CallContract(ctx, msg, nil)
		fmt.Printf("Transaction reverted: %v\n", err)
		return
	}

	// Extract the transfer event from the receipt
	var transferred *token.TokenTransfer

	for _, log := range receipt.Logs {
		transferred, err = tokenInstance.ParseTransfer(*log)
		if err == nil {
			break
		}
	}

	if transferred != nil {
		fmt.Printf("Transferred %d tokens from %s to %s\n", transferred.Value, transferred.From.Hex(), transferred.To.Hex())
	}

	// Call the contract method (read-only)
	toBalance, err := tokenInstance.BalanceOf(&bind.CallOpts{Context: ctx}, toAddress)
	handleError(err)

	fmt.Printf("To balance: %d\n", toBalance)
}

func mustParsePrivateKey() *ecdsa.PrivateKey {
	rawPrivateKey := os.Getenv("PRIVATE_KEY")

	// Parse the private key
	privateKey, err := crypto.HexToECDSA(rawPrivateKey)
	handleError(err)

	return privateKey
}

func handleError(err error) {
	if err != nil {
		panic(err)
	}
}

테스트넷 RPC 클라이언트 연결

// Connect to Ethereum client with RPC endpoint
client, err := ethclient.DialContext(ctx, os.Getenv("RPC_ENDPOINT"))
handleError(err)

defer client.Close()

fmt.Println("Successfully connected to Ethereum client")

배포된 스마트 컨트랙트의 인스턴스 생성

// Change these addresses to match your contract!
contractAddress := common.HexToAddress("0x5FbDB2315678afecb367f032d93F642f64180aa3")
toAddress := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")

// Create an instance of the contract, specifying its address
tokenInstance, err := token.NewToken(contractAddress, client)
handleError(err)

contractAddress: 배포된 스마트 컨트랙트의 주소

toAddress: transfer 함수의 대상 주소, 로컬 테스트넷 실행 시에 제공되는 두 번째 주소를 사용함

16진수 프라이빗 키를 ECDSA 프라이빗 키로 파싱

// Parse wallet private key
privateKey := mustParsePrivateKey()

트랜잭션 서명자 생성

address := crypto.PubkeyToAddress(privateKey.PublicKey)

fmt.Printf("Deploying contract from address %s\n", address.Hex())

// Get nonce, gas price and chain ID from the Ethereum client
nonce, err := client.PendingNonceAt(ctx, address)
handleError(err)

gasPrice, err := client.SuggestGasPrice(ctx)
handleError(err)

fmt.Printf("Suggested gas price: %s\n", gasPrice)

chainID, err := client.NetworkID(ctx)
handleError(err)

fmt.Printf("Chain ID: %d\n", chainID)

// Create an signer with the private key, chain ID and nonce
signer, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID)
handleError(err)

signer.GasPrice = gasPrice
signer.GasLimit = 3000000
signer.Nonce = big.NewInt(int64(nonce))

transfer 함수 실행 및 마이닝 된 트랜잭션 상태 확인

// Call transfer method (state-changing)
tx, err := tokenInstance.Transfer(signer, toAddress, big.NewInt(1000000))
handleError(err)

fmt.Printf("Transaction hash: %s\n", tx.Hash().Hex())

// Wait for the transaction to be mined
receipt, err := bind.WaitMined(context.Background(), client, tx)
handleError(err)

fmt.Printf("Transaction receipt status %d\n", receipt.Status)

// If the transaction was reverted by the EVM, we can see the reason
if receipt.Status == 0 {
	msg := ethereum.CallMsg{
		From:     address,
		To:       tx.To(),
		Gas:      tx.Gas(),
		GasPrice: tx.GasPrice(),
		Value:    tx.Value(),
		Data:     tx.Data(),
	}

	_, err = client.CallContract(ctx, msg, nil)
	fmt.Printf("Transaction reverted: %v\n", err)
	return
}

로그에서 Transfer 이벤트 추출

// Extract the transfer event from the receipt
var transferred *token.TokenTransfer

for _, log := range receipt.Logs {
	transferred, err = tokenInstance.ParseTransfer(*log)
	if err == nil {
		break
	}
}

if transferred != nil {
	fmt.Printf("Transferred %d tokens from %s to %s\n", transferred.Value, transferred.From.Hex(), transferred.To.Hex())
}

토큰을 전송받은 주소의 잔액 확인

// Call the contract method (read-only)
toBalance, err := tokenInstance.BalanceOf(&bind.CallOpts{Context: ctx}, toAddress)
handleError(err)

fmt.Printf("To balance: %d\n", toBalance)

2. 코드 실행

$ go run ./cmd/interact/
2023/12/12 13:38:36 Successfully connected to Ethereum client
Suggested gas price: 1879547559
Chain ID: 31337
Transaction hash: 0x8336dceaef677f437377c6c90caf4ba15c3851c8f386c060e386fab5f90df64c
Transaction receipt status 1
Transferred 1000000 tokens from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 to 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
To balance: 1000000

3. 트랜잭션이 revert되는 경우

함수 호출에 잘못된 인수 전달

tx, err := tokenInstance.Transfer(signer, toAddress, big.NewInt(-1))

코드 실행

$ go run ./cmd/interact/
Successfully connected to Ethereum client
Suggested gas price: 1879547559
Chain ID: 31337
Transaction hash: 0x428986a4b3faa46db5e354fc218ed7b31ff09706e00204fd30340fe401bcd81e
Transaction receipt status 0
Transaction reverted: execution reverted

4. 전체 코드