diff --git a/.env b/.env index 1e4a0f09dd..d461586f17 100644 --- a/.env +++ b/.env @@ -12,3 +12,5 @@ NEXT_PUBLIC_INFURA_IPFS_API_KEY="" NEXT_PUBLIC_INFURA_IPFS_API_SECRET="" # site preview links NEXT_PUBLIC_SITE_URL="https://app.dev.fractalframework.xyz/" +# Shutter Public Key +NEXT_PUBLIC_SHUTTER_EON_PUBKEY=0x0e6493bbb4ee8b19aa9b70367685049ff01dc9382c46aed83f8bc07d2a5ba3e6030bd83b942c1fd3dff5b79bef3b40bf6b666e51e7f0be14ed62daaffad47435265f5c9403b1a801921981f7d8659a9bd91fe92fb1cf9afdb16178a532adfaf51a237103874bb03afafe9cab2118dae1be5f08a0a28bf488c1581e9db4bc23ca \ No newline at end of file diff --git a/next.config.js b/next.config.js index f27179fc00..0e51a6af5a 100644 --- a/next.config.js +++ b/next.config.js @@ -1,6 +1,40 @@ +class WasmChunksFixPlugin { + apply(compiler) { + compiler.hooks.thisCompilation.tap('WasmChunksFixPlugin', compilation => { + compilation.hooks.processAssets.tap({ name: 'WasmChunksFixPlugin' }, assets => + Object.entries(assets).forEach(([pathname, source]) => { + if (!pathname.match(/\.wasm$/)) return; + compilation.deleteAsset(pathname); + + const name = pathname.split('/')[1]; + const info = compilation.assetsInfo.get(pathname); + compilation.emitAsset(name, source, info); + }) + ); + }); + } +} + /** @type {import('next').NextConfig} */ module.exports = { output: undefined, + webpack(config, { isServer, dev }) { + config.experiments = { + asyncWebAssembly: true, + layers: true, + }; + + config.resolve.fallback = { + fs: false, + }; + + if (!dev && isServer) { + config.output.webassemblyModuleFilename = 'chunks/[id].wasm'; + config.plugins.push(new WasmChunksFixPlugin()); + } + + return config; + }, images: { dangerouslyAllowSVG: true, contentDispositionType: 'attachment', diff --git a/package-lock.json b/package-lock.json index 5b6f9438aa..18366a227c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,8 @@ "@safe-global/safe-service-client": "^1.5.2", "@sentry/react": "^7.42.0", "@sentry/tracing": "^7.42.0", + "@shutter-network/shutter-crypto": "^1.0.1", + "@snapshot-labs/snapshot.js": "^0.7.3", "@ukstv/jazzicon-react": "^1.0.0", "@walletconnect/ethereum-provider": "^1.8.0", "axios": "^0.27.2", @@ -3646,6 +3648,11 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" }, + "node_modules/@ensdomains/eth-ens-namehash": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/@ensdomains/eth-ens-namehash/-/eth-ens-namehash-2.0.15.tgz", + "integrity": "sha512-JRDFP6+Hczb1E0/HhIg0PONgBYasfGfDheujmfxaZaAv/NAH4jE6Kf48WbqfRZdxt4IZI3jl3Ri7sZ1nP09lgw==" + }, "node_modules/@envelop/core": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@envelop/core/-/core-3.0.6.tgz", @@ -9489,6 +9496,14 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/@shutter-network/shutter-crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shutter-network/shutter-crypto/-/shutter-crypto-1.0.1.tgz", + "integrity": "sha512-bA8uFUkjBaec0ui8za7cEEvspcUTi5DdsO/U8mq2MkgdVu+JRoNuQOhS0hVbh77FXOhXVIZXNf6iHxRIk/IjJQ==", + "dependencies": { + "browser-or-node": "^2.0.0" + } + }, "node_modules/@sideway/address": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", @@ -9534,6 +9549,50 @@ "@sinonjs/commons": "^2.0.0" } }, + "node_modules/@snapshot-labs/snapshot.js": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@snapshot-labs/snapshot.js/-/snapshot.js-0.7.3.tgz", + "integrity": "sha512-T+cjaJ9ZPLlKicPTxuN6FIQXUgADjvit/QFmwsOlGWxpMzAQ79k5rWm0ldex96JTPIFucUMgqHqWLA6UZwmmTQ==", + "dependencies": { + "@ensdomains/eth-ens-namehash": "^2.0.15", + "@ethersproject/abi": "^5.6.4", + "@ethersproject/address": "^5.6.1", + "@ethersproject/bytes": "^5.6.1", + "@ethersproject/contracts": "^5.6.2", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/providers": "^5.6.8", + "@ethersproject/units": "^5.7.0", + "@ethersproject/wallet": "^5.6.2", + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "cross-fetch": "^3.1.6", + "json-to-graphql-query": "^2.2.4", + "lodash.set": "^4.3.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@snapshot-labs/snapshot.js/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@snapshot-labs/snapshot.js/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "node_modules/@solana/buffer-layout": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", @@ -11048,6 +11107,42 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "node_modules/anser": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", @@ -11799,6 +11894,11 @@ "run-parallel-limit": "^1.1.0" } }, + "node_modules/browser-or-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz", + "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==" + }, "node_modules/browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -12797,11 +12897,11 @@ "devOptional": true }, "node_modules/cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", + "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", "dependencies": { - "node-fetch": "2.6.7" + "node-fetch": "^2.6.12" } }, "node_modules/cross-spawn": { @@ -14806,8 +14906,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-diff": { "version": "1.2.0", @@ -17573,6 +17672,11 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, + "node_modules/json-to-graphql-query": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/json-to-graphql-query/-/json-to-graphql-query-2.2.5.tgz", + "integrity": "sha512-5Nom9inkIMrtY992LMBBG1Zaekrc10JaRhyZgprwHBVMDtRgllTvzl0oBbg13wJsVZoSoFNNMaeIVQs0P04vsA==" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -17833,6 +17937,11 @@ "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" }, + "node_modules/lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==" + }, "node_modules/lodash.throttle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", @@ -20232,9 +20341,9 @@ } }, "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -21265,7 +21374,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, "engines": { "node": ">=6" } @@ -24504,7 +24612,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -28036,6 +28143,11 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" }, + "@ensdomains/eth-ens-namehash": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/@ensdomains/eth-ens-namehash/-/eth-ens-namehash-2.0.15.tgz", + "integrity": "sha512-JRDFP6+Hczb1E0/HhIg0PONgBYasfGfDheujmfxaZaAv/NAH4jE6Kf48WbqfRZdxt4IZI3jl3Ri7sZ1nP09lgw==" + }, "@envelop/core": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@envelop/core/-/core-3.0.6.tgz", @@ -32490,6 +32602,14 @@ } } }, + "@shutter-network/shutter-crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shutter-network/shutter-crypto/-/shutter-crypto-1.0.1.tgz", + "integrity": "sha512-bA8uFUkjBaec0ui8za7cEEvspcUTi5DdsO/U8mq2MkgdVu+JRoNuQOhS0hVbh77FXOhXVIZXNf6iHxRIk/IjJQ==", + "requires": { + "browser-or-node": "^2.0.0" + } + }, "@sideway/address": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", @@ -32535,6 +32655,45 @@ "@sinonjs/commons": "^2.0.0" } }, + "@snapshot-labs/snapshot.js": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@snapshot-labs/snapshot.js/-/snapshot.js-0.7.3.tgz", + "integrity": "sha512-T+cjaJ9ZPLlKicPTxuN6FIQXUgADjvit/QFmwsOlGWxpMzAQ79k5rWm0ldex96JTPIFucUMgqHqWLA6UZwmmTQ==", + "requires": { + "@ensdomains/eth-ens-namehash": "^2.0.15", + "@ethersproject/abi": "^5.6.4", + "@ethersproject/address": "^5.6.1", + "@ethersproject/bytes": "^5.6.1", + "@ethersproject/contracts": "^5.6.2", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/providers": "^5.6.8", + "@ethersproject/units": "^5.7.0", + "@ethersproject/wallet": "^5.6.2", + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "cross-fetch": "^3.1.6", + "json-to-graphql-query": "^2.2.4", + "lodash.set": "^4.3.2" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, "@solana/buffer-layout": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", @@ -33667,6 +33826,32 @@ "uri-js": "^4.2.2" } }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, "anser": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", @@ -34250,6 +34435,11 @@ "run-parallel-limit": "^1.1.0" } }, + "browser-or-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz", + "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==" + }, "browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -35052,11 +35242,11 @@ "devOptional": true }, "cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", + "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", "requires": { - "node-fetch": "2.6.7" + "node-fetch": "^2.6.12" } }, "cross-spawn": { @@ -36670,8 +36860,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-diff": { "version": "1.2.0", @@ -38803,6 +38992,11 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, + "json-to-graphql-query": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/json-to-graphql-query/-/json-to-graphql-query-2.2.5.tgz", + "integrity": "sha512-5Nom9inkIMrtY992LMBBG1Zaekrc10JaRhyZgprwHBVMDtRgllTvzl0oBbg13wJsVZoSoFNNMaeIVQs0P04vsA==" + }, "json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -39007,6 +39201,11 @@ "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" }, + "lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==" + }, "lodash.throttle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", @@ -40795,9 +40994,9 @@ } }, "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "requires": { "whatwg-url": "^5.0.0" } @@ -41564,8 +41763,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "pvtsutils": { "version": "1.3.2", @@ -44028,7 +44226,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "requires": { "punycode": "^2.1.0" } diff --git a/package.json b/package.json index cc288716da..a47a16194e 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "@safe-global/safe-service-client": "^1.5.2", "@sentry/react": "^7.42.0", "@sentry/tracing": "^7.42.0", + "@shutter-network/shutter-crypto": "^1.0.1", + "@snapshot-labs/snapshot.js": "^0.7.3", "@ukstv/jazzicon-react": "^1.0.0", "@walletconnect/ethereum-provider": "^1.8.0", "axios": "^0.27.2", diff --git a/public/assets/scripts/shutter-crypto.wasm b/public/assets/scripts/shutter-crypto.wasm new file mode 100644 index 0000000000..1e04459d95 Binary files /dev/null and b/public/assets/scripts/shutter-crypto.wasm differ diff --git a/src/components/Proposals/ProposalActions/CastVote.tsx b/src/components/Proposals/ProposalActions/CastVote.tsx index 56c0cf9a43..4501bb5c4a 100644 --- a/src/components/Proposals/ProposalActions/CastVote.tsx +++ b/src/components/Proposals/ProposalActions/CastVote.tsx @@ -10,19 +10,27 @@ import { AzoriusProposal, FractalProposalState, AzoriusVoteChoice, + ExtendedSnapshotProposal, } from '../../../types'; import { useVoteContext } from '../ProposalVotes/context/VoteContext'; -function Vote({ proposal }: { proposal: FractalProposal }) { +function Vote({ + proposal, + extendedSnapshotProposal, +}: { + proposal: FractalProposal; + extendedSnapshotProposal?: ExtendedSnapshotProposal; +}) { const [pending, setPending] = useState(false); const { t } = useTranslation(['common', 'proposal']); const { isLoaded: isCurrentBlockLoaded, currentBlockNumber } = useCurrentBlockNumber(); const azoriusProposal = proposal as AzoriusProposal; - const { castVote } = useCastVote({ + const { castVote, castSnapshotVote } = useCastVote({ proposal, setPending, + extendedSnapshotProposal, }); const { isSnapshotProposal } = useSnapshotProposal(proposal); @@ -51,6 +59,24 @@ function Vote({ proposal }: { proposal: FractalProposal }) { canVoteLoading || hasVotedLoading; + if (isSnapshotProposal && extendedSnapshotProposal) { + return ( + <> + {extendedSnapshotProposal.choices.map(choice => ( + + ))} + + ); + } + return ( ; + return ( + + ); case FractalProposalState.EXECUTABLE: case FractalProposalState.TIMELOCKED: return ; @@ -28,9 +44,11 @@ export function ProposalActions({ proposal }: { proposal: FractalProposal }) { export function ProposalAction({ proposal, expandedView, + extendedSnapshotProposal, }: { proposal: FractalProposal; expandedView?: boolean; + extendedSnapshotProposal?: ExtendedSnapshotProposal; }) { const { node: { daoAddress }, @@ -119,7 +137,10 @@ export function ProposalAction({ - + ); } diff --git a/src/components/Proposals/ProposalInfo.tsx b/src/components/Proposals/ProposalInfo.tsx index 0f5e249a52..123481654a 100644 --- a/src/components/Proposals/ProposalInfo.tsx +++ b/src/components/Proposals/ProposalInfo.tsx @@ -1,8 +1,10 @@ -import { Box, Flex, Text, Image } from '@chakra-ui/react'; +import { Box, Flex, Text, Image, Button } from '@chakra-ui/react'; +import { Shield } from '@decent-org/fractal-ui'; +import { useTranslation } from 'react-i18next'; import useSnapshotProposal from '../../hooks/DAO/loaders/snapshot/useSnapshotProposal'; import { useGetMetadata } from '../../hooks/DAO/proposal/useGetMetadata'; import { useFractal } from '../../providers/App/AppProvider'; -import { FractalProposal } from '../../types'; +import { ExtendedSnapshotProposal, FractalProposal } from '../../types'; import { ActivityDescription } from '../Activity/ActivityDescription'; import Snapshot from '../ui/badges/Snapshot'; import { ModalType } from '../ui/modals/ModalProvider'; @@ -10,8 +12,13 @@ import { useFractalModal } from '../ui/modals/useFractalModal'; import ProposalExecutableCode from '../ui/proposal/ProposalExecutableCode'; import ProposalStateBox from '../ui/proposal/ProposalStateBox'; -export function ProposalInfo({ proposal }: { proposal: FractalProposal }) { +export function ProposalInfo({ + proposal, +}: { + proposal: FractalProposal | ExtendedSnapshotProposal; +}) { const metaData = useGetMetadata(proposal); + const { t } = useTranslation('proposal'); const { node: { daoSnapshotURL }, } = useFractal(); @@ -27,10 +34,31 @@ export function ProposalInfo({ proposal }: { proposal: FractalProposal }) { > {isSnapshotProposal && ( - + <> + + {(proposal as ExtendedSnapshotProposal).privacy === 'shutter' && ( + + )} + )} diff --git a/src/components/Proposals/ProposalVotes/index.tsx b/src/components/Proposals/ProposalVotes/index.tsx index c0a7318479..9ee111f749 100644 --- a/src/components/Proposals/ProposalVotes/index.tsx +++ b/src/components/Proposals/ProposalVotes/index.tsx @@ -9,7 +9,7 @@ import { Text, } from '@chakra-ui/react'; import { BigNumber } from 'ethers'; -import { useCallback, useMemo } from 'react'; +import { ReactNode, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { BACKGROUND_SEMI_TRANSPARENT } from '../../../constants/common'; import { useFractal } from '../../../providers/App/AppProvider'; @@ -25,18 +25,32 @@ import ProgressBar from '../../ui/utils/ProgressBar'; import ProposalERC20VoteItem from './ProposalERC20VoteItem'; import ProposalERC721VoteItem from './ProposalERC721VoteItem'; -export function VotesPercentage({ label, percentage }: { label: string; percentage: number }) { +export function VotesPercentage({ + label, + percentage, + children, +}: { + label: string; + percentage: number; + children?: ReactNode; +}) { return ( - - {label} - + + {label} + + {children} + ); diff --git a/src/components/Proposals/SnapshotProposalDetails/SnapshotProposalSummary.tsx b/src/components/Proposals/SnapshotProposalDetails/SnapshotProposalSummary.tsx index 6c2b68fb1b..216adb6ecd 100644 --- a/src/components/Proposals/SnapshotProposalDetails/SnapshotProposalSummary.tsx +++ b/src/components/Proposals/SnapshotProposalDetails/SnapshotProposalSummary.tsx @@ -1,6 +1,5 @@ import { Text, Box, Divider, Flex } from '@chakra-ui/react'; import { format } from 'date-fns'; -import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { BACKGROUND_SEMI_TRANSPARENT } from '../../../constants/common'; import { ExtendedSnapshotProposal } from '../../../types'; @@ -10,28 +9,15 @@ import ExternalLink from '../../ui/links/ExternalLink'; import { InfoBoxLoader } from '../../ui/loaders/InfoBoxLoader'; import { ExtendedProgressBar } from '../../ui/utils/ProgressBar'; import { InfoRow } from '../MultisigProposalDetails/TxDetails'; +import useTotalVotes from './hooks/useTotalVotes'; interface ISnapshotProposalSummary { proposal?: ExtendedSnapshotProposal; } export default function SnapshotProposalSummary({ proposal }: ISnapshotProposalSummary) { - const [totalVotesCasted, setTotalVotesCasted] = useState(0); const { t } = useTranslation(['proposal', 'common', 'navigation']); - - useEffect(() => { - if (proposal) { - let newTotalVotesCasted = 0; - Object.keys(proposal.votesBreakdown).forEach(voteChoice => { - const voteChoiceBreakdown = proposal.votesBreakdown[voteChoice]; - newTotalVotesCasted += voteChoiceBreakdown.total; - }); - - if (newTotalVotesCasted !== totalVotesCasted) { - setTotalVotesCasted(newTotalVotesCasted); - } - } - }, [proposal, totalVotesCasted]); + const { totalVotesCasted } = useTotalVotes({ proposal }); if (!proposal) { return ( @@ -75,26 +61,6 @@ export default function SnapshotProposalSummary({ proposal }: ISnapshotProposalS #{proposal.ipfs.slice(0, 7)} - {proposal.privacy === 'shutter' && ( - - - {t('privacy')} - - - {t('shutterPrivacy')} - - - )} 0 ? totalVotesCasted / proposal.quorum : 100} + valueLabel={`${totalVotesCasted}/${proposal.quorum}`} + percentage={ + proposal.quorum > 0 ? (totalVotesCasted / proposal.quorum || 1) * 100 : 100 + } requiredPercentage={100} unit="" /> diff --git a/src/components/Proposals/SnapshotProposalDetails/SnapshotProposalVoteItem.tsx b/src/components/Proposals/SnapshotProposalDetails/SnapshotProposalVoteItem.tsx new file mode 100644 index 0000000000..6f7aa7418c --- /dev/null +++ b/src/components/Proposals/SnapshotProposalDetails/SnapshotProposalVoteItem.tsx @@ -0,0 +1,42 @@ +import { Grid, GridItem, Text } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import useDisplayName from '../../../hooks/utils/useDisplayName'; +import { useFractal } from '../../../providers/App/AppProvider'; +import { ExtendedSnapshotProposal, SnapshotVote } from '../../../types'; +import StatusBox from '../../ui/badges/StatusBox'; + +interface ISnapshotProposalVoteItem { + proposal: ExtendedSnapshotProposal; + vote: SnapshotVote; +} + +export default function SnapshotProposalVoteItem({ proposal, vote }: ISnapshotProposalVoteItem) { + const { t } = useTranslation(); + const { displayName } = useDisplayName(vote.voter); + const { + readOnly: { user }, + } = useFractal(); + return ( + + + + {displayName} + {user.address === vote.voter && t('isMeSuffix')} + + + + + {vote.choice} + + + + + {vote.votingWeight} {proposal.strategies[0].params.symbol} + + + + ); +} diff --git a/src/components/Proposals/SnapshotProposalDetails/SnapshotProposalVotes.tsx b/src/components/Proposals/SnapshotProposalDetails/SnapshotProposalVotes.tsx index 4220aa0ce3..54aa41379f 100644 --- a/src/components/Proposals/SnapshotProposalDetails/SnapshotProposalVotes.tsx +++ b/src/components/Proposals/SnapshotProposalDetails/SnapshotProposalVotes.tsx @@ -1,9 +1,106 @@ -import { SnapshotProposal } from '../../../types'; +import { Divider, Flex, Grid, GridItem, Text } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import { BACKGROUND_SEMI_TRANSPARENT } from '../../../constants/common'; +import { ExtendedSnapshotProposal, FractalProposalState } from '../../../types'; +import ContentBox from '../../ui/containers/ContentBox'; +import { ProposalCountdown } from '../../ui/proposal/ProposalCountdown'; +import { VotesPercentage } from '../ProposalVotes'; +import SnapshotProposalVoteItem from './SnapshotProposalVoteItem'; +import useTotalVotes from './hooks/useTotalVotes'; interface ISnapshotProposalVotes { - proposal: SnapshotProposal; + proposal: ExtendedSnapshotProposal; } -export default function SnapshotProposalVotes({}: ISnapshotProposalVotes) { - return
Snapshot Votes
; +export default function SnapshotProposalVotes({ proposal }: ISnapshotProposalVotes) { + const { t } = useTranslation('proposal'); + const { totalVotesCasted } = useTotalVotes({ proposal }); + const { votes, votesBreakdown, choices, strategies, privacy, state } = proposal; + const strategySymbol = strategies[0].params.symbol; + + return ( + <> + + + {t('breakdownTitle')} + + + {t('totalVotes')} + + + {totalVotesCasted} {strategySymbol} + + + + + + {choices.map(choice => ( + + + {proposal.privacy === 'shutter' + ? `? ${strategySymbol}` + : `${votesBreakdown[choice].total} ${strategySymbol}`} + + + ))} + + + + {votes && votes.length !== 0 && ( + + + {t('votesTitle')} ({totalVotesCasted}) + + + + {privacy === 'shutter' && state !== FractalProposalState.CLOSED ? ( + + + {t('shutterVotesHidden')} | + + + + ) : ( + votes.map(vote => ( + + )) + )} + + + )} + + ); } diff --git a/src/components/Proposals/SnapshotProposalDetails/hooks/useTotalVotes.ts b/src/components/Proposals/SnapshotProposalDetails/hooks/useTotalVotes.ts new file mode 100644 index 0000000000..1aa411bf9e --- /dev/null +++ b/src/components/Proposals/SnapshotProposalDetails/hooks/useTotalVotes.ts @@ -0,0 +1,24 @@ +import { useState, useEffect } from 'react'; +import { ExtendedSnapshotProposal } from '../../../../types'; + +export default function useTotalVotes({ proposal }: { proposal?: ExtendedSnapshotProposal }) { + const [totalVotesCasted, setTotalVotesCasted] = useState(0); + + useEffect(() => { + if (proposal) { + let newTotalVotesCasted = 0; + if (proposal.votesBreakdown) { + Object.keys(proposal.votesBreakdown).forEach(voteChoice => { + const voteChoiceBreakdown = proposal.votesBreakdown[voteChoice]; + newTotalVotesCasted += voteChoiceBreakdown.total; + }); + + if (newTotalVotesCasted !== totalVotesCasted) { + setTotalVotesCasted(newTotalVotesCasted); + } + } + } + }, [proposal, totalVotesCasted]); + + return { totalVotesCasted }; +} diff --git a/src/components/Proposals/SnapshotProposalDetails/index.tsx b/src/components/Proposals/SnapshotProposalDetails/index.tsx index b464f11a29..aca9a85e33 100644 --- a/src/components/Proposals/SnapshotProposalDetails/index.tsx +++ b/src/components/Proposals/SnapshotProposalDetails/index.tsx @@ -33,9 +33,11 @@ export default function SnapshotProposalDetails({ proposal }: ISnapshotProposalD - + - + {!!extendedSnapshotProposal && ( + + )} @@ -43,6 +45,7 @@ export default function SnapshotProposalDetails({ proposal }: ISnapshotProposalD diff --git a/src/components/ui/badges/Snapshot.tsx b/src/components/ui/badges/Snapshot.tsx index dcaff722d1..64e87957af 100644 --- a/src/components/ui/badges/Snapshot.tsx +++ b/src/components/ui/badges/Snapshot.tsx @@ -1,18 +1,20 @@ import { Button, ButtonProps, Image } from '@chakra-ui/react'; +import { ArrowAngleUp } from '@decent-org/fractal-ui'; import { t } from 'i18next'; interface Props extends ButtonProps { snapshotURL: string; + isExternal?: boolean; } -export default function Snapshot({ snapshotURL, ...rest }: Props) { +export default function Snapshot({ snapshotURL, isExternal, ...rest }: Props) { return ( ); diff --git a/src/components/ui/proposal/ProposalCountdown.tsx b/src/components/ui/proposal/ProposalCountdown.tsx index 4970cdcfb1..692444fd56 100644 --- a/src/components/ui/proposal/ProposalCountdown.tsx +++ b/src/components/ui/proposal/ProposalCountdown.tsx @@ -18,7 +18,13 @@ const zeroPad = (num: number) => String(num).padStart(2, '0'); * * For all other states this component will simply return null. */ -export function ProposalCountdown({ proposal }: { proposal: FractalProposal }) { +export function ProposalCountdown({ + proposal, + showIcon = true, +}: { + proposal: FractalProposal; + showIcon?: boolean; +}) { const secondsLeft = useProposalCountdown(proposal); const { t } = useTranslation('proposal'); @@ -70,7 +76,7 @@ export function ProposalCountdown({ proposal }: { proposal: FractalProposal }) { justifyContent="flex-end" alignItems="center" > - {Icon && } + {showIcon && Icon && } >; + extendedSnapshotProposal?: ExtendedSnapshotProposal; }) => { const { governanceContracts: { ozLinearVotingContract, erc721LinearVotingContract }, governance, + node: { daoSnapshotURL }, + readOnly: { + user: { address }, + }, } = useFractal(); + const { data: signer } = useSigner(); const azoriusGovernance = useMemo(() => governance as AzoriusGovernance, [governance]); const { type } = azoriusGovernance; @@ -79,7 +99,54 @@ const useCastVote = ({ getHasVoted, ] ); - return { castVote }; + + const castSnapshotVote = useCallback( + async (choice: string) => { + if (signer && signer?.provider && address && daoSnapshotURL && extendedSnapshotProposal) { + let toastId; + try { + toastId = toast(t('pendingCastVote'), { + autoClose: false, + closeOnClick: false, + draggable: false, + closeButton: false, + progress: 1, + }); + if (extendedSnapshotProposal.privacy === 'shutter') { + const encryptedChoice = await encryptWithShutter( + choice, + extendedSnapshotProposal.proposalId + ); + await client.vote(signer.provider as ethers.providers.Web3Provider, address, { + space: daoSnapshotURL, + proposal: extendedSnapshotProposal.proposalId, + type: extendedSnapshotProposal.type, + privacy: extendedSnapshotProposal.privacy, + choice: encryptedChoice!, + app: 'fractal', + }); + } else { + await client.vote(signer.provider as ethers.providers.Web3Provider, address, { + space: daoSnapshotURL, + proposal: extendedSnapshotProposal.proposalId, + type: extendedSnapshotProposal.type, + choice, + app: 'fractal', + }); + } + toast.dismiss(toastId); + toast.success(t('successCastVote')); + } catch (e) { + toast.dismiss(toastId); + toast.error('failedCastVote'); + console.error('Error while casting Snapshot vote', e); + } + } + }, + [signer, address, daoSnapshotURL, extendedSnapshotProposal, t] + ); + + return { castVote, castSnapshotVote }; }; export default useCastVote; diff --git a/src/i18n/locales/en/proposal.json b/src/i18n/locales/en/proposal.json index 96332776c8..97c4e78a87 100644 --- a/src/i18n/locales/en/proposal.json +++ b/src/i18n/locales/en/proposal.json @@ -122,6 +122,9 @@ "singleSnapshotVotingSystem": "Single choice voting", "unknownSnapshotVotingSystem": "Unknown voting system", "ipfs": "IPFS", - "shutterPrivacy": "Shutter", - "shutterQuorumHelper": "Voting results are not exposed untill voting is finalized" + "privacy": "Privacy", + "shielded": "Shielded", + "shutterPrivacy": "Shielded", + "shutterVotesHidden": "Results hidden during voting", + "totalVotes": "Total votes" } \ No newline at end of file diff --git a/src/types/daoProposal.ts b/src/types/daoProposal.ts index 503c9bdc93..167c06ec42 100644 --- a/src/types/daoProposal.ts +++ b/src/types/daoProposal.ts @@ -89,10 +89,17 @@ export interface SnapshotVoteBreakdown { }; } +export type SnapshotProposalType = + | 'single-choice' + | 'approval' + | 'quadratic' + | 'ranked-choice' + | 'weighted' + | 'basic'; export interface ExtendedSnapshotProposal extends SnapshotProposal { snapshot: number; // Number of block snapshotState: string; // State retrieved from Snapshot - type: 'basic' | 'single'; + type: SnapshotProposalType; quorum?: number; privacy?: string; ipfs: string; diff --git a/src/utils/shutter.ts b/src/utils/shutter.ts new file mode 100644 index 0000000000..49c30b1de9 --- /dev/null +++ b/src/utils/shutter.ts @@ -0,0 +1,27 @@ +'use client'; + +import { init, encrypt } from '@shutter-network/shutter-crypto'; +import { BigNumber, utils } from 'ethers'; + +export default async function encryptWithShutter( + choice: string, + id: string +): Promise { + const shutterPath = '/assets/scripts/shutter-crypto.wasm'; + await init(shutterPath); + + const { arrayify, hexlify, toUtf8Bytes, formatBytes32String, randomBytes } = utils; + + const bytesChoice = toUtf8Bytes(choice); + const message = arrayify(bytesChoice); + const eonPublicKey = arrayify(process.env.NEXT_PUBLIC_SHUTTER_EON_PUBKEY!); + + const is32ByteString = id.substring(0, 2) === '0x'; + const proposalId = arrayify(is32ByteString ? id : formatBytes32String(id)); + + const sigma = arrayify(BigNumber.from(randomBytes(32))); + + const encryptedMessage = await encrypt(message, eonPublicKey, proposalId, sigma); + + return hexlify(encryptedMessage) ?? null; +}