본문으로 건너뛰기
이 페이지는 영문에서 기계 번역되었으므로 오역이나 어색한 표현이 있을 수 있습니다. 따라서 정확한 정보는 영어 원문을 참조하시기 바랍니다. 또한 잦은 업데이트로 인해 일부 콘텐츠는 영문이 그대로 남아있을 수 있습니다. Crowdin에서 이 페이지의 번역을 개선하는 데 동참하여 도움을 주세요. (Crowdin translation page, Contributing guide)

체인링크 CCIP로 카이아에서 크로스체인 NFT 구축하기: 실무 가이드

소개​

대체 불가능한 토큰은 블록체인 기술의 가장 잘 알려진 사용 사례 중 하나가 되었으며, 고유하고 검증 가능한 디지털 자산을 생성할 수 있게 해줍니다. 그러나 기존의 NFT 구현은 단일 블록체인에 묶여 있습니다. 이러한 제한은 유연성을 떨어뜨리고 커뮤니티, 유동성, 유틸리티가 다를 수 있는 생태계에서 자산을 자유롭게 이동하지 못하게 합니다.

크로스체인 NFT는 NFT가 고유성과 출처를 유지하면서 블록체인 간에 원활하게 이동할 수 있게 함으로써 이러한 문제를 해결합니다. 개발자는 체인링크의 체인 간 상호운용성 프로토콜(CCIP)을 통해 표준화된 보안 메시징 프레임워크를 사용하여 체인 간에 신뢰할 수 있는 연결고리를 구축할 수 있습니다.

이 가이드에서는 번 앤 민트 모델을 사용해 크로스체인 NFT를 빌드하고 배포하는 방법을 설명합니다. NFT는 소스 체인에서 소각되고 동일한 토큰아이디와 메타데이터로 대상 체인에서 다시 발행되어, 주어진 시간에 유효한 사본이 하나만 존재하도록 보장합니다.

전제 조건​

시작하기 전에 다음 설정이 완료되었는지 확인하세요:

  • Node.js 및 npm
  • 안전모
    • 설치합니다: npm 설치 --save-dev 하드햇
    • 프로젝트 초기화: npx hardhat --init
  • 메타마스크 지갑
    • 개발 지갑을 만들거나 설정합니다.
    • 카이아 카이로스 테스트넷과 이더리움 세폴리아 네트워크를 모두 메타마스크에 추가하세요.
  • 수도꼭지에서 토큰 테스트
    • 카이아: 컨트랙트를 배포하거나 트랜잭션을 전송할 때 카이아에서 가스 수수료를 지불합니다.
    • LINK (테스트넷): LINK로 결제할 때 CCIP 수수료를 부담합니다.
    • 세폴리아 이더: 세폴리아에서 가스 요금을 지불하며, 선택 시 기본 이더로 CCIP 수수료를 충당할 수도 있습니다.
  • 파일베이스 계정
    • NFT 메타데이터(IPFS 스토리지)를 업로드하고 검색하는 데 필요합니다.

크로스 체인 NFT는 어떻게 작동하나요?​

대체 불가능한 토큰은 단일 블록체인에 기록된 고유한 디지털 토큰입니다. 발행, 전송, 소유권 등 핵심 동작은 해당 체인에 연결된 스마트 컨트랙트에 의해 정의됩니다. 이 때문에 NFT는 추가 메커니즘 없이는 자연스럽게 블록체인을 이동할 수 없습니다. 상호 운용성을 위해 개발자는 여러 체인에 컴패니언 컨트랙트를 배포하고 크로스체인 메시징을 통해 이를 연결합니다. 그 결과 크로스체인 NFT는 여러 블록체인에 존재하는 동등한 토큰이지만 주어진 시간에 하나의 사본만 활성화되는 토큰입니다.

크로스 체인 NFT는 일반적으로 다음 세 가지 방법 중 하나로 구현됩니다:

  • 소각 및 민트: 소스 체인에서 NFT를 소각한 다음, 대상 체인에서 이에 상응하는 NFT를 발행합니다.

  • 잠금 및 민트: NFT가 소스 체인에 잠기고 대상 체인에 복제본이 발행됩니다. 반환하려면 사본을 소각하여 원본의 잠금을 해제해야 합니다.

  • 잠금 및 잠금 해제: 동일한 컬렉션이 여러 체인에 배포됩니다. 소유자는 한 체인에서 NFT를 잠그면 다른 체인에서 해당 NFT의 잠금을 해제하여 한 번에 하나의 사본만 사용할 수 있도록 합니다.

