Skip to Content
Developer HubGuides & Tutorials

Adding Live. Step 5: Place a bet

Refer to the “Place a bet” and “Order errors” documentation to understand the process.

To place a live bet, you’ll need:

  • Ensure allowance & approve token spending for the liveRelayer contract address.
  • Prepare the order data.
  • Sign a typed message (EIP712) using the user wallet.
  • Send the order to the Azuro API.
  • Retrieve the order and transaction status from the Azuro API.

Take a look at the source code of useLiveBetFee

Get Relayer fee amount for live bets:

import { useQuery } from '@tanstack/react-query' import { useChain } from '../contexts/chain' import { getLiveBetFee } from '../utils/getLiveBetFee' import { type ChainId, environments, getApiUrl } from '../config' export type LiveBetFeeResponse = { gasLimit: number, gasPrice: number, betTokenRate: number, gasPriceInBetToken: number, slippage: number, gasAmount: number, relayerFeeAmount: string, beautyRelayerFeeAmount: string, symbol: string, decimals: number } export const getLiveBetFee = async (chainId: ChainId): Promise<LiveBetFeeResponse> => { const api = getApiUrl(chainId) const environment = environments[chainId] const response = await fetch(`${api}/orders/gas?environment=${environment}`) const data: LiveBetFeeResponse = await response.json() return data } type Props = { enabled?: boolean } export const useLiveBetFee = ({ enabled }: Props = { enabled: true }) => { const { appChain } = useChain() const queryFn = async () => { const { gasAmount, relayerFeeAmount, beautyRelayerFeeAmount, } = await getLiveBetFee(appChain.id) return { gasAmount: BigInt(gasAmount), relayerFeeAmount: BigInt(relayerFeeAmount), formattedRelayerFeeAmount: beautyRelayerFeeAmount, } } let { isFetching, data, refetch } = useQuery({ queryKey: [ '/live-bet-fee', appChain.id ], queryFn, enabled, refetchOnWindowFocus: false, refetchInterval: 10000, }) if (!enabled) { data = undefined } return { gasAmount: data?.gasAmount, relayerFeeAmount: data?.relayerFeeAmount, formattedRelayerFeeAmount: data?.formattedRelayerFeeAmount, refetch, loading: isFetching, } }
ℹ️

For more information check Relayer fee section.

Take a look at the source code of usePrepareBet which manages both prematch and live logic.

Here are the highlights of usePrepareBet for Live bets:

