JAVA SpringBoot(스프링부트) Remix Sol 블록체인 로컬서버 8545 연동

목록으로 돌아가기

들어가며

자바 스프링부트관련 블록체인 소스 코드는 블로그에 많지만 정확한 연동 관련 글은 많지 않은 것 같아 글을 작성하게 됐습니다. 사실 블록체인에 관심이 많은 편은 아니기도 하고… 학습한 시간 자체가 짧아서 그런걸까 인터넷 상의 정보만으로 연동하기가 어렵더라구요… 아무튼 바로 시작하겠습니다.

순서

3번까지 자바 스프링 부트에서 진행할 필요 없습니다. 독단적인 폴더에서 진행하셔도 됩니다.

  1. sol 파일 준비하기
  2. sol 파일 bin, abi 파일 추출 (2가지 방법)
  3. web3j를 활용한 자바 파일 추출
  4. springboot 연동법

환경

진행된 컴퓨터는 Mac M1 Os입니다. 윈도우도 할 수 있는 방법에 대해서 같이 언급은 드릴예정입니다.

혹시라도 궁금해 하실 분이 있을 거 같아 geth 버젼도 공유해드립니다.

    sol % geth version
    Geth
    Version: 1.9.9-stable
    Git Commit: 017449971e1e9e220efcd97d3313a0e27f47003b
    Git Commit Date: 20191206
    Architecture: amd64
    Protocol Versions: [64 63]
    Go Version: go1.13.5
    Operating System: darwin
    GOPATH=
    GOROOT=/Users/travis/.gimme/versions/go1.13.5.darwin.amd64



1단계 Sol 파일 개발

먼저 배포를 할 Sol 파일을 준비합니다. 저 같은 경우에는 아래와 같은 간단한 기부 시스템 sol을 작성했습니다.

pragma solidity ^0.4.24; // ①

contract DonationSystem {
    // 사용자의 등록 여부를 저장하는 매핑
    mapping(address => bool) public users;

    // 사용자의 잔액을 저장하는 매핑
    mapping(address => uint256) public balances;

    // 사용자 등록 이벤트
    event UserRegistered(address indexed userAddress);

    // 계정 충전 이벤트
    event AccountFunded(address indexed userAddress, uint256 amount);

    // 이체 이벤트
    event TransferE(address indexed from, address indexed to, uint256 amount);

    // 사용자 등록 함수
    function register(address userAddress) public returns (address) {
        // 사용자가 이미 등록되어 있는지 확인
        require(!users[userAddress], "User already registered");
        
        // 사용자 등록
        users[userAddress] = true;
        
        // 사용자 등록 이벤트 발생
        emit UserRegistered(userAddress);
        
        // 등록된 사용자의 주소 반환
        return userAddress;
    }

    // 계정 충전 함수
    function fundAccount(address userAddress, uint256 amount) public payable returns (bool) {
        // 충전할 이더량이 유효한지 확인
        require(amount > 0, "Funding amount must be greater than zero");

        // 사용자의 잔액 증가
        balances[userAddress] += amount;
        
        // 계정 충전 이벤트 발생
        emit AccountFunded(userAddress, amount);
        
        // 성공적으로 충전됨을 반환
        return true;
    }

    // 계정 출금 함수
    function withdrawFromAccount(address userAddress, uint256 amount) public returns (bool) {
        // 출금할 이더량이 유효한지 확인
        require(amount > 0, "Withdrawal amount must be greater than zero");
        require(balances[userAddress] >= amount, "Insufficient balance");

        // 사용자의 잔액 감소
        balances[userAddress] -= amount;
        
        // 계정 출금 이벤트 발생
        emit AccountFunded(userAddress, amount);
        
        // 성공적으로 출금됨을 반환
        return true;
    }

    // 이체 함수
    function transfer(address from, address to, uint256 amount) public returns (bool) {
        // 송신자와 수신자가 등록된 사용자인지 확인
        require(users[from], "Sender is not registered");
        require(users[to], "Recipient is not registered");
        
        // 송신자의 잔액이 충분한지 확인
        require(balances[from] >= amount, "Insufficient balance");

        // 송신자의 잔액 감소, 수신자의 잔액 증가
        balances[from] -= amount;
        balances[to] += amount;
        
        // 이체 이벤트 발생
        emit TransferE(from, to, amount);
        
        // 이체 성공을 반환
        return true;
    }

    // 사용자의 잔액 조회 함수
    function getBalance(address userAddress) public view returns (uint256) {
        // 사용자의 잔액 반환
        return balances[userAddress];
    }
}




