diff --git a/app/api/.env.example b/app/api/.env.example index 1337c73a7..ff179dcb5 100644 --- a/app/api/.env.example +++ b/app/api/.env.example @@ -33,6 +33,6 @@ SENDGRID_MAIL_FROM_EMAIL=mail_from_email_set_in_sendgrid WEB_CLIENT_BASE_URL=https://pocre.netlify.app # the base url of web app # pinata config (for ipfs storage) -PINATA_API_JWT_SECRET=api_key_jwt_secret_provided_by_pinata +PINATA_JWT_SECRET=api_key_jwt_secret_provided_by_pinata PINATA_API_JSON_PIN_URL=pinata_api_url_to_pin_json PINATA_API_UNPIN_URL=pinata_api_url_to_unpin_data \ No newline at end of file diff --git a/app/api/src/controllers/creation.controller.ts b/app/api/src/controllers/creation.controller.ts index d0f6a6c76..7220bf280 100644 --- a/app/api/src/controllers/creation.controller.ts +++ b/app/api/src/controllers/creation.controller.ts @@ -219,3 +219,22 @@ export const updateCreationById = catchAsync(async (req, res): Promise => res.send(updatedCreation); }); + +export const publishCreationOnchain = catchAsync(async (req, res): Promise => { + // get original creation + const foundCreation = await creationService.getCreationById(req.params.creation_id); + + // block publishing on chain if creation is draft + if (foundCreation?.is_draft) { + throw new ApiError(httpStatus.NOT_ACCEPTABLE, `draft creation cannot be published onchain`); + } + + // update creation + const updatedCreation = await creationService.updateCreationById( + req.params.creation_id, + { is_onchain: true }, + { owner_id: (req.user as IUserDoc).user_id } + ); + + res.send(updatedCreation); +}); diff --git a/app/api/src/db/pool.ts b/app/api/src/db/pool.ts index 696aedcd4..6c7d933a7 100644 --- a/app/api/src/db/pool.ts +++ b/app/api/src/db/pool.ts @@ -128,6 +128,7 @@ const init = async (): Promise> => { is_claimable bool default true, ipfs_hash character varying, created_at TIMESTAMP NOT NULL DEFAULT NOW(), + is_onchain bool default false, CONSTRAINT author_id FOREIGN KEY(author_id) REFERENCES users(user_id) diff --git a/app/api/src/docs/components.yml b/app/api/src/docs/components.yml index 9ba5caee7..f954ab7b1 100644 --- a/app/api/src/docs/components.yml +++ b/app/api/src/docs/components.yml @@ -175,6 +175,8 @@ components: type: bool is_claimable: type: bool + is_onchain: + type: bool created_at: type: string format: date-time @@ -191,6 +193,7 @@ components: created_at: 2022-09-05T19:00:00.000Z is_draft: false is_claimable: true + is_onchain: false CreationProof: type: object @@ -584,7 +587,16 @@ components: $ref: '#/components/schemas/Error' example: code: 406 - message: creation has ongoing litigation process + message: creation has ongoing litigation process + CreationDraftNotAllowedOnchain: + description: Draft creation cannot be published onchain + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + code: 406 + message: draft creation cannot be published onchain CreationAlreadyAssignedToLitigation: description: Creation already assigned to a litigation content: diff --git a/app/api/src/routes/v1/creation.route.ts b/app/api/src/routes/v1/creation.route.ts index 5e3a53b0f..c359aae63 100644 --- a/app/api/src/routes/v1/creation.route.ts +++ b/app/api/src/routes/v1/creation.route.ts @@ -1,8 +1,8 @@ import express from 'express'; -import validate from '../../middlewares/validate'; -import * as creationValidation from '../../validations/creation.validation'; import * as creationController from '../../controllers/creation.controller'; import auth from '../../middlewares/auth'; +import validate from '../../middlewares/validate'; +import * as creationValidation from '../../validations/creation.validation'; const router = express.Router(); @@ -17,6 +17,10 @@ router .patch(auth(), validate(creationValidation.updateCreation), creationController.updateCreationById) .delete(auth(), validate(creationValidation.deleteCreation), creationController.deleteCreationById); +router + .route('/:creation_id/onchain') + .post(auth(), validate(creationValidation.publishCreationOnchain), creationController.publishCreationOnchain); + router .route('/:creation_id/proof') .get(validate(creationValidation.getCreationProof), creationController.getCreationProofById); @@ -465,3 +469,27 @@ export default router; * "500": * $ref: '#/components/responses/InternalServerError' */ + +/** + * @swagger + * /creations/{creation_id}/onchain: + * post: + * summary: Publish creation on chain + * description: Stores the publish status of a creation on chain. + * tags: [Creation] + * security: + * - bearerAuth: [] + * responses: + * "201": + * description: Created + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Creation' + * "404": + * $ref: '#/components/responses/CreationNotFound' + * "406": + * $ref: '#/components/responses/CreationDraftNotAllowedOnchain' + * "500": + * $ref: '#/components/responses/InternalServerError' + */ diff --git a/app/api/src/services/creation.service.ts b/app/api/src/services/creation.service.ts index b044b7c07..cf15979bd 100644 --- a/app/api/src/services/creation.service.ts +++ b/app/api/src/services/creation.service.ts @@ -21,6 +21,7 @@ interface ICreation { is_draft: boolean; is_claimable: boolean; ipfs_hash: string; + is_onchain: boolean; } interface ICreationQuery { limit: number; @@ -57,6 +58,7 @@ interface ICreationDoc { is_draft: boolean; is_claimable: boolean; ipfs_hash: string; + is_onchain: boolean; } /** @@ -226,7 +228,9 @@ export const queryCreations = async (options: ICreationQuery): Promise { + const wasmExtensionRegExp = /\.wasm$/; + config.resolve.extensions.push('.wasm'); + config.resolve.fallback = { + ...config.resolve.fallback, + buffer: require.resolve('buffer'), + stream: require.resolve('stream'), + }; + config.plugins = [ + ...config.plugins, + new webpack.ProvidePlugin({ + Buffer: ['buffer', 'Buffer'], + }), + ]; + config.experiments = { + syncWebAssembly: true, + asyncWebAssembly: true, + }; + + for (const rule of config.module.rules) { + for (const oneOf of (rule.oneOf || [])) { + if (oneOf.type === 'asset/resource') { + oneOf.exclude.push(wasmExtensionRegExp); + } + } + } + + return config; + }, + }, +}; diff --git a/app/web-frontend/package.json b/app/web-frontend/package.json index a6e21aed3..a20945eab 100644 --- a/app/web-frontend/package.json +++ b/app/web-frontend/package.json @@ -3,25 +3,23 @@ "version": "0.1.0", "private": true, "dependencies": { + "@craco/craco": "^7.0.0", "@emotion/react": "^11.9.3", "@emotion/styled": "^11.9.3", - "@emurgo/cardano-serialization-lib-asmjs": "^10.2.0", "@hookform/resolvers": "^2.9.8", + "@meshsdk/core": "^1.3.0", + "@meshsdk/react": "^1.1.3", "@mui/icons-material": "^5.8.4", "@mui/material": "^5.9.0", "@mui/styles": "^5.9.0", "@mui/system": "^5.10.8", - "@popperjs/core": "^2.11.6", - "@svgr/core": "^6.4.0", "@tanstack/react-query": "^4.14.1", - "@types/popper.js": "^1.11.0", "blakejs": "^1.1.1", "buffer": "^6.0.3", "joi": "^17.6.0", "js-cookie": "^3.0.1", "materialize-css": "^1.0.0-rc.2", "moment": "^2.29.4", - "popper.js": "^1.16.1", "qrcode": "^1.5.1", "react": "^17.0.2", "react-copy-to-clipboard": "^5.1.0", @@ -35,11 +33,10 @@ "react-slick": "^0.29.0", "react-transition-group": "^4.4.5", "slick-carousel": "^1.8.1", - "web-vitals": "^2.1.4", + "stream": "^0.0.2", "zustand": "^4.1.1" }, "devDependencies": { - "@svgr/webpack": "^6.4.0", "eslint": "8.11.0", "eslint-config-airbnb": "^19.0.4", "eslint-plugin-import": "^2.25.3", @@ -54,10 +51,10 @@ "lint-staged": "^12.3.7" }, "scripts": { - "start": "react-scripts --max-old-space-size=8192 start", - "build": "react-scripts --max-old-space-size=8192 build", - "test": "set CI=true&& react-scripts --passWithNoTests test", - "eject": "react-scripts eject", + "start": "craco start", + "build": "craco build", + "test": "set CI=true&& craco --passWithNoTests test", + "eject": "craco eject", "lint": "eslint ./src --ext .js", "lint:fix": "eslint ./src --ext .js --fix", "prepare": "cd .. && cd .. && husky install app/web-frontend/.husky" diff --git a/app/web-frontend/src/api/requests.js b/app/web-frontend/src/api/requests.js index 9dd3f4d16..d16eb2b2d 100644 --- a/app/web-frontend/src/api/requests.js +++ b/app/web-frontend/src/api/requests.js @@ -37,7 +37,7 @@ const REQUEST_TEMPLATE = (endpoint) => ({ const User = { ...REQUEST_TEMPLATE('users'), invite: REQUEST_TEMPLATE('users/invite').create }; const Material = REQUEST_TEMPLATE('materials'); -const Creation = REQUEST_TEMPLATE('creations'); +const Creation = { ...REQUEST_TEMPLATE('creations'), storePublishStatus: async (id) => await REQUEST_TEMPLATE(`creations/${id}/onchain`).create() }; const Decision = REQUEST_TEMPLATE('decision'); const Recognition = REQUEST_TEMPLATE('recognitions'); const Litigation = REQUEST_TEMPLATE('litigations'); diff --git a/app/web-frontend/src/components/LoginForm/useLogin.jsx b/app/web-frontend/src/components/LoginForm/useLogin.jsx index 95d344b6a..1fdf0c89c 100644 --- a/app/web-frontend/src/components/LoginForm/useLogin.jsx +++ b/app/web-frontend/src/components/LoginForm/useLogin.jsx @@ -11,7 +11,7 @@ const useLogin = ({ inviteToken = null }) => { const [selectedWalletAddressHashed, setSelectedWalletAddressHashed] = useState(null); const [availableWallets, setAvailableWallets] = useState([]); - const getWalletsList = () => setAvailableWallets(getAvailableWallets()); + const getWalletsList = async () => setAvailableWallets(await getAvailableWallets()); const getSelectedWalletAddress = async (wallet) => { try { @@ -41,7 +41,7 @@ const useLogin = ({ inviteToken = null }) => { isSuccess: loginSuccess, isLoading: isLoggingIn, } = useMutation({ - mutationFn: async () => { + mutationFn: async (selectedWallet) => { // login with wallet const response = inviteToken ? await Auth.signup({ @@ -49,7 +49,13 @@ const useLogin = ({ inviteToken = null }) => { wallet_address: selectedWalletAddressOriginal, }) : await Auth.login({ wallet_address: selectedWalletAddressOriginal }); - authUser.setUser({ ...response.user, hashedWalletAddress: selectedWalletAddressHashed }); + + // store cookies + authUser.setUser({ + ...response.user, + selectedWallet: selectedWallet.wallet, + hashedWalletAddress: selectedWalletAddressHashed, + }); authUser.setJWTToken(response.token); }, }); diff --git a/app/web-frontend/src/components/cards/CreationCard/index.jsx b/app/web-frontend/src/components/cards/CreationCard/index.jsx index d1ee0741f..f6a33f91e 100644 --- a/app/web-frontend/src/components/cards/CreationCard/index.jsx +++ b/app/web-frontend/src/components/cards/CreationCard/index.jsx @@ -65,6 +65,9 @@ function CreationCard({ canDelete = true, onEditClick = () => {}, onDeleteClick = () => {}, + canPublish = false, + isPublished = false, + onPublish = () => {}, }) { const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(null); const [showMediaPreview, setShowMediaPreview] = useState(null); @@ -247,6 +250,29 @@ function CreationCard({ + {canPublish && ( + + {isPublished ? ( + + ) : ( + + )} + + )} {interactionBtns && ( diff --git a/app/web-frontend/src/config.js b/app/web-frontend/src/config.js index 66fed8aaa..2663c7fbe 100644 --- a/app/web-frontend/src/config.js +++ b/app/web-frontend/src/config.js @@ -1,5 +1,21 @@ const API_BASE_URL = 'https://pocre-api.herokuapp.com/v1/'; +const POCRE_WALLET_ADDRESS = 'addr_test1qr0nvz3xurstmkj3h3a32knxsgpzvz4g8z3lvhhya9ffzh74uhu2hd3kjx8v9p906g4sejyj3w7q76zqwsgt4w9drfnsp8jhz7'; // preview testnet address || IMPORTANT: dont make real transactions + +const CHARGES = { + CREATION_PUBLISHING_ADA: 9, +}; + +const TRANSACTION_PURPOSES = { + CREATION_PUBLISHING: 'Creation Publishing', +}; + +const IPFS_BASE_URL = 'https://gateway.pinata.cloud/ipfs/'; + export { API_BASE_URL, + POCRE_WALLET_ADDRESS, + CHARGES, + IPFS_BASE_URL, + TRANSACTION_PURPOSES, }; diff --git a/app/web-frontend/src/index.jsx b/app/web-frontend/src/index.jsx index a16e1566b..5262d7953 100644 --- a/app/web-frontend/src/index.jsx +++ b/app/web-frontend/src/index.jsx @@ -1,6 +1,6 @@ +import { MeshProvider } from '@meshsdk/react'; import { createTheme, ThemeProvider } from '@mui/material/styles'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; import './styles/index.css'; @@ -20,9 +20,11 @@ const queryClient = new QueryClient(); ReactDOM.render( - - - + + + + + , document.getElementById('root'), ); diff --git a/app/web-frontend/src/pages/Creations/Details/index.jsx b/app/web-frontend/src/pages/Creations/Details/index.jsx index e665c3c4a..eacf81099 100644 --- a/app/web-frontend/src/pages/Creations/Details/index.jsx +++ b/app/web-frontend/src/pages/Creations/Details/index.jsx @@ -20,6 +20,7 @@ import { useEffect, useState } from 'react'; import { Link, useNavigate, useParams } from 'react-router-dom'; import authUser from 'utils/helpers/authUser'; import useCreationDelete from '../common/hooks/useCreationDelete'; +import useCreationPublish from '../common/hooks/useCreationPublish'; import './index.css'; import useDetails from './useDetails'; @@ -53,6 +54,13 @@ export default function CreationDetails() { resetDeletionErrors, } = useCreationDelete(); + const { + isPublishingCreation, + publishCreationStatus, + publishCreation, + resetPublishErrors, + } = useCreationPublish(); + const generateQRCodeBase64 = async (text) => { const code = await QRCode.toDataURL(`${window.location.origin}/creations/${text}`, { width: 150, @@ -102,7 +110,7 @@ export default function CreationDetails() { return ( - {isDeletingCreation && } + {(isDeletingCreation || isPublishingCreation) && } {(deleteCreationStatus.success || deleteCreationStatus.error) && ( )} + {(publishCreationStatus.success || publishCreationStatus.error) && ( + + + {publishCreationStatus.success || publishCreationStatus.error} + + + )} {showShareOptions && ( setShowShareOptions(false)} @@ -172,6 +191,21 @@ export default function CreationDetails() { marginTop="18px" width="100%" > + {user?.user_id === creation?.author?.user_id + && !creation?.is_draft + && !creation?.is_onchain + && ( + + )} + {user?.user_id === creation?.author?.user_id && creation?.is_draft && (