@azuro-org/sdk
Version:
One-stop solution for building betting dApps on the Azuro Protocol.
1,238 lines • 206 kB
JavaScript
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