diff --git a/functions/package.json b/functions/package.json index 79de1b65..83abac53 100644 --- a/functions/package.json +++ b/functions/package.json @@ -18,7 +18,8 @@ "firebase-admin": "10.0.0", "firebase-functions": "3.16.0", "lodash": "4.17.21", - "mailgun-js": "0.22.0" + "mailgun-js": "0.22.0", + "stripe": "8.194.0" }, "devDependencies": { "@types/mailgun-js": "0.22.12", diff --git a/functions/src/index.ts b/functions/src/index.ts index 088ca534..cd2d429a 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -5,5 +5,6 @@ admin.initializeApp() // export * from './keep-awake' export * from './place-bet' export * from './resolve-market' +export * from './stripe' export * from './sell-bet' export * from './create-contract' diff --git a/functions/src/stripe.ts b/functions/src/stripe.ts new file mode 100644 index 00000000..aac128cb --- /dev/null +++ b/functions/src/stripe.ts @@ -0,0 +1,126 @@ +import * as functions from 'firebase-functions' +import * as admin from 'firebase-admin' +import Stripe from 'stripe' + +import { payUser } from './resolve-market' + +const stripe = new Stripe(functions.config().stripe.apikey, { + apiVersion: '2020-08-27', + typescript: true, +}) + +// manage at https://dashboard.stripe.com/test/products?active=true +const manticDollarStripePrice = + admin.instanceId().app.options.projectId === 'mantic-markets' + ? { + 500: 'price_1KFQXcGdoFKoCJW770gTNBrm', + 1000: 'price_1KFQp1GdoFKoCJW7Iu0dsF65', + 2500: 'price_1KFQqNGdoFKoCJW7SDvrSaEB', + 10000: 'price_1KFQraGdoFKoCJW77I4XCwM3', + } + : { + 500: 'price_1K8W10GdoFKoCJW7KWORLec1', + 1000: 'price_1K8bC1GdoFKoCJW76k3g5MJk', + 2500: 'price_1K8bDSGdoFKoCJW7avAwpV0e', + 10000: 'price_1K8bEiGdoFKoCJW7Us4UkRHE', + } + +export const createCheckoutSession = functions + .runWith({ minInstances: 1 }) + .https.onRequest(async (req, res) => { + const userId = req.query.userId?.toString() + + const manticDollarQuantity = req.query.manticDollarQuantity?.toString() + + if (!userId) { + res.status(400).send('Invalid user ID') + return + } + + if ( + !manticDollarQuantity || + !Object.keys(manticDollarStripePrice).includes(manticDollarQuantity) + ) { + res.status(400).send('Invalid Mantic Dollar quantity') + return + } + + const referrer = + req.query.referer || req.headers.referer || 'https://mantic.markets' + + const session = await stripe.checkout.sessions.create({ + metadata: { + userId, + manticDollarQuantity, + }, + line_items: [ + { + price: + manticDollarStripePrice[ + manticDollarQuantity as unknown as keyof typeof manticDollarStripePrice + ], + quantity: 1, + }, + ], + mode: 'payment', + success_url: `${referrer}?funding-success`, + cancel_url: `${referrer}?funding-failiure`, + }) + + res.redirect(303, session.url || '') + }) + +export const stripeWebhook = functions + .runWith({ minInstances: 1 }) + .https.onRequest(async (req, res) => { + let event + + try { + event = stripe.webhooks.constructEvent( + req.rawBody, + req.headers['stripe-signature'] as string, + functions.config().stripe.webhooksecret + ) + } catch (e: any) { + console.log(`Webhook Error: ${e.message}`) + res.status(400).send(`Webhook Error: ${e.message}`) + return + } + + if (event.type === 'checkout.session.completed') { + const session = event.data.object as any + await issueMoneys(session) + } + + res.status(200).send('success') + }) + +const issueMoneys = async (session: any) => { + const { id: sessionId } = session + + const query = await firestore + .collection('stripe-transactions') + .where('sessionId', '==', sessionId) + .get() + + if (!query.empty) { + console.log('session', sessionId, 'already processed') + return + } + + const { userId, manticDollarQuantity } = session.metadata + const payout = Number.parseInt(manticDollarQuantity) + + await firestore.collection('stripe-transactions').add({ + userId, + manticDollarQuantity: payout, // save as number + sessionId, + session, + }) + + await payUser([userId, payout]) + + console.log('user', userId, 'paid M$', payout) +} + +const firestore = admin.firestore() diff --git a/functions/yarn.lock b/functions/yarn.lock index 6cec7f41..5a958ea1 100644 --- a/functions/yarn.lock +++ b/functions/yarn.lock @@ -314,7 +314,7 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== -"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0": +"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@>=8.1.0": version "17.0.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.1.tgz#88d501e84b6185f6489ecee4ba9e8fcec7f29bb2" integrity sha512-NXKvBVUzIbs6ylBwmOwHFkZS2EXCcjnqr8ZCRNaXBkHAf+3mn/rPcJxwrzuc6movh8fxQAsUUfYklJ/EG+hZqQ== @@ -464,6 +464,14 @@ bytes@3.1.1: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.1.tgz#3f018291cb4cbad9accb6e6970bca9c8889e879a" integrity sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg== +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -895,6 +903,11 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + ftp@~0.3.10: version "0.3.10" resolved "https://registry.yarnpkg.com/ftp/-/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d" @@ -946,6 +959,15 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-intrinsic@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" + integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + get-stream@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" @@ -1018,6 +1040,18 @@ gtoken@^5.0.4: google-p12-pem "^3.0.3" jws "^4.0.0" +has-symbols@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" + integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + hash-stream-validation@^0.2.2: version "0.2.4" resolved "https://registry.yarnpkg.com/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz#ee68b41bf822f7f44db1142ec28ba9ee7ccb7512" @@ -1428,6 +1462,11 @@ object-hash@^2.1.1: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== +object-inspect@^1.9.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" + integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -1605,6 +1644,13 @@ qs@6.9.6: resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee" integrity sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ== +qs@^6.6.0: + version "6.10.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.2.tgz#c1431bea37fc5b24c5bdbafa20f16bdf2a4b9ffe" + integrity sha512-mSIdjzqznWgfd4pMii7sHtaYF8rx8861hBO80SraY5GT0XQibWZWJSid0avzHGkDIZLImux2S5mXO0Hfct2QCw== + dependencies: + side-channel "^1.0.4" + range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -1729,6 +1775,15 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + signal-exit@^3.0.2: version "3.0.6" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af" @@ -1822,6 +1877,14 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" +stripe@8.194.0: + version "8.194.0" + resolved "https://registry.yarnpkg.com/stripe/-/stripe-8.194.0.tgz#67fc7a34260f95f9103834a1f0962d27c608cf73" + integrity sha512-iERByJUNA7sdkfQ3fD1jcrAZqPxCtTmL2EUzvHUVLXyoacDrflkq4ux5KFxYhfCIerrOAhquVj17+sBHn96/Kg== + dependencies: + "@types/node" ">=8.1.0" + qs "^6.6.0" + stubs@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" diff --git a/web/components/add-funds-button.tsx b/web/components/add-funds-button.tsx new file mode 100644 index 00000000..3c70604e --- /dev/null +++ b/web/components/add-funds-button.tsx @@ -0,0 +1,76 @@ +import clsx from 'clsx' +import { useState } from 'react' + +import { useUser } from '../hooks/use-user' +import { checkoutURL } from '../lib/service/stripe' +import { FundsSelector } from './yes-no-selector' + +export function AddFundsButton(props: { className?: string }) { + const { className } = props + const user = useUser() + + const [amountSelected, setAmountSelected] = useState< + 500 | 1000 | 2500 | 10000 + >(500) + + return ( + <> + + + +