2단계 Sol bin, abi 파일 추출하기

방법 1 > Sol MacOs, Windows Remix 사용하기

Remix 사용법은 추후 포스팅 하겠습니다. 해당 내용은 구글링 하시면 자료가 많아 여기서는 언급안하겠습니다.
sol 파일을 remix에 올린뒤 컴파일 합니다.

리믹스 바로가기

아래 사진과 같이 컴파일 창에서 컴파일을 하게 되면 ABI와 Bytecode 복사 버튼이 생깁니다.

이미지 설명

vscode 편집기를 연 다음 원하시는 이름으로 .abi .bin 확장자 파일을 만드시고 내용을 복붙하시면 됩니다.

DonationSystem.abi  # ABI 내용 복붙한 파일
DonationSystem.bin  # Bytecode 내용 복붙한 파일

방법 2 > Sol MacOs solidity 사용하기

먼저 solidity를 설치합니다.

brew install solidity

그후 터미널 창에서 sol 파일 디렉토리로 이동합니다.
아래 명령어를 활용해서 bin, sol 파일을 추출합니다.
만일 sol 파일의 버젼이 0.8.25 버전 이상이어야 한다는 에러가 발생한다면 방법 1로 진행하세요!
geth 버젼에 따라 0.8.25로 컴파일 후 진행하시면 거래 실패가 계속 될 수 있습니다.
제가 그랬거든요… ㅠㅠ 호환되는지 궁금하시다면 먼저 remix에서 8545 포트 연동 후 테스트 해보시면 됩니다.
테스트 시 트렌젝션 발생으로 판단하지 마시고 거래 송금이 진짜 됐는지 찍어서 확인해 주셔야 합니다.
반대로 geth 버젼이 높으시다면 꼭 제가한 0.4.24 버젼 말고 딴걸로 하셔야 해요

solc --bin --abi DonationSystem.sol -o build/

아래와 같이 build 폴더가 생성된 것을 확인하실 수 있습니다.
폴더 안에 bin과 abi 파일이 생성됐을 겁니다.

% ls

DonationSystem.sol	build



3단계 web3j를 활용한 자바 파일 추출

web3j를 먼저 설치해야 합니다.
먼저 화 내지 마시고 openjdk 에러가 발생한다면 다음 블로그를 보고 web3j를 설치해주세요 관련 내용을 종합했습니다.
openjdk_web3j 에러 해결 포스트

Mac/Linux

brew tap web3j/web3j
brew install web3j

Windows

choco install web3j

설치가 완료 됐다면 이제 자바 파일을 추출하면됩니다. -0 src 는 자바 src가 아니라 그냥 임의로 한겁니다. 독립적인 폴더에서 해도 상관없어요.

# web3j solidity generate -b [bin파일경로] -a [abi파일경로] -o [생성될 파일 위치] -p [패키지 이름]

web3j solidity generate -b build/DonationSystem.bin -a build/DonationSystem.abi -o src -p com

하게 되면 src 폴더가 생성되고 com 폴더 밑에 자바 파일이 생성된 것을 확인할 수 있습니다.



4단계 springboot 연동법

이제 추출된 자바 파일을 스프링부트 패키지에 원하는 곳에 옯겨주세요.
대게 contract 폴더를 생성한 뒤 그 안에 저장을 합니다.
아래 이미지를 보시면 이해가 가실겁니다. 일단 contract 폴더에 생성했던 파일을 옮겼습니다.

이미지 설명

아래는 이제 추출된 클래스를 사용할 수 있게 서비스단 객체를 만드는 부분입니다.
중요한 부분은 다음과 같이 sol 파일에서 만들었던 함수에 아래와 같이 매개변수 값을 주어 send 하는 형식으로 진행됩니다.
fundAccount 같은 경우 Wei 요소의 추가 파라미터를 요구하기 때문에 인자가 하나더 있는 것뿐입니다.
contract.fundAccount(userAddress, amount, amount).send();

DonationSystemClient 소스코드

package com.oneul.service;

import org.web3j.crypto.Credentials;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.methods.response.TransactionReceipt;
import org.web3j.protocol.http.HttpService;
import org.web3j.tx.ClientTransactionManager;
import org.web3j.tx.TransactionManager;
import org.web3j.tx.gas.ContractGasProvider;
import org.web3j.tx.gas.StaticGasProvider;

import com.oneul.contract.DonationSystem;

import java.security.SecureRandom;

import java.math.BigInteger;

