UNPKG

@azuro-org/sdk

Version:

One-stop solution for building betting dApps on the Azuro Protocol.

1,238 lines 206 kB
import {c as cookieKeys,l as localStorageKeys,D as DEFAULT_DEADLINE,L as LIVE_STATISTICS_SUPPORTED_SPORTS,a as LIVE_STATISTICS_SUPPORTED_PROVIDERS}from'./config.js';import {jsx}from'react/jsx-runtime';import {createContext,useContext,useSyncExternalStore,useState,use,useRef,useEffect,useCallback,useMemo,useReducer}from'react';import {chainsData,getConditionsState,calcMinOdds,ConditionState,OutcomeState,getAvailableFreebets,getBetCalculation,GraphBetStatus,getGamesByIds,BetResult,SelectionResult,BetConditionStatus,SelectionKind,BetOrderState,Legacy_Bet_OrderBy,OrderDirection,LegacyGameStatus,GameState,LegacyBetsDocument,LegacyLiveGamesDocument,getConditionsByGameIds,getPredefinedCombo,groupConditionsByMarket,GameOrderBy,getGamesByFilters,searchGames,getSports,getNavigation,getBetFee,BettorsDocument,ODDS_DECIMALS,GameBetsDocument,paymasterAbi,getComboBetTypedData,createComboBet,getBetTypedData,createBet,getBet,getProviderFromId,getPrecalculatedCashouts,getCalculatedCashout,getCashoutTypedData,createCashout,CashoutState,getCashout,getBonuses,getWaveLevels,getWaveStats,getWavePeriods,getWaveLeaderBoard,activateWave,getSiweNonce,buildSiweMessage,verifySiwe,getUserFavorites,createUserFavorite,deleteUserFavorite}from'@azuro-org/toolkit';import {useAccount,useReadContract,useBalance,useConfig,useSendTransaction,useWaitForTransactionReceipt,useWalletClient,useWriteContract}from'wagmi';import {queryOptions,useQuery,useInfiniteQuery,useQueryClient,useMutation}from'@tanstack/react-query';import {getMarketName,getSelectionName}from'@azuro-org/dictionaries';import gql from'graphql-tag';import {print,Kind}from'graphql';import {formatUnits,parseUnits,erc20Abi,encodeFunctionData,maxUint256,decodeEventLog,getAddress}from'viem';import {waitForTransactionReceipt}from'wagmi/actions';import {gnosis,base,baseSepolia}from'viem/chains';var SportHub; (function (SportHub) { SportHub["Sports"] = "sports"; SportHub["Esports"] = "esports"; })(SportHub || (SportHub = {})); var BetType; (function (BetType) { BetType["Unredeemed"] = "unredeemed"; BetType["Pending"] = "pending"; BetType["Accepted"] = "accepted"; BetType["Settled"] = "settled"; BetType["CashedOut"] = "cashedOut"; })(BetType || (BetType = {}));const DumbContext = createContext(null); const DumbContextWalletClients = createContext({ client: undefined, getClientForChain: async () => undefined, }); let ready = false; let listeners = []; const readyStore = { setReady(value = true) { ready = value; emitChange(); }, subscribe(listener) { listeners = [...listeners, listener]; return () => { listeners = listeners.filter(l => l !== listener); }; }, getSnapshot() { return ready; }, }; const emitChange = () => { for (let listener of listeners) { listener(); } }; let useAccountWithAA = () => useContext(DumbContext); let _useAAWalletClients = () => useContext(DumbContextWalletClients); const useExtendedAccount = () => { const account = useAccount(); const ready = useSyncExternalStore(readyStore.subscribe, readyStore.getSnapshot, () => false); const accountWithAA = useAccountWithAA(); return accountWithAA || { ...account, isAAWallet: false, ready, isReady: ready }; }; const useAAWalletClients = () => { useSyncExternalStore(readyStore.subscribe, readyStore.getSnapshot, () => false); return _useAAWalletClients(); }; (async () => { try { const pkg = await import('@azuro-org/sdk-social-aa-connector'); if (typeof pkg.useAccount === 'function') { useAccountWithAA = pkg.useAccount; } if (typeof pkg.useAAWalletClient === 'function') { _useAAWalletClients = pkg.useAAWalletClients; } readyStore.setReady(); } catch (e) { readyStore.setReady(); } })();const ChainContext = createContext(null); const useChain = () => { return useContext(ChainContext); }; const useOptionalChain = (chainId) => { let chainData = chainId ? chainsData[chainId] : use(ChainContext); if (!chainData) { throw new Error('Please provide chainId or use ChainProvider'); } return chainData; }; const ChainProvider = (props) => { const { children, initialChainId } = props; const [appChainId, setAppChainId] = useState(initialChainId); const { chain: walletChain } = useExtendedAccount(); const walletChainId = walletChain?.id || null; const isRightNetwork = walletChainId === appChainId; const { chain, contracts, betToken, graphql, socket, api, environment } = chainsData[appChainId]; const handleChangeChain = (chainId) => { document.cookie = `${cookieKeys.appChainId}=${chainId};path=/;`; setAppChainId(chainId); }; const context = { appChain: chain, chain, walletChain, contracts, betToken, graphql, socket, api, environment, isRightNetwork, setAppChainId: handleChangeChain, }; return (jsx(ChainContext.Provider, { value: context, children: children })); };var SocketCloseReason$1; (function (SocketCloseReason) { SocketCloseReason[SocketCloseReason["Unmount"] = 3000] = "Unmount"; })(SocketCloseReason$1 || (SocketCloseReason$1 = {})); const FeedSocketContext = createContext(null); const useFeedSocket = () => { return useContext(FeedSocketContext); }; const FeedSocketProvider = ({ children }) => { const { socket: socketUrl } = useChain(); const [socket, setSocket] = useState(); const isSocketReady = socket?.readyState === WebSocket.OPEN; const prevSocketUrl = useRef(socketUrl); const isConnectedRef = useRef(false); const connect = () => { if (isConnectedRef.current) { return; } isConnectedRef.current = true; const newSocket = new WebSocket(`${socketUrl}/feed`); const handleOpen = () => { setSocket(newSocket); }; const handleClose = (event) => { setSocket(undefined); isConnectedRef.current = false; newSocket.removeEventListener('open', handleOpen); newSocket.removeEventListener('close', handleClose); newSocket.removeEventListener('error', handleError); if (event.code !== SocketCloseReason$1.Unmount) { setTimeout(connect, 1000); } }; const handleError = () => { isConnectedRef.current = false; }; newSocket.addEventListener('open', handleOpen); newSocket.addEventListener('close', handleClose); newSocket.addEventListener('error', handleError); }; useEffect(() => { connect(); return () => { socket?.close(SocketCloseReason$1.Unmount); }; }, []); useEffect(() => { if (!socket || !isSocketReady || prevSocketUrl.current === socketUrl) { return; } socket.close(); prevSocketUrl.current = socketUrl; }, [socketUrl, isSocketReady]); const value = { socket, isSocketReady }; return (jsx(FeedSocketContext.Provider, { value: value, children: children })); };function debounce(func, wait, withMaxRequests) { let timeout; let requests = 0; return function (...args) { const context = this; const later = function () { timeout = undefined; requests = 0; func.apply(context, args); }; if (withMaxRequests) { requests++; } if (timeout !== undefined) { clearTimeout(timeout); } if (requests > 100) { later(); } else { timeout = setTimeout(later, wait); } }; }const createQueueAction = (subscribe, unsubscribe) => { const actions = { subscribe: [], unsubscribe: [], }; // group batch requests const request = debounce(() => { const subscribeQueue = [...actions.subscribe]; const unsubscribeQueue = [...actions.unsubscribe]; actions.subscribe = []; actions.unsubscribe = []; const weights = {}; subscribeQueue.forEach(id => { if (!weights[id]) { weights[id] = 1; } else { weights[id]++; } }); unsubscribeQueue.forEach(id => { if (!weights[id]) { weights[id] = -1; } else { weights[id]--; } }); const { subscribeWeights, unsubscribeWeights } = Object.keys(weights).reduce((acc, id) => { // ATTN: equal cause we need to fire subscribe for new elements // and then trigger watcher (we don't have store) if (weights[id] > 0) { acc.subscribeWeights[id] = weights[id]; } else if (weights[id] < 0) { acc.unsubscribeWeights[id] = weights[id]; } return acc; }, { subscribeWeights: {}, unsubscribeWeights: {}, }); if (Object.keys(subscribeWeights).length) { subscribe(subscribeWeights); } if (Object.keys(unsubscribeWeights).length) { unsubscribe(unsubscribeWeights); } }, 50); const run = (action, values) => { request(); values.forEach(value => { actions[action].push(value); }); }; return run; };const createWatcher = () => { const timers = new Map(); const subscribers = new Map(); const trigger = (key, value) => { const handlers = subscribers.get(key) || []; handlers.forEach((cb) => { cb(value); }); }; const subscribe = (key, cb) => { const handlers = subscribers.get(key) || []; handlers.push(cb); subscribers.set(key, handlers); return function unsubscribe() { const handlers = subscribers.get(key) || []; const newHandlers = handlers.filter((handler) => handler !== cb); if (newHandlers.length) { subscribers.set(key, newHandlers); } else { subscribers.delete(key); } }; }; const dispatch = (key, value) => { let timer = timers.get(key); if (timer !== undefined) { clearTimeout(timer); } timer = setTimeout(() => { timers.delete(key); trigger(key, value); }, 200); timers.set(key, timer); }; return { subscribe, dispatch, }; };const conditionWatcher = createWatcher();const outcomeWatcher = createWatcher();var Event$1; (function (Event) { Event["Subscribe"] = "SubscribeConditions"; Event["Unsubscribe"] = "UnsubscribeConditions"; Event["Update"] = "ConditionUpdated"; })(Event$1 || (Event$1 = {})); const ConditionUpdatesContext = createContext(null); const useConditionUpdates = () => { return useContext(ConditionUpdatesContext); }; const ConditionUpdatesProvider = ({ children }) => { const { environment } = useChain(); const { socket, isSocketReady } = useFeedSocket(); const subscribers = useRef({}); const subscribe = useCallback((weights) => { if (socket?.readyState !== 1) { return; } const newSubscribers = []; Object.keys(weights).forEach((conditionId) => { if (typeof subscribers.current[conditionId] === 'undefined') { newSubscribers.push(conditionId); subscribers.current[conditionId] = 0; } subscribers.current[conditionId] += weights[conditionId]; }); if (!newSubscribers.length) { return; } socket.send(JSON.stringify({ event: Event$1.Subscribe, data: { conditionIds: newSubscribers, environment, }, })); }, [socket, environment]); const unsubscribeCall = useCallback((conditionIds) => { if (socket?.readyState !== 1) { return; } socket.send(JSON.stringify({ event: Event$1.Unsubscribe, data: { conditionIds, environment, }, })); }, [socket, environment]); const unsubscribe = useCallback((weights) => { if (socket?.readyState !== 1) { return; } // we mustn't unsubscribe for condition if it has more that 1 subscriber const newUnsubscribers = []; Object.keys(weights).forEach((conditionId) => { if (subscribers.current[conditionId]) { subscribers.current[conditionId] += weights[conditionId]; if (subscribers.current[conditionId] === 0) { delete subscribers.current[conditionId]; newUnsubscribers.push(conditionId); } } }); if (!newUnsubscribers.length) { return; } unsubscribeCall(newUnsubscribers); }, [socket, unsubscribeCall]); const runAction = useCallback(createQueueAction(subscribe, unsubscribe), [subscribe, unsubscribe]); const subscribeToUpdates = useCallback((conditionIds) => { const filtered = conditionIds.filter(Boolean); if (!filtered.length) { return; } runAction('subscribe', filtered); }, [runAction]); const unsubscribeToUpdates = useCallback((conditionIds) => { const filtered = conditionIds.filter(Boolean); if (!filtered.length) { return; } runAction('unsubscribe', filtered); }, [runAction]); useEffect(() => { if (!isSocketReady || !socket) { return; } const handleMessage = (message) => { const { event, data } = JSON.parse(message.data); if (event !== Event$1.Update) { return; } const { id: conditionId, outcomes, state, isLiveEnabled, isPrematchEnabled, isCashoutEnabled, gameId } = data; const eventData = { conditionId: conditionId, state, gameId, isCashoutEnabled, isLiveEnabled, isPrematchEnabled, outcomes, }; conditionWatcher.dispatch(conditionId, eventData); outcomes.forEach(({ outcomeId, currentOdds, turnover, state: outcomeState, hidden }) => { outcomeWatcher.dispatch(`${conditionId}-${outcomeId}`, { odds: +currentOdds, turnover, state: outcomeState, hidden, }); }); }; const handleClose = () => { subscribers.current = {}; }; socket.addEventListener('message', handleMessage); socket.addEventListener('close', handleClose); return () => { socket.removeEventListener('message', handleMessage); socket.removeEventListener('close', handleClose); if (socket.readyState !== WebSocket.OPEN) { subscribers.current = {}; } }; }, [socket]); const value = { isSocketReady, subscribeToUpdates, unsubscribeToUpdates, }; return (jsx(ConditionUpdatesContext.Provider, { value: value, children: children })); };const liveStatisticWatcher = createWatcher();var SocketCloseReason; (function (SocketCloseReason) { SocketCloseReason[SocketCloseReason["Unmount"] = 3000] = "Unmount"; })(SocketCloseReason || (SocketCloseReason = {})); var Status; (function (Status) { Status["InProgress"] = "In progress"; Status["NotStarted"] = "Not started yet"; Status["Finished"] = "Finished"; Status["PreFinished"] = "PreFinished"; Status["CoverageLost"] = "Coverage lost"; Status["Suspended"] = "Suspended"; })(Status || (Status = {})); var SoccerIncidentType; (function (SoccerIncidentType) { SoccerIncidentType["Goal"] = "goal"; SoccerIncidentType["Corner"] = "corner"; SoccerIncidentType["YellowCard"] = "yellow_card"; SoccerIncidentType["RedCard"] = "red_card"; SoccerIncidentType["Substitution"] = "substitution"; SoccerIncidentType["GoalKick"] = "goal_kick"; SoccerIncidentType["TrownIn"] = "throw_in"; SoccerIncidentType["Penalty"] = "penalty"; SoccerIncidentType["Offside"] = "offside"; SoccerIncidentType["ScoreAfterFirstHalf"] = "score_after_first_half"; SoccerIncidentType["ScoreAfterFullTime"] = "score_after_full_time"; SoccerIncidentType["ScoreAfterExtraTime"] = "score_after_extra_time"; SoccerIncidentType["ScoreAfterExtraTimeHalfTime"] = "score_after_extra_time_half_time"; SoccerIncidentType["ScoreAfterPenalties"] = "score_after_penalties"; SoccerIncidentType["ExtraTimeStart"] = "extra_time_start"; SoccerIncidentType["PenaltiesStart"] = "penalties_start"; SoccerIncidentType["PenaltyShootOut"] = "penalty_shoot_out"; SoccerIncidentType["MissPenalty"] = "miss_penalty"; })(SoccerIncidentType || (SoccerIncidentType = {})); var BasketballIncidentType; (function (BasketballIncidentType) { BasketballIncidentType["MatchStart"] = "match_start"; BasketballIncidentType["MatchEnd"] = "match_end"; BasketballIncidentType["QuarterStart"] = "quarter_start"; BasketballIncidentType["QuarterEnd"] = "quarter_end"; BasketballIncidentType["Point"] = "point"; BasketballIncidentType["FreeThrow"] = "free_throw"; BasketballIncidentType["MissedThrow"] = "missed_throw"; BasketballIncidentType["MissedFreeThrow"] = "missed_free_throw"; BasketballIncidentType["PlayersOnCort"] = "players_on_cort"; BasketballIncidentType["PlayersWarmingUp"] = "players_warming_up"; BasketballIncidentType["Foul"] = "foul"; BasketballIncidentType["Rebound"] = "rebound"; BasketballIncidentType["Timeout"] = "timeout"; BasketballIncidentType["TimeoutStart"] = "timeout_start"; BasketballIncidentType["TimeoutEnd"] = "timeout_end"; BasketballIncidentType["Turnover"] = "turnover"; BasketballIncidentType["Block"] = "block"; BasketballIncidentType["Steal"] = "steal"; BasketballIncidentType["Halftime"] = "halftime"; BasketballIncidentType["Fulltime"] = "fulltime"; BasketballIncidentType["Overtime"] = "overtime"; BasketballIncidentType["OvertimeStart"] = "overtime_start"; BasketballIncidentType["OvertimeEnd"] = "overtime_end"; })(BasketballIncidentType || (BasketballIncidentType = {})); var TennisIncidentType; (function (TennisIncidentType) { TennisIncidentType["MatchStart"] = "match_start"; TennisIncidentType["MatchEnd"] = "match_end"; TennisIncidentType["SetStart"] = "set_start"; TennisIncidentType["SetEnd"] = "set_end"; TennisIncidentType["PlayersOnCort"] = "players_on_cort"; TennisIncidentType["PlayersWarmingUp"] = "players_warming_up"; TennisIncidentType["FirstServer"] = "first_server"; TennisIncidentType["Service"] = "service"; TennisIncidentType["Point"] = "point"; TennisIncidentType["Game"] = "game"; TennisIncidentType["Set"] = "set"; TennisIncidentType["Fault"] = "fault"; })(TennisIncidentType || (TennisIncidentType = {})); var VolleyballIncidentType; (function (VolleyballIncidentType) { VolleyballIncidentType["MatchStart"] = "match_start"; VolleyballIncidentType["MatchEnd"] = "match_end"; VolleyballIncidentType["SetStart"] = "set_start"; VolleyballIncidentType["SetEnd"] = "set_end"; VolleyballIncidentType["FirstServer"] = "first_server"; VolleyballIncidentType["Rally"] = "rally"; VolleyballIncidentType["PointWon"] = "point_won"; VolleyballIncidentType["ServiceError"] = "service_error"; VolleyballIncidentType["Timeout"] = "timeout"; VolleyballIncidentType["TimeoutStart"] = "timeout_start"; VolleyballIncidentType["TimeoutEnd"] = "timeout_end"; VolleyballIncidentType["Ace"] = "ace"; VolleyballIncidentType["MatchDelay"] = "match_delay"; VolleyballIncidentType["PlayersOnCort"] = "players_on_cort"; VolleyballIncidentType["PlayersWarmingUp"] = "players_warming_up"; VolleyballIncidentType["Service"] = "service"; })(VolleyballIncidentType || (VolleyballIncidentType = {})); const LiveStatisticsSocketContext = createContext(null); const useLiveStatisticsSocket = () => { return useContext(LiveStatisticsSocketContext); }; const LiveStatisticsSocketProvider = ({ children }) => { const { socket: socketUrl } = useChain(); const [socket, setSocket] = useState(); const isSocketReady = socket?.readyState === WebSocket.OPEN; const isConnectedRef = useRef(false); const prevSocketUrl = useRef(socketUrl); const subscribers = useRef({}); const subscribe = useCallback((weights) => { if (socket?.readyState !== 1) { return; } Object.keys(weights).forEach((gameId) => { if (typeof subscribers.current[gameId] === 'undefined') { subscribers.current[gameId] = 0; } subscribers.current[gameId] += weights[gameId]; }); socket.send(JSON.stringify({ action: 'subscribe', gameIds: Object.keys(weights), })); }, [socket]); const unsubscribeCall = (gameIds) => { if (socket?.readyState !== 1) { return; } socket.send(JSON.stringify({ action: 'unsubscribe', gameIds, })); }; const unsubscribe = useCallback((weights) => { if (socket?.readyState !== 1) { return; } // we mustn't unsubscribe for condition if it has more that 1 subscriber const newUnsubscribers = []; Object.keys(weights).forEach((gameId) => { if (subscribers.current[gameId]) { subscribers.current[gameId] += weights[gameId]; if (subscribers.current[gameId] === 0) { delete subscribers.current[gameId]; newUnsubscribers.push(gameId); } } }); if (!newUnsubscribers.length) { return; } unsubscribeCall(newUnsubscribers); }, [socket, unsubscribeCall]); const runAction = useCallback(createQueueAction(subscribe, unsubscribe), [subscribe, unsubscribe]); const subscribeToUpdates = useCallback((gameIds) => { runAction('subscribe', gameIds); }, [runAction]); const unsubscribeToUpdates = useCallback((gameIds) => { runAction('unsubscribe', gameIds); }, [runAction]); const connect = () => { if (isConnectedRef.current) { return; } isConnectedRef.current = true; const newSocket = new WebSocket(`${socketUrl}/statistics/games`); const handleOpen = () => { setSocket(newSocket); }; const handleClose = (event) => { setSocket(undefined); isConnectedRef.current = false; newSocket.removeEventListener('open', handleOpen); newSocket.removeEventListener('message', handleMessage); newSocket.removeEventListener('close', handleClose); newSocket.removeEventListener('error', handleError); if (event.code !== SocketCloseReason.Unmount) { setTimeout(connect, 1000); } }; const handleError = () => { isConnectedRef.current = false; }; const handleMessage = (message) => { JSON.parse(message.data.toString()).forEach((data) => { const { id, fixture, live } = data; let statsData = { status: fixture?.status || null, scoreBoard: live?.scoreBoard || null, stats: live?.stats || null, virtualCourtId: fixture?.virtualCourtId || null, timeline: live?.timeline || null, jerseyColors: live?.jerseyColors || null, clock: live?.clock || null, }; liveStatisticWatcher.dispatch(id, statsData); }); }; newSocket.addEventListener('open', handleOpen); newSocket.addEventListener('message', handleMessage); newSocket.addEventListener('close', handleClose); newSocket.addEventListener('error', handleError); }; useEffect(() => { connect(); return () => { socket?.close(); }; }, []); useEffect(() => { if (!socket || !isSocketReady || prevSocketUrl.current === socketUrl) { return; } socket.close(); prevSocketUrl.current = socketUrl; }, [socketUrl, isSocketReady]); const value = { isSocketReady, subscribeToUpdates, unsubscribeToUpdates, }; return (jsx(LiveStatisticsSocketContext.Provider, { value: value, children: children })); };const createBatch = (fn, isSet = true) => { let idsWaitList = isSet ? new Set() : []; let settlersWaitList = []; const request = debounce(async (fn, ...rest) => { const ids = [...idsWaitList]; const settlers = settlersWaitList; if (idsWaitList instanceof Set) { idsWaitList.clear(); } else { idsWaitList = []; } settlersWaitList = []; try { const data = await fn(ids, ...rest); settlers.forEach(({ resolve }) => { resolve(data); }); } catch (err) { settlers.forEach(({ reject }) => { reject(err); }); } }, 50, true); const batch = (ids, ...rest) => { request(fn, ...rest); ids.forEach(id => { if (idsWaitList instanceof Set) { idsWaitList.add(id); } else { idsWaitList.push(id); } }); return new Promise((resolve, reject) => { settlersWaitList.push({ resolve, reject }); }); }; return batch; };const getConditions = async (conditionEntityIds, chainId) => { const conditions = await getConditionsState({ chainId, conditionIds: conditionEntityIds, }); return conditions.reduce((acc, condition) => { const { conditionId, outcomes: _outcomes } = condition; const outcomes = _outcomes.reduce((acc, outcome) => { acc[outcome.outcomeId] = outcome; return acc; }, {}); acc[conditionId] = { ...condition, outcomes, }; return acc; }, {}); }; const batchFetchConditions = createBatch(getConditions);const formatToFixed = (value, digitsCount) => { value = String(value); if (!/\./.test(value)) { return value; } const [int, digits] = value.split('.'); if (digitsCount === 0 || !digits) { return int; } if (digits.length <= digitsCount) { return value; } return `${int}.${digits.substring(0, digitsCount)}`; };/** * Watch real-time odds updates for multiple selections (betslip). * Subscribes to condition updates via websocket and calculates total odds with slippage protection. * * Returns individual odds for each selection and combined total odds. * * - Docs: https://gem.azuro.org/hub/apps/sdk/watch/useOdds * * @example * import { useOdds } from '@azuro-org/sdk' * * // ... * * const selections = [ * { conditionId: '123...', outcomeId: '1' }, * { conditionId: '456...', outcomeId: '2' } * ] * const { data, isFetching } = useOdds({ selections }) * const { odds, totalOdds } = data * * * return selections.map(({ conditionId, outcomeId }) => { * const key = `${conditionId}-${outcomeId}` * * return ( * <SelectionView * key={key} * conditionId={conditionId} * outcomeId={outcomeId} * odds={odds[key]} * /> * ) * } * */ const useOdds = ({ selections }) => { const { appChain } = useChain(); const { isSocketReady, subscribeToUpdates, unsubscribeToUpdates } = useConditionUpdates(); const [odds, setOdds] = useState({}); const [isFetching, setFetching] = useState(true); const { selectionsKey, conditionsKey } = useMemo(() => (selections?.reduce((acc, { conditionId, outcomeId }) => { acc.selectionsKey += acc.selectionsKey ? `-${conditionId}/${outcomeId}` : `${conditionId}/${outcomeId}`; acc.conditionsKey += acc.conditionsKey ? `-${conditionId}` : `${conditionId}`; return acc; }, { selectionsKey: '', conditionsKey: '', })), [selections]); const prevSelectionsKeyRef = useRef(selectionsKey); if (selectionsKey !== prevSelectionsKeyRef.current) { setFetching(true); setOdds({}); } prevSelectionsKeyRef.current = selectionsKey; const totalOdds = useMemo(() => { return +formatToFixed(calcMinOdds({ odds: Object.values(odds), slippage: 0 }), 2); }, [odds]); useEffect(() => { if (!isSocketReady || !conditionsKey.length) { return; } const conditionIds = conditionsKey.split('-'); subscribeToUpdates(conditionIds); return () => { unsubscribeToUpdates(conditionIds); }; }, [isSocketReady, conditionsKey]); useEffect(() => { const unsubscribeList = selections.map(({ conditionId, outcomeId }) => { return outcomeWatcher.subscribe(`${conditionId}-${outcomeId}`, (data) => { setOdds(prevOdds => ({ ...prevOdds, [`${conditionId}-${outcomeId}`]: data.odds, })); }); }); return () => { unsubscribeList.forEach((unsubscribe) => { unsubscribe(); }); }; }, [selectionsKey]); useEffect(() => { if (!conditionsKey.length) { return; } const conditionIds = conditionsKey.split('-'); (async () => { const data = await batchFetchConditions(conditionIds, appChain.id); const newOdds = selections.reduce((acc, { conditionId, outcomeId }) => { acc[`${conditionId}-${outcomeId}`] = +(data[conditionId]?.outcomes[outcomeId]?.odds || 1); return acc; }, {}); setOdds(newOdds); setFetching(false); })(); }, [selectionsKey, appChain.id]); return { data: { odds, totalOdds, }, isFetching, }; };/** * Watch real-time state updates for a list of conditions. * Subscribes to condition updates via websocket and tracks state changes (Active, Stopped, Resolved, etc.). * Requires `FeedSocketProvider` and `ConditionUpdatesProvider` (both are included in `AzuroSDKProvider`). * * Returns `data` - a map of condition IDs to their current state. * Returns `conditionsMap` - a map `{ [conditionId]: { state: ConditionState, hidden: boolean } }` of conditions. * * The `hidden` field indicates whether a condition may be hidden from the game markets list. * It starts as `true` for stopped secondary conditions in `ConditionDetailedData`. * When a socket update arrives for a hidden condition, it is set back to `false` — * meaning the condition is still alive and was only temporarily stopped by the provider. * * - Docs: https://gem.azuro.org/hub/apps/sdk/watch-hooks/useConditionsState * * @example * import { useConditionState } from '@azuro-org/sdk' * import { ConditionState, type ConditionDetailedData } from '@azuro-org/toolkit' * * // best approach for the list of game's markets * const { data, conditionsMap, isFetching } = useConditionState({ * // Pick<ConditionDetailedData, 'conditionId' | 'state' | 'hidden'>[] * conditions, * }) * * // OR if you have a condition ID list only, like in the betslip * const { data, conditionsMap, isFetching } = useConditionState({ * conditionIds: [ '123...', '456...' ], * // optional, if not provided, it will fetch the initial states from the API * initialStates: [ ConditionState.Active, ConditionState.Stopped ], * }) * */ const useConditionsState = ({ conditionIds: _conditionIds, initialStates, conditions }) => { const { isSocketReady, subscribeToUpdates, unsubscribeToUpdates } = useConditionUpdates(); const { appChain } = useChain(); const { conditionIds, conditionsKey, initialState } = useMemo(() => { const empty = { conditionIds: [], conditionsKey: '', initialState: { states: {}, statesMap: {} } }; if (conditions) { return conditions.reduce((acc, { conditionId, state, hidden }) => { acc.conditionIds.push(conditionId); acc.conditionsKey += conditionId; acc.initialState.statesMap[conditionId] = { state, hidden: Boolean(hidden) }; acc.initialState.states[conditionId] = state; return acc; }, empty); } if (_conditionIds) { return _conditionIds.reduce((acc, conditionId) => { acc.conditionIds.push(conditionId); acc.conditionsKey += conditionId; if (initialStates?.[conditionId]) { acc.initialState.statesMap[conditionId] = { state: initialStates[conditionId], hidden: false }; acc.initialState.states[conditionId] = initialStates[conditionId]; } return acc; }, empty); } return empty; }, [_conditionIds, initialStates, conditions]); const [state, setState] = useState(initialState); const shouldFetchStates = useMemo(() => conditionIds.some((id) => !state?.states?.[id]), [state, conditionIds]); const prevConditionsKeyRef = useRef(conditionsKey); if (conditionsKey !== prevConditionsKeyRef.current && state !== initialState) { // if conditions are changed (including cleared to empty), reset the state for the new conditions setState(initialState); } prevConditionsKeyRef.current = conditionsKey; useEffect(() => { if (!isSocketReady || !conditionsKey.length) { return; } subscribeToUpdates(conditionIds); return () => { unsubscribeToUpdates(conditionIds); }; }, [conditionsKey, isSocketReady]); useEffect(() => { if (!conditionIds.length) { return; } const unsubscribeList = conditionIds.map((conditionId) => { return conditionWatcher.subscribe(conditionId, (data) => { const { state: newState } = data; setState(prevData => { const newStates = { states: { ...prevData.states, [conditionId]: newState, }, statesMap: { ...prevData.statesMap, [conditionId]: { state: newState, // if condition got an update, then it isn't dead, mark it as visible hidden: false, }, }, }; return newStates; }); }); }); return () => { unsubscribeList.forEach((unsubscribe) => { unsubscribe(); }); }; }, [conditionsKey]); useEffect(() => { if (!conditionIds.length || !shouldFetchStates) { return; } (async () => { const data = await batchFetchConditions(conditionIds, appChain.id); setState((prevValue) => { return conditionIds.reduce((acc, conditionId) => { const hidden = prevValue.statesMap[conditionId]?.hidden ?? false; const state = data?.[conditionId]?.state || prevValue.states[conditionId] || ConditionState.Removed; acc.states[conditionId] = state; acc.statesMap[conditionId] = { state, hidden, }; return acc; }, { states: { ...prevValue.states }, statesMap: { ...prevValue.statesMap } }); }); })(); }, [conditionsKey, appChain.id, shouldFetchStates]); return { data: state.states, conditionsMap: state.statesMap, isFetching: shouldFetchStates, }; };const getKey = (conditionId, outcomeId) => `${conditionId}-${outcomeId}`; /** * Watch real-time state updates for a list of outcomes. * Subscribes to condition updates via websocket and tracks per-outcome state changes * (Active, Stopped, Canceled, Won, Lost) and visibility. * Requires `FeedSocketProvider` and `ConditionUpdatesProvider` (both are included in `AzuroSDKProvider`). * * This is the outcome-level analog of `useConditionsState`: a single condition can hold several outcomes * that independently become hidden or change state, so each outcome carries its own `state`/`hidden`. * * Returns `data` - a map of `${conditionId}-${outcomeId}` keys to their current `OutcomeState`. * Returns `outcomesMap` - a map `{ [`${conditionId}-${outcomeId}`]: { odds, turnover, state, hidden } }` * holding the full `OutcomeUpdateData` for each outcome (live odds/turnover plus state/hidden). * * The `hidden` field indicates whether an outcome should be hidden from the market's outcome list, * independently of the condition's own `hidden` flag. * * - Docs: https://gem.azuro.org/hub/apps/sdk/watch-hooks/useOutcomesState * * @example * import { useOutcomesState } from '@azuro-org/sdk' * import { OutcomeState, type MarketOutcome } from '@azuro-org/toolkit' * * // best approach for a condition's outcomes (MarketOutcome[] from groupConditionsByMarket) * const { data, outcomesMap, isFetching } = useOutcomesState({ * outcomes: condition.outcomes, * }) * * // OR if you have selections only, like in the betslip * const { data, outcomesMap, isFetching } = useOutcomesState({ * selections: [ { conditionId: '123...', outcomeId: '1' } ], * // optional, keyed by `${conditionId}-${outcomeId}`; fetched from the API when omitted * initialStates: { '123...-1': OutcomeState.Active }, * }) * */ const useOutcomesState = ({ selections, initialStates, outcomes }) => { const { isSocketReady, subscribeToUpdates, unsubscribeToUpdates } = useConditionUpdates(); const { appChain } = useChain(); const { selectionsList, conditionIds, selectionsKey, initialState } = useMemo(() => { const conditionIdsSet = new Set(); const selectionsList = []; const initialState = { states: {}, statesMap: {} }; let selectionsKey = ''; const register = (conditionId, outcomeId) => { const key = getKey(conditionId, outcomeId); conditionIdsSet.add(conditionId); selectionsList.push({ conditionId, outcomeId, key }); selectionsKey += key; return key; }; if (outcomes) { outcomes.forEach(({ conditionId, outcomeId, odds, state, hidden }) => { const key = register(conditionId, outcomeId); // turnover only arrives via live socket updates — seed it empty initialState.statesMap[key] = { odds, turnover: '', state, hidden: Boolean(hidden) }; initialState.states[key] = state; }); } else if (selections) { selections.forEach(({ conditionId, outcomeId }) => { const key = register(conditionId, outcomeId); if (initialStates?.[key]) { initialState.statesMap[key] = { odds: 0, turnover: '', state: initialStates[key], hidden: false }; initialState.states[key] = initialStates[key]; } }); } return { selectionsList, conditionIds: Array.from(conditionIdsSet), selectionsKey, initialState, }; }, [selections, initialStates, outcomes]); const [state, setState] = useState(initialState); const shouldFetchStates = useMemo(() => selectionsList.some(({ key }) => !state?.states?.[key]), [state, selectionsList]); const prevSelectionsKeyRef = useRef(selectionsKey); if (selectionsKey !== prevSelectionsKeyRef.current && state !== initialState) { // if selections are changed (including cleared to empty), reset the state for the new selections setState(initialState); } prevSelectionsKeyRef.current = selectionsKey; useEffect(() => { if (!isSocketReady || !selectionsKey.length) { return; } subscribeToUpdates(conditionIds); return () => { unsubscribeToUpdates(conditionIds); }; }, [selectionsKey, isSocketReady]); useEffect(() => { if (!selectionsList.length) { return; } const unsubscribeList = selectionsList.map(({ key }) => { return outcomeWatcher.subscribe(key, (data) => { setState(prevData => ({ states: { ...prevData.states, [key]: data.state, }, statesMap: { ...prevData.statesMap, // store the full OutcomeUpdateData (odds, turnover, state, hidden) [key]: { ...prevData.statesMap[key], ...data, }, }, })); }); }); return () => { unsubscribeList.forEach((unsubscribe) => { unsubscribe(); }); }; }, [selectionsKey]); useEffect(() => { if (!selectionsList.length || !shouldFetchStates) { return; } (async () => { const data = await batchFetchConditions(conditionIds, appChain.id); setState((prevValue) => { return selectionsList.reduce((acc, { conditionId, outcomeId, key }) => { const fetched = data?.[conditionId]?.outcomes?.[outcomeId]; const prev = prevValue.statesMap[key]; const state = fetched?.state || prevValue.states[key] || OutcomeState.Canceled; const hidden = fetched?.hidden ?? prev?.hidden ?? false; // REST odds are strings; coerce. turnover isn't returned by REST — keep last known. const odds = +(fetched?.odds ?? prev?.odds ?? 0); const turnover = prev?.turnover ?? ''; acc.states[key] = state; acc.statesMap[key] = { odds, turnover, state, hidden, }; return acc; }, { states: { ...prevValue.states }, statesMap: { ...prevValue.statesMap } }); }); })(); }, [selectionsKey, appChain.id, shouldFetchStates]); return { data: state.states, outcomesMap: state.statesMap, isFetching: shouldFetchStates, }; };const getUseAvailableFreebetsQueryOptions = (params) => { const { account, affiliate, selections, chainId, query = {} } = params; return queryOptions({ queryKey: ['available-freebets', chainId, account?.toLowerCase(), affiliate?.toLowerCase(), selections], queryFn: async () => { const freebets = await getAvailableFreebets({ chainId, account, affiliate, selections, }); if (!freebets) { return []; } return freebets; }, refetchOnWindowFocus: false, ...query, }); }; /** * Retrieves available freebets for a specific account, affiliate, and bet selections. * Only returns freebets that can be applied to the provided selections. * * - Docs: https://gem.azuro.org/hub/apps/sdk/bonus/useAvailableFreebets * * @example * import { useAvailableFreebets } from '@azuro-org/sdk' * * const { data: freebets, isLoading } = useAvailableFreebets({ * account: '0x...', * affiliate: '0x...', * selections: [{ conditionId: '123', outcomeId: '1' }], * }) * */ const useAvailableFreebets = (props) => { const { chain: appChain } = useOptionalChain(props.chainId); return useQuery(getUseAvailableFreebetsQueryOptions({ ...props, chainId: appChain.id })); };const useForceUpdate = () => { const [increment, setState] = useState(0); const forceUpdate = useCallback(() => { setState((v) => ++v); }, []); return { increment, forceUpdate }; };const getUseBetCalculationQueryOptions = (params) => { const { selections, chainId, account, query = {} } = params; const { betToken } = chainsData[chainId]; return queryOptions({ queryKey: ['bet-calc', chainId, account, selections], queryFn: async () => { const data = await getBetCalculation({ chainId, selections, account, }); let result = { minBet: undefined, maxBet: '0', }; if (typeof data?.minBet !== 'undefined') { result.minBet = formatToFixed(data.minBet, betToken.decimals); } if (data?.maxBet) { result.maxBet = formatToFixed(data.maxBet, betToken.decimals); } return result; }, // disable cache gcTime: 0, refetchOnWindowFocus: false, enabled: Boolean(selections.length), ...query, }); }; /** * Calculates the minimum and maximum bet amount for given selections. * User's account is required to provide the **correct** maximum bet amount. * * Used in betslip provider (`useDetailedBetslip` hook) * * - Docs: https://gem.azuro.org/hub/apps/sdk/data-hooks/useBetCalculation * * @example <caption>Basic usage</caption> * import { useBetCalculation } from '@azuro-org/sdk' * * const { data, isFetching } = useBetCalculation({ selections, account }) * const { minBet, maxBet } = data || {} * * @example <caption>Get the same from `useDetailedBetslip` (`BetslipProvider`) for selections in betslip</caption> * import { useDetailedBetslip } from '@azuro-org/sdk' * * const { minBet, maxBet, isBetCalculationFetching } = useDetailedBetslip() * */ const useBetCalculation = (props) => { const { selections, chainId, account, query = {} } = props; const { chain: appChain } = useOptionalChain(chainId); const { isSocketReady, subscribeToUpdates, unsubscribeToUpdates } = useConditionUpdates(); const selectionsKey = useMemo(() => (selections.map(({ conditionId, outcomeId }) => `${conditionId}/${outcomeId}`).join('-')), [selections]); const queryData = useQuery(getUseBetCalculationQueryOptions({ selections, chainId: appChain.id, account, query })); const { refetch } = queryData; useEffect(() => { if (!isSocketReady || !selections?.length) { return; } const conditionIds = selections.map(({ conditionId }) => conditionId); subscribeToUpdates(conditionIds); return () => { unsubscribeToUpdates(conditionIds); }; }, [selectionsKey, isSocketReady]); useEffect(() => { if (!selections?.length) { return; } const unsubscribeList = selections.map(({ conditionId }) => { return conditionWatcher.subscribe(conditionId, () => refetch()); }); return () => { unsubscribeList.forEach((unsubscribe) => { unsubscribe(); }); }; }, [selectionsKey]); return queryData; };var BetslipDisableReason; (function (BetslipDisableReason) { BetslipDisableReason["ConditionState"] = "ConditionState"; BetslipDisableReason["BetAmountGreaterThanMaxBet"] = "BetAmountGreaterThanMaxBet"; BetslipDisableReason["BetAmountLowerThanMinBet"] = "BetAmountLowerThanMinBet"; BetslipDisableReason["ComboWithForbiddenItem"] = "ComboWithForbiddenItem"; BetslipDisableReason["ComboWithSameGame"] = "ComboWithSameGame"; BetslipDisableReason["SelectedOutcomesTemporarySuspended"] = "SelectedOutcomesTemporarySuspended"; BetslipDisableReason["TotalOddsTooLow"] = "TotalOddsTooLow"; /** * @deprecated Only for v2 */ BetslipDisableReason["PrematchConditionInStartedGame"] = "PrematchConditionInStartedGame"; BetslipDisableReason["FreeBetExpired"] = "FreeBetExpired"; })(BetslipDisableReason || (BetslipDisableReason = {})); const BaseBetslipContext = createContext(null); const DetailedBetslipContext = createContext(null); const useBaseBetslip = () => { return useContext(BaseBetslipContext); }; const useDetailedBetslip = () => { return useContext(DetailedBetslipContext); }; const BetslipProvider = (props) => { const { children, affiliate } = props; const { appChain } = useChain(); const account = useExtendedAccount(); const { forceUpdate } = useForceUpdate(); const [items, setItems] = useState([]); const [selectedFreebet, setFreebet] = useState(); const [betAmount, setBetAmount] = useState(''); // co