练习 1:房地产通证化

完整代码链接: https://github.com/andrejrakic/rwa-primer

调用Zillow API

在创建我们的通证之前,让我们看看我们将从哪里获取数据。

本练习中, 我们将使用 Zillow's API

这是一个测试用的API接口。如果想要了解更多关于此API的详细信息,请参考 Zillow 官方文档

让我们先从编写 JavaScript 函数开始,这个函数能帮助我们获取创建NFT所需的信息。

为了构建NFT的元数据,我们需要获取如下数据:

  • 房产地址

  • 建造年份

  • 土地面积

  • 居住面积

  • 卧室数量

我们可以利用 API 来获取这些信息。在将其添加进智能合约前,我们会使用 Functions Playground 来测试这段 JavaScript 代码。

// Import the ethers library from npm
const { ethers } = await import("npm:ethers@6.10.0");
const Hash = await import("npm:ipfs-only-hash@4.0.0");

// Make an HTTP request to fetch real estate data
const apiResponse = await Functions.makeHttpRequest({
  url: `https://api.bridgedataoutput.com/api/v2/OData/test/Property('P_5dba1fb94aa4055b9f29696f')?access_token=6baca547742c6f96a6ff71b138424f21`,
});

// Extract relevant data from the API response
const realEstateAddress = apiResponse.data.UnparsedAddress;
const yearBuilt = Number(apiResponse.data.YearBuilt);
const lotSizeSquareFeet = Number(apiResponse.data.LotSizeSquareFeet);
const livingArea = Number(apiResponse.data.LivingArea);
const bedroomsTotal = Number(apiResponse.data.BedroomsTotal);

const metadata = {
  name: "Real Estate Token",
  attributes: [
    { trait_type: "realEstateAddress", value: realEstateAddress },
    { trait_type: "yearBuilt", value: yearBuilt },
    { trait_type: "lotSizeSquareFeet", value: lotSizeSquareFeet },
    { trait_type: "livingArea", value: livingArea },
    { trait_type: "bedroomsTotal", value: bedroomsTotal }
  ]
};

// Stringify the JSON object
const metadataString = JSON.stringify(metadata);

const ipfsCid = await Hash.of(metadataString);
console.log(ipfsCid);

return Functions.encodeString(`ipfs://${ipfsCid}`);

定价信息

我们还将通过单独调用同一个API来收集定价信息

// Import ethers library
const { ethers } = await import("npm:ethers@6.10.0");

// Create ABI coder for data encoding
const abiCoder = ethers.AbiCoder.defaultAbiCoder();

// Get token ID from input arguments
const tokenId = args[0];

// Fetch property data from API
const apiResponse = await Functions.makeHttpRequest({
  url: `https://api.bridgedataoutput.com/api/v2/OData/test/Property('P_5dba1fb94aa4055b9f29696f')?access_token=6baca547742c6f96a6ff71b138424f21`,
});

// Extract and convert pricing data to numbers
const listPrice = Number(apiResponse.data.ListPrice);
const originalListPrice = Number(apiResponse.data.OriginalListPrice);
const taxAssessedValue = Number(apiResponse.data.TaxAssessedValue);

// Encode data for NFT use
const encoded = abiCoder.encode(
  [`uint256`, `uint256`, `uint256`, `uint256`],
  [tokenId, listPrice, originalListPrice, taxAssessedValue]
);

// Log data for verification
console.log(
  `Token ID: ${tokenId} \nList Price: ${listPrice} \nOriginal List Price: ${originalListPrice} \nTax Assessed Value: ${taxAssessedValue}`
);

// Return encoded data as bytes
return ethers.getBytes(encoded);

将代码存储于链上

我们的JavaScript代码已经准备就绪。接下来,我们将创建首个智能合约以将代码存储在区块链上。 创建一个名为FunctionsSource.sol的合约。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/**
 * 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.
 */
