diff --git a/docs/tutorials/oracles/application.md b/docs/tutorials/oracles/application.md new file mode 100644 index 00000000..02ae8df9 --- /dev/null +++ b/docs/tutorials/oracles/application.md @@ -0,0 +1,421 @@ +--- +title: "Part 4: Automating pricing decisions" +--- + +Now that your contract can use pricing data, you can act on that data to make trading decisions. +In this section, you set up a simple off-chain application to monitor prices and use the contract to buy and sell its simulated token. + +You can access the smart contract in many ways, but a simple way is to use a Node.JS application because Pyth provides a Node SDK that simplifies getting pricing data from Hermes. +The application that you create in this section also uses the [Viem](https://viem.sh/) EVM toolkit to interact with Etherlink. + +1. In the same directory as your `contracts` folder, create a directory named `app` to store your off-chain application. + +1. Go into the `app` folder and run `npm init -y` to initialize a Node.JS application. + +1. Run this command to install the Pyth and Viem dependencies: + + ```bash + npm add @pythnetwork/hermes-client @pythnetwork/price-service-client ts-node typescript viem + ``` + +1. Run this command to initialize TypeScript: + + ```bash + tsc --init + ``` + +1. In the `tsconfig.json` file, uncomment the `resolveJsonModule` line so `resolveJsonModule` is set to `true`. +This setting allows programs to import JSON files easily. + +1. Also in the `tsconfig.json` file, set the `target` field to `ES2020`. + +1. Create a file named `src/checkRate.ts` for the code of the application. + +1. In the file, import the dependencies: + + ```javascript + import { HermesClient, PriceUpdate } from "@pythnetwork/hermes-client"; + import { createWalletClient, http, getContract, createPublicClient, defineChain, Account, parseEther } from "viem"; + import { privateKeyToAccount } from "viem/accounts"; + import { abi } from "../../contracts/out/TutorialContract.sol/TutorialContract.json"; + ``` + + These dependencies include the Pyth and Viem toolkits and the compiled ABI of your contract. + You may need to change the path to your compiled contract if you put the contract in a different place relative to this file. + +1. Add these constants to access the environment variables you set, or edit this code to hard-code the values: + + ```javascript + // Pyth ID for exchange rate of XTZ to USD + const XTZ_USD_ID = process.env["XTZ_USD_ID"] as string; + + // Contract I deployed + const CONTRACT_ADDRESS = process.env["DEPLOYMENT_ADDRESS"] as any; // sandbox + + // My account based on private key + const myAccount: Account = privateKeyToAccount(`0x${process.env["PRIVATE_KEY"] as any}`); + ``` + +1. Add this code to define a custom chain for the Etherlink sandbox. +Viem (in `view/chains`) has built-in objects that represent Etherlink Mainnet and Testnet, but you must create your own to use the sandbox. + + ```javascript + // Viem custom chain definition for Etherlink sandbox + const etherlinkSandbox = defineChain({ + id: 128123, + name: 'EtherlinkSandbox', + nativeCurrency: { + decimals: 18, + name: 'tez', + symbol: 'xtz', + }, + rpcUrls: { + default: { + http: [process.env["RPC_URL"] as string], + }, + }, + }); + ``` + +1. Add these Viem objects that represent the wallet and chain so you can access them in code later: + + ```javascript + // Viem objects that allow programs to call the chain + const walletClient = createWalletClient({ + account: myAccount, + chain: etherlinkSandbox, // Or use etherlinkTestnet from "viem/chains" + transport: http(), + }); + const contract = getContract({ + address: CONTRACT_ADDRESS, + abi: abi, + client: walletClient, + }); + const publicClient = createPublicClient({ + chain: etherlinkSandbox, // Or use etherlinkTestnet from "viem/chains" + transport: http() + }); + ``` + +1. Add these constants, which you can change later to adjust how the program works: + + ```javascript + // Delay in seconds between polling Hermes for price data + const DELAY = 3; + // Minimum change in exchange rate that counts as a price fluctuation + const CHANGE_THRESHOLD = 0.0001; + ``` + +1. Add these utility functions: + + ```javascript + // Utility function to call read-only smart contract function + const getBalance = async () => parseInt(await contract.read.getBalance([myAccount.address]) as string); + + // Pause for a given number of seconds + const delaySeconds = (seconds: number) => new Promise(res => setTimeout(res, seconds*1000)); + ``` + +1. Add this function to get current price update data from Hermes, just like the `curl` command you used in previous sections: + + ```javascript + // Utility function to call Hermes and return the current price of one XTZ in USD + const getPrice = async (connection: HermesClient) => { + const priceIds = [XTZ_USD_ID]; + const priceFeedUpdateData = await connection.getLatestPriceUpdates(priceIds) as PriceUpdate; + const parsedPrice = priceFeedUpdateData.parsed![0].price; + const actualPrice = parseInt(parsedPrice.price) * (10 ** parsedPrice.expo) + return actualPrice; + } + ``` + + This function receives a Hermes connection object and returns the current XTZ/USD price. + +1. Add this utility function to check the price repeatedly and return the new price when it has changed above a given threshold: + + ```javascript + // Get the baseline price and poll until it changes past the threshold + const alertOnPriceFluctuations = async (_baselinePrice: number, connection: HermesClient): Promise => { + const baselinePrice = _baselinePrice; + await delaySeconds(DELAY); + let updatedPrice = await getPrice(connection); + while (Math.abs(baselinePrice - updatedPrice) < CHANGE_THRESHOLD) { + await delaySeconds(DELAY); + updatedPrice = await getPrice(connection); + } + return updatedPrice; + } + ``` + +1. Add a `run` function to contain the main logic of the application: + + ```javascript + const run = async () => { + + // Logic goes here + + } + + run(); + ``` + +1. Replace the `// Logic goes here` comment with this code, which checks your account and calls the contract's `initAccount` function if necessary to give you some simulated tokens to start with: + + ```javascript + // Check balance first + let balance = await getBalance(); + console.log("Starting balance:", balance); + // If not enough tokens, initialize balance with 5 tokens in the contract + if (balance < 5) { + console.log("Initializing account with 5 tokens"); + const initHash = await contract.write.initAccount([myAccount.address]); + await publicClient.waitForTransactionReceipt({ hash: initHash }); + balance = await getBalance() + console.log("Initialized account. New balance is", balance); + } + ``` + +1. After that code, add this code to create the connection to the Hermes client: + + ```javascript + const connection = new HermesClient("https://hermes.pyth.network"); + ``` + +1. Add this loop, which iterates a certain number of times or until the account runs out of tokens: + + ```javascript + let i = 0; + while (balance > 0 && i < 5) { + console.log("\n"); + console.log("Iteration", i++); + let baselinePrice = await getPrice(connection); + console.log("Baseline price:", baselinePrice, "USD to 1 XTZ"); + const oneUSDBaseline = Math.ceil((1/baselinePrice) * 10000) / 10000; // Round up to four decimals + console.log("Or", oneUSDBaseline, "XTZ to 1 USD"); + + const updatedPrice = await alertOnPriceFluctuations(baselinePrice, connection); + console.log("Price changed:", updatedPrice, "USD to 1 XTZ"); + const priceFeedUpdateData = await connection.getLatestPriceUpdates([XTZ_USD_ID]); + const oneUSD = Math.ceil((1/updatedPrice) * 10000) / 10000; // Round up to four decimals + + if (baselinePrice < updatedPrice) { + // Buy + console.log("Price of USD relative to XTZ went down; time to buy"); + console.log("Sending", oneUSD, "XTZ (about one USD)"); + const buyHash = await contract.write.buy( + [[`0x${priceFeedUpdateData.binary.data[0]}`]] as any, + { value: parseEther(oneUSD.toString()), gas: 30000000n }, + ); + await publicClient.waitForTransactionReceipt({ hash: buyHash }); + console.log("Bought one token for", oneUSD, "XTZ"); + } else if (baselinePrice > updatedPrice) { + console.log("Price of USD relative to XTZ went up; time to sell"); + // Sell + const sellHash = await contract.write.sell( + [[`0x${priceFeedUpdateData.binary.data[0]}`]] as any, + { gas: 30000000n } + ); + await publicClient.waitForTransactionReceipt({ hash: sellHash }); + console.log("Sold one token for", oneUSD, "XTZ"); + } + balance = await getBalance(); + } + ``` + + The code in this loop uses the `alertOnPriceFluctuations` function wait until the XTZ/USD price has changed significantly. + If the price of USD relative to XTZ went down, it's cheaper to buy the simulated token, so the code buys one. + If the price of USD went up, it sells a token. + +The complete program looks like this: + +```javascript +import { HermesClient, PriceUpdate } from "@pythnetwork/hermes-client"; +import { createWalletClient, http, getContract, createPublicClient, defineChain, Account, parseEther } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { abi } from "../../contracts/out/TutorialContract.sol/TutorialContract.json"; + +// Pyth ID for exchange rate of XTZ to USD +const XTZ_USD_ID = process.env["XTZ_USD_ID"] as string; + +// Contract I deployed +const CONTRACT_ADDRESS = process.env["DEPLOYMENT_ADDRESS"] as any; // sandbox + +// My account based on private key +const myAccount: Account = privateKeyToAccount(`0x${process.env["PRIVATE_KEY"] as any}`); + +// Viem custom chain definition for Etherlink sandbox +const etherlinkSandbox = defineChain({ + id: 128123, + name: 'EtherlinkSandbox', + nativeCurrency: { + decimals: 18, + name: 'tez', + symbol: 'xtz', + }, + rpcUrls: { + default: { + http: [process.env["RPC_URL"] as string], + }, + }, +}); + +// Viem objects that allow programs to call the chain +const walletClient = createWalletClient({ + account: myAccount, + chain: etherlinkSandbox, // Or use etherlinkTestnet from "viem/chains" + transport: http(), +}); +const contract = getContract({ + address: CONTRACT_ADDRESS, + abi: abi, + client: walletClient, +}); +const publicClient = createPublicClient({ + chain: etherlinkSandbox, // Or use etherlinkTestnet from "viem/chains" + transport: http() +}); + +// Delay in seconds between polling Hermes for price data +const DELAY = 3; +// Minimum change in exchange rate that counts as a price fluctuation +const CHANGE_THRESHOLD = 0.0001; + +// Utility function to call read-only smart contract function +const getBalance = async () => parseInt(await contract.read.getBalance([myAccount.address]) as string); + +// Pause for a given number of seconds +const delaySeconds = (seconds: number) => new Promise(res => setTimeout(res, seconds*1000)); + +// Utility function to call Hermes and return the current price of one XTZ in USD +const getPrice = async (connection: HermesClient) => { + const priceIds = [XTZ_USD_ID]; + const priceFeedUpdateData = await connection.getLatestPriceUpdates(priceIds) as PriceUpdate; + const parsedPrice = priceFeedUpdateData.parsed![0].price; + const actualPrice = parseInt(parsedPrice.price) * (10 ** parsedPrice.expo) + return actualPrice; +} + +// Get the baseline price and poll until it changes past the threshold +const alertOnPriceFluctuations = async (_baselinePrice: number, connection: HermesClient): Promise => { + const baselinePrice = _baselinePrice; + await delaySeconds(DELAY); + let updatedPrice = await getPrice(connection); + while (Math.abs(baselinePrice - updatedPrice) < CHANGE_THRESHOLD) { + await delaySeconds(DELAY); + updatedPrice = await getPrice(connection); + } + return updatedPrice; +} + +const run = async () => { + + // Check balance first + let balance = await getBalance(); + console.log("Starting balance:", balance); + // If not enough tokens, initialize balance with 5 tokens in the contract + if (balance < 5) { + console.log("Initializing account with 5 tokens"); + const initHash = await contract.write.initAccount([myAccount.address]); + await publicClient.waitForTransactionReceipt({ hash: initHash }); + balance = await getBalance() + console.log("Initialized account. New balance is", balance); + } + + const connection = new HermesClient("https://hermes.pyth.network"); + + let i = 0; + while (balance > 0 && i < 5) { + console.log("\n"); + console.log("Iteration", i++); + let baselinePrice = await getPrice(connection); + console.log("Baseline price:", baselinePrice, "USD to 1 XTZ"); + const oneUSDBaseline = Math.ceil((1/baselinePrice) * 10000) / 10000; // Round up to four decimals + console.log("Or", oneUSDBaseline, "XTZ to 1 USD"); + + const updatedPrice = await alertOnPriceFluctuations(baselinePrice, connection); + console.log("Price changed:", updatedPrice, "USD to 1 XTZ"); + const priceFeedUpdateData = await connection.getLatestPriceUpdates([XTZ_USD_ID]); + const oneUSD = Math.ceil((1/updatedPrice) * 10000) / 10000; // Round up to four decimals + + if (baselinePrice < updatedPrice) { + // Buy + console.log("Price of USD relative to XTZ went down; time to buy"); + console.log("Sending", oneUSD, "XTZ (about one USD)"); + const buyHash = await contract.write.buy( + [[`0x${priceFeedUpdateData.binary.data[0]}`]] as any, + { value: parseEther(oneUSD.toString()), gas: 30000000n }, + ); + await publicClient.waitForTransactionReceipt({ hash: buyHash }); + console.log("Bought one token for", oneUSD, "XTZ"); + } else if (baselinePrice > updatedPrice) { + console.log("Price of USD relative to XTZ went up; time to sell"); + // Sell + const sellHash = await contract.write.sell( + [[`0x${priceFeedUpdateData.binary.data[0]}`]] as any, + { gas: 30000000n } + ); + await publicClient.waitForTransactionReceipt({ hash: sellHash }); + console.log("Sold one token for", oneUSD, "XTZ"); + } + balance = await getBalance(); + } +} + +run(); + +``` + +To run the off-chain application, run the command `npx ts-node src/checkRate.ts`. +The application calls the `buy` and `sell` function based on real-time data from Hermes. +Here is the output from a sample run: + +``` +Starting balance: 0 +Initializing account with 5 tez +Initialized account. New balance is 5 + + +Iteration 0 +Baseline price: 0.5179437100000001 USD to 1 XTZ +Or 1.9308 XTZ to 1 USD +Price changed: 0.5177393 USD to 1 XTZ +Price of USD relative to XTZ went up; time to sell +Sold one token for 1.9315 XTZ + + +Iteration 1 +Baseline price: 0.51764893 USD to 1 XTZ +Or 1.9319 XTZ to 1 USD +Price changed: 0.51743925 USD to 1 XTZ +Price of USD relative to XTZ went up; time to sell +Sold one token for 1.9326 XTZ + + +Iteration 2 +Baseline price: 0.51749921 USD to 1 XTZ +Or 1.9324 XTZ to 1 USD +Price changed: 0.51762153 USD to 1 XTZ +Price of USD relative to XTZ went down; time to buy +Sending 1.932 XTZ (about one USD) +Bought one token for 1.932 XTZ + + +Iteration 3 +Baseline price: 0.51766628 USD to 1 XTZ +Or 1.9318 XTZ to 1 USD +Price changed: 0.51781075 USD to 1 XTZ +Price of USD relative to XTZ went down; time to buy +Sending 1.9313 XTZ (about one USD) +Bought one token for 1.9313 XTZ + + +Iteration 4 +Baseline price: 0.51786312 USD to 1 XTZ +Or 1.9311 XTZ to 1 USD +Price changed: 0.51770622 USD to 1 XTZ +Price of USD relative to XTZ went up; time to sell +Sold one token for 1.9316 XTZ +``` + +Now you can use the pricing data in the contract from off-chain applications. +You could expand this application by customizing the buy and sell logic or by tracking your account's balance to see if you earned XTZ. diff --git a/docs/tutorials/oracles/environment.md b/docs/tutorials/oracles/environment.md new file mode 100644 index 00000000..44ccb0ab --- /dev/null +++ b/docs/tutorials/oracles/environment.md @@ -0,0 +1,119 @@ +--- +title: "Part 1: Setting up a development environment" +--- + +There are many tools that you can use for developing applications on EVM-compatible systems, but this tutorial uses the [Foundry](https://getfoundry.sh) suite, which includes these tools: + +- `forge`: For building, testing, debugging, and deploying smart contracts +- `anvil`: For local development nodes +- `cast`: For sending transactions to EVM chains from the command line + +This tutorial uses Etherlink's built-in [Sandbox](/building-on-etherlink/sandbox) mode so you can run a local Etherlink environment to deploy and test applications without having to get XTZ from a faucet or deploy contracts to Etherlink Testnet. + +Follow these steps to set up a local development environment with Foundry and the Etherlink sandbox mode: + +## Install prerequisites + +Before you begin, make sure that you have these programs installed: + +- Node.JS +- `jq` +- `curl` + +## Installing and configuring Foundry + +1. Install Foundry from https://getfoundry.sh. + +1. Create a keypair to use with the Etherlink sandbox by running this command: + + ```bash + cast wallet new + ``` + + The output of the command includes the address and private key of the new account. + +1. Put the address of the account in the `ADDRESS` environment variable. + +1. Put the private key of the account in the `PRIVATE_KEY` environment variable. + +## Setting up the Etherlink sandbox + +1. Get the current version of the Octez EVM node (the `octez-evm-node` binary) from the Octez release page: https://gitlab.com/tezos/tezos/-/releases. + +1. Verify that you have at least version 0.29 of the `octez-evm-node` binary by running this command in a terminal window: + + ```bash + octez-evm-node --version + ``` + +1. Run this command to configure the local sandbox environment: + + ```bash + octez-evm-node init config --network testnet \ + --dont-track-rollup-node \ + --config-file \ + --data-dir + ``` + + Use a new location for the configuration file for the variable `` (such as `~/sandbox-config.json`) and a new location for the node's data directory for the variable ``, as in this example: + + ```bash + octez-evm-node init config --network testnet \ + --dont-track-rollup-node \ + --config-file ~/sandbox-config.json \ + --data-dir ~/sandbox-node + ``` + + :::note + + Use a new directory for the `--data-dir` argument, not a directory that you have used for another EVM node. + After you use the node in sandbox mode with a certain data directory, you cannot re-use that data directory for running an EVM node on Etherlink Mainnet or Testnet. + + ::: + +1. Run this command to start the EVM node in sandbox mode, using the same values for the variables `` and `` and the address you created for `$ADDRESS`: + + ```bash + octez-evm-node run sandbox --network testnet \ + --init-from-snapshot \ + --config-file \ + --data-dir \ + --fund $ADDRESS + ``` + + This command starts the node in sandbox mode and sends 10,000 XTZ to your address. + This sandbox state starts with the current state of Etherlink Testnet but is a separate environment, so you can't use it to deploy contracts or make transactions on Testnet. + +1. Wait for the node to download teh snapshot of Etherlink Testnet and synchronize with the current state. +This can take a few minutes depending on your connection and how old the most recent snapshot is. + + The sandbox environment is ready when the EVM node's log shows the level of the new head block, as in this example: + + ``` + Jun 16 14:26:32.041 NOTICE │ head is now 19809131, applied in 10.681ms + ``` + +1. Keep the terminal window that is running the EVM node open. + +1. In a new terminal window, verify that the sandbox is running by running this command: + + ```bash + curl -X POST -H 'Content-Type: application/json' \ + --data '{"jsonrpc": "2.0","method": "eth_getBalance","params":["'$ADDRESS'"],"id": 1}' \ + http://localhost:8545 + ``` + + You may need to set the `ADDRESS` environment variable in this terminal window. + + The response shows the account's balance in the sandbox in hexadecimal format: + + ```json + {"jsonrpc":"2.0","result":"0x21e19e0c9bab2400000","id":1} + ``` + + The response is 10000000000000000000000 wei, or 10,000 XTZ. + As with Ethereum, Etherlink records its native token (XTZ) in units of 10^18, also referred to as wei. + +Now you can use Foundry to work with Etherlink in a local sandbox environment. +In the next section, you will create a contract and deploy it to this sandbox. +Continue to [Part 2: Getting information from the Pyth oracle](/tutorials/oracles/get_data). diff --git a/docs/tutorials/oracles/get_data.md b/docs/tutorials/oracles/get_data.md new file mode 100644 index 00000000..e28c03ac --- /dev/null +++ b/docs/tutorials/oracles/get_data.md @@ -0,0 +1,523 @@ +--- +title: "Part 2: Getting information from the Pyth oracle" +--- + +Getting price information from the Pyth oracle takes a few steps: + +1. The off-chain caller gets price update data from Hermes, Pyth's service that listens for price updates and provides them to off-chain applications via a REST API. +This data contains the information that Pyth needs to provide a current price to on-chain applications. + +1. The off-chain caller sends that update data to the smart contract. + +1. Based on the update data, smart contract calculates the fee that Pyth charges to provide that price data to smart contracts. + +1. The smart contract calls Pyth's on-chain application and pays the fee. + +1. The Pyth on-chain application gets the price data from Hermes and provides it to the smart contract. + +1. The smart contract stores the updated price data. + +## Getting oracle data in a contract + +Follow these steps to create a contract that uses the Pyth oracle in the way described above: + +1. Create a directory to store your work in: + + ```bash + mkdir -p etherlink_defi/contracts + cd etherlink_defi/contracts + ``` + + Later you will create a folder named `etherlink_defi/app` to store the off-chain portion of the tutorial application. + +1. Create an empty Foundry project in the `etherlink_defi/contracts` folder: + + ```bash + forge init + ``` + + This command creates starter contracts and tests. + +1. Remove the starter contracts and tests by running this command: + + ```bash + rm -r src/* test/* script/* + ``` + +1. Set up a Node.JS project and install the Pyth SDK by running these commands: + + ```bash + npm init -y + npm install @pythnetwork/pyth-sdk-solidity + ``` + +1. Run this command to create mappings that tell Foundry where to find the SDK so you can import it from Solidity contracts: + + ```bash + echo '@pythnetwork/pyth-sdk-solidity/=node_modules/@pythnetwork/pyth-sdk-solidity' > remappings.txt + ``` + +1. Create a file named `src/TutorialContract.sol` and open it in any text editor. + +1. Paste this stub of a Solidity smart contract into the file: + + ```solidity + // SPDX-License-Identifier: UNLICENSED + pragma solidity ^0.8.13; + + import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; + + contract TutorialContract { + IPyth pyth; + bytes32 xtzUsdPriceId; + + constructor(address _pyth, bytes32 _xtzUsdPriceId) { + pyth = IPyth(_pyth); + xtzUsdPriceId = _xtzUsdPriceId; + } + + // Functions go here + + } + ``` + + The contract stores two variables: an object that represents the Pyth on-chain application and an identifier that represents the exchange rate that the contract is interested in. + In this case, the contract is interested in the exchange rate between Tezos/Etherlink XTZ and USD. + + This stub includes only the contract constructor; you add functions in the next few steps. + +1. Replace the `// Functions go here` comment with this function: + + ```solidity + // Update the price + function updatePrice(bytes[] calldata pythPriceUpdate) public { + uint updateFee = pyth.getUpdateFee(pythPriceUpdate); + pyth.updatePriceFeeds{ value: updateFee }(pythPriceUpdate); + } + ``` + + This function receives price update data that an off-chain caller got from Hermes. + It uses this data and the Pyth on-chain application to get the cost of the on-chain price update. + Finally, it passes the fee and the price data to Pyth. + + If this function succeeds, the `pyth` variable in the contract is updated with current price data, which other functions in the smart contract can access, as in the next function that you will add. + +1. After the `updatePrice` function, add this function: + + ```solidity + // Get 1 USD in wei + function getPrice() public view returns (uint256) { + PythStructs.Price memory price = pyth.getPriceNoOlderThan( + xtzUsdPriceId, + 60 + ); + uint xtzPrice18Decimals = (uint(uint64(price.price)) * (10 ** 18)) / + (10 ** uint8(uint32(-1 * price.expo))); + uint oneDollarInWei = ((10 ** 18) * (10 ** 18)) / xtzPrice18Decimals; + return oneDollarInWei; + } + ``` + + This function uses the Pyth data in the `pyth` variable to return the amount of wei currently equal to 1 USD. + It uses the `pyth.getPriceNoOlderThan` function, which fails if the data is stale, in this case older than 60 seconds. + Later in this section you add tests to verify that this function is accurate. + +1. After the `getPrice` function, add this function: + + ```solidity + // Update and get the price in a single step + function updateAndGet(bytes[] calldata pythPriceUpdate) external payable returns (uint256) { + updatePrice((pythPriceUpdate)); + return getPrice(); + } + ``` + + Because the price goes stale, it's convenient to update the price and retrieve it in a single step. + This function merely calls the two pervious functions in order. + +1. Make sure that your contract compiles by running this command: + + ```bash + forge build + ``` + +If you see any errors, make sure that the contract matches this code: + +```solidity +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; + +contract TutorialContract { + IPyth pyth; + bytes32 xtzUsdPriceId; + + constructor(address _pyth, bytes32 _xtzUsdPriceId) { + pyth = IPyth(_pyth); + xtzUsdPriceId = _xtzUsdPriceId; + } + + // Update the price + function updatePrice(bytes[] calldata pythPriceUpdate) public { + uint updateFee = pyth.getUpdateFee(pythPriceUpdate); + pyth.updatePriceFeeds{ value: updateFee }(pythPriceUpdate); + } + + // Get 1 USD in wei + function getPrice() public view returns (uint256) { + PythStructs.Price memory price = pyth.getPriceNoOlderThan( + xtzUsdPriceId, + 60 + ); + uint xtzPrice18Decimals = (uint(uint64(price.price)) * (10 ** 18)) / + (10 ** uint8(uint32(-1 * price.expo))); + uint oneDollarInWei = ((10 ** 18) * (10 ** 18)) / xtzPrice18Decimals; + return oneDollarInWei; + } + + // Update and get the price in a single step + function updateAndGet(bytes[] calldata pythPriceUpdate) external payable returns (uint256) { + updatePrice((pythPriceUpdate)); + return getPrice(); + } +} +``` + +## Testing the data + +To test the contract and how it gets data from Pyth, you can write Foundry tests that use a mocked version of Pyth. +In these steps, you set the exchange rate in the mocked version of Pyth and use tests to verify that the contract gets that exchange rate correctly: + +1. Create a test file named `test/TutorialContract.t.sol` and open it in any text editor. + +1. Put this test stub code in the file: + + ```solidity + // SPDX-License-Identifier: UNLICENSED + pragma solidity ^0.8.13; + + import { Test, console2 } from "forge-std/Test.sol"; + import { TutorialContract } from "../src/TutorialContract.sol"; + import { MockPyth } from "@pythnetwork/pyth-sdk-solidity/MockPyth.sol"; + + contract ContractToTest is Test { + MockPyth public pyth; + bytes32 XTZ_PRICE_FEED_ID = bytes32(uint256(0x1)); + TutorialContract public myContract; + + uint256 XTZ_TO_WEI = 10 ** 18; + + function setUp() public { + pyth = new MockPyth(60, 1); + myContract = new TutorialContract(address(pyth), XTZ_PRICE_FEED_ID); + } + + // Test functions go here + + } + ``` + + This stub imports your contract and creates an instance of it in the`setUp` function, which runs automatically before each test. + It creates a mocked version of Pyth for the purposes of the test. + +1. Replace the `// Test functions go here` comment with this utility function: + + ```solidity + // Utility function to create a mocked Pyth price update for the test + function createXtzUpdate( + int64 xtzPrice + ) private view returns (bytes[] memory) { + bytes[] memory updateData = new bytes[](1); + updateData[0] = pyth.createPriceFeedUpdateData( + XTZ_PRICE_FEED_ID, + xtzPrice * 100000, // price + 10 * 100000, // confidence + -5, // exponent + xtzPrice * 100000, // emaPrice + 10 * 100000, // emaConfidence + uint64(block.timestamp), // publishTime + uint64(block.timestamp) // prevPublishTime + ); + + return updateData; + } + ``` + + This function accepts a price as an integer (such as 10 to mean that 10 XTZ equals 1 USD) and creates mocked Hermes data for a price update. + +1. After the `createXtzUpdate` function, add this utility function: + + ```solidity + // Utility function to set the Pyth price + function setXtzPrice(int64 xtzPrice) private { + bytes[] memory updateData = createXtzUpdate(xtzPrice); + uint updateFee = pyth.getUpdateFee(updateData); + vm.deal(address(this), updateFee); + pyth.updatePriceFeeds{ value: updateFee }(updateData); + } + ``` + + This function calls the `createXtzUpdate` function to create the mocked Hermes data. + Then it gets the amount of the fee from the mocked instance of Pyth and sends this fee and the data to Pyth. + + These utility functions mirror the functions in the contract, but they are to operate the mocked instance of Pyth. + These functions allow the test to set the current price in the Pyth on-chain application itself, which the contract accesses. + The next functions that you create use an instance of your contract in the test and verify that it gets the data from Pyth correctly. + +1. After the `setXtzPrice` function, add this test function: + + ```solidity + // Set the price that 5 XTZ = 1 USD and verify + function testUpdateAndGet() public { + // Set price in mocked version of Pyth + int64 xtzPrice = 5; + setXtzPrice(xtzPrice); + + // Call the updateAndGet function and send enough for the Pyth fee + bytes[] memory updateData = createXtzUpdate(xtzPrice); + uint updateFee = pyth.getUpdateFee(updateData); + vm.deal(address(this), updateFee); + + // Verify that the contract has the same exchange rate for XTZ/USD + uint256 priceWei = myContract.updateAndGet{ value: updateFee }(updateData); + assertEq(priceWei, XTZ_TO_WEI / 5); + } + ``` + + This function uses the utility functions to set the current exchange rate of 5 XTZ to 1 USD. + Then it calls the contract's `updateAndGet` function. + Finally, the test verifies that the response from that function matches the price that it set in Pyth. + + This is a simple test, but it verifies that the contract can get accurate information from Pyth and return it in a useful manner to callers. + +1. After the `testUpdateAndGet` function, add this test function: + + ```solidity + // Test that the transaction fails with stale data + function testStaleData() public { + int64 xtzPrice = 10; + setXtzPrice(xtzPrice); + bytes[] memory updateData = createXtzUpdate(xtzPrice); + uint updateFee = pyth.getUpdateFee(updateData); + vm.deal(address(this), updateFee); + + // Wait until the data is stale + skip(120); + + // Expect the update to fail with stale data + vm.expectRevert(); + myContract.getPrice(); + } + ``` + + Of course, it's important to test failure cases, so this test does the same thing as the previous test but waits 2 minutes so the data goes stale. + The command `vm.expectRevert();` assumes that the following call to the contract's `getPrice` function will fail. + This way, if the contract allows the test to request stale data, the test fails. + +1. Run this command to run the test: + + ```bash + forge test + ``` + + The result from the tests should show that it ran the test functions successfully, as in this example: + + ``` + Ran 2 tests for test/TutorialContract.t.sol:ContractToTest + [PASS] testStaleData() (gas: 175874) + [PASS] testUpdateAndGet() (gas: 203736) + Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.07ms (655.13µs CPU time) + + Ran 1 test suite in 146.43ms (2.07ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests) + ``` + +If your tests have errors, make sure that the test matches the following code and that the paths in the test (such as the path to your contract) are correct: + +```solidity +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import { Test, console2 } from "forge-std/Test.sol"; +import { TutorialContract } from "../src/TutorialContract.sol"; +import { MockPyth } from "@pythnetwork/pyth-sdk-solidity/MockPyth.sol"; + +contract ContractToTest is Test { + MockPyth public pyth; + bytes32 XTZ_PRICE_FEED_ID = bytes32(uint256(0x1)); + TutorialContract public myContract; + + uint256 XTZ_TO_WEI = 10 ** 18; + + function setUp() public { + pyth = new MockPyth(60, 1); + myContract = new TutorialContract(address(pyth), XTZ_PRICE_FEED_ID); + } + + // Utility function to create a mocked Pyth price update for the test + function createXtzUpdate( + int64 xtzPrice + ) private view returns (bytes[] memory) { + bytes[] memory updateData = new bytes[](1); + updateData[0] = pyth.createPriceFeedUpdateData( + XTZ_PRICE_FEED_ID, + xtzPrice * 100000, // price + 10 * 100000, // confidence + -5, // exponent + xtzPrice * 100000, // emaPrice + 10 * 100000, // emaConfidence + uint64(block.timestamp), // publishTime + uint64(block.timestamp) // prevPublishTime + ); + + return updateData; + } + + // Utility function to set the Pyth price + function setXtzPrice(int64 xtzPrice) private { + bytes[] memory updateData = createXtzUpdate(xtzPrice); + uint updateFee = pyth.getUpdateFee(updateData); + vm.deal(address(this), updateFee); + pyth.updatePriceFeeds{ value: updateFee }(updateData); + } + + // Set the price that 5 XTZ = 1 USD and verify + function testUpdateAndGet() public { + // Set price + int64 xtzPrice = 5; + setXtzPrice(xtzPrice); + + // Call the updateAndGet function and send enough for the Pyth fee + bytes[] memory updateData = createXtzUpdate(xtzPrice); + uint updateFee = pyth.getUpdateFee(updateData); + vm.deal(address(this), updateFee); + + // Verify that the contract has the same exchange rate for XTZ/USD + uint256 priceWei = myContract.updateAndGet{ value: updateFee }(updateData); + assertEq(priceWei, XTZ_TO_WEI / 5); + } + + // Test that the transaction fails with stale data + function testStaleData() public { + int64 xtzPrice = 10; + setXtzPrice(xtzPrice); + bytes[] memory updateData = createXtzUpdate(xtzPrice); + uint updateFee = pyth.getUpdateFee(updateData); + vm.deal(address(this), updateFee); + + // Wait until the data is stale + skip(120); + + // Expect the update to fail with stale data + vm.expectRevert(); + myContract.getPrice(); + } +} +``` + +Now you know that the contract works and you can try deploying it to the Etherlink sandbox. + +## Deploying to Etherlink + +Foundry has built-in commands to deploy and call smart contracts, so in this section you use Foundry to deploy your contract and call it from the command line. + +1. Ensure that your EVM node is still running as described in [Part 1: Setting up a development environment](/tutorials/oracles/environment). + +1. Make sure that the contract is compiled by running this command: + + ```bash + forge build + ``` + + The `forge test` command automatically compiles the contract, but before deploying you should be sure that you have compiled the current source code. + +1. Make sure that these environment variables are set: + + - `ADDRESS`: The address of the account that you created with the `cast wallet new` command and funded in the Etherlink sandbox + - `PRIVATE_KEY`: The private key of the account + - `RPC_URL`: The address of the sandbox node, by default `http://localhost:8545` + +1. Set the `XTZ_USD_ID` environment variable to the Pyth ID of the XTZ/USD exchange rate. +These price feeds are listed at https://www.pyth.network/developers/price-feed-ids, where you can see that the price feed ID for XTZ/USD is: + + ``` + 0x0affd4b8ad136a21d79bc82450a325ee12ff55a235abc242666e423b8bcffd03 + ``` + +1. Set the `PYTH_OP_ETHERLINK_TESTNET_ADDRESS` environment variable to the address of the Pyth on-chain application on Etherlink Testnet. +The addresses of Pyth applications are listed at https://docs.pyth.network/price-feeds/contract-addresses/evm, where you can see that the Pyth application is deployed on both Etherlink Testnet and Etherlink Mainnet at this address: + + ``` + 0x2880aB155794e7179c9eE2e38200202908C17B43 + ``` + +1. Using these environment variables, deploy the contract to the local sandbox by running this command: + + ```bash + forge create src/TutorialContract.sol:TutorialContract \ + --private-key $PRIVATE_KEY \ + --rpc-url $RPC_URL \ + --broadcast \ + --constructor-args $PYTH_OP_ETHERLINK_TESTNET_ADDRESS $XTZ_USD_ID + ``` + + This Foundry command deploys the contract and returns the address of the deployed contract and the hash of the deployment transaction. + If you get errors, verify that the path to the contract and name of the contract are correct. + Also check that the environment variables are set to the correct values. + +1. Set the `DEPLOYMENT_ADDRESS` environment variable to the address of the deployed contract. + +1. Call the contract by getting update data from Hermes, sending it and the update fee to the `updateAndGet` function, and then calling the `getPrice` function, as described in the next steps. + + Because the price data goes stale after 60 seconds, you need to run these commands within 60 seconds. + You can put them in a single shell script to run at once or you can copy and paste them quickly. + + 1. Get the price update data from Hermes by running this command: + + ```bash + curl -s "https://hermes.pyth.network/v2/updates/price/latest?&ids[]=$XTZ_USD_ID" | jq -r ".binary.data[0]" > price_update.txt + ``` + + 1. Send the price update data and some XTZ for the update fees by running this command: + + ```bash + cast send \ + --private-key $PRIVATE_KEY \ + --rpc-url $RPC_URL \ + -j 1 \ + --value 0.0005ether \ + $DEPLOYMENT_ADDRESS \ + "updateAndGet(bytes[])" \ + "[0x`cat price_update.txt`]" + ``` + + This command includes a small amount of XTZ to pay the fee. + The command refers to it as `ether` but really it means the native token of the chain, in this case Etherlink XTZ. + + The response to the command includes information about the transaction but not the price data. + + 1. Retrieve the price from the contract by running this command: + + ```bash + cast call \ + --private-key $PRIVATE_KEY \ + --rpc-url $RPC_URL \ + -j 1 \ + $DEPLOYMENT_ADDRESS \ + "getPrice()" + ``` + + This command calls the read-only function `getPrice` and returns the current data in the `pyth` object in the contract. + It returns the amount of XTZ that equal one USD. + + For example, assume that the response is `0x0000000000000000000000000000000000000000000000001950f85eb8a92984`. + This hex number corresponds to 1824230934793759108 wei, or about 1.82 XTZ. + This means that 1.82 XTZ equals 1 USD, so one XTZ is equal to 1 / 1.82 USD, or about 0.55 USD. + You can paste the hex number that you get in response to a hex to decimal converter such as https://www.rapidtables.com/convert/number/hex-to-decimal.html. + +If the commands failed, verify that the `curl` command to get the Hermes data succeeds; it should write a long string of hex code to the file `price_update.txt`. +Also make sure that the environment variables are correct and that you are copying and pasting the commands into your terminal correctly. + +Now you have a smart contract that can get up-to-date price information from Pyth. +In the next section, you expand the smart contract to buy and sell based on that information. +Continue to [Part 3: Using price data to buy and sell tokens](/tutorials/oracles/tokens). diff --git a/docs/tutorials/oracles/index.md b/docs/tutorials/oracles/index.md new file mode 100644 index 00000000..b1b9e2ea --- /dev/null +++ b/docs/tutorials/oracles/index.md @@ -0,0 +1,38 @@ +--- +title: "Tutorial: Use the Pyth oracle for DeFi applications on Etherlink" +--- + +Etherlink is great for DeFi applications because of its low latency, fast confirmation times, and low fees. +This tutorial shows you how to set up a simple application that gets market information from [Pyth](https://www.pyth.network/) and makes trading decisions based on those prices. + +Pyth is a network of oracles, which provide information about currency prices and other market data to smart contracts. +As described in [Oracles](https://docs.tezos.com/smart-contracts/oracles) on docs.tezos.com, smart contracts cannot call external APIs, so they depend on oracles for information about the world outside the blockchain. +Smart contracts must call oracles in a specific way and pay fees to use them. + +This tutorial is an adaptation of this Pyth tutorial for Etherlink and its sandbox development environment: +https://docs.pyth.network/price-feeds/create-your-first-pyth-app/evm/part-1 + +## Learning objectives + +In this tutorial, you learn how to: + +- Start and use the Etherlink sandbox environment +- Call the Pyth DeFi oracle +- Use the Foundry toolkit to build, test, and deploy a smart contract to Etherlink +- Call an oracle from a smart contract +- Call the smart contract from an off-chain application +- Make basic buy and sell decisions based on the information from the oracle + +## Tutorial application + +The application that you create in this tutorial has an on-chain component in the form of a smart contract deployed to Etherlink. +It also has an off-chain component in the form of a Node.JS application that calls the smart contract. + +The code for the completed application is in this GitHub repository: https://github.com/trilitech/tutorial-applications/tree/main/etherlink-defi. + +## Tutorial sections + +- [Part 1: Setting up a development environment](/tutorials/oracles/environment) +- [Part 2: Getting information from the Pyth oracle](/tutorials/oracles/get_data) +- [Part 3: Using price data to buy and sell tokens](/tutorials/oracles/tokens) +- [Part 4: Automating pricing decisions](/tutorials/oracles/application) diff --git a/docs/tutorials/oracles/tokens.md b/docs/tutorials/oracles/tokens.md new file mode 100644 index 00000000..fcbecb7f --- /dev/null +++ b/docs/tutorials/oracles/tokens.md @@ -0,0 +1,309 @@ +--- +title: "Part 3: Using price data to buy and sell tokens" +--- + +Now that the smart contract has current data about prices, you can use that data to buy and sell tokens. +In this section, you expand the contract to simulate a decentralized exchange (DEX) that buys and sells tokens. +Specifically, you add a ledger of token owners to the contract to simulate a token, and `buy` and `sell` functions that allow users to buy and sell a token based on the current price of USD. + +## Adding buy and sell functions + +To simulate a token in a very simple way, the contract needs a ledger of token owners and functions to buy and sell that token. +Of course, this token is not compliant with any token standard, so it's not a good example of a token, but it's enough to simulate buying and selling for the purposes of the tutorial. + +1. Near the top of the `src/TutorialContract.sol` file, next to the `pyth` and `xtzUsdPriceId` storage variables, add a map for the token owners: + + ```solidity + mapping(address => uint256) balances; + ``` + + This map associates addresses of owners to the number of tokens they own. + +1. After the other functions in the contract, add this function to simulate buying one token: + + ```solidity + // Buy function: increments sender's balance by 1 + function buy(bytes[] calldata pythPriceUpdate) external payable { + + // Update price + updatePrice(pythPriceUpdate); + uint256 oneDollarInWei = getPrice(); + + // Require 1 USD worth of XTZ + if (msg.value >= oneDollarInWei) { + balances[msg.sender] += 1; + } else { + revert InsufficientFee(); + } + } + ``` + + This function accepts the price update data and passes it to the `updateAndGet` function that you created in the previous section. + Then it verifies that the user sent the correct amount of XTZ based on the updated price data. + If so, it increments the sender's balance by one token. + +1. After the `buy` function, add this function to simulate selling one token: + + ```solidity + // Sell function: decrements sender's balance by 1 + function sell(bytes[] calldata pythPriceUpdate) external { + require(getBalance(msg.sender) > 0, "Insufficient balance to sell"); + updatePrice(pythPriceUpdate); + uint256 oneDollarInWei = getPrice(); + + // Send the user 1 USD worth of XTZ + require(address(this).balance > oneDollarInWei, "Not enough XTZ to send"); + (bool sent, ) = msg.sender.call{value: oneDollarInWei}(""); + require(sent, "Failed to send XTZ"); + balances[msg.sender] -= 1; + } + ``` + + This function updates the price in the same way that the other functions do. + It decrements the sender's balance by one token and sends them one USD in XTZ. + Of course, this contract isn't actually buying and selling tokens through a DEX, so when you deploy the contract, you will include enough sandbox XTZ for it to pay for these sell operations. + +1. After the `sell` function, add this function to initialize a user's account with 5 simulated tokens: + + ```solidity + // Initialize accounts with 5 tokens for the sake of the tutorial + function initAccount(address user) external { + require(balances[msg.sender] < 5, "You already have at least 5 tokens"); + balances[user] = 5; + } + ``` + + This function simplifies the buy and sell process that you will set up later by giving the user 5 tokens for free to start. + This function is also only for the tutorial and would not be in a real contract. + +1. After the `initAccount` function, add this function to get an address's current balance of the simulated token: + + ```solidity + function getBalance(address user) public view returns (uint256) { + return balances[user]; + } + ``` + +1. After the functions, add this declaration of the error that the contract throws if the user does not send enough XTZ: + + ```solidity + // Error raised if the payment is not sufficient + error InsufficientFee(); + ``` + +The complete contract looks like this: + +```solidity +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; + +contract TutorialContract { + IPyth pyth; + bytes32 xtzUsdPriceId; + mapping(address => uint256) balances; + + constructor(address _pyth, bytes32 _xtzUsdPriceId) { + pyth = IPyth(_pyth); + xtzUsdPriceId = _xtzUsdPriceId; + } + + // Update the price + function updatePrice(bytes[] calldata pythPriceUpdate) public { + uint updateFee = pyth.getUpdateFee(pythPriceUpdate); + pyth.updatePriceFeeds{ value: updateFee }(pythPriceUpdate); + } + + // Get 1 USD in wei + function getPrice() public view returns (uint256) { + PythStructs.Price memory price = pyth.getPriceNoOlderThan( + xtzUsdPriceId, + 60 + ); + uint xtzPrice18Decimals = (uint(uint64(price.price)) * (10 ** 18)) / + (10 ** uint8(uint32(-1 * price.expo))); + uint oneDollarInWei = ((10 ** 18) * (10 ** 18)) / xtzPrice18Decimals; + return oneDollarInWei; + } + + // Update and get the price in a single step + function updateAndGet(bytes[] calldata pythPriceUpdate) external payable returns (uint256) { + updatePrice((pythPriceUpdate)); + return getPrice(); + } + + // Buy function: increments sender's balance by 1 + function buy(bytes[] calldata pythPriceUpdate) external payable { + + // Update price + updatePrice(pythPriceUpdate); + uint256 oneDollarInWei = getPrice(); + + // Require 1 USD worth of XTZ + if (msg.value >= oneDollarInWei) { + balances[msg.sender] += 1; + } else { + revert InsufficientFee(); + } + } + + // Sell function: decrements sender's balance by 1 + function sell(bytes[] calldata pythPriceUpdate) external { + require(getBalance(msg.sender) > 0, "Insufficient balance to sell"); + updatePrice(pythPriceUpdate); + uint256 oneDollarInWei = getPrice(); + + // Send the user 1 USD worth of XTZ + require(address(this).balance > oneDollarInWei, "Not enough XTZ to send"); + (bool sent, ) = msg.sender.call{value: oneDollarInWei}(""); + require(sent, "Failed to send XTZ"); + balances[msg.sender] -= 1; + } + + // Initialize accounts with 5 tokens for the sake of the tutorial + function initAccount(address user) external { + require(balances[msg.sender] < 5, "You already have at least 5 tokens"); + balances[user] = 5; + } + + function getBalance(address user) public view returns (uint256) { + return balances[user]; + } + + // Error raised if the payment is not sufficient + error InsufficientFee(); +} +``` + +Of course, you could customize these `buy` and `sell` functions to allow users to buy and sell more than one token at a time, but this is enough to demonstrate that the contract pins the price of tokens to one USD in XTZ. + +## Testing the buy and sell functions + +You could test these new functions in many ways, but in these steps you add a simple test and run it to be sure that the new functions work. + +1. Add this test function after the other functions in the file `test/TutorialContract.t.sol`: + + ```solidity + // Test a full buy/sell scenario + function testContract() public { + bytes[] memory updateData = createXtzUpdate(10); + + // Set up a test user + address testUser = address(0x5E11E1); + vm.deal(testUser, XTZ_TO_WEI); + vm.startPrank(testUser); + + // Test buying and selling + myContract.initAccount(testUser); + myContract.buy{ value: XTZ_TO_WEI / 10 }(updateData); + myContract.buy{ value: XTZ_TO_WEI / 10 }(updateData); + assertEq(7, myContract.getBalance(testUser)); + myContract.sell(updateData); + assertEq(6, myContract.getBalance(testUser)); + } + ``` + +1. Compile and test the contract by running this command: + + ```bash + forge test + ``` + + If you see any test failures, make sure your contract and test match the code above. + +1. Deploy the new contract to the sandbox by following these steps: + + 1. Make sure that these environment variables are set: + + - `ADDRESS`: The address of the account that you created with the `cast wallet new` command and funded in the Etherlink sandbox + - `PRIVATE_KEY`: The private key of the account + - `RPC_URL`: The address of the sandbox node, by default `http://localhost:8545` + - `XTZ_USD_ID`: the Pyth ID of the XTZ/USD exchange rate: `0x0affd4b8ad136a21d79bc82450a325ee12ff55a235abc242666e423b8bcffd03` + - `PYTH_OP_ETHERLINK_TESTNET_ADDRESS`: `0x2880aB155794e7179c9eE2e38200202908C17B43` for Etherlink Testnet + + 1. Using these environment variables, deploy the contract to the local sandbox by running this command: + + ```bash + forge create src/TutorialContract.sol:TutorialContract \ + --private-key $PRIVATE_KEY \ + --rpc-url $RPC_URL \ + --broadcast \ + --constructor-args $PYTH_OP_ETHERLINK_TESTNET_ADDRESS $XTZ_USD_ID \ + --value 100ether + ``` + + Like the `cast send` command that you used to send XTZ in the previous section, this command includes a `--value` argument to fund the contract with some XTZ so it can pay when a user sells the simulated token. + +1. Set the `DEPLOYMENT_ADDRESS` environment variable to the address of the deployed contract. + +1. (Optional) As you did in the previous section, call the contract from the command line by getting the price data from Hermes: + + 1. Get the price update data from Hermes by running this command: + + ```bash + curl -s "https://hermes.pyth.network/v2/updates/price/latest?&ids[]=$XTZ_USD_ID" | jq -r ".binary.data[0]" > price_update.txt + ``` + + 1. Send the price update data by running this command: + + ```bash + cast send \ + --private-key $PRIVATE_KEY \ + --rpc-url $RPC_URL \ + -j 1 \ + --value 0.0005ether \ + $DEPLOYMENT_ADDRESS \ + "updateAndGet(bytes[])" \ + "[0x`cat price_update.txt`]" + ``` + + 1. Retrieve the price from the contract by running this command: + + ```bash + cast call \ + --private-key $PRIVATE_KEY \ + --rpc-url $RPC_URL \ + -j 1 \ + $DEPLOYMENT_ADDRESS \ + "getPrice()" + ``` + + 1. Convert the hex number in the response to an amount of XTZ. + For example, you can paste the hex number that you get in response to a hex to decimal converter such as https://www.rapidtables.com/convert/number/hex-to-decimal.html. + + For example, if the response is `0x0000000000000000000000000000000000000000000000001950f85eb8a92984`, it corresponds to 1824230934793759108 wei, or about 1.82 XTZ. + You can use a converter such as https://eth-converter.com/ to convert wei to the primary token. + + 1. Send that amount of XTZ to the contract's `buy` function, as in this example, which rounds up to 1.85 XTZ for safety: + + ```bash + cast send \ + --private-key $PRIVATE_KEY \ + --rpc-url $RPC_URL \ + -j 1 \ + --value 1.85ether \ + $DEPLOYMENT_ADDRESS \ + "buy(bytes[])" \ + "[0x`cat price_update.txt`]" + ``` + + 1. If the previous command succeeded, check your balance of tokens by calling the `getBalance` function: + + ```bash + cast call \ + --private-key $PRIVATE_KEY \ + --rpc-url $RPC_URL \ + -j 1 \ + $DEPLOYMENT_ADDRESS \ + "getBalance(address)" \ + "$ADDRESS" + ``` + + This command should return `0x0000000000000000000000000000000000000000000000000000000000000001`, representing the one simulated token that you bought. + + If you can't run all of the commands before the price goes stale, don't worry, because in the next section you write a program to automate the process. + +Now you know that the contract can get price data from the Pyth oracle and use that data to make pricing decisions. +From here, you can expand the contract to handle multiple currencies or do other things with the price data. +Continue to [Part 4: Automating pricing decisions](/tutorials/oracles/application). diff --git a/sidebars.js b/sidebars.js index 02c8b33d..064a7125 100644 --- a/sidebars.js +++ b/sidebars.js @@ -42,7 +42,7 @@ const sidebars = { }, { type: 'category', - label: 'Tutorial', + label: 'Tutorial: Smart contract', collapsed: false, items: [ 'tutorials/marketpulse/index', @@ -54,6 +54,18 @@ const sidebars = { 'tutorials/marketpulse/cicd', ], }, + { + type: 'category', + label: 'Tutorial: DeFi and oracles', + collapsed: false, + items: [ + 'tutorials/oracles/index', + 'tutorials/oracles/environment', + 'tutorials/oracles/get_data', + 'tutorials/oracles/tokens', + 'tutorials/oracles/application', + ], + }, { type: 'category', label: 'Bridging', diff --git a/src/theme/DocSidebarItem/Category/index.tsx b/src/theme/DocSidebarItem/Category/index.tsx index 095ef56c..2545eb0a 100644 --- a/src/theme/DocSidebarItem/Category/index.tsx +++ b/src/theme/DocSidebarItem/Category/index.tsx @@ -104,7 +104,7 @@ function CollapseButton({ ); } -const ITEMICONS = ['/img/FiHome.svg', '/img/FiBookOpen.svg', '/img/BiSortAlt2.svg', '/img/FiBox.svg', '/img/FiWifi.svg', '/img/FiSettings.svg', '/img/FiUsers.svg', '/img/FiTrendingUp.svg', '/img/FiBookOpen.svg'] +const ITEMICONS = ['/img/FiHome.svg', '/img/FiBookOpen.svg', '/img/FiBookOpen.svg', '/img/BiSortAlt2.svg', '/img/FiBox.svg', '/img/FiWifi.svg', '/img/FiSettings.svg', '/img/FiUsers.svg', '/img/FiTrendingUp.svg', '/img/FiBookOpen.svg'] export default function DocSidebarItemCategory({ item,