티스토리 뷰

전체 코드

 

dig-solidity/hyperlane-v3 at main · piatoss3612/dig-solidity

Contribute to piatoss3612/dig-solidity development by creating an account on GitHub.

github.com


1. Foundry 프로젝트 초기화

forge init hyperlane-v3

2. 라이브러리 설치

$ forge install hyperlane-xyz/hyperlane-monorepo@main
$ forge install OpenZeppelin/openzeppelin-contracts@release-v4.9
$ forge install OpenZeppelin/openzeppelin-contracts-upgradeable@release-v4.9

3. 라이브러리 리맵핑

$ forge remappings > remappings.txt
  • remappings.txt 파일 수정
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
ds-test/=lib/forge-std/lib/ds-test/src/
erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/
forge-std/=lib/forge-std/src/
@hyperlane-v3/=lib/hyperlane-monorepo/solidity/
openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/
openzeppelin-contracts/=lib/openzeppelin-contracts/

4. CrosschainNft.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;

import {IMessageRecipient} from "@hyperlane-v3/contracts/interfaces/IMessageRecipient.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ERC721, ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";

contract CrosschainNft is ERC721URIStorage, IMessageRecipient {
    address public mailbox;
    uint256 public tokenId;

    error NotMailbox();

    event Minted(address indexed to, uint256 indexed tokenId, string tokenURI);

    modifier onlyMailbox() {
        if (msg.sender != mailbox) {
            revert NotMailbox();
        }
        _;
    }

    constructor(address _mailbox) ERC721("CrosschainNft", "CCNFT") {
        mailbox = _mailbox;
    }

    function handle(
        uint32 /*_origin*/,
        bytes32 /*_sender*/,
        bytes memory _body
    ) external payable onlyMailbox {
        (address to, string memory tokenURI) = abi.decode(
            _body,
            (address, string)
        );

        uint256 _tokenId = ++tokenId;

        _mint(to, _tokenId);
        _setTokenURI(_tokenId, tokenURI);

        emit Minted(to, _tokenId, tokenURI);
    }
}

5. CrosschainNftRouter.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;

import {IMailbox} from "@hyperlane-v3/contracts/interfaces/IMailbox.sol";
import {IRouter} from "@hyperlane-v3/contracts/interfaces/IRouter.sol";
import {TypeCasts} from "@hyperlane-v3/contracts/libs/TypeCasts.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract CrosschainNftRouter is Ownable, IRouter {
    error RouteAlreadyEnrolled();
    error RouteNotFound();
    error InvalidInputLength();

    address public mailbox;
    uint32[] private _innerDomains;
    mapping(uint32 => bytes32) private _innerRouters;

    constructor(address _mailbox) Ownable(msg.sender) {
        mailbox = _mailbox;
    }

    function domains() external view override returns (uint32[] memory) {
        return _innerDomains;
    }

    function routers(uint32 _domain) external view override returns (bytes32) {
        return _innerRouters[_domain];
    }

    function enrollRemoteRouter(
        uint32 _domain,
        bytes32 _router
    ) external onlyOwner {
        if (_innerRouters[_domain] != bytes32(0)) {
            revert RouteAlreadyEnrolled();
        }
        _innerRouters[_domain] = _router;
        _innerDomains.push(_domain);
    }

    function enrollRemoteRouters(
        uint32[] calldata _domains,
        bytes32[] calldata _routers
    ) external onlyOwner {
        if (_domains.length != _routers.length) {
            revert InvalidInputLength();
        }

        for (uint256 i = 0; i < _domains.length; i++) {
            if (_innerRouters[_domains[i]] != bytes32(0)) {
                revert RouteAlreadyEnrolled();
            }
            _innerRouters[_domains[i]] = _routers[i];
            _innerDomains.push(_domains[i]);
        }
    }

    function sendNft(
        uint32 _domainId,
        address _to,
        string memory _tokenURI
    ) external payable {
        bytes32 recipientAddress = _tryGetrecipientAddress(_domainId);
        bytes memory messageBody = abi.encode(_to, _tokenURI);

        IMailbox(mailbox).dispatch{value: msg.value}(
            _domainId,
            recipientAddress,
            messageBody
        );
    }

    function estimateFee(
        uint32 _domainId,
        address _to,
        string memory _tokenURI
    ) external view returns (uint256) {
        bytes32 recipientAddress = _tryGetrecipientAddress(_domainId);
        bytes memory messageBody = abi.encode(_to, _tokenURI);

        return _estimateFee(_domainId, recipientAddress, messageBody);
    }

    function _tryGetrecipientAddress(
        uint32 _domainId
    ) internal view returns (bytes32 recipientAddress) {
        recipientAddress = _innerRouters[_domainId];
        if (recipientAddress == bytes32(0)) {
            revert RouteNotFound();
        }
    }

    function _estimateFee(
        uint32 destinationDomain,
        bytes32 recipientAddress,
        bytes memory messageBody
    ) internal view returns (uint256) {
        return
            IMailbox(mailbox).quoteDispatch(
                destinationDomain,
                recipientAddress,
                messageBody
            );
    }
}

6. 배포 준비

  • .env 파일 생성
MUMBAI_RPC_URL=https://polygon-mumbai.g.alchemy.com/v2/<API_KEY>
SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/<API_KEY>
ARBITRUM_RPC_URL=https://arb-sepolia.g.alchemy.com/v2/<API_KEY>

PRIVATE_KEY=<YOUR_PRIVATE_KEY>

