diff --git a/backend/api/src/app.ts b/backend/api/src/app.ts index 05b63acfb2..8f9384254e 100644 --- a/backend/api/src/app.ts +++ b/backend/api/src/app.ts @@ -47,8 +47,6 @@ import { awardBounty } from './award-bounty' import { addBounty } from './add-bounty' import { cancelbounty } from './cancel-bounty' import { createAnswerCPMM } from './create-answer-cpmm' -import { createportfolio } from './create-portfolio' -import { updateportfolio } from './update-portfolio' import { searchgiphy } from './search-giphy' import { manachantweet } from './manachan-tweet' import { managram } from './managram' @@ -147,6 +145,7 @@ import { createuser } from 'api/create-user' import { verifyPhoneNumber } from 'api/verify-phone-number' import { requestOTP } from 'api/request-phone-otp' import { multiSell } from 'api/multi-sell' +import { convertCashToMana } from './convert-cash-to-mana' import { convertSpiceToMana } from './convert-sp-to-mana' import { donate } from './donate' import { getFeed } from 'api/get-feed' @@ -342,6 +341,7 @@ const handlers: { [k in APIPath]: APIHandler } = { managrams: getManagrams, manalink: createManalink, donate: donate, + 'convert-cash-to-mana': convertCashToMana, 'convert-sp-to-mana': convertSpiceToMana, 'market/:id/positions': getPositions, me: getMe, @@ -484,8 +484,6 @@ app.post('/follow-topic', ...apiRoute(followtopic)) app.post('/league-activity', ...apiRoute(leagueActivity)) app.post('/cancel-bounty', ...apiRoute(cancelbounty)) app.post('/edit-answer-cpmm', ...apiRoute(editanswercpmm)) -app.post('/createportfolio', ...apiRoute(createportfolio)) -app.post('/updateportfolio', ...apiRoute(updateportfolio)) app.post('/searchgiphy', ...apiRoute(searchgiphy)) app.post('/manachantweet', ...apiRoute(manachantweet)) app.post('/refer-user', ...apiRoute(referuser)) diff --git a/backend/api/src/convert-cash-to-mana.ts b/backend/api/src/convert-cash-to-mana.ts new file mode 100644 index 0000000000..d15478b51c --- /dev/null +++ b/backend/api/src/convert-cash-to-mana.ts @@ -0,0 +1,60 @@ +import { APIError, APIHandler } from './helpers/endpoint' +import { type TxnData, insertTxns } from 'shared/txn/run-txn' +import { createSupabaseDirectClient } from 'shared/supabase/init' +import { incrementBalance } from 'shared/supabase/users' +import { betsQueue } from 'shared/helpers/fn-queue' +import { CASH_TO_MANA_CONVERSION_RATE } from 'common/envs/constants' +import { calculateRedeemablePrizeCash } from 'shared/calculate-redeemable-prize-cash' + +export const convertCashToMana: APIHandler<'convert-cash-to-mana'> = async ( + { amount }, + auth +) => { + const pg = createSupabaseDirectClient() + + await betsQueue.enqueueFn(async () => { + // check if user has enough cash + await pg.tx(async (tx) => { + const redeemable = await calculateRedeemablePrizeCash(auth.uid, tx) + if (redeemable < amount) { + throw new APIError(403, 'Not enough redeemable balance') + } + + await incrementBalance(tx, auth.uid, { + cashBalance: -amount, + balance: amount * CASH_TO_MANA_CONVERSION_RATE, + }) + }) + + // key for equivalence + const insertTime = Date.now() + + const toBank: TxnData = { + category: 'CONVERT_CASH', + fromType: 'USER', + fromId: auth.uid, + toType: 'BANK', + toId: 'BANK', + amount: amount, + token: 'SPICE', + description: 'Convert cash to mana', + data: { insertTime }, + } + + const toYou: TxnData = { + category: 'CONVERT_CASH_DONE', + fromType: 'BANK', + fromId: 'BANK', + toType: 'USER', + toId: auth.uid, + amount: amount, + token: 'M$', + description: 'Convert cash to mana', + data: { + insertTime, + }, + } + + await pg.tx((tx) => insertTxns(tx, [toBank, toYou])) + }, [auth.uid]) +} diff --git a/backend/api/src/create-portfolio.ts b/backend/api/src/create-portfolio.ts deleted file mode 100644 index 61ffccb50e..0000000000 --- a/backend/api/src/create-portfolio.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { z } from 'zod' -import { randomUUID } from 'crypto' - -import { getUser } from 'shared/utils' -import { slugify } from 'common/util/slugify' -import { randomString } from 'common/util/random' -import { APIError, authEndpoint, validate } from './helpers/endpoint' -import { - MAX_PORTFOLIO_NAME_LENGTH, - Portfolio, - convertPortfolio, -} from 'common/portfolio' -import { createSupabaseDirectClient } from 'shared/supabase/init' - -const portfolioSchema = z.object({ - name: z.string().min(1).max(MAX_PORTFOLIO_NAME_LENGTH), - items: z.array( - z.object({ - contractId: z.string(), - answerId: z.string().optional(), - position: z.union([z.literal('YES'), z.literal('NO')]), - }) - ), -}) - -export const createportfolio = authEndpoint(async (req, auth) => { - const pg = createSupabaseDirectClient() - - const { name, items } = validate(portfolioSchema, req.body) - - const creator = await getUser(auth.uid) - if (!creator) throw new APIError(401, 'Your account was not found') - - const slug = await getSlug(name) - - console.log( - 'creating portfolio owned by', - creator.username, - 'name', - name, - 'slug', - slug, - 'items', - items - ) - - const portfolio: Portfolio = { - id: randomUUID(), - creatorId: creator.id, - slug, - name, - items, - createdTime: Date.now(), - } - - const returnedPortfolio = await pg.one( - 'insert into portfolios (id, creator_id, slug, name, items) values ($1, $2, $3, $4, $5) returning *', - [ - portfolio.id, - portfolio.creatorId, - portfolio.slug, - portfolio.name, - JSON.stringify(portfolio.items), - ], - convertPortfolio - ) - - console.log('returned portfolio', returnedPortfolio) - - return { status: 'success', portfolio: returnedPortfolio } -}) - -export const getSlug = async (name: string) => { - const proposedSlug = slugify(name) - - const preexistingPortfolio = await portfolioExists(proposedSlug) - - return preexistingPortfolio - ? proposedSlug + '-' + randomString() - : proposedSlug -} - -export async function portfolioExists(slug: string) { - const pg = createSupabaseDirectClient() - const post = await pg.oneOrNone(`select 1 from portfolios where slug = $1`, [ - slug, - ]) - - return !!post -} diff --git a/backend/api/src/create-public-chat-message.ts b/backend/api/src/create-public-chat-message.ts index f837b26450..db150f5590 100644 --- a/backend/api/src/create-public-chat-message.ts +++ b/backend/api/src/create-public-chat-message.ts @@ -30,7 +30,7 @@ export const createPublicChatMessage: APIHandler< throw new APIError(500, 'Failed to create chat message.') } - broadcast('chat_message', {}) + broadcast('public-chat', {}) return convertPublicChatMessage({ ...chatMessage, diff --git a/backend/api/src/donate.ts b/backend/api/src/donate.ts index 505898febb..04dba338fa 100644 --- a/backend/api/src/donate.ts +++ b/backend/api/src/donate.ts @@ -3,7 +3,12 @@ import { charities } from 'common/charity' import { APIError } from 'api/helpers/endpoint' import { runTxn } from 'shared/txn/run-txn' import { createSupabaseDirectClient } from 'shared/supabase/init' -import { CHARITY_FEE, MIN_SPICE_DONATION } from 'common/envs/constants' +import { + MIN_CASH_DONATION, + MIN_SPICE_DONATION, + CHARITY_FEE, + TWOMBA_ENABLED, +} from 'common/envs/constants' import { getUser } from 'shared/utils' export const donate: APIHandler<'donate'> = async ({ amount, to }, auth) => { @@ -16,12 +21,20 @@ export const donate: APIHandler<'donate'> = async ({ amount, to }, auth) => { const user = await getUser(auth.uid, tx) if (!user) throw new APIError(401, 'Your account was not found') - if (user.spiceBalance < amount) { - throw new APIError(403, 'Insufficient prize points') + const balance = TWOMBA_ENABLED ? user.cashBalance : user.spiceBalance + if (balance < amount) { + throw new APIError( + 403, + `Insufficient ${TWOMBA_ENABLED ? 'cash' : 'prize points'} balance` + ) } - if (amount < MIN_SPICE_DONATION) { - throw new APIError(400, 'Minimum donation is 25,000 prize points') + const min = TWOMBA_ENABLED ? MIN_CASH_DONATION : MIN_SPICE_DONATION + if (amount < min) { + throw new APIError( + 400, + `Minimum donation is ${min} ${TWOMBA_ENABLED ? 'cash' : 'prize points'}` + ) } // add donation to charity @@ -35,7 +48,7 @@ export const donate: APIHandler<'donate'> = async ({ amount, to }, auth) => { toType: 'BANK', toId: 'BANK', amount: fee, - token: 'SPICE', + token: TWOMBA_ENABLED ? 'CASH' : 'SPICE', data: { charityId: charity.id, }, @@ -48,7 +61,7 @@ export const donate: APIHandler<'donate'> = async ({ amount, to }, auth) => { toType: 'CHARITY', toId: charity.id, amount: donation, - token: 'SPICE', + token: TWOMBA_ENABLED ? 'CASH' : 'SPICE', } as const await runTxn(tx, feeTxn) diff --git a/backend/api/src/get-bets.ts b/backend/api/src/get-bets.ts index a2f0b442f9..b540ffc5fe 100644 --- a/backend/api/src/get-bets.ts +++ b/backend/api/src/get-bets.ts @@ -6,7 +6,8 @@ import { import { getContractIdFromSlug } from 'shared/supabase/contracts' import { getUserIdFromUsername } from 'shared/supabase/users' import { getBetsWithFilter } from 'shared/supabase/bets' -import { NON_POINTS_BETS_LIMIT } from 'common/supabase/bets' +import { convertBet, NON_POINTS_BETS_LIMIT } from 'common/supabase/bets' +import { filterDefined } from 'common/util/array' export const getBets: APIHandler<'bets'> = async (props) => { const { @@ -24,6 +25,7 @@ export const getBets: APIHandler<'bets'> = async (props) => { includeZeroShareRedemptions, count, points, + id, } = props if (limit === 0) { return [] @@ -34,6 +36,14 @@ export const getBets: APIHandler<'bets'> = async (props) => { ) } const pg = createSupabaseDirectClient() + if (id) { + const bet = await pg.map( + `select * from contract_bets where bet_id = $1`, + [id], + (r) => (r ? convertBet(r) : undefined) + ) + return filterDefined(bet) + } const userId = props.userId ?? (await getUserIdFromUsername(pg, username)) const contractId = diff --git a/backend/api/src/get-mana-supply.ts b/backend/api/src/get-mana-supply.ts index 0cf288d53f..3c93efac6a 100644 --- a/backend/api/src/get-mana-supply.ts +++ b/backend/api/src/get-mana-supply.ts @@ -1,6 +1,8 @@ import { getManaSupply as fetchManaSupply } from 'shared/mana-supply' import { APIHandler } from './helpers/endpoint' +import { createSupabaseDirectClient } from 'shared/supabase/init' export const getManaSupply: APIHandler<'get-mana-supply'> = async () => { - return await fetchManaSupply(false) + const pg = createSupabaseDirectClient() + return await fetchManaSupply(pg) } diff --git a/backend/api/src/gidx/callback.ts b/backend/api/src/gidx/callback.ts index f20cdcc4d3..2fee4f1508 100644 --- a/backend/api/src/gidx/callback.ts +++ b/backend/api/src/gidx/callback.ts @@ -4,6 +4,8 @@ import { getGIDXCustomerProfile } from 'shared/gidx/helpers' import { getVerificationStatusInternal } from 'api/gidx/get-verification-status' import { createSupabaseDirectClient } from 'shared/supabase/init' import { broadcast } from 'shared/websockets/server' +import { createPaymentSuccessNotification } from 'shared/create-notification' +import { PaymentCompletedData } from 'common/notification' export const identityCallbackGIDX: APIHandler< 'identity-callback-gidx' @@ -73,14 +75,39 @@ export const paymentCallbackGIDX: APIHandler<'payment-callback-gidx'> = async ( JSON.stringify(props), ] ) + broadcast('gidx-checkout-session/' + MerchantSessionID, { StatusCode, StatusMessage, }) + // TODO: if cashout txn is failed, give back the mana cash + if (TransactionStatusMessage === 'Complete' && TransactionStatusCode === 1) { + log('Payment successful') - // TODO: Double check here that the txns were sent given successful payment - // and if not, resend them + const paymentData = await pg.oneOrNone( + `select user_id, amount, currency, payment_method_type, payment_amount_type from gidx_receipts where merchant_transaction_id = $1 + and user_id is not null + limit 1`, + [MerchantTransactionID], + (row) => + ({ + userId: row.user_id as string, + amount: row.amount as number, + currency: row.currency as string, + paymentMethodType: row.payment_method_type as string, + paymentAmountType: row.payment_amount_type as string, + } as PaymentCompletedData | null) + ) + log('userId for payment', paymentData) + + // Debit for us = credit for user + if (paymentData && paymentData.paymentAmountType === 'Debit') { + await createPaymentSuccessNotification(paymentData, MerchantTransactionID) + } + } + // TODO: Double check here if txns were not sent given successful payment + // and if so, send them return { MerchantTransactionID } } diff --git a/backend/api/src/gidx/complete-cashout-session.ts b/backend/api/src/gidx/complete-cashout-session.ts index 2acd407718..484f090293 100644 --- a/backend/api/src/gidx/complete-cashout-session.ts +++ b/backend/api/src/gidx/complete-cashout-session.ts @@ -7,6 +7,7 @@ import { getGIDXStandardParams, getUserRegistrationRequirements, getLocalServerIP, + GIDX_BASE_URL, } from 'shared/gidx/helpers' import { log } from 'shared/monitoring/log' import { createSupabaseDirectClient } from 'shared/supabase/init' @@ -17,8 +18,7 @@ import { getUser, LOCAL_DEV } from 'shared/utils' import { SWEEPIES_CASHOUT_FEE } from 'common/economy' import { calculateRedeemablePrizeCash } from 'shared/calculate-redeemable-prize-cash' -const ENDPOINT = - 'https://api.gidx-service.in/v3.0/api/DirectCashier/CompleteSession' +const ENDPOINT = GIDX_BASE_URL + '/v3.0/api/DirectCashier/CompleteSession' export const completeCashoutSession: APIHandler< 'complete-cashout-session-gidx' diff --git a/backend/api/src/gidx/complete-checkout-session.ts b/backend/api/src/gidx/complete-checkout-session.ts index af06ceb5ce..7df1465f08 100644 --- a/backend/api/src/gidx/complete-checkout-session.ts +++ b/backend/api/src/gidx/complete-checkout-session.ts @@ -1,24 +1,24 @@ import { APIError, APIHandler } from 'api/helpers/endpoint' +import { PaymentAmount, PaymentAmountsGIDX } from 'common/economy' +import { TWOMBA_ENABLED } from 'common/envs/constants' import { CompleteSessionDirectCashierResponse, ProcessSessionCode, } from 'common/gidx/gidx' +import { getIp } from 'shared/analytics' import { getGIDXStandardParams, - getUserRegistrationRequirements, getLocalServerIP, + getUserRegistrationRequirements, + GIDX_BASE_URL, } from 'shared/gidx/helpers' import { log } from 'shared/monitoring/log' -import { updateUser } from 'shared/supabase/users' import { createSupabaseDirectClient } from 'shared/supabase/init' +import { updateUser } from 'shared/supabase/users' import { runTxn } from 'shared/txn/run-txn' -import { PaymentAmountsGIDX, PaymentAmount } from 'common/economy' -import { getIp } from 'shared/analytics' -import { TWOMBA_ENABLED } from 'common/envs/constants' -import { LOCAL_DEV } from 'shared/utils' +import { getUser, LOCAL_DEV } from 'shared/utils' -const ENDPOINT = - 'https://api.gidx-service.in/v3.0/api/DirectCashier/CompleteSession' +const ENDPOINT = GIDX_BASE_URL + '/v3.0/api/DirectCashier/CompleteSession' const getPaymentAmountForWebPrice = (price: number) => { const amount = PaymentAmountsGIDX.find((p) => p.priceInDollars === price) @@ -32,7 +32,11 @@ export const completeCheckoutSession: APIHandler< 'complete-checkout-session-gidx' > = async (props, auth, req) => { if (!TWOMBA_ENABLED) throw new APIError(400, 'GIDX registration is disabled') + const userId = auth.uid + const user = await getUser(userId) + if (!user) throw new APIError(500, 'Your account was not found') + const { phoneNumberWithCode } = await getUserRegistrationRequirements(userId) const { PaymentMethod, @@ -66,7 +70,20 @@ export const completeCheckoutSession: APIHandler< }, ...getGIDXStandardParams(MerchantSessionID), } - log('complete checkout session body:', body) + const { + PaymentMethod: { + CardNumber: _, + ExpirationDate: __, + CVV: ___, + ...paymentMethodWithoutCCInfo + }, + ...bodyWithoutPaymentMethod + } = body + const bodyToLog = { + ...bodyWithoutPaymentMethod, + PaymentMethod: paymentMethodWithoutCCInfo, + } + log('Complete checkout session body:', bodyToLog) const res = await fetch(ENDPOINT, { method: 'POST', @@ -86,6 +103,7 @@ export const completeCheckoutSession: APIHandler< PaymentDetails, SessionStatusMessage, ResponseCode, + SessionID, } = data if (ResponseCode >= 300) { return { @@ -120,7 +138,8 @@ export const completeCheckoutSession: APIHandler< paymentAmount, CompletedPaymentAmount, MerchantTransactionID, - MerchantSessionID + SessionID, + user.sweepstakesVerified ?? false ) return { status: 'success', @@ -176,7 +195,8 @@ const sendCoins = async ( amount: PaymentAmount, paidInCents: number, transactionId: string, - sessionId: string + sessionId: string, + isSweepsVerified: boolean ) => { const data = { transactionId, type: 'gidx', paidInCents, sessionId } const pg = createSupabaseDirectClient() @@ -206,7 +226,9 @@ const sendCoins = async ( await pg.tx(async (tx) => { await runTxn(tx, manaPurchaseTxn) - await runTxn(tx, cashBonusTxn) + if (isSweepsVerified) { + await runTxn(tx, cashBonusTxn) + } await updateUser(tx, userId, { purchasedMana: true, }) diff --git a/backend/api/src/gidx/get-checkout-session.ts b/backend/api/src/gidx/get-checkout-session.ts index da4d227a37..4bac0c322b 100644 --- a/backend/api/src/gidx/get-checkout-session.ts +++ b/backend/api/src/gidx/get-checkout-session.ts @@ -6,6 +6,7 @@ import { } from 'common/gidx/gidx' import { GIDXCallbackUrl, + GIDX_BASE_URL, getGIDXStandardParams, getLocalServerIP, throwIfIPNotWhitelisted, @@ -20,8 +21,7 @@ import { getVerificationStatus } from 'common/user' import { getUser, LOCAL_DEV } from 'shared/utils' import { createSupabaseDirectClient } from 'shared/supabase/init' -const ENDPOINT = - 'https://api.gidx-service.in/v3.0/api/DirectCashier/CreateSession' +const ENDPOINT = GIDX_BASE_URL + '/v3.0/api/DirectCashier/CreateSession' export const getCheckoutSession: APIHandler< 'get-checkout-session-gidx' @@ -83,7 +83,7 @@ export const getCheckoutSession: APIHandler< } } const ID_ENDPOINT = - 'https://api.gidx-service.in/v3.0/api/CustomerIdentity/CustomerProfile' + GIDX_BASE_URL + '/v3.0/api/CustomerIdentity/CustomerProfile' const idBody = { MerchantCustomerID: userId, ...getGIDXStandardParams(), diff --git a/backend/api/src/gidx/get-monitor-status.ts b/backend/api/src/gidx/get-monitor-status.ts index d0418b9173..6900d3e5c8 100644 --- a/backend/api/src/gidx/get-monitor-status.ts +++ b/backend/api/src/gidx/get-monitor-status.ts @@ -3,6 +3,7 @@ import { getGIDXStandardParams, getLocalServerIP, getUserSweepstakesRequirements, + GIDX_BASE_URL, throwIfIPNotWhitelisted, verifyReasonCodes, } from 'shared/gidx/helpers' @@ -26,8 +27,7 @@ export const getMonitorStatus: APIHandler<'get-monitor-status-gidx'> = async ( const userId = auth.uid const pg = createSupabaseDirectClient() const user = await getUserSweepstakesRequirements(userId) - const ENDPOINT = - 'https://api.gidx-service.in/v3.0/api/CustomerIdentity/CustomerMonitor' + const ENDPOINT = GIDX_BASE_URL + '/v3.0/api/CustomerIdentity/CustomerMonitor' const body = { MerchantCustomerID: userId, DeviceIpAddress: ENABLE_FAKE_CUSTOMER @@ -53,7 +53,18 @@ export const getMonitorStatus: APIHandler<'get-monitor-status-gidx'> = async ( const data = (await res.json()) as GIDXMonitorResponse - log('Monitor response:', data) + const { ApiKey: _, ...dataToLog } = data + log('Monitor response:', dataToLog) + await pg.none( + 'insert into user_monitor_status (user_id, data, reason_codes, fraud_confidence_score, identity_confidence_score) values ($1, $2, $3, $4, $5)', + [ + userId, + dataToLog, + data.ReasonCodes, + data.FraudConfidenceScore, + data.IdentityConfidenceScore, + ] + ) throwIfIPNotWhitelisted(data.ResponseCode, data.ResponseMessage) const { status, message } = await verifyReasonCodes( user, diff --git a/backend/api/src/gidx/get-verification-documents.ts b/backend/api/src/gidx/get-verification-documents.ts index 6b6a888fb0..44fee83cdd 100644 --- a/backend/api/src/gidx/get-verification-documents.ts +++ b/backend/api/src/gidx/get-verification-documents.ts @@ -2,6 +2,7 @@ import { APIError, APIHandler } from 'api/helpers/endpoint' import { log } from 'shared/utils' import { getGIDXStandardParams, + GIDX_BASE_URL, throwIfIPNotWhitelisted, } from 'shared/gidx/helpers' import { GIDXDocument } from 'common/gidx/gidx' @@ -29,8 +30,7 @@ export const getVerificationDocuments: APIHandler< } export const getIdentityVerificationDocuments = async (userId: string) => { - const ENDPOINT = - 'https://api.gidx-service.in/v3.0/api/DocumentLibrary/CustomerDocuments' + const ENDPOINT = GIDX_BASE_URL + '/v3.0/api/DocumentLibrary/CustomerDocuments' const body = { MerchantCustomerID: userId, ...getGIDXStandardParams(), diff --git a/backend/api/src/gidx/register.ts b/backend/api/src/gidx/register.ts index 6c23534ef0..45b461e650 100644 --- a/backend/api/src/gidx/register.ts +++ b/backend/api/src/gidx/register.ts @@ -6,6 +6,7 @@ import { getGIDXStandardParams, getLocalServerIP, getUserRegistrationRequirements, + GIDX_BASE_URL, throwIfIPNotWhitelisted, verifyReasonCodes, } from 'shared/gidx/helpers' @@ -19,7 +20,7 @@ import { getIp } from 'shared/analytics' import { distributeKycBonus } from 'shared/distribute-kyc-bonus' const ENDPOINT = - 'https://api.gidx-service.in/v3.0/api/CustomerIdentity/CustomerRegistration' + GIDX_BASE_URL + '/v3.0/api/CustomerIdentity/CustomerRegistration' export const register: APIHandler<'register-gidx'> = async ( props, diff --git a/backend/api/src/gidx/upload-document.ts b/backend/api/src/gidx/upload-document.ts index f1234273b1..dff2664e7e 100644 --- a/backend/api/src/gidx/upload-document.ts +++ b/backend/api/src/gidx/upload-document.ts @@ -1,5 +1,5 @@ import { APIError, APIHandler } from 'api/helpers/endpoint' -import { getGIDXStandardParams } from 'shared/gidx/helpers' +import { getGIDXStandardParams, GIDX_BASE_URL } from 'shared/gidx/helpers' import { isProd, log } from 'shared/utils' import * as admin from 'firebase-admin' import { PROD_CONFIG } from 'common/envs/prod' @@ -11,7 +11,7 @@ import { updateUser } from 'shared/supabase/users' import { TWOMBA_ENABLED } from 'common/envs/constants' const ENDPOINT = - 'https://api.gidx-service.in/v3.0/api/DocumentLibrary/DocumentRegistration' + GIDX_BASE_URL + '/v3.0/api/DocumentLibrary/DocumentRegistration' export const uploadDocument: APIHandler<'upload-document-gidx'> = async ( props, auth diff --git a/backend/api/src/multi-sell.ts b/backend/api/src/multi-sell.ts index 06423e444c..3860a6e62f 100644 --- a/backend/api/src/multi-sell.ts +++ b/backend/api/src/multi-sell.ts @@ -123,7 +123,14 @@ const multiSellMain: APIHandler<'multi-sell'> = async (props, auth) => { const allOrdersToCancel = bets.flatMap((result) => result.allOrdersToCancel) const makers = bets.flatMap((result) => result.makers ?? []) const user = bets[0].user - await onCreateBets(fullBets, contract, user, allOrdersToCancel, makers) + await onCreateBets( + fullBets, + contract, + user, + allOrdersToCancel, + makers, + bets.some((b) => b.streakIncremented) + ) } return { diff --git a/backend/api/src/on-create-bet.ts b/backend/api/src/on-create-bet.ts index 3047e15eb2..80fc053cf0 100644 --- a/backend/api/src/on-create-bet.ts +++ b/backend/api/src/on-create-bet.ts @@ -1,10 +1,4 @@ -import { - log, - getUsers, - revalidateContractStaticProps, - getBettingStreakResetTimeBeforeNow, - getUser, -} from 'shared/utils' +import { log, getUsers, revalidateContractStaticProps } from 'shared/utils' import { Bet, LimitBet, maker } from 'common/bet' import { CPMMContract, @@ -55,7 +49,6 @@ import { } from 'shared/helpers/add-house-subsidy' import { debounce } from 'api/helpers/debounce' import { Fees } from 'common/fees' -import { updateUser } from 'shared/supabase/users' import { broadcastNewBets } from 'shared/websockets/helpers' import { getAnswersForContract } from 'shared/supabase/answers' import { followContractInternal } from 'api/follow-contract' @@ -65,7 +58,8 @@ export const onCreateBets = async ( contract: CPMMContract | CPMMMultiContract | CPMMNumericContract, originalBettor: User, ordersToCancel: LimitBet[] | undefined, - makers: maker[] | undefined + makers: maker[] | undefined, + streakIncremented: boolean ) => { const pg = createSupabaseDirectClient() broadcastNewBets(contract.id, contract.visibility, bets) @@ -119,13 +113,15 @@ export const onCreateBets = async ( const earliestBet = nonRedemptionNonApiBets[0] - // Follow suggestion should be before betting streak update (which updates lastBetTime) - !originalBettor.lastBetTime && - (await createFollowSuggestionNotification(originalBettor.id, contract, pg)) - - await updateBettingStreak(originalBettor, earliestBet, contract) - await Promise.all([ + !originalBettor.lastBetTime && + (await createFollowSuggestionNotification( + originalBettor.id, + contract, + pg + )), + streakIncremented && + (await payBettingStreak(originalBettor, earliestBet, contract)), replyBet && (await handleBetReplyToComment(replyBet, contract, originalBettor, pg)), followContractInternal(pg, contract.id, true, originalBettor.id), @@ -136,9 +132,6 @@ export const onCreateBets = async ( nonRedemptionNonApiBets ), addToLeagueIfNotInOne(pg, originalBettor.id), - updateUser(pg, originalBettor.id, { - lastBetTime: earliestBet.createdTime, - }), ]) } @@ -316,37 +309,15 @@ const handleBetReplyToComment = async ( ) } -const updateBettingStreak = async ( +const payBettingStreak = async ( oldUser: User, bet: Bet, contract: Contract ) => { const pg = createSupabaseDirectClient() - const result = await pg.tx(async (tx) => { - // refetch user to prevent race conditions - const bettor = (await getUser(oldUser.id, tx)) ?? oldUser - const betStreakResetTime = getBettingStreakResetTimeBeforeNow() - const lastBetTime = bettor.lastBetTime ?? 0 - - // If they've already bet after the reset time - if (lastBetTime > betStreakResetTime) { - return { - bonusAmount: 0, - sweepsBonusAmount: 0, - newBettingStreak: bettor.currentBettingStreak, - txn: null, - sweepsTxn: null, - } - } - - const newBettingStreak = (bettor?.currentBettingStreak ?? 0) + 1 - // Otherwise, add 1 to their betting streak - await updateUser(tx, bettor.id, { - currentBettingStreak: newBettingStreak, - }) - - if (!humanish(bettor)) { + const newBettingStreak = (oldUser.currentBettingStreak ?? 0) + 1 + if (!humanish(oldUser)) { return { bonusAmount: 0, sweepsBonusAmount: 0, @@ -372,7 +343,7 @@ const updateBettingStreak = async ( 'id' | 'createdTime' | 'fromId' > = { fromType: 'BANK', - toId: bettor.id, + toId: oldUser.id, toType: 'USER', amount: bonusAmount, token: 'M$', @@ -384,7 +355,7 @@ const updateBettingStreak = async ( let sweepsTxn = null let sweepsBonusAmount = 0 - if (bettor.sweepstakesVerified && TWOMBA_ENABLED) { + if (oldUser.sweepstakesVerified && TWOMBA_ENABLED) { sweepsBonusAmount = Math.min( BETTING_STREAK_SWEEPS_BONUS_AMOUNT * newBettingStreak, BETTING_STREAK_SWEEPS_BONUS_MAX @@ -395,7 +366,7 @@ const updateBettingStreak = async ( 'id' | 'createdTime' | 'fromId' > = { fromType: 'BANK', - toId: bettor.id, + toId: oldUser.id, toType: 'USER', amount: sweepsBonusAmount, token: 'CASH', @@ -409,17 +380,15 @@ const updateBettingStreak = async ( return { txn, sweepsTxn, bonusAmount, sweepsBonusAmount, newBettingStreak } }) - if (result.txn) { - await createBettingStreakBonusNotification( - oldUser, - result.txn.id, - bet, - contract, - result.bonusAmount, - result.newBettingStreak, - result.sweepsTxn ? result.sweepsBonusAmount : undefined - ) - } + await createBettingStreakBonusNotification( + oldUser, + result.txn.id, + bet, + contract, + result.bonusAmount, + result.newBettingStreak, + result.sweepsTxn ? result.sweepsBonusAmount : undefined + ) } export const sendUniqueBettorNotificationToCreator = async ( diff --git a/backend/api/src/place-bet.ts b/backend/api/src/place-bet.ts index 6dc59f8672..2628b4ab51 100644 --- a/backend/api/src/place-bet.ts +++ b/backend/api/src/place-bet.ts @@ -1,14 +1,6 @@ -import { - groupBy, - isEqual, - mapValues, - sortBy, - sumBy, - uniq, - uniqBy, -} from 'lodash' +import { groupBy, isEqual, mapValues, sumBy, uniq, uniqBy } from 'lodash' import { APIError, type APIHandler } from './helpers/endpoint' -import { Contract, CPMM_MIN_POOL_QTY, MarketContract } from 'common/contract' +import { CPMM_MIN_POOL_QTY, MarketContract } from 'common/contract' import { User } from 'common/user' import { BetInfo, @@ -19,12 +11,12 @@ import { import { removeUndefinedProps } from 'common/util/object' import { Bet, LimitBet, maker } from 'common/bet' import { floatingEqual } from 'common/util/math' -import { getContract, getUser, log, metrics } from 'shared/utils' +import { contractColumnsToSelect, log, metrics } from 'shared/utils' import { Answer } from 'common/answer' import { CpmmState, getCpmmProbability } from 'common/calculate-cpmm' import { ValidatedAPIParams } from 'common/api/schema' import { onCreateBets } from 'api/on-create-bet' -import { BANNED_TRADING_USER_IDS } from 'common/envs/constants' +import { BANNED_TRADING_USER_IDS, TWOMBA_ENABLED } from 'common/envs/constants' import * as crypto from 'crypto' import { formatMoneyWithDecimals } from 'common/util/format' import { @@ -32,7 +24,11 @@ import { SupabaseDirectClient, SupabaseTransaction, } from 'shared/supabase/init' -import { bulkIncrementBalances, incrementBalance } from 'shared/supabase/users' +import { + bulkIncrementBalances, + incrementBalance, + incrementStreak, +} from 'shared/supabase/users' import { runShortTrans } from 'shared/short-transaction' import { convertBet } from 'common/supabase/bets' import { @@ -44,13 +40,11 @@ import { broadcastOrders } from 'shared/websockets/helpers' import { betsQueue } from 'shared/helpers/fn-queue' import { FLAT_TRADE_FEE } from 'common/fees' import { redeemShares } from './redeem-shares' -import { - getAnswer, - getAnswersForContract, - updateAnswers, -} from 'shared/supabase/answers' +import { updateAnswers } from 'shared/supabase/answers' import { updateContract } from 'shared/supabase/contracts' import { filterDefined } from 'common/util/array' +import { convertUser } from 'common/supabase/users' +import { convertAnswer, convertContract } from 'common/supabase/contracts' export const placeBet: APIHandler<'bet'> = async (props, auth) => { const isApi = auth.creds.kind === 'key' @@ -199,14 +193,28 @@ export const placeBetMain = async ( return result }) - const { newBet, fullBets, allOrdersToCancel, betId, makers, betGroupId } = - result + const { + newBet, + fullBets, + allOrdersToCancel, + betId, + makers, + betGroupId, + streakIncremented, + } = result log(`Main transaction finished - auth ${uid}.`) metrics.inc('app/bet_count', { contract_id: contractId }) const continuation = async () => { - await onCreateBets(fullBets, contract, user, allOrdersToCancel, makers) + await onCreateBets( + fullBets, + contract, + user, + allOrdersToCancel, + makers, + streakIncremented + ) } const time = Date.now() - startTime @@ -230,9 +238,43 @@ export const fetchContractBetDataAndValidate = async ( isApi: boolean ) => { const { amount, contractId } = body - const answerId = 'answerId' in body ? body.answerId : undefined + const answerIds = + 'answerIds' in body + ? body.answerIds + : 'answerId' in body && body.answerId !== undefined + ? [body.answerId] + : undefined + + const queries = ` + select * from users where id = $1; + select ${contractColumnsToSelect} from contracts where id = $2; + select * from answers + where contract_id = $2 and ( + ($3 is null or id in ($3:list)) or + (select (data->'shouldAnswersSumToOne')::boolean from contracts where id = $2) + ); + select b.*, u.balance, u.cash_balance from contract_bets b join users u on b.user_id = u.id + where b.contract_id = $2 and ( + ($3 is null or b.answer_id in ($3:list)) or + (select (data->'shouldAnswersSumToOne')::boolean from contracts where id = $2) + ) + and not b.is_filled and not b.is_cancelled; + ` + + const results = await pgTrans.multi(queries, [ + uid, + contractId, + answerIds ?? null, + ]) + const user = convertUser(results[0][0]) + const contract = convertContract(results[1][0]) + const answers = results[2].map(convertAnswer) + const unfilledBets = results[3].map(convertBet) as (LimitBet & { + balance: number + cash_balance: number + })[] - const contract = await getContract(pgTrans, contractId) + if (!user) throw new APIError(404, 'User not found.') if (!contract) throw new APIError(404, 'Contract not found.') if (contract.mechanism === 'none' || contract.mechanism === 'qf') throw new APIError(400, 'This is not a market') @@ -242,29 +284,37 @@ export const fetchContractBetDataAndValidate = async ( throw new APIError(403, 'Trading is closed.') if (isResolved) throw new APIError(403, 'Market is resolved.') - const answersPromise = getAnswersForBet( - pgTrans, - contract, - answerId, - 'answerIds' in body ? body.answerIds : undefined + const balanceByUserId = Object.fromEntries( + uniqBy(unfilledBets, (b) => b.userId).map((bet) => [ + bet.userId, + contract.token === 'CASH' ? bet.cash_balance : bet.balance, + ]) ) - - const unfilledBets = await getUnfilledBets( - pgTrans, - contractId, - // Fetch all limit orders if answers should sum to one. - 'shouldAnswersSumToOne' in contract && contract.shouldAnswersSumToOne - ? undefined - : answerId + const unfilledBetUserIds = Object.keys(balanceByUserId) + const balance = contract.token === 'CASH' ? user.cashBalance : user.balance + if (amount !== undefined && balance < amount) + throw new APIError(403, 'Insufficient balance.') + if ( + (!user.sweepstakesVerified || !user.idVerified) && + contract.token === 'CASH' + ) { + throw new APIError( + 403, + 'You must be kyc verified to trade on sweepstakes markets.' + ) + } + if (BANNED_TRADING_USER_IDS.includes(user.id) || user.userDeleted) { + throw new APIError(403, 'You are banned or deleted. And not #blessed.') + } + log( + `Loaded user ${user.username} with id ${user.id} betting on slug ${contract.slug} with contract id: ${contract.id}.` + ) + if (contract.outcomeType === 'STONK' && isApi) { + throw new APIError(403, 'API users cannot bet on STONK contracts.') + } + log( + `Loaded user ${user.username} with id ${user.id} betting on slug ${contract.slug} with contract id: ${contract.id}.` ) - const unfilledBetUserIds = uniq(unfilledBets.map((bet) => bet.userId)) - - const [user, balanceByUserId] = await Promise.all([ - validateBet(pgTrans, uid, amount, contract, isApi), - getUserBalances(pgTrans, unfilledBetUserIds), - ]) - - const answers = await answersPromise return { user, @@ -276,33 +326,6 @@ export const fetchContractBetDataAndValidate = async ( } } -const getAnswersForBet = async ( - pgTrans: SupabaseDirectClient, - contract: Contract, - answerId: string | undefined, - answerIds: string[] | undefined -) => { - const { mechanism } = contract - const contractId = contract.id - - if (answerId && mechanism === 'cpmm-multi-1') { - if (contract.shouldAnswersSumToOne) { - return await getAnswersForContract(pgTrans, contractId) - } else { - // Only fetch the one answer if it's independent multi. - const answer = await getAnswer(pgTrans, answerId) - if (answer) - return sortBy( - uniqBy([answer, ...contract.answers], (a) => a.id), - (a) => a.index - ) - } - } else if (answerIds) { - return await getAnswersForContract(pgTrans, contractId) - } - return undefined -} - export const calculateBetResult = ( body: ValidatedAPIParams<'bet'>, user: User, @@ -516,24 +539,31 @@ export const executeNewBetResult = async ( [contract.token === 'CASH' ? 'cashBalance' : 'balance']: -newBet.amount - apiFee, }) + const streakIncremented = await incrementStreak( + pgTrans, + user, + newBet.createdTime + ) log(`Updated user ${user.username} balance - auth ${user.id}.`) - const totalCreatorFee = - newBet.fees.creatorFee + - sumBy(otherBetResults, (r) => r.bet.fees.creatorFee) - if (totalCreatorFee !== 0) { - await incrementBalance(pgTrans, contract.creatorId, { - balance: totalCreatorFee, - totalDeposits: totalCreatorFee, - }) + if (!TWOMBA_ENABLED) { + const totalCreatorFee = + newBet.fees.creatorFee + + sumBy(otherBetResults, (r) => r.bet.fees.creatorFee) + if (totalCreatorFee !== 0) { + await incrementBalance(pgTrans, contract.creatorId, { + balance: totalCreatorFee, + totalDeposits: totalCreatorFee, + }) - log( - `Updated creator ${ - contract.creatorUsername - } with fee gain ${formatMoneyWithDecimals(totalCreatorFee)} - ${ - contract.creatorId - }.` - ) + log( + `Updated creator ${ + contract.creatorUsername + } with fee gain ${formatMoneyWithDecimals(totalCreatorFee)} - ${ + contract.creatorId + }.` + ) + } } const answerUpdates: { @@ -641,50 +671,10 @@ export const executeNewBetResult = async ( fullBets, user, betGroupId, + streakIncremented, } } -export const validateBet = async ( - pgTrans: SupabaseTransaction | SupabaseDirectClient, - uid: string, - amount: number | undefined, - contract: Contract, - isApi: boolean -) => { - const user = await getUser(uid, pgTrans) - if (!user) throw new APIError(404, 'User not found.') - - const balance = contract.token === 'CASH' ? user.cashBalance : user.balance - if (amount !== undefined && balance < amount) - throw new APIError(403, 'Insufficient balance.') - if ( - (!user.sweepstakesVerified || !user.idVerified) && - contract.token === 'CASH' - ) { - throw new APIError( - 403, - 'You must be kyc verified to trade on sweepstakes markets.' - ) - } - if (BANNED_TRADING_USER_IDS.includes(uid) || user.userDeleted) { - throw new APIError(403, 'You are banned or deleted. And not #blessed.') - } - // if (!isVerified(user)) { - // throw new APIError(403, 'You must verify your phone number to bet.') - // } - log( - `Loaded user ${user.username} with id ${user.id} betting on slug ${contract.slug} with contract id: ${contract.id}.` - ) - if (contract.outcomeType === 'STONK' && isApi) { - throw new APIError(403, 'API users cannot bet on STONK contracts.') - } - log( - `Loaded user ${user.username} with id ${user.id} betting on slug ${contract.slug} with contract id: ${contract.id}.` - ) - - return user -} - export async function bulkUpdateLimitOrders( db: SupabaseDirectClient, updates: Array<{ diff --git a/backend/api/src/place-multi-bet.ts b/backend/api/src/place-multi-bet.ts index 19caba974f..c13ea15e12 100644 --- a/backend/api/src/place-multi-bet.ts +++ b/backend/api/src/place-multi-bet.ts @@ -99,7 +99,14 @@ export const placeMultiBetMain = async ( ) const makers = results.flatMap((result) => result.makers ?? []) const user = results[0].user - await onCreateBets(fullBets, contract, user, allOrdersToCancel, makers) + await onCreateBets( + fullBets, + contract, + user, + allOrdersToCancel, + makers, + results.some((r) => r.streakIncremented) + ) } return { diff --git a/backend/api/src/refer-user.ts b/backend/api/src/refer-user.ts index a4f543c376..ee273d6a83 100644 --- a/backend/api/src/refer-user.ts +++ b/backend/api/src/refer-user.ts @@ -108,7 +108,7 @@ async function handleReferral( toId: referredByUserId, toType: 'USER', amount: REFERRAL_AMOUNT, - token: TWOMBA_ENABLED ? 'CASH' : 'SPICE', + token: TWOMBA_ENABLED ? 'M$' : 'SPICE', category: 'REFERRAL', description: `Referred new user id: ${user.id} for ${REFERRAL_AMOUNT}`, } as const diff --git a/backend/api/src/sell-shares.ts b/backend/api/src/sell-shares.ts index b2d583dd5a..d81bbf089c 100644 --- a/backend/api/src/sell-shares.ts +++ b/backend/api/src/sell-shares.ts @@ -312,10 +312,24 @@ const sellSharesMain: APIHandler<'market/:contractId/sell'> = async ( ) }) - const { newBet, betId, makers, fullBets, allOrdersToCancel } = result + const { + newBet, + betId, + makers, + fullBets, + allOrdersToCancel, + streakIncremented, + } = result const continuation = async () => { - await onCreateBets(fullBets, contract, user, allOrdersToCancel, makers) + await onCreateBets( + fullBets, + contract, + user, + allOrdersToCancel, + makers, + streakIncremented + ) } return { result: { ...newBet, betId }, continue: continuation } } diff --git a/backend/api/src/update-portfolio.ts b/backend/api/src/update-portfolio.ts deleted file mode 100644 index b329442292..0000000000 --- a/backend/api/src/update-portfolio.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { authEndpoint, validate } from './helpers/endpoint' -import { z } from 'zod' -import { createSupabaseDirectClient } from 'shared/supabase/init' -import { MAX_PORTFOLIO_NAME_LENGTH, convertPortfolio } from 'common/portfolio' - -const schema = z.object({ - id: z.string(), - name: z.string().min(1).max(MAX_PORTFOLIO_NAME_LENGTH).optional(), - items: z - .array( - z.object({ - contractId: z.string(), - answerId: z.string().optional(), - position: z.union([z.literal('YES'), z.literal('NO')]), - }) - ) - .optional(), -}) - -export const updateportfolio = authEndpoint(async (req) => { - const { id, name, items } = validate(schema, req.body) - - console.log('updating', id, name, items) - const pg = createSupabaseDirectClient() - - let updatedPortfolio - if (name) { - updatedPortfolio = await pg.one( - 'update portfolios set name = $2 where id = $1 returning *', - [id, name], - convertPortfolio - ) - } - if (items) { - updatedPortfolio = await pg.one( - 'update portfolios set items = $2 where id = $1 returning *', - [id, JSON.stringify(items)], - convertPortfolio - ) - } - console.log('updated portfolio', updatedPortfolio) - return { status: 'success', portfolio: updatedPortfolio } -}) diff --git a/backend/api/src/validate-iap.ts b/backend/api/src/validate-iap.ts index ea884614a0..955f5a17e9 100644 --- a/backend/api/src/validate-iap.ts +++ b/backend/api/src/validate-iap.ts @@ -9,6 +9,7 @@ import { sendThankYouEmail } from 'shared/emails' import { runTxn } from 'shared/txn/run-txn' import { createSupabaseDirectClient } from 'shared/supabase/init' import { IOS_PRICES } from 'common/economy' +import { TWOMBA_ENABLED } from 'common/envs/constants' const bodySchema = z .object({ @@ -72,6 +73,10 @@ export const validateiap = authEndpoint(async (req, auth) => { log('productId', productId, 'not found in price data') throw new APIError(400, 'productId not found in price data') } + + const user = await getUser(userId) + if (!user) throw new APIError(500, 'Your account was not found') + const { priceInDollars, bonusInDollars } = priceData const manaPayout = priceData.mana * quantity const revenue = priceData.priceInDollars * quantity * 0.7 // Apple takes 30% @@ -111,7 +116,10 @@ export const validateiap = authEndpoint(async (req, auth) => { }, description: `Deposit M$${manaPayout} from BANK for mana purchase`, } as Omit - const bonusPurchaseTxn = bonusInDollars + + const isBonusEligible = TWOMBA_ENABLED && user.sweepstakesVerified + + const bonusPurchaseTxn = isBonusEligible && bonusInDollars ? ({ fromId: 'EXTERNAL', fromType: 'BANK', @@ -145,9 +153,6 @@ export const validateiap = authEndpoint(async (req, auth) => { log('user', userId, 'paid M$', manaPayout) - const user = await getUser(userId) - if (!user) throw new APIError(500, 'Your account was not found') - const privateUser = await getPrivateUser(userId) if (!privateUser) throw new APIError(500, 'Private user not found') diff --git a/backend/scheduler/deploy-scheduler.sh b/backend/scheduler/deploy-scheduler.sh index c55bb66d03..4a330bce99 100755 --- a/backend/scheduler/deploy-scheduler.sh +++ b/backend/scheduler/deploy-scheduler.sh @@ -64,23 +64,24 @@ fi echo "Current time: $(date "+%Y-%m-%d %I:%M:%S %p")" +COMMON_ARGS=( + --project ${GCLOUD_PROJECT} + --zone ${ZONE} + --container-image ${IMAGE_URL} + --container-env NEXT_PUBLIC_FIREBASE_ENV=${NEXT_PUBLIC_FIREBASE_ENV},GOOGLE_CLOUD_PROJECT=${GCLOUD_PROJECT} +) + # If you augment the instance, be sure to increase --max-old-space-size in the Dockerfile if [ "${INITIALIZE}" = true ]; then # If you just deleted the instance you don't need this line # gcloud compute addresses create ${SERVICE_NAME} --project ${GCLOUD_PROJECT} --region ${REGION} gcloud compute instances create-with-container ${SERVICE_NAME} \ - --project ${GCLOUD_PROJECT} \ - --zone ${ZONE} \ + "${COMMON_ARGS[@]}" \ --address ${IP_ADDRESS_NAME} \ - --container-image ${IMAGE_URL} \ --machine-type ${MACHINE_TYPE} \ - --container-env NEXT_PUBLIC_FIREBASE_ENV=${NEXT_PUBLIC_FIREBASE_ENV} \ - --container-env GOOGLE_CLOUD_PROJECT=${GCLOUD_PROJECT} \ --scopes default,cloud-platform \ --tags http-server else gcloud compute instances update-container ${SERVICE_NAME} \ - --project ${GCLOUD_PROJECT} \ - --zone ${ZONE} \ - --container-image ${IMAGE_URL} + "${COMMON_ARGS[@]}" fi diff --git a/backend/scheduler/src/jobs/increment-streak-forgiveness.ts b/backend/scheduler/src/jobs/increment-streak-forgiveness.ts index e9350d4dd4..07cb4a2fea 100644 --- a/backend/scheduler/src/jobs/increment-streak-forgiveness.ts +++ b/backend/scheduler/src/jobs/increment-streak-forgiveness.ts @@ -15,22 +15,46 @@ const bulkUpdateUsersByChunk = async ( const now = new Date() let chunkStart = new Date(earliestTime) let chunkEnd = new Date(chunkStart.getTime() + daysInterval * DAY_MS) + const maxRetries = 5 while (chunkStart < now) { - await pg.none( - ` - update users - set data = data || jsonb_build_object('streakForgiveness', coalesce((data->>'streakForgiveness')::numeric, 0) + 1) - where created_time >= $1 and created_time < $2 - `, - [chunkStart.toISOString(), chunkEnd.toISOString()] - ) - log( - 'Updated streak forgiveness for users created between', - chunkStart, - 'and', - chunkEnd - ) + let success = false + let attempts = 0 + + while (!success && attempts < maxRetries) { + try { + log( + 'Updating streak forgiveness for users created between', + chunkStart, + 'and', + chunkEnd + ) + await pg.none( + ` + update users + set data = data || jsonb_build_object('streakForgiveness', coalesce((data->>'streakForgiveness')::numeric, 0) + 1) + where created_time >= $1 and created_time < $2 + `, + [chunkStart.toISOString(), chunkEnd.toISOString()] + ) + success = true + } catch (error) { + attempts++ + log( + 'Failed to update streak forgiveness, attempt', + attempts, + 'error:', + error + ) + if (attempts >= maxRetries) { + throw new Error( + `Failed to update streak forgiveness after ${maxRetries} attempts` + ) + } + const waitTime = Math.random() * 10000 + await new Promise((resolve) => setTimeout(resolve, waitTime)) + } + } chunkStart = chunkEnd chunkEnd = new Date(chunkStart.getTime() + daysInterval * DAY_MS) diff --git a/backend/scheduler/src/jobs/index.ts b/backend/scheduler/src/jobs/index.ts index 1c467a0b05..27175ed995 100644 --- a/backend/scheduler/src/jobs/index.ts +++ b/backend/scheduler/src/jobs/index.ts @@ -152,7 +152,7 @@ export function createJobs() { // Monthly jobs: createJob( 'increment-streak-forgiveness', - '0 0 0 1 * *', // 1st day of the month at 12am PST + '0 0 3 1 * *', // 3am PST on the 1st day of the month incrementStreakForgiveness ), createJob( diff --git a/backend/scheduler/src/jobs/update-stats.ts b/backend/scheduler/src/jobs/update-stats.ts index aa19566d8b..8e461b07aa 100644 --- a/backend/scheduler/src/jobs/update-stats.ts +++ b/backend/scheduler/src/jobs/update-stats.ts @@ -22,10 +22,14 @@ import { import { bulkUpsert } from 'shared/supabase/utils' import { saveCalibrationData } from 'shared/calculate-calibration' import { MANA_PURCHASE_RATE_CHANGE_DATE } from 'common/envs/constants' -import { calculateManaStats } from 'shared/calculate-mana-stats' +import { + updateTxnStats, + insertLatestManaStats, +} from 'shared/calculate-mana-stats' import { getFeedConversionScores } from 'shared/feed-analytics' import { buildArray } from 'common/util/array' import { type Tables } from 'common/supabase/utils' +import { recalculateAllUserPortfolios } from 'shared/mana-supply' interface StatEvent { id: string @@ -53,7 +57,9 @@ export const updateStatsCore = async (daysAgo: number) => { await updateStatsBetween(pg, start, end) const startOfYesterday = endDay.subtract(1, 'day').startOf('day').valueOf() - await calculateManaStats(startOfYesterday, 1) + await updateTxnStats(pg, startOfYesterday, 1) + await recalculateAllUserPortfolios(pg) + await insertLatestManaStats(pg) await saveCalibrationData(pg) diff --git a/backend/scripts/chaos.ts b/backend/scripts/chaos.ts index 9bfd5790ad..3fdbc41b65 100644 --- a/backend/scripts/chaos.ts +++ b/backend/scripts/chaos.ts @@ -10,7 +10,8 @@ import { getRandomTestBet } from 'shared/test/bets' const URL = `https://${DEV_CONFIG.apiEndpoint}/v0` // const URL = `http://localhost:8088/v0` -const USE_OLD_MARKET = true +const OLD_MARKET_SLUG = 'chaos-sweeps--cash' +const USE_OLD_MARKET = !!OLD_MARKET_SLUG const ENABLE_LIMIT_ORDERS = true if (require.main === module) { @@ -20,48 +21,51 @@ if (require.main === module) { return } const privateUsers = await getTestUsers(firestore, pg, 100) - const marketCreations = [ - { - question: 'test ' + Math.random().toString(36).substring(7), - outcomeType: 'MULTIPLE_CHOICE', - answers: Array(50) - .fill(0) - .map((_, i) => 'answer ' + i), - shouldAnswersSumToOne: true, - }, - // { - // question: 'test ' + Math.random().toString(36).substring(7), - // outcomeType: 'BINARY', - // }, - // { - // question: 'test ' + Math.random().toString(36).substring(7), - // outcomeType: 'MULTIPLE_CHOICE', - // answers: Array(50) - // .fill(0) - // .map((_, i) => 'answer ' + i), - // shouldAnswersSumToOne: false, - // }, - ] - log('creating markets') - const markets = await Promise.all( - marketCreations.map(async (market) => { - const resp = await fetch(URL + `/market`, { - method: 'POST', - headers: { - Authorization: `Key ${privateUsers[0].apiKey}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(market), + let markets = [] + if (!USE_OLD_MARKET) { + const marketCreations = [ + { + question: 'test ' + Math.random().toString(36).substring(7), + outcomeType: 'MULTIPLE_CHOICE', + answers: Array(50) + .fill(0) + .map((_, i) => 'answer ' + i), + shouldAnswersSumToOne: true, + }, + // { + // question: 'test ' + Math.random().toString(36).substring(7), + // outcomeType: 'BINARY', + // }, + // { + // question: 'test ' + Math.random().toString(36).substring(7), + // outcomeType: 'MULTIPLE_CHOICE', + // answers: Array(50) + // .fill(0) + // .map((_, i) => 'answer ' + i), + // shouldAnswersSumToOne: false, + // }, + ] + log('creating markets') + markets = await Promise.all( + marketCreations.map(async (market) => { + const resp = await fetch(URL + `/market`, { + method: 'POST', + headers: { + Authorization: `Key ${privateUsers[0].apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(market), + }) + if (resp.status !== 200) { + console.error('Failed to create market', await resp.text()) + } + return resp.json() }) - if (resp.status !== 200) { - console.error('Failed to create market', await resp.text()) - } - return resp.json() - }) - ) + ) + } const contracts = await pg.map( `select * from contracts where slug in ($1:list)`, - USE_OLD_MARKET ? [['test-ubyxer']] : [markets.map((m: any) => m.slug)], + USE_OLD_MARKET ? [[OLD_MARKET_SLUG]] : [markets.map((m: any) => m.slug)], convertContract ) log(`Found ${contracts.length} contracts`) diff --git a/backend/scripts/get-mana-supply.ts b/backend/scripts/get-mana-supply.ts index 800c6a26ba..9a0291929c 100644 --- a/backend/scripts/get-mana-supply.ts +++ b/backend/scripts/get-mana-supply.ts @@ -3,9 +3,9 @@ import { log } from 'shared/utils' import { getManaSupply } from 'shared/mana-supply' if (require.main === module) { - runScript(async () => { + runScript(async ({ pg }) => { log('Getting mana supply...') - const manaSupply = await getManaSupply(false) + const manaSupply = await getManaSupply(pg) console.log(manaSupply) }) } diff --git a/backend/scripts/regen-schema.ts b/backend/scripts/regen-schema.ts index 811aa63b0a..cf914ee9d9 100644 --- a/backend/scripts/regen-schema.ts +++ b/backend/scripts/regen-schema.ts @@ -49,6 +49,13 @@ async function getTableInfo(pg: SupabaseDirectClient, tableName: string) { AND NOT tgisinternal`, [tableName] ) + const rlsEnabled = await pg.one( + `SELECT relrowsecurity + FROM pg_class + WHERE oid = $1::regclass`, + [tableName] + ) + const rls = !!rlsEnabled.relrowsecurity const policies = await pg.any( `SELECT @@ -89,6 +96,7 @@ async function getTableInfo(pg: SupabaseDirectClient, tableName: string) { tableName, foreignKeys, triggers, + rls, policies, indexes, } @@ -215,10 +223,13 @@ async function generateSQLFiles(pg: SupabaseDirectClient) { functions.splice(i, 1) // remove from list so we don't duplicate } } + if (tableInfo.rls) { + content += `-- Row Level Security\n` + content += `ALTER TABLE ${tableInfo.tableName} ENABLE ROW LEVEL SECURITY;\n` + } if (tableInfo.policies.length > 0) { content += `-- Policies\n` - content += `ALTER TABLE ${tableInfo.tableName} ENABLE ROW LEVEL SECURITY;\n\n` } for (const policy of tableInfo.policies) { content += `DROP POLICY IF EXISTS "${policy.policy_name}" ON ${tableInfo.tableName};\n` diff --git a/backend/scripts/save-mana-stats.ts b/backend/scripts/save-mana-stats.ts index 46919abc62..a5d6815583 100644 --- a/backend/scripts/save-mana-stats.ts +++ b/backend/scripts/save-mana-stats.ts @@ -3,12 +3,18 @@ import * as utc from 'dayjs/plugin/utc' import * as timezone from 'dayjs/plugin/timezone' dayjs.extend(utc) dayjs.extend(timezone) - import { runScript } from './run-script' -import { calculateManaStats } from 'shared/calculate-mana-stats' +import { + updateTxnStats, + updateManaStatsBetween, +} from 'shared/calculate-mana-stats' +import { revalidateStaticProps } from 'shared/utils' -runScript(async () => { +runScript(async ({ pg }) => { const endDay = dayjs().tz('America/Los_Angeles') - const startOfYesterday = endDay.subtract(1, 'day').startOf('day').valueOf() - await calculateManaStats(startOfYesterday, 1) + const start = endDay.subtract(7, 'day').startOf('day').valueOf() + await updateTxnStats(pg, start, 7) + await updateManaStatsBetween(pg, start, 7) + + await revalidateStaticProps('/stats') }) diff --git a/backend/shared/src/calculate-mana-stats.ts b/backend/shared/src/calculate-mana-stats.ts index c58c03b8e3..03bc29ed7d 100644 --- a/backend/shared/src/calculate-mana-stats.ts +++ b/backend/shared/src/calculate-mana-stats.ts @@ -1,7 +1,10 @@ -import { createSupabaseDirectClient } from 'shared/supabase/init' +import { SupabaseDirectClient } from 'shared/supabase/init' import { DAY_MS } from 'common/util/time' -import { getManaSupply } from 'shared/mana-supply' -import { insert } from './supabase/utils' +import { + getManaSupply, + getManaSupplyEachDayBetweeen as getManaSupplyEachDay, +} from 'shared/mana-supply' +import { bulkInsert, insert } from './supabase/utils' type txnStats = { start_time: string @@ -14,11 +17,11 @@ type txnStats = { total_amount: number } -export const calculateManaStats = async ( +export const updateTxnStats = async ( + pg: SupabaseDirectClient, startDate: number, numberOfDays: number ) => { - const pg = createSupabaseDirectClient() for (let i = 0; i < numberOfDays; i++) { const startTime = new Date(startDate + i * DAY_MS).toISOString() const endTime = new Date(startDate + (i + 1) * DAY_MS).toISOString() @@ -57,37 +60,32 @@ export const calculateManaStats = async ( [startTime, endTime], (row) => (row.platform_fees ? (row.platform_fees as number) : 0) ) - await Promise.all( - [ - ...txnSummaries, - { - start_time: startTime, - end_time: endTime, - from_type: 'USER', - to_type: 'BANK', - token: 'M$', - quest_type: null, - category: 'BET_FEES', - total_amount: fees, - }, - ].map(async (txnData) => { - await pg.none( - `insert into txn_summary_stats (start_time, end_time, from_type, to_type, token, quest_type, category, total_amount) - values ($1, $2, $3, $4, $5, $6, $7, $8) - `, - [...Object.values(txnData)] - ) - }) - ) + await bulkInsert(pg, 'txn_summary_stats', [ + ...txnSummaries, + { + start_time: startTime, + end_time: endTime, + from_type: 'USER', + to_type: 'BANK', + token: 'M$', + quest_type: null, + category: 'BET_FEES', + total_amount: fees, + }, + ]) } +} + +export const insertLatestManaStats = async (pg: SupabaseDirectClient) => { const now = new Date().toISOString() - const ms = await getManaSupply(true) + const ms = await getManaSupply(pg) await insert(pg, 'mana_supply_stats', { start_time: now, end_time: now, total_value: ms.totalManaValue, total_cash_value: ms.totalCashValue, - balance: ms.cashBalance, + balance: ms.manaBalance, + cash_balance: ms.cashBalance, spice_balance: ms.spiceBalance, investment_value: ms.manaInvestmentValue, cash_investment_value: ms.cashInvestmentValue, @@ -96,3 +94,29 @@ export const calculateManaStats = async ( amm_cash_liquidity: ms.ammCashLiquidity, }) } + +export const updateManaStatsBetween = async ( + pg: SupabaseDirectClient, + startDate: number, + numberOfDays: number +) => { + const stats = await getManaSupplyEachDay(pg, startDate, numberOfDays) + await bulkInsert( + pg, + 'mana_supply_stats', + stats.map((ms) => ({ + start_time: ms.day, + end_time: ms.day, + total_value: ms.totalManaValue, + total_cash_value: ms.totalCashValue, + balance: ms.manaBalance, + cash_balance: ms.cashBalance, + spice_balance: ms.spiceBalance, + investment_value: ms.manaInvestmentValue, + cash_investment_value: ms.cashInvestmentValue, + loan_total: ms.loanTotal, + amm_liquidity: 0, + amm_cash_liquidity: 0, + })) + ) +} diff --git a/backend/shared/src/create-notification.ts b/backend/shared/src/create-notification.ts index d5fe3a96a8..82319bb5a8 100644 --- a/backend/shared/src/create-notification.ts +++ b/backend/shared/src/create-notification.ts @@ -10,6 +10,7 @@ import { NOTIFICATION_DESCRIPTIONS, notification_reason_types, NotificationReason, + PaymentCompletedData, ReviewNotificationData, UniqueBettorData, } from 'common/notification' @@ -562,6 +563,7 @@ export const createBetFillNotification = async ( limitOrderRemaining: remainingAmount, limitAt: limitAt.toString(), outcomeType: contract.outcomeType, + token: contract.token, } as BetFillData, } const pg = createSupabaseDirectClient() @@ -615,6 +617,7 @@ export const createLimitBetCanceledNotification = async ( limitOrderRemaining: remainingAmount, limitAt: limitAt.toString(), outcomeType: contract.outcomeType, + token: contract.token, } as BetFillData, } const pg = createSupabaseDirectClient() @@ -665,6 +668,7 @@ export const createLimitBetExpiredNotification = async ( limitOrderRemaining: remainingAmount, limitAt: limitAt.toString(), outcomeType: contract.outcomeType, + token: contract.token, } as BetFillData, } const pg = createSupabaseDirectClient() @@ -1053,6 +1057,7 @@ export const createNewBettorNotification = async ( outcomeType, ...pseudoNumericData, totalAmountBet: sumBy(bets, 'amount'), + token: contract.token, } as UniqueBettorData), } await insertNotificationToSupabase(notification, pg) @@ -2078,3 +2083,27 @@ export const createExtraPurchasedManaNotification = async ( const pg = createSupabaseDirectClient() await insertNotificationToSupabase(notification, pg) } + +export const createPaymentSuccessNotification = async ( + paymentData: PaymentCompletedData, + transactionId: string +) => { + const notification: Notification = { + id: crypto.randomUUID(), + userId: paymentData.userId, + reason: 'payment_status', + createdTime: Date.now(), + isSeen: false, + sourceId: transactionId, + sourceType: 'payment_status', + sourceUpdateType: 'created', + sourceUserName: '', + sourceUserUsername: '', + sourceUserAvatarUrl: '', + sourceText: '', + data: paymentData, + } + + const pg = createSupabaseDirectClient() + await insertNotificationToSupabase(notification, pg) +} diff --git a/backend/shared/src/gidx/helpers.ts b/backend/shared/src/gidx/helpers.ts index 237cfea583..f5d669f88d 100644 --- a/backend/shared/src/gidx/helpers.ts +++ b/backend/shared/src/gidx/helpers.ts @@ -5,7 +5,7 @@ import { ID_ERROR_MSG, IDENTITY_AND_FRAUD_THRESHOLD, } from 'common/gidx/gidx' -import { getPrivateUserSupabase, getUser, LOCAL_DEV, log } from 'shared/utils' +import { getPrivateUserSupabase, getUser, isProd, log } from 'shared/utils' import { getPhoneNumber } from 'shared/helpers/get-phone-number' import { ENV_CONFIG } from 'common/envs/constants' import { @@ -24,10 +24,13 @@ import { intersection } from 'lodash' import { updatePrivateUser, updateUser } from 'shared/supabase/users' import { User } from 'common/user' -// TODO: when in production, configure endpoint here: https://portal.gidx-service.in/Integration/Index#ProfileNotification -export const GIDXCallbackUrl = LOCAL_DEV - ? 'https://enabled-bream-sharply.ngrok-free.app' - : 'https://' + ENV_CONFIG.apiEndpoint +export const GIDXCallbackUrl = 'https://' + ENV_CONFIG.apiEndpoint +// If you want to test your local endpoint, use ngrok or similar service +// LOCAL_DEV ? 'https://enabled-bream-sharply.ngrok-free.app' : ENV_CONFIG.apiEndpoint + +export const GIDX_BASE_URL = isProd() + ? 'https://api.gidx-service.com' + : 'https://api.gidx-service.in' export const getGIDXStandardParams = (MerchantSessionID?: string) => ({ // TODO: before merging into main, switch from sandbox key to production key in prod @@ -40,8 +43,7 @@ export const getGIDXStandardParams = (MerchantSessionID?: string) => ({ }) export const getGIDXCustomerProfile = async (userId: string) => { - const ENDPOINT = - 'https://api.gidx-service.in/v3.0/api/CustomerIdentity/CustomerProfile' + const ENDPOINT = GIDX_BASE_URL + '/v3.0/api/CustomerIdentity/CustomerProfile' const body = { ...getGIDXStandardParams(), MerchantCustomerID: userId, @@ -122,7 +124,7 @@ export const verifyReasonCodes = async ( const updates = { idVerified: true, sweepstakes5kLimit: hasAny(limitTo5kCashoutCodes), - } + } as Partial if ( user.idVerified !== updates.idVerified || user.sweepstakes5kLimit !== updates.sweepstakes5kLimit @@ -133,7 +135,7 @@ export const verifyReasonCodes = async ( const updates = { idVerified: false, sweepstakesVerified: false, - } + } as Partial if ( user.idVerified !== updates.idVerified || user.sweepstakesVerified !== updates.sweepstakesVerified @@ -231,11 +233,11 @@ export const verifyReasonCodes = async ( ) const updates = { sweepstakesVerified: false, - kycLastAttempt: Date.now(), - } + kycLastAttemptTime: Date.now(), + } as Partial if ( user.sweepstakesVerified !== updates.sweepstakesVerified || - user.kycLastAttempt !== updates.kycLastAttempt + user.kycLastAttemptTime !== updates.kycLastAttemptTime ) { await updateUser(pg, userId, updates) } diff --git a/backend/shared/src/mana-supply.ts b/backend/shared/src/mana-supply.ts index 24b7088e48..283147cf6b 100644 --- a/backend/shared/src/mana-supply.ts +++ b/backend/shared/src/mana-supply.ts @@ -1,30 +1,94 @@ -import { createSupabaseDirectClient } from 'shared/supabase/init' +import { + SupabaseDirectClient, + createSupabaseDirectClient, +} from 'shared/supabase/init' import { chunk } from 'lodash' import { updateUserMetricsCore } from 'shared/update-user-metrics-core' import { log } from 'shared/utils' +import { DAY_MS } from 'common/util/time' +import { millisToTs } from 'common/supabase/utils' -export const getManaSupply = async (recalculateAllUserPortfolios: boolean) => { - const pg = createSupabaseDirectClient() - if (recalculateAllUserPortfolios) { - const allUserIdsWithInvestments = await pg.map( - ` - select distinct u.id from users u - join user_contract_metrics ucm on u.id = ucm.user_id - join contracts c on ucm.contract_id = c.id - where u.data->'lastBetTime' is not null - and c.resolution_time is null; +export const recalculateAllUserPortfolios = async ( + pg: SupabaseDirectClient +) => { + const allUserIdsWithInvestments = await pg.map( + ` + select distinct u.id from users u + join user_contract_metrics ucm on u.id = ucm.user_id + join contracts c on ucm.contract_id = c.id + where u.data->'lastBetTime' is not null + and c.resolution_time is null; + `, + [], + (r) => r.id as string + ) + const chunks = chunk(allUserIdsWithInvestments, 1000) + let processed = 0 + for (const userIds of chunks) { + await updateUserMetricsCore(userIds) + processed += userIds.length + log(`Processed ${processed} of ${allUserIdsWithInvestments.length} users`) + } +} + +// note this is missing amm liquidity +export const getManaSupplyEachDayBetweeen = async ( + pg: SupabaseDirectClient, + startDate: number, + numberOfDays: number +) => { + const results = [] + + for (let day = 0; day < numberOfDays; day++) { + const start = startDate + day * DAY_MS + const end = start + DAY_MS + + // claude generated - takes advantage of users table being much smaller than user_portfolio_history + const userPortfolio = await pg.one( + `with last_history as ( + select uph.* from + users u left join lateral ( + select * + from user_portfolio_history + where user_id = u.id and ts <= millis_to_ts($1) + order by ts desc + limit 1 + ) uph on true + ) + select + sum(balance) as mana_balance, + sum(spice_balance) as spice_balance, + sum(cash_balance) as cash_balance, + sum(investment_value) as mana_investment_value, + sum(cash_investment_value) as cash_investment_value, + sum(loan_total) as loan_total + from last_history + where balance + spice_balance + investment_value > 0; `, - [], - (r) => r.id as string + [end], + (r: any) => ({ + day: millisToTs(start), + totalManaValue: + Number(r.mana_balance) + + Number(r.spice_balance) + + Number(r.mana_investment_value), + totalCashValue: + Number(r.cash_balance) + Number(r.cash_investment_value), + manaBalance: Number(r.mana_balance), + spiceBalance: Number(r.spice_balance), + cashBalance: Number(r.cash_balance), + manaInvestmentValue: Number(r.mana_investment_value), + cashInvestmentValue: Number(r.cash_investment_value), + loanTotal: Number(r.loan_total), + }) ) - const chunks = chunk(allUserIdsWithInvestments, 1000) - let processed = 0 - for (const userIds of chunks) { - await updateUserMetricsCore(userIds) - processed += userIds.length - log(`Processed ${processed} of ${allUserIdsWithInvestments.length} users`) - } + results.push(userPortfolio) + console.log('fetched results for ', millisToTs(start)) } + return results +} + +export const getManaSupply = async (pg: SupabaseDirectClient) => { const userPortfolio = await pg.one( `select sum(u.balance + u.spice_balance + coalesce(uphl.investment_value, 0)) as total_mana_value, @@ -68,15 +132,17 @@ const getAMMLiquidity = async () => { const pg = createSupabaseDirectClient() const [binaryLiquidity, multiLiquidity] = await Promise.all([ pg.many<{ sum: number; token: 'MANA' | 'CASH' }>( - `select sum((data->>'prob')::numeric * (data->'pool'->>'YES')::numeric + (1-(data->>'prob')::numeric) *(data->'pool'->>'NO')::numeric + (data->'subsidyPool')::numeric) + `select + sum((data->>'prob')::numeric * (data->'pool'->>'YES')::numeric + (1-(data->>'prob')::numeric) *(data->'pool'->>'NO')::numeric + (data->'subsidyPool')::numeric), + token from contracts where resolution is null and mechanism = 'cpmm-1' group by token`, [] ), pg.many<{ sum: number; token: 'MANA' | 'CASH' }>( - `select sum(prob * pool_yes + (1-prob) * pool_no + subsidy_pool) from answers - join contracts on contract_id = contracts.id + `select sum(prob * pool_yes + (1-prob) * pool_no + subsidy_pool), contracts.token + from answers join contracts on contract_id = contracts.id where contracts.resolution is null group by contracts.token`, [] diff --git a/backend/shared/src/supabase/search-contracts.ts b/backend/shared/src/supabase/search-contracts.ts index b0bdb6ed5b..d26e01f35d 100644 --- a/backend/shared/src/supabase/search-contracts.ts +++ b/backend/shared/src/supabase/search-contracts.ts @@ -213,7 +213,7 @@ export function getSearchContractSQL(args: { const answersSubQuery = renderSql( select('distinct a.contract_id'), from('answers a'), - where(`a.text_fts @@ websearch_to_tsquery('english', $1)`, [term]) + where(`a.text_fts @@ websearch_to_tsquery('english_extended', $1)`, [term]) ) // Normal full text search @@ -233,18 +233,24 @@ export function getSearchContractSQL(args: { term.length && [ searchType === 'prefix' && where( - `question_fts @@ to_tsquery('english', $1)`, + `question_fts @@ to_tsquery('english_extended', $1)`, constructPrefixTsQuery(term) ), searchType === 'without-stopwords' && - where(`question_fts @@ websearch_to_tsquery('english', $1)`, term), + where( + `question_fts @@ websearch_to_tsquery('english_extended', $1)`, + term + ), searchType === 'with-stopwords' && where( `question_nostop_fts @@ websearch_to_tsquery('english_nostop_with_prefix', $1)`, term ), searchType === 'description' && - where(`description_fts @@ websearch_to_tsquery('english', $1)`, term), + where( + `description_fts @@ websearch_to_tsquery('english_extended', $1)`, + term + ), ], orderBy(getSearchContractSortSQL(sort)), diff --git a/backend/shared/src/supabase/users.ts b/backend/shared/src/supabase/users.ts index 6dfc3bc48c..e5f1a0fb70 100644 --- a/backend/shared/src/supabase/users.ts +++ b/backend/shared/src/supabase/users.ts @@ -1,5 +1,8 @@ -import { pgp, SupabaseDirectClient } from 'shared/supabase/init' -import { Row } from 'common/supabase/utils' +import { + pgp, + SupabaseDirectClient, + SupabaseTransaction, +} from 'shared/supabase/init' import { WEEK_MS } from 'common/util/time' import { APIError } from 'common/api/utils' import { User } from 'common/user' @@ -8,6 +11,8 @@ import { broadcastUpdatedUser, broadcastUpdatedPrivateUser, } from 'shared/websockets/helpers' +import { removeUndefinedProps } from 'common/util/object' +import { getBettingStreakResetTimeBeforeNow } from 'shared/utils' // used for API to allow username as parm export const getUserIdFromUsername = async ( @@ -118,18 +123,72 @@ export const incrementBalance = async ( `update users set ${updates .map(([k, v]) => `${k} = ${k} + ${v}`) .join(',')} where id = $1 - returning *`, + returning id, ${updates.map(([k]) => k).join(', ')}`, [id] ) - broadcastUpdatedUser({ - id, - balance: result.balance, - cashBalance: result.cash_balance, - spiceBalance: result.spice_balance, - totalDeposits: result.total_deposits, - totalCashDeposits: result.total_cash_deposits, - }) + broadcastUpdatedUser( + removeUndefinedProps({ + id, + balance: result.balance, + cashBalance: result.cash_balance, + spiceBalance: result.spice_balance, + totalDeposits: result.total_deposits, + totalCashDeposits: result.total_cash_deposits, + }) + ) +} + +export const incrementStreak = async ( + tx: SupabaseTransaction, + user: User, + newBetTime: number +) => { + const betStreakResetTime = getBettingStreakResetTimeBeforeNow() + + const incremented = await tx.one( + ` + WITH old_data AS ( + SELECT + coalesce((data->>'lastBetTime')::bigint, 0) AS lastBetTime, + coalesce((data->>'currentBettingStreak')::int, 0) AS currentBettingStreak + FROM users + WHERE id = $1 + ) + UPDATE users SET + data = jsonb_set( + jsonb_set(data, '{currentBettingStreak}', + CASE + WHEN old_data.lastBetTime < $2 + THEN (old_data.currentBettingStreak + 1)::text::jsonb + ELSE old_data.currentBettingStreak::text::jsonb + END + ), + '{lastBetTime}', to_jsonb($3)::jsonb + ) + FROM old_data + WHERE users.id = $1 + RETURNING + CASE + WHEN old_data.lastBetTime < $2 THEN true + ELSE false + END AS streak_incremented + `, + [user.id, betStreakResetTime, newBetTime], + (r) => r.streak_incremented + ) + + broadcastUpdatedUser( + removeUndefinedProps({ + id: user.id, + currentBettingStreak: incremented + ? (user?.currentBettingStreak ?? 0) + 1 + : undefined, + lastBetTime: newBetTime, + }) + ) + + return incremented } export const bulkIncrementBalances = async ( @@ -147,7 +206,7 @@ export const bulkIncrementBalances = async ( const values = userUpdates .map((update) => - pgp.as.format(`($1, $2, $3, $4, $5)`, [ + pgp.as.format(`($1, $2, $3, $4, $5, $6)`, [ update.id, update.balance ?? 0, update.cashBalance ?? 0, @@ -163,22 +222,21 @@ export const bulkIncrementBalances = async ( balance = u.balance + v.balance, cash_balance = u.cash_balance + v.cash_balance, spice_balance = u.spice_balance + v.spice_balance, - total_deposits = u.total_deposits + v.total_deposits - from (values ${values}) as v(id, balance, cash_balance, spice_balance, total_deposits) + total_deposits = u.total_deposits + v.total_deposits, + total_cash_deposits = u.total_cash_deposits + v.total_cash_deposits + from (values ${values}) as v(id, balance, cash_balance, spice_balance, total_deposits, total_cash_deposits) where u.id = v.id - returning u.* + returning u.id, u.balance, u.cash_balance, u.spice_balance, u.total_deposits, u.total_cash_deposits `) - for (const row of results as Pick< - Row<'users'>, - 'id' | 'balance' | 'cash_balance' | 'spice_balance' | 'total_deposits' - >[]) { + for (const row of results) { broadcastUpdatedUser({ id: row.id, balance: row.balance, cashBalance: row.cash_balance, spiceBalance: row.spice_balance, totalDeposits: row.total_deposits, + totalCashDeposits: row.total_cash_deposits, }) } } diff --git a/backend/shared/src/test/users.ts b/backend/shared/src/test/users.ts index 5967b9391b..94d9102d4e 100644 --- a/backend/shared/src/test/users.ts +++ b/backend/shared/src/test/users.ts @@ -1,7 +1,7 @@ import { log } from 'shared/monitoring/log' -import { incrementBalance } from 'shared/supabase/users' +import { incrementBalance, updateUser } from 'shared/supabase/users' import { SupabaseDirectClient } from 'shared/supabase/init' -import { randomString } from 'common/util/random' +import { randomString, secureRandomString } from 'common/util/random' import * as admin from 'firebase-admin' import { createUserMain } from 'shared/create-user-main' @@ -22,7 +22,7 @@ export const getTestUsers = async ( Array.from({ length: missing }).map(async () => { const userCredential = await auth.createUser({ email: 'manifoldTestNewUser+' + randomString() + '@gmail.com', - password: randomString(), + password: secureRandomString(16), emailVerified: true, displayName: 'Manifold Test User', }) @@ -59,12 +59,21 @@ export const getTestUsers = async ( ) log('got private users') await Promise.all( - privateUsers.map((pu) => - incrementBalance(pg, pu.id, { + privateUsers.map(async (pu) => { + await incrementBalance(pg, pu.id, { balance: 10_000, + cashBalance: 1_000, + totalCashDeposits: 1_000, totalDeposits: 10_000, }) - ) + await updateUser(pg, pu.id, { + sweepstakesVerified: true, + idVerified: true, + sweepstakes5kLimit: false, + kycDocumentStatus: 'verified', + kycLastAttemptTime: Date.now(), + }) + }) ) const apiKeysMissing = privateUsers.filter((p) => !p.apiKey) log(`${privateUsers.length} user balances incremented by 10k`) @@ -85,7 +94,7 @@ export const getTestUsers = async ( [apiKeysMissing.map((p) => p.id)], (r) => ({ id: r.id as string, apiKey: r.api_key as string }) ) - return [...refetchedUsers, ...privateUsers.filter((p) => !p.apiKey)] + return [...refetchedUsers, ...privateUsers.filter((p) => p.apiKey)] } return privateUsers } diff --git a/backend/shared/src/utils.ts b/backend/shared/src/utils.ts index e4368c4adb..6902c0dc5b 100644 --- a/backend/shared/src/utils.ts +++ b/backend/shared/src/utils.ts @@ -107,13 +107,14 @@ export const isProd = () => { return admin.app().options.projectId === 'mantic-markets' } } +export const contractColumnsToSelect = `data, importance_score, conversion_score, view_count, token` export const getContract = async ( pg: SupabaseDirectClient, contractId: string ) => { const res = await pg.map( - `select data, importance_score, conversion_score, view_count, token from contracts where id = $1 + `select ${contractColumnsToSelect} from contracts where id = $1 limit 1`, [contractId], (row) => convertContract(row) @@ -129,7 +130,7 @@ export const getContractSupabase = async (contractId: string) => { export const getContractFromSlugSupabase = async (contractSlug: string) => { const pg = createSupabaseDirectClient() const res = await pg.map( - `select data, importance_score, conversion_score, view_count, token from contracts where slug = $1 + `select ${contractColumnsToSelect} from contracts where slug = $1 limit 1`, [contractSlug], (row) => convertContract(row) diff --git a/backend/supabase/answers.sql b/backend/supabase/answers.sql index 3731ba045d..02d9bfbc73 100644 --- a/backend/supabase/answers.sql +++ b/backend/supabase/answers.sql @@ -13,7 +13,7 @@ create table if not exists index integer, total_liquidity numeric default 0, subsidy_pool numeric default 0, - text_fts tsvector generated always as (to_tsvector('english'::regconfig, text)) stored, + text_fts tsvector generated always as (to_tsvector('english_extended'::regconfig, text)) stored, prob_change_day numeric default 0, prob_change_week numeric default 0, prob_change_month numeric default 0, @@ -25,9 +25,10 @@ create table if not exists is_other boolean default false not null ); --- Policies +-- Row Level Security alter table answers enable row level security; +-- Policies drop policy if exists "public read" on answers; create policy "public read" on answers for diff --git a/backend/supabase/audit_events.sql b/backend/supabase/audit_events.sql index 9ff3a4438f..8612e3633d 100644 --- a/backend/supabase/audit_events.sql +++ b/backend/supabase/audit_events.sql @@ -10,9 +10,10 @@ create table if not exists data jsonb ); --- Policies +-- Row Level Security alter table audit_events enable row level security; +-- Policies drop policy if exists "public read" on audit_events; create policy "public read" on audit_events for diff --git a/backend/supabase/chart_annotations.sql b/backend/supabase/chart_annotations.sql index 092717fc01..2b213e86ee 100644 --- a/backend/supabase/chart_annotations.sql +++ b/backend/supabase/chart_annotations.sql @@ -26,9 +26,10 @@ create table if not exists ) ); --- Policies +-- Row Level Security alter table chart_annotations enable row level security; +-- Policies drop policy if exists "public read" on chart_annotations; create policy "public read" on chart_annotations for all using (true); diff --git a/backend/supabase/chat_messages.sql b/backend/supabase/chat_messages.sql index 32c517c8d9..33330559f0 100644 --- a/backend/supabase/chat_messages.sql +++ b/backend/supabase/chat_messages.sql @@ -8,9 +8,10 @@ create table if not exists created_time timestamp with time zone default now() not null ); --- Policies +-- Row Level Security alter table chat_messages enable row level security; +-- Policies drop policy if exists "public read" on chat_messages; create policy "public read" on chat_messages for diff --git a/backend/supabase/contract_bets.sql b/backend/supabase/contract_bets.sql index 826530c8ab..8db3a4d4d7 100644 --- a/backend/supabase/contract_bets.sql +++ b/backend/supabase/contract_bets.sql @@ -67,7 +67,24 @@ begin end; $function$; +-- Row Level Security +alter table contract_bets enable row level security; + -- Indexes +drop index if exists contract_bets_bet_id_key; + +create unique index contract_bets_bet_id_key on public.contract_bets using btree (bet_id); + +drop index if exists contract_bets_contract_limit_orders; + +create index contract_bets_contract_limit_orders on public.contract_bets using btree ( + contract_id, + is_filled, + is_cancelled, + is_redemption, + created_time desc +); + drop index if exists contract_bets_contract_user_id; create index contract_bets_contract_user_id on public.contract_bets using btree (contract_id, user_id, created_time desc); @@ -76,36 +93,22 @@ drop index if exists contract_bets_created_time; create index contract_bets_created_time on public.contract_bets using btree (contract_id, created_time desc); -drop index if exists contract_bets_user_id; +drop index if exists contract_bets_created_time_only; -create index contract_bets_user_id on public.contract_bets using btree (user_id, created_time desc); +create index contract_bets_created_time_only on public.contract_bets using btree (created_time desc); drop index if exists contract_bets_historical_probs; create index contract_bets_historical_probs on public.contract_bets using btree (contract_id, answer_id, created_time desc) include (prob_before, prob_after); -drop index if exists contract_bets_created_time_only; +drop index if exists contract_bets_user_id; -create index contract_bets_created_time_only on public.contract_bets using btree (created_time desc); +create index contract_bets_user_id on public.contract_bets using btree (user_id, created_time desc); drop index if exists contract_bets_user_id_contract_id; create index contract_bets_user_id_contract_id on public.contract_bets using btree (user_id, contract_id, created_time); -drop index if exists contract_bets_contract_limit_orders; - -create index contract_bets_contract_limit_orders on public.contract_bets using btree ( - contract_id, - is_filled, - is_cancelled, - is_redemption, - created_time desc -); - drop index if exists contract_bets_user_outstanding_limit_orders; create index contract_bets_user_outstanding_limit_orders on public.contract_bets using btree (user_id, is_filled, is_cancelled); - -drop index if exists contract_bets_bet_id_key; - -create unique index contract_bets_bet_id_key on public.contract_bets using btree (bet_id); diff --git a/backend/supabase/contract_comment_edits.sql b/backend/supabase/contract_comment_edits.sql index c196256349..357eafed50 100644 --- a/backend/supabase/contract_comment_edits.sql +++ b/backend/supabase/contract_comment_edits.sql @@ -9,9 +9,10 @@ create table if not exists created_time timestamp with time zone default now() not null ); --- Policies +-- Row Level Security alter table contract_comment_edits enable row level security; +-- Policies drop policy if exists "public read" on contract_comment_edits; create policy "public read" on contract_comment_edits for diff --git a/backend/supabase/contract_comments.sql b/backend/supabase/contract_comments.sql index a3c7b477e8..cd43df3845 100644 --- a/backend/supabase/contract_comments.sql +++ b/backend/supabase/contract_comments.sql @@ -31,9 +31,10 @@ or replace function public.comment_populate_cols () returns trigger language plp return new; end $function$; --- Policies +-- Row Level Security alter table contract_comments enable row level security; +-- Policies drop policy if exists "public read" on contract_comments; create policy "public read" on contract_comments for diff --git a/backend/supabase/contract_edits.sql b/backend/supabase/contract_edits.sql index be6809443f..9568b8952e 100644 --- a/backend/supabase/contract_edits.sql +++ b/backend/supabase/contract_edits.sql @@ -10,9 +10,10 @@ create table if not exists updated_keys text[] ); --- Policies +-- Row Level Security alter table contract_edits enable row level security; +-- Policies drop policy if exists "public read" on contract_edits; create policy "public read" on contract_edits for diff --git a/backend/supabase/contract_embeddings.sql b/backend/supabase/contract_embeddings.sql index 88be70292c..3ae2e005e0 100644 --- a/backend/supabase/contract_embeddings.sql +++ b/backend/supabase/contract_embeddings.sql @@ -6,9 +6,10 @@ create table if not exists embedding vector (1536) not null ); --- Policies +-- Row Level Security alter table contract_embeddings enable row level security; +-- Policies drop policy if exists "admin write access" on contract_embeddings; create policy "admin write access" on contract_embeddings for all to service_role; diff --git a/backend/supabase/contract_follows.sql b/backend/supabase/contract_follows.sql index bd99e37a4f..629b437e51 100644 --- a/backend/supabase/contract_follows.sql +++ b/backend/supabase/contract_follows.sql @@ -6,9 +6,10 @@ create table if not exists created_time timestamp with time zone default now() not null ); --- Policies +-- Row Level Security alter table contract_follows enable row level security; +-- Policies drop policy if exists "public read" on contract_follows; create policy "public read" on contract_follows for diff --git a/backend/supabase/contract_liquidity.sql b/backend/supabase/contract_liquidity.sql index 5bfe28a7a5..c7e677af9b 100644 --- a/backend/supabase/contract_liquidity.sql +++ b/backend/supabase/contract_liquidity.sql @@ -6,9 +6,10 @@ create table if not exists data jsonb not null ); --- Policies +-- Row Level Security alter table contract_liquidity enable row level security; +-- Policies drop policy if exists "public read" on contract_liquidity; create policy "public read" on contract_liquidity for diff --git a/backend/supabase/contracts.sql b/backend/supabase/contracts.sql index 9b8f203fc3..94daaa121e 100644 --- a/backend/supabase/contracts.sql +++ b/backend/supabase/contracts.sql @@ -15,10 +15,12 @@ create table if not exists resolution_probability numeric, resolution text, popularity_score numeric default 0 not null, - question_fts tsvector generated always as (to_tsvector('english'::regconfig, question)) stored, + question_fts tsvector generated always as ( + to_tsvector('english_extended'::regconfig, question) + ) stored, description_fts tsvector generated always as ( to_tsvector( - 'english'::regconfig, + 'english_extended'::regconfig, add_creator_name_to_description (data) ) ) stored, @@ -134,9 +136,10 @@ begin end; $function$; --- Policies +-- Row Level Security alter table contracts enable row level security; +-- Policies drop policy if exists "public read" on contracts; create policy "public read" on contracts for @@ -160,6 +163,10 @@ drop index if exists contracts_creator_id; create index contracts_creator_id on public.contracts using btree (creator_id, created_time); +drop index if exists contracts_daily_score; + +create index contracts_daily_score on public.contracts using btree (daily_score desc); + drop index if exists contracts_elasticity; create index contracts_elasticity on public.contracts using btree ((((data ->> 'elasticity'::text))::numeric) desc); @@ -224,6 +231,10 @@ drop index if exists description_fts; create index description_fts on public.contracts using gin (description_fts); +drop index if exists market_tier_idx; + +create index market_tier_idx on public.contracts using btree (tier); + drop index if exists question_fts; create index question_fts on public.contracts using gin (question_fts); @@ -231,11 +242,3 @@ create index question_fts on public.contracts using gin (question_fts); drop index if exists question_nostop_fts; create index question_nostop_fts on public.contracts using gin (question_nostop_fts); - -drop index if exists market_tier_idx; - -create index market_tier_idx on public.contracts using btree (tier); - -drop index if exists contracts_daily_score; - -create index contracts_daily_score on public.contracts using btree (daily_score desc); diff --git a/backend/supabase/creator_portfolio_history.sql b/backend/supabase/creator_portfolio_history.sql index cb75a6ade4..d4b84ec6ef 100644 --- a/backend/supabase/creator_portfolio_history.sql +++ b/backend/supabase/creator_portfolio_history.sql @@ -10,9 +10,10 @@ create table if not exists views integer not null ); --- Policies +-- Row Level Security alter table creator_portfolio_history enable row level security; +-- Policies drop policy if exists "public read" on creator_portfolio_history; create policy "public read" on creator_portfolio_history for diff --git a/backend/supabase/daily_stats.sql b/backend/supabase/daily_stats.sql index 3bc815a18d..2710a6d2e4 100644 --- a/backend/supabase/daily_stats.sql +++ b/backend/supabase/daily_stats.sql @@ -27,9 +27,10 @@ create table if not exists cash_bet_amount numeric ); --- Policies +-- Row Level Security alter table daily_stats enable row level security; +-- Policies drop policy if exists "public read" on daily_stats; create policy "public read" on daily_stats for diff --git a/backend/supabase/dashboard_groups.sql b/backend/supabase/dashboard_groups.sql index b97f79827a..703d5f502d 100644 --- a/backend/supabase/dashboard_groups.sql +++ b/backend/supabase/dashboard_groups.sql @@ -12,9 +12,10 @@ add constraint dashboard_groups_dashboard_id_fkey foreign key (dashboard_id) ref alter table dashboard_groups add constraint public_dashboard_groups_group_id_fkey foreign key (group_id) references groups (id) on update cascade on delete cascade; --- Policies +-- Row Level Security alter table dashboard_groups enable row level security; +-- Policies drop policy if exists "Enable read access for admin" on dashboard_groups; create policy "Enable read access for admin" on dashboard_groups for diff --git a/backend/supabase/dashboards.sql b/backend/supabase/dashboards.sql index 745f6b6ce0..6bcbc9a519 100644 --- a/backend/supabase/dashboards.sql +++ b/backend/supabase/dashboards.sql @@ -21,9 +21,10 @@ create table if not exists alter table dashboards add constraint dashboards_creator_id_fkey foreign key (creator_id) references users (id); --- Policies +-- Row Level Security alter table dashboards enable row level security; +-- Policies drop policy if exists "Enable read access for admin" on dashboards; create policy "Enable read access for admin" on dashboards for diff --git a/backend/supabase/discord_messages_markets.sql b/backend/supabase/discord_messages_markets.sql index 121d0e664b..d3e011b21a 100644 --- a/backend/supabase/discord_messages_markets.sql +++ b/backend/supabase/discord_messages_markets.sql @@ -9,6 +9,9 @@ create table if not exists thread_id text ); +-- Row Level Security +alter table discord_messages_markets enable row level security; + -- Indexes drop index if exists discord_messages_markets_pkey; diff --git a/backend/supabase/discord_users.sql b/backend/supabase/discord_users.sql index 8deb90ace4..c64cd16585 100644 --- a/backend/supabase/discord_users.sql +++ b/backend/supabase/discord_users.sql @@ -6,6 +6,9 @@ create table if not exists user_id text not null ); +-- Row Level Security +alter table discord_users enable row level security; + -- Indexes drop index if exists discord_users_pkey; diff --git a/backend/supabase/functions.sql b/backend/supabase/functions.sql index 9209b1cfa8..ba5e36f613 100644 --- a/backend/supabase/functions.sql +++ b/backend/supabase/functions.sql @@ -277,9 +277,12 @@ or replace function public.get_donations_by_charity () returns table ( ) language sql as $function$ select to_id as charity_id, count(distinct from_id) as num_supporters, - sum(case when token = 'M$' - then amount / 100 - else amount / 1000 end + sum(case + when token = 'M$' then amount / 100 + when token = 'SPICE' then amount / 1000 + when token = 'CASH' then amount + else 0 + end ) as total from txns where category = 'CHARITY' @@ -320,7 +323,6 @@ BEGIN END; $function$; - create or replace function public.get_non_empty_private_message_channel_ids ( p_user_id text, @@ -435,7 +437,6 @@ order by last_updated_time desc limit max $function$; - create or replace function public.get_user_bet_contracts (this_user_id text, this_limit integer) returns table (data json) language sql immutable parallel SAFE as $function$ select c.data diff --git a/backend/supabase/gidx_receipts.sql b/backend/supabase/gidx_receipts.sql index f35a69f975..b368810257 100644 --- a/backend/supabase/gidx_receipts.sql +++ b/backend/supabase/gidx_receipts.sql @@ -1,7 +1,7 @@ -- This file is autogenerated from regen-schema.ts create table if not exists gidx_receipts ( - id bigint not null, + id bigint primary key generated always as identity, user_id text, merchant_transaction_id text not null, session_id text not null, @@ -13,7 +13,7 @@ create table if not exists transaction_status_code text, transaction_status_message text, merchant_session_id text, - amount bigint, + amount numeric(20, 2), currency text, payment_method_type text, payment_amount_type text, @@ -29,11 +29,10 @@ create table if not exists alter table gidx_receipts add constraint gidx_receipts_user_id_fkey foreign key (user_id) references users (id); --- Indexes -drop index if exists gidx_receipts_pkey; - -create unique index gidx_receipts_pkey on public.gidx_receipts using btree (id); +-- Row Level Security +alter table gidx_receipts enable row level security; +-- Indexes drop index if exists cash_out_receipts_user_id_idx; create index cash_out_receipts_user_id_idx on public.gidx_receipts using btree (user_id); diff --git a/backend/supabase/group_contracts.sql b/backend/supabase/group_contracts.sql index b6ff62feb8..34e1b7309d 100644 --- a/backend/supabase/group_contracts.sql +++ b/backend/supabase/group_contracts.sql @@ -6,9 +6,10 @@ create table if not exists alter table group_contracts add constraint group_contracts_group_id_fke foreign key (group_id) references groups (id) not valid; --- Policies +-- Row Level Security alter table group_contracts enable row level security; +-- Policies drop policy if exists "Enable read access for bets on markets user can access" on group_contracts; create policy "Enable read access for bets on markets user can access" on group_contracts for diff --git a/backend/supabase/group_embeddings.sql b/backend/supabase/group_embeddings.sql index b049a53ed8..0007bdd662 100644 --- a/backend/supabase/group_embeddings.sql +++ b/backend/supabase/group_embeddings.sql @@ -10,9 +10,10 @@ create table if not exists alter table group_embeddings add constraint public_group_embeddings_group_id_fkey foreign key (group_id) references groups (id) on update cascade on delete cascade; --- Policies +-- Row Level Security alter table group_embeddings enable row level security; +-- Policies drop policy if exists "admin write access" on group_embeddings; create policy "admin write access" on group_embeddings for all to service_role; diff --git a/backend/supabase/group_invites.sql b/backend/supabase/group_invites.sql index e08e6b2f76..b0a8b31c9b 100644 --- a/backend/supabase/group_invites.sql +++ b/backend/supabase/group_invites.sql @@ -43,9 +43,10 @@ BEGIN END; $function$; --- Policies +-- Row Level Security alter table group_invites enable row level security; +-- Policies drop policy if exists "Enable read access for admin" on group_invites; create policy "Enable read access for admin" on group_invites for diff --git a/backend/supabase/group_members.sql b/backend/supabase/group_members.sql index 0e42ba9373..b3a06e6e6c 100644 --- a/backend/supabase/group_members.sql +++ b/backend/supabase/group_members.sql @@ -32,9 +32,10 @@ or replace function public.increment_group_members () returns trigger language p return new; end $function$; --- Policies +-- Row Level Security alter table group_members enable row level security; +-- Policies drop policy if exists "public read" on group_members; create policy "public read" on group_members for diff --git a/backend/supabase/groups.sql b/backend/supabase/groups.sql index 3b423208ed..bd62a3dc73 100644 --- a/backend/supabase/groups.sql +++ b/backend/supabase/groups.sql @@ -30,9 +30,10 @@ or replace function public.group_populate_cols () returns trigger language plpgs return new; end $function$; --- Policies +-- Row Level Security alter table groups enable row level security; +-- Policies drop policy if exists "public read" on groups; create policy "public read" on groups for diff --git a/backend/supabase/kyc_bonus_rewards.sql b/backend/supabase/kyc_bonus_rewards.sql index f5302a4bba..e5d6a3edbd 100644 --- a/backend/supabase/kyc_bonus_rewards.sql +++ b/backend/supabase/kyc_bonus_rewards.sql @@ -12,6 +12,9 @@ create table if not exists alter table kyc_bonus_rewards add constraint kyc_bonus_rewards_user_id_fkey foreign key (user_id) references users (id); +-- Row Level Security +alter table kyc_bonus_rewards enable row level security; + -- Indexes drop index if exists kyc_bonus_rewards_pkey; diff --git a/backend/supabase/league_chats.sql b/backend/supabase/league_chats.sql index c489b28fe9..866635e372 100644 --- a/backend/supabase/league_chats.sql +++ b/backend/supabase/league_chats.sql @@ -10,9 +10,10 @@ create table if not exists owner_id text ); --- Policies +-- Row Level Security alter table league_chats enable row level security; +-- Policies drop policy if exists "public read" on league_chats; create policy "public read" on league_chats for diff --git a/backend/supabase/leagues.sql b/backend/supabase/leagues.sql index d7fdfcab2e..3300b698f1 100644 --- a/backend/supabase/leagues.sql +++ b/backend/supabase/leagues.sql @@ -12,9 +12,10 @@ create table if not exists id uuid default gen_random_uuid () not null ); --- Policies +-- Row Level Security alter table leagues enable row level security; +-- Policies drop policy if exists "public read" on leagues; create policy "public read" on leagues for diff --git a/backend/supabase/love_answers.sql b/backend/supabase/love_answers.sql index 1fd0d7b50d..dd05780b78 100644 --- a/backend/supabase/love_answers.sql +++ b/backend/supabase/love_answers.sql @@ -10,9 +10,10 @@ create table if not exists integer integer ); --- Policies +-- Row Level Security alter table love_answers enable row level security; +-- Policies drop policy if exists "public read" on love_answers; create policy "public read" on love_answers for diff --git a/backend/supabase/love_compatibility_answers.sql b/backend/supabase/love_compatibility_answers.sql index c27db8b61b..afba57feea 100644 --- a/backend/supabase/love_compatibility_answers.sql +++ b/backend/supabase/love_compatibility_answers.sql @@ -11,9 +11,10 @@ create table if not exists importance integer not null ); --- Policies +-- Row Level Security alter table love_compatibility_answers enable row level security; +-- Policies drop policy if exists "public read" on love_compatibility_answers; create policy "public read" on love_compatibility_answers for diff --git a/backend/supabase/love_likes.sql b/backend/supabase/love_likes.sql index 58ed2d9d9a..a219e13be7 100644 --- a/backend/supabase/love_likes.sql +++ b/backend/supabase/love_likes.sql @@ -7,9 +7,10 @@ create table if not exists created_time timestamp with time zone default now() not null ); --- Policies +-- Row Level Security alter table love_likes enable row level security; +-- Policies drop policy if exists "public read" on love_likes; create policy "public read" on love_likes for diff --git a/backend/supabase/love_questions.sql b/backend/supabase/love_questions.sql index bdd27a464f..07165e3c32 100644 --- a/backend/supabase/love_questions.sql +++ b/backend/supabase/love_questions.sql @@ -10,9 +10,10 @@ create table if not exists multiple_choice_options jsonb ); --- Policies +-- Row Level Security alter table love_questions enable row level security; +-- Policies drop policy if exists "public read" on love_questions; create policy "public read" on love_questions for all using (true); diff --git a/backend/supabase/love_ships.sql b/backend/supabase/love_ships.sql index 00d4757213..f9842ab1c3 100644 --- a/backend/supabase/love_ships.sql +++ b/backend/supabase/love_ships.sql @@ -8,9 +8,10 @@ create table if not exists created_time timestamp with time zone default now() not null ); --- Policies +-- Row Level Security alter table love_ships enable row level security; +-- Policies drop policy if exists "public read" on love_ships; create policy "public read" on love_ships for diff --git a/backend/supabase/love_stars.sql b/backend/supabase/love_stars.sql index 527be26dec..311767b4e6 100644 --- a/backend/supabase/love_stars.sql +++ b/backend/supabase/love_stars.sql @@ -7,9 +7,10 @@ create table if not exists created_time timestamp with time zone default now() not null ); --- Policies +-- Row Level Security alter table love_stars enable row level security; +-- Policies drop policy if exists "public read" on love_stars; create policy "public read" on love_stars for diff --git a/backend/supabase/love_waitlist.sql b/backend/supabase/love_waitlist.sql index bc621fdca2..3e762d7d17 100644 --- a/backend/supabase/love_waitlist.sql +++ b/backend/supabase/love_waitlist.sql @@ -6,9 +6,10 @@ create table if not exists created_time timestamp with time zone default now() not null ); --- Policies +-- Row Level Security alter table love_waitlist enable row level security; +-- Policies drop policy if exists "anon insert" on love_waitlist; create policy "anon insert" on love_waitlist for insert diff --git a/backend/supabase/lover_comments.sql b/backend/supabase/lover_comments.sql index 519f04fc65..532a523ca5 100644 --- a/backend/supabase/lover_comments.sql +++ b/backend/supabase/lover_comments.sql @@ -13,9 +13,10 @@ create table if not exists hidden boolean default false not null ); --- Policies +-- Row Level Security alter table lover_comments enable row level security; +-- Policies drop policy if exists "public read" on lover_comments; create policy "public read" on lover_comments for all using (true); diff --git a/backend/supabase/lovers.sql b/backend/supabase/lovers.sql index fd09349933..fb0ac9a9cb 100644 --- a/backend/supabase/lovers.sql +++ b/backend/supabase/lovers.sql @@ -45,9 +45,10 @@ create table if not exists age integer default 18 not null ); --- Policies +-- Row Level Security alter table lovers enable row level security; +-- Policies drop policy if exists "public read" on lovers; create policy "public read" on lovers for diff --git a/backend/supabase/mana_supply_stats.sql b/backend/supabase/mana_supply_stats.sql index 42e395bd4f..fd5588815b 100644 --- a/backend/supabase/mana_supply_stats.sql +++ b/backend/supabase/mana_supply_stats.sql @@ -17,9 +17,10 @@ create table if not exists amm_cash_liquidity numeric default 0 not null ); --- Policies +-- Row Level Security alter table mana_supply_stats enable row level security; +-- Policies drop policy if exists "public read" on mana_supply_stats; create policy "public read" on mana_supply_stats for diff --git a/backend/supabase/manachan_tweets.sql b/backend/supabase/manachan_tweets.sql index 3533600367..aad028b3aa 100644 --- a/backend/supabase/manachan_tweets.sql +++ b/backend/supabase/manachan_tweets.sql @@ -10,9 +10,10 @@ create table if not exists username text ); --- Policies +-- Row Level Security alter table manachan_tweets enable row level security; +-- Policies drop policy if exists "Enable read access for all users" on manachan_tweets; create policy "Enable read access for all users" on manachan_tweets for diff --git a/backend/supabase/manalink_claims.sql b/backend/supabase/manalink_claims.sql index 3ab3b4342f..954897e187 100644 --- a/backend/supabase/manalink_claims.sql +++ b/backend/supabase/manalink_claims.sql @@ -2,9 +2,10 @@ create table if not exists manalink_claims (manalink_id text not null, txn_id text not null); --- Policies +-- Row Level Security alter table manalink_claims enable row level security; +-- Policies drop policy if exists "public read" on manalink_claims; create policy "public read" on manalink_claims for diff --git a/backend/supabase/manalinks.sql b/backend/supabase/manalinks.sql index 4fe95dffdd..ec78c1f765 100644 --- a/backend/supabase/manalinks.sql +++ b/backend/supabase/manalinks.sql @@ -10,9 +10,10 @@ create table if not exists message text ); --- Policies +-- Row Level Security alter table manalinks enable row level security; +-- Policies drop policy if exists "Enable read access for admin" on manalinks; create policy "Enable read access for admin" on manalinks for diff --git a/backend/supabase/market_ads.sql b/backend/supabase/market_ads.sql index 8cd4689c3c..bec93c7ebd 100644 --- a/backend/supabase/market_ads.sql +++ b/backend/supabase/market_ads.sql @@ -14,9 +14,10 @@ create table if not exists alter table market_ads add constraint market_ads_market_id_fkey foreign key (market_id) references contracts (id); --- Policies +-- Row Level Security alter table market_ads enable row level security; +-- Policies drop policy if exists "admin write access" on market_ads; create policy "admin write access" on market_ads for all to service_role; diff --git a/backend/supabase/news.sql b/backend/supabase/news.sql index 02bc63454e..3cdf786d5e 100644 --- a/backend/supabase/news.sql +++ b/backend/supabase/news.sql @@ -16,9 +16,10 @@ create table if not exists group_ids text[] ); --- Policies +-- Row Level Security alter table news enable row level security; +-- Policies drop policy if exists "public read" on news; create policy "public read" on news for diff --git a/backend/supabase/old_post_comments.sql b/backend/supabase/old_post_comments.sql index e564aa5688..dcc4482de0 100644 --- a/backend/supabase/old_post_comments.sql +++ b/backend/supabase/old_post_comments.sql @@ -25,9 +25,10 @@ or replace function public.post_comment_populate_cols () returns trigger languag return new; end $function$; --- Policies +-- Row Level Security alter table old_post_comments enable row level security; +-- Policies drop policy if exists "auth read" on old_post_comments; create policy "auth read" on old_post_comments for diff --git a/backend/supabase/old_posts.sql b/backend/supabase/old_posts.sql index 0934fb061e..c4fcaed0a0 100644 --- a/backend/supabase/old_posts.sql +++ b/backend/supabase/old_posts.sql @@ -30,14 +30,15 @@ or replace function public.post_populate_cols () returns trigger language plpgsq return new; end $function$; --- Policies +-- Row Level Security alter table old_posts enable row level security; -drop policy if exists "admin read" on old_posts; +-- Policies +drop policy if exists "public read" on old_posts; -create policy "admin read" on old_posts for +create policy "public read" on old_posts for select - to service_role using (true); + using (true); -- Indexes drop index if exists posts_pkey; diff --git a/backend/supabase/platform_calibration.sql b/backend/supabase/platform_calibration.sql index 77b5cd7877..c991b59450 100644 --- a/backend/supabase/platform_calibration.sql +++ b/backend/supabase/platform_calibration.sql @@ -6,9 +6,10 @@ create table if not exists data jsonb not null ); --- Policies +-- Row Level Security alter table platform_calibration enable row level security; +-- Policies drop policy if exists "public read" on platform_calibration; create policy "public read" on platform_calibration for diff --git a/backend/supabase/portfolios.sql b/backend/supabase/portfolios.sql index e738f63b58..4238e8b69a 100644 --- a/backend/supabase/portfolios.sql +++ b/backend/supabase/portfolios.sql @@ -9,9 +9,10 @@ create table if not exists created_time timestamp with time zone default now() not null ); --- Policies +-- Row Level Security alter table portfolios enable row level security; +-- Policies drop policy if exists "public read" on portfolios; create policy "public read" on portfolios for diff --git a/backend/supabase/posts.sql b/backend/supabase/posts.sql index 3b2cc8904e..f20b85b427 100644 --- a/backend/supabase/posts.sql +++ b/backend/supabase/posts.sql @@ -12,9 +12,10 @@ create table if not exists bet_id text ); --- Policies +-- Row Level Security alter table posts enable row level security; +-- Policies drop policy if exists "public read" on posts; create policy "public read" on posts for diff --git a/backend/supabase/private_user_message_channel_members.sql b/backend/supabase/private_user_message_channel_members.sql index 1266c25ab2..7b130daf34 100644 --- a/backend/supabase/private_user_message_channel_members.sql +++ b/backend/supabase/private_user_message_channel_members.sql @@ -10,6 +10,9 @@ create table if not exists notify_after_time timestamp with time zone default now() not null ); +-- Row Level Security +alter table private_user_message_channel_members enable row level security; + -- Indexes drop index if exists private_user_message_channel_members_pkey; diff --git a/backend/supabase/private_user_message_channels.sql b/backend/supabase/private_user_message_channels.sql index 6d969727bb..389c1a0425 100644 --- a/backend/supabase/private_user_message_channels.sql +++ b/backend/supabase/private_user_message_channels.sql @@ -7,9 +7,10 @@ create table if not exists title text ); --- Policies +-- Row Level Security alter table private_user_message_channels enable row level security; +-- Policies drop policy if exists "public read" on private_user_message_channels; create policy "public read" on private_user_message_channels for all using (true); diff --git a/backend/supabase/private_user_messages.sql b/backend/supabase/private_user_messages.sql index 8cf1895829..7514433eb0 100644 --- a/backend/supabase/private_user_messages.sql +++ b/backend/supabase/private_user_messages.sql @@ -9,6 +9,9 @@ create table if not exists visibility text default 'private'::text not null ); +-- Row Level Security +alter table private_user_messages enable row level security; + -- Indexes drop index if exists private_user_messages_pkey; diff --git a/backend/supabase/private_user_phone_numbers.sql b/backend/supabase/private_user_phone_numbers.sql index 63fba857cf..40569c88f6 100644 --- a/backend/supabase/private_user_phone_numbers.sql +++ b/backend/supabase/private_user_phone_numbers.sql @@ -8,6 +8,9 @@ create table if not exists phone_number text not null ); +-- Row Level Security +alter table private_user_phone_numbers enable row level security; + -- Indexes drop index if exists private_user_phone_numbers_pkey; diff --git a/backend/supabase/private_user_seen_message_channels.sql b/backend/supabase/private_user_seen_message_channels.sql index 679e937eb2..d694733b23 100644 --- a/backend/supabase/private_user_seen_message_channels.sql +++ b/backend/supabase/private_user_seen_message_channels.sql @@ -7,9 +7,10 @@ create table if not exists created_time timestamp with time zone default now() not null ); --- Policies +-- Row Level Security alter table private_user_seen_message_channels enable row level security; +-- Policies drop policy if exists "private member insert" on private_user_seen_message_channels; create policy "private member insert" on private_user_seen_message_channels for insert diff --git a/backend/supabase/private_users.sql b/backend/supabase/private_users.sql index ff19360924..d6494c1156 100644 --- a/backend/supabase/private_users.sql +++ b/backend/supabase/private_users.sql @@ -7,9 +7,10 @@ create table if not exists weekly_portfolio_email_sent boolean default false ); --- Policies +-- Row Level Security alter table private_users enable row level security; +-- Policies drop policy if exists "private read" on private_users; create policy "private read" on private_users for diff --git a/backend/supabase/push_notification_tickets.sql b/backend/supabase/push_notification_tickets.sql index a7e53b3885..780e088106 100644 --- a/backend/supabase/push_notification_tickets.sql +++ b/backend/supabase/push_notification_tickets.sql @@ -10,6 +10,9 @@ create table if not exists receipt_error text ); +-- Row Level Security +alter table push_notification_tickets enable row level security; + -- Indexes drop index if exists push_notification_tickets_pkey; diff --git a/backend/supabase/q_and_a.sql b/backend/supabase/q_and_a.sql index f58f345790..2352df9e66 100644 --- a/backend/supabase/q_and_a.sql +++ b/backend/supabase/q_and_a.sql @@ -10,9 +10,10 @@ create table if not exists deleted boolean default false not null ); --- Policies +-- Row Level Security alter table q_and_a enable row level security; +-- Policies drop policy if exists "public read" on q_and_a; create policy "public read" on q_and_a for diff --git a/backend/supabase/q_and_a_answers.sql b/backend/supabase/q_and_a_answers.sql index 2a745eb35d..18c1bfaa7d 100644 --- a/backend/supabase/q_and_a_answers.sql +++ b/backend/supabase/q_and_a_answers.sql @@ -10,9 +10,10 @@ create table if not exists deleted boolean default false not null ); --- Policies +-- Row Level Security alter table q_and_a_answers enable row level security; +-- Policies drop policy if exists "public read" on q_and_a_answers; create policy "public read" on q_and_a_answers for diff --git a/backend/supabase/scheduler_info.sql b/backend/supabase/scheduler_info.sql index d6b720f30d..070dacef6e 100644 --- a/backend/supabase/scheduler_info.sql +++ b/backend/supabase/scheduler_info.sql @@ -1,15 +1,17 @@ +-- This file is autogenerated from regen-schema.ts create table if not exists scheduler_info ( - id bigint generated by default as identity primary key, + id bigint not null, job_name text not null, created_time timestamp with time zone default now() not null, last_start_time timestamp with time zone, last_end_time timestamp with time zone ); --- Policies +-- Row Level Security alter table scheduler_info enable row level security; +-- Policies drop policy if exists "public read" on scheduler_info; create policy "public read" on scheduler_info for all using (true); @@ -18,3 +20,7 @@ create policy "public read" on scheduler_info for all using (true); drop index if exists scheduler_info_job_name_key; create unique index scheduler_info_job_name_key on public.scheduler_info using btree (job_name); + +drop index if exists scheduler_info_pkey; + +create unique index scheduler_info_pkey on public.scheduler_info using btree (id); diff --git a/backend/supabase/seed.sql b/backend/supabase/seed.sql index 469c1c8e2e..2e8a192362 100644 --- a/backend/supabase/seed.sql +++ b/backend/supabase/seed.sql @@ -23,6 +23,9 @@ create extension if not exists pg_trgm; /* for UUID generation */ create extension if not exists pgcrypto; +/* for accent-insensitive search */ +create extension if not exists unaccent; + /* enable `explain` via the HTTP API for convenience */ alter role authenticator set @@ -54,5 +57,17 @@ hword, hword_part, word with + unaccent, english_stem_nostop, english_prefix; + +create text search configuration public.english_extended ( + copy = english +); + +alter text search configuration public.english_extended +alter mapping for hword, +hword_part, +word +with + unaccent; diff --git a/backend/supabase/sent_emails.sql b/backend/supabase/sent_emails.sql index aff81545d6..da59f95764 100644 --- a/backend/supabase/sent_emails.sql +++ b/backend/supabase/sent_emails.sql @@ -7,6 +7,9 @@ create table if not exists created_time timestamp with time zone default now() not null ); +-- Row Level Security +alter table sent_emails enable row level security; + -- Indexes drop index if exists sent_emails_pkey; diff --git a/backend/supabase/stats.sql b/backend/supabase/stats.sql index 2c579d34f1..15744b3caf 100644 --- a/backend/supabase/stats.sql +++ b/backend/supabase/stats.sql @@ -2,9 +2,10 @@ create table if not exists stats (title text not null, daily_values numeric[]); --- Policies +-- Row Level Security alter table stats enable row level security; +-- Policies drop policy if exists "public read" on stats; create policy "public read" on stats for diff --git a/backend/supabase/topic_embeddings.sql b/backend/supabase/topic_embeddings.sql index a2a8933248..497b9968e3 100644 --- a/backend/supabase/topic_embeddings.sql +++ b/backend/supabase/topic_embeddings.sql @@ -6,9 +6,10 @@ create table if not exists embedding vector (1536) not null ); --- Policies +-- Row Level Security alter table topic_embeddings enable row level security; +-- Policies drop policy if exists "admin write access" on topic_embeddings; create policy "admin write access" on topic_embeddings for all to service_role; diff --git a/backend/supabase/txn_summary_stats.sql b/backend/supabase/txn_summary_stats.sql index e96a06e935..17178ebf30 100644 --- a/backend/supabase/txn_summary_stats.sql +++ b/backend/supabase/txn_summary_stats.sql @@ -14,9 +14,10 @@ create table if not exists cash_amount numeric default 0 not null ); --- Policies +-- Row Level Security alter table txn_summary_stats enable row level security; +-- Policies drop policy if exists "public read" on txn_summary_stats; create policy "public read" on txn_summary_stats for diff --git a/backend/supabase/txns.sql b/backend/supabase/txns.sql index 02c23aa2d6..8df0c81928 100644 --- a/backend/supabase/txns.sql +++ b/backend/supabase/txns.sql @@ -25,9 +25,10 @@ create table if not exists ) ); --- Policies +-- Row Level Security alter table txns enable row level security; +-- Policies drop policy if exists "public read" on txns; create policy "public read" on txns for @@ -43,18 +44,18 @@ drop index if exists txns_category_native; create index txns_category_native on public.txns using btree (category); -drop index if exists txns_from_created_time; - -create index txns_from_created_time on public.txns using btree (from_id, created_time); - -drop index if exists txns_to_created_time; +drop index if exists txns_category_to_id; -create index txns_to_created_time on public.txns using btree (to_id, created_time); +create index txns_category_to_id on public.txns using btree (category, to_id); drop index if exists txns_category_to_id_from_id; create index txns_category_to_id_from_id on public.txns using btree (category, to_id, from_id); -drop index if exists txns_category_to_id; +drop index if exists txns_from_created_time; -create index txns_category_to_id on public.txns using btree (category, to_id); +create index txns_from_created_time on public.txns using btree (from_id, created_time); + +drop index if exists txns_to_created_time; + +create index txns_to_created_time on public.txns using btree (to_id, created_time); diff --git a/backend/supabase/user_comment_view_events.sql b/backend/supabase/user_comment_view_events.sql index f5736c981a..bc72b179e1 100644 --- a/backend/supabase/user_comment_view_events.sql +++ b/backend/supabase/user_comment_view_events.sql @@ -8,6 +8,9 @@ create table if not exists comment_id text not null ); +-- Row Level Security +alter table user_comment_view_events enable row level security; + -- Indexes drop index if exists user_comment_view_events_pkey; diff --git a/backend/supabase/user_contract_interactions.sql b/backend/supabase/user_contract_interactions.sql index 54c27326c9..d69f46eb6a 100644 --- a/backend/supabase/user_contract_interactions.sql +++ b/backend/supabase/user_contract_interactions.sql @@ -13,6 +13,9 @@ create table if not exists feed_type text ); +-- Row Level Security +alter table user_contract_interactions enable row level security; + -- Indexes drop index if exists user_contract_interactions_pkey; diff --git a/backend/supabase/user_contract_metrics.sql b/backend/supabase/user_contract_metrics.sql index 5bde2b26ca..dbd62dbb3d 100644 --- a/backend/supabase/user_contract_metrics.sql +++ b/backend/supabase/user_contract_metrics.sql @@ -15,9 +15,10 @@ create table if not exists profit_adjustment numeric ); --- Policies +-- Row Level Security alter table user_contract_metrics enable row level security; +-- Policies drop policy if exists "public read" on user_contract_metrics; create policy "public read" on user_contract_metrics for diff --git a/backend/supabase/user_contract_views.sql b/backend/supabase/user_contract_views.sql index 49f0b68bec..7c4d61e9b8 100644 --- a/backend/supabase/user_contract_views.sql +++ b/backend/supabase/user_contract_views.sql @@ -25,9 +25,10 @@ create table if not exists constraint user_contract_views_promoted_views_check check ((promoted_views >= 0)) ); --- Policies +-- Row Level Security alter table user_contract_views enable row level security; +-- Policies drop policy if exists "self and admin read" on user_contract_views; create policy "self and admin read" on user_contract_views for @@ -48,10 +49,6 @@ drop index if exists user_contract_views_contract_id; create index user_contract_views_contract_id on public.user_contract_views using btree (contract_id, user_id); -drop index if exists user_contract_views_user_id; - -create unique index user_contract_views_user_id on public.user_contract_views using btree (user_id, contract_id) nulls not distinct; - drop index if exists user_contract_views_user_contract_ts; create index user_contract_views_user_contract_ts on public.user_contract_views using btree (user_id, contract_id) include ( @@ -59,3 +56,7 @@ create index user_contract_views_user_contract_ts on public.user_contract_views last_promoted_view_ts, last_card_view_ts ); + +drop index if exists user_contract_views_user_id; + +create unique index user_contract_views_user_id on public.user_contract_views using btree (user_id, contract_id) nulls not distinct; diff --git a/backend/supabase/user_disinterests.sql b/backend/supabase/user_disinterests.sql index 2abff82413..6fbabd6824 100644 --- a/backend/supabase/user_disinterests.sql +++ b/backend/supabase/user_disinterests.sql @@ -10,9 +10,10 @@ create table if not exists feed_id bigint ); --- Policies +-- Row Level Security alter table user_disinterests enable row level security; +-- Policies drop policy if exists "public read" on user_disinterests; create policy "public read" on user_disinterests for diff --git a/backend/supabase/user_embeddings.sql b/backend/supabase/user_embeddings.sql index c46ab7b2c5..dde669cd89 100644 --- a/backend/supabase/user_embeddings.sql +++ b/backend/supabase/user_embeddings.sql @@ -8,9 +8,10 @@ create table if not exists disinterest_embedding vector (1536) ); --- Policies +-- Row Level Security alter table user_embeddings enable row level security; +-- Policies drop policy if exists "admin write access" on user_embeddings; create policy "admin write access" on user_embeddings for all to service_role; diff --git a/backend/supabase/user_events.sql b/backend/supabase/user_events.sql index e5ced926b6..ef03c69e80 100644 --- a/backend/supabase/user_events.sql +++ b/backend/supabase/user_events.sql @@ -11,15 +11,10 @@ create table if not exists ad_id text ); --- Policies +-- Row Level Security alter table user_events enable row level security; -drop policy if exists "user can insert" on user_events; - -create policy "user can insert" on user_events for insert -with - check (true); - +-- Policies drop policy if exists "self and admin read" on user_events; create policy "self and admin read" on user_events for @@ -31,6 +26,12 @@ select ) ); +drop policy if exists "user can insert" on user_events; + +create policy "user can insert" on user_events for insert +with + check (true); + -- Indexes drop index if exists user_events_pkey; diff --git a/backend/supabase/user_follows.sql b/backend/supabase/user_follows.sql index ffc76d1d2c..a93f6c20fc 100644 --- a/backend/supabase/user_follows.sql +++ b/backend/supabase/user_follows.sql @@ -6,9 +6,10 @@ create table if not exists created_time timestamp with time zone default now() not null ); --- Policies +-- Row Level Security alter table user_follows enable row level security; +-- Policies drop policy if exists "public read" on user_follows; create policy "public read" on user_follows for diff --git a/backend/supabase/user_monitor_status.sql b/backend/supabase/user_monitor_status.sql new file mode 100644 index 0000000000..3d937e3e35 --- /dev/null +++ b/backend/supabase/user_monitor_status.sql @@ -0,0 +1,15 @@ +create table + user_monitor_status ( + id bigint primary key generated always as identity, + user_id text not null references users (id), + reason_codes text[], + fraud_confidence_score int, + identity_confidence_score int, + data JSONB not null, + created_time TIMESTAMPTZ default now() + ); + +-- Enable row-level security +alter table user_monitor_status enable row level security; + +create index idx_user_monitor_status_user_id on user_monitor_status (user_id, created_time desc); diff --git a/backend/supabase/user_notifications.sql b/backend/supabase/user_notifications.sql index 00ce6676f3..cf17c26a3b 100644 --- a/backend/supabase/user_notifications.sql +++ b/backend/supabase/user_notifications.sql @@ -6,9 +6,10 @@ create table if not exists data jsonb not null ); --- Policies +-- Row Level Security alter table user_notifications enable row level security; +-- Policies drop policy if exists "public read" on user_notifications; create policy "public read" on user_notifications for diff --git a/backend/supabase/user_portfolio_history.sql b/backend/supabase/user_portfolio_history.sql index 80fdf14117..36cfe9ee38 100644 --- a/backend/supabase/user_portfolio_history.sql +++ b/backend/supabase/user_portfolio_history.sql @@ -10,9 +10,9 @@ create table if not exists id bigint not null, spice_balance numeric default 0 not null, profit numeric, - cash_balance numeric default 0 not null, cash_investment_value numeric default 0 not null, - total_cash_deposits numeric default 0 not null + total_cash_deposits numeric default 0 not null, + cash_balance numeric default 0 not null ); -- Triggers @@ -43,9 +43,10 @@ begin end; $function$; --- Policies +-- Row Level Security alter table user_portfolio_history enable row level security; +-- Policies drop policy if exists "public read" on user_portfolio_history; create policy "public read" on user_portfolio_history for diff --git a/backend/supabase/user_portfolio_history_latest.sql b/backend/supabase/user_portfolio_history_latest.sql index 61e02833d4..f366907116 100644 --- a/backend/supabase/user_portfolio_history_latest.sql +++ b/backend/supabase/user_portfolio_history_latest.sql @@ -10,9 +10,9 @@ create table if not exists spice_balance numeric default 0.0 not null, last_calculated timestamp with time zone not null, profit numeric, - cash_balance numeric default 0.0 not null, cash_investment_value numeric default 0.0 not null, - total_cash_deposits numeric default 0.0 not null + total_cash_deposits numeric default 0.0 not null, + cash_balance numeric default 0.0 not null ); -- Indexes diff --git a/backend/supabase/user_quest_metrics.sql b/backend/supabase/user_quest_metrics.sql index 987b999720..6ee8d06c8c 100644 --- a/backend/supabase/user_quest_metrics.sql +++ b/backend/supabase/user_quest_metrics.sql @@ -7,9 +7,10 @@ create table if not exists idempotency_key text ); --- Policies +-- Row Level Security alter table user_quest_metrics enable row level security; +-- Policies drop policy if exists "public read" on user_quest_metrics; create policy "public read" on user_quest_metrics for diff --git a/backend/supabase/user_reactions.sql b/backend/supabase/user_reactions.sql index 09d30a47ed..e27073dcce 100644 --- a/backend/supabase/user_reactions.sql +++ b/backend/supabase/user_reactions.sql @@ -10,9 +10,10 @@ create table if not exists reaction_type character varying(20) ); --- Policies +-- Row Level Security alter table user_reactions enable row level security; +-- Policies drop policy if exists "public read" on user_reactions; create policy "public read" on user_reactions for diff --git a/backend/supabase/user_seen_chats.sql b/backend/supabase/user_seen_chats.sql index a6938a2739..6386395fcb 100644 --- a/backend/supabase/user_seen_chats.sql +++ b/backend/supabase/user_seen_chats.sql @@ -7,9 +7,10 @@ create table if not exists created_time timestamp with time zone default now() not null ); --- Policies +-- Row Level Security alter table user_seen_chats enable row level security; +-- Policies drop policy if exists "public read" on user_seen_chats; create policy "public read" on user_seen_chats for diff --git a/backend/supabase/user_topic_interests.sql b/backend/supabase/user_topic_interests.sql index be670d6eeb..6cbe8afc96 100644 --- a/backend/supabase/user_topic_interests.sql +++ b/backend/supabase/user_topic_interests.sql @@ -7,6 +7,9 @@ create table if not exists group_ids_to_activity jsonb not null ); +-- Row Level Security +alter table user_topic_interests enable row level security; + -- Indexes drop index if exists user_topic_interests_pkey; diff --git a/backend/supabase/user_topics.sql b/backend/supabase/user_topics.sql index eb9185721a..8f6d6aba44 100644 --- a/backend/supabase/user_topics.sql +++ b/backend/supabase/user_topics.sql @@ -7,9 +7,10 @@ create table if not exists topics text[] not null ); --- Policies +-- Row Level Security alter table user_topics enable row level security; +-- Policies drop policy if exists "public read" on user_topics; create policy "public read" on user_topics for diff --git a/backend/supabase/user_view_events.sql b/backend/supabase/user_view_events.sql index c689c279a8..1a3cb4f1c1 100644 --- a/backend/supabase/user_view_events.sql +++ b/backend/supabase/user_view_events.sql @@ -10,15 +10,18 @@ create table if not exists ad_id text ); +-- Row Level Security +alter table user_view_events enable row level security; + -- Indexes drop index if exists user_view_events_pkey; create unique index user_view_events_pkey on public.user_view_events using btree (id); +drop index if exists user_view_events_contract_id_name_created_time; + +create index user_view_events_contract_id_name_created_time on public.user_view_events using btree (contract_id, name, created_time desc); + drop index if exists user_view_events_name_contract_id_user_id; create index user_view_events_name_contract_id_user_id on public.user_view_events using btree (user_id, contract_id, name); - -drop index if exists user_view_events_contract_id_name_created_time; --- useful for conversion scores -create index user_view_events_contract_id_name_created_time on public.user_view_events using btree (contract_id, name, created_time desc); diff --git a/backend/supabase/users.sql b/backend/supabase/users.sql index b14bb9dea5..dbff4c6bc0 100644 --- a/backend/supabase/users.sql +++ b/backend/supabase/users.sql @@ -20,9 +20,10 @@ create table if not exists total_cash_deposits numeric default 0 not null ); --- Policies +-- Row Level Security alter table users enable row level security; +-- Policies drop policy if exists "public read" on users; create policy "public read" on users for diff --git a/backend/supabase/weekly_update.sql b/backend/supabase/weekly_update.sql index 81c216a517..81199d6608 100644 --- a/backend/supabase/weekly_update.sql +++ b/backend/supabase/weekly_update.sql @@ -13,9 +13,10 @@ create table if not exists alter table weekly_update add constraint weekly_update_user_id_fkey foreign key (user_id) references users (id); --- Policies +-- Row Level Security alter table weekly_update enable row level security; +-- Policies drop policy if exists "public read" on weekly_update; create policy "public read" on weekly_update for diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts index 9cbaef1e3c..26fdf2da8f 100644 --- a/common/src/api/schema.ts +++ b/common/src/api/schema.ts @@ -342,6 +342,7 @@ export const API = (_apiTypeCheck = { returns: [] as Bet[], props: z .object({ + id: z.string().optional(), userId: z.string().optional(), username: z.string().optional(), contractId: z.string().or(z.array(z.string())).optional(), @@ -712,6 +713,12 @@ export const API = (_apiTypeCheck = { authed: true, props: z.object({ amount: z.number().positive().finite().safe() }).strict(), }, + 'convert-cash-to-mana': { + method: 'POST', + visibility: 'public', + authed: true, + props: z.object({ amount: z.number().positive().finite().safe() }).strict(), + }, 'request-loan': { method: 'GET', visibility: 'undocumented', diff --git a/common/src/calculate-cpmm-arbitrage.ts b/common/src/calculate-cpmm-arbitrage.ts index c312436b2f..3a560a5b00 100644 --- a/common/src/calculate-cpmm-arbitrage.ts +++ b/common/src/calculate-cpmm-arbitrage.ts @@ -52,7 +52,10 @@ export function calculateCpmmMultiArbitrageBet( : MIN_CPMM_PROB if ( (answerToBuy.prob < MIN_CPMM_PROB && outcome === 'NO') || - (answerToBuy.prob > MAX_CPMM_PROB && outcome === 'YES') + (answerToBuy.prob > MAX_CPMM_PROB && outcome === 'YES') || + // Fixes limit order fills at current price when limitProb is set to a diff price and user has shares to redeem + (answerToBuy.prob > limitProb && outcome === 'YES') || + (answerToBuy.prob < limitProb && outcome === 'NO') ) { return noFillsReturn(outcome, answerToBuy, collectedFees) } diff --git a/common/src/contract.ts b/common/src/contract.ts index 7cf2ce626b..7dd98c8805 100644 --- a/common/src/contract.ts +++ b/common/src/contract.ts @@ -423,6 +423,7 @@ export type ContractParams = { betReplies: Bet[] cash?: { contract: Contract + lastBetTime?: number pointsString: string multiPointsString: { [answerId: string]: string } userPositionsByOutcome: ContractMetricsByOutcome diff --git a/common/src/economy.ts b/common/src/economy.ts index f0405c1f96..e78000e6fa 100644 --- a/common/src/economy.ts +++ b/common/src/economy.ts @@ -63,12 +63,11 @@ export const getTieredCost = ( export const KYC_VERIFICATION_BONUS_CASH = 1 export const BETTING_STREAK_SWEEPS_BONUS_AMOUNT = 0.05 export const BETTING_STREAK_SWEEPS_BONUS_MAX = 0.25 -/* Mana bonuses */ +/* Mana bonuses */ export const STARTING_BALANCE = 100 // for sus users, i.e. multiple sign ups for same person export const SUS_STARTING_BALANCE = 10 - export const PHONE_VERIFICATION_BONUS = 1000 export const REFERRAL_AMOUNT = 1000 @@ -121,54 +120,77 @@ export const PaymentAmounts = [ export const PaymentAmountsGIDX = [ { - mana: 10_000, + mana: 1_000, priceInDollars: 15, bonusInDollars: 10, }, { - mana: 25_000, + mana: 2_500, priceInDollars: 30, bonusInDollars: 25, }, { - mana: 100_000, + mana: 10_000, priceInDollars: 110, bonusInDollars: 100, }, { - mana: 1_000_000, + mana: 100_000, priceInDollars: 1_000, bonusInDollars: 1000, }, ] export type PaymentAmount = (typeof PaymentAmounts)[number] + export const MANA_WEB_PRICES = TWOMBA_ENABLED ? PaymentAmountsGIDX : PaymentAmounts export type WebManaAmounts = (typeof PaymentAmounts)[number]['mana'] // TODO: these prices should be a function of whether the user is sweepstakes verified or not -export const IOS_PRICES = [ - { - mana: 10_000, - priceInDollars: 14.99, - bonusInDollars: TWOMBA_ENABLED ? 10 : 0, - sku: 'mana_1000', - }, - { - mana: 25_000, - priceInDollars: 35.99, - bonusInDollars: TWOMBA_ENABLED ? 25 : 0, - sku: 'mana_2500', - }, - { - mana: 100_000, - priceInDollars: 142.99, - bonusInDollars: TWOMBA_ENABLED ? 100 : 0, - sku: 'mana_10000', - }, - // No 1M option on ios: the fees are too high -] +export const IOS_PRICES = !TWOMBA_ENABLED + ? [ + { + mana: 10_000, + priceInDollars: 14.99, + bonusInDollars: 0, + sku: 'mana_1000', + }, + { + mana: 25_000, + priceInDollars: 35.99, + bonusInDollars: 0, + sku: 'mana_2500', + }, + { + mana: 100_000, + priceInDollars: 142.99, + bonusInDollars: 0, + sku: 'mana_10000', + }, + // No 1M option on ios: the fees are too high + ] + : [ + { + mana: 1_000, + priceInDollars: 14.99, + bonusInDollars: 10, + sku: 'mana_1000', + }, + { + mana: 2_500, + priceInDollars: 35.99, + bonusInDollars: 25, + sku: 'mana_2500', + }, + { + mana: 10_000, + priceInDollars: 142.99, + bonusInDollars: 100, + sku: 'mana_10000', + }, + // No 1M option on ios: the fees are too high + ] export const SWEEPIES_CASHOUT_FEE = 0.05 export const MIN_CASHOUT_AMOUNT = 25 diff --git a/common/src/envs/constants.ts b/common/src/envs/constants.ts index aee917ac96..ac64dbfe9a 100644 --- a/common/src/envs/constants.ts +++ b/common/src/envs/constants.ts @@ -15,9 +15,13 @@ export const TWOMBA_ENABLED = true export const PRODUCT_MARKET_FIT_ENABLED = false export const SPICE_PRODUCTION_ENABLED = true export const SPICE_TO_MANA_CONVERSION_RATE = 1 +export const CASH_TO_MANA_CONVERSION_RATE = 100 +export const MIN_CASH_DONATION = 25 export const MIN_SPICE_DONATION = 25000 export const CHARITY_FEE = 0.05 +export const CASH_TO_CHARITY_DOLLARS = 1 - CHARITY_FEE export const SPICE_TO_CHARITY_DOLLARS = (1 / 1000) * (1 - CHARITY_FEE) // prize points -> dollars +export const NY_FL_CASHOUT_LIMIT = 5000 export const SPICE_NAME = 'Prize Point' export const SWEEPIES_NAME = 'Sweepcash' diff --git a/common/src/fees.ts b/common/src/fees.ts index f7002b2ca3..ad59b0dc1f 100644 --- a/common/src/fees.ts +++ b/common/src/fees.ts @@ -1,4 +1,5 @@ import { addObjects } from 'common/util/object' +import { TWOMBA_ENABLED } from './envs/constants' export const FEE_START_TIME = 1713292320000 @@ -12,6 +13,14 @@ export const getFeesSplit = ( totalFees: number, previouslyCollectedFees: Fees ) => { + if (TWOMBA_ENABLED) { + return { + creatorFee: 0, + platformFee: totalFees, + liquidityFee: 0, + } + } + const before1k = Math.max( 0, CREATORS_EARN_WHOLE_FEE_UP_TO - previouslyCollectedFees.creatorFee diff --git a/common/src/gidx/gidx.ts b/common/src/gidx/gidx.ts index dd823c33d8..d60fb90ec8 100644 --- a/common/src/gidx/gidx.ts +++ b/common/src/gidx/gidx.ts @@ -194,6 +194,7 @@ type LocationDetailType = { } export type GIDXMonitorResponse = { + ApiKey: string MerchantCustomerID: string ReasonCodes: string[] WatchChecks: WatchCheckType[] diff --git a/common/src/notification.ts b/common/src/notification.ts index a2153b0815..2aefc6e5bd 100644 --- a/common/src/notification.ts +++ b/common/src/notification.ts @@ -78,6 +78,7 @@ export type notification_source_types = | 'airdrop' | 'manifest_airdrop' | 'extra_purchased_mana' + | 'payment_status' export type love_notification_source_types = | 'love_contract' @@ -328,6 +329,10 @@ export const NOTIFICATION_DESCRIPTIONS: notification_descriptions = { simple: 'You just received 9x your purchased mana in 2024', detailed: 'Manifold has sent you a gift of 9x your purchased mana in 2024.', }, + payment_status: { + simple: 'Payment updates', + detailed: 'Updates on your payment statuses', + }, } export type BettingStreakData = { @@ -351,6 +356,7 @@ export type BetFillData = { limitOrderRemaining?: number limitAt?: string outcomeType?: OutcomeType + token?: ContractToken } export type ContractResolutionData = { @@ -374,6 +380,7 @@ export type UniqueBettorData = { isPartner?: boolean totalUniqueBettors?: number totalAmountBet?: number + token?: ContractToken } export type ReviewNotificationData = { @@ -399,6 +406,14 @@ export type ExtraPurchasedManaData = { amount: number } +export type PaymentCompletedData = { + userId: string + amount: number + currency: string + paymentMethodType: string + paymentAmountType: string +} + export function getSourceIdForLinkComponent( sourceId: string, sourceType?: notification_source_types diff --git a/common/src/portfolio.ts b/common/src/portfolio.ts deleted file mode 100644 index 641bc5807e..0000000000 --- a/common/src/portfolio.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { convertSQLtoTS, tsToMillis } from './supabase/utils' - -export type Portfolio = { - id: string - creatorId: string - slug: string - name: string - items: PortfolioItem[] - createdTime: number -} - -export type PortfolioItem = { - contractId: string - answerId?: string - position: 'YES' | 'NO' -} - -export const MAX_PORTFOLIO_NAME_LENGTH = 140 - -export const convertPortfolio = (portfolioRow: any) => { - return convertSQLtoTS(portfolioRow, { - created_time: tsToMillis, - }) as Portfolio -} - -export function portfolioPath(portfolioSlug: string) { - return `/portfolios/${portfolioSlug}` -} diff --git a/common/src/txn.ts b/common/src/txn.ts index 9b8b90097a..decdb6494c 100644 --- a/common/src/txn.ts +++ b/common/src/txn.ts @@ -21,6 +21,8 @@ type AnyTxnType = | ContractUndoProduceSpice | ConsumeSpice | ConsumeSpiceDone + | ConvertCash + | ConvertCashDone | QfPayment | QfAddPool | QfDividend @@ -92,7 +94,7 @@ type Donation = { fromType: 'USER' toType: 'CHARITY' category: 'CHARITY' - token: 'SPICE' | 'M$' + token: 'SPICE' | 'M$' | 'CASH' } type Tip = { @@ -154,7 +156,7 @@ type CharityFee = { fromType: 'USER' toType: 'BANK' category: 'CHARITY_FEE' - token: 'SPICE' + token: 'SPICE' | 'CASH' data: { charityId: string } @@ -266,7 +268,7 @@ type ConsumeSpice = { category: 'CONSUME_SPICE' token: 'SPICE' data: { - siblingId: string + insertTime: number } } type ConsumeSpiceDone = { @@ -275,7 +277,28 @@ type ConsumeSpiceDone = { category: 'CONSUME_SPICE_DONE' token: 'M$' data: { - siblingId: string + insertTime: number + } +} + +// these come in pairs to convert cash to mana +type ConvertCash = { + fromType: 'USER' + toType: 'BANK' + category: 'CONVERT_CASH' + token: 'CASH' + data: { + insertTime: number + } +} + +type ConvertCashDone = { + fromType: 'BANK' + toType: 'USER' + category: 'CONVERT_CASH_DONE' + token: 'M$' + data: { + insertTime: number } } @@ -534,7 +557,8 @@ export type ContractProduceSpiceTxn = Txn & ContractProduceSpice export type ContractUndoProduceSpiceTxn = Txn & ContractUndoProduceSpice export type ConsumeSpiceTxn = Txn & ConsumeSpice export type ConsumeSpiceDoneTxn = Txn & ConsumeSpiceDone - +export type ConvertCashTxn = Txn & ConvertCash +export type ConvertCashDoneTxn = Txn & ConvertCashDone export type QfTxn = Txn & QfId export type QfPaymentTxn = QfTxn & QfPayment export type QfAddPoolTxn = QfTxn & QfAddPool diff --git a/common/src/user-notification-preferences.ts b/common/src/user-notification-preferences.ts index 53a8f39efe..9a2fadfe9d 100644 --- a/common/src/user-notification-preferences.ts +++ b/common/src/user-notification-preferences.ts @@ -45,6 +45,7 @@ export type notification_preferences = { airdrop: notification_destination_types[] manifest_airdrop: notification_destination_types[] extra_purchased_mana: notification_destination_types[] + payment_status: notification_destination_types[] // Leagues league_changed: notification_destination_types[] @@ -140,6 +141,7 @@ export const getDefaultNotificationPreferences = (isDev?: boolean) => { airdrop: constructPref(true, false, false), manifest_airdrop: constructPref(true, false, false), extra_purchased_mana: constructPref(true, false, false), + payment_status: constructPref(true, false, false), // Leagues league_changed: constructPref(true, false, false), @@ -213,7 +215,11 @@ export const getNotificationDestinationsForUser = ( const unsubscribeEndpoint = getApiUrl('unsubscribe') try { const notificationPreference = getNotificationPreference(reason) - const destinations = notificationSettings[notificationPreference] ?? [] + // Get default destinations if user has no settings for this preference + // TODO: write the default notif preferences to the user's settings when missing + const destinations = + notificationSettings[notificationPreference] ?? + getDefaultNotificationPreferences()[notificationPreference] const optOutOfAllSettings = notificationSettings.opt_out_all // Your market closure notifications are high priority, opt-out doesn't affect their delivery const optedOutOfEmail = diff --git a/common/src/user.ts b/common/src/user.ts index 5d6b0c0742..6f1ba5c6c3 100644 --- a/common/src/user.ts +++ b/common/src/user.ts @@ -75,7 +75,7 @@ export type User = { verifiedPhone?: boolean // KYC related fields: - kycLastAttempt?: number + kycLastAttemptTime?: number kycDocumentStatus?: 'fail' | 'pending' | 'await-documents' | 'verified' sweepstakesVerified?: boolean idVerified?: boolean @@ -195,6 +195,12 @@ export const USER_BLOCKED_MESSAGE = 'User is blocked' export const USER_NOT_REGISTERED_MESSAGE = 'User must register' export const USER_VERIFIED_MESSSAGE = 'User is verified' +export const PROMPT_VERIFICATION_MESSAGES = [ + USER_NOT_REGISTERED_MESSAGE, + PHONE_NOT_VERIFIED_MESSAGE, + IDENTIFICATION_FAILED_MESSAGE, +] + export const getVerificationStatus = ( user: User ): { diff --git a/common/src/util/format.ts b/common/src/util/format.ts index 1849887592..03aa147d1c 100644 --- a/common/src/util/format.ts +++ b/common/src/util/format.ts @@ -35,7 +35,7 @@ export function formatWithToken(variables: { }) { const { amount, token, toDecimal, short } = variables if (token === 'CASH') { - return formatSweepies(amount, toDecimal) + return formatSweepies(amount, { toDecimal, short }) } if (toDecimal) { return formatMoneyWithDecimals(amount) @@ -54,12 +54,33 @@ export function formatMoney(amount: number, token?: ContractToken) { return formatter.format(newAmount).replace('$', ENV_CONFIG.moneyMoniker) } -export function formatSweepies(amount: number, toDecimal?: number) { - return SWEEPIES_MONIKER + formatSweepiesNumber(amount, toDecimal) +export function formatSweepies( + amount: number, + parameters?: { + toDecimal?: number + short?: boolean + } +) { + return SWEEPIES_MONIKER + formatSweepiesNumber(amount, parameters) } -export function formatSweepiesNumber(amount: number, toDecimal?: number) { - return amount.toFixed(toDecimal ?? 2) +export function formatSweepiesNumber( + amount: number, + parameters?: { + toDecimal?: number + short?: boolean + } +) { + const { toDecimal, short } = parameters ?? {} + const toDecimalPlace = toDecimal ?? 2 + if (short && amount > 1000) { + return formatLargeNumber(amount) + } + // return amount.toFixed(toDecimal ?? 2) + return amount.toLocaleString('en-US', { + minimumFractionDigits: toDecimalPlace, + maximumFractionDigits: toDecimalPlace, + }) } export function formatSpice(amount: number) { diff --git a/common/src/util/random.ts b/common/src/util/random.ts index fb02fa197e..11ff120ae2 100644 --- a/common/src/util/random.ts +++ b/common/src/util/random.ts @@ -1,5 +1,7 @@ // max 10 length string. For longer, concat multiple // Often used as a unique identifier. +import { randomBytes } from 'crypto' + export const randomString = (length = 10) => Math.random() .toString(36) @@ -57,3 +59,7 @@ export const shuffle = (array: unknown[], rand: () => number) => { ;[array[i], array[swapIndex]] = [array[swapIndex], array[i]] } } + +export const secureRandomString = (length: number): string => { + return randomBytes(length).toString('hex').slice(0, length) +} diff --git a/web/components/add-funds-modal.tsx b/web/components/add-funds-modal.tsx index 86a06cd9ec..e5fe5f439b 100644 --- a/web/components/add-funds-modal.tsx +++ b/web/components/add-funds-modal.tsx @@ -1,10 +1,7 @@ 'use client' import clsx from 'clsx' -import { AD_REDEEM_REWARD } from 'common/boost' import { - BETTING_STREAK_BONUS_MAX, IOS_PRICES, - REFERRAL_AMOUNT, MANA_WEB_PRICES, WebManaAmounts, PaymentAmount, @@ -17,7 +14,6 @@ import { Txn } from 'common/txn' import { DAY_MS } from 'common/util/time' import { sum } from 'lodash' import Image from 'next/image' -import Link from 'next/link' import { useEffect, useState } from 'react' import { Row } from 'web/components/layout/row' import { useUser } from 'web/hooks/use-user' @@ -36,6 +32,13 @@ import { FaStore } from 'react-icons/fa6' import router from 'next/router' import { useIosPurchases } from 'web/hooks/use-ios-purchases' +const BUY_MANA_GRAPHICS = [ + '/buy-mana-graphics/10k.png', + '/buy-mana-graphics/25k.png', + '/buy-mana-graphics/100k.png', + '/buy-mana-graphics/1M.png', +] + export function AddFundsModal(props: { open: boolean setOpen(open: boolean): void @@ -122,10 +125,11 @@ export function BuyManaTab(props: { onClose: () => void }) { )}
- {prices.map((amounts) => { + {prices.map((amounts, index) => { return isNative && platform === 'ios' ? ( void }) { > { @@ -172,12 +177,13 @@ export function BuyManaTab(props: { onClose: () => void }) { export function PriceTile(props: { amounts: PaymentAmount + index: number loading: WebManaAmounts | null disabled: boolean onClick?: () => void isSubmitButton?: boolean }) { - const { loading, onClick, isSubmitButton, amounts } = props + const { loading, onClick, isSubmitButton, amounts, index } = props const { mana, priceInDollars, bonusInDollars } = amounts const isCurrentlyLoading = loading === mana @@ -219,28 +225,8 @@ export function PriceTile(props: { )} > { { - return ( -
    - - 🚀 Browse feed for {' '} - from each boosted question - - - 🔥 Streak bonus (up to{' '} - per day) - - - 👋 Refer a friend for{' '} - {' '} - after their first trade - -
- ) -} - export const SpiceToManaForm = (props: { onBack: () => void onClose: () => void @@ -325,21 +291,6 @@ export const SpiceToManaForm = (props: { ) } -const Item = (props: { children: React.ReactNode; url?: string }) => { - const { children, url } = props - return ( -
  • - {url ? ( - -
    {children}
    - - ) : ( -
    {children}
    - )} -
  • - ) -} - export const use24hrUsdPurchases = (userId: string) => { const [purchases, setPurchases] = useState([]) diff --git a/web/components/answers/answer-components.tsx b/web/components/answers/answer-components.tsx index dd99f9b7a9..adf68415fc 100644 --- a/web/components/answers/answer-components.tsx +++ b/web/components/answers/answer-components.tsx @@ -545,6 +545,10 @@ export function AnswerPosition(props: { const noWinnings = totalShares.NO ?? 0 const position = yesWinnings - noWinnings const isCashContract = contract.token === 'CASH' + const canSell = tradingAllowed(contract, answer) + const won = + (position > 1e-7 && answer.resolution === 'YES') || + (position < -1e-7 && answer.resolution === 'NO') return ( - Payout + {canSell ? 'Payout' : won ? 'Paid out' : 'Held out for'} {position > 1e-7 ? ( <> @@ -586,18 +590,17 @@ export function AnswerPosition(props: {
    - {(!contract.closeTime || contract.closeTime > Date.now()) && - !answer.resolutionTime && ( - <> - · - - - )} + {canSell && ( + <> + · + + + )} ) } diff --git a/web/components/answers/answers-panel.tsx b/web/components/answers/answers-panel.tsx index 2df146b901..08427599fc 100644 --- a/web/components/answers/answers-panel.tsx +++ b/web/components/answers/answers-panel.tsx @@ -76,6 +76,7 @@ import { import { SearchCreateAnswerPanel } from './create-answer-panel' import { debounce } from 'lodash' import { RelativeTimestamp } from '../relative-timestamp' +import { buildArray } from 'common/util/array' export const SHOW_LIMIT_ORDER_CHARTS_KEY = 'SHOW_LIMIT_ORDER_CHARTS_KEY' const MAX_DEFAULT_ANSWERS = 20 @@ -192,7 +193,7 @@ export function AnswersPanel(props: { enabled: isAdvancedTrader && shouldShowLimitOrderChart, }) - const [shouldShowPositions, setShouldShowPositions] = useState(true) + const [shouldShowPositions, setShouldShowPositions] = useState(!allResolved) const moreCount = answers.length - answersToShow.length // Note: Hide answers if there is just one "Other" answer. @@ -623,12 +624,7 @@ export function Answer(props: { resolvedProb === 0 ? 'text-ink-700' : 'text-ink-900' ) - const showSellButton = - !resolution && - hasBets && - user && - (!contract.closeTime || contract.closeTime > Date.now()) && - !answer.resolutionTime + const showPosition = hasBets && user const userHasLimitOrders = shouldShowLimitOrderChart && (yourUnfilledBets ?? []).length > 0 @@ -645,8 +641,7 @@ export function Answer(props: { const answerCreator = useDisplayUserByIdOrAnswer(answer) const answerCreatorIsNotContractCreator = !!answerCreator && answerCreator.username !== contract.creatorUsername - - const dropdownItems = [ + const dropdownItems = buildArray( { name: 'author info', nonButtonContent: ( @@ -672,24 +667,19 @@ export function Answer(props: { ), }, - ...(canEdit && answer.poolYes != undefined && !answer.isOther - ? [ - { - icon: , - name: 'Edit', - onClick: () => setEditingAnswer(answer), - }, - ] - : []), - ...(onCommentClick - ? [ - { - icon: , - name: 'Comment', - onClick: onCommentClick, - }, - ] - : []), + canEdit && + answer.poolYes != undefined && + !answer.isOther && { + icon: , + name: 'Edit', + onClick: () => setEditingAnswer(answer), + }, + + onCommentClick && { + icon: , + name: 'Comment', + onClick: onCommentClick, + }, { icon: , name: 'Trades', @@ -701,16 +691,12 @@ export function Answer(props: { ), onClick: () => setTradesModalOpen(true), }, - ...(hasLimitOrders - ? [ - { - icon: , - name: getOrderBookButtonLabel(unfilledBets), - onClick: () => setLimitBetModalOpen(true), - }, - ] - : []), - ] + hasLimitOrders && { + icon: , + name: getOrderBookButtonLabel(unfilledBets), + onClick: () => setLimitBetModalOpen(true), + } + ) return ( @@ -765,8 +751,6 @@ export function Answer(props: { icon={} items={dropdownItems} withinOverflowContainer - menuItemsClass="!z-50" - className="!z-50" /> } @@ -789,7 +773,7 @@ export function Answer(props: { } > - {showSellButton && ( + {showPosition && ( )} - {userHasLimitOrders && showSellButton && <>·} + {userHasLimitOrders && showPosition && <>·} {userHasLimitOrders && ( )} - + {tradesModalOpen && ( + + )} {!!hasLimitOrders && ( diff --git a/web/components/bet/bet-panel.tsx b/web/components/bet/bet-panel.tsx index 13b07c30ae..ceabf0ba5c 100644 --- a/web/components/bet/bet-panel.tsx +++ b/web/components/bet/bet-panel.tsx @@ -39,7 +39,6 @@ import { TRADE_TERM, TWOMBA_ENABLED, } from 'common/envs/constants' -import { KYC_VERIFICATION_BONUS_CASH } from 'common/economy' import { getFeeTotal } from 'common/fees' import { getFormattedMappedValue } from 'common/pseudo-numeric' import { getStonkDisplayShares, STONK_NO, STONK_YES } from 'common/stonk' @@ -54,7 +53,7 @@ import { useIsAdvancedTrader } from 'web/hooks/use-is-advanced-trader' import { useUser } from 'web/hooks/use-user' import { track, withTracking } from 'web/lib/service/analytics' import { isAndroid, isIOS } from 'web/lib/util/device' -import { Button, buttonClass } from '../buttons/button' +import { Button } from '../buttons/button' import { WarningConfirmationButton } from '../buttons/warning-confirmation-button' import { getAnswerColor } from '../charts/contract/choice' import { ChoicesToggleGroup } from '../widgets/choices-toggle-group' @@ -63,9 +62,9 @@ import LimitOrderPanel from './limit-order-panel' import { MoneyDisplay } from './money-display' import { OrderBookPanel, YourOrders } from './order-book' import { YesNoSelector } from './yes-no-selector' -import Link from 'next/link' import { blockFromSweepstakes, identityPending } from 'common/user' -import { CoinNumber } from '../widgets/coin-number' +import { CashoutLimitWarning } from './cashout-limit-warning' +import { VerifyButton } from '../twomba/toggle-verify-callout' export type BinaryOutcomes = 'YES' | 'NO' | undefined @@ -136,14 +135,15 @@ export function BuyPanel(props: { ) } else if (contract.token === 'CASH' && user && !user.idVerified) { return ( - - Verify your info to start trading on sweepstakes markets and earn a - bonus of{' '} - !{' '} - - Verify - - + +
    + Must be verified to {TRADE_TERM} +
    +

    + Verify your info to start trading on sweepstakes markets! +

    + + ) } else if (contract.token === 'CASH' && blockFromSweepstakes(user)) { return ( @@ -727,6 +727,7 @@ export const BuyPanelBody = (props: { )} )} + {isCashContract && } {user && ( diff --git a/web/components/bet/cashout-limit-warning.tsx b/web/components/bet/cashout-limit-warning.tsx new file mode 100644 index 0000000000..14be10d826 --- /dev/null +++ b/web/components/bet/cashout-limit-warning.tsx @@ -0,0 +1,109 @@ +import clsx from 'clsx' +import { + CHARITY_FEE, + NY_FL_CASHOUT_LIMIT, + SWEEPIES_NAME, +} from 'common/envs/constants' +import { User } from 'common/user' +import { formatMoneyUSD, formatPercent } from 'common/util/format' +import { useState } from 'react' +import { IoIosWarning } from 'react-icons/io' +import { Col } from '../layout/col' +import { Modal, MODAL_CLASS } from '../layout/modal' +import { CoinNumber } from '../widgets/coin-number' + +export function CashoutLimitWarning(props: { + user: User | null | undefined + className?: string +}) { + const { user, className } = props + const [open, setOpen] = useState(false) + + if (!user || !user.sweepstakes5kLimit) { + return <> + } + + return ( + <> +
    + + New York and Florida have a{' '} + {formatMoneyUSD(NY_FL_CASHOUT_LIMIT)} cashout limit per market.{' '} + +
    + + +
    + Cashout Limit +
    + + Residents of New York and Florida have a{' '} + {formatMoneyUSD(NY_FL_CASHOUT_LIMIT)} cashout limit per + market. + + + +
    + {SWEEPIES_NAME} to Cash Conversion +
    + + {' '} + = $1, with a {formatPercent(CHARITY_FEE)} fee. To + receive the full {formatMoneyUSD(NY_FL_CASHOUT_LIMIT)} in cash + after the fee, you would need to redeem approximately{' '} + {' '} + worth of {SWEEPIES_NAME}. + + + +
    Cashout Limit
    + + Any Sweepies exceeding this{' '} + {' '} + limit remain in your account for participating in other + sweepstakes markets, but they cannot be redeemed for cash. + + + + +
    + Multi-Choice Markets +
    + + In multi-choice markets, the cashout limit applies separately to + each answer. This means you can redeem up to{' '} + {' '} + (to recieve {formatMoneyUSD(NY_FL_CASHOUT_LIMIT)}) for each + answer. + + + +
    + + ) +} diff --git a/web/components/bet/fees.tsx b/web/components/bet/fees.tsx index 4ccc1b838f..a596938a93 100644 --- a/web/components/bet/fees.tsx +++ b/web/components/bet/fees.tsx @@ -1,4 +1,4 @@ -import { TRADE_TERM } from 'common/envs/constants' +import { TRADE_TERM, TWOMBA_ENABLED } from 'common/envs/constants' import { InfoTooltip } from '../widgets/info-tooltip' import { MoneyDisplay } from './money-display' @@ -20,7 +20,11 @@ export const FeeDisplay = (props: { diff --git a/web/components/bet/order-book.tsx b/web/components/bet/order-book.tsx index d44b2ea66c..482d1861ef 100644 --- a/web/components/bet/order-book.tsx +++ b/web/components/bet/order-book.tsx @@ -14,11 +14,11 @@ import { } from 'common/contract' import { getFormattedMappedValue } from 'common/pseudo-numeric' import { formatPercent } from 'common/util/format' -import { sortBy } from 'lodash' +import { groupBy, keyBy, sortBy, sumBy } from 'lodash' import { useState } from 'react' import { usePersistentInMemoryState } from 'web/hooks/use-persistent-in-memory-state' import { useUser } from 'web/hooks/use-user' -import { useDisplayUserById } from 'web/hooks/use-user-supabase' +import { useDisplayUserById, useUsers } from 'web/hooks/use-user-supabase' import { api } from 'web/lib/api/api' import { getCountdownString } from 'web/lib/util/time' import { Button } from '../buttons/button' @@ -41,6 +41,9 @@ import { Subtitle } from '../widgets/subtitle' import { Table } from '../widgets/table' import { Tooltip } from '../widgets/tooltip' import { MoneyDisplay } from './money-display' +import { MultipleOrSingleAvatars } from '../multiple-or-single-avatars' +import { DisplayUser } from 'common/api/user-types' +import { UserLink } from '../widgets/user-link' export function YourOrders(props: { contract: @@ -304,6 +307,142 @@ function OrderRow(props: { ) } +export function CollatedOrderTable(props: { + limitBets: LimitBet[] + contract: + | BinaryContract + | PseudoNumericContract + | StonkContract + | CPMMMultiContract + | MultiContract + side: 'YES' | 'NO' +}) { + const { limitBets, contract, side } = props + const isBinaryMC = isBinaryMulti(contract) + const groupedBets = groupBy(limitBets, (b) => b.limitProb) + + return ( +
    + + Buy + {isBinaryMC ? ( + + ) : side === 'YES' ? ( + + ) : ( + + )} + + +
    + {Object.entries(groupedBets).map(([prob, bets]) => ( + + ))} +
    +
    + ) +} + +function CollapsedOrderRow(props: { + contract: + | BinaryContract + | PseudoNumericContract + | StonkContract + | CPMMMultiContract + | MultiContract + limitProb: number + bets: LimitBet[] +}) { + const { contract, limitProb, bets } = props + const { outcome } = bets[0] + const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC' + const isBinaryMC = isBinaryMulti(contract) + + const total = sumBy(bets, (b) => b.orderAmount - b.amount) + + const allUsers = useUsers(bets.map((b) => b.userId))?.filter( + (a) => a != null + ) as DisplayUser[] | undefined + const usersById = keyBy(allUsers, (u) => u.id) + + // find 3 largest users + const userBets = groupBy(bets, (b) => b.userId) + const userSums = Object.entries(userBets).map( + ([userId, bets]) => + [userId, sumBy(bets, (b) => b.orderAmount - b.amount)] as const + ) + const largest = sortBy(userSums, ([, sum]) => -sum) + .slice(0, 3) + .map(([userId]) => usersById[userId]) + .filter((u) => u != null) + .reverse() + + const [collapsed, setCollapsed] = useState(true) + + return ( + <> +
    + {isPseudoNumeric + ? getFormattedMappedValue(contract, limitProb) + : isBinaryMC + ? formatPercent(getBinaryMCProb(limitProb, outcome)) + : formatPercent(limitProb)} +
    + +
    + setCollapsed((c) => !c)} + /> +
    + +
    + +
    + + {!collapsed && + bets.map((b) => { + const u = usersById[b.userId] + return ( +
    +
    + + +
    +
    + +
    +
    + ) + })} + + ) +} + export function OrderBookButton(props: { limitBets: LimitBet[] contract: @@ -331,7 +470,6 @@ export function OrderBookButton(props: { contract={contract} answer={answer} showTitle - expanded /> @@ -354,10 +492,9 @@ export function OrderBookPanel(props: { | CPMMMultiContract | MultiContract answer?: Answer - expanded?: boolean showTitle?: boolean }) { - const { limitBets, contract, expanded, answer, showTitle } = props + const { limitBets, contract, answer, showTitle } = props const yesBets = sortBy( limitBets.filter((bet) => bet.outcome === 'YES'), @@ -373,19 +510,10 @@ export function OrderBookPanel(props: { const isCPMMMulti = contract.mechanism === 'cpmm-multi-1' const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC' - const maxShownNotExpanded = 3 - const moreOrdersCountYes = Math.max(0, yesBets.length - maxShownNotExpanded) - const moreOrdersCountNo = Math.max(0, noBets.length - maxShownNotExpanded) - const moreOrdersCount = moreOrdersCountYes + moreOrdersCountNo - const [isExpanded, setIsExpanded] = usePersistentInMemoryState( - expanded ?? false, - `${contract.id}-orderbook-expanded` - ) - if (limitBets.length === 0) return <> return ( - + Order book{' '} {answer.text}} - - + - + - {moreOrdersCount > 0 && ( - - )} - {!isPseudoNumeric && yesBets.length >= 2 && noBets.length >= 2 && ( <>

    diff --git a/web/components/buttons/button.tsx b/web/components/buttons/button.tsx index 1b08eb2cad..03c6eef28f 100644 --- a/web/components/buttons/button.tsx +++ b/web/components/buttons/button.tsx @@ -4,6 +4,7 @@ import { LoadingIndicator } from 'web/components/widgets/loading-indicator' export type SizeType = '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' export type ColorType = + | 'amber' | 'amber-outline' | 'green' | 'green-outline' @@ -49,6 +50,7 @@ export function buttonClass(size: SizeType, color: ColorType) { baseButtonClasses, sizeClasses[size], color === 'amber-outline' && [outline, 'text-amber-500 hover:bg-amber-500'], + color === 'amber' && [solid, 'bg-amber-600 hover:bg-amber-700'], color === 'green' && [solid, 'bg-teal-500 hover:bg-teal-600'], color === 'green-outline' && [outline, 'text-teal-500 hover:bg-teal-500'], color === 'red' && [solid, 'bg-scarlet-500 hover:bg-scarlet-600'], diff --git a/web/components/buttons/referrals-button.tsx b/web/components/buttons/referrals-button.tsx index ce44272fb4..19611b89d4 100644 --- a/web/components/buttons/referrals-button.tsx +++ b/web/components/buttons/referrals-button.tsx @@ -16,7 +16,7 @@ import { LoadingIndicator } from 'web/components/widgets/loading-indicator' import { ExclamationCircleIcon } from '@heroicons/react/outline' import { referUser } from 'web/lib/api/api' import { CopyLinkRow } from 'web/components/buttons/copy-link-button' -import { ENV_CONFIG } from 'common/envs/constants' +import { ENV_CONFIG, TWOMBA_ENABLED } from 'common/envs/constants' import { canSetReferrer } from 'web/lib/firebase/users' import { REFERRAL_AMOUNT } from 'common/economy' import { Subtitle } from '../widgets/subtitle' @@ -60,7 +60,7 @@ export function Referrals(props: { user: User }) { Refer a friend for{' '} void + redeemableCash: number +}) => { + const { redeemableCash, onBack } = props + + const [sweepiesAmount, setSweepiesAmount] = useState( + redeemableCash + ) + const [manaAmount, setManaAmount] = useState( + redeemableCash * CASH_TO_MANA_CONVERSION_RATE + ) + const [activeInput, setActiveInput] = useState<'sweepies' | 'mana'>( + 'sweepies' + ) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + const updateAmounts = ( + newAmount: number | undefined, + type: 'sweepies' | 'mana' + ) => { + if (type === 'sweepies') { + setSweepiesAmount(newAmount) + setManaAmount( + newAmount ? newAmount * CASH_TO_MANA_CONVERSION_RATE : undefined + ) + } else { + setManaAmount(newAmount) + setSweepiesAmount( + newAmount ? newAmount / CASH_TO_MANA_CONVERSION_RATE : undefined + ) + } + } + const notEnoughCashError = !!sweepiesAmount && sweepiesAmount > redeemableCash + + const onSubmit = async () => { + if (!sweepiesAmount) return + setLoading(true) + try { + await api('convert-cash-to-mana', { + amount: sweepiesAmount, + }) + setLoading(false) + updateAmounts(sweepiesAmount, 'sweepies') + setError(null) + } catch (e) { + console.error(e) + setError(e instanceof APIError ? e.message : 'Error converting') + setLoading(false) + } + } + + return ( + + Convert at a rate of {CASH_TO_MANA_CONVERSION_RATE} {SWEEPIES_NAME} to 1 + mana. + +
    Trade
    + { + updateAmounts(newAmount, 'sweepies') + setActiveInput('sweepies') + }} + isSweepies + label={} + className={activeInput === 'mana' ? 'opacity-50' : ''} + inputClassName={clsx( + activeInput === 'mana' ? 'cursor-pointer' : '', + 'w-full' + )} + onClick={() => setActiveInput('sweepies')} + /> +
    + {notEnoughCashError && ( +
    + You don't have enough redeemable {SWEEPIES_NAME} +
    + )} +
    + + +
    For
    + { + updateAmounts(newAmount, 'mana') + setActiveInput('mana') + }} + label={} + className={activeInput === 'sweepies' ? 'opacity-50' : ''} + inputClassName={clsx( + activeInput === 'sweepies' ? 'cursor-pointer' : '', + 'w-full' + )} + onClick={() => setActiveInput('mana')} + /> + + + + + + {error} + + ) +} diff --git a/web/components/cashout/select-cashout-options.tsx b/web/components/cashout/select-cashout-options.tsx new file mode 100644 index 0000000000..e43361edac --- /dev/null +++ b/web/components/cashout/select-cashout-options.tsx @@ -0,0 +1,197 @@ +import clsx from 'clsx' +import { MIN_CASHOUT_AMOUNT } from 'common/economy' +import { + CASH_TO_MANA_CONVERSION_RATE, + CHARITY_FEE, + SWEEPIES_NAME, +} from 'common/envs/constants' +import { User } from 'common/user' +import Link from 'next/link' +import { + baseButtonClasses, + Button, + buttonClass, +} from 'web/components/buttons/button' +import { Col } from 'web/components/layout/col' +import { Row } from 'web/components/layout/row' +import { getNativePlatform } from 'web/lib/native/is-native' +import { CashoutPagesType } from 'web/pages/cashout' +import { ManaCoin } from 'web/public/custom-components/manaCoin' +import { CoinNumber } from '../widgets/coin-number' + +export function SelectCashoutOptions(props: { + user: User + redeemableCash: number + setPage: (page: CashoutPagesType) => void + allDisabled?: boolean +}) { + const { user, setPage, allDisabled, redeemableCash } = props + const { isNative, platform } = getNativePlatform() + const isNativeIOS = isNative && platform === 'ios' + + const noHasMinRedeemableCash = redeemableCash < MIN_CASHOUT_AMOUNT + const hasNoRedeemableCash = redeemableCash === 0 + + return ( + + + + + +
    Get Mana
    +
    + Trade your {SWEEPIES_NAME} for mana. You'll get{' '} + {CASH_TO_MANA_CONVERSION_RATE} mana for every 1 {SWEEPIES_NAME}. +
    + +
    + + + + + mana value + + + + {!isNativeIOS && ( + + + donate + +
    Donate to Charity
    +
    + Donate your {SWEEPIES_NAME} as USD to a charitable cause. +
    + +
    + + + Visit charity page + + + + {noHasMinRedeemableCash && !allDisabled ? ( + + You need at least{' '} + {' '} + to donate + + ) : null} + + + + ${redeemableCash.toFixed(2)} + {' '} + value + + + + + )} + + + + donate + +
    Redeem for USD
    +
    + Redeem your {SWEEPIES_NAME} for USD. There will be a{' '} + {CHARITY_FEE * 100}% fee charged. +
    + +
    + + + + + {noHasMinRedeemableCash && !allDisabled ? ( + + You need at least{' '} + {' '} + to cash out + + ) : null} + + + + ${((1 - CHARITY_FEE) * redeemableCash).toFixed(2)} + {' '} + value + + + + + + ) +} diff --git a/web/components/charts/contract/depth-chart.tsx b/web/components/charts/contract/depth-chart.tsx index 5d88ae58db..e9a387d922 100644 --- a/web/components/charts/contract/depth-chart.tsx +++ b/web/components/charts/contract/depth-chart.tsx @@ -8,7 +8,7 @@ import { getDisplayProbability } from 'common/calculate' import { HistoryPoint } from 'common/chart' import { scaleLinear } from 'd3-scale' import { AreaWithTopStroke, SVGChart, formatPct } from '../helpers' -import { curveStepBefore, line } from 'd3-shape' +import { curveStepAfter } from 'd3-shape' import { axisBottom, axisRight } from 'd3-axis' import { formatLargeNumber } from 'common/util/format' import { Answer } from 'common/answer' @@ -45,28 +45,23 @@ export function DepthChart(props: { const xScale = scaleLinear().domain([0, 1]).range([0, width]) const yScale = scaleLinear().domain([0, maxAmount]).range([height, 0]) - const dl = line() - .x((p) => xScale(p.x)) - .y((p) => yScale(p.y)) - .curve(curveStepBefore) const yAxis = axisRight(yScale).ticks(8).tickFormat(formatLargeNumber) const xAxis = axisBottom(xScale).ticks(6).tickFormat(formatPct) - const dYes = dl(yesData) - const dNo = dl(noData) - - if (dYes === null || dNo === null) return null + if (yesData.length === 0 || noData.length === 0) { + return null + } return ( - + xScale(p.x)} py0={yScale(0)} py1={(p) => yScale(p.y)} - curve={curveStepBefore} + curve={curveStepAfter} /> xScale(p.x)} py0={yScale(0)} py1={(p) => yScale(p.y)} - curve={curveStepBefore} + curve={curveStepAfter} /> {/* line at current value */} @@ -92,7 +87,8 @@ export function DepthChart(props: { } // Converts a list of LimitBets into a list of coordinates to render into a depth chart. -// Going in order of probability, the y value accumulates each order's amount. +// The y value accumulates each order's amount. +// Note this means YES bets are in reverse probability order function cumulative(bets: LimitBet[]): HistoryPoint[] { const result: HistoryPoint[] = [] let totalAmount = 0 diff --git a/web/components/charts/generic-charts.tsx b/web/components/charts/generic-charts.tsx index 33c8476dce..888bcc46e2 100644 --- a/web/components/charts/generic-charts.tsx +++ b/web/components/charts/generic-charts.tsx @@ -728,6 +728,7 @@ export const SingleValueHistoryChart =

    (props: { hideXAxis?: boolean onGraphClick?: () => void areaClassName?: string + noWatermark?: boolean className?: string }) => { const { @@ -749,6 +750,7 @@ export const SingleValueHistoryChart =

    (props: { hideXAxis, onGraphClick, areaClassName, + noWatermark, } = props useLayoutEffect(() => { @@ -873,6 +875,7 @@ export const SingleValueHistoryChart =

    (props: { pointerMode={pointerMode} hideXAxis={hideXAxis} yKind={yKind} + noWatermark={noWatermark} > {typeof color !== 'string' && ( diff --git a/web/components/charts/stats.tsx b/web/components/charts/stats.tsx index a35c24abb6..05f0e8b862 100644 --- a/web/components/charts/stats.tsx +++ b/web/components/charts/stats.tsx @@ -81,6 +81,7 @@ export function DailyChart(props: { values: Point[]; pct?: boolean }) { curve={curveLinear} zoomParams={zoomParams} showZoomer + noWatermark /> )} diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index def1e2aa19..9f7e238052 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -25,8 +25,9 @@ export function AuthorInfo(props: { contract: Contract resolverId?: string // live updated className?: string + userNameClass?: string }) { - const { contract, resolverId, className } = props + const { contract, resolverId, className, userNameClass } = props const { creatorId, creatorName, creatorUsername, creatorAvatarUrl } = contract const resolver = useDisplayUserById(resolverId) return ( @@ -38,7 +39,7 @@ export function AuthorInfo(props: { size={'xs'} /> - + ) : contract.token === 'CASH' && user && !user.idVerified ? ( - - Verify your info to start trading on sweepstakes markets and earn a - bonus of{' '} - - !{' '} - - Verify - - + +

    + Must be verified to {TRADE_TERM} +
    +

    + Verify your info to start trading on sweepstakes markets! +

    + + ) : contract.token === 'CASH' && blockFromSweepstakes(user) ? ( You are not eligible to trade on sweepstakes markets. diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index 9232bf86d4..a43110d6cd 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -134,6 +134,7 @@ export function ContractTabs(props: { title: positionsTitle, content: ( 0 && @@ -151,6 +152,7 @@ export function ContractTabs(props: { content: ( {formatWithToken({ amount: collectedFees.creatorFee, - token: isCashContract ? 'CASH' : 'M$', - toDecimal: isCashContract ? 4 : 2, + token: 'M$', + toDecimal: 2, })}{' '} earned diff --git a/web/components/contract/twomba-contract-page.tsx b/web/components/contract/twomba-contract-page.tsx index eaa3a90ecf..f416328ad6 100644 --- a/web/components/contract/twomba-contract-page.tsx +++ b/web/components/contract/twomba-contract-page.tsx @@ -74,6 +74,8 @@ import { SpiceCoin } from 'web/public/custom-components/spiceCoin' import { YourTrades } from 'web/pages/[username]/[contractSlug]' import { useSweepstakes } from '../sweestakes-context' import { useMonitorStatus } from 'web/hooks/use-monitor-status' +import { ToggleVerifyCallout } from '../twomba/toggle-verify-callout' +import { useRouter } from 'next/router' export function TwombaContractPageContent(props: ContractParams) { const { @@ -89,7 +91,14 @@ export function TwombaContractPageContent(props: ContractParams) { cash, } = props - const { isPlay } = useSweepstakes() + const { isPlay, setIsPlay } = useSweepstakes() + const router = useRouter() + useEffect(() => { + if (router.isReady) { + setIsPlay(router.query.play !== 'false') + } + }, [router.isReady]) + const livePlayContract = useLiveContractWithAnswers(props.contract) const liveCashContract = props.cash ? // eslint-disable-next-line react-hooks/rules-of-hooks @@ -143,7 +152,7 @@ export function TwombaContractPageContent(props: ContractParams) { contractId: cash?.contract.id ?? '', outcomeType: cash?.contract.outcomeType, userId: user?.id, - lastBetTime: props.lastBetTime, + lastBetTime: cash?.lastBetTime, totalBets: cash?.totalBets ?? 0, pointsString: cash?.pointsString, multiPointsString: cash?.multiPointsString, @@ -288,7 +297,6 @@ export function TwombaContractPageContent(props: ContractParams) {
    )} - {(headerStuck || !coverImageUrl) && ( )} + {!!liveContract.siblingContractId && ( + + )}
    -
    - - - -
    + + -
    + - {showRelatedMarketsBelowBet && ( - - )} {showReview && user && (
    + + + + {!user && } {!!user && ( @@ -441,19 +448,7 @@ export function TwombaContractPageContent(props: ContractParams) { contract={props.contract} /> )} - {showExplainerPanel && ( -
    -

    What is this?

    - -
    - )} - {comments.length > 3 && ( - - )} + {isResolved && resolution !== 'CANCEL' && ( <>
    + {showExplainerPanel && ( +
    +

    What is this?

    + +
    + )} - {!!currentContract.siblingContractId && } + {!!currentContract.siblingContractId && ( +
    + + +
    + )} {!playContract.coverImageUrl && isCreator && ( onClick('What is Manifold?')} > -
    - Manifold lets you {TRADE_TERM} on upcoming events using play money. As - other users {TRADE_TERM} against you, it creates a probability of how - likely the event will happen—this is known as a prediction market. -
    -
    + + +) + +export const WhatIsManifoldContent = ({ + className, +}: { + className?: string +}) => ( + <> + {TWOMBA_ENABLED ? ( +
    + Manifold lets you {TRADE_TERM} on upcoming events. As other users{' '} + {TRADE_TERM} against you, it creates a probability of how likely the + event will happen—this is known as a prediction market. +
    + ) : ( +
    + Manifold lets you {TRADE_TERM} on upcoming events using play money. As + other users {TRADE_TERM} against you, it creates a probability of how + likely the event will happen—this is known as a prediction market. +
    + )} +
    {capitalize(TRADE_TERM)} on current events, politics, tech, and AI, or create your own market about an event you care about for others to trade on!
    - + ) const WhyBet = ({ onClick }: { onClick: (sectionTitle: string) => void }) => ( @@ -104,7 +126,7 @@ const WhyBet = ({ onClick }: { onClick: (sectionTitle: string) => void }) => ( {capitalize(TRADING_TERM)} contributes to accurate answers of important, real-world questions.
    - {SPICE_PRODUCTION_ENABLED && ( + {!TWOMBA_ENABLED && SPICE_PRODUCTION_ENABLED && (
    {capitalize(TRADE_TERM)} to win prizepoints! Redeem them and we will donate to a charity of your choice. Our users have{' '} @@ -118,6 +140,22 @@ const WhyBet = ({ onClick }: { onClick: (sectionTitle: string) => void }) => ( so far!
    )} + {TWOMBA_ENABLED && ( + <> +
    + {capitalize(TRADE_TERM)} for a chance to win real cash prizes{' '} + when you trade with{' '} + + + + {' '} + {SWEEPIES_NAME} ({SWEEPIES_MONIKER}) + + + . +
    + + )}
    Get started for free! No credit card required.
    diff --git a/web/components/gidx/location-panel.tsx b/web/components/gidx/location-panel.tsx index 0575152b96..45eaaf3336 100644 --- a/web/components/gidx/location-panel.tsx +++ b/web/components/gidx/location-panel.tsx @@ -4,17 +4,13 @@ import { GPSData, } from 'common/gidx/gidx' import { useEffect, useState } from 'react' +import { Button } from 'web/components/buttons/button' +import { LoadingIndicator } from 'web/components/widgets/loading-indicator' import { useNativeMessages } from 'web/hooks/use-native-messages' import { getIsNative } from 'web/lib/native/is-native' import { postMessageToNative } from 'web/lib/native/post-message' -import { Col } from 'web/components/layout/col' -import { LoadingIndicator } from 'web/components/widgets/loading-indicator' -import { Row } from 'web/components/layout/row' -import { Button } from 'web/components/buttons/button' -import { - registrationBottomRowClass, - registrationColClass, -} from 'web/components/gidx/register-user-form' +import { BottomRow } from './register-component-helpers' +import { LocationBlockedIcon } from 'web/public/custom-components/locationBlockedIcon' export const LocationPanel = (props: { setLocation: (data: GPSData) => void @@ -128,21 +124,18 @@ export const LocationPanel = (props: { } if (!checkedPermissions) { - return ( - - - - ) + return } return ( - - Location required - + <> + + Location required + You must allow location sharing to verify that you're in a participating municipality. - + @@ -153,7 +146,7 @@ export const LocationPanel = (props: { > Share location - + {locationError && ( {locationError} @@ -162,6 +155,6 @@ export const LocationPanel = (props: { : ''} )} - + ) } diff --git a/web/components/gidx/register-component-helpers.tsx b/web/components/gidx/register-component-helpers.tsx new file mode 100644 index 0000000000..54d0d6e9d0 --- /dev/null +++ b/web/components/gidx/register-component-helpers.tsx @@ -0,0 +1,16 @@ +import { Row } from 'web/components/layout/row' + +export function InputTitle(props: { + className?: string + children: React.ReactNode +}) { + return ( + + {props.children} + + ) +} + +export function BottomRow(props: { children: React.ReactNode }) { + return {props.children} +} diff --git a/web/components/gidx/register-user-form.tsx b/web/components/gidx/register-user-form.tsx index 778a169a47..a91963a97d 100644 --- a/web/components/gidx/register-user-form.tsx +++ b/web/components/gidx/register-user-form.tsx @@ -29,9 +29,15 @@ import { import { LocationPanel } from 'web/components/gidx/location-panel' import { KYC_VERIFICATION_BONUS_CASH } from 'common/economy' import { CoinNumber } from 'web/components/widgets/coin-number' - -export const registrationColClass = 'gap-3 p-4' -export const registrationBottomRowClass = 'mb-4 mt-4 w-full gap-16' +import { RegisterIcon } from 'web/public/custom-components/registerIcon' +import { + BottomRow, + InputTitle, +} from 'web/components/gidx/register-component-helpers' +import { DocumentUploadIcon } from 'web/public/custom-components/documentUploadIcon' +import { LocationBlockedIcon } from 'web/public/custom-components/locationBlockedIcon' +import { RiUserForbidLine } from 'react-icons/ri' +import { PiClockCountdown } from 'react-icons/pi' export const RegisterUserForm = (props: { user: User @@ -131,20 +137,21 @@ export const RegisterUserForm = (props: { if (page === 'intro') { return ( - - - Identity Verification + <> + +
    Identity Verification
    + + To use sweepstakes coins, you must verify your identity. - To use sweepstakes coins, you must verify your identity. - + - - + + ) } @@ -177,38 +184,40 @@ export const RegisterUserForm = (props: { } if (page === 'form') { - const sectionClass = 'gap-2 w-full sm:w-96' + const sectionClass = 'gap-0.5 w-full' return ( - - - Identity Verification - - - First Name - - setUserInfo({ ...userInfo, FirstName: e.target.value }) - } - /> - - - Last Name - - setUserInfo({ ...userInfo, LastName: e.target.value }) - } - /> - + <> + Identity Verification + +
    + + First Name + + setUserInfo({ ...userInfo, FirstName: e.target.value }) + } + /> + + + + Last Name + + setUserInfo({ ...userInfo, LastName: e.target.value }) + } + /> + +
    - Date of Birth + Date of Birth - Citizenship Country + Email Address + + setUserInfo({ ...userInfo, EmailAddress: e.target.value }) + } + /> + + +
    + + + Citizenship Country @@ -229,8 +252,9 @@ export const RegisterUserForm = (props: { } /> + - Address Line 1 + Address Line 1 - Address Line 2 + Address Line 2 - Email Address + City - setUserInfo({ ...userInfo, EmailAddress: e.target.value }) - } + onChange={(e) => setUserInfo({ ...userInfo, City: e.target.value })} /> - - - City + + + State - setUserInfo({ ...userInfo, City: e.target.value }) + setUserInfo({ ...userInfo, StateCode: e.target.value }) } /> - - State + + Postal Code - setUserInfo({ ...userInfo, StateCode: e.target.value }) + setUserInfo({ ...userInfo, PostalCode: e.target.value }) } /> - - Postal Code - - setUserInfo({ ...userInfo, PostalCode: e.target.value }) - } - /> - - {error && ( {error} @@ -315,7 +325,7 @@ export const RegisterUserForm = (props: { )} - + - - + + ) } if (page === 'documents') { return ( - - + <> + Identity Document Verification router.back()} next={() => setPage('final')} /> - + ) } if (user.kycDocumentStatus === 'pending') { return ( - - - Verification pending + <> + + Verification pending + + Thank you for submitting your identification information! Your + identity verification is pending. Check back later to see if you're + verified. - Thank you for submitting your identification information! Your identity - verification is pending. Check back later to see if you're verified. - + - + ) } return ( - - - Identity Verification Complete + <> +
    🎉
    + + Identity Verification Complete! - + Hooray! Now you can participate in sweepstakes markets. We sent you{' '} {' '} to get started. - +
    {/*// TODO: auto-redirect rather than make them click this button*/} {redirect === 'checkout' ? ( @@ -445,7 +475,7 @@ export const RegisterUserForm = (props: { Done )} - - +
    + ) } diff --git a/web/components/multiple-or-single-avatars.tsx b/web/components/multiple-or-single-avatars.tsx index de8b620d32..6e0759a545 100644 --- a/web/components/multiple-or-single-avatars.tsx +++ b/web/components/multiple-or-single-avatars.tsx @@ -3,9 +3,11 @@ import { Col } from 'web/components/layout/col' import { Row } from './layout/row' import clsx from 'clsx' import { UserHovercard } from './user/user-hovercard' +import { buildArray } from 'common/util/array' export const MultipleOrSingleAvatars = (props: { avatars: Array<{ avatarUrl: string; id: string }> + total?: number onClick?: () => void size: AvatarSizeType // TODO: standardize these numbers so they are calculated from the size @@ -13,7 +15,7 @@ export const MultipleOrSingleAvatars = (props: { startLeft?: number className?: string }) => { - const { avatars, className, onClick, size } = props + const { avatars, total, className, onClick, size } = props const combineAvatars = ( avatars: Array<{ avatarUrl: string; id: string }> ) => { @@ -26,22 +28,54 @@ export const MultipleOrSingleAvatars = (props: { const max = avatarsToCombine.length const startLeft = (props.startLeft ?? 0.1) * (max - 1) const spacing = props.spacing ?? 0.3 - return avatarsToCombine.map((n, index) => ( -
    0 - ? { - marginLeft: `${-startLeft + index * spacing}rem`, - } - : {} - } - > - - - -
    - )) + + const s = + size == '2xs' + ? 4 + : size == 'xs' + ? 6 + : size == 'sm' + ? 8 + : size == 'md' + ? 10 + : size == 'lg' + ? 12 + : size == 'xl' + ? 24 + : 10 + const sizeInPx = s * 4 + + return buildArray( + avatarsToCombine.map((n, index) => ( +
    0 + ? { + marginLeft: `${-startLeft + index * spacing}rem`, + } + : {} + } + > + +
    + )), + total && total > maxToShow && ( +
    + +{total - maxToShow} +
    + ) + ) } return ( - + {children} @@ -98,6 +98,17 @@ export function DowntimeBanner() { ) } +export function WatchPartyBanner() { + return ( + + 🇺🇸 Join the presidential debate watch party on Manifold TV! 🇺🇸 + + ) +} + export const useBanner = (name: string) => { const [bannerSeen, setBannerSeen] = usePersistentLocalState( 0, diff --git a/web/components/notifications/income-summary-notifications.tsx b/web/components/notifications/income-summary-notifications.tsx index 78c68fff3b..58b2009150 100644 --- a/web/components/notifications/income-summary-notifications.tsx +++ b/web/components/notifications/income-summary-notifications.tsx @@ -678,7 +678,7 @@ function IncomeNotificationLabel(props: { } const BettorStatusLabel = (props: { uniqueBettorData: UniqueBettorData }) => { - const { bet, outcomeType, answerText, totalAmountBet } = + const { bet, outcomeType, answerText, totalAmountBet, token } = props.uniqueBettorData const { amount, outcome } = bet const showProb = @@ -689,7 +689,7 @@ const BettorStatusLabel = (props: { uniqueBettorData: UniqueBettorData }) => { return ( - {formatMoney(totalAmountBet ?? amount)} + {formatMoney(totalAmountBet ?? amount, token)} {' '} {showOutcome && `${outcome} `} on{' '} diff --git a/web/components/notifications/notification-types.tsx b/web/components/notifications/notification-types.tsx index 82c7c6894b..99a83f2848 100644 --- a/web/components/notifications/notification-types.tsx +++ b/web/components/notifications/notification-types.tsx @@ -9,6 +9,7 @@ import { ExtraPurchasedManaData, getSourceUrl, Notification, + PaymentCompletedData, ReactionNotificationTypes, ReviewNotificationData, } from 'common/notification' @@ -17,7 +18,7 @@ import { MANIFOLD_USER_NAME, MANIFOLD_USER_USERNAME, } from 'common/user' -import { formatMoney } from 'common/util/format' +import { formatMoney, formatMoneyUSD } from 'common/util/format' import { floatingEqual } from 'common/util/math' import { WeeklyPortfolioUpdate } from 'common/weekly-portfolio-update' import { sortBy } from 'lodash' @@ -66,9 +67,9 @@ import { PrimaryNotificationLink, QuestionOrGroupLink, } from './notification-helpers' -import { SPICE_COLOR } from 'web/components/portfolio/portfolio-value-graph' import { SpiceCoin } from 'web/public/custom-components/spiceCoin' -import { TRADED_TERM } from 'common/envs/constants' +import { TRADED_TERM, TWOMBA_ENABLED } from 'common/envs/constants' +import { BsBank } from 'react-icons/bs' export function NotificationItem(props: { notification: Notification @@ -142,6 +143,14 @@ export function NotificationItem(props: { setHighlighted={setHighlighted} /> ) + } else if (reason === 'payment_status') { + return ( + + ) } else if (reason === 'bounty_added') { return ( Your limit order{' '} - {limitOrderTotal && <>for {formatMoney(limitOrderTotal)}} is + {limitOrderTotal && <>for {formatMoney(limitOrderTotal, token)}} is complete )} {!!limitOrderRemaining && ( <> - You have {formatMoney(limitOrderRemaining)} - {limitOrderTotal && <>/{formatMoney(limitOrderTotal)}} remaining in - your order + You have {formatMoney(limitOrderRemaining, token)} + {limitOrderTotal && <>/{formatMoney(limitOrderTotal, token)}}{' '} + remaining in your order )} @@ -1369,6 +1381,7 @@ function LiquidityNotification(props: { sourceUserUsername, sourceText, sourceContractTitle, + data, } = notification return ( {' '} added{' '} - {sourceText && {formatMoney(parseInt(sourceText))} of}{' '} + {sourceText && ( + {formatMoney(parseInt(sourceText), data?.token)} of + )}{' '} liquidity{' '} {!isChildOfGroup && ( @@ -1440,11 +1455,8 @@ function ReferralProgramNotification(props: { Refer friends and get{' '} @@ -1918,3 +1930,33 @@ function ExtraPurchasedManaNotification(props: { ) } + +export function PaymentSuccessNotification(props: { + notification: Notification + highlighted: boolean + setHighlighted: (highlighted: boolean) => void + isChildOfGroup?: boolean +}) { + const { notification, isChildOfGroup, highlighted, setHighlighted } = props + const { amount, currency, paymentMethodType } = + notification.data as PaymentCompletedData + return ( + } + subtitle={ + + You should receive your funds within the next couple days. + + } + > + + Your {paymentMethodType} payment for {formatMoneyUSD(amount)} {currency}{' '} + was approved! + + + ) +} diff --git a/web/components/registration-verify-phone.tsx b/web/components/registration-verify-phone.tsx index c1efd08391..9e225cd18e 100644 --- a/web/components/registration-verify-phone.tsx +++ b/web/components/registration-verify-phone.tsx @@ -1,14 +1,14 @@ -import { Col } from 'web/components/layout/col' -import { Button } from 'web/components/buttons/button' -import { api } from 'web/lib/api/api' import { useEffect, useState } from 'react' -import { Input } from 'web/components/widgets/input' -import { PhoneInput } from 'react-international-phone' import { toast } from 'react-hot-toast' +import { PhoneInput } from 'react-international-phone' import 'react-international-phone/style.css' -import { Row } from 'web/components/layout/row' -import { track } from 'web/lib/service/analytics' +import { Button } from 'web/components/buttons/button' +import { Input } from 'web/components/widgets/input' import { useUser } from 'web/hooks/use-user' +import { api } from 'web/lib/api/api' +import { track } from 'web/lib/service/analytics' +import { PhoneIcon } from 'web/public/custom-components/phoneIcon' +import { BottomRow } from './gidx/register-component-helpers' export function RegistrationVerifyPhone(props: { cancel: () => void @@ -63,20 +63,19 @@ export function RegistrationVerifyPhone(props: { }, [user?.verifiedPhone]) return ( - + <> {page === 0 && ( - - - Verify your phone number - + <> + + Verify your phone number setPhoneNumber(phone)} placeholder={'Phone Number'} - className={'ml-3'} + className={'mx-auto'} /> - + @@ -87,23 +86,20 @@ export function RegistrationVerifyPhone(props: { > Request code - - + + )} {page === 1 && ( - - - - Enter verification code - - setOtp(e.target.value)} - placeholder="123456" - /> - - + <> + + Enter verification code + setOtp(e.target.value)} + placeholder="123456" + /> + @@ -114,9 +110,9 @@ export function RegistrationVerifyPhone(props: { > Verify - - + + )} - + ) } diff --git a/web/components/stats/bonus-summary.tsx b/web/components/stats/bonus-summary.tsx index c6e28c1391..2f4da97958 100644 --- a/web/components/stats/bonus-summary.tsx +++ b/web/components/stats/bonus-summary.tsx @@ -161,6 +161,7 @@ export const BonusSummary = (props: { background: 'black', borderRadius: '8px', pointerEvents: 'none', + opacity: 0, }} >
    diff --git a/web/components/stats/mana-summary.tsx b/web/components/stats/mana-summary.tsx index f97936e9df..b34569f956 100644 --- a/web/components/stats/mana-summary.tsx +++ b/web/components/stats/mana-summary.tsx @@ -5,34 +5,68 @@ import { scaleBand, scaleLinear, scaleOrdinal } from 'd3-scale' import { stack } from 'd3-shape' import { axisBottom, axisRight } from 'd3-axis' import { max } from 'd3-array' -import { uniq } from 'lodash' +import { unzip, zip, pick } from 'lodash' import { renderToString } from 'react-dom/server' import { Col } from 'web/components/layout/col' import { formatWithCommas } from 'common/util/format' - -const colors = [ - '#60d775', - '#FFDFBA', - '#BAFFC9', - '#BAE1FF', - '#D4A5A5', - '#A5D4D4', - '#D4D4A5', - '#D4A5C2', - '#FFB7B7', - '#B7FFB7', - '#FFD700', -] +import { Title } from 'web/components/widgets/title' type DateAndCategoriesToTotals = { date: string } & { [key: string]: number } -const categoryToColor = new Map() + +const categoryToLabel = { + total_value: 'total mana (-loans)', + balance: 'mana balance', + spice_balance: 'spice balance', + investment_value: 'invested', + loan_total: 'loans', + amm_liquidity: 'amm liquidity', + total_cash_value: 'total prize cash', + cash_balance: 'prize cash balance', + cash_investment_value: 'invested', + amm_cash_liquidity: 'amm liquidity', +} + +const categoryToColor = { + total_value: '#FFF0FF', + balance: '#B690D6', + spice_balance: '#FFA620', + investment_value: '#30A0C6', + loan_total: '#FFB7B7', + amm_liquidity: '#B7FFB7', + total_cash_value: '#FFFFF0', + cash_balance: '#FFD700', + cash_investment_value: '#60D0C6', + amm_cash_liquidity: '#20D020', +} + +const [categories, colors] = zip(...Object.entries(categoryToColor)) as [ + string[], + string[] +] +const colorScale = scaleOrdinal().domain(categories).range(colors) export const ManaSupplySummary = (props: { manaSupplyStats: rowFor<'mana_supply_stats'>[] }) => { const { manaSupplyStats } = props + const [manaData, cashData] = orderAndGroupData(manaSupplyStats) + + return ( + <> + Mana supply over time + + Prize cash supply supply over time + + + ) +} + +const StackedChart = (props: { + data: ({ date: string } & Partial>)[] +}) => { + const { data } = props const svgRef = useRef(null) const tooltipRef = useRef(null) const xAxisRef = useRef(null) @@ -42,15 +76,7 @@ export const ManaSupplySummary = (props: { const height = 500 const innerWidth = width - margin.left - margin.right const innerHeight = height - margin.top - margin.bottom - const { data, xScale, stackGen, colorScale, yScale, keys } = useMemo(() => { - const data = orderAndGroupData(manaSupplyStats) - const uniqueCategories = uniq(Object.keys(manaSupplyStats[0])) - for (let i = 0; i < uniqueCategories.length; i++) { - categoryToColor.set(uniqueCategories[i], colors[i]) - } - const colorScale = scaleOrdinal() - .domain(uniqueCategories) - .range(colors) + const { xScale, layers, yScale, keys } = useMemo(() => { const xScale = scaleBand() .domain(data.map((d) => d.date)) .range([0, innerWidth]) @@ -59,15 +85,18 @@ export const ManaSupplySummary = (props: { const yScale = scaleLinear().range([innerHeight, 0]) const keys = Array.from( - new Set(data.flatMap((d) => Object.keys(d)).filter((k) => k !== 'date')) + new Set(data.flatMap((d) => Object.keys(d))) + ).filter( + (key) => !['date', 'total_value', 'total_cash_value'].includes(key) ) + const stackGen = stack<{ [key: string]: number }>().keys(keys) - const layers = stackGen(data) + const layers = stackGen(data as any) const maxY = max(layers, (layer) => max(layer, (d) => d[1] as number)) || 0 xScale.domain(data.map((d) => d.date)) yScale.domain([0, maxY]).nice() - return { data, xScale, yScale, keys, colorScale, stackGen } - }, [manaSupplyStats.length]) + return { data, xScale, yScale, keys, colorScale, layers } + }, [data.length]) useEffect(() => { if (xScale && xAxisRef.current) { @@ -90,7 +119,7 @@ export const ManaSupplySummary = (props: { {data.length > 0 && keys.length > 0 && - stackGen?.(data).map((layer, i) => ( + layers.map((layer, i) => ( {layer.map((d, j) => ( ms.start_time === datum.date - )!.total_value as number - } - categoryToColor={categoryToColor} - data={datum} - /> - ) + renderToString() ) }} onMouseMove={(event) => { @@ -149,6 +168,7 @@ export const ManaSupplySummary = (props: { background: 'black', borderRadius: '8px', pointerEvents: 'none', + opacity: 0, }} > @@ -156,30 +176,34 @@ export const ManaSupplySummary = (props: { } const orderAndGroupData = (data: rowFor<'mana_supply_stats'>[]) => { - return data.map((datum) => { - const { - id: _, - created_time: __, - end_time: ___, - start_time, - total_value: ____, - total_cash_value: _____, - ...rest - } = datum - - return { - ...rest, - date: start_time, - } as any as DateAndCategoriesToTotals - }) + return unzip( + data.map((datum) => [ + { + date: datum.start_time, + ...pick(datum, [ + 'total_value', + 'balance', + 'spice_balance', + 'investment_value', + 'loan_total', + 'amm_liquidity', + ]), + }, + { + date: datum.start_time, + ...pick(datum, [ + 'total_cash_value', + 'cash_balance', + 'cash_investment_value', + 'amm_cash_liquidity', + ]), + }, + ]) + ) } -const StackedChartTooltip = (props: { - data: DateAndCategoriesToTotals - totalValue: number - categoryToColor: Map -}) => { - const { data, totalValue, categoryToColor } = props +const StackedChartTooltip = (props: { data: DateAndCategoriesToTotals }) => { + const { data } = props return ( {new Date(Date.parse(data.date)).toLocaleString('en-us', { @@ -189,15 +213,17 @@ const StackedChartTooltip = (props: { hour: 'numeric', })}
    - total (-loans): {formatWithCommas(totalValue)} {Object.keys(data) .filter((k) => k !== 'date') .map((key) => ( - {key}: {formatWithCommas(data[key])} + {categoryToLabel[key as keyof typeof categoryToLabel]}:{' '} + {formatWithCommas(data[key])} ))} diff --git a/web/components/sweestakes-context.tsx b/web/components/sweestakes-context.tsx index 24f5acb1ff..c486dc6a9c 100644 --- a/web/components/sweestakes-context.tsx +++ b/web/components/sweestakes-context.tsx @@ -1,5 +1,5 @@ import React, { createContext, useContext } from 'react' -import { usePersistentQueryState } from 'web/hooks/use-persistent-query-state' +import { usePersistentLocalState } from 'web/hooks/use-persistent-local-state' type SweepstakesContextType = { isPlay: boolean @@ -13,7 +13,7 @@ const SweepstakesContext = createContext( export const SweepstakesProvider: React.FC<{ children: React.ReactNode }> = ({ children, }) => { - const [queryPlay, setQueryPlay] = usePersistentQueryState('play', 'true') + const [queryPlay, setQueryPlay] = usePersistentLocalState('play', 'true') const isPlay = !queryPlay || queryPlay === 'true' const setIsPlay = (isPlay: boolean) => { diff --git a/web/components/twomba/toggle-verify-callout.tsx b/web/components/twomba/toggle-verify-callout.tsx new file mode 100644 index 0000000000..138b8c6a77 --- /dev/null +++ b/web/components/twomba/toggle-verify-callout.tsx @@ -0,0 +1,119 @@ +import { XIcon } from '@heroicons/react/solid' +import clsx from 'clsx' +import { KYC_VERIFICATION_BONUS_CASH } from 'common/economy' +import { + getVerificationStatus, + PROMPT_VERIFICATION_MESSAGES, +} from 'common/user' +import Link from 'next/link' +import { usePersistentLocalState } from 'web/hooks/use-persistent-local-state' +import { useUser } from 'web/hooks/use-user' +import { buttonClass } from '../buttons/button' +import { CoinNumber } from '../widgets/coin-number' +import { SWEEPIES_NAME, TRADE_TERM } from 'common/envs/constants' +import { capitalize } from 'lodash' + +export function ToggleVerifyCallout(props: { + className?: string + caratClassName?: string +}) { + const { className, caratClassName } = props + const user = useUser() + const [dismissed, setDismissed] = usePersistentLocalState( + false, + `toggle-verify-callout-dismissed-${user?.id ?? 'logged-out'}` + ) + + if (dismissed) return null + + // TWODO: Add a link to about here + if (!user) { + return ( + +
    + This is a {SWEEPIES_NAME} market! {capitalize(TRADE_TERM)} with{' '} + {SWEEPIES_NAME} for the chance to win real cash prizes. +
    +
    + ) + } + + const { message } = getVerificationStatus(user) + + if (!PROMPT_VERIFICATION_MESSAGES.includes(message)) return null + + return ( + + Why stop at play money? Verify your identity and start earning{' '} + real cash prizes today. +
    +
    +
    +
    +
    + +
    + ) +} + +function CalloutFrame(props: { + children: React.ReactNode + className?: string + caratClassName?: string + setDismissed: (dismissed: boolean) => void +}) { + const { children, className, caratClassName, setDismissed } = props + return ( +
    +
    + + {children} +
    +
    +
    +
    +
    +
    +
    + ) +} + +export function VerifyButton(props: { className?: string }) { + const { className } = props + return ( + + Verify and claim + + + ) +} diff --git a/web/components/user/user-hovercard.tsx b/web/components/user/user-hovercard.tsx index 8509468965..9a836dcbc3 100644 --- a/web/components/user/user-hovercard.tsx +++ b/web/components/user/user-hovercard.tsx @@ -14,6 +14,7 @@ import { Col } from '../layout/col' import { FullUser } from 'common/api/user-types' import { useIsClient } from 'web/hooks/use-is-client' import { TRADE_TERM } from 'common/envs/constants' +import { SimpleCopyTextButton } from 'web/components/buttons/copy-link-button' export type UserHovercardProps = { children: React.ReactNode @@ -104,6 +105,14 @@ const FetchUserHovercardContent = forwardRef(
    Joined {dayjs(user.createdTime).format('MMM DD, YYYY')}
    + {isMod && ( + + )} diff --git a/web/components/widgets/coin-number.tsx b/web/components/widgets/coin-number.tsx index 6e629aaf1f..d89fab4151 100644 --- a/web/components/widgets/coin-number.tsx +++ b/web/components/widgets/coin-number.tsx @@ -78,7 +78,10 @@ export function CoinNumber(props: { '---' ) : coinType === 'sweepies' || coinType === 'CASH' ? ( // TWODO: give sweepies all the variations as well - formatSweepiesNumber(Math.abs(amount ?? 0)) + formatSweepiesNumber(Math.abs(amount ?? 0), { + toDecimal: numberType == 'toDecimal' ? 2 : undefined, + short: numberType == 'short' ? true : false, + }) ) : numberType == 'short' ? ( shortenNumber( +formatMoneyNoMoniker(Math.abs(amount ?? 0)).replaceAll(',', '') diff --git a/web/hooks/use-persistent-query-state.ts b/web/hooks/use-persistent-query-state.ts index c9efc24b00..1589264cbd 100644 --- a/web/hooks/use-persistent-query-state.ts +++ b/web/hooks/use-persistent-query-state.ts @@ -1,6 +1,6 @@ import { useRouter } from 'next/router' import { useEffect, useState } from 'react' -import { pickBy, debounce } from 'lodash' +import { pickBy, debounce, mapValues } from 'lodash' import { usePersistentInMemoryState } from './use-persistent-in-memory-state' type UrlParams = Record @@ -21,14 +21,19 @@ export const usePersistentQueriesState = ( // On page load, initialize the state to the current query params once. useEffect(() => { if (router.isReady) { - setState({ ...defaultValue, ...router.query }) + setState({ + ...defaultValue, + ...mapValues(router.query, (v) => + typeof v === 'string' ? decodeURIComponent(v) : v + ), + }) setReady(true) } }, [router.isReady]) const setRouteQuery = debounce((newQuery: string) => { const { pathname } = router - const q = newQuery ? '?' + encodeURI(newQuery) : '' + const q = newQuery ? '?' + newQuery : '' router.replace(pathname + q, undefined, { shallow: true }) }, 200) @@ -37,8 +42,8 @@ export const usePersistentQueriesState = ( const newState = { ...state, ...router.query, ...update } as T setState(newState) const query = pickBy(newState, (v) => v) - const newQuery = Object.keys(query) - .map((key) => `${key}=${query[key]}`) + const newQuery = Object.entries(query) + .map(([key, val]) => `${key}=${encodeURIComponent(val!)}`) .join('&') setRouteQuery(newQuery) } diff --git a/web/hooks/use-portfolios.ts b/web/hooks/use-portfolios.ts deleted file mode 100644 index ebf562aba9..0000000000 --- a/web/hooks/use-portfolios.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Portfolio } from 'common/portfolio' -import { usePersistentInMemoryState } from './use-persistent-in-memory-state' -import { useEffect } from 'react' -import { getAllPortfolios } from 'web/lib/supabase/portfolio' - -export const usePortfolios = () => { - const [portfolios, setPortfolios] = usePersistentInMemoryState( - [], - 'all-portfolios' - ) - - useEffect(() => { - getAllPortfolios().then(setPortfolios) - }, []) - - return portfolios -} diff --git a/web/hooks/use-public-chat-messages.ts b/web/hooks/use-public-chat-messages.ts index 75288b9c7d..536293e948 100644 --- a/web/hooks/use-public-chat-messages.ts +++ b/web/hooks/use-public-chat-messages.ts @@ -21,7 +21,7 @@ export function usePublicChat(channelId: string, limit: number) { .select('*') .eq('channel_id', channelId) .order('created_time', { ascending: false }) - .gt('id', newestId) + .gt('id', newestId ?? 0) .limit(limit) if (data) { diff --git a/web/lib/api/api.ts b/web/lib/api/api.ts index 5f2817c2d8..1b5b8f742e 100644 --- a/web/lib/api/api.ts +++ b/web/lib/api/api.ts @@ -4,7 +4,6 @@ import { JSONContent } from '@tiptap/core' import { Group, PrivacyStatusType } from 'common/group' import { AD_RATE_LIMIT } from 'common/boost' import { ContractComment } from 'common/comment' -import { Portfolio, PortfolioItem } from 'common/portfolio' import { ReportProps } from 'common/report' import { BaseDashboard, DashboardItem } from 'common/dashboard' import { Bet } from 'common/bet' @@ -207,27 +206,6 @@ export function cancelBounty(params: { contractId: string }) { return call(getApiUrl('cancel-bounty'), 'POST', params) } -export function createPortfolio(params: { - name: string - items: PortfolioItem[] -}) { - return call(getApiUrl('createportfolio'), 'POST', params) -} - -export function updatePortfolio(params: { id: string } & Partial) { - return call(getApiUrl('updateportfolio'), 'POST', params) -} - -export function buyPortfolio( - params: { - portfolioId: string - amount: number - buyOpposite?: boolean - } & Partial -) { - return call(getApiUrl('buyportfolio'), 'POST', params) -} - export function searchGiphy(params: { term: string; limit: number }) { return call(getApiUrl('searchgiphy'), 'POST', params) } diff --git a/web/lib/supabase/portfolio.ts b/web/lib/supabase/portfolio.ts deleted file mode 100644 index 65e9d465c3..0000000000 --- a/web/lib/supabase/portfolio.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { convertPortfolio } from 'common/portfolio' -import { run } from 'common/supabase/utils' -import { db } from './db' - -export async function getPortfolioBySlug(slug: string) { - const { data } = await run(db.from('portfolios').select().eq('slug', slug)) - if (data && data.length > 0) { - return convertPortfolio(data[0]) - } - return null -} - -export async function getAllPortfolios() { - const { data } = await run(db.from('portfolios').select('*')) - if (data && data.length > 0) { - return data.map(convertPortfolio) - } - return [] -} diff --git a/web/lib/supabase/txns.ts b/web/lib/supabase/txns.ts index 4be0e23614..f515d4e482 100644 --- a/web/lib/supabase/txns.ts +++ b/web/lib/supabase/txns.ts @@ -39,7 +39,14 @@ export function getDonationsPageQuery(charityId: string) { const donations = txnData.map((t) => ({ user: usersById[t.from_id!], ts: tsToMillis(t.created_time), - amount: t.token == 'M$' ? t.amount / 100 : t.amount / 1000, + amount: + t.token == 'M$' + ? t.amount / 100 + : t.token == 'SPICE' + ? t.amount / 1000 + : t.token == 'CASH' + ? t.amount + : 0, })) return donations } diff --git a/web/next.config.js b/web/next.config.js index e40221dbb3..93006f15e9 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -142,9 +142,9 @@ module.exports = { permanent: true, }, { - source: '/post/:slug*', - destination: '/old-posts/:slug*', - permanent: false, + source: '/old-posts/:slug*', + destination: '/post/:slug*', + permanent: true, }, { source: '/questions:slug*', @@ -167,7 +167,7 @@ module.exports = { { source: '/dashboard/:slug', destination: '/news/:slug', - permanent: false, // TODO: after 1/7/2024 change this and below to true + permanent: true, }, { source: '/home:slug*', diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index f2371277bb..b8e4947d1c 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -94,7 +94,7 @@ import { pick } from 'lodash' export async function getStaticProps(ctx: { params: { username: string; contractSlug: string } }) { - const { contractSlug } = ctx.params + const { username, contractSlug } = ctx.params const adminDb = await initSupabaseAdmin() const contract = await getContractFromSlug(adminDb, contractSlug) @@ -122,7 +122,7 @@ export async function getStaticProps(ctx: { return { redirect: { - destination: `/username/${slug}?play=false`, + destination: `/${username}/${slug}?play=false`, permanent: false, }, } @@ -138,6 +138,7 @@ export async function getStaticProps(ctx: { const params = await getContractParams(cashContract, adminDb) cash = pick(params, [ 'contract', + 'lastBetTime', 'pointsString', 'multiPointsString', 'userPositionsByOutcome', diff --git a/web/pages/about.tsx b/web/pages/about.tsx index 6b5cda2c08..dec6c337c5 100644 --- a/web/pages/about.tsx +++ b/web/pages/about.tsx @@ -6,7 +6,10 @@ import { PrivacyTermsLab } from 'web/components/privacy-terms' import { SEO } from 'web/components/SEO' import { Title } from 'web/components/widgets/title' import { getNativePlatform } from 'web/lib/native/is-native' -import { ExplainerPanel } from 'web/components/explainer-panel' +import { + ExplainerPanel, + WhatIsManifoldContent, +} from 'web/components/explainer-panel' import { LabCard } from './lab' import { TRADE_TERM } from 'common/envs/constants' import { capitalize } from 'lodash' @@ -27,19 +30,7 @@ export default function AboutPage() { About -
    -
    - Manifold lets you {TRADE_TERM} on upcoming events using play - money. As other users {TRADE_TERM} against you, it creates a - probability of how likely the event will happen—this is known as a - prediction market. -
    -
    - {capitalize(TRADE_TERM)} on current events, politics, tech, and - AI, or create your own market about an event you care about for - others to trade on! -
    -
    + diff --git a/web/pages/cashout.tsx b/web/pages/cashout.tsx index acb8f32ec3..6bfb97cbc6 100644 --- a/web/pages/cashout.tsx +++ b/web/pages/cashout.tsx @@ -1,18 +1,17 @@ +import clsx from 'clsx' import { KYC_VERIFICATION_BONUS_CASH, MIN_CASHOUT_AMOUNT, SWEEPIES_CASHOUT_FEE, } from 'common/economy' -import { SWEEPIES_NAME } from 'common/envs/constants' +import { SWEEPIES_NAME, TRADED_TERM } from 'common/envs/constants' import { CheckoutSession, GPSData } from 'common/gidx/gidx' import { ageBlocked, getVerificationStatus, - IDENTIFICATION_FAILED_MESSAGE, locationBlocked, - PHONE_NOT_VERIFIED_MESSAGE, + PROMPT_VERIFICATION_MESSAGES, USER_BLOCKED_MESSAGE, - USER_NOT_REGISTERED_MESSAGE, } from 'common/user' import { formatSweepies, formatSweepsToUSD } from 'common/util/format' import Link from 'next/link' @@ -20,11 +19,18 @@ import { useRouter } from 'next/router' import { useState } from 'react' import { MdOutlineNotInterested } from 'react-icons/md' import { RiUserForbidLine } from 'react-icons/ri' -import { Button } from 'web/components/buttons/button' +import { + baseButtonClasses, + Button, + buttonClass, +} from 'web/components/buttons/button' +import { CashToManaForm } from 'web/components/cashout/cash-to-mana' +import { SelectCashoutOptions } from 'web/components/cashout/select-cashout-options' import { LocationPanel } from 'web/components/gidx/location-panel' import { UploadDocuments } from 'web/components/gidx/upload-document' import { AmountInput } from 'web/components/widgets/amount-input' import { CoinNumber } from 'web/components/widgets/coin-number' +import { InfoTooltip } from 'web/components/widgets/info-tooltip' import { Input } from 'web/components/widgets/input' import { LoadingIndicator } from 'web/components/widgets/loading-indicator' import { useAPIGetter } from 'web/hooks/use-api-getter' @@ -38,12 +44,62 @@ import { Col } from '../components/layout/col' import { Page } from '../components/layout/page' import { Row } from '../components/layout/row' +export type CashoutPagesType = + | 'select-cashout-method' + | MoneyCashoutPagesType + | ManaCashoutPagesType + +type MoneyCashoutPagesType = + | 'location' + | 'get-session' + | 'ach-details' + | 'waiting' + | 'documents' + +type ManaCashoutPagesType = 'custom-mana' + +function SweepiesStats(props: { + redeemableCash: number + cashBalance: number + className?: string +}) { + const { redeemableCash, cashBalance, className } = props + return ( + + +
    + Redeemable + + + +
    + + +
    + +
    Total
    + + + + ) +} + const CashoutPage = () => { const user = useUser() const router = useRouter() - const [page, setPage] = useState< - 'location' | 'get-session' | 'ach-details' | 'waiting' | 'documents' - >('location') + const [page, setPage] = useState('select-cashout-method') const [NameOnAccount, setNameOnAccount] = useState('') const [AccountNumber, setAccountNumber] = useState('') const [RoutingNumber, setRoutingNumber] = useState('') @@ -159,120 +215,97 @@ const CashoutPage = () => { const isLocationBlocked = locationBlocked(user, privateUser) const isAgeBlocked = ageBlocked(user, privateUser) - if (isLocationBlocked) { - return ( - - - - -
    Your location is blocked
    -

    - You are unable to cash out at the moment. -

    - - -
    - ) - } - - if (isAgeBlocked) { - return ( - - - - - 18+ - -
    You must be 18 or older to cash out
    -

    - You are unable to cash out at the moment. -

    - - -
    - ) - } - // redirects to registration page if user if identification failed - if (status !== 'success') { + if (status !== 'success' || isLocationBlocked || isAgeBlocked) { return ( - - {message == USER_NOT_REGISTERED_MESSAGE || - message == PHONE_NOT_VERIFIED_MESSAGE || - message == IDENTIFICATION_FAILED_MESSAGE ? ( - - -
    You're not registered yet...
    -

    - Registration is required to cash out. -

    + + {isLocationBlocked ? ( + + + +
    Your location is blocked!
    +

    + You are unable to cash out at the moment. +

    + +
    + ) : isAgeBlocked ? ( + + + +
    You must be 18+
    +

    + You are unable to cash out at the moment. +

    + +
    + ) : PROMPT_VERIFICATION_MESSAGES.includes(message) ? ( + + + + +
    You're not verified yet...
    +

    + Verification is required to cash out. +

    + +
    - Register and get{' '} - + Verify and get + + + ) : message == USER_BLOCKED_MESSAGE ? ( - - -
    Your registration failed
    -

    - You are unable to cash out at the moment. -

    - + + + +
    Your verification failed
    +

    + You are unable to cash out at the moment. +

    + +
    ) : ( - - -
    Cashout unavailable
    -

    - You are unable to cash out at the moment. -

    - + + + +
    Cashout unavailable
    +

    + You are unable to cash out at the moment. +

    + +
    )} - -
    - ) - } - - if (redeemableCash == 0) { - return ( - - -
    - You don't have any redeemable {SWEEPIES_NAME} -
    - - -
    - Redeemable {SWEEPIES_NAME} -
    - - - -
    Total {SWEEPIES_NAME}
    - - -
    -

    - You can only redeem {SWEEPIES_NAME} that you win trading in a market - that resolves. -

    + {(isLocationBlocked || isAgeBlocked) && ( + + )} +
    ) @@ -281,12 +314,30 @@ const CashoutPage = () => { return ( - - Cash Out + + Redeem {SWEEPIES_NAME} + {!user || page === 'get-session' ? ( - ) : page === 'documents' ? ( + ) : page == 'select-cashout-method' ? ( + <> + + + ) : page == 'custom-mana' ? ( + setPage('select-cashout-method')} + redeemableCash={redeemableCash} + /> + ) : page == 'documents' ? ( setPage('location')} diff --git a/web/pages/charity/[charitySlug].tsx b/web/pages/charity/[charitySlug].tsx index ae04d50548..9127f5ded6 100644 --- a/web/pages/charity/[charitySlug].tsx +++ b/web/pages/charity/[charitySlug].tsx @@ -28,8 +28,11 @@ import { CollapsibleContent } from 'web/components/widgets/collapsible-content' import { PaginationNextPrev } from 'web/components/widgets/pagination' import { CoinNumber } from 'web/components/widgets/coin-number' import { + CASH_TO_CHARITY_DOLLARS, + MIN_CASH_DONATION, MIN_SPICE_DONATION, SPICE_TO_CHARITY_DOLLARS, + TWOMBA_ENABLED, } from 'common/envs/constants' type DonationItem = { user: User; ts: number; amount: number } @@ -121,7 +124,14 @@ function CharityPage(props: { user={user} charity={charity} onDonated={(user, ts, amount) => { - pagination.prepend({ user, ts, amount: amount / 1000 }) + pagination.prepend({ + user, + ts, + amount: + (TWOMBA_ENABLED + ? CASH_TO_CHARITY_DOLLARS + : SPICE_TO_CHARITY_DOLLARS) * amount, + }) setShowConfetti(true) }} /> @@ -170,8 +180,8 @@ function DonationBox(props: { const [isSubmitting, setIsSubmitting] = useState(false) const [error, setError] = useState() - const donateDisabled = - isSubmitting || !amount || !!error || amount < MIN_SPICE_DONATION + const min = TWOMBA_ENABLED ? MIN_CASH_DONATION : MIN_SPICE_DONATION + const donateDisabled = isSubmitting || !amount || !!error || amount < min const onSubmit: React.FormEventHandler = async (e) => { if (!user || donateDisabled) return @@ -197,19 +207,23 @@ function DonationBox(props: { {charity.name} receives - {formatMoneyUSD(SPICE_TO_CHARITY_DOLLARS * (amount || 0))} + {formatMoneyUSD( + (TWOMBA_ENABLED + ? CASH_TO_CHARITY_DOLLARS + : SPICE_TO_CHARITY_DOLLARS) * (amount || 0) + )} @@ -231,7 +245,11 @@ function DonationBox(props: { )}
    - {' '} + {' '} donation minimum
    diff --git a/web/pages/checkout.tsx b/web/pages/checkout.tsx index 47062ab55f..6dae9cb181 100644 --- a/web/pages/checkout.tsx +++ b/web/pages/checkout.tsx @@ -30,6 +30,7 @@ import { LocationPanel } from 'web/components/gidx/location-panel' import { formatMoneyUSD } from 'common/util/format' import { capitalize } from 'lodash' import { useIosPurchases } from 'web/hooks/use-ios-purchases' +import { CashoutLimitWarning } from 'web/components/bet/cashout-limit-warning' const CheckoutPage = () => { const user = useUser() @@ -219,26 +220,28 @@ function FundsSelector(props: { )} > {TWOMBA_ENABLED ? ( - - Buy mana to trade in your favorite questions. Always free to play, - no purchase necessary. - +
    + + Buy mana to trade in your favorite questions. Always free to play, + no purchase necessary. + + +
    ) : ( Buy mana to trade in your favorite questions. )} - {pastLimit && ( You have reached your daily purchase limit. Please try again tomorrow. )} -
    - {prices.map((amounts) => ( + {prices.map((amounts, index) => ( onSelect(amounts.mana)} diff --git a/web/pages/gidx/register.tsx b/web/pages/gidx/register.tsx index 8a0fe3c952..db717cc24c 100644 --- a/web/pages/gidx/register.tsx +++ b/web/pages/gidx/register.tsx @@ -4,6 +4,7 @@ import { LoadingIndicator } from 'web/components/widgets/loading-indicator' import { RegisterUserForm } from 'web/components/gidx/register-user-form' import { TWOMBA_ENABLED } from 'common/envs/constants' +import { Col } from 'web/components/layout/col' const HomePage = () => { const user = useUser() @@ -11,11 +12,13 @@ const HomePage = () => { if (!TWOMBA_ENABLED) return null return ( - {!user || !privateUser ? ( - - ) : ( - - )} + + {!user || !privateUser ? ( + + ) : ( + + )} + ) } diff --git a/web/pages/old-posts/[slug]/index.tsx b/web/pages/post/[slug]/index.tsx similarity index 98% rename from web/pages/old-posts/[slug]/index.tsx rename to web/pages/post/[slug]/index.tsx index ab2e448701..ef30292747 100644 --- a/web/pages/old-posts/[slug]/index.tsx +++ b/web/pages/post/[slug]/index.tsx @@ -67,7 +67,7 @@ export default function PostPage(props: {
    @@ -146,7 +146,7 @@ function RichEditPost(props: { } function postPath(postSlug: string) { - return `/old-posts/${postSlug}` + return `/post/${postSlug}` } async function getPostBySlug(slug: string) { diff --git a/web/pages/referrals.tsx b/web/pages/referrals.tsx index e03c304d02..0febcaeff8 100644 --- a/web/pages/referrals.tsx +++ b/web/pages/referrals.tsx @@ -5,13 +5,12 @@ import { useUser } from 'web/hooks/use-user' import { Page } from 'web/components/layout/page' import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth' import { CopyLinkRow } from 'web/components/buttons/copy-link-button' -import { ENV_CONFIG } from 'common/envs/constants' +import { ENV_CONFIG, TWOMBA_ENABLED } from 'common/envs/constants' import { InfoBox } from 'web/components/widgets/info-box' import { QRCode } from 'web/components/widgets/qr-code' import { REFERRAL_AMOUNT } from 'common/economy' import { formatMoney } from 'common/util/format' import { CoinNumber } from 'web/components/widgets/coin-number' -import { SPICE_COLOR } from 'web/components/portfolio/portfolio-value-graph' import clsx from 'clsx' export const getServerSideProps = redirectIfLoggedOut('/') @@ -45,11 +44,8 @@ export default function ReferralsPage() {
    Invite new users to Manifold and get{' '} {' '} diff --git a/web/pages/stats.tsx b/web/pages/stats.tsx index 09d60fed3e..284cfeb91a 100644 --- a/web/pages/stats.tsx +++ b/web/pages/stats.tsx @@ -20,7 +20,6 @@ import { api } from '../lib/api/api' import { Column, Row as rowfor } from 'common/supabase/utils' import { BonusSummary } from 'web/components/stats/bonus-summary' import { ManaSupplySummary } from 'web/components/stats/mana-summary' -import { Row } from 'web/components/layout/row' import { average } from 'common/util/math' import { useCallback, useState } from 'react' import { Button } from 'web/components/buttons/button' @@ -105,13 +104,13 @@ export function CustomAnalytics(props: { const differenceInCashSinceYesterday = currentSupply.total_cash_value - yesterdaySupply.total_cash_value - const [fromBankSummaryMana, fromBankSummaryCash] = partition( + const [fromBankSummaryCash, fromBankSummaryMana] = partition( fromBankSummary, - (f) => f.token === 'MANA' + (f) => f.token === 'CASH' ) - const [toBankSummaryMana, toBankSummaryCash] = partition( + const [toBankSummaryCash, toBankSummaryMana] = partition( toBankSummary, - (f) => f.token === 'MANA' + (f) => f.token === 'CASH' ) const latestRecordingTime = orderBy(fromBankSummary, 'start_time', 'desc')[0] @@ -214,64 +213,64 @@ export function CustomAnalytics(props: { Mana supply - - Supply Today - -
    Balances
    -
    - {formatMoney(currentSupply.balance)} -
    -
    - -
    Prize point balances
    -
    - ₽{formatWithCommas(currentSupply.spice_balance)} -
    -
    - -
    Cash balances
    -
    - {formatSweepies(currentSupply.cash_balance)} -
    -
    - -
    Investment
    -
    - {formatMoney(currentSupply.investment_value)} -
    -
    - {/* -
    Loans
    -
    - {formatMoney(manaSupply.loanTotal)} -
    -
    */} - -
    AMM liquidity
    -
    - {formatMoney(currentSupply.amm_liquidity)} -
    -
    - -
    Unaccounted for since yesterday
    -
    - mana: {formatMoney(unaccountedDifference)} -
    -
    - cash: {formatSweepies(cashUnaccountedDifference)} -
    -
    - -
    Total
    -
    - {formatMoney(currentSupply.total_value)} -
    -
    - {formatSweepies(currentSupply.total_cash_value)} -
    -
    - - Mana supply over time +
    +
    Supply Today
    +
    Mana
    +
    Prize Cash
    + +
    Balances
    +
    + {formatMoney(currentSupply.balance)} +
    +
    + {formatSweepies(currentSupply.cash_balance)} +
    + +
    Prize point balances
    +
    + ₽{formatWithCommas(currentSupply.spice_balance)} +
    + +
    Investment
    +
    + {formatMoney(currentSupply.investment_value)} +
    +
    + {formatSweepies(currentSupply.cash_investment_value)} +
    + + {/* +
    Loans
    +
    + {formatMoney(manaSupply.loanTotal)} +
    + */} + +
    AMM liquidity
    +
    + {formatMoney(currentSupply.amm_liquidity)} +
    +
    + {formatSweepies(currentSupply.amm_cash_liquidity)} +
    + +
    +
    Unaccounted since yesterday
    +
    + {formatMoney(unaccountedDifference)} +
    +
    + {formatSweepies(cashUnaccountedDifference)} +
    + +
    Total
    +
    + {formatMoney(currentSupply.total_value)} +
    +
    + {formatSweepies(currentSupply.total_cash_value)} +
    +
    Transactions from Manifold @@ -279,7 +278,7 @@ export function CustomAnalytics(props: { Transactions to Manifold - (Ignores mana purchases) + (Ignores mana purchases) diff --git a/web/public/custom-components/documentUploadIcon.tsx b/web/public/custom-components/documentUploadIcon.tsx new file mode 100644 index 0000000000..87bab6c1c5 --- /dev/null +++ b/web/public/custom-components/documentUploadIcon.tsx @@ -0,0 +1,30 @@ +export function DocumentUploadIcon(props: { + height?: number + className?: string +}) { + const { className } = props + + const height = props.height ? props.height * 4 : 48 + const width = height * 1.25 + + return ( + + + + + + ) +} diff --git a/web/public/custom-components/phoneIcon.tsx b/web/public/custom-components/phoneIcon.tsx new file mode 100644 index 0000000000..13ee6ee41c --- /dev/null +++ b/web/public/custom-components/phoneIcon.tsx @@ -0,0 +1,22 @@ +export function PhoneIcon(props: { height?: number; className?: string }) { + const { className } = props + + const height = props.height ? props.height * 4 : 48 + const width = height * 1.25 + + return ( + + + + + + + ) +} diff --git a/web/public/images/cash-icon.png b/web/public/images/cash-icon.png new file mode 100644 index 0000000000..cc89bd9c63 Binary files /dev/null and b/web/public/images/cash-icon.png differ