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 EVM toolkit to interact with Etherlink.
-
In the same directory as your
contracts
folder, create a directory namedapp
to store your off-chain application. -
Go into the
app
folder and runnpm init -y
to initialize a Node.JS application. -
Run this command to install the Pyth and Viem dependencies:
npm add @pythnetwork/hermes-client @pythnetwork/price-service-client ts-node typescript viem
-
Run this command to initialize TypeScript:
tsc --init
-
In the
tsconfig.json
file, uncomment theresolveJsonModule
line soresolveJsonModule
is set totrue
. This setting allows programs to import JSON files easily. -
Also in the
tsconfig.json
file, set thetarget
field toES2020
. -
Create a file named
src/checkRate.ts
for the code of the application. -
In the file, import the dependencies:
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.
-
Add these constants to access the environment variables you set, or edit this code to hard-code the values:
// 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}`); -
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.// 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],
},
},
}); -
Add these Viem objects that represent the wallet and chain so you can access them in code later:
// 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()
}); -
Add these constants, which you can change later to adjust how the program works:
// 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; -
Add these utility functions:
// 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)); -
Add this function to get current price update data from Hermes, just like the
curl
command you used in previous sections:// 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.
-
Add this utility function to check the price repeatedly and return the new price when it has changed above a given threshold:
// Get the baseline price and poll until it changes past the threshold
const alertOnPriceFluctuations = async (_baselinePrice: number, connection: HermesClient): Promise<number> => {
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;
} -
Add a
run
function to contain the main logic of the application:const run = async () => {
// Logic goes here
}
run(); -
Replace the
// Logic goes here
comment with this code, which checks your account and calls the contract'sinitAccount
function if necessary to give you some simulated tokens to start with:// 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);
} -
After that code, add this code to create the connection to the Hermes client:
const connection = new HermesClient("https://hermes.pyth.network");
-
Add this loop, which iterates a certain number of times or until the account runs out of tokens:
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:
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<number> => {
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.