이 가이드에서는 크로스체인 NFT에 번 앤 민트 모델을 사용하겠습니다. NFT는 한 체인에서 제거되고 다른 체인에서 다시 생성되며, 전체 프로세스는 체인링크 CCIP에 의해 구동됩니다.

시작하기​

이 가이드에서는 체인링크 CCIP를 사용하여 카이아 카이로스 테스트넷과 이더리움 세폴리아 간에 크로스체인 NFT를 발행하고 전송하는 방법을 설명합니다.

결국에는 할 수 있게 될 것입니다:

  • 카이로스 테스트넷과 이더리움 세폴리아 모두에 대해 구성된 하드햇 프로젝트를 초기화합니다.
  • 종속성으로 체인링크 CCIP 컨트랙트 및 인터페이스 추가하기
  • 크로스 체인 전송을 위한 번 앤 민트 메커니즘으로 크로스 체인 NFT 컨트랙트를 구현합니다.
  • 두 네트워크에 컨트랙트를 배포하고 체인 간에 NFT를 전송합니다.

하드햇 프로젝트 만들기​

이 튜토리얼에서는 하드햇 3을 사용하여 컨트랙트를 배포하고 상호 작용합니다. Hardhat 3는 암호화된 키 저장소에 대한 기본 지원, Solidity에서 테스트 작성 기능, 향상된 프로젝트 툴링과 같은 새로운 기능을 제공합니다.

아래 단계에 따라 프로젝트를 설정하세요:

  1. Node.js 및 npm 설치 확인

    다음 명령을 실행하여 Node.js 및 npm이 설치되었는지 확인합니다:


node -v
npm -v

  1. 새 프로젝트 디렉터리 초기화

    새 폴더를 만들고, 그 폴더로 이동하여 Node.js 프로젝트를 초기화합니다:


mkdir ccip-nft-kaia-hardhat-example
cd ccip-nft-kaia-hardhat-example
npm init -y

  1. 하드햇 프로젝트 만들기

    실행:


npx hardhat --init

메시지가 표시되면 Node.js 테스트 러너와 에테르가 포함된 샘플 프로젝트를 선택합니다. 현재 디렉터리에서 초기화하고 필요한 모든 종속성을 설치합니다.

필수 계약 설치​

체인링크 CCIP 컨트랙트를 설치합니다:


npm i @chainlink/contracts-ccip --save-dev

표준 체인링크 컨트랙트를 설치합니다:


npm i @chainlink/contracts --save-dev

오픈제플린 컨트랙트를 설치합니다(ERC-721 및 기타 기본 구현 제공):


npm i @openzeppelin/contracts --save-dev

NFT 메타데이터 구성​

컨트랙트를 작성하기 전에 발행할 NFT의 사양을 정의해 보겠습니다. 각 NFT에는 이름, 설명, 이미지를 설명하는 메타데이터가 필요하며, 이는 JSON 파일에 저장되고 IPFS에서 호스팅됩니다.

이 가이드에서는 이미지와 메타데이터를 모두 저장하기 위해 Filebase를 사용하겠습니다. 나만의 NFT를 만들려면 Filebase를 통해 이미지와 메타데이터 JSON 파일을 IPFS에 업로드하세요. 업로드 후 파일 탭에서 파일 이름을 클릭하고 IPFS URL을 복사합니다. 이와 비슷하게 보일 것입니다:


https://disastrous-turquoise-parakeet.myfilebase.com/ipfs/QmY1LZF8JHo2r3h4X5VzLLXtJujqnBFGTyo2aqR9joXnt8

다음은 사용할 수 있는 샘플 메타데이터 파일입니다:


{
"name": "Kairos NFT",
"description": "gkaia frens! gazuaaaaa!!!",
"image": "https://disastrous-turquoise-parakeet.myfilebase.com/ipfs/QmRvQc4wZCp6NF7dFL4ywiWTG7FSH3KKGUAkXGgsdYfcKi"
}

스마트 컨트랙트 작성​

이 섹션에서는 체인링크 CCIP로 구동되는 번 앤 민트 모델을 사용하여 블록체인 간 NFT 전송을 가능하게 하는 콘트랙트를 구현합니다.