abstract contract FunctionsSource {
    string public getNftMetadata =
        "const { ethers } = await import('npm:ethers@6.10.0');"
        "const Hash = await import('npm:ipfs-only-hash@4.0.0');"
        "const apiResponse = await Functions.makeHttpRequest({"
        "    url: `https://api.bridgedataoutput.com/api/v2/OData/test/Property('P_5dba1fb94aa4055b9f29696f')?access_token=6baca547742c6f96a6ff71b138424f21`,"
        "});"
        "const realEstateAddress = apiResponse.data.UnparsedAddress;"
        "const yearBuilt = Number(apiResponse.data.YearBuilt);"
        "const lotSizeSquareFeet = Number(apiResponse.data.LotSizeSquareFeet);"
        "const livingArea = Number(apiResponse.data.LivingArea);"
        "const bedroomsTotal = Number(apiResponse.data.BedroomsTotal);"
        "const metadata = {"
        "name: `Real Estate Token`,"
        "attributes: ["
        "{ trait_type: `realEstateAddress`, value: realEstateAddress },"
        "{ trait_type: `yearBuilt`, value: yearBuilt },"
        "{ trait_type: `lotSizeSquareFeet`, value: lotSizeSquareFeet },"
        "{ trait_type: `livingArea`, value: livingArea },"
        "{ trait_type: `bedroomsTotal`, value: bedroomsTotal }"
        "]"
        "};"
        "const metadataString = JSON.stringify(metadata);"
        "const ipfsCid = await Hash.of(metadataString);"
        "return Functions.encodeString(`ipfs://${ipfsCid}`);";

    string public getPrices =
        "const { ethers } = await import('npm:ethers@6.10.0');"
        "const abiCoder = ethers.AbiCoder.defaultAbiCoder();"
        "const tokenId = args[0];"
        "const apiResponse = await Functions.makeHttpRequest({"
        "    url: `https://api.bridgedataoutput.com/api/v2/OData/test/Property('P_5dba1fb94aa4055b9f29696f')?access_token=6baca547742c6f96a6ff71b138424f21`,"
        "});"
        "const listPrice = Number(apiResponse.data.ListPrice);"
        "const originalListPrice = Number(apiResponse.data.OriginalListPrice);"
        "const taxAssessedValue = Number(apiResponse.data.TaxAssessedValue);"
        "const encoded = abiCoder.encode([`uint256`, `uint256`, `uint256`, `uint256`], [tokenId, listPrice, originalListPrice, taxAssessedValue]);"
        "return ethers.getBytes(encoded);";
}

在智能合约中存储 JavaScript 代码可能会有些棘手。请注意以下几点:

  • 用一对双引号包裹每一行代码

  • 在代码中使用单引号和反引号来转义字符串

  • 如果你想将所有 JavaScript 代码存储在一行中,那么请确保分号的正确位置

运用工厂模式发行 ERC-1155 通证

我们的通证化资产项目由多个不同的智能合约组成,这些合约需要我们在本次训练营期间开发。下图展示了整个架构,其中我们将在每个部分使用的 Chainlink 服务用蓝色字母标记出来。

创建 ERC1155Core.sol

首先,我们需要创建 ERC1155Core.sol文件。这个智能合约包含了所有与 ERC-1155 标准相关的逻辑,此外还有我们用于初始发行以及后续通过 CCIP 进行跨链转账时会用到的自定义 mintmintBatch 函数。

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {ERC1155Supply, ERC1155} from "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Supply.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.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.
 */
