체인의정석

Chainlink functions 사용해보기 본문

블록체인/NFT & BRIDGE

Chainlink functions 사용해보기

체인의정석 2023. 10. 13. 19:01
728x90
반응형

들어가며 

chainlink functions의 경우 블록체인에서의 web2 연산을 하도록 연결해주는 서비스이다.

이번 포스팅에서는 chainlink functions의 기초예제를 하는 방법에 대해 자세히 다루도록 하겠다.

 

기본 구조와 UseCase

정리 출처 : "해당 부분은" CP labs Eric Lee 님이 보내주신 설명글 입니다.

구조

스마트컨트랙트가 DON(Decentralized Oracle Network)에게 코드를 보내면, 각 노드들이 서버리스 환경에서 코드를 실행하고 각각의 값을 모은 다음에 최종 결과를 컨트랙트로 전달하는 구조

Use-case

- Connect to any public data -> 실시간 스포츠 결과 or 날씨 통계량 받아올 수 있음

- Connect to public data and transform it before consumption -> 트위터 API로부터 데이터를 읽어온 다음에 감성 분석 하기등

- Connect to a password-protected data source -> 스마트워치, ERP등등과도 연결할 수 있다

- Connect to an external decentralized database -> IPFS같은데에 연결하고 오프체인 연산을 진행하고 올리는 등

- Connect to your web2 application and build complex hybrid smart contract

- Fetch data from any Web2 -> AWS S3, Firebase ,GCS

Environment

- Chainlink functions toolkit npm package를 다운받아서 import해서 사용하는 형태

- 컨트랙트 짜려면 functionsClient.sol 이랑 funciotnsRequest.sol을 상속받아야한다

출처: https://docs.chain.link/chainlink-functions/resources/architecture

 

1. getting started - 여기서 기본세팅을 해야 한다.

https://docs.chain.link/chainlink-functions/getting-started

 

Blockchain Oracles for Connected Smart Contracts | Chainlink Documentation

Chainlink is the most widely used oracle network for powering universally connected smart contracts, enabling any blockchain to access real-world data & APIs.

docs.chain.link

*  먼저 노드 버전은 18로 맞추어주어야 한다.

nvm 설치 후 노드버전 18로 바꾸는법

https://it-timehacker.tistory.com/465

 

zsh: command not found: nvm 오류해결, NVM 설치방법

zsh: command not found: nvm 오류는 nvm (Node Version Manager)이 시스템에 설치되어 있지 않거나 zsh 쉘 설정에서 제대로 로드되지 않았음을 나타냅니다. 설치: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/i

it-timehacker.tistory.com

* 모듈 설치 - 해당 실습에서는 다음 모듈을 주력으로 사용한다.

해당 툴 킷을 사용한 함수들이 체인링크에서 사용되는 모듈들이기 때문에 필수적으로 받아줘야 한다.

npm i @chainlink/functions-toolkit

그리고 해당 모듈에서 Deno를 사용하기 때문에 Deno를 설치해 주어야 한다고 한다.

brew install deno

 

2. Functions에서 subscription 등록하기

https://functions.chain.link/mumbai/new

 

Functions | Chainlink

Supercharge your smart contract development with Chainlink Functions. Connect your smart contracts with any Web2 API and perform custom computations in minutes.

functions.chain.link

구독이 되면 이런식으로 LINK를 예치하고 consume이 가능한 계정으로 등록이 된다.

3. API 호출 예제 살펴보기

https://docs.chain.link/chainlink-functions/tutorials/api-query-parameters

 

Blockchain Oracles for Connected Smart Contracts | Chainlink Documentation

Chainlink is the most widely used oracle network for powering universally connected smart contracts, enabling any blockchain to access real-world data & APIs.

docs.chain.link

먼저 예제의 Consumer 컨트렉트 코드를 배포한다. 이때 컨트렉트의 생성자에서 router 주소를지정해 주어야하는데 mubai의 주소는 다음과 같다.

0x6E2dc0F9DB014aE19888F539E59285D2Ea04244C

해당 예제는 여기있는 코드를 사용한다고 한다.

여기서 Consumer는 컨트렉트인데 나중에 UI에 consumer 주소를 넣으라고 할 때도 배포된 컨트렉트 주소를 넣어야 한다.