프로젝트의 컨트랙트 디렉토리에 'CrosschainNFT.sol'이라는 새 파일을 생성하고 다음 코드를 붙여넣습니다:


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {ERC721Burnable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol";
import {IAny2EVMMessageReceiver} from "@chainlink/contracts-ccip/contracts/interfaces/IAny2EVMMessageReceiver.sol";
import {OwnerIsCreator} from "@chainlink/contracts/src/v0.8/shared/access/OwnerIsCreator.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol";
/**
* THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
* DO NOT USE THIS CODE IN PRODUCTION.
*/
// Source chain is Ethereum Sepolia
// Destination chain is Kairos Testnet
contract CrosschainNFT is ERC721, ERC721URIStorage, ERC721Burnable, IAny2EVMMessageReceiver, ReentrancyGuard, OwnerIsCreator {
using SafeERC20 for IERC20;
enum PayFeesIn {
Native,
LINK
}
error InvalidRouter(address router);
error OnlyOnEthereumSepolia();
error NotEnoughBalanceForFees(uint256 currentBalance, uint256 calculatedFees);
error NothingToWithdraw();
error FailedToWithdrawEth(address owner, address target, uint256 value);
error ChainNotEnabled(uint64 chainSelector);
error SenderNotEnabled(address sender);
error OperationNotAllowedOnCurrentChain(uint64 chainSelector);
struct crosschainNFTDetails {
address crosschainNFTAddress;
bytes ccipExtraArgsBytes;
}
uint256 constant ETHEREUM_SEPOLIA_CHAIN_ID = 11155111;
string tokenNFTURI = "https://disastrous-turquoise-parakeet.myfilebase.com/ipfs/QmY1LZF8JHo2r3h4X5VzLLXtJujqnBFGTyo2aqR9joXnt8";
IRouterClient internal immutable i_ccipRouter;
LinkTokenInterface internal immutable i_linkToken;
uint64 private immutable i_currentChainSelector;
uint256 private _nextTokenId;
mapping(uint64 destChainSelector => crosschainNFTDetails crosschainNFTPerChain) public s_chains;
event ChainEnabled(uint64 chainSelector, address xNftAddress, bytes ccipExtraArgs);
event ChainDisabled(uint64 chainSelector);
event CrossChainSent(
address from, address to, uint256 tokenId, uint64 sourceChainSelector, uint64 destinationChainSelector
);
event CrossChainReceived(
address from, address to, uint256 tokenId, uint64 sourceChainSelector, uint64 destinationChainSelector
);
modifier onlyRouter() {
if (msg.sender != address(i_ccipRouter)) {
revert InvalidRouter(msg.sender);
}
_;
}
modifier onlyOnEthereumSepolia() {
if (block.chainid != ETHEREUM_SEPOLIA_CHAIN_ID) {
revert OnlyOnEthereumSepolia();
}
_;
}
modifier onlyEnabledChain(uint64 _chainSelector) {
if (s_chains[_chainSelector].crosschainNFTAddress == address(0)) {
revert ChainNotEnabled(_chainSelector);
}
_;
}
modifier onlyEnabledSender(uint64 _chainSelector, address _sender) {
if (s_chains[_chainSelector].crosschainNFTAddress != _sender) {
revert SenderNotEnabled(_sender);
}
_;
}
modifier onlyOtherChains(uint64 _chainSelector) {
if (_chainSelector == i_currentChainSelector) {
revert OperationNotAllowedOnCurrentChain(_chainSelector);
}
_;
}
constructor(address ccipRouterAddress, address linkTokenAddress, uint64 currentChainSelector)
ERC721("Cross Chain NFT", "XNFT")
{
if (ccipRouterAddress == address(0)) revert InvalidRouter(address(0));
i_ccipRouter = IRouterClient(ccipRouterAddress);
i_linkToken = LinkTokenInterface(linkTokenAddress);
i_currentChainSelector = currentChainSelector;
}
function mint() external onlyOnEthereumSepolia {
uint256 tokenId = _nextTokenId++;
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, tokenNFTURI);
}
function enableChain(uint64 chainSelector, address crosschainNFTAddress, bytes memory ccipExtraArgs)
external
onlyOwner
onlyOtherChains(chainSelector)
{
s_chains[chainSelector] = crosschainNFTDetails({crosschainNFTAddress: crosschainNFTAddress, ccipExtraArgsBytes: ccipExtraArgs});
emit ChainEnabled(chainSelector, crosschainNFTAddress, ccipExtraArgs);
}
function disableChain(uint64 chainSelector) external onlyOwner onlyOtherChains(chainSelector) {
delete s_chains[chainSelector];
emit ChainDisabled(chainSelector);
}
function crossChainTransferFrom(
address from,
address to,
uint256 tokenId,
uint64 destinationChainSelector,
PayFeesIn payFeesIn
) external nonReentrant onlyEnabledChain(destinationChainSelector) returns (bytes32 messageId) {
string memory tokenUri = tokenURI(tokenId);
_burn(tokenId);
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(s_chains[destinationChainSelector].crosschainNFTAddress),
data: abi.encode(from, to, tokenId, tokenUri),
tokenAmounts: new Client.EVMTokenAmount[](0),
extraArgs: s_chains[destinationChainSelector].ccipExtraArgsBytes,
feeToken: payFeesIn == PayFeesIn.LINK ? address(i_linkToken) : address(0)
});
// Get the fee required to send the CCIP message
uint256 fees = i_ccipRouter.getFee(destinationChainSelector, message);
if (payFeesIn == PayFeesIn.LINK) {
if (fees > i_linkToken.balanceOf(address(this))) {
revert NotEnoughBalanceForFees(i_linkToken.balanceOf(address(this)), fees);
}
// Approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK
i_linkToken.approve(address(i_ccipRouter), fees);
// Send the message through the router and store the returned message ID
messageId = i_ccipRouter.ccipSend(destinationChainSelector, message);
} else {
if (fees > address(this).balance) {
revert NotEnoughBalanceForFees(address(this).balance, fees);
}
// Send the message through the router and store the returned message ID
messageId = i_ccipRouter.ccipSend{value: fees}(destinationChainSelector, message);
}
emit CrossChainSent(from, to, tokenId, i_currentChainSelector, destinationChainSelector);
}
/// @inheritdoc IAny2EVMMessageReceiver
function ccipReceive(Client.Any2EVMMessage calldata message)
external
virtual
override
onlyRouter
nonReentrant
onlyEnabledChain(message.sourceChainSelector)
onlyEnabledSender(message.sourceChainSelector, abi.decode(message.sender, (address)))
{
uint64 sourceChainSelector = message.sourceChainSelector;
(address from, address to, uint256 tokenId, string memory tokenUri) =
abi.decode(message.data, (address, address, uint256, string));
_safeMint(to, tokenId);
_setTokenURI(tokenId, tokenUri);
emit CrossChainReceived(from, to, tokenId, sourceChainSelector, i_currentChainSelector);
}
function withdraw(address _beneficiary) public onlyOwner {
uint256 amount = address(this).balance;
if (amount == 0) revert NothingToWithdraw();
(bool sent,) = _beneficiary.call{value: amount}("");
if (!sent) revert FailedToWithdrawEth(msg.sender, _beneficiary, amount);
}
function withdrawToken(address _beneficiary, address _token) public onlyOwner {
uint256 amount = IERC20(_token).balanceOf(address(this));
if (amount == 0) revert NothingToWithdraw();
IERC20(_token).safeTransfer(_beneficiary, amount);
}
function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) {
return super.tokenURI(tokenId);
}
function getCCIPRouter() public view returns (address) {
return address(i_ccipRouter);
}
function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721URIStorage) returns (bool) {
return interfaceId == type(IAny2EVMMessageReceiver).interfaceId || super.supportsInterface(interfaceId);
}
}