contract ERC1155Core is ERC1155Supply, OwnerIsCreator {
    address internal s_issuer;

    // Optional mapping for token URIs
    mapping(uint256 tokenId => string) private _tokenURIs;

    event SetIssuer(address indexed issuer);

    error ERC1155Core_CallerIsNotIssuerOrItself(address msgSender);

    modifier onlyIssuerOrItself() {
        if (msg.sender != address(this) && msg.sender != s_issuer) {
            revert ERC1155Core_CallerIsNotIssuerOrItself(msg.sender);
        }
        _;
    }

    // Used as the URI for all token types by relying on ID substitution, e.g. https://token-cdn-domain/{id}.json
    constructor(string memory uri_) ERC1155(uri_) {}

    function setIssuer(address _issuer) external onlyOwner {
        s_issuer = _issuer;

        emit SetIssuer(_issuer);
    }

    function mint(address _to, uint256 _id, uint256 _amount, bytes memory _data, string memory _tokenUri)
        public
        onlyIssuerOrItself
    {
        _mint(_to, _id, _amount, _data);
        _tokenURIs[_id] = _tokenUri;
    }

    function mintBatch(
        address _to,
        uint256[] memory _ids,
        uint256[] memory _amounts,
        bytes memory _data,
        string[] memory _tokenUris
    ) public onlyIssuerOrItself {
        _mintBatch(_to, _ids, _amounts, _data);
        for (uint256 i = 0; i < _ids.length; ++i) {
            _tokenURIs[_ids[i]] = _tokenUris[i];
        }
    }

    function burn(address account, uint256 id, uint256 amount) public onlyIssuerOrItself {
        if (account != _msgSender() && !isApprovedForAll(account, _msgSender())) {
            revert ERC1155MissingApprovalForAll(_msgSender(), account);
        }

        _burn(account, id, amount);
    }

    function burnBatch(address account, uint256[] memory ids, uint256[] memory amounts) public onlyIssuerOrItself {
        if (account != _msgSender() && !isApprovedForAll(account, _msgSender())) {
            revert ERC1155MissingApprovalForAll(_msgSender(), account);
        }

        _burnBatch(account, ids, amounts);
    }

    function uri(uint256 tokenId) public view override returns (string memory) {
        string memory tokenURI = _tokenURIs[tokenId];

        return bytes(tokenURI).length > 0 ? tokenURI : super.uri(tokenId);
    }

    function _setURI(uint256 tokenId, string memory tokenURI) internal {
        _tokenURIs[tokenId] = tokenURI;
        emit URI(uri(tokenId), tokenId);
    }
}

创建 CrossChainBurnAndMintERC1155.sol

该合约将继承 ERC1155Core.sol,并在其基本的 ERC-1155 功能基础上进行扩展以支持通过Chainlink CCIP 实现的跨链转账。

任何 NFT 都是通过智能合约实现的,而这些智能合约本质上与单一区块链相连。这意味着任何跨链 NFT 的实现至少需要两个区块链上的两个智能合约并在它们之间的互联。

考虑到以上内容,跨链 NFT 可以通过三种方式进行实施:

  • 销毁与铸造: NFT 持有者将其 NFT 放入源链上的智能合约中并将其销毁,实际上是从这条区块链上删除该 NFT。一旦完成这一动作,目标区块链上将从相应的智能合约中创造一个等价的 NFT。这一过程可以在两个方向上发生。

  • 锁定与铸造: NFT 持有者在源链上的智能合约中锁定他们的 NFT,同时在目标区块链上创造一个等效的 NFT。当持有者想要将NFT移回时,他们需销毁目标链上的 NFT,从而解锁原始区块链上的 NFT。

  • 锁定与解锁: 同一 NFT 集合在多个区块链上进行铸造。NFT 持有者可在源区块链上锁定自己的 NFT,以在目标区块链上解锁等效的 NFT。这意味着任何时候只有一个 NFT 的实例能处于活跃状态,即使这个 NFT 在同一时间存在跨区块链的多个副本。

我们将要实现的是销毁与铸造机制。

每种情形都需要一个位于中间层的跨链消息传递协议,用于从一条区块链向另一条区块链发送数据指令。

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {ERC1155Core, ERC1155} from "./ERC1155Core.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {IAny2EVMMessageReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IAny2EVMMessageReceiver.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Withdraw} from "./utils/Withdraw.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.
 */