공식 문서를 안 읽고 하다가 여기서 해메게 되었는데 consumer가 컨트랙트이기 때문에 api를 실행하고 여기에 따라서 어떤 작업을 한다고 하면 consumer 컨트렉트 뒤에 무언가를 추가하면 될 것 같다.

 

 

https://github.com/smartcontractkit/smart-contract-examples/tree/main/functions-examples/examples/2-call-api

해당 깃허브를 먼저 클론한 후에

git clone https://github.com/smartcontractkit/smart-contract-examples.git && \
cd ./smart-contract-examples/functions-examples/

해당 경로에서 npm instll을 해주고

.env.enc에

POLYGON_MUMBAI_RPC_URL=
PRIVATE_KEY=

받아온 코드에서 2가지 환경변수를 정의해준다.

이후 

npx env-enc set-pw

해당 환경변수에 대한 비밀번호 입력후

npx env-enc set

해당 변수에

- PRIVATE_KEY
- POLYGON_MUMBAI_RPC_URL

이렇게 2개를 넣은 후에 각각 value 값을 넣어서 설정한다. 이러면 암호화 된 환경변수가 올라가서 더 안전하다고 한다.

const consumerAddress = "0x5418ed830A6756031F6CF96fA302D5a95D1dBbcb"; // REPLACE this with your Functions consumer address
const subscriptionId = 3; // REPLACE this with your subscription ID

해당 부분을 위에 2번째 장에서 설정한 아이디와 지갑주소로 변경한다.

그리고 실행을 하면

node examples/2-call-api/request.js

이런식으로 api를 호출해서 결과 값을 확인할 수 있다.

이를 응용하여 여러가지 작업을 하기 시작하면 된다.

실행을 해보니 잘 작동하는 것을 볼 수 있었다.

5. 코드 분석하기

일단 모든 chainlink functions의 경우

다음과 같이

1. source : 해당 소스의 경우 체인링크 functions에서 호출하는 연산값
2. request : source를 넣어서 실제로 보내는 값

로 분리되어 있다.

결국 request는 사용자가 맘대로 쓰면 되지만 source에 대한 부분은 해당 코드 전체가 파라미터로 들어가게되면 해당 파라미터를 받아서 체인링크의 분산 노드들이 돌려주는 식으로 돌아가는 것 같다.

정말 이렇게 source에 해당하는 부분은 Input데이터로 들어가게 된다.
https://mumbai.polygonscan.com/tx/0x99b70944262ff2f0f8c4c98eaa9927c3519267522fd82d5442a5cfcc6cf913d4

 

Polygon Transaction Hash (Txhash) Details | PolygonScan

Polygon (MATIC) detailed transaction info for txhash 0x99b70944262ff2f0f8c4c98eaa9927c3519267522fd82d5442a5cfcc6cf913d4. The transaction status, block confirmation, gas fee, MATIC, and token transfer are shown.

mumbai.polygonscan.com

딱 봐도 많은 연산은 지원이 안 될 것처럼 생겼다.

그러나 간단한 연산은 가능하며 무엇보다 모듈을 쓰게 되면 API 호출이 가능하다. 그래서 API 호출을 써서 이런저런 복잡한 연산을 처리하고 source.js 자체적으로 하는 연산은 최소화 시키는 구조인 것 같다.

먼저 api 호출에 대해실행하는 부분은 다음 문서에 명시되어 있다.

https://docs.chain.link/chainlink-functions/api-reference/javascript-source#syntax

 

Blockchain Oracles for Connected Smart Contracts | Chainlink Documentation

Chainlink is the most widely used oracle network for powering universally connected smart contracts, enabling any blockchain to access real-world data & APIs.

docs.chain.link

const cryptoCompareRequest = Functions.makeHttpRequest({
  url: url,
  headers: {
    "Content-Type": "application/json",
  },
  params: {
    fsyms: fromSymbol,
    tsyms: toSymbol,
  },
});

// Execut

이런식으로 Functions.makeHttpRequest를 사용하면 source 안에서 API 호출을 할 수 있다.

source.js 예시 (2-call-api)

// This example shows how to make a call to an open API (no authentication required)
// to retrieve asset price from a symbol(e.g., ETH) to another symbol (e.g., USD)

