diff --git a/docs/docs/api.md b/docs/docs/api.md index c898ded0..1c73fc05 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -599,12 +599,12 @@ $ curl https://manifold.markets/api/v0/market/{marketId}/resolve -X POST \ ### `POST /v0/market/[marketId]/sell` -Sells some quantity of shares in a market on behalf of the authorized user. +Sells some quantity of shares in a binary market on behalf of the authorized user. Parameters: -- `outcome`: Required. One of `YES`, `NO`, or a `number` indicating the numeric - bucket ID, depending on the market type. +- `outcome`: Optional. One of `YES`, or `NO`. If you leave it off, and you only + own one kind of shares, you will sell that kind of shares. - `shares`: Optional. The amount of shares to sell of the outcome given above. If not provided, all the shares you own will be sold. diff --git a/functions/src/sell-shares.ts b/functions/src/sell-shares.ts index b6238434..ec08ab86 100644 --- a/functions/src/sell-shares.ts +++ b/functions/src/sell-shares.ts @@ -1,4 +1,4 @@ -import { sumBy, uniq } from 'lodash' +import { mapValues, groupBy, sumBy, uniq } from 'lodash' import * as admin from 'firebase-admin' import { z } from 'zod' @@ -9,7 +9,7 @@ import { getCpmmSellBetInfo } from '../../common/sell-bet' import { addObjects, removeUndefinedProps } from '../../common/util/object' import { getValues, log } from './utils' import { Bet } from '../../common/bet' -import { floatingLesserEqual } from '../../common/util/math' +import { floatingEqual, floatingLesserEqual } from '../../common/util/math' import { getUnfilledBetsQuery, updateMakers } from './place-bet' import { FieldValue } from 'firebase-admin/firestore' import { redeemShares } from './redeem-shares' @@ -17,7 +17,7 @@ import { redeemShares } from './redeem-shares' const bodySchema = z.object({ contractId: z.string(), shares: z.number().optional(), // leave it out to sell all shares - outcome: z.enum(['YES', 'NO']), + outcome: z.enum(['YES', 'NO']).optional(), // leave it out to sell whichever you have }) export const sellshares = newEndpoint({}, async (req, auth) => { @@ -46,9 +46,31 @@ export const sellshares = newEndpoint({}, async (req, auth) => { throw new APIError(400, 'Trading is closed.') const prevLoanAmount = sumBy(userBets, (bet) => bet.loanAmount ?? 0) + const betsByOutcome = groupBy(userBets, (bet) => bet.outcome) + const sharesByOutcome = mapValues(betsByOutcome, (bets) => + sumBy(bets, (b) => b.shares) + ) - const outcomeBets = userBets.filter((bet) => bet.outcome == outcome) - const maxShares = sumBy(outcomeBets, (bet) => bet.shares) + let chosenOutcome: 'YES' | 'NO' + if (outcome != null) { + chosenOutcome = outcome + } else { + const nonzeroShares = Object.entries(sharesByOutcome).filter( + ([_k, v]) => !floatingEqual(0, v) + ) + if (nonzeroShares.length == 0) { + throw new APIError(400, "You don't own any shares in this market.") + } + if (nonzeroShares.length > 1) { + throw new APIError( + 400, + `You own multiple kinds of shares, but did not specify which to sell.` + ) + } + chosenOutcome = nonzeroShares[0][0] as 'YES' | 'NO' + } + + const maxShares = sharesByOutcome[chosenOutcome] const sharesToSell = shares ?? maxShares if (!floatingLesserEqual(sharesToSell, maxShares)) @@ -63,7 +85,7 @@ export const sellshares = newEndpoint({}, async (req, auth) => { const { newBet, newPool, newP, fees, makers } = getCpmmSellBetInfo( soldShares, - outcome, + chosenOutcome, contract, prevLoanAmount, unfilledBets