chainlink

Chainlink

|RWA Tokenization
Designing Our Tokenization Solution
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.19;
3import { FunctionsClient } from "@chainlink/contracts/src/v0.8/functions/v1_0_0/FunctionsClient.sol";
4import { FunctionsRequest } from "@chainlink/contracts/src/v0.8/functions/v1_0_0/libraries/FunctionsRequest.sol";
5import { ConfirmedOwner } from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
6
7import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol";
8import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
9import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol";
10
11import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
12import { ERC721URIStorage } from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
13import { ERC721Burnable } from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
14
15/**
16 * @title Chainlink Functions example consuming Real Estate API
17 */
18contract RealEstate is
19    FunctionsClient,
20    ConfirmedOwner,
21    ERC721("Tokenized Real Estate", "tRE"),
22    ERC721URIStorage,
23    ERC721Burnable
24{
25    using FunctionsRequest for FunctionsRequest.Request;
26    using SafeERC20 for IERC20;
27
28    struct APIResponse {
29        uint index;
30        string tokenId;
31        string response;
32    }
33
34    struct House {
35        string tokenId;
36        address recipientAddress;
37        string homeAddress; 
38        string listPrice; 
39        string squareFootage;
40        uint createTime;
41        uint lastUpdate;
42    }
43    
44    House[] public houseInfo;
45
46    // Chainlink Functions script source code.
47    string private constant SOURCE_PRICE_INFO =
48        "const id = args[0];"
49        "const priceResponse = await Functions.makeHttpRequest({"
50        "url: `https://api.chateau.voyage/house/${id}`,"
51        "});"
52        "if (priceResponse.error) {"
53        "throw Error('Housing Price Request Error');"
54        "}"
55        "const price = priceResponse.data.latestValue;"
56        "return Functions.encodeString(price);";
57
58    bytes32 public donId; // DON ID for the Functions DON to which the requests are sent
59    uint64 private subscriptionId; // Subscription ID for the Chainlink Functions
60    uint32 private gasLimit; // Gas limit for the Chainlink Functions callbacks
61    uint public epoch; // Time interval for price updates.
62    uint private _totalHouses;
63
64    // Mapping of request IDs to API response info
65    mapping(bytes32 => APIResponse) public requests;
66    mapping(string => bytes32) public latestRequestId;
67    mapping(string tokenId => string price) public latestPrice;
68
69
70    event LastPriceRequested(bytes32 indexed requestId, string tokenId);
71    event LastPriceReceived(bytes32 indexed requestId, string response);
72
73    event RequestFailed(bytes error);
74
75    constructor(
76        address router,
77        bytes32 _donId,
78        uint64 _subscriptionId,
79        uint32 _gasLimit,
80        uint _epoch
81    ) FunctionsClient(router) ConfirmedOwner(msg.sender) {
82        donId = _donId;
83        subscriptionId = _subscriptionId;
84        gasLimit = _gasLimit;
85        epoch = _epoch;
86    }
87
88    /**
89     * @notice Issues new tokenized real estate NFT asset.
90     */
91    function issueHouse(
92        address recipientAddress, 
93        string memory homeAddress, 
94        string memory listPrice,
95        string memory squareFootage
96    ) external onlyOwner {
97        uint index = _totalHouses;
98        string memory tokenId = string(abi.encode(index));
99
100        // increase: _totalHouses.
101        _totalHouses++;
102
103        // create: instance of a House.
104       houseInfo.push(House({
105            tokenId: tokenId,
106            recipientAddress: recipientAddress,
107            homeAddress: homeAddress,
108            listPrice: listPrice,
109            squareFootage: squareFootage,
110            createTime: block.timestamp,
111            lastUpdate: block.timestamp
112        }));
113
114        setURI(
115            index,
116            homeAddress,
117            listPrice, 
118            squareFootage
119        );
120
121        _safeMint(recipientAddress, index);
122    }
123
124    /**
125     * @notice Request `lastPrice` for a given `tokenId`
126     * @param tokenId id of said token e.g. 0
127     */
128    function requestPrice(string calldata tokenId, uint index) external {
129        string[] memory args = new string[](1);
130        args[0] = tokenId;
131
132        // gets: houseInfo[tokenId]
133        House storage house = houseInfo[index];
134
135        // ensures: price update is not too soon (i.e. not until a full epoch elapsed).
136        require(block.timestamp - house.lastUpdate >= epoch, "RealEstate: Price update too soon");
137
138        bytes32 requestId = _sendRequest(SOURCE_PRICE_INFO, args);
139        // maps: `tokenId` associated with a given `requestId`.
140        requests[requestId].tokenId = tokenId;
141        // maps: `index` associated with a given `requestId`.
142        requests[requestId].index = index;
143
144        latestRequestId[tokenId] = requestId;
145
146        emit LastPriceRequested(requestId, tokenId);
147    }
148
149    /**
150     * @notice Construct and store a URI containing the off-chain data.
151     * @param tokenId the tokenId associated with the home.
152     * @param homeAddress the address of the home.
153     * @param listPrice year the home was built.
154     * @param squareFootage size of the home (in ft^2)
155     */
156    function setURI(
157        uint tokenId,
158        string memory homeAddress,
159        string memory listPrice,
160        string memory squareFootage
161    ) internal {
162        // [then] create URI: with property details.
163        string memory uri = Base64.encode(
164            bytes(
165                string(
166                    abi.encodePacked(
167                        '{"name": "Tokenized Real Estate",'
168                        '"description": "Tokenized Real Estate",',
169                        '"image": "",'
170                        '"attributes": [',
171                        '{"trait_type": "homeAddress",',
172                        '"value": ',
173                        homeAddress,
174                        "}",
175                        ',{"trait_type": "listPrice",',
176                        '"value": ',
177                        listPrice,
178                        "}",
179                        ',{"trait_type": "squareFootage",',
180                        '"value": ',
181                        squareFootage,
182                        "}",
183                        "]}"
184                    )
185                )
186            )
187        );
188            // [then] create: finalTokenURI: with metadata.
189            string memory finalTokenURI = string(abi.encodePacked("data:application/json;base64,", uri));
190
191            // [then] set: tokenURI for a given `tokenId`, containing metadata.
192            _setTokenURI(tokenId, finalTokenURI);
193
194    }
195
196    /**
197     * @notice Process the response from the executed Chainlink Functions script
198     * @param requestId The request ID
199     * @param response The response from the Chainlink Functions script
200     */
201    function _processResponse(
202        bytes32 requestId,
203        bytes memory response
204    ) private {
205        requests[requestId].response = string(response);
206
207        uint index = requests[requestId].index;
208        string memory tokenId = requests[requestId].tokenId;
209
210        // store: latest price for a given `tokenId`.
211        latestPrice[tokenId] = string(response);
212
213        // gets: houseInfo[tokenId]
214        House storage house = houseInfo[index];
215
216        // updates: listPrice for a given `tokenId`.
217        house.listPrice = string(response);
218        // updates: lastUpdate for a given `tokenId`.
219        house.lastUpdate = block.timestamp;
220
221        emit LastPriceReceived(requestId, string(response));
222    }
223
224    // CHAINLINK FUNCTIONS //
225
226    /**
227     * @notice Triggers an on-demand Functions request
228     * @param args String arguments passed into the source code and accessible via the global variable `args`
229     */
230    function _sendRequest(
231        string memory source,
232        string[] memory args
233    ) internal returns (bytes32 requestId) {
234        FunctionsRequest.Request memory req;
235        req.initializeRequest(
236            FunctionsRequest.Location.Inline,
237            FunctionsRequest.CodeLanguage.JavaScript,
238            source
239        );
240        if (args.length > 0) {
241            req.setArgs(args);
242        }
243        requestId = _sendRequest(
244            req.encodeCBOR(),
245            subscriptionId,
246            gasLimit,
247            donId
248        );
249    }
250
251    /**
252     * @notice Fulfillment callback function
253     * @param requestId The request ID, returned by sendRequest()
254     * @param response Aggregated response from the user code
255     * @param err Aggregated error from the user code or from the execution pipeline
256     * Either response or error parameter will be set, but never both
257     */
258    function fulfillRequest(
259        bytes32 requestId,
260        bytes memory response,
261        bytes memory err
262    ) internal override {
263        if (err.length > 0) {
264            emit RequestFailed(err);
265            return;
266        }
267        _processResponse(requestId, response);
268    }
269
270    // ERC721 SETTINGS //
271
272    // gets: tokenURI for a given `tokenId`.
273    function tokenURI(
274        uint tokenId
275    ) public view override(ERC721, ERC721URIStorage) returns (string memory) {
276        return super.tokenURI(tokenId);
277    }
278
279    // checks: interface is supported by this contract.
280    function supportsInterface(
281        bytes4 interfaceId
282    ) public view override(ERC721, ERC721URIStorage) returns (bool) {
283        return super.supportsInterface(interfaceId);
284    }
285
286    function totalHouses() public view returns (uint) {
287        return _totalHouses;
288    }
289
290    // OWNER SETTING //
291
292    // prevents excessive calls from UI.
293    function setEpoch(uint _epoch) public onlyOwner {
294        epoch = _epoch;
295    }
296}