// CryptoCompare API https://min-api.cryptocompare.com/documentation?key=Price&cat=multipleSymbolsFullPriceEndpoint

// Refer to https://github.com/smartcontractkit/functions-hardhat-starter-kit#javascript-code

// Arguments can be provided when a request is initated on-chain and used in the request source code as shown below
const fromSymbol = args[0];
const toSymbol = args[1];

// make HTTP request
const url = `https://min-api.cryptocompare.com/data/pricemultifull`;
console.log(`HTTP GET Request to ${url}?fsyms=${fromSymbol}&tsyms=${toSymbol}`);

// construct the HTTP Request object. See: https://github.com/smartcontractkit/functions-hardhat-starter-kit#javascript-code
// params used for URL query parameters
// Example of query: https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD
const cryptoCompareRequest = Functions.makeHttpRequest({
  url: url,
  headers: {
    "Content-Type": "application/json",
  },
  params: {
    fsyms: fromSymbol,
    tsyms: toSymbol,
  },
});

// Execute the API request (Promise)
const cryptoCompareResponse = await cryptoCompareRequest;
if (cryptoCompareResponse.error) {
  console.error(cryptoCompareResponse.error);
  throw Error("Request failed");
}

const data = cryptoCompareResponse["data"];
if (data.Response === "Error") {
  console.error(data.Message);
  throw Error(`Functional error. Read message: ${data.Message}`);
}

// extract the price
const price = data["RAW"][fromSymbol][toSymbol]["PRICE"];
console.log(`${fromSymbol} price is: ${price.toFixed(2)} ${toSymbol}`);

// Solidity doesn't support decimals so multiply by 100 and round to the nearest integer
// Use Functions.encodeUint256 to encode an unsigned integer to a Buffer
return Functions.encodeUint256(Math.round(price * 100));

보면 요청을 보내고 받은 응답값에서 예외 처리 등을 해주고 원하는 값을 뽑아낸다.
뽑아낸 값에서 연산 까지 마치고 난 후에 return Functions. 를 써서 응답값을 주면 컨트렉트 응답 값이 해당 코드로 나오게 된다.

해당 source.js 파일은 request.js 에서 사용하게 되는데 다음과 같이 사용된다.

  // Initialize functions settings
  const source = fs
    .readFileSync(path.resolve(__dirname, "source.js"))
    .toString();

  const args = ["ETH", "USD"];
  const gasLimit = 300000;

이렇게 파일을 읽어와서 request.js에서 

  const routerAddress = "0x6E2dc0F9DB014aE19888F539E59285D2Ea04244C";
  const linkTokenAddress = "0x326C977E6efc84E512bB9C30f76E30c160eD06FB";
  const donId = "fun-polygon-mumbai-1";
  const explorerUrl = "https://mumbai.polygonscan.com"

라우터 주소와 ID 등을 넣고 나서 툴킷에서 가져온

const {
  SubscriptionManager,
  simulateScript,
  ResponseListener,
  ReturnType,
  decodeResult,
  FulfillmentCode,
} = require("@chainlink/functions-toolkit");

해당 모듈들을 써서 실행을 시킨다.

callStatic을 써서 상태변화 없이 요청을 보내면 requestID가 나오는데 해당 아이디를 통해 진행 상황을 체크하고

그냥 sendRequest를 사용하면 해당 함수가 실행되게 된다.

  // To simulate the call and get the requestId.
  const requestId = await functionsConsumer.callStatic.sendRequest(
    source, // source
    "0x", // user hosted secrets - encryptedSecretsUrls - empty in this example
    0, // don hosted secrets - slot ID - empty in this example
    0, // don hosted secrets - version - empty in this example
    args,
    [], // bytesArgs - arguments can be encoded off-chain to bytes.
    subscriptionId,
    gasLimit,
    ethers.utils.formatBytes32String(donId) // jobId is bytes32 representation of donId
  );

  // Actual transaction call
  const transaction = await functionsConsumer.sendRequest(
    source, // source
    "0x", // user hosted secrets - encryptedSecretsUrls - empty in this example
    0, // don hosted secrets - slot ID - empty in this example
    0, // don hosted secrets - version - empty in this example
    args,
    [], // bytesArgs - arguments can be encoded off-chain to bytes.
    subscriptionId,
    gasLimit,
    ethers.utils.formatBytes32String(donId) // jobId is bytes32 representation of donId
  );

