This is an example implementation of a dApp that uses RedStone oracles.
The repo contains an implementation of an NFT marketplace dApp with so-called "stable" price. It means that sellers can create sell orders (offers), specifying price amount in USD. But buyers are able to pay with native coins, required amount of which is calculated dynamically in the moment of the order execution.
Please feel free to contact the RedStone team on Discord if you have any questions.
We use hardhat and ethers.js for deployment scripts and contract tests and React for frontend implementation.
├── contracts # Solidity contracts
│ ├── ExampleNFT.sol # Example ERC721 contract
│ ├── Marketplace.sol # Simple NFT marketplace contract
│ ├── StableMarketplace.sol # NFT marketplace contract with stable price
│ └── ...
├── public # Folder with public html files and images for React app
├── scripts # Contract deployment scripts
├── src # React app source code
│ ├── components
│ │ ├── App.tsx # Main React component
│ ├── core
│ │ ├── blockchain.ts # JS module responsible for interaction with blockchain and contracts
│ ├── config/ # Folder with contract ABIs and deployed contract addresses
│ └── ...
├── test # Contract tests
└── ...
ExampleNFT
is a simple ERC721 contract with automated sequential token id assignment
function mint() external {
_mint(msg.sender, nextTokenId);
nextTokenId++;
}
This contract extends ERC721Enumerable
implementation created by the @openzeppelin
team, which adds view functions for listing all tokens and tokens owned by a user.
Marketplace
is an NFT marketplace contract, which allows to post sell orders for any NFT token that follows EIP-721 non-fungible token standard. It has the following functions:
// Created a new sell order
// This function requires approval for transfer on the specified NFT token
function postSellOrder(address nftContractAddress, uint256 tokenId, uint256 price) external {}
// Only order creator can call this function
function cancelOrder(uint256 orderId) external {}
// Allows to get info about all orders (including canceled, and executed ones)
function getAllOrders() public view returns (SellOrder[] memory) {}
// Returns expected price in AVAX for the given order
function getPrice(uint256 orderId) public view returns (uint256) {}
// Requires sending at least the minimal amount of AVAX
function buy(uint256 orderId) external payable {}
The implementation is quite straightforward, so we won't describe it here. You can check the full contract code in the contracts/Marketplace.sol.
StableMarketplace
is the marketplace contract with the stable price support. It extends the Marketplace.sol
implementation and only overrides its _getPriceFromOrder
function.
// `_getPriceFromOrder` function uses the `getOracleNumericValueFromTxMsg` function,
// which fetches signed data from tx calldata and verifies its signature
function _getPriceFromOrder(SellOrder memory order) internal view override returns (uint256)
{
uint256 ethPrice = getOracleNumericValueFromTxMsg(bytes32("ETH"));
return (order.price / ethPrice) * (10 ** 8);
}
For being able to use RedStone data, the contract extends the MainDemoConsumerBase.sol
redstone contract.
import "@redstone-finance/evm-connector/contracts/data-services/MainDemoConsumerBase.sol";
import "./Marketplace.sol";
contract StableMarketplace is Marketplace, MainDemoConsumerBase {
...
}
You can check the code of the React app in the src
folder. We tried to simplify it as much as possible and leave only the core marketplace functions.
The main UI logic is located in the App.tsx
file, and the contract interaction logic is in the blockchain.ts
file.
If you take a look into the blockchain.ts
file code, you'll notice that each contract call that needs to process RedStone data is made on a contract instance, that was wrapped by @redstone-finance/evm-connector.
import { WrapperBuilder } from "@redstone-finance/evm-connector";
async function getContractInstance(contractName) {
...
return new ethers.Contract(address, abi, signer);
}
async function buy(orderId) {
const marketplace = await getContractInstance("marketplace");
// Wrapping marketplace contract instance.
// It enables fetching data from redstone data pool
// for each contract function call
const wrappedMarketplaceContract = WrapperBuilder.wrap(
marketplace
).usingDataService(
{
dataServiceId: "redstone-main-demo",
uniqueSignersCount: 1,
dataFeeds: ["ETH"],
},
["https://d33trozg86ya9x.cloudfront.net"]
);
// Checking expected amount
const expectedAvaxAmount = await wrappedMarketplaceContract.getPrice(orderId);
...
}
You can read much more about contract wrapping and @redstone-finance/evm-connector
here.
We've used hardhat test framework to contract tests. All the tests are located in the test folder.
💡 Note that each contract function that needs RedStone oracle data is also called on a wrapped ethers contract instance.
const expectedAvaxAmount = await wrappedMarketplaceContract.getPrice(orderId);
The app is already deployed on Avalanche FUJI testnet. You can check it at: https://stable-marketplace.redstone.finance/
You can also clone this repo and build the app locally. Please follow the steps below:
git clone https://github.com/redstone-finance/stable-price-marketplace
cd stable-price-marketplace
yarn install
yarn run-local-node
yarn deploy-contracts:local
yarn app:start
The app should be running on http://localhost:3000
Select Networks dropdown
-> Add network
and enter the following details:
Network Name | hardhat-local |
---|---|
New RPC URL | http://localhost:8545 |
Chain ID | 1337 |
Currency Symbol | AVAX |
Then hit the Save
button.
User 1
:0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
User 2
:0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
You can see more keys in your console below the yarn run-local-node
After visiting the app first time you will see an almost empty screen with the + Mint new NFT
link. Click this link to mint new NFTs. After the minting transaction confirmation you will see your NFT in the left column.
Once you mint any NFTs, you can post sell order for each one of them. Click the SELL button and provide the USD value. You will be asked to confirm 2 transactions: for NFT transfer approval, and for the marketplace order creation. After their confirmation, you will see your order in the Orders column.
You can also switch metamask account and buy the NFT. I would recommend to open the developer tools in browser at the network tab and explore network requests that are being sent before the buy transaction sending.
You should see at least 2 requests with the ETH price data and crypto signatures. This data along with signatures is being attached for each contract call, that wants to process redstone oracle data.