Execute Transaction
This page describes how to build and execute the on-chain transaction after getting a quote.
Overview
Execution uses EIP-712 signatures. The caller (your aggregator) builds the transaction data for StoveRouter.execute() itself — Stove's backend doesn't build or broadcast this transaction for you.
Step 1: Check Allowance
Before executing, confirm the user has approved enough of the relevant token to the StoveRouter contract.
| Trade type | Token to approve | Approval amount |
|---|---|---|
| Buy | USDT/USDC (assetToken) | amountInUsed |
| Sell | StockToken (stockToken) | amountInUsed |
import { ethers } from 'ethers';
const ERC20_ABI = [
'function allowance(address owner, address spender) view returns (uint256)'
];
async function checkAllowance(tokenAddress, owner, spender, requiredAmount) {
const provider = new ethers.BrowserProvider(window.ethereum);
const token = new ethers.Contract(tokenAddress, ERC20_ABI, provider);
const allowance = await token.allowance(owner, spender);
return allowance >= BigInt(requiredAmount);
}Step 2: Submit Approval (if needed)
Approval target (spender) is the StoveRouter contract address.
ethers.js
import { ethers } from 'ethers';
const ERC20_ABI = [
'function approve(address spender, uint256 amount) returns (bool)'
];
async function approveToken(approval, signer) {
const token = new ethers.Contract(approval.token_address, ERC20_ABI, signer);
const tx = await token.approve(approval.spender, approval.amount);
const receipt = await tx.wait();
return receipt;
}
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
for (const approval of quoteResponse.required_approvals) {
await approveToken(approval, signer);
}viem
import { createWalletClient, custom, parseAbi } from 'viem';
import { bscTestnet } from 'viem/chains';
const walletClient = createWalletClient({ chain: bscTestnet, transport: custom(window.ethereum) });
const [account] = await walletClient.getAddresses();
async function approveToken(approval: { token_address: string; spender: string; amount: string }) {
return walletClient.writeContract({
address: approval.token_address as `0x${string}`,
abi: parseAbi(['function approve(address spender, uint256 amount) returns (bool)']),
functionName: 'approve',
args: [approval.spender as `0x${string}`, BigInt(approval.amount)],
account
});
}
for (const approval of quoteResponse.required_approvals) {
await approveToken(approval);
}Step 3: Build the Transaction
Quote struct
struct Quote {
bytes32 quoteId;
string ticker;
uint16 exchange; // Exchange code — shared Exchange enum, see Precise Execution API Overview
bool isBuy;
uint256 chainId;
string amountType; // "stablecoin_in" or "stocktoken_in"
uint256 amountIn;
uint256 amountInUsed;
uint256 amountOut;
uint256 feeAmount;
address stockToken;
address assetToken;
uint256 price;
uint256 expiresAt;
}execute function
function execute(
Quote calldata quote,
bytes calldata signature,
address recipient
) external returns (uint256 amountOut)| Parameter | Type | Description |
|---|---|---|
| quote | Quote calldata | Quote data |
| signature | bytes calldata | EIP-712 signature |
| recipient | address | Recipient address (pass address(0) to default to msg.sender) |
Returns amountOut — share quantity for buys, asset token amount for sells.
Contract ABI
[
{
"inputs": [
{ "components": [
{ "name": "quoteId", "type": "bytes32" },
{ "name": "ticker", "type": "string" },
{ "name": "exchange", "type": "uint16" },
{ "name": "isBuy", "type": "bool" },
{ "name": "chainId", "type": "uint256" },
{ "name": "amountType", "type": "string" },
{ "name": "amountIn", "type": "uint256" },
{ "name": "amountInUsed", "type": "uint256" },
{ "name": "amountOut", "type": "uint256" },
{ "name": "feeAmount", "type": "uint256" },
{ "name": "stockToken", "type": "address" },
{ "name": "assetToken", "type": "address" },
{ "name": "price", "type": "uint256" },
{ "name": "expiresAt", "type": "uint256" }
], "name": "quote", "type": "tuple" },
{ "name": "signature", "type": "bytes" },
{ "name": "recipient", "type": "address" }
],
"name": "execute",
"outputs": [{ "name": "amountOut", "type": "uint256" }],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{ "name": "quoteId", "type": "bytes32" }],
"name": "isQuoteUsed",
"outputs": [{ "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "DOMAIN_SEPARATOR",
"outputs": [{ "name": "", "type": "bytes32" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{ "components": [
{ "name": "quoteId", "type": "bytes32" },
{ "name": "ticker", "type": "string" },
{ "name": "exchange", "type": "uint16" },
{ "name": "isBuy", "type": "bool" },
{ "name": "chainId", "type": "uint256" },
{ "name": "amountType", "type": "string" },
{ "name": "amountIn", "type": "uint256" },
{ "name": "amountInUsed", "type": "uint256" },
{ "name": "amountOut", "type": "uint256" },
{ "name": "feeAmount", "type": "uint256" },
{ "name": "stockToken", "type": "address" },
{ "name": "assetToken", "type": "address" },
{ "name": "price", "type": "uint256" },
{ "name": "expiresAt", "type": "uint256" }
], "name": "quote", "type": "tuple" }
],
"name": "getQuoteDigest",
"outputs": [{ "name": "", "type": "bytes32" }],
"stateMutability": "view",
"type": "function"
}
]EIP-712 Signature Verification
Domain separator: name StoveRouter, version 1, current chainId, verifyingContract = StoveRouter address.
Type hash:
keccak256(
"Quote(bytes32 quoteId,string ticker,uint16 exchange,bool isBuy,uint256 chainId,string amountType,uint256 amountIn,uint256 amountInUsed,uint256 amountOut,uint256 feeAmount,address stockToken,address assetToken,uint256 price,uint256 expiresAt)"
)Verification flow: compute quoteHash, then digest = keccak256("\x19\x01" || domainSeparator || quoteHash), recover the signer via ECDSA from digest and signature, and confirm it matches the authorized signer.
Step 4: Broadcast
ethers.js
import { ethers } from 'ethers';
const STOVE_ROUTER_ABI = [
'function execute((bytes32 quoteId, string ticker, uint16 exchange, bool isBuy, uint256 chainId, string amountType, uint256 amountIn, uint256 amountInUsed, uint256 amountOut, uint256 feeAmount, address stockToken, address assetToken, uint256 price, uint256 expiresAt) quote, bytes signature, address recipient) returns (uint256)'
];
async function executeRouterOrder(quoteResponse, stoveRouterAddress) {
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const userAddress = await signer.getAddress();
const stoveRouter = new ethers.Contract(stoveRouterAddress, STOVE_ROUTER_ABI, signer);
const tx = await stoveRouter.execute(quoteResponse.quote, quoteResponse.signature, userAddress);
const receipt = await tx.wait();
return receipt;
}Events
event BuyExecuted(
bytes32 indexed quoteId, address indexed caller, address indexed recipient,
string ticker, uint16 exchange, uint256 stockAmount, uint256 assetAmount,
uint256 feeAmount, uint256 expiresAt
);
event SellExecuted(
bytes32 indexed quoteId, address indexed caller, address indexed recipient,
string ticker, uint16 exchange, uint256 stockAmount, uint256 assetAmount,
uint256 feeAmount, uint256 expiresAt
);Contract Errors
| Error | Description |
|---|---|
InvalidSignature() | Signature invalid, or signer not authorized |
QuoteExpired() | Quote has expired |
QuoteAlreadyUsed() | Quote already used (replay protection) |
UnauthorizedCaller() | Calling contract not on the whitelist |
AssetTokenNotWhitelisted() | Asset token not whitelisted |
StockTokenNotRegistered() | StockToken not registered in the manager |
ChainIdMismatch() | Chain ID mismatch |
ZeroAddress() | Address is zero |
InvalidQuantity() | Quantity is zero |
Error Handling Example
try {
const tx = await stoveRouter.execute(quote, signature, recipient);
const receipt = await tx.wait();
console.log('Transaction successful:', receipt.hash);
} catch (error) {
if (error.message.includes('QuoteExpired')) {
console.error('Quote has expired — request a new quote');
} else if (error.message.includes('InvalidSignature')) {
console.error('Invalid signature');
} else if (error.message.includes('QuoteAlreadyUsed')) {
console.error('Quote has already been used');
} else if (error.message.includes('UnauthorizedCaller')) {
console.error('Caller is not authorized');
} else {
console.error('Transaction failed:', error.message);
}
}Notes
- Quote validity: 10 seconds — execute before it expires.
- Approval target: the
StoveRoutercontract. - Atomic execution: settles in a single transaction, no locking step.
- Replay protection: each
quoteIdcan only be used once. - Whitelist: enforced on mainnet only — testnet does not require the caller to be whitelisted.