From e6185fb89fe7d8d6a586d149fea689c7e89e3b28 Mon Sep 17 00:00:00 2001 From: Ladd Hoffman Date: Wed, 20 Mar 2024 20:00:56 -0500 Subject: [PATCH] client now verifies hash and signature on api read --- backend/index.js | 8 +- backend/package-lock.json | 11 +- backend/package.json | 3 +- client/package-lock.json | 179 ++++++++++++++++++ client/package.json | 3 + client/src/App.jsx | 15 +- .../components/{ => posts}/AddPostModal.jsx | 34 ++-- .../components/{ => posts}/ViewPostModal.jsx | 0 .../AvailabilityStakes.jsx | 4 +- .../{ => work-contracts}/WorkContract.jsx | 4 +- .../{ => work-contracts}/WorkRequests.jsx | 24 +-- client/src/contract-addresses.json | 6 +- client/src/utils/Post.js | 75 ++++++++ ethereum/.env.example | 5 +- ethereum/contract-addresses.json | 6 +- ethereum/scripts/deploy.js | 8 +- 16 files changed, 322 insertions(+), 63 deletions(-) rename client/src/components/{ => posts}/AddPostModal.jsx (71%) rename client/src/components/{ => posts}/ViewPostModal.jsx (100%) rename client/src/components/{ => work-contracts}/AvailabilityStakes.jsx (97%) rename client/src/components/{ => work-contracts}/WorkContract.jsx (93%) rename client/src/components/{ => work-contracts}/WorkRequests.jsx (91%) create mode 100644 client/src/utils/Post.js diff --git a/backend/index.js b/backend/index.js index dc9afc7..978289b 100644 --- a/backend/index.js +++ b/backend/index.js @@ -3,9 +3,8 @@ const { Level } = require('level'); const { recoverPersonalSignature } = require('@metamask/eth-sig-util'); // const { ecrecover, fromRpcSig, pubToAddress } = require('@ethereumjs/util'); // const { Keccak } = require('sha3'); -const { - createHash, -} = require('node:crypto'); +const objectHash = require('object-hash'); +// const { createHash } = require('node:crypto'); require('dotenv').config(); @@ -36,7 +35,8 @@ app.post('/write', async (req, res) => { } // Compute content hash const data = { author, content, signature }; - const hash = createHash('sha256').update(JSON.stringify(data)).digest('base64url'); + const hash = objectHash(data); + console.log('write', hash); // Store content db.put(hash, data); diff --git a/backend/package-lock.json b/backend/package-lock.json index 5974a01..603d6b2 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,7 +13,8 @@ "axios": "^1.6.7", "dotenv": "^16.4.5", "express": "^4.18.2", - "level": "^8.0.1" + "level": "^8.0.1", + "object-hash": "^3.0.0" }, "devDependencies": { "eslint": "^8.56.0", @@ -2933,6 +2934,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", diff --git a/backend/package.json b/backend/package.json index 8e7dc6d..ac883da 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,7 +13,8 @@ "axios": "^1.6.7", "dotenv": "^16.4.5", "express": "^4.18.2", - "level": "^8.0.1" + "level": "^8.0.1", + "object-hash": "^3.0.0" }, "devDependencies": { "eslint": "^8.56.0", diff --git a/client/package-lock.json b/client/package-lock.json index 8a8fea4..a4aae3a 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@helia/dag-json": "^3.0.2", "@libp2p/websockets": "^8.0.16", + "@metamask/eth-sig-util": "^7.0.1", "@metamask/sdk-react": "^0.16.0", "@multiformats/multiaddr": "^12.2.1", "@tanstack/react-table": "^8.13.2", @@ -17,8 +18,10 @@ "bootstrap": "^5.3.3", "bootswatch": "^5.3.3", "buffer": "^6.0.3", + "create-hash": "^1.2.0", "helia": "^4.1.0", "ipfs-core": "^0.18.1", + "object-hash": "^3.0.0", "prop-types": "^15.8.1", "react": "^18.2.0", "react-bootstrap": "^2.10.1", @@ -5325,6 +5328,156 @@ "uint8arrays": "^5.0.2" } }, + "node_modules/@metamask/abi-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@metamask/abi-utils/-/abi-utils-2.0.2.tgz", + "integrity": "sha512-B/A1dY/w4F/t6cDHUscklO6ovb/ztFsrsTXFd8QlqSByk/vyy+QbPE3VVpmmyI/7RX+PA1AJcvBdzCIz+r9dVQ==", + "dependencies": { + "@metamask/utils": "^8.0.0", + "superstruct": "^1.0.3" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@metamask/abi-utils/node_modules/@metamask/utils": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-8.4.0.tgz", + "integrity": "sha512-dbIc3C7alOe0agCuBHM1h71UaEaEqOk2W8rAtEn8QGz4haH2Qq7MoK6i7v2guzvkJVVh79c+QCzIqphC3KvrJg==", + "dependencies": { + "@ethereumjs/tx": "^4.2.0", + "@noble/hashes": "^1.3.1", + "@scure/base": "^1.1.3", + "@types/debug": "^4.1.7", + "debug": "^4.3.4", + "pony-cause": "^2.1.10", + "semver": "^7.5.4", + "superstruct": "^1.0.3", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@metamask/abi-utils/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@metamask/abi-utils/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@metamask/abi-utils/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@metamask/abi-utils/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/@metamask/eth-sig-util": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@metamask/eth-sig-util/-/eth-sig-util-7.0.1.tgz", + "integrity": "sha512-59GSrMyFH2fPfu7nKeIQdZ150zxXNNhAQIUaFRUW+MGtVA4w/ONbiQobcRBLi+jQProfIyss51G8pfLPcQ0ylg==", + "dependencies": { + "@ethereumjs/util": "^8.1.0", + "@metamask/abi-utils": "^2.0.2", + "@metamask/utils": "^8.1.0", + "ethereum-cryptography": "^2.1.2", + "tweetnacl": "^1.0.3", + "tweetnacl-util": "^0.15.1" + }, + "engines": { + "node": "^16.20 || ^18.16 || >=20" + } + }, + "node_modules/@metamask/eth-sig-util/node_modules/@metamask/utils": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-8.4.0.tgz", + "integrity": "sha512-dbIc3C7alOe0agCuBHM1h71UaEaEqOk2W8rAtEn8QGz4haH2Qq7MoK6i7v2guzvkJVVh79c+QCzIqphC3KvrJg==", + "dependencies": { + "@ethereumjs/tx": "^4.2.0", + "@noble/hashes": "^1.3.1", + "@scure/base": "^1.1.3", + "@types/debug": "^4.1.7", + "debug": "^4.3.4", + "pony-cause": "^2.1.10", + "semver": "^7.5.4", + "superstruct": "^1.0.3", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@metamask/eth-sig-util/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@metamask/eth-sig-util/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@metamask/eth-sig-util/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@metamask/eth-sig-util/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/@metamask/object-multiplex": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@metamask/object-multiplex/-/object-multiplex-1.3.0.tgz", @@ -20180,6 +20333,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -20895,6 +21056,14 @@ "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==" }, + "node_modules/pony-cause": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-2.1.10.tgz", + "integrity": "sha512-3IKLNXclQgkU++2fSi93sQ6BznFuxSLB11HdvZQ6JW/spahf/P1pAHBQEahr20rs0htZW0UDkM1HmA+nZkXKsw==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -23275,6 +23444,16 @@ "node": "*" } }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, + "node_modules/tweetnacl-util": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", + "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/client/package.json b/client/package.json index f634be0..5d9ee0d 100644 --- a/client/package.json +++ b/client/package.json @@ -12,6 +12,7 @@ "dependencies": { "@helia/dag-json": "^3.0.2", "@libp2p/websockets": "^8.0.16", + "@metamask/eth-sig-util": "^7.0.1", "@metamask/sdk-react": "^0.16.0", "@multiformats/multiaddr": "^12.2.1", "@tanstack/react-table": "^8.13.2", @@ -19,8 +20,10 @@ "bootstrap": "^5.3.3", "bootswatch": "^5.3.3", "buffer": "^6.0.3", + "create-hash": "^1.2.0", "helia": "^4.1.0", "ipfs-core": "^0.18.1", + "object-hash": "^3.0.0", "prop-types": "^15.8.1", "react": "^18.2.0", "react-bootstrap": "^2.10.1", diff --git a/client/src/App.jsx b/client/src/App.jsx index 78fd946..4170ccf 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -3,7 +3,6 @@ import { } from 'react'; import { useSDK } from '@metamask/sdk-react'; import { Web3 } from 'web3'; -import axios from 'axios'; import Button from 'react-bootstrap/Button'; import Tab from 'react-bootstrap/Tab'; @@ -21,9 +20,10 @@ import Web3Context from './contexts/Web3Context'; import DAOArtifact from './assets/DAO.json'; import Work1Artifact from './assets/Work1.json'; import OnboardingArtifact from './assets/Onboarding.json'; -import WorkContract from './components/WorkContract'; -import AddPostModal from './components/AddPostModal'; -import ViewPostModal from './components/ViewPostModal'; +import WorkContract from './components/work-contracts/WorkContract'; +import AddPostModal from './components/posts/AddPostModal'; +import ViewPostModal from './components/posts/ViewPostModal'; +import Post from './utils/Post'; function App() { const { @@ -252,11 +252,8 @@ function App() { const handleShowAddPost = () => setShowAddPost(true); const handleShowViewPost = async (post) => { - const res = await axios.get(`/api/read/${post.contentId}`); - const { data } = res; - // TODO: Verify base64url(sha256(JSON.stringify(data))) = contentId - // TODO: Verify data.author = post.author - setViewPostContent(data.content); + const { content } = await Post.read(post.contentId); + setViewPostContent(content); setShowViewPost(true); }; diff --git a/client/src/components/AddPostModal.jsx b/client/src/components/posts/AddPostModal.jsx similarity index 71% rename from client/src/components/AddPostModal.jsx rename to client/src/components/posts/AddPostModal.jsx index 2a3a5f9..903402a 100644 --- a/client/src/components/AddPostModal.jsx +++ b/client/src/components/posts/AddPostModal.jsx @@ -2,11 +2,10 @@ import { useCallback, useContext, useState } from 'react'; import Button from 'react-bootstrap/Button'; import Form from 'react-bootstrap/Form'; import Modal from 'react-bootstrap/Modal'; -import axios from 'axios'; import PropTypes from 'prop-types'; -import { Buffer } from 'buffer/'; // note: the trailing slash is important! -import Web3Context from '../contexts/Web3Context'; +import Web3Context from '../../contexts/Web3Context'; +import Post from '../../utils/Post'; function AddPostModal({ show, setShow, title, postToBlockchain, onSubmit, @@ -20,28 +19,21 @@ function AddPostModal({ const handleSubmit = useCallback(async () => { // Upload content to API - // TODO: include metamask signature - const msg = `0x${Buffer.from(content, 'utf8').toString('hex')}`; - const signature = await provider.request({ - method: 'personal_sign', - params: [msg, account], - }); - const data = { - author: account, content, signature, - }; - const res = await axios.post('/api/write', data); - const hash = res.data; - setShow(false); + const post = new Post({ content }); + // Include metamask signature + await post.sign(provider, account); + // Clear the input and hide the modal setContent(''); + setShow(false); + // Write to API + await post.write(); + // If requested, upload the hash to the blockchain if (postToBlockchain) { - // Upload hash to blockchain - await DAO.methods.addPost(account, hash).send({ - from: account, - gas: 1000000, - }); + await post.publish(DAO, account); } + // If requested, call callback if (onSubmit) { - onSubmit(hash, data); + onSubmit(post); } }, [provider, DAO, account, content, setShow, postToBlockchain, onSubmit]); diff --git a/client/src/components/ViewPostModal.jsx b/client/src/components/posts/ViewPostModal.jsx similarity index 100% rename from client/src/components/ViewPostModal.jsx rename to client/src/components/posts/ViewPostModal.jsx diff --git a/client/src/components/AvailabilityStakes.jsx b/client/src/components/work-contracts/AvailabilityStakes.jsx similarity index 97% rename from client/src/components/AvailabilityStakes.jsx rename to client/src/components/work-contracts/AvailabilityStakes.jsx index 1fe1d28..3fddd91 100644 --- a/client/src/components/AvailabilityStakes.jsx +++ b/client/src/components/work-contracts/AvailabilityStakes.jsx @@ -2,8 +2,8 @@ import { useCallback, useContext, useEffect } from 'react'; import { PropTypes } from 'prop-types'; import Button from 'react-bootstrap/Button'; -import Web3Context from '../contexts/Web3Context'; -import WorkContractContext from '../contexts/WorkContractContext'; +import Web3Context from '../../contexts/Web3Context'; +import WorkContractContext from '../../contexts/WorkContractContext'; const getAvailabilityStatus = (stake) => { if (stake.reclaimed) return 'Reclaimed'; diff --git a/client/src/components/WorkContract.jsx b/client/src/components/work-contracts/WorkContract.jsx similarity index 93% rename from client/src/components/WorkContract.jsx rename to client/src/components/work-contracts/WorkContract.jsx index c7316f4..17a6891 100644 --- a/client/src/components/WorkContract.jsx +++ b/client/src/components/work-contracts/WorkContract.jsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { PropTypes } from 'prop-types'; -import useList from '../utils/List'; -import WorkContractContext from '../contexts/WorkContractContext'; +import useList from '../../utils/List'; +import WorkContractContext from '../../contexts/WorkContractContext'; import AvailabilityStakes from './AvailabilityStakes'; import WorkRequests from './WorkRequests'; diff --git a/client/src/components/WorkRequests.jsx b/client/src/components/work-contracts/WorkRequests.jsx similarity index 91% rename from client/src/components/WorkRequests.jsx rename to client/src/components/work-contracts/WorkRequests.jsx index df729e7..be67a47 100644 --- a/client/src/components/WorkRequests.jsx +++ b/client/src/components/work-contracts/WorkRequests.jsx @@ -2,15 +2,15 @@ import { useCallback, useContext, useEffect, useState, } from 'react'; import { PropTypes } from 'prop-types'; -import axios from 'axios'; import Button from 'react-bootstrap/Button'; import Web3 from 'web3'; -import Web3Context from '../contexts/Web3Context'; -import useList from '../utils/List'; -import WorkContractContext from '../contexts/WorkContractContext'; -import AddPostModal from './AddPostModal'; -import ViewPostModal from './ViewPostModal'; +import Web3Context from '../../contexts/Web3Context'; +import useList from '../../utils/List'; +import WorkContractContext from '../../contexts/WorkContractContext'; +import AddPostModal from '../posts/AddPostModal'; +import ViewPostModal from '../posts/ViewPostModal'; +import Post from '../../utils/Post'; const getRequestStatus = (request) => { switch (Number(request.status)) { @@ -115,8 +115,7 @@ function WorkRequests({ setShowEvidenceModal(true); }; - const onSubmitRequest = useCallback(async (hash) => { - // TODO: Accept input, upload to API, include hash in contract call + const onSubmitRequest = useCallback(async ({ hash }) => { const web3 = new Web3(provider); const priceWei = BigInt(web3.utils.toWei(price, 'ether')); await workContract.methods.requestWork(hash).send({ @@ -126,7 +125,7 @@ function WorkRequests({ }); }, [provider, workContract, account, price]); - const onSubmitEvidence = useCallback(async (hash) => { + const onSubmitEvidence = useCallback(async ({ hash }) => { await workContract.methods.submitWorkEvidence(currentRequestId, hash).send({ from: account, gas: 1000000, @@ -134,11 +133,8 @@ function WorkRequests({ }, [workContract, account, currentRequestId]); const handleShowViewRequestModal = async (request) => { - const res = await axios.get(`/api/read/${request.requestContentId}`); - const { data } = res; - // TODO: Verify base64url(sha256(JSON.stringify(data))) = contentId - // TODO: Verify data.author = post.author - setViewRequestContent(data.content); + const { content } = await Post.read(request.requestContentId); + setViewRequestContent(content); setShowViewRequestModal(true); }; diff --git a/client/src/contract-addresses.json b/client/src/contract-addresses.json index 92cb5ff..ed5bf3d 100644 --- a/client/src/contract-addresses.json +++ b/client/src/contract-addresses.json @@ -1,8 +1,8 @@ { "localhost": { - "DAO": "0x2D812555F4eF06267406D80E7fA01Ac3288f626c", - "Work1": "0x3CAB55d59af095618F2ee539463E33447cfc97BA", - "Onboarding": "0xaB0c7Cf9A436978F55831C8EdB67892419ABAE62" + "DAO": "0x691Bcb6a8378Cec103BE58Dfa037DC57E6FFf4d1", + "Work1": "0xC489CE618A049B413CE0AED9Fc7219a04510ddbb", + "Onboarding": "0x3477A098fBFe09aa26693012176baAEa16d9D2DA" }, "sepolia": { "DAO": "0xa3b15aBD114C2332652A4fD5f9A43B86315E5078", diff --git a/client/src/utils/Post.js b/client/src/utils/Post.js new file mode 100644 index 0000000..f11de88 --- /dev/null +++ b/client/src/utils/Post.js @@ -0,0 +1,75 @@ +import axios from 'axios'; +// trailing slash is deliberate, to differentiate this package from the core node module +import { Buffer } from 'buffer/'; +import { recoverPersonalSignature } from '@metamask/eth-sig-util'; +// import createHash from 'create-hash'; +import objectHash from 'object-hash'; + +// Make Buffer available to recoverPersonalSignature +window.Buffer = Buffer; + +class Post { + constructor({ + author, content, signature, hash, + }) { + this.author = author; + this.content = content; + this.signature = signature; + this.hash = hash; + } + + // Read from API + static async read(hash) { + const { data: { content, author, signature } } = await axios.get(`/api/read/${hash}`); + // Verify hash + const derivedHash = objectHash({ author, content, signature }); + if (hash !== derivedHash) { + throw new Error('Hash mismatch'); + } + // Verify signature + let recovered; + try { + recovered = recoverPersonalSignature({ data: content, signature }); + } catch (e) { + throw new Error('Signature error', e); + } + if (recovered !== author) { + throw new Error('Author mismatch'); + } + return new Post({ + content, author, signature, hash, + }); + } + + // Include MetaMask signature + async sign(web3Provider, account) { + this.author = account; + const msg = `0x${Buffer.from(this.content, 'utf8').toString('hex')}`; + this.signature = await web3Provider.request({ + method: 'personal_sign', + params: [msg, account], + }); + return this; + } + + // Write to API + async write() { + const data = { + author: this.author, + content: this.content, + signature: this.signature, + }; + const { data: hash } = await axios.post('/api/write', data); + this.hash = hash; + } + + // Upload hash to blockchain + async publish(DAO, account) { + await DAO.methods.addPost(account, this.hash).send({ + from: account, + gas: 1000000, + }); + } +} + +export default Post; diff --git a/ethereum/.env.example b/ethereum/.env.example index ceb1918..49eaa36 100644 --- a/ethereum/.env.example +++ b/ethereum/.env.example @@ -1 +1,4 @@ -SEPOLIA_PRIVATE_KEY= \ No newline at end of file +SEPOLIA_PRIVATE_KEY= +ETHERSCAN_API_KEY= +WORK1_PRICE="0.001" +ONBOARDING_PRICE="0.001" \ No newline at end of file diff --git a/ethereum/contract-addresses.json b/ethereum/contract-addresses.json index 92cb5ff..ed5bf3d 100644 --- a/ethereum/contract-addresses.json +++ b/ethereum/contract-addresses.json @@ -1,8 +1,8 @@ { "localhost": { - "DAO": "0x2D812555F4eF06267406D80E7fA01Ac3288f626c", - "Work1": "0x3CAB55d59af095618F2ee539463E33447cfc97BA", - "Onboarding": "0xaB0c7Cf9A436978F55831C8EdB67892419ABAE62" + "DAO": "0x691Bcb6a8378Cec103BE58Dfa037DC57E6FFf4d1", + "Work1": "0xC489CE618A049B413CE0AED9Fc7219a04510ddbb", + "Onboarding": "0x3477A098fBFe09aa26693012176baAEa16d9D2DA" }, "sepolia": { "DAO": "0xa3b15aBD114C2332652A4fD5f9A43B86315E5078", diff --git a/ethereum/scripts/deploy.js b/ethereum/scripts/deploy.js index 0a64435..94a293b 100644 --- a/ethereum/scripts/deploy.js +++ b/ethereum/scripts/deploy.js @@ -3,7 +3,11 @@ const fs = require('fs'); const contractAddresses = require('../contract-addresses.json'); +require('dotenv').config(); + const network = process.env.HARDHAT_NETWORK; +const work1Price = process.env.WORK1_PRICE || 0.001; +const onboardingPrice = process.env.ONBOARDING_PRICE || '0.001'; async function main() { const dao = await ethers.deployContract('DAO'); @@ -21,8 +25,8 @@ async function main() { fs.copyFileSync(`./artifacts/contracts/${name}.sol/${name}.json`, `../client/src/assets/${name}.json`); }; - await deployWorkContract('Work1', ethers.parseEther('0.001')); - await deployWorkContract('Onboarding', ethers.parseEther('0.001')); + await deployWorkContract('Work1', ethers.parseEther(work1Price)); + await deployWorkContract('Onboarding', ethers.parseEther(onboardingPrice)); fs.writeFileSync('../client/src/contract-addresses.json', JSON.stringify(contractAddresses, null, 2)); console.log('Wrote file', fs.realpathSync('../client/src/contract-addresses.json'));