Working with Oracle Data¶
Now that we know how much POL are in the smart wallet, and we can send a simple tranaction, let's take care of working with the actual Oracle data.
Before we deep dive into sending a Data-Dependent transaction, we need to take care of two things:
- We need to know how much POL we need to send for an Ounce of Silver
- We need to know how much it costs to query this data
Previewing Oracle Data¶
To preview data, we're going to create a new Hook. This hook will query the Bundler Preview RPC endpoint and poll for pricing updates.
Add the following file to your project in src/utils/fetchPreviewData.ts
import { unescape } from "querystring";
import { useEffect, useState } from "react";
type MorpherPricePreview = {
timestampInMilis: number;
priceDecimal: number;
value: number;
}
export function usePreviewAPIPolling(feedId: string, timeout: number): MorpherPricePreview | undefined {
const [data, setData] = useState<MorpherPricePreview | undefined>(undefined);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://oracle-bundler.morpher.com/rpc', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
'jsonrpc': '2.0',
'method': 'eth_oracleDataPreview',
'params': [
feedId
],
'id': 1
})
})
const json = await response.json();
console.log(json.result)
console.log(decodeHexString(json.result));
setData(decodeHexString(json.result))
} catch (error) {
console.error(error);
}
};
fetchData();
const intervalId = setInterval(fetchData, timeout);
// Clear the interval on unmount
return () => clearInterval(intervalId);
}, [feedId, delay]);
return data;
}
function decodeHexString(hexString: `0x{string}`): MorpherPricePreview {
// Ensure the input is a 64-character hex string (32 bytes)
if (hexString.length !== 64 && hexString.length !== 66) {
throw new Error("Input should be a 32-byte (64-character) hex string. Given " + hexString);
}
const startChar = hexString.length === 64 ? 0 : 2;
// Step 1: Extract the first 6 bytes for the timestamp (first 12 hex characters)
const timestampHex = hexString.slice(0 + startChar, 12 + startChar); // First 6 bytes
const timestamp = parseInt(`0x${timestampHex}`, 16); // Convert to a number in milliseconds
// Step 2: Extract the 7th byte for the price decimal (next 2 hex characters)
const priceDecimalHex = hexString.slice(12 + startChar, 14 + startChar); // 7th byte
const priceDecimal = parseInt(priceDecimalHex, 16); // Should always be 18 (0x12)
// Step 3: Extract the price value from the last 25 bytes (50 hex characters)
const priceValueHex = hexString.slice(14 + startChar); // Remaining 50 hex characters
// Step 4: Split into integer part (first 14 hex chars) and decimal part (last 36 hex chars)
const priceIntegerHex = priceValueHex.slice(0, 14); // First 7 bytes (14 hex chars)
const priceDecimalHexPart = priceValueHex.slice(14); // Last 18 bytes (36 hex chars)
// Convert integer part from hex to number
const priceInteger = BigInt(`0x${priceIntegerHex}`);
// Convert decimal part from hex to a number
const priceDecimalPart = BigInt(`0x${priceDecimalHexPart}`);
// Step 5: Combine the price: integer part + (decimal part / 10^18)
const fullPrice = Number(priceInteger) + Number(priceDecimalPart) / 10 ** 18;
// Return the decoded values
return {
timestampInMilis: timestamp,
priceDecimal,
value: fullPrice
};
}
This hook will poll the API every X seconds and decode the hex encoded data returned by that API. In particular, the decodeHexString
function will treat the returned bytes32 value as a string and simply slice the string into:
- The first 6 bytes, or 12 hex characters for the timestamp
- The next 1 byte or 2 hex characters for the number of decimals (18 or 0x12)
- The next 25 bytes for the price.
And then it returns the decoded values we can further use.
Library
Please make sure to regularly check our documentation on oracle.morpher.com, we have a set of library functions on our roadmap so this will be available at some point directly from the dd-abstraction-kit npm package.
Now let's use this in our index.tsx file. We'll use the Hook and add a red button that reads "Mint one NFT (xx.xxxxx POL)" where xx.xxxx POL is the current price of Silver in POL tokens. Extend the index.tsx
file with this:
...
import { usePreviewApiPolling } from '../utils/fetchPreviewData';
const Home: NextPage = () => {
...
const pricePreviewPOL = usePreviewApiPolling("0x9a668d8b2069cae627ac3dff9136de03849d0742ff88edb112e7be6b4663b37d", 5000);
const pricePreviewXAG = usePreviewApiPolling("0xa77010d8a18857daea7ece96bedb40730ab5be50d8094d2bd8008926ca492844", 5000);
...
return (
...
<button className='text-white bg-gradient-to-r from-red-400 via-red-500 to-red-600 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2'>
Mint one NFT ({pricePreviewXAG && pricePreviewPOL ? pricePreviewXAG.value / pricePreviewPOL?.value : '...'} POL)
</button>
...
);
};
export default Home;
If you have added this to your app, then it should look something like this:
Calculating data costs¶
Next, we need to calculate how much it costs to actually query this data. We are querying two data points:
MORPHER:COMMODITY_XAG and MORPHER:CRYPTO_POL
Feed IDs should be update-able
Please make sure to take the right feed IDs from https://oracle.morpher.com/feeds and also make them update-able in your own smart contracts. The naming might change (e.g. Matic to Pol renaming), we sometimes need to deprecate pairs in favor of other pairs etc.
To query the prices for the Data points, we can query the Oracle Entrypoint Smart Contract on chain. There is a prices mapping which is public, so it becomes automatically a getter function. This function takes two parameters: The provider address and the feed ID. Every feed ID can have its own pricing information. Generally, the crypto feeds we provide are significantly cheaper than the other feeds, although we have no settled on an exact price yet and these are subjec to change depending on the usage of the oracle.
To query the prices, we can use the useReadContract hook from wagmi to do that. Let's add this to the index.tsx file:
const dataPriceXAG = useReadContract({
address: process.env.NEXT_PUBLIC_ORACLE_ADDRESS as `0x${string}`,
abi: PriceOracle.abi,
functionName: "prices",
args: [process.env.NEXT_PUBLIC_PROVIDER_ADDRESS as `0x${string}`, keccak256(Buffer.from('MORPHER:COMMODITY_XAG', 'utf-8'))]
})
And then add the dataPriceXAG to the Button Label, so we see how much we need to add in fees to the transaction value:
<button className='text-white bg-gradient-to-r from-red-400 via-red-500 to-red-600 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2'>
Mint one NFT ({pricePreviewXAG && pricePreviewPOL ? pricePreviewXAG.value / pricePreviewPOL?.value : '...'} POL + {dataPriceXAG.isFetched && parseFloat(formatEther(dataPriceXAG.data as bigint))} POL)
</button>
Data Price
In our example we simply add the data query pricing for commodity XAG. We neglect the pricing for the POL market. In a live app you need to add together all the fees for all data requests against the oracle.
If everything works, then the app should look like this:
Next we need to send the actual minting transaction.
Sending a Data-Dependent Transaction¶
The last part is sending a data-dependent transaction. In this section we create a mint function that is called onClick from the button. Inside that mint function, we have to take care of a few things:
- We have to create the calldata for
safeMint(address)
to mint a new NFT for a specific address - We need to calculate the value we need to send along to mint the NFT for the right price
- Create and sign the user operation
- Send the user operation to an Oracle enabled bundler
But first and foremost, we need to attach an onClick handler to our Button:
<button onClick={() => mintNft()} className='text-white bg-gradient-to-r from-red-400 via-red-500 to-red-600 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2'>
Mint one NFT ({pricePreviewXAG && pricePreviewPOL ? pricePreviewXAG.value / pricePreviewPOL?.value : '...'} POL + {dataPriceXAG.isFetched && parseFloat(formatEther(dataPriceXAG.data as bigint))} POL)
</button>
Then add the mint function to the body of the index.tsx file:
const Home: NextPage = () => {
...
async function mintNft() {
if(smartAccount == undefined || address == undefined) return;
//create the call data
//add the exact value plus some buffer
//that is XAG/POL + data cost
//create the safeMint transaction
//create the user operation
//sign the user operation
//convert the signature to an EIP712 user op signature
//send it and console log the user operation hash which can be seen on jiffyscan later
}
...
Let's take care of the first part, the calldata. The calldata for the transaction is the ABI encoded call to safeMint(address)
with the address attached. We can either calculate this by hand, or use getFunctionSelector
and createCallData
from the dd-abstraction-kit:
async function mintNft() {
if(smartAccount == undefined || address == undefined) return;
//create the call data
const mintFunctionSelector = getFunctionSelector("safeMint(address)");
const mintTransactionCallData = createCallData(
mintFunctionSelector,
["address"],
[smartAccount.accountAddress]
); //<<-- we mint for the smart account an NFT
...
This will create a transcation data field to the NFT contract to interact with the safeMint
function and mint one NFT for the smartAccount account address. But the safeMint function expects value for one Ounce of Silver in POL plus the data fees, how to calculate these? Very similar as we did already for the Button-Label:
//add the exact value plus some buffer
//that is XAG/POL + data cost
let value = pricePreviewPOL !== undefined && pricePreviewXAG !== undefined ? (pricePreviewXAG.value / pricePreviewPOL.value) : 0;
value += parseFloat(formatEther(dataPriceXAG.data as bigint));
Data Price
Please be sure to add data prices for all the data you query. In our case we add enough buffer that the transaction goes through and the crypto fee is negligibly small.
Then we create the transaction that needs to be sent:
//create the safeMint transaction
const mintTransaction = {
to: process.env.NEXT_PUBLIC_TOKEN_ADDRESS!,
value: BigInt(value * 1.1 * 1e18),
data: mintTransactionCallData
};
Next we're going to create the User Operation using the createUserOperation
function:
//create the user operation
const mintUserOp = await smartAccount.createUserOperation(
[mintTransaction],
publicClient?.transport.url,
process.env.NEXT_PUBLIC_BUNDLER_RPC!,
)
This function needs an RPC endpoint to estimate the gas. In our case we can use the publicClient hook from Wagmi to get a public client. You can also use any RPC endpoint either a dedicated hosted one (Infura, Chainstack, Alchemy, etc). Let's add the public client to our index.tsx file:
...
import { useAccount, useBalance, useChainId, usePublicClient, useReadContract, useSendTransaction, useWalletClient } from 'wagmi';
...
const Home: NextPage = () => {
...
const publicClient = usePublicClient();
async function mintNft() {
if(smartAccount == undefined || address == undefined) return;
...
//create the user operation
const mintUserOp = await smartAccount.createUserOperation(
[mintTransaction],
publicClient?.transport.url,
process.env.NEXT_PUBLIC_BUNDLER_RPC!,
)
...
The next part is to sign the transaction. For this we need to create the domain, add in the types for the V6 Entrpoint and deconstruct the UserOperation. We also need the chainID hook and the walletClient hook to get access to signTypedData
:
...
import { useAccount, useBalance, useChainId, usePublicClient, useReadContract, useSendTransaction, useWalletClient } from 'wagmi';
import { createCallData, EIP712_SAFE_OPERATION_V6_TYPE, getFunctionSelector, SafeAccountV0_2_0 as SafeAccount } from '@morpher-io/dd-abstractionkit';
const Home: NextPage = () => {
...
const publicClient = usePublicClient();
const walletClient = useWalletClient();
const chainId = useChainId();
async function mintNft() {
if(smartAccount == undefined || address == undefined) return;
...
//create the user operation
const mintUserOp = await smartAccount.createUserOperation(
[mintTransaction],
publicClient?.transport.url,
process.env.NEXT_PUBLIC_BUNDLER_RPC!,
)
//sign the user operation
const domain = {
chainId,
verifyingContract: smartAccount.safe4337ModuleAddress as `0x${string}`
}
const types = EIP712_SAFE_OPERATION_V6_TYPE;
const {sender, ...userOp} = mintUserOp;
const safeUserOperation = {
...userOp,
safe: mintUserOp.sender,
validUntil: BigInt(0),
validAfter: BigInt(0),
entryPoint: smartAccount.entrypointAddress
};
const signature = await walletClient.data?.signTypedData({
domain,
types,
primaryType: 'SafeOp',
message: safeUserOperation
});
...
If you followed until here, then you can press the mint button and metamask (or whatever wallet you use) should pop up and ask you to sign:
Now we need to convert that signature into an EIP712 signature for the UserOperation:
//convert the signature to an EIP712 user op signature
mintUserOp.signature = SafeAccount.formatEip712SignaturesToUseroperationSignature([address], [signature as string]);
And then we can send it off and console log the UserOperationHash:
//send it and console log the user operation hash which can be seen on jiffyscan later
const userOpResponse = await smartAccount.sendUserOperation(mintUserOp, process.env.NEXT_PUBLIC_BUNDLER_RPC!);
console.log(userOpResponse.userOperationHash);
Here is again the full mint function for reference:
const publicClient = usePublicClient();
const walletClient = useWalletClient();
const chainId = useChainId();
async function mintNft() {
if(smartAccount == undefined || address == undefined) return;
//create the call data
const mintFunctionSelector = getFunctionSelector("safeMint(address)");
const mintTransactionCallData = createCallData(
mintFunctionSelector,
["address"],
[smartAccount.accountAddress]
); //<<-- we mint for the smart account an NFT
//add the exact value plus some buffer
//that is XAG/POL + data cost
let value = pricePreviewPOL !== undefined && pricePreviewXAG !== undefined ? (pricePreviewXAG.value / pricePreviewPOL.value) : 0;
value += parseFloat(formatEther(dataPriceXAG.data as bigint));
//create the safeMint transaction
const mintTransaction = {
to: process.env.NEXT_PUBLIC_TOKEN_ADDRESS!,
value: BigInt(value * 1.1 * 1e18),
data: mintTransactionCallData
};
//create the user operation
const mintUserOp = await smartAccount.createUserOperation(
[mintTransaction],
publicClient?.transport.url,
process.env.NEXT_PUBLIC_BUNDLER_RPC!,
)
//sign the user operation
const domain = {
chainId,
verifyingContract: smartAccount.safe4337ModuleAddress as `0x${string}`
}
const types = EIP712_SAFE_OPERATION_V6_TYPE;
const {sender, ...userOp} = mintUserOp;
const safeUserOperation = {
...userOp,
safe: mintUserOp.sender,
validUntil: BigInt(0),
validAfter: BigInt(0),
entryPoint: smartAccount.entrypointAddress
};
const signature = await walletClient.data?.signTypedData({
domain,
types,
primaryType: 'SafeOp',
message: safeUserOperation
});
console.log(signature);
//convert the signature to an EIP712 user op signature
mintUserOp.signature = SafeAccount.formatEip712SignaturesToUseroperationSignature([address], [signature as string]);
console.log(mintUserOp);
//send it and console log the user operation hash which can be seen on jiffyscan later
const userOpResponse = await smartAccount.sendUserOperation(mintUserOp, process.env.NEXT_PUBLIC_BUNDLER_RPC!);
console.log(userOpResponse.userOperationHash);
}
If you click the button, it should create a new safeMint UserOperation: