4 [텍스트 P2E게임] {배치서버 구현[오류해결]}

🔥 개요

일단 솔리디티 코드 작성 작업이 끝나게 되었습니다.

이더스캔에서 이름이 인식되지 않는 이유는 별게 아니였습니다

일단 변경하는데에 시간이 필요하였습니다.

  • 즉 그냥 기다리면 되는 것...

저는 함수 이름을 바꿔서 그런줄 알아서

함수 이름을 그대로 사용하 였습니다.

하지만 이런식이면 ERC-721과 ERC-20을 하나의 컨트랙트에서 사용이 불가능 합니다...

  • 왜냐하면 함수의 이름이 같기 떄문입니다.

그러기 떄문에

import "./libraries/Token.sol";
import "./libraries/NFT.sol";

contract Character is NFT("item", "ITM") {
    Token private gold;

이런식으로 참고할수 있는 변수를 선언함으로써 활용을 하였습니다.

constructor(address token) {
        gold = Token(token);
    }

    function goldTotalSupply() public view returns (uint256) {
        return gold.totalSupply();
    }

    function goldBalanceOf(address account) public view returns (uint256) {
        return gold.balanceOf(account);
    }

    function goldCheck(address account) public view returns (bool) {
        return gold.check(account);
    }

    function goldMint(address to, uint256 amount) public {
        gold.mintGold(amount, to);
    }

    function goldTransfer(
        address from,
        address to,
        uint256 amount
    ) public {
        gold.transfer(from, to, amount);
    }

    function goldBurn(address account, uint256 amount) public {
        gold.burn(account, amount);
    }

    function mintAll(address[] memory account, uint256 amount) public {
        gold.mintGoldAll(account, amount);
    }
  • 그후 Token의 함수를 사용하기 위해 작성해 주었습니다

그후 Character 컨트랙트를 작성 하였습니다.

 function makeCharacter(address _address) public returns (bool) {
        require(checkUser[_address] == false);
        checkUser[_address] = true;
        Characters storage character = _Character[_address];
        character.Pow = 1;
        character.limit = 300;

        emit NewUser(_address, character.Pow, character.limit);
        return true;
    }

    function IncreasePow(address _address) public isOwner(_address) {
        require(goldBalanceOf(_address) >= PowFee);
        goldBurn(_address, PowFee);

        Characters storage character = _Character[_address];
        uint32 number = uint32(getStatus());
        character.Pow += number;
    }

까다롭게 작성을 하지는 않았습니다.

단순히 캐릭터를 생성해 주게 되고 해당 캐릭터의 능력치를 조절할수 있는 함수를 몇가지 추가 하였습니다.

  • 이후의 함수는 더 있습니다.

이 부분에서 좀더 랜덤적인 요소를 추가하는 것이 게임의 재미를 향상 시킬것 같아서

keccack256을 활용 하였습니다.

function getRandomNumber() internal view returns (uint256) {
        uint256 total = goldTotalSupply();
        return uint256(keccak256(abi.encodePacked(msg.sender, total))) % 100;
    }

    function getStatus() public view returns (uint256) {
        return getRandomNumber() % 10;
    }
  • 이 부분이 랜덤한 값을 뺴오는 부분 입니다.

이후 저는 컨트랙트 작업이 끝났다고 생각을 하였고

이제 web3를 활용하여 서버에서 일정 시간마다 토큰을 전송해주는 부분을 작성 하였습니다.

  • 이 부분은 node-schedule를 활용하였습니다.

node-schdule를 활용하여 일정 시간마다 함수나 axios가 작동하게 해주었습니다.

배치서버를 구현하여 하나의 서버에서 모든것을 처리하기보다는 다른 서버에서 일을 나눠서 작업하게 구현을 하였습니다.

const TokenSchema = mongoose.Schema({
  check: {
    type: Boolean,
    default: false,
  },
  To: {
    type: String,
    default: "",
  },
  To_Array: {
    type: [String],
    default: [],
  },
  messageHash: String,
  v: String,
  r: String,
  s: String,
  rawTransaction: String,
  transactionHash: String,
});
  • DB 모델 입니다.
let tx = {
      from: process.env.Server_Address,
      to: process.env.Character_CA,
      nonce: nonce,
      gas: 50000,
      data: method.mintAll(address, 10).encodeABI(),
    };
    await web3.eth.accounts
      .signTransaction(tx, process.env.Server_PrivateKey)
      .then(async (Tx) => {
        const makeTokenDB = await new TokenDB({
          To_Array: address,
          messageHash: Tx.messageHash,
          v: Tx.v,
          r: Tx.r,
          s: Tx.s,
          rawTransaction: Tx.rawTransaction,
          transactionHash: Tx.transactionHash,
        });
        makeTokenDB.save();
      });
  • 특정 시간에 작동하는 코드 입니다.

이런식으로 DB에 값을 저장한뒤에


 const answer = await TokenDB.find({ check: false });
 
 for (let i = 0; i < answer.length; i++) {
      await web3.eth.sendSignedTransaction(
        answer[i].rawTransaction,
        (err, hash) => {
          if (err) console.log(err);
          else console.log(hash);
        }
      );
      await TokenDB.findOneAndUpdate(
        { id: answer[i].id },
        {
          check: true,
        },
        {
          new: true,
        }
      );
      console.log("BlockChain토큰 지급 완료!");
    }

이후 다른 시간대에 DB에 있는 값을 뺴와서 실제로 트랜잭션을 전송하게 해준뒤에

전송한 DB값을 갱신하에 후에 다시 가져오지 않게 수정합니다.

  • 부분적인 코드만 가져왔습니다.

🔥 문제점 발견 및 해결

일단 오류 문구는 Returned error: nonce too low라는 문구가 발생을 하였습니다.

DB에 값이 한개가 있고 이 부분을 처리하면 저런 오류가 발생을 하지 않지만

두개 이상이 되면 바로 오류가 발생을 하였습니다.

이 문제는 굉장히 간단한 문제였지만 처음 보는 에러여서 저는 많은 시간을 투자 해야 했습니다...

🔨 문제 원인

실제로 트랜잭션이 전송이 되지 않았기 떄문에 발생하는 오류 입니다.

만약 DB에 저장되는 트랜잭션이 2개라면 아직은 트랜잭션이 전송이 되지 않았기 때문에 같은 논스의 값을 가진 트랜잭션이 저장이 될 것 입니다.

그러면 먼저 저장된 값이 트랜잭션이 전송이 되면 그후에 오는 Nonce값은 이전 값의 Nonce값을 가지고 있기 떄문에

저런 오류가 발생을 하는 것 이였습니다.

그러기 떄문에 저는 트랜잭션을 DB에 저장을 할떄에 Nonce값을 1씩 갱신시켜 줘야 합니다.

🔨 해결

일단 기본적으로 web3에서 nonce값을 가져오는 코드는 web3.eth.getTransactionCount(address) 입니다.

하지만 이 방법으로 갱신을 하는 것은 실제로 트랜잭션이 검증이 이루어 지지 않은 상태이기 떄문에

검증이 이루어 지기 전에는 계속해서 같은 값이 나오게 될 것입니다.

그러기 떄문에 임의적으로 갱신시킨 nonce값을 저장해 주어야 합니다.

export let nonce;

const getnonce = async () => {
  const firstNonce = await web3.eth.getTransactionCount(
    process.env.Server_Address
  );
  nonce = firstNonce;
  console.log("account의 nonce값 = " + nonce);
};

export const plusnonce = async () => {
  nonce++;
};

두가지 함수를 사용 하였습니다.

일단 서버가 시작될떄에 nonce라는 변수를 갱신시켜 줍니다.

그후 DB에 값이 저장될떄마다 plusnonce를 작동시킴으로써 임의적으로 증가된 nonce를 트랜잭션에 넣어주는 것 입니다.

그러면 처음 DB에 저장이 되는 nonce는 1, 그후는 2, 3. 4 이런식으로 저장이 될 것입니다.

  • 여기에서 주의해야하는 점은 실제로 트랜잭션이 전송이 되고 검증이 이루어 지지 않으면
  • web3.eth.getTransactionCount(address)값은 계속 고정이 된다는 점 입니다.

🔥 후기 및 계획

간단한 문제였지만 생각보다 시간을 많이 잡아먹었습니다...ㅠㅠ

하지만 그래도 이제 솔리디티 작성 부분은 끝났습니다.

  • 추가적으로 경매 기능을 작성해보는 계획은 가지고 있습니다.

하지만 일단 기본적으로 모두 재료가 완료가 되었으니

이제 본격적으로 web3작업을 진행하고자 합니다!!

추가적으로 발견한 오류 또는 진행 사항을 작성해볼 것 입니다.

감사합니다!

좋은 웹페이지 즐겨찾기