contract CrossChainBurnAndMintERC1155 is ERC1155Core, IAny2EVMMessageReceiver, ReentrancyGuard, Withdraw {
    enum PayFeesIn {
        Native,
        LINK
    }

    error InvalidRouter(address router);
    error NotEnoughBalanceForFees(uint256 currentBalance, uint256 calculatedFees);
    error ChainNotEnabled(uint64 chainSelector);
    error SenderNotEnabled(address sender);
    error OperationNotAllowedOnCurrentChain(uint64 chainSelector);

    struct XNftDetails {
        address xNftAddress;
        bytes ccipExtraArgsBytes;
    }

    IRouterClient internal immutable i_ccipRouter;
    LinkTokenInterface internal immutable i_linkToken;
    uint64 private immutable i_currentChainSelector;

    mapping(uint64 destChainSelector => XNftDetails xNftDetailsPerChain) public s_chains;

    event ChainEnabled(uint64 chainSelector, address xNftAddress, bytes ccipExtraArgs);
    event ChainDisabled(uint64 chainSelector);
    event CrossChainSent(
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes data,
        uint64 sourceChainSelector,
        uint64 destinationChainSelector
    );
    event CrossChainReceived(
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes data,
        uint64 sourceChainSelector,
        uint64 destinationChainSelector
    );

    modifier onlyRouter() {
        if (msg.sender != address(i_ccipRouter)) {
            revert InvalidRouter(msg.sender);
        }
        _;
    }

    modifier onlyEnabledChain(uint64 _chainSelector) {
        if (s_chains[_chainSelector].xNftAddress == address(0)) {
            revert ChainNotEnabled(_chainSelector);
        }
        _;
    }

    modifier onlyEnabledSender(uint64 _chainSelector, address _sender) {
        if (s_chains[_chainSelector].xNftAddress != _sender) {
            revert SenderNotEnabled(_sender);
        }
        _;
    }

    modifier onlyOtherChains(uint64 _chainSelector) {
        if (_chainSelector == i_currentChainSelector) {
            revert OperationNotAllowedOnCurrentChain(_chainSelector);
        }
        _;
    }

    constructor(string memory uri_, address ccipRouterAddress, address linkTokenAddress, uint64 currentChainSelector)
        ERC1155Core(uri_)
    {
        i_ccipRouter = IRouterClient(ccipRouterAddress);
        i_linkToken = LinkTokenInterface(linkTokenAddress);
        i_currentChainSelector = currentChainSelector;
    }

    function enableChain(uint64 chainSelector, address xNftAddress, bytes memory ccipExtraArgs)
        external
        onlyOwner
        onlyOtherChains(chainSelector)
    {
        s_chains[chainSelector] = XNftDetails({xNftAddress: xNftAddress, ccipExtraArgsBytes: ccipExtraArgs});

        emit ChainEnabled(chainSelector, xNftAddress, ccipExtraArgs);
    }

    function disableChain(uint64 chainSelector) external onlyOwner onlyOtherChains(chainSelector) {
        delete s_chains[chainSelector];

        emit ChainDisabled(chainSelector);
    }

    function crossChainTransferFrom(
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data,
        uint64 destinationChainSelector,
        PayFeesIn payFeesIn
    ) external nonReentrant onlyEnabledChain(destinationChainSelector) returns (bytes32 messageId) {
        string memory tokenUri = uri(id);
        burn(from, id, amount);

        Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
            receiver: abi.encode(s_chains[destinationChainSelector].xNftAddress),
            data: abi.encode(from, to, id, amount, data, 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, id, amount, data, i_currentChainSelector, destinationChainSelector);
    }

    function supportsInterface(bytes4 interfaceId) public view override(ERC1155) returns (bool) {
        return interfaceId == type(IAny2EVMMessageReceiver).interfaceId || super.supportsInterface(interfaceId);
    }

    /// @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 id, uint256 amount, bytes memory data, string memory tokenUri) =
            abi.decode(message.data, (address, address, uint256, uint256, bytes, string));

        mint(to, id, amount, data, tokenUri);

        emit CrossChainReceived(from, to, id, amount, data, sourceChainSelector, i_currentChainSelector);
    }
}

如果你尝试编译这份合约,你会看到一个编译错误。这是因为合约中有对来自utils文件夹中的Withdraw.sol智能合约的导入语句。由于我们会将LINK代币充值到CrossChainBurnAndMintERC1155.sol名下以支付CCIP费用,因此Withdraw.sol将包含从本合约中提取任何ERC-20代币及原生代币的逻辑。

./utils文件夹中创建Withdraw.sol。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.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.
 */