코드 연습​

크로스체인NFT는 블록체인 간에 NFT를 전송하기 위해 체인링크 CCIP를 통합하는 ERC-721 콘트랙트입니다. 소스 체인에서 NFT를 소각하고 동일한 토큰아이디와 토큰URI를 사용하여 대상에서 다시 채굴합니다. 이 콘트랙트는 enableChain을 통해 승인된 대상 체인 레지스트리를 유지하고, 크로스체인 메시징을 위해 체인링크 라우터(IRouterClient)에 의존하며, 네이티브 가스 토큰 또는 LINK로 수수료 지불을 지원합니다.

주요 기능

  • enableChain

컨트랙트 소유자가 대상 블록체인을 등록할 수 있습니다. 대응하는 NFT 컨트랙트 주소와 CCIP 인수를 s_chains 매핑에 저장하여 해당 체인을 유효한 전송 대상으로 화이트리스트에 올립니다. 설정이 완료되면 체인 활성화 이벤트가 발생합니다.

  • 교차 체인 전송

체인 간 NFT 전송을 실행합니다. 먼저 대상 체인이 활성화되어 있는지 확인한 다음 NFT 메타데이터(tokenURI)를 검색하고 소스 체인에서 토큰을 소각합니다. 그런 다음 송금 세부 정보가 포함된 CCIP 메시지를 작성하고 필요한 수수료를 계산한 후 LINK 또는 네이티브 가스로 결제합니다. 라우터를 통해 메시지가 전송되면 전송을 기록하기 위해 CrossChainSent 이벤트가 발생합니다.

