Migration Guide: Toolkit v7.0.0
This guide will help you migrate from toolkit v6.x to v7.0.0. The whole release is one change: the protocol no longer resolves conditions — it resolves outcomes.
Overview
A Condition used to carry a result of its own:
it could be Resolved or Canceled, and the outcomes underneath inherited that verdict. That model could
not describe reality. Within one condition, the protocol can settle one outcome as Won, another as
Lost, void a third (Canceled — stake refunded), and leave a fourth still open.
So in v7:
ConditionStateis now exactly{ Active, Stopped }— a condition tells you whether it’s taking bets, nothing more.OutcomeStateis where settlement lives — unchanged as an enum, but now the only place a result is expressed.MarketOutcome.isWonis gone, because it was derived from the condition and was wrong.- New helper
isOutcomeSettledand a root export ofOutcomeResultmake the per-outcome model easy to consume.
This is a major version update with breaking changes. It pairs with SDK v8.0.0 — if you use the SDK, read the SDK v8 Migration Guide as well, since several of its changes are behavioural and produce no compile error.
Breaking Changes
1. enum ConditionState reduced to Active | Stopped
Canceled, Removed and Resolved are removed from the enum. A condition is now only ever open for
bets (Active) or not (Stopped).
Before:
import { ConditionState } from '@azuro-org/toolkit'
enum ConditionState {
Active = 'Active',
Canceled = 'Canceled',
Removed = 'Removed',
Resolved = 'Resolved',
Stopped = 'Stopped'
}After:
import { ConditionState } from '@azuro-org/toolkit'
enum ConditionState {
Active = 'Active',
Stopped = 'Stopped',
}Any code that referenced a removed member stops compiling. Rewrite it against the outcome instead:
Before:
// "is this market finished?"
const isFinished = condition.state === ConditionState.Resolved
|| condition.state === ConditionState.Canceled
// "is this market gone from the UI?"
const isGone = condition.state === ConditionState.RemovedAfter:
import { isOutcomeSettled, ConditionState } from '@azuro-org/toolkit'
// "is this market finished?" - true only when every outcome under it has settled
const isFinished = condition.outcomes.every(({ state }) => isOutcomeSettled(state))
// "is this market gone from the UI?" - it isn't a state any more.
// A condition that is absent from the feed reads as Stopped; a condition the provider
// pulled from the grid carries `hidden: true`.
const isGone = condition.hidden || condition.state === ConditionState.StoppedRemoved had no replacement member on purpose. It used to mean “hide this from the UI entirely”,
which conflated two different things: a condition that isn’t in the feed (now Stopped) and a condition
the provider took off the grid (hidden: true on ConditionDetailedData). Handle those two separately.
2. MarketOutcome.isWon removed
isWon is deleted with no alias. Use the outcome’s own state.
Before:
import { groupConditionsByMarket } from '@azuro-org/toolkit'
const markets = groupConditionsByMarket(conditions)
const outcome = markets[0]!.conditions[0]!.outcomes[0]!
if (outcome.isWon) {
// won
}
else {
// "lost" - including outcomes that were never settled at all
}After:
import { groupConditionsByMarket, OutcomeState, isOutcomeSettled } from '@azuro-org/toolkit'
const markets = groupConditionsByMarket(conditions)
const outcome = markets[0]!.conditions[0]!.outcomes[0]!
if (!isOutcomeSettled(outcome.state)) {
// still open - Active or Stopped
}
else if (outcome.state === OutcomeState.Won) {
// won
}
else if (outcome.state === OutcomeState.Canceled) {
// voided - stake refunded
}
else {
// OutcomeState.Lost
}Why isWon had to go, and why a straight rename would not have fixed it.
isWon was derived only from the condition-level wonOutcomeIds. Whenever the condition had no
wonOutcomeIds — which is every condition that isn’t resolved yet, and every condition settled
per-outcome — isWon was undefined. Code written as outcome.isWon ? 'Won' : 'Lost' therefore
rendered every outcome of an unsettled market as lost, and had no way at all to express a void.
state distinguishes all five cases, so it is the only per-outcome source of truth.
3. Per-outcome bet results are typed OutcomeResult
BetMetaData.selections[].outcome.condition.outcomes[].result was typed SelectionResult, an enum with
only Won and Lost. It could not represent a voided leg, so a refunded selection had to be read as
null — indistinguishable from a selection that hasn’t settled yet.
It is now typed OutcomeResult, which adds Canceled.
Before:
enum SelectionResult {
Lost = 'Lost',
Won = 'Won'
}
// BetMetaData
selections: Array<{
result: SelectionResult | null
outcome: {
condition: {
outcomes: {
result: SelectionResult | null // no way to express a void
outcomeId: string
sortOrder: number
}[]
// ...
}
outcomeId: string
sortOrder: number
}
// ...
}>After:
enum OutcomeResult {
Won = 'Won',
Lost = 'Lost',
Canceled = 'Canceled'
}
// BetMetaData
selections: Array<{
result: SelectionResult | null // unchanged - per-selection signal
outcome: {
condition: {
outcomes: {
result: OutcomeResult | null // retyped - can now be Canceled
outcomeId: string
sortOrder: number
}[]
// ...
}
outcomeId: string
sortOrder: number
}
// ...
}>If you narrowed on SelectionResult when reading a bet’s legs, widen it:
import { OutcomeResult } from '@azuro-org/toolkit'
const legResult = selection.outcome.condition.outcomes
.find(({ outcomeId }) => outcomeId === selection.outcome.outcomeId)?.result
const isLegVoided = legResult === OutcomeResult.Canceled
const isLegWon = legResult === OutcomeResult.Won
const isLegPending = !legResultThe result lives on the condition’s outcome list, keyed by outcomeId — not on the selection.
selection.result is still typed SelectionResult (Won | Lost), so it cannot report a void on its own.
New Features
isOutcomeSettled
import { isOutcomeSettled, OutcomeState } from '@azuro-org/toolkit'
isOutcomeSettled(OutcomeState.Won) // true
isOutcomeSettled(OutcomeState.Lost) // true
isOutcomeSettled(OutcomeState.Canceled) // true
isOutcomeSettled(OutcomeState.Active) // false
isOutcomeSettled(OutcomeState.Stopped) // falseOne predicate for “does this outcome still have a result pending?”. See the full docs.
calcComboOdds
A combo’s total odds, priced the way the protocol prices them, for the rules in force when the bet was
placed. Use it for any bet that already exists; keep
calcMinOdds for the minimum odds to place a new one.
import { calcComboOdds, MARGIN_APPLIED_AT } from '@azuro-org/toolkit'
// same legs, different eras - the feed's fee has a start date
calcComboOdds({ odds: [ 2, 1.75 ], createdAt: MARGIN_APPLIED_AT }) // fee removed per leg
calcComboOdds({ odds: [ 2, 1.75 ], createdAt: MARGIN_APPLIED_AT - 1 }) // plain productLeave voided legs out of the array. An empty array prices at 1, so a bet with nothing left standing
returns its stake. Full details on the calcComboOdds page.
MARGIN_APPLIED_AT
Unix seconds, the moment the feed started applying its fee to every outcome’s odds. Exported because reconstructing any historical combo figure needs it.
BetsReportEntry.createdAt is required
Only affects code that builds entries itself and calls calcBetsReport directly —
getBetsReport fills it in. Required rather than optional because
no default is safe: defaulting either way silently mis-prices one era of bets.
const entry: BetsReportEntry = {
id: bet.id,
+ createdAt: +bet.createdBlockTimestamp,
...
}OutcomeResult exported from the package root
import { OutcomeResult } from '@azuro-org/toolkit'
enum OutcomeResult {
Won = 'Won',
Lost = 'Lost',
Canceled = 'Canceled'
}Use it when reading settlement off a bet. It is the bet-side counterpart of OutcomeState: OutcomeState
describes an outcome in the feed (and can be Active / Stopped), OutcomeResult describes how a bet’s
leg finished.
selections[].outcome.result on the bets query
BetFragment and BetsQuery now request result on each selection’s outcome:
selections: Array<{
odds: string
result?: SelectionResult | null
outcome: {
outcomeId: string
title?: string | null
result?: OutcomeResult | null // new
condition: {
conditionId: string
title?: string | null
status: ConditionStatus
gameId: string
wonOutcomeIds?: Array<string> | null
}
}
}>This is the authoritative per-outcome settlement signal for a bet. If you build bet history from the
raw query rather than through the SDK’s useBets, read this field to
decide whether a leg won, lost or was voided — do not derive it from the condition’s status or
wonOutcomeIds.
What did not change
This is the most common source of confusion after upgrading, so it is worth stating explicitly:
-
OutcomeStateis unchanged — stillActive | Canceled | Stopped | Won | Lost. It was already the per-outcome state; v7 just makes it the only one. -
GameStateis unchanged. -
ConditionStatus/BetConditionStatusis unchanged and is a different enum. It comes from the bets subgraph (Created | Resolved | Canceled | Paused) and describes a condition entity there. It is not the feed’sConditionState, and itsResolved/Canceledmembers were not removed.// subgraph — unchanged enum ConditionStatus { Created = 'Created', Resolved = 'Resolved', Canceled = 'Canceled', Paused = 'Paused' } // feed — reduced in v7 enum ConditionState { Active = 'Active', Stopped = 'Stopped', } -
Condition-level
wonOutcomeIdsis still served by the feed onConditionDetailedData. It is not removed — but it only ever describes wins, so don’t use it to decide whether an outcome lost or was voided. Use the outcome’sstate.
Grep your call sites
The removed enum members and isWon fail to compile, which makes them easy to find. The rest are
semantic, so search for them by hand:
# 1. Removed enum members - these will not compile, but grep finds them faster than tsc
grep -rn "ConditionState.Resolved\|ConditionState.Canceled\|ConditionState.Removed" src/
# 2. isWon - removed with no alias
grep -rn "isWon" src/
# 3. Condition-level result derivation - usually a bug now
grep -rn "wonOutcomeIds" src/
# 4. Per-outcome results narrowed to the old `SelectionResult` type
grep -rn "SelectionResult" src/For each hit, the rewrite is the same shape: ask the outcome, not the condition.
| You were asking | Ask instead |
|---|---|
condition.state === ConditionState.Resolved | condition.outcomes.every(o => isOutcomeSettled(o.state)) |
condition.state === ConditionState.Canceled | outcome.state === OutcomeState.Canceled, per outcome |
condition.state === ConditionState.Removed | condition.hidden, or state === ConditionState.Stopped |
outcome.isWon | outcome.state === OutcomeState.Won |
condition.wonOutcomeIds.includes(outcomeId) | outcome.state === OutcomeState.Won |
selection.outcome.condition.status === ConditionStatus.Canceled | selection.outcome.result === OutcomeResult.Canceled |
Migration Checklist
- Update package version:
npm install @azuro-org/[email protected] - Remove every reference to
ConditionState.Resolved,ConditionState.CanceledandConditionState.Removed - Replace
outcome.isWonwithoutcome.state === OutcomeState.Won - Audit every
!isWon/isWon ? … : …branch — the false branch used to swallow pending and voided outcomes, and it must now be split into three cases - Replace condition-level “is it finished?” checks with
isOutcomeSettledover the condition’s outcomes - Stop deriving per-outcome results from
wonOutcomeIds - Widen anything typed
SelectionResulton a bet’s per-outcome result toOutcomeResult, and handleCanceled - Confirm you are not confusing
ConditionStatus(subgraph) withConditionState(feed) - If you use the SDK, follow the SDK v8 Migration Guide — it has changes that produce no compile error
Common Migration Patterns
Pattern 1: rendering a market after the game ends
Before:
if (condition.state === ConditionState.Resolved) {
return outcomes.map(outcome => (
outcome.isWon ? renderWon(outcome) : renderLost(outcome)
))
}After:
import { isOutcomeSettled, OutcomeState } from '@azuro-org/toolkit'
return outcomes.map((outcome) => {
if (!isOutcomeSettled(outcome.state)) {
return renderBettable(outcome)
}
if (outcome.state === OutcomeState.Won) {
return renderWon(outcome)
}
if (outcome.state === OutcomeState.Canceled) {
return renderRefunded(outcome)
}
return renderLost(outcome)
})Note that the condition-level branch disappears entirely: outcomes of the same condition can be in different states at the same time, so there is no single verdict to branch on above them.
Pattern 2: excluding a voided leg from a combo
A voided leg is refunded, so it must not contribute to the combo’s odds.
Before:
// every leg counted, because a void was indistinguishable from "not settled yet"
const totalOdds = calcMinOdds({ odds: legs.map(leg => +leg.odds), slippage: 0 })After:
import { calcComboOdds, OutcomeResult } from '@azuro-org/toolkit'
// `legs` are selections from the bets query, whose outcome carries `result`
const payingLegs = legs.filter(leg => leg.outcome.result !== OutcomeResult.Canceled)
// `calcComboOdds` answers 1 for an empty list, so an all-void combo returns the stake
const totalOdds = calcComboOdds({
odds: payingLegs.map(leg => +leg.odds),
createdAt: +bet.createdAt,
})Getting this wrong overstates the payout. A combo whose voided leg is still multiplied into the total reports a potential win the protocol will never pay.
Use calcComboOdds, not calcMinOdds, to price a bet that
already exists. The feed’s fee has a start date, and a combo placed before it is priced as the plain
product of its legs — calcMinOdds always assumes the fee, so it overstates an older bet by roughly
(1/0.99)^(legs-1). calcComboOdds picks the rule from createdAt. Keep calcMinOdds for the
minimum odds to place a new bet with, where slippage applies.
Pattern 3: telling “voided” apart from “not in the feed”
Before:
// OutcomeState.Canceled was ambiguous: voided, or simply absent from the feed
const isMissing = state === OutcomeState.CanceledAfter:
// Canceled now strictly means voided - real money was refunded.
// An outcome that is absent from the feed reads as Stopped.
const isVoided = state === OutcomeState.Canceled
const isMissingOrSuspended = state === OutcomeState.StoppedIf you use the SDK, this is where the fallback happens: useOutcomeState / useOutcomesState return
OutcomeState.Stopped for an outcome the feed doesn’t know about — they used to return
OutcomeState.Canceled.
Getting Help
If you encounter issues during migration:
- Check the Toolkit Documentation
- Read the SDK v8 Migration Guide if you consume the toolkit through the SDK
- Report bugs on GitHub