contract Withdraw is OwnerIsCreator {
    using SafeERC20 for IERC20;

    error NothingToWithdraw();
    error FailedToWithdrawEth(address owner, address target, uint256 value);

    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);
    }
}

创建RealEstatePriceDetails.sol

该辅助智能合约将用于周期性地使用Chainlink Automation和Chainlink Functions服务来获取我们现实世界资产的价格详情。为了使其正常运作,我们已经创建了JavaScript脚本并将其内容放入到FunctionsSource.sol中。

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {FunctionsClient} from "@chainlink/contracts/src/v0.8/functions/v1_0_0/FunctionsClient.sol";
import {FunctionsRequest} from "@chainlink/contracts/src/v0.8/functions/v1_0_0/libraries/FunctionsRequest.sol";
import {FunctionsSource} from "./FunctionsSource.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.
 */
contract RealEstatePriceDetails is FunctionsClient, FunctionsSource, OwnerIsCreator {
    using FunctionsRequest for FunctionsRequest.Request;

    struct PriceDetails {
        uint80 listPrice;
        uint80 originalListPrice;
        uint80 taxAssessedValue;
    }

    address internal s_automationForwarderAddress;

    mapping(uint256 tokenId => PriceDetails) internal s_priceDetails;

    error OnlyAutomationForwarderOrOwnerCanCall();

    modifier onlyAutomationForwarderOrOwner() {
        if (msg.sender != s_automationForwarderAddress && msg.sender != owner()) {
            revert OnlyAutomationForwarderOrOwnerCanCall();
        }
        _;
    }

    constructor(address functionsRouterAddress) FunctionsClient(functionsRouterAddress) {}

    function setAutomationForwarder(address automationForwarderAddress) external onlyOwner {
        s_automationForwarderAddress = automationForwarderAddress;
    }

    function updatePriceDetails(string memory tokenId, uint64 subscriptionId, uint32 gasLimit, bytes32 donID)
        external
        onlyAutomationForwarderOrOwner
        returns (bytes32 requestId)
    {
        FunctionsRequest.Request memory req;
        req.initializeRequestForInlineJavaScript(this.getPrices());

        string[] memory args = new string[](1);
        args[0] = tokenId;

        req.setArgs(args);

        requestId = _sendRequest(req.encodeCBOR(), subscriptionId, gasLimit, donID);
    }

    function getPriceDetails(uint256 tokenId) external view returns (PriceDetails memory) {
        return s_priceDetails[tokenId];
    }

    function fulfillRequest(bytes32, /*requestId*/ bytes memory response, bytes memory err) internal override {
        if (err.length != 0) {
            revert(string(err));
        }

        (uint256 tokenId, uint256 listPrice, uint256 originalListPrice, uint256 taxAssessedValue) =
            abi.decode(response, (uint256, uint256, uint256, uint256));

        s_priceDetails[tokenId] = PriceDetails({
            listPrice: uint80(listPrice),
            originalListPrice: uint80(originalListPrice),
            taxAssessedValue: uint80(taxAssessedValue)
        });
    }
}

创建 RealEstateToken.sol

最后来创建我们的主合约,它将继承上述所有的合约。

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {CrossChainBurnAndMintERC1155} from "./CrossChainBurnAndMintERC1155.sol";
import {RealEstatePriceDetails} from "./RealEstatePriceDetails.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.
 */