이제 '크로스체인NFT.sol'의 핵심 흐름이 명확해졌으니, 다음 단계로 넘어가 보겠습니다.

스마트 컨트랙트 컴파일하기​

스마트 컨트랙트를 컴파일하려면 실행하세요:


npx hardhat build

스마트 컨트랙트 배포​

이 섹션에서는 필요한 변수를 설정한 다음 이더리움 세폴리아(소스 체인)와 카이로스 테스트넷(대상 체인) 모두에 CrosschainNFT.sol 컨트랙트를 배포하겠습니다.

암호화된 키 저장소 사용​

Hardhat 3의 장점 중 하나는 일반 텍스트 파일 대신 암호화된 키 저장소에 개인 키 및 RPC URL과 같은 민감한 값을 저장할 수 있다는 점입니다. 이 가이드에서는 세폴리아 및 카이로스의 PRIVATE_KEY와 RPC URL을 암호화합니다.

개인 키 추가


npx hardhat keystore set PRIVATE_KEY

이 명령을 처음 실행하면 Hardhat에서 키 저장소에 대한 비밀번호를 만들라는 메시지가 표시됩니다. 값을 추가하거나 업데이트할 때마다 이 비밀번호가 필요합니다.

각 네트워크에 대한 RPC URL 추가


npx hardhat keystore set KAIROS_RPC_URL
npx hardhat keystore set SEPOLIA_RPC_URL

마지막으로 hardhat.config.ts 파일을 편집하여 이러한 암호화된 값을 로드하고 두 네트워크를 구성합니다.


