mintCostForBudget function

MintBudgetCost mintCostForBudget(
  1. MintBudgetInputs inputs
)

A fill whose ALL-IN cost fits a budget — expiry_market::mint_exact_cost computed client-side, with the same lot search over the same cost function, so the answer is the quantity that entrypoint would size and the cost it would debit.

Without it a "spend exactly $X" flow has to guess: every fee is charged ON TOP of the premium, so the caller subtracts an estimated fee load, pads it against an abort, and systematically underspends. Here the whole search runs locally; submit the all-in budget through tx.mintCost on deployments carrying mint_exact_cost.

Sizing also respects the fill's maximum payout and the 32-bit lot cap; either can leave substantial budget unspent. When only the budget binds, one more lot would exceed it. If the budget fill exceeds its maximum payout, the contract's step-down is best effort: rounding can make it miss a larger admissible fill, including one meeting minQuantity. The chain caps the budget at the account balance first (MintBudgetInputs.accountBalance).

Implementation

MintBudgetCost mintCostForBudget(MintBudgetInputs inputs) {
  _assertCostInputs(
      inputs.fees, inputs.book, inputs.penaltyRate, inputs.lotSize);
  final resolved = _resolveBoundaries(inputs.probabilities);
  final boundaries = resolved.boundaries;
  final lotSize = inputs.lotSize ?? POSITION_LOT_SIZE;
  final ttl = _timeToExpiry(_rawMs(inputs.expiryMs),
      _rawMs(inputs.nowMs ?? DateTime.now().millisecondsSinceEpoch));
  final minQuantity =
      inputs.minQuantity == null ? lotSize : _rawAmount(inputs.minQuantity!);
  final requestedBudget = _rawAmount(inputs.budget);
  var budget = requestedBudget;
  final accountBalance = inputs.accountBalance;
  if (accountBalance != null) budget = _min(budget, _rawAmount(accountBalance));

  _assertRangeMintPolicy(inputs.fees, boundaries);

  // `strike_exposure::max_quantity_for_premium` — the premium-only fit bounds
  // the all-in fit from above, because every other term is non-negative.
  final probability = rangeProbabilityRaw(boundaries);
  var lo = BigInt.zero;
  var hi = MAX_QUANTITY_LOTS;
  while (lo < hi) {
    final mid = (lo + hi + BigInt.one) ~/ BigInt.two;
    if (_mulDown(probability, mid * lotSize) <= budget) {
      lo = mid;
    } else {
      hi = mid - BigInt.one;
    }
  }

  // `expiry_market::quote_exact_cost_terms`, first half: the budget search
  // proper. Exact, because every all-in term is nondecreasing in quantity for
  // fixed pre-trade state.
  BigInt allInCostAt(BigInt quantity) =>
      _mintQuoteAt(inputs, boundaries, quantity, ttl).cost;
  hi = lo;
  lo = BigInt.zero;
  while (lo < hi) {
    final mid = (lo + hi + BigInt.one) ~/ BigInt.two;
    if (allInCostAt(mid * lotSize) <= budget) {
      lo = mid;
    } else {
      hi = mid - BigInt.one;
    }
  }
  final budgetLots = lo;
  final budgetQuantity = budgetLots * lotSize;

  // Second half: the maximum-payout bound (`cost <= quantity`) is
  // deliberately NOT part of that search. It is not monotone — cost and
  // quantity both rise, and the independent floors in each cost term let
  // `cost(q) <= q` flip back to true at a larger lot wherever unit cost sits
  // within rounding of one — so binary-searching it would discard admissible
  // fills. It is consulted only when the budget fill breaches it, and the
  // step-down runs strictly below that fill, so every candidate already fits
  // the budget. This fallback mirrors Move's best-effort search; its
  // nonmonotone predicate means it can miss larger admissible fills,
  // including one satisfying minQuantity.
  var lots = budgetLots;
  if (budgetLots > BigInt.zero &&
      allInCostAt(budgetQuantity) > budgetQuantity) {
    var stepLo = BigInt.zero;
    var stepHi = budgetLots - BigInt.one;
    while (stepLo < stepHi) {
      final mid = (stepLo + stepHi + BigInt.one) ~/ BigInt.two;
      final candidate = mid * lotSize;
      if (allInCostAt(candidate) <= candidate) {
        stepLo = mid;
      } else {
        stepHi = mid - BigInt.one;
      }
    }
    // No admissible smaller fill: fall back to the budget fill, which then
    // fails the maximum-payout bound below exactly as the chain's own quote
    // aborts on it.
    lots = stepLo == BigInt.zero ? budgetLots : stepLo;
  }

  final quantity = lots * lotSize;
  if (quantity < minQuantity) {
    throw PredictInputError('budget $budget sizes $quantity, below the '
        '$minQuantity minimum (EMintQuantityBelowMin)');
  }
  final premium = _mulDown(probability, quantity);
  if (premium < MIN_PREMIUM) {
    throw PredictInputError('budget $budget sizes a premium of $premium, '
        'below the $MIN_PREMIUM minimum (EPremiumBelowMinimum)');
  }
  _assertValidQuantity(quantity, lotSize);
  final quote =
      _mintCostFrom(inputs, boundaries, resolved.exact, quantity, ttl);
  if (quote.raw.cost > quantity) {
    throw PredictInputError('sized fill inside budget $budget costs more '
        'than it can pay out: ${quote.raw.cost} exceeds $quantity '
        '(EMintCostAboveMaxPayout)');
  }
  return MintBudgetCost(
    quantity: quote.quantity,
    entryProbability: quote.entryProbability,
    premium: quote.premium,
    fees: quote.fees,
    cost: quote.cost,
    costPerContract: quote.costPerContract,
    payoutMultiple: quote.payoutMultiple,
    raw: quote.raw,
    exactProbabilities: quote.exactProbabilities,
    budget: fromRaw(requestedBudget, 6),
    effectiveBudget: fromRaw(budget, 6),
    unspentBudget: fromRaw(requestedBudget - quote.raw.cost, 6),
    rawBudget: requestedBudget,
    rawEffectiveBudget: budget,
    rawUnspentBudget: requestedBudget - quote.raw.cost,
  );
}