contract RealEstateToken is CrossChainBurnAndMintERC1155, RealEstatePriceDetails {
    /**
     *
     *  ██████╗ ███████╗ █████╗ ██╗         ███████╗███████╗████████╗ █████╗ ████████╗███████╗    ████████╗ ██████╗ ██╗  ██╗███████╗███╗   ██╗
     *  ██╔══██╗██╔════╝██╔══██╗██║         ██╔════╝██╔════╝╚══██╔══╝██╔══██╗╚══██╔══╝██╔════╝    ╚══██╔══╝██╔═══██╗██║ ██╔╝██╔════╝████╗  ██║
     *  ██████╔╝█████╗  ███████║██║         █████╗  ███████╗   ██║   ███████║   ██║   █████╗         ██║   ██║   ██║█████╔╝ █████╗  ██╔██╗ ██║
     *  ██╔══██╗██╔══╝  ██╔══██║██║         ██╔══╝  ╚════██║   ██║   ██╔══██║   ██║   ██╔══╝         ██║   ██║   ██║██╔═██╗ ██╔══╝  ██║╚██╗██║
     *  ██║  ██║███████╗██║  ██║███████╗    ███████╗███████║   ██║   ██║  ██║   ██║   ███████╗       ██║   ╚██████╔╝██║  ██╗███████╗██║ ╚████║
     *  ╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝╚══════╝    ╚══════╝╚══════╝   ╚═╝   ╚═╝  ╚═╝   ╚═╝   ╚══════╝       ╚═╝    ╚═════╝ ╚═╝  ╚═╝╚══════╝╚═╝  ╚═══╝
     *
     */
    constructor(
        string memory uri_,
        address ccipRouterAddress,
        address linkTokenAddress,
        uint64 currentChainSelector,
        address functionsRouterAddress
    )
        CrossChainBurnAndMintERC1155(uri_, ccipRouterAddress, linkTokenAddress, currentChainSelector)
        RealEstatePriceDetails(functionsRouterAddress)
    {}
}

部署 RealEstateToken.sol 到 Avalanche Fuji 测试网

确保将 Solidity 编译器优化器设置为200次运行,并将EVM版本设置为 "Paris"。

为了将RealEstateToken.sol代币部署到 Avalanche Fuji 测试网,您需要向构造函数提供以下信息:

  • uri_: ""(这是ERC-1155代币的元数据基础URI,我们将其置为空)

  • ccipRouterAddress: 0xF694E193200268f9a4868e4Aa017A0118C9a8177

  • linkTokenAddress: 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846

  • currentChainSelector: 14767482510784806043

  • functionsRouterAddress: 0xA9d587a00A31A52Ed70D6026794a8FC5E2F5dCb0

创建 Issuer.sol

正如我们之前所提到的,发行部分超出了本次训练营的范围。因此为了简化操作,我们将仅仅通过铸造一个模拟版本的房地产以 ERC-1155 通证的形式直接到 Alice 的地址,来满足这次练习的需要。

为了实现这一点,我们将使用Issuer.sol辅助合约。

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {RealEstateToken} from "./RealEstateToken.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {FunctionsClient} from "@chainlink/contracts/src/v0.8/functions/v1_0_0/FunctionsClient.sol";
import {FunctionsRequest} from "@chainlink/contracts/src/v0.8/functions/v1_0_0/libraries/FunctionsRequest.sol";
import {FunctionsSource} from "./FunctionsSource.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.
 */
contract Issuer is FunctionsClient, FunctionsSource, OwnerIsCreator {
    using FunctionsRequest for FunctionsRequest.Request;

    error LatestIssueInProgress();

    struct FractionalizedNft {
        address to;
        uint256 amount;
    }

    RealEstateToken internal immutable i_realEstateToken;

    bytes32 internal s_lastRequestId;
    uint256 private s_nextTokenId;

    mapping(bytes32 requestId => FractionalizedNft) internal s_issuesInProgress;

    constructor(address realEstateToken, address functionsRouterAddress) FunctionsClient(functionsRouterAddress) {
        i_realEstateToken = RealEstateToken(realEstateToken);
    }

    function issue(address to, uint256 amount, uint64 subscriptionId, uint32 gasLimit, bytes32 donID)
        external
        onlyOwner
        returns (bytes32 requestId)
    {
        if (s_lastRequestId != bytes32(0)) revert LatestIssueInProgress();

        FunctionsRequest.Request memory req;
        req.initializeRequestForInlineJavaScript(this.getNftMetadata());
        requestId = _sendRequest(req.encodeCBOR(), subscriptionId, gasLimit, donID);

        s_issuesInProgress[requestId] = FractionalizedNft(to, amount);

        s_lastRequestId = requestId;
    }

    function cancelPendingRequest() external onlyOwner {
        s_lastRequestId = bytes32(0);
    }

    function fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal override {
        if (err.length != 0) {
            revert(string(err));
        }

        if (s_lastRequestId == requestId) {
            string memory tokenURI = string(response);

            uint256 tokenId = s_nextTokenId++;
            FractionalizedNft memory fractionalizedNft = s_issuesInProgress[requestId];

            i_realEstateToken.mint(fractionalizedNft.to, tokenId, fractionalizedNft.amount, "", tokenURI);

            s_lastRequestId = bytes32(0);
        }
    }
}

