diff --git a/docs/docs/api.md b/docs/docs/api.md index 667c68b8..8b7dce30 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -579,6 +579,26 @@ $ 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. + +Parameters: + +- `outcome`: Required. One of `YES`, `NO`, or a `number` indicating the numeric + bucket ID, depending on the market type. +- `shares`: Optional. The amount of shares to sell of the outcome given + above. If not provided, all the shares you own will be sold. + +Example request: + +``` +$ curl https://manifold.markets/api/v0/market/{marketId}/sell -X POST \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Key {...}' \ + --data-raw '{"outcome": "YES", "shares": 10}' +``` + ### `GET /v0/bets` Gets a list of bets, ordered by creation date descending. diff --git a/functions/src/sell-shares.ts b/functions/src/sell-shares.ts index 40ea0f4a..b6238434 100644 --- a/functions/src/sell-shares.ts +++ b/functions/src/sell-shares.ts @@ -16,7 +16,7 @@ import { redeemShares } from './redeem-shares' const bodySchema = z.object({ contractId: z.string(), - shares: z.number(), + shares: z.number().optional(), // leave it out to sell all shares outcome: z.enum(['YES', 'NO']), }) @@ -49,11 +49,12 @@ export const sellshares = newEndpoint({}, async (req, auth) => { const outcomeBets = userBets.filter((bet) => bet.outcome == outcome) const maxShares = sumBy(outcomeBets, (bet) => bet.shares) + const sharesToSell = shares ?? maxShares - if (!floatingLesserEqual(shares, maxShares)) + if (!floatingLesserEqual(sharesToSell, maxShares)) throw new APIError(400, `You can only sell up to ${maxShares} shares.`) - const soldShares = Math.min(shares, maxShares) + const soldShares = Math.min(sharesToSell, maxShares) const unfilledBetsSnap = await transaction.get( getUnfilledBetsQuery(contractDoc) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 4d27918b..aea38c86 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -771,7 +771,9 @@ export function SellPanel(props: { const betDisabled = isSubmitting || !amount || error // Sell all shares if remaining shares would be < 1 - const sellQuantity = amount === Math.floor(shares) ? shares : amount + const isSellingAllShares = amount === Math.floor(shares) + + const sellQuantity = isSellingAllShares ? shares : amount async function submitSell() { if (!user || !amount) return @@ -780,7 +782,7 @@ export function SellPanel(props: { setIsSubmitting(true) await sellShares({ - shares: sellQuantity, + shares: isSellingAllShares ? undefined : amount, outcome: sharesOutcome, contractId: contract.id, }) diff --git a/web/pages/api/v0/market/[id]/sell.ts b/web/pages/api/v0/market/[id]/sell.ts new file mode 100644 index 00000000..431121f2 --- /dev/null +++ b/web/pages/api/v0/market/[id]/sell.ts @@ -0,0 +1,28 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { + CORS_ORIGIN_MANIFOLD, + CORS_ORIGIN_LOCALHOST, +} from 'common/envs/constants' +import { applyCorsHeaders } from 'web/lib/api/cors' +import { fetchBackend, forwardResponse } from 'web/lib/api/proxy' + +export const config = { api: { bodyParser: true } } + +export default async function route(req: NextApiRequest, res: NextApiResponse) { + await applyCorsHeaders(req, res, { + origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST], + methods: 'POST', + }) + + const { id } = req.query + const contractId = id as string + + if (req.body) req.body.contractId = contractId + try { + const backendRes = await fetchBackend(req, 'sellshares') + await forwardResponse(res, backendRes) + } catch (err) { + console.error('Error talking to cloud function: ', err) + res.status(500).json({ message: 'Error communicating with backend.' }) + } +}