public class DonationSystemClient {

    private static final String PRIVATE_KEY = "geth에서 8545 포트에 배포했던 유저 개인키 값";
    private static final String INFURA_URL = "http://localhost:8545";
    private static final BigInteger GAS_PRICE = BigInteger.valueOf(20000000000L);
    private static final BigInteger GAS_LIMIT = BigInteger.valueOf(6721975L);

    private Web3j web3;
    private DonationSystem contract;
    private Credentials credentials;
    private TransactionManager transactionManager;
    private ContractGasProvider gasProvider;

    public DonationSystemClient() {
        this.web3 = Web3j.build(new HttpService(INFURA_URL));
        this.credentials = Credentials.create(PRIVATE_KEY);
        this.transactionManager = new ClientTransactionManager(web3, credentials.getAddress());
        this.gasProvider = new StaticGasProvider(GAS_PRICE, GAS_LIMIT);

        try {
            this.contract = DonationSystem.deploy(
                    web3,
                    transactionManager,
                    gasProvider).send();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public String registerUser() {
        try {
            while (true) {
                String userAddress = generateRandomAddress();
                TransactionReceipt registerReceipt = contract.register(userAddress).send();
                // 성공한 경우
                if (registerReceipt.isStatusOK()) {
                    return userAddress;
                }
                // 실패한 경우
                else {
                    System.out.println("Failed to register user, retrying...");
                    // 잠시 대기 후 재시도
                    Thread.sleep(1000); // 1초 대기
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            return "Failed to register user";
        }
    }

    public static String generateRandomAddress() {
        SecureRandom random = new SecureRandom();
        byte[] addressBytes = new byte[20]; // 20 바이트 크기의 주소를 생성
        random.nextBytes(addressBytes);
        return new BigInteger(1, addressBytes).toString(16); // 16진수로 변환하여 반환
    }

    public String fundAccount(String userAddress, BigInteger amount) {
        try {
            TransactionReceipt fundReceipt = contract.fundAccount(userAddress, amount, amount).send(); // 1 ETH
            System.out.println(fundReceipt);
            return "Account funded successfully with amount: " + amount.toString();
        } catch (Exception e) {
            e.printStackTrace();
            return "Failed to fund account";
        }
    }

    public String withdrawFromAccount(String userAddress, BigInteger amount) {
        try {
            TransactionReceipt withdrawReceipt = contract.withdrawFromAccount(userAddress, amount).send(); // 1 ETH
            return "Amount withdrawn successfully: " + amount.toString();
        } catch (Exception e) {
            e.printStackTrace();
            return "Failed to withdraw amount";
        }
    }

    public String getBalance(String userAddress) {
        try {
            System.out.println(userAddress);
            BigInteger balance = contract.getBalance(userAddress).send();
            return "Balance: " + balance.toString();
        } catch (Exception e) {
            e.printStackTrace();
            return "Failed to get balance";
        }
    }

    public String transfer(String from, String to, BigInteger amount) {
        try {
            TransactionReceipt transferReceipt = contract.transfer(from, to, amount).send();
            return "Transfer completed successfully";
        } catch (Exception e) {

            return "Failed to transfer";
        }
    }
}

DonationController 소스코드

package com.oneul.controller;

import com.oneul.service.DonationSystemClient;
import org.springframework.web.bind.annotation.*;

import java.math.BigInteger;

@RestController
@RequestMapping("/donation")
public class DonationController {

    private final DonationSystemClient donationSystemClient;

    public DonationController() {
        this.donationSystemClient = new DonationSystemClient();
    }

    @PostMapping("/register")
    public String registerUser() {
        return donationSystemClient.registerUser();
    }

    @PostMapping("/fund")
    public String fundAccount(@RequestParam String userAddress, @RequestParam BigInteger amount) {
        return donationSystemClient.fundAccount(userAddress, amount);
    }

    @PostMapping("/withdraw")
    public String withdrawFromAccount(@RequestParam String userAddress, @RequestParam BigInteger amount) {
        return donationSystemClient.withdrawFromAccount(userAddress, amount);
    }

    @PostMapping("/balance")
    public String getBalance(@RequestParam String userAddress) {
        return donationSystemClient.getBalance(userAddress);
    }

    @PostMapping("/transfer")
    public String transfer(@RequestParam String from, @RequestParam String to, @RequestParam BigInteger amount) {
        return donationSystemClient.transfer(from, to, amount);
    }
}



작게 나마 도움이 되셨으면 좋겠습니다.

author-profile
Written by 유찬영

댓글