SEPOLIA_ETHERSCAN_API_KEY=<YOUR_ETHERSCAN_API_KEY>
POLYGONSCAN_API_KEY=<YOUR_POLYGONSCAN_API_KEY>
  • foundry.toml 파일 수정
[profile.default]
src = "src"
out = "out"
libs = ["lib"]

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
[rpc_endpoints]
sepolia = "${SEPOLIA_RPC_URL}"
mumbai = "${MUMBAI_RPC_URL}"

[etherscan]
sepolia = { key = "${SEPOLIA_ETHERSCAN_API_KEY}" }
mumbai = { key = "${POLYGONSCAN_API_KEY}", url = "https://api-testnet.polygonscan.com/api" }

7. CrosschainNftRouter 배포

  • Sepolia 네트워크에 배포 및 검증
  • Sepolia에 배포된 Hyperlane Mailbox의 주소: 0xfFAEF09B3cd11D9b20d1a19bECca54EEC2884766
contract Deploy is Script {
    function setUp() public {}

    function run() public {
        uint256 privateKey = vm.envUint("PRIVATE_KEY");

        vm.startBroadcast(privateKey);

        CrosschainNftRouter instance = new CrosschainNftRouter(
            0xfFAEF09B3cd11D9b20d1a19bECca54EEC2884766
        );

        console.log("Deployed CrosschainNftRouter at: ", address(instance));

        vm.stopBroadcast();
    }
}
$ forge script script/CrosschainNftRouter.s.sol:Deploy --rpc-url sepolia --broadcast --verify -vvvv
...
Script ran successfully.

== Logs ==
  Deployed CrosschainNftRouter at:  0xe996557C3A4D3586786359ccCaBe9f76842D1783

8. CrosschainNft 배포

  • Polygon Mumbai 네트워크에 배포 및 검증
  • Polygon Mumbai 에 배포된 Hyperlane Mailbox의 주소: 0x2d1889fe5B092CD988972261434F7E5f26041115
contract Deploy is Script {
    function setUp() public {}

    function run() public {
        uint256 privateKey = vm.envUint("PRIVATE_KEY");

        vm.startBroadcast(privateKey);

        CrosschainNft instance = new CrosschainNft(
            0x2d1889fe5B092CD988972261434F7E5f26041115
        );

        console.log("Deployed CrosschainNft at: ", address(instance));

        vm.stopBroadcast();
    }
}
$ forge script script/CrosschainNftMumbai.s.sol:Deploy --rpc-url mumbai --broadcast --verify -vvvv
...
Script ran successfully.

== Logs ==
  Deployed CrosschainNft at:  0x3716B00671B801f34bB4c99Aba5889A13d65c42E

9. CrosschainNftRouter를 통해 NFT 발행

  • router 등록
  • 가스비 계산
  • NFT 전송
contract SendNFT is Script {
    function setUp() public {}

    function run() public {
        uint256 privateKey = vm.envUint("PRIVATE_KEY");

        vm.startBroadcast(privateKey);

        CrosschainNftRouter instance = CrosschainNftRouter(
            0xe996557C3A4D3586786359ccCaBe9f76842D1783
        );

        uint32 domainId = 80001;
        bytes32 recipientAddress = TypeCasts.addressToBytes32(
            0x3716B00671B801f34bB4c99Aba5889A13d65c42E
        );

        instance.enrollRemoteRouter(domainId, recipientAddress);

        string
            memory uri = "https://green-main-hoverfly-930.mypinata.cloud/ipfs/QmXbFt5tDifdSgPmhFrwD56iNsJqbxCZ8dSdv4qzp49PNs";

        uint256 fee = instance.estimateFee(domainId, vm.addr(privateKey), uri);

        console.log("Estimated fee: ", fee);

        instance.sendNft{value: fee}(domainId, vm.addr(privateKey), uri);

        console.log("Sent NFT to domain: ", domainId);

        vm.stopBroadcast();
    }
}
$ forge script script/CrosschainNftRouter.s.sol:SendNFT --rpc-url sepolia --broadcast -vvvv
...
Script ran successfully.

== Logs ==
  Estimated fee:  1
  Sent NFT to domain:  80001

10. Hyperlane Explorer

 

The Hyperlane Network

The Permissionless Interoperability layer built for the modular future. Hyperlane is the first interoperability layer that enables you to permissionlessly connect any blockchain, out-of-the-box.

www.hyperlane.xyz


11. 발행된 NFT 확인

contract CheckNFT is Script {
    function setUp() public {}

    function run() public {
        uint256 privateKey = vm.envUint("PRIVATE_KEY");

        vm.startBroadcast(privateKey);

        CrosschainNft instance = CrosschainNft(
            0x3716B00671B801f34bB4c99Aba5889A13d65c42E
        );

        address owner = instance.ownerOf(1);

        console.log("NFT Owner: ", owner);

        string memory uri = instance.tokenURI(1);

        console.log("NFT URI: ", uri);

        vm.stopBroadcast();
    }
}
$ forge script script/CrosschainNftMumbai.s.sol:CheckNFT --rpc-url mumbai -vvvv
...
Script ran successfully.

== Logs ==
  NFT Owner:  0x965B0E63e00E7805569ee3B428Cf96330DFc57EF
  NFT URI:  https://green-main-hoverfly-930.mypinata.cloud/ipfs/QmXbFt5tDifdSgPmhFrwD56iNsJqbxCZ8dSdv4qzp49PNs
최근에 올라온 글
최근에 달린 댓글
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Total
Today
Yesterday
글 보관함