部署 Issuer.sol 到 Avalanche Fuji 测试网

要将Issuer.sol部署到 Avalanche Fuji 上,我们需要向其构造函数提供以下信息:

  • realEstateToken: RealEstateToken.sol 合约的地址,这个合约我们之前已经部署过了。

  • functionsRouterAddress: 0xA9d587a00A31A52Ed70D6026794a8FC5E2F5dCb0

调用RealEstateToken.sol的setIssuer函数

在Avalanche Fuji上,调用 RealEstateToken.solsetIssuer函数,并提供以下信息:

  • _issuer: 先前部署的 Issuer.sol的地址

你使用Chainlink Functions的订阅来支付、管理和追踪Functions请求。

  1. 点击Connect wallet:

  2. 阅读并接受Chainlink基金会的服务条款。然后点击 MetaMask.

  3. 确保你的钱包已连接至 Avalanche Fuji 测试网。如果没有,点击页面右上角的网络名称,并选择Avalanche Fuji

  4. 点击 Create Subscription:

  1. 提供一个电子邮件地址和订阅名称

  2. 确认订阅的创建

  3. 在创建订阅后,Functions界面会提示你为订阅充值。点击 Add funds 并充值 10 个 LINK:

  4. 为订阅充值后,将Issuer.solRealEstateToken.sol合约添加为该订阅的消费者。

  5. 记住你的订阅ID

调用 Issuer.sol 的issue函数

为了发行ERC-1155代币给Alice,用你部署该合约所使用的地址调用Issuer.sol的issue函数,并提供以下信息:

  • to: Alice的钱包地址 (一个你的任意地址即可)

  • amount: 20

  • subscriptionId: 刚刚创建的Chainlink Functions的订阅ID

  • gasLimit: 300000

  • donID: 0x66756e2d6176616c616e6368652d66756a692d31000000000000000000000000 (fun-avalanche-fuji-1)

  1. Chainlink Automation App中,点击蓝色的 Register new Upkeep 按钮。

  2. 在 Trigger 选项中选择 Time-based

  3. Target contract address中输入RealEstateToken.sol合约地址

  4. 成功输入合约地址和ABI后,以CRON表达式的形式指定您的时间安排。我们将每24小时调用一次updatePriceDetails函数。我们需要的CRON表达式是 0 0 * * * (这意味着该函数将在每天的00:00被触发)

  1. 提供你选择的"Upkeep name". 设置300000作为"Gas limit". 设置5 LINK作为 "Starting balance (LINK)"

  2. 点击Register upkeep并在MetaMask中确认交易

调用 RealEstateToken.sol 的 setAutomationForwarder函数

在 Chainlink Automation 网络中,每个已注册的 upkeep 都有其独特的Forwarder合约。你可以在你的 Chainlink Automation Upkeep仪表板上找到它的地址。

调用RealEstateToken.sol的setAutomationForwarder函数并提供:

  • automationForwarderAddressForwarder合约的地址

启用跨链转账

按照以下步骤启用跨链转账:

  1. 在新的区块链上部署RealEstateToken.sol。

  2. 调用enableChain函数,并为之前在其他区块链上部署的每一个 RealEstateToken.sol 提供chainSelectorxNftAddress ccipExtraArgs。这里仅指Avalanche Fuji的相关参数。

  3. 在之前部署了RealEstateToken.sol的所有其他区块链上(目前仅限 Ethereum Sepolia),调用enableChain函数,并提供新区块链的 chainSelectorxNftAddress ccipExtraArgs,即你刚部署了新的 RealEstateToken.sol 的区块链。

Last updated