결국 컨트렉트에서 실행되는 함수는

    function sendRequest(
        string memory source,
        bytes memory encryptedSecretsUrls,
        uint8 donHostedSecretsSlotID,
        uint64 donHostedSecretsVersion,
        string[] memory args,
        bytes[] memory bytesArgs,
        uint64 subscriptionId,
        uint32 gasLimit,
        bytes32 jobId
    ) external onlyOwner returns (bytes32 requestId) {
        FunctionsRequest.Request memory req;
        req.initializeRequestForInlineJavaScript(source);
        if (encryptedSecretsUrls.length > 0)
            req.addSecretsReference(encryptedSecretsUrls);
        else if (donHostedSecretsVersion > 0) {
            req.addDONHostedSecrets(
                donHostedSecretsSlotID,
                donHostedSecretsVersion
            );
        }
        if (args.length > 0) req.setArgs(args);
        if (bytesArgs.length > 0) req.setBytesArgs(bytesArgs);
        s_lastRequestId = _sendRequest(
            req.encodeCBOR(),
            subscriptionId,
            gasLimit,
            jobId
        );
        return s_lastRequestId;
    }

 

해당 함수이고 여기에 대한 리턴 값의 경우 디코딩 하면 원하는 형태의 값이 나오게 된다.

디코딩의 경우 역시 툴킷 모듈을 써서 하면 값이 출력된다. 인코딩 시 원하는 데이터 타입으로 바꾸고 싶다면 3번 예제를 통해서 souce.js 부분에 대한 결과 값을 커스터마이징 하는 부분을 보면 된다.

          const decodedResponse = decodeResult(
            response.responseBytesHexstring,
            ReturnType.string
          );

그리고 만약 오류가 났다? 그럼 consumerAddress가 배포한 컨트렉트 주소가 맞는지, 그리고 functins subscribe manger에서 해당 주소가 등록이 되어있는지 예치된 링크는 충분히 남아있는지 보면 된다.

결과가 나오면 다음과 같이 나오게 된다.

i-MacBookPro functions-examples % node examples/3-custom-response/request.js 
secp256k1 unavailable, reverting to browser version
Start simulation...
Simulation result {
  capturedTerminalOutput: 'HTTP GET Request to https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD\n' +
    'ETH price is: 1545.50 USD. 24h Volume is 157736.55 USD. Market: CCCAGG\n',
  responseBytesHexstring: '0x7b227072696365223a22313534352e3530222c22766f6c756d65223a223135373733362e3535222c226c6173744d61726b6574223a22434343414747227d'
}
✅ Decoded response to string:  {"price":"1545.50","volume":"157736.55","lastMarket":"CCCAGG"}

Estimate request costs...
Fulfillment cost estimated to 0.20047387430649 LINK

Make request...

✅ Functions request sent! Transaction hash 0xcdeb669510bd66aafa95df305a040afc265041997a093c29be9456527740efcc -  Request id is 0xdb6179ff66ed9d33caef612cb45bd20f0df63eeb030a315515e4cbba97981de9. Waiting for a response...
See your request in the explorer https://mumbai.polygonscan.com/tx/0xcdeb669510bd66aafa95df305a040afc265041997a093c29be9456527740efcc

✅ Request 0xdb6179ff66ed9d33caef612cb45bd20f0df63eeb030a315515e4cbba97981de9 successfully fulfilled. Cost is 0.200018166211104544 LINK.Complete reponse:  {
  requestId: '0xdb6179ff66ed9d33caef612cb45bd20f0df63eeb030a315515e4cbba97981de9',
  subscriptionId: 417,
  totalCostInJuels: 200018166211104544n,
  responseBytesHexstring: '0x7b227072696365223a22313534342e3437222c22766f6c756d65223a223135383136372e3530222c226c6173744d61726b6574223a22434343414747227d',
  errorString: '',
  returnDataBytesHexstring: '0x',
  fulfillmentCode: 0
}

✅ Decoded response to string:  {"price":"1544.47","volume":"158167.50","lastMarket":"CCCAGG"}
728x90
반응형
Comments