Skip to content
Stove Website

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 typeToken to approveApproval amount
BuyUSDT/USDC (assetToken)amountInUsed
SellStockToken (stockToken)amountInUsed
javascript
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

javascript
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

typescript
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 ​

solidity
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 ​

solidity
function execute(
    Quote calldata quote,
    bytes calldata signature,
    address recipient
) external returns (uint256 amountOut)
ParameterTypeDescription
quoteQuote calldataQuote data
signaturebytes calldataEIP-712 signature
recipientaddressRecipient address (pass address(0) to default to msg.sender)

Returns amountOut — share quantity for buys, asset token amount for sells.

Contract ABI ​

json
[
  {
    "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:

solidity
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

javascript
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 ​

solidity
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 ​

ErrorDescription
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 ​

javascript
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 StoveRouter contract.
  • Atomic execution: settles in a single transaction, no locking step.
  • Replay protection: each quoteId can only be used once.
  • Whitelist: enforced on mainnet only — testnet does not require the caller to be whitelisted.