diff --git a/.env.sample b/.env.sample index 01c40bd..fb330e2 100644 --- a/.env.sample +++ b/.env.sample @@ -1 +1,5 @@ -INFURA_KEY= \ No newline at end of file +INFURA_KEY= +STRIPE_SECRET_KEY=sk_ +STRIPE_PUBLISHABLE_KEY=pk_ +STRIPE_WEBHOOK_SECRET=whsec_ +STATIC_DIR="https://genesis.re/" \ No newline at end of file diff --git a/contracts/min-Genesis.js b/contracts/min-Genesis.js new file mode 100644 index 0000000..e6be5c0 --- /dev/null +++ b/contracts/min-Genesis.js @@ -0,0 +1,13 @@ +const GENESIS_ADDRESS = "0x3ef61d25b2bf303de52efdd5e50698bed8f9eb8d"; +const GENESIS_ABI = [ + { + //@ts-ignore + inputs: [], + name: "currentPrice", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, +]; + +module.exports = { GENESIS_ADDRESS, GENESIS_ABI }; diff --git a/controllers/stripeController.js b/controllers/stripeController.js new file mode 100644 index 0000000..05ac974 --- /dev/null +++ b/controllers/stripeController.js @@ -0,0 +1,155 @@ +const { default: Stripe } = require("stripe"); +const { GENESIS_ABI, GENESIS_ADDRESS } = require("../contracts/min-Genesis"); +const express = require("express"); +const { ethers } = require("ethers"); +const bodyParser = require("body-parser"); +const env = require("dotenv"); +env.config({ path: "./.env" }); +const router = express.Router(); +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { + apiVersion: "2023-10-16", + appInfo: { + // For sample support and debugging, not required for production: + name: "Genesis RE", + url: "https://genesis.re/", + version: "1.1.0", + }, +}); + +router.get("/config", (_, res) => { + // Serve checkout page. + res.send({ + publishableKey: process.env.STRIPE_PUBLISHABLE_KEY, + }); +}); + +async function convertEthToEur(ethPrice) { + try { + const response = await fetch( + "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=eur" + ); + const data = await response.json(); + + const eurPrice = ethPrice * data.ethereum.eur; + return eurPrice; + } catch (err) { + console.error(err); + return null; + } +} +async function getCurrentPrice() { + try { + const network = "homestead"; // MAINNET + + const provider = new ethers.providers.InfuraProvider( + network, + process.env.INFURA_KEY + ); + let GENESIS = new ethers.Contract(GENESIS_ADDRESS, GENESIS_ABI, provider); + let currentPrice = ethers.utils.formatEther(await GENESIS.currentPrice()); + + const currentPriceEur = await convertEthToEur(Number(currentPrice)); + if (currentPriceEur !== null) { + console.log( + "Current price: " + currentPrice + " ETH or " + currentPriceEur + " EUR" + ); + return currentPriceEur; + } else { + console.log("Unable to fetch EUR price"); + } + } catch (err) { + console.log(err); + } +} +router.get("/create-payment-intent", async (req, res) => { + const price = await getCurrentPrice(); + // Create a PaymentIntent with the order amount and currency. + const params = { + amount: Math.round(Number(price * 100)), + currency: "EUR", + automatic_payment_methods: { + enabled: true, + }, + }; + try { + const paymentIntent = await stripe.paymentIntents.create(params); + + // Send publishable key and PaymentIntent client_secret to client. + res.send({ + clientSecret: paymentIntent.client_secret, + amount: Math.round(Number(price)), + }); + } catch (e) { + res.status(400).send({ + error: { + message: e.message, + }, + }); + } +}); +router.get("/create-test-intent", async (req, res) => { + // Create a PaymentIntent with the order amount and currency. + const params = { + amount: 100, + currency: "EUR", + automatic_payment_methods: { + enabled: true, + }, + }; + try { + const paymentIntent = await stripe.paymentIntents.create(params); + + // Send publishable key and PaymentIntent client_secret to client. + res.send({ + clientSecret: paymentIntent.client_secret, + amount: 1, + }); + } catch (e) { + res.status(400).send({ + error: { + message: e.message, + }, + }); + } +}); +router.post( + "/webhook", + // Use body-parser to retrieve the raw body as a buffer. + // @ts-ignore + bodyParser.raw({ type: "application/json" }), + async (req, res) => { + // Retrieve the event by verifying the signature using the raw body and secret. + let event; + + try { + event = stripe.webhooks.constructEvent( + req.body, + req.headers["stripe-signature"], + process.env.STRIPE_WEBHOOK_SECRET + ); + } catch (err) { + console.log(`⚠️ Webhook signature verification failed.`); + res.sendStatus(400); + return; + } + + // Extract the data from the event. + const data = event.data; + const eventType = event.type; + + if (eventType === "payment_intent.succeeded") { + // Cast the event into a PaymentIntent to make use of the types. + const pi = data.object; + // Funds have been captured + // Fulfill any orders, e-mail receipts, etc + // To cancel the payment after capture you will need to issue a Refund (https://stripe.com/docs/api/refunds). + console.log("💰 Payment captured!"); + } else if (eventType === "payment_intent.payment_failed") { + // Cast the event into a PaymentIntent to make use of the types. + const pi = data.object; + console.log("❌ Payment failed."); + } + res.sendStatus(200); + } +); +module.exports = router; diff --git a/index.js b/index.js index 2d0f047..8071657 100644 --- a/index.js +++ b/index.js @@ -18,6 +18,7 @@ const ADDRESS_staging = require("./contracts/Address-staging"); const ABI_prod = require("./contracts/ABI-prod"); const ABI_staging = require("./contracts/ABI-staging"); +const stripeController = require("./controllers/stripeController"); const evaluationController = require("./controllers/evaluationController"); // Some quirky issue: https://github.com/ethers-io/ethers.js/discussions/4387 (hard to tell why it is required, a dedicated format for events) @@ -434,6 +435,8 @@ function _saveOrganisationToDB( app.use("/", evaluationController); +app.use("/", stripeController); + app.get("/reports", async (req, res) => { const reportItems = await grabReports(); res.json(reportItems); diff --git a/package.json b/package.json index 1443c26..c6e3e89 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,10 @@ "dotenv": "^16.3.1", "ethers": "^5.6", "express": "^4.18.2", - "sqlite3": "^5.1.6" + "sqlite3": "^5.1.6", + "stripe": "^14.18.0" }, "devDependencies": { "nodemon": "^3.0.1" } -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 6b9f070..42b9b03 100644 --- a/yarn.lock +++ b/yarn.lock @@ -518,6 +518,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.33.tgz#8c29a0036771569662e4635790ffa9e057db379b" integrity sha512-qelS/Ra6sacc4loe/3MSjXNL1dNQ/GjxNHVzuChwMfmk7HuycRLVQN2qNY3XahK+fZc5E2szqQSKUyAF0E+2bg== +"@types/node@>=8.1.0": + version "20.11.20" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.20.tgz#f0a2aee575215149a62784210ad88b3a34843659" + integrity sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg== + dependencies: + undici-types "~5.26.4" + "@vercel/build-utils@7.4.1": version "7.4.1" resolved "https://registry.yarnpkg.com/@vercel/build-utils/-/build-utils-7.4.1.tgz#5274e79e6ec64606cf262bb43108e028212825cb" @@ -2150,6 +2157,13 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" +qs@^6.11.0: + version "6.11.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" + integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== + dependencies: + side-channel "^1.0.4" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -2420,6 +2434,14 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== +stripe@^14.18.0: + version "14.18.0" + resolved "https://registry.yarnpkg.com/stripe/-/stripe-14.18.0.tgz#71f6d192c322418bd0962897006b0f47e232f94c" + integrity sha512-yLqKPqYgGJbMxrQiE4+i2i00ZVA2NRIZbZ1rejzj5XR3F3Uc+1iy9QE133knZudhVGMw367b8vTpB8D9pYMETw== + dependencies: + "@types/node" ">=8.1.0" + qs "^6.11.0" + supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -2548,6 +2570,11 @@ undefsafe@^2.0.5: resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + undici@5.26.5: version "5.26.5" resolved "https://registry.yarnpkg.com/undici/-/undici-5.26.5.tgz#f6dc8c565e3cad8c4475b187f51a13e505092838"