usePrepareBet.ts
import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt, usePublicClient, useWalletClient } from 'wagmi' import { parseUnits, encodeAbiParameters, parseAbiParameters, type Address, erc20Abi, type TransactionReceipt, type Hex, type TypedDataDomain } from 'viem' import { useState } from 'react' import { gnosis } from 'viem/chains' import { useChain } from '../contexts/chain' import { DEFAULT_DEADLINE, ODDS_DECIMALS, MAX_UINT_256, liveHostAddress, getApiUrl, liveBetAmount, environments } from '../config' import type { Selection } from '../global' import { useBetsCache } from './useBetsCache' import { useLiveBetFee } from './useLiveBetFee' enum LiveOrderState { Created = 'Created', Pending = 'Pending', Sent = 'Sent', Accepted = 'Accepted', Rejected = 'Rejected' } type LiveCreateOrderResponse = { id: string state: LiveOrderState errorMessage?: string } type LiveGetOrderResponse = { txHash: string odds: string betId: string } & LiveCreateOrderResponse type Props = { betAmount: string slippage: number affiliate: Address selections: Selection[] odds: Record<string, number> totalOdds: number liveEIP712Attention?: string deadline?: number onSuccess?(receipt?: TransactionReceipt): void onError?(err?: Error): void } export const usePrepareBet = (props: Props) => { const { betAmount, slippage, deadline, affiliate, selections, odds, totalOdds, liveEIP712Attention, onSuccess, onError } = props const isLiveBet = selections.some(({ coreAddress }) => coreAddress === liveHostAddress) const account = useAccount() const publicClient = usePublicClient() const walletClient = useWalletClient() const { appChain, contracts, betToken } = useChain() const { addBet } = useBetsCache() const { relayerFeeAmount: rawRelayerFeeAmount, formattedRelayerFeeAmount: relayerFeeAmount, loading: isRelayerFeeFetching, } = useLiveBetFee({ enabled: isLiveBet, }) const [ isLiveBetPending, setLiveBetPending ] = useState(false) const [ isLiveBetProcessing, setLiveBetProcessing ] = useState(false) const approveAddress = isLiveBet ? contracts.liveRelayer?.address : contracts.proxyFront.address const allowanceTx = useReadContract({ chainId: appChain.id, address: betToken.address, abi: erc20Abi, functionName: 'allowance', args: [ account.address!, approveAddress!, ], query: { enabled: Boolean(account.address) && Boolean(approveAddress), }, }) const approveTx = useWriteContract() const approveReceipt = useWaitForTransactionReceipt({ hash: approveTx.data, }) const isApproveRequired = Boolean( allowanceTx.data !== undefined && +betAmount && allowanceTx.data < parseUnits(isLiveBet ? String((+betAmount + +relayerFeeAmount!)) : betAmount, betToken.decimals) ) const approve = async () => { const hash = await approveTx.writeContractAsync({ address: betToken.address!, abi: erc20Abi, functionName: 'approve', args: [ approveAddress!, MAX_UINT_256, ], }) await publicClient!.waitForTransactionReceipt({ hash, }) allowanceTx.refetch() } const betTx = useWriteContract() const betReceipt = useWaitForTransactionReceipt({ hash: betTx.data, }) const placeBet = async () => { if (!totalOdds) { return } const fixedAmount = +parseFloat(String(betAmount)).toFixed(betToken.decimals) const rawAmount = parseUnits(`${fixedAmount}`, betToken.decimals) const minOdds = 1 + (+totalOdds - 1) * (100 - slippage) / 100 const fixedMinOdds = +parseFloat(String(minOdds)).toFixed(ODDS_DECIMALS) const rawMinOdds = parseUnits(`${fixedMinOdds}`, ODDS_DECIMALS) const rawDeadline = BigInt(Math.floor(Date.now() / 1000) + (deadline || DEFAULT_DEADLINE)) let txHash: Hex try { if (isLiveBet) { setLiveBetPending(true) const { conditionId, outcomeId } = selections[0]! const order = { bet: { attention: liveEIP712Attention || 'By signing this transaction, I agree to place a bet for a live event on \'Azuro SDK Example', affiliate, core: contracts.liveCore!.address, amount: String(rawAmount), chainId: appChain.id, conditionId: conditionId, outcomeId: +outcomeId, minOdds: String(rawMinOdds), nonce: String(Date.now()), expiresAt: Math.floor(Date.now() / 1000) + 2000, relayerFeeAmount: String(rawRelayerFeeAmount), }, } const EIP712Domain: TypedDataDomain = { name: 'Live Betting', version: '1.0.0', chainId: appChain.id, verifyingContract: contracts.liveCore!.address, } const clientBetDataTypes = { ClientBetData: [ { name: 'attention', type: 'string' }, { name: 'affiliate', type: 'address' }, { name: 'core', type: 'address' }, { name: 'amount', type: 'uint128' }, { name: 'nonce', type: 'uint256' }, { name: 'conditionId', type: 'uint256' }, { name: 'outcomeId', type: 'uint64' }, { name: 'minOdds', type: 'uint64' }, { name: 'expiresAt', type: 'uint256' }, { name: 'chainId', type: 'uint256' }, { name: 'relayerFeeAmount', type: 'uint256' }, ], } as const const signature = await walletClient!.data!.signTypedData({ account: account.address, domain: EIP712Domain, primaryType: 'ClientBetData', types: clientBetDataTypes, message: { attention: order.bet.attention, affiliate: order.bet.affiliate, core: order.bet.core, amount: BigInt(order.bet.amount), nonce: BigInt(order.bet.nonce), conditionId: BigInt(order.bet.conditionId), outcomeId: BigInt(order.bet.outcomeId), minOdds: BigInt(order.bet.minOdds), expiresAt: BigInt(order.bet.expiresAt), chainId: BigInt(order.bet.chainId), relayerFeeAmount: BigInt(order.bet.relayerFeeAmount), }, }) setLiveBetPending(false) setLiveBetProcessing(true) const signedBet = { environment: environments[appChain.id], bettor: account.address!.toLowerCase(), data: order, bettorSignature: signature, } const apiUrl = getApiUrl(appChain.id) const createOrderResponse = await fetch(`${apiUrl}/orders`, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify(signedBet), }) const { id: orderId, state: newOrderState, errorMessage, }: LiveCreateOrderResponse = await createOrderResponse.json() if (newOrderState === LiveOrderState.Created) { txHash = await new Promise<Hex>((res, rej) => { const interval = setInterval(async () => { const getOrderResponse = await fetch(`${apiUrl}/orders/${orderId}`) const { state, txHash }: LiveGetOrderResponse = await getOrderResponse.json() if (state === LiveOrderState.Rejected) { clearInterval(interval) rej() } if (txHash) { clearInterval(interval) res(txHash as Hex) } }, 1000) }) setLiveBetProcessing(false) } else { throw Error(errorMessage) } } else { let coreAddress: Address let data: Address if (selections.length > 1) { coreAddress = contracts.prematchComboCore.address const tuple: [ bigint, bigint ][] = selections.map(({ conditionId, outcomeId }) => [ BigInt(conditionId), BigInt(outcomeId), ]) data = encodeAbiParameters( parseAbiParameters('(uint256, uint64)[]'), [ tuple, ] ) } else { coreAddress = contracts.prematchCore.address const { conditionId, outcomeId } = selections[0]! data = encodeAbiParameters( parseAbiParameters('uint256, uint64'), [ BigInt(conditionId), BigInt(outcomeId), ] ) } txHash = await betTx.writeContractAsync({ address: contracts.proxyFront.address, abi: contracts.proxyFront.abi, functionName: 'bet', value: BigInt(0), args: [ contracts.lp.address, [ { core: coreAddress, amount: rawAmount, expiresAt: rawDeadline, extraData: { affiliate, minOdds: rawMinOdds, data, }, }, ], ], }) } const receipt = await publicClient?.waitForTransactionReceipt({ hash: txHash, }) if (receipt) { addBet({ receipt, bet: { amount: `${fixedAmount}`, selections, odds, }, }) } if (onSuccess) { onSuccess(receipt) } } catch (err) { setLiveBetPending(false) setLiveBetProcessing(false) if (onError) { onError(err as any) } } } const submit = () => { if (isApproveRequired) { return approve() } return placeBet() } return { isAllowanceLoading: allowanceTx.isLoading, isApproveRequired, isRelayerFeeFetching, submit, relayerFeeAmount, approveTx: { isPending: approveTx.isPending, isProcessing: approveReceipt.isLoading, }, betTx: { isPending: betTx.isPending || isLiveBetPending, isProcessing: betReceipt.isLoading || isLiveBetProcessing, }, } }
⚠️

Please ensure that the order amount contains maximum 2 decimal signs, otherwise our backend will reject the order. Examples:

  • 15 USDT - valid ✅
  • 15.2 USDT - valid ✅
  • 15.25 USDT - valid ✅
  • 15.125 USDT - invalid ❌
ℹ️

To obtain the minOdds, utilize the calcLiveOdds function from our Toolkit in conjunction with the odds data retrieved from the Socket API.

import { calcLiveOdds } from '@azuro-org/toolkit' socket.onmessage = (message) => { const data = JSON.parse(message.data.toString()) data.forEach(data => { if (data.outcomes) { // new odds received const { id: conditionId, reinforcement, margin, winningOutcomesCount } = data const oddsData = { conditionId: conditionId, reinforcement: +reinforcement, margin: +margin, winningOutcomesCount: +winningOutcomesCount, outcomes: {}, } oddsData.outcomes = data.outcomes.reduce((acc, { id, odds, clearOdds, maxStake }) => { acc[id] = { odds, clearOdds, } return acc }, {}) const minOdds = calcLiveOdds({ selection: { conditionId, outcomeId, coreAddress }, betAmount: '10', // 10 USDT oddsData, }) } }); }
ℹ️

nonce is an integer number that represents an order identifier and it should:

  1. Be unique for the bettor’s wallet address.
  2. Be higher than previous used value.

So we recommend to use current timestamp (in milliseconds) to prevent any collisions, but you are welcome to come up with your solution.

Like another potentially big integers we pass it as a string for safety.

⚠️

Consider that before each order request you’ll need to ensure that the user has approved enough tokens for bet itself and a fee for relayer that will compensate his native currency spending. So the spending limit should be equal or higher than order.amount + order.relayerFeeAmount.

Last updated on