체인의정석

ethers & websocket & http로 이벤트 구독 및 크로스체인 트랜잭션 실행, 트랜잭션 정보값 뽑아내기 본문

블록체인/Ethers & web3

ethers & websocket & http로 이벤트 구독 및 크로스체인 트랜잭션 실행, 트랜잭션 정보값 뽑아내기

체인의정석 2024. 7. 18. 10:18
728x90
반응형

크로스체인 트랜잭션 실행 로직을 만들 때 만든 코드 중 다시 사용할 만한 코드만 뽑아서 기록해둔다. 해당 로직은 Ethers v6 버전으로 진행하였다.

A. Ethers & WebSocket 사용하여 이벤트 구독하기


A-1. ping, pong 을 통해 커넥션 살리기

이벤트 구독이 일시적으로 되더라도 계속해서 ws 구독을 유지시키려면 다음과 같이 체크 로직을 만들어서 돌려주어야 한다.

const { ethers } = require("ethers");

const ResilientWebsocket = async (url) => {
    const EXPECTED_PONG_BACK = 15000;
    const KEEP_ALIVE_CHECK_INTERVAL = 30 * 1000; //7500;

    const debug = (message) => {
    console.debug(new Date().toISOString(), message);
    };

    let terminate = false;
    let pingTimeout = null;
    let keepAliveInterval = null;
    let wsp;
  
    const startConnection = (url) => {
        wsp = new ethers.WebSocketProvider(url);

          
          setInterval(() => {
            debug("Checking if the connection is alive, sending a ping");
    
            wsp.websocket.ping();
    
            pingTimeout = setTimeout(() => {
              console.error("pingTimeout");
              if (wsp) wsp.websocket.terminate();
              handleWebSocketConnectionDead();
            }, EXPECTED_PONG_BACK);
          }, KEEP_ALIVE_CHECK_INTERVAL);
    };

    function handleWebSocketConnectionDead() {
      terminate = true;
      if (keepAliveInterval) clearInterval(keepAliveInterval);
      if (pingTimeout) clearTimeout(pingTimeout);
      throw new Error("WebSocket connection closed with code ", e);
    };

    
    startConnection(url);
    
    wsp.websocket.on('pong', () => {
      debug("Received pong, so connection is alive, clearing the timeout");
      if (pingTimeout) clearTimeout(pingTimeout);
    });
    console.log("ws checking has started!")
    return wsp;

  };

  module.exports = {
    ResilientWebsocket
  }

B.  이벤트 구독하기

먼저 ABI, 주소, provider를 받아와서 다음과 같이 이벤트를 구독할 수 있다. 이벤트가 발생되면 handleSendMessageA 함수가 실행된다.

    provider = new ethers.JsonRpcProvider(ENDPOINT);
    ContractEX = new ethers.Contract(Address, ABI, provider);

    ContractEX.on("Eventname", async (...args) => {
      console.log(`Listening for Eventname events from A chain`);
      await handleSendMessageA(...args);
    });

handleSendMessageA 함수는 다음과 같다. 만약 해당 이벤트를 통해서 다른 트랜잭션을 발생시키고 싶다면 아래와 같이 실해앟면 된다. 아래 예시는 A체인에 이벤트가 발생할 시 B체인에 이벤트를 발생시키는 예시이다. websocket으로는 트랜잭션을 쏠 수 없기에 트랜잭션을 쏘기 위하여 privateKey정보가 담긴 wallet 객체를 따로 만들어서 해당 부분을 구현하였다.

 const handleSendMessageA = async (caller, uri, params, deadline, event) => {
    try {
      providerA = new ethers.JsonRpcProvider(providerUrl_A);
      walletA = new ethers.Wallet(privateKeyA, providerA);
      ContractA = new ethers.Contract(ContractAAddress, ContractAABI, walletA);
      providerB = new ethers.JsonRpcProvider(providerUrl_B);
      walletB = new ethers.Wallet(privateKeyB, providerB);
      erc721LaneB = new ethers.Contract(ContractBAddress, ContractBABI, walletB);

     const BTx = await BTx.exTX(input1, input2);
     await BTx.wait();

    } catch (error) {
      console.log("handleSendMessageA error >>", error);
    }
  };

C.  이벤트 구독하면서 TXhash 및 blockTimstamp, data 뽑아내기


만약 여기서 트랜잭션 해시를 뽑아내고 데이터도 뽑아내고 싶다면 아래와 같이 진행하면 된다.
위의 응답값에서 event.log.transactionHash를 가져오면 txHash를 도출할 수 있다.

      const txHash = event.log.transactionHash;

해당 txhash로 부터 블록번호, 가스사용량, blockTimstamp는 다음 함수를 통해서 뽑아낼 수 있다.

const getTxDataFromHash = async (provider, txhash) => {
  const txReceipt = await provider.getTransactionReceipt(txhash);
  const res = {};

  res.blockNumber = Number(txReceipt.blockNumber);
  res.gasUsed = Number(txReceipt.gasUsed);

  const blockTimestamp = await provider.getBlock(Number(txReceipt.blockNumber));
  res.blockTimestamp = blockTimestamp.timestamp;

  return res;
}

module.exports = {
    getTxDataFromHash
}

* ethers v6의 경우 provider는 다음과 같이 넣어주면 된다.

      provider = new ethers.JsonRpcProvider(ENDPOINT);

위의 timestamp 가져와서 DB에 시간 기록할 때는 아래 함수를 사용할 것.

function formatDateToMySQLDatetime(timestamp) {
    const date = new Date(timestamp);
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const day = String(date.getDate()).padStart(2, '0');
    const hours = String(date.getHours()).padStart(2, '0');
    const minutes = String(date.getMinutes()).padStart(2, '0');
    const seconds = String(date.getSeconds()).padStart(2, '0');
    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  }

module.exports = {formatDateToMySQLDatetime};

D. endpoint가 불안정할 경우


만약 endpoint를 무료 버전으로 사용할 경우 이벤트 구독이 잘 작동하다가도 끊길 수가 있다.
이경우 유로 endpoint를 구독하거나 직접 노드를 구축해야하는데 임시적으로 여러 enpoint를 받아와서 재시작하는 로직을 넣어줄 수 있다. 이때 이벤트 구독을 해주는 websocket을 제대로 제거하지 않으면 중복적으로 이벤트를 구독할 수 있기 때문에 정확히 객체를 지정해서 삭제해주고 주기적으로 종료시켜주면 docker-compose가 다시 띄워주는 구조로 만들어서 해결이 가능하다.

  const shutdown = () => {
    console.log('Shutting down the process after 5 minutes');
    delete 이벤트구독 함수(변수);
    delete ResilientWebsocket;
    process.exit(0); //일반적인 경로로 종료되었음을 표시
  };
  
  setTimeout(shutdown, 300 * 1000); // Schedule shutdown after 5 minutes (300,000 milliseconds)

가장 좋은것은 잘 작동하는 enpoint를 여러개 두고 문제가 생기면 갈아서 다시 삭제하고 시작하는 로직이지만 무료 enpoint는 내부적으로 에러 로그만 찍고 끝내버리는 경우가 많아서 위의 방법이 임시적으로는 유효하다.

728x90
반응형
Comments