练习 1:房地产通证化
完整代码链接: https://github.com/andrejrakic/rwa-primer
调用Zillow API
在创建我们的通证之前,让我们看看我们将从哪里获取数据。
本练习中, 我们将使用 Zillow's API
让我们先从编写 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);";
}
运用工厂模式发行 ERC-1155 通证
我们的通证化资产项目由多个不同的智能合约组成,这些合约需要我们在本次训练营期间开发。下图展示了整个架构,其中我们将在每个部分使用的 Chainlink 服务用蓝色字母标记出来。

创建 ERC1155Core.sol
首先,我们需要创建 ERC1155Core.sol
文件。这个智能合约包含了所有与 ERC-1155 标准相关的逻辑,此外还有我们用于初始发行以及后续通过 CCIP 进行跨链转账时会用到的自定义 mint
和 mintBatch
函数。
// 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
函数
setIssuer
函数在Avalanche Fuji上,调用 RealEstateToken.sol
的setIssuer
函数,并提供以下信息:
_issuer: 先前部署的
Issuer.sol
的地址
创建 Chainlink Functions 的订阅并为之充值
你使用Chainlink Functions的订阅来支付、管理和追踪Functions请求。
点击Connect wallet:
阅读并接受Chainlink基金会的服务条款。然后点击 MetaMask.
确保你的钱包已连接至 Avalanche Fuji 测试网。如果没有,点击页面右上角的网络名称,并选择Avalanche Fuji。
点击 Create Subscription:

提供一个电子邮件地址和订阅名称
确认订阅的创建
在创建订阅后,Functions界面会提示你为订阅充值。点击 Add funds 并充值 10 个 LINK:
为订阅充值后,将
Issuer.sol
和RealEstateToken.sol
合约添加为该订阅的消费者。记住你的订阅ID
调用 Issuer.sol 的issue
函数
issue
函数为了发行ERC-1155代币给Alice,用你部署该合约所使用的地址调用Issuer.sol的issue
函数,并提供以下信息:
to: Alice的钱包地址 (一个你的任意地址即可)
amount:
20
subscriptionId: 刚刚创建的Chainlink Functions的订阅ID
gasLimit:
300000
donID:
0x66756e2d6176616c616e6368652d66756a692d31000000000000000000000000
(fun-avalanche-fuji-1)
故障排除:
由于 ipfs npm 包的问题,首次Functions请求可能会被撤销。如果遇到这种情况,应使用 owner() 地址来调用 cancelPendingRequest 函数,然后重复调用 issue 函数进行尝试
创建 Chainlink Automation 的订阅并为之充值
在Chainlink Automation App中,点击蓝色的 Register new Upkeep 按钮。
在 Trigger 选项中选择 Time-based
Target contract address中输入
RealEstateToken.sol
合约地址成功输入合约地址和ABI后,以CRON表达式的形式指定您的时间安排。我们将每24小时调用一次
updatePriceDetails
函数。我们需要的CRON表达式是0 0 * * *
(这意味着该函数将在每天的00:00被触发)

提供你选择的"Upkeep name". 设置
300000
作为"Gas limit". 设置5
LINK作为 "Starting balance (LINK)"点击Register upkeep并在MetaMask中确认交易
调用 RealEstateToken.sol 的 setAutomationForwarder
函数
setAutomationForwarder
函数在 Chainlink Automation 网络中,每个已注册的 upkeep 都有其独特的Forwarder
合约。你可以在你的 Chainlink Automation Upkeep仪表板上找到它的地址。
调用RealEstateToken.sol的setAutomationForwarder
函数并提供:
automationForwarderAddress:
Forwarder
合约的地址
启用跨链转账
按照以下步骤启用跨链转账:
在新的区块链上部署RealEstateToken.sol。
调用
enableChain
函数,并为之前在其他区块链上部署的每一个 RealEstateToken.sol 提供chainSelector,xNftAddress 和 ccipExtraArgs。这里仅指Avalanche Fuji的相关参数。在之前部署了RealEstateToken.sol的所有其他区块链上(目前仅限 Ethereum Sepolia),调用
enableChain
函数,并提供新区块链的 chainSelector,xNftAddress 和 ccipExtraArgs,即你刚部署了新的 RealEstateToken.sol 的区块链。
Last updated