Tokenize Real Estate with Functions
Perform custom computation off-chain using Web2 data in your smart contract.
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}