import type { HardhatUserConfig } from "hardhat/config";
import hardhatToolboxMochaEthersPlugin from "@nomicfoundation/hardhat-toolbox-mocha-ethers";
import { configVariable } from "hardhat/config";
const config: HardhatUserConfig = {
plugins: [hardhatToolboxMochaEthersPlugin],
solidity: {
profiles: {
default: {
version: "0.8.28",
},
production: {
version: "0.8.28",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
},
},
networks: {
hardhatMainnet: {
type: "edr-simulated",
chainType: "l1",
},
hardhatOp: {
type: "edr-simulated",
chainType: "op",
},
kairosTestnet: {
type: "http",
chainType: "l1",
url: configVariable("KAIROS_RPC_URL"),
accounts: [configVariable("PRIVATE_KEY")],
},
ethereumSepolia: {
type: "http",
chainType: "l1",
url: configVariable("SEPOLIA_RPC_URL"),
accounts: [configVariable("PRIVATE_KEY")],
},
},
};
export default config;

다음 단계는 이더리움 세폴리아 및 카이로스 테스트넷에 각각 크로스체인NFT 스마트 컨트랙트를 배포하는 것입니다.

이더리움 세폴리아에 크로스체인NFT.sol 배포하기​

배포하기 전에 체인링크 CCIP 디렉토리에서 이더리움 세폴리아에 대한 다음 값을 가져옵니다:

  • 체인 선택기
  • CCIP 라우터 주소
  • 링크 토큰 주소

이러한 값은 배포 스크립트에 필요합니다. 다음으로, 프로젝트의 ignition/modules 폴더로 이동하여 'deployEthereumSepolia.ts'라는 새 파일을 생성하고 다음 코드를 붙여넣습니다:


// This setup uses Hardhat Ignition to manage smart contract deployments.
// Learn more about it at https://hardhat.org/ignition
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
const ETHEREUM_SEPOLIA_ROUTER_ADDRESS = `0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59`;
const ETHEREUM_SEPOLIA_LINK_TOKEN_ADDRESS = `0x779877A7B0D9E8603169DdbD7836e478b4624789`;
const ETHEREUM_SEPOLIA_CHAIN_SELECTOR = `16015286601757825753`;
const CrosschainNFTSepoliaModule = buildModule("CrosschainNFTSepoliaModule", (m) => {
const crosschainNFTSepolia = m.contract("CrosschainNFT", [ETHEREUM_SEPOLIA_ROUTER_ADDRESS, ETHEREUM_SEPOLIA_LINK_TOKEN_ADDRESS, ETHEREUM_SEPOLIA_CHAIN_SELECTOR], {
});
return { crosschainNFTSepolia };
});
export default CrosschainNFTSepoliaModule;

배포 스크립트를 실행합니다:


npx hardhat ignition deploy ignition/modules/deployEthereumSepolia.ts --network ethereumSepolia

카이로스 테스트넷에 크로스체인NFT.sol 배포하기​

배포하기 전에 체인링크 CCIP 디렉토리에서 카이로스 테스트넷의 다음 값을 가져옵니다:

  • 체인 선택기
  • CCIP 라우터 주소
  • 링크 토큰 주소

이러한 값은 배포 스크립트에 필요합니다. 그런 다음 프로젝트의 ignition/modules 폴더로 이동하여 '배포카이로스테스트넷.ts'라는 새 파일을 생성하고 다음 코드를 붙여넣습니다:


// This setup uses Hardhat Ignition to manage smart contract deployments.
// Learn more about it at https://hardhat.org/ignition
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
const KAIROS_TESTNET_ROUTER_ADDRESS = `0x41477416677843fCE577748D2e762B6638492755`;
const KAIROS_TESTNET_LINK_TOKEN_ADDRESS = `0xAF3243f975afe2269Da8Ffa835CA3A8F8B6A5A36`;
const KAIROS_TESTNET_CHAIN_SELECTOR = `2624132734533621656`;
const CrosschainNFTKairosModule = buildModule("CrosschainNFTKairosModule", (m) => {
const crosschainNFTKairos = m.contract("CrosschainNFT", [KAIROS_TESTNET_ROUTER_ADDRESS, KAIROS_TESTNET_LINK_TOKEN_ADDRESS, KAIROS_TESTNET_CHAIN_SELECTOR], {
});
return { crosschainNFTKairos };
});
export default CrosschainNFTKairosModule;

배포 스크립트를 실행합니다:


npx hardhat ignition deploy ignition/modules/deployKairosTestnet.ts --network kairosTestnet

스마트 컨트랙트와 상호작용하기​

이 섹션에서는 배포된 크로스체인NFT 스마트 컨트랙트와 상호작용하기 위해 각각 enableChain, mint, crosschainTransfer 함수를 실행해 보겠습니다.

1단계: 이더리움 세폴리아에서 enableChain을 호출합니다.​

enableChain을 호출하기 전에 다음 값을 준비합니다:

  • 세폴리아 컨트랙트 주소: 이더리움 세폴리아에 배포된 크로스체인NFT.sol 컨트랙트의 주소입니다.
  • 카이로스 컨트랙트 주소: 카이로스 테스트넷에 배포된 크로스체인NFT.sol 컨트랙트의 주소입니다.
  • 체인 선택기: 2624132734533621656 (카이로스 테스트넷의 CCIP 체인 선택기).
  • CCIP extraArgs: 0x97a657c9000000000000000000000000000000000000000000000000000000000007A120(가스 제한이 500,000으로 설정된 extraArgs의 기본 인코딩 값입니다).

그런 다음 스크립트 폴더에 새 타입스크립트 파일을 생성하고 이름을 'enableChainSepolia.ts'로 지정한 다음 다음 코드를 붙여넣습니다:


// scripts/enableChainSepolia.ts
import { network } from "hardhat";
async function main() {
const connection = await network.connect({
network: "ethereumSepolia"
});
const { ethers } = connection;
const [signer] = await ethers.getSigners();
console.log(`Using account: ${signer.address}`);
// Get the contract factory by name
const CrosschainNFT = await ethers.getContractFactory("CrosschainNFT", signer);
// Contract addresses and parameters
const crosschainNFTAddressEthereumSepolia = `0xb1fe42BBd7842703820C7480c22409b872319B22`;
const crosschainNFTAddressKairosTestnet = `0x8c464Bb9Bf364F68b898ed0708b8f5F66EF6Cfb1`;
const chainSelectorKairosTestnet = `2624132734533621656`;
const ccipExtraArgs = `0x97a657c9000000000000000000000000000000000000000000000000000000000007A120`;
// Attach to the deployed contract
const crosschainNFTSepolia = CrosschainNFT.attach(crosschainNFTAddressEthereumSepolia);
console.log(`Enabling chain for Kairos Testnet...`);
const tx = await crosschainNFTSepolia.enableChain(
chainSelectorKairosTestnet,
crosschainNFTAddressKairosTestnet,
ccipExtraArgs
);
console.log(`Transaction hash: ${tx.hash}`);
console.log(`Waiting for confirmation...`);
const receipt = await tx.wait();
console.log(`Transaction confirmed in block: ${receipt?.blockNumber}`);
console.log(`Chain enabled successfully!`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

다음 명령을 실행하여 함수를 호출합니다:


npx hardhat run scripts/enableChainSepolia.ts --network ethereumSepolia

2단계: 카이로스 테스트넷에서 enableChain을 호출합니다.​

enableChain을 호출하기 전에 다음 값을 준비합니다:

  • 카이로스 컨트랙트 주소: 카이로스 테스트넷에 배포된 크로스체인NFT.sol 컨트랙트의 주소입니다.
  • 세폴리아 컨트랙트 주소: 이더리움 세폴리아에 배포된 크로스체인NFT.sol 컨트랙트의 주소입니다.
  • 체인 선택기: 16015286601757825753 (카이로스 테스트넷의 CCIP 체인 선택기)
  • CCIP extraArgs: 0x97a657c9000000000000000000000000000000000000000000000000000000000007A120(가스 제한이 500,000으로 설정된 extraArgs의 기본 인코딩 값입니다.)

그런 다음 스크립트 폴더에 새 TypeScript 파일을 만들고 이름을 enableChainKairos.ts로 지정한 다음 다음 코드를 붙여넣습니다:


// scripts/enableChainKairos.ts
import { network } from "hardhat";
async function main() {
const connection = await network.connect({
network: "kairosTestnet"
});
const { ethers } = connection;
const [signer] = await ethers.getSigners();
console.log(`Using account: ${signer.address}`);
// Get the contract factory by name
const CrosschainNFT = await ethers.getContractFactory("CrosschainNFT", signer);
// Contract addresses and parameters
const crosschainNFTAddressKairosTestnet = `0x8c464Bb9Bf364F68b898ed0708b8f5F66EF6Cfb1`;
const crosschainNFTAddressEthereumSepolia = `0xb1fe42BBd7842703820C7480c22409b872319B22`;
const chainSelectorEthereumSepolia = `16015286601757825753`;
const ccipExtraArgs = `0x97a657c9000000000000000000000000000000000000000000000000000000000007A120`;
// Attach to the deployed contract on Kairos
const crosschainNFTKairos = CrosschainNFT.attach(crosschainNFTAddressKairosTestnet);
console.log(`Enabling chain for Ethereum Sepolia...`);
const tx = await crosschainNFTKairos.enableChain(
chainSelectorEthereumSepolia,
crosschainNFTAddressEthereumSepolia,
ccipExtraArgs
);
console.log(`Transaction hash: ${tx.hash}`);
console.log(`Waiting for confirmation...`);
const receipt = await tx.wait();
console.log(`Transaction confirmed in block: ${receipt?.blockNumber}`);
console.log(`Chain enabled successfully!`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

다음 명령을 실행하여 함수를 호출합니다:


npx hardhat run scripts/enableChainKairos.ts --network KairosTestnet

3단계: 이더리움 세폴리아에서 LINK로 계약 자금 조달하기​

CCIP 수수료를 충당하려면 이더리움 세폴리아에 배포된 크로스체인NFT 컨트랙트(crosschainNFTAddressEthereumSepolia)에 LINK로 자금을 조달하세요. 제공된 수도꼭지에서 테스트 링크를 받을 수 있습니다. 이 가이드에서는 LINK 3개를 보내면 충분합니다.

4단계: 이더리움 세폴리아에서 새로운 크로스체인NFT를 발행하기​

다음으로, 이더리움 세폴리아에 배포된 크로스체인NFT 콘트랙트에서 새로운 NFT를 발행합니다.

스크립트 폴더에 새 TypeScript 파일을 만들고 이름을 mint.ts로 지정한 다음 다음 코드를 붙여넣습니다:


// scripts/mint.ts
import { network } from "hardhat";
async function main() {
// Connect to the network
const connection = await network.connect({
network: "ethereumSepolia"
});
if (connection.networkName !== "ethereumSepolia") {
console.error(`Must be called from Ethereum Sepolia`);
process.exitCode = 1;
return;
}
const { ethers } = connection;
const [signer] = await ethers.getSigners();
console.log(`Using account: ${signer.address}`);
// Get the contract factory
const CrosschainNFT = await ethers.getContractFactory("CrosschainNFT", signer);
const crosschainNFTAddressEthereumSepolia = `0xb1fe42BBd7842703820C7480c22409b872319B22`
// Attach to the deployed contract
const crosschainNFT = CrosschainNFT.attach(crosschainNFTAddressEthereumSepolia);
console.log(`Minting NFT...`);
const tx = await crosschainNFT.mint();
console.log(`Transaction hash: ${tx.hash}`);
console.log(`Waiting for confirmation...`);
const receipt = await tx.wait();
console.log(`Transaction confirmed in block: ${receipt?.blockNumber}`);
console.log(`NFT minted successfully!`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

이 스크립트는 채굴 과정을 처리하고 교차 체인 전송을 위해 NFT를 준비합니다.

다음 명령을 실행하여 함수를 호출합니다:


npx hardhat run scripts/mint.ts --network ethereumSepolia

5단계: 체인 간 NFT 전송하기​

이더리움 세폴리아에서는 크로스체인 트랜스퍼 프롬 함수를 호출하여 NFT를 카이로스 테스트넷으로 전송합니다.

다음 값을 입력합니다:

  • 보낸 사람: 이더리움 세폴리아의 EOA 주소
  • 받는 사람: 카이로스 테스트넷에서 수신자의 EOA 주소(본인의 주소일 수도 있음)
  • 토큰아이디: 전송하려는 NFT의 ID입니다.
  • 대상체인선택자: 2624132734533621656 (카이로스 테스트넷의 CCIP 체인 선택기)
  • payFeesIn: 1 (CCIP 수수료가 LINK로 결제됨을 나타냄)

전송 스크립트 실행

스크립트 폴더에 새 타입스크립트 파일을 만들고 이름을 crossChainTransferNFT.ts로 지정한 다음 다음 코드를 붙여넣습니다:


// scripts/crossChainTransferNFT.ts
import { network } from "hardhat";
async function main() {
// Connect to the network
const connection = await network.connect({
network: "ethereumSepolia"
});
// Check if we're on the correct network
if (connection.networkName !== "ethereumSepolia") {
console.error(`Must be called from Ethereum Sepolia`);
process.exitCode = 1;
return;
}
const { ethers } = connection;
const [signer] = await ethers.getSigners();
console.log(`Using account: ${signer.address}`);
// Get the contract factory
const CrosschainNFT = await ethers.getContractFactory("CrosschainNFT", signer);
const crosschainNFTAddressEthereumSepolia = `0xb1fe42BBd7842703820C7480c22409b872319B22`;
// Transfer parameters
const from = `0x7b467A6962bE0ac80784F131049A25CDE27d62Fb`;
const to = `0x7b467A6962bE0ac80784F131049A25CDE27d62Fb`;
const tokenId = 0; // Put NFT token id here
const destinationChainSelector = "2624132734533621656"; // Kairos Testnet
const payFeesIn = 1; // 0 - Native, 1 - LINK
// Attach to the deployed contract
const crosschainNFT = CrosschainNFT.attach(crosschainNFTAddressEthereumSepolia);
const tx = await crosschainNFT.crossChainTransferFrom(
from,
to,
tokenId,
destinationChainSelector,
payFeesIn
);
console.log(`Transaction hash: ${tx.hash}`);
console.log(`Waiting for confirmation...`);
const receipt = await tx.wait();
console.log(`Transaction confirmed in block: ${receipt?.blockNumber}`);
console.log(`Cross-chain transfer initiated successfully!`);
console.log(`Note: The NFT will arrive on Kairos Testnet after CCIP processes the message.`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

그런 다음 스크립트를 실행합니다:


npx hardhat run scripts/crossChainTransferNFT.ts --network ethereumSepolia

전송 확인

CCIP Explorer](https://ccip.chain.link/#/side-drawer/msg/0x2a43cf8076ed6290dd0bf8bdbbc87abe2d238da43b6bf514e70909dd0f35c9db)에서 크로스체인 전송을 모니터링하고, Kaiascan에서 트랜잭션을 확인할 수 있습니다.

NFT가 카이로스 테스트넷에 도착하면 메타마스크 지갑에 추가합니다:

  1. 메타마스크에서 NFT 탭을 엽니다.
  2. NFT 가져오기를 클릭합니다.
  3. 카이로스 테스트넷의 크로스체인NFT 컨트랙트 주소와 받은 토큰아이디를 입력합니다(예: 0).

이제 NFT가 메타마스크 지갑에 표시됩니다.

결론​

이 튜토리얼에서는 체인링크 CCIP를 사용해 번 앤 민트 모델을 사용해 카이아 카이로스 테스트넷과 이더리움 세폴리아 간에 NFT를 전송하는 방법을 배웠습니다.

CCIP에 대해 자세히 알아보고 추가 사용 사례를 살펴보려면 공식 체인링크 CCIP 문서를 참조하시기 바랍니다.

페이지를 개선해 주세요