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
(opens in a new tab)
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
(opens in a new tab) which manages both prematch and live logic.
Here are the highlights of usePrepareBet
for Live bets:
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:
- Be unique for the bettor's wallet address.
- 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
.