From 36a8ca0b0ca4a4456f41d3fbd7b9824c3cacfe78 Mon Sep 17 00:00:00 2001 From: Danish Arora <35004822+danisharora099@users.noreply.github.com> Date: Wed, 2 Apr 2025 21:03:49 +0530 Subject: [PATCH] feat: keystore management (#119) --- .../keystore-management/package-lock.json | 416 ++++++++++++++---- examples/keystore-management/package.json | 2 +- .../keystore-management/src/app/layout.tsx | 19 +- examples/keystore-management/src/app/page.tsx | 11 + .../src/components/KeystoreManager.tsx | 123 ++++++ .../components/RLNMembershipRegistration.tsx | 151 +++++-- .../src/contexts/KeystoreContext.tsx | 160 +++++++ .../src/contexts/RLNFactory.tsx | 14 +- .../src/contexts/RLNLightContext.tsx | 33 +- .../src/contexts/RLNUnifiedContext.tsx | 2 +- .../src/contexts/RLNUnifiedContext2.tsx | 137 +++++- .../src/contexts/RLNUnifiedHook.tsx | 2 +- .../{RLNContext.tsx => RLNZerokitContext.tsx} | 36 +- .../src/utils/fileUtils.ts | 62 +++ 14 files changed, 967 insertions(+), 201 deletions(-) create mode 100644 examples/keystore-management/src/components/KeystoreManager.tsx create mode 100644 examples/keystore-management/src/contexts/KeystoreContext.tsx rename examples/keystore-management/src/contexts/{RLNContext.tsx => RLNZerokitContext.tsx} (88%) create mode 100644 examples/keystore-management/src/utils/fileUtils.ts diff --git a/examples/keystore-management/package-lock.json b/examples/keystore-management/package-lock.json index e38db54..9c1cff0 100644 --- a/examples/keystore-management/package-lock.json +++ b/examples/keystore-management/package-lock.json @@ -8,7 +8,7 @@ "name": "waku-keystore-management", "version": "0.1.0", "dependencies": { - "@waku/rln": "0.0.2-5c50ed7.0", + "@waku/rln": "0.1.5-6198efb.0", "next": "15.1.7", "react": "^19.0.0", "react-dom": "^19.0.0" @@ -1517,21 +1517,21 @@ } }, "node_modules/@libp2p/interface-internal": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/@libp2p/interface-internal/-/interface-internal-2.3.7.tgz", - "integrity": "sha512-u0I4zqUJhhPbL2ReX88068Sudv2uA/Z1sn6EeD8mr5kkGActTrzxoTjjxscmiQgQybOVyvJbkTiJciorT0ZyPw==", + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@libp2p/interface-internal/-/interface-internal-2.3.9.tgz", + "integrity": "sha512-1hW/yHktO3txc+r4ASmVA9GbNN6ZoGnH8Bt9VrYwY580BT53TP3eipn3Bo1XyGBDtmV6bpQiKhFK5AYVbhnz0g==", "license": "Apache-2.0 OR MIT", "dependencies": { "@libp2p/interface": "^2.7.0", - "@libp2p/peer-collections": "^6.0.23", + "@libp2p/peer-collections": "^6.0.25", "@multiformats/multiaddr": "^12.3.3", "progress-events": "^1.0.1" } }, "node_modules/@libp2p/logger": { - "version": "5.1.12", - "resolved": "https://registry.npmjs.org/@libp2p/logger/-/logger-5.1.12.tgz", - "integrity": "sha512-9K18gnPXxPkgPTQKHgCYaUimlbYheAEogDXvYme1TsPEBPH9oYTVsFpZhe5r92auE3aNuzUSR3VD0TCs/bAZ7g==", + "version": "5.1.13", + "resolved": "https://registry.npmjs.org/@libp2p/logger/-/logger-5.1.13.tgz", + "integrity": "sha512-JKyMlySG8T+LpItsj9Vma57yap/A0HqJ8ZdaHvgdoThhSOfqcRs8oRWO/2EG0Q5hUXugw//EAT+Ptj8MyNdbjQ==", "license": "Apache-2.0 OR MIT", "dependencies": { "@libp2p/interface": "^2.7.0", @@ -1542,21 +1542,21 @@ } }, "node_modules/@libp2p/peer-collections": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/@libp2p/peer-collections/-/peer-collections-6.0.23.tgz", - "integrity": "sha512-eJYmiq2KeUpm5727bl2ngSOnxPnc/VzKnHL2s30x9DfGxl2KpsFBuqAhnjpe5eCshHfQhXsIdbH+5IkY9jobrQ==", + "version": "6.0.25", + "resolved": "https://registry.npmjs.org/@libp2p/peer-collections/-/peer-collections-6.0.25.tgz", + "integrity": "sha512-sU6mjwANQvVPgTgslRZvxZ6cYzQJ66QmNHm6mrM0cx03Yf1heWnvL28N/P781nGsUjo1cJD7xB5ctAGk6A/lXw==", "license": "Apache-2.0 OR MIT", "dependencies": { "@libp2p/interface": "^2.7.0", - "@libp2p/peer-id": "^5.0.16", - "@libp2p/utils": "^6.5.7", + "@libp2p/peer-id": "^5.1.0", + "@libp2p/utils": "^6.6.0", "multiformats": "^13.3.1" } }, "node_modules/@libp2p/peer-id": { - "version": "5.0.16", - "resolved": "https://registry.npmjs.org/@libp2p/peer-id/-/peer-id-5.0.16.tgz", - "integrity": "sha512-gRVTWk8LvkSBStvqxc4A1JycEo4H+rJwwefdBmLR+d3fHiUf/2Y6t5elQJzouxykwurAglr8DnUhwCB/pQ9eQQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@libp2p/peer-id/-/peer-id-5.1.0.tgz", + "integrity": "sha512-9Xob9DDg1uBboM2QvJ5nyPbsjxsNS9obmGAYeAtLSx5aHAIC4AweJQFHssUUCfW7mufkzX/s3zyR62XPR4SYyQ==", "license": "Apache-2.0 OR MIT", "dependencies": { "@libp2p/crypto": "^5.0.15", @@ -1581,16 +1581,16 @@ } }, "node_modules/@libp2p/utils": { - "version": "6.5.7", - "resolved": "https://registry.npmjs.org/@libp2p/utils/-/utils-6.5.7.tgz", - "integrity": "sha512-fu6oRgSCOYlbuQObuF/ZVynMc7vdAfekxfi7B7TW6KjZEWmzeErvX1iEk9pfyJo5D0IxBCIMPRkN+Rr5MDfNWg==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@libp2p/utils/-/utils-6.6.0.tgz", + "integrity": "sha512-QjS1+r+jInOxULjdATBc1N/gorUWUoJqEKxpqTcB2wOwCipzB58RYR3n3QPeoRHj1mVMhZujE1dTbmK/Nafhqg==", "license": "Apache-2.0 OR MIT", "dependencies": { "@chainsafe/is-ip": "^2.0.2", "@chainsafe/netmask": "^2.0.0", "@libp2p/crypto": "^5.0.15", "@libp2p/interface": "^2.7.0", - "@libp2p/logger": "^5.1.12", + "@libp2p/logger": "^5.1.13", "@multiformats/multiaddr": "^12.3.3", "@sindresorhus/fnv1a": "^3.1.0", "any-signal": "^4.1.1", @@ -1954,6 +1954,50 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "license": "(Unlicense OR Apache-2.0)" + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -2265,16 +2309,16 @@ } }, "node_modules/@waku/core": { - "version": "0.0.34-5c50ed7.0", - "resolved": "https://registry.npmjs.org/@waku/core/-/core-0.0.34-5c50ed7.0.tgz", - "integrity": "sha512-GWYRzLZTfWgwXrK/KCWU3bbsYiE7pCyjGHKOfTDnUX9Z0LEx9fs5nDF7LyK41uefrZ0c6zrth3TH+Rkawm5aHw==", + "version": "0.0.35-6198efb.0", + "resolved": "https://registry.npmjs.org/@waku/core/-/core-0.0.35-6198efb.0.tgz", + "integrity": "sha512-9ta28XPCs9mOT9DmaewxHuZyWVME5bqRsF8Hardrd4QaHVLEaE5nlqi1nF7PoxdwF+l+XmZ2x/gqwgDee6qPwg==", "license": "MIT OR Apache-2.0", "dependencies": { "@libp2p/ping": "2.0.1", - "@waku/enr": "0.0.28-5c50ed7.0", - "@waku/interfaces": "0.0.29-5c50ed7.0", - "@waku/proto": "0.0.9-5c50ed7.0", - "@waku/utils": "0.0.22-5c50ed7.0", + "@waku/enr": "0.0.29-6198efb.0", + "@waku/interfaces": "0.0.30-6198efb.0", + "@waku/proto": "0.0.10-6198efb.0", + "@waku/utils": "0.0.23-6198efb.0", "debug": "^4.3.4", "it-all": "^3.0.4", "it-length-prefixed": "^9.0.4", @@ -2312,9 +2356,9 @@ } }, "node_modules/@waku/enr": { - "version": "0.0.28-5c50ed7.0", - "resolved": "https://registry.npmjs.org/@waku/enr/-/enr-0.0.28-5c50ed7.0.tgz", - "integrity": "sha512-Ms4FSw/fvkC63yHlikXCSRNUbxcAph4Os4uveVEnevV96B9k5GIoy5VzkLQbimWEdplgNBTiPzwI47O2AhvDrw==", + "version": "0.0.29-6198efb.0", + "resolved": "https://registry.npmjs.org/@waku/enr/-/enr-0.0.29-6198efb.0.tgz", + "integrity": "sha512-vInEV3d2LV1fakhXIACYn4PBZwYLZNpKYqDMU1YJQ3yoQv6nHl0emeGz7GviyXh+2qyLFjEKzesqhLht41kP/A==", "license": "MIT OR Apache-2.0", "dependencies": { "@ethersproject/rlp": "^5.7.0", @@ -2322,7 +2366,7 @@ "@libp2p/peer-id": "^5.0.1", "@multiformats/multiaddr": "^12.0.0", "@noble/secp256k1": "^1.7.1", - "@waku/utils": "0.0.22-5c50ed7.0", + "@waku/utils": "0.0.23-6198efb.0", "debug": "^4.3.4", "js-sha3": "^0.9.2" }, @@ -2339,21 +2383,21 @@ } }, "node_modules/@waku/interfaces": { - "version": "0.0.29-5c50ed7.0", - "resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.29-5c50ed7.0.tgz", - "integrity": "sha512-yfu9+SYLHpA7z9U1VuYlbhQVb5hBfUnqvAT7cLSbBKW4S13+gHaalZ3SFpZy8hYTqiIA5MTvXysWlSfLv6zIyw==", + "version": "0.0.30-6198efb.0", + "resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.30-6198efb.0.tgz", + "integrity": "sha512-K+DIqbSFct0/1RYg2QdpviXOps9mDo04quW7jvlUvKP9qVBDvDUBlZrvfw+kIHr77g0DzyYLganuLv/Ocd13rw==", "license": "MIT OR Apache-2.0", "dependencies": { - "@waku/proto": "0.0.9-5c50ed7.0" + "@waku/proto": "0.0.10-6198efb.0" }, "engines": { "node": ">=20" } }, "node_modules/@waku/proto": { - "version": "0.0.9-5c50ed7.0", - "resolved": "https://registry.npmjs.org/@waku/proto/-/proto-0.0.9-5c50ed7.0.tgz", - "integrity": "sha512-3QfqbglgWjotrlzrNgtbiXDnxOOP68Bew2OZMhDW8bF7bUdBsq1Y8eih6eJPEcQ/8EP53OQ+gJ0FFv8BTaKLrw==", + "version": "0.0.10-6198efb.0", + "resolved": "https://registry.npmjs.org/@waku/proto/-/proto-0.0.10-6198efb.0.tgz", + "integrity": "sha512-PQktYd1wgQ9Ihdyf+u950ugJL3LUfLh0kEvEHlSLhD9fOsipaegPBR7EsK78cecpZP1hBxuOZwjr5coO3byWlg==", "license": "MIT OR Apache-2.0", "dependencies": { "protons-runtime": "^5.4.0" @@ -2363,19 +2407,24 @@ } }, "node_modules/@waku/rln": { - "version": "0.0.2-5c50ed7.0", - "resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.0.2-5c50ed7.0.tgz", - "integrity": "sha512-8C1OevAJMgZn7FWjqsQIMS5PCgAKlr9Dcs4nyXCvTaHBptkFWzVfhVa7hdz2EpcOtlYD1HqsvDeT17S15m1lmA==", + "version": "0.1.5-6198efb.0", + "resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.1.5-6198efb.0.tgz", + "integrity": "sha512-v/x79fKH6M1E4qjpRWdGErSGt6p7LBLc9tt7G/ZFWJY8afBHbtOH3JrJ0ZqDc4oZsQbRRMGVLJ6WbJIDkQtoYA==", "license": "MIT OR Apache-2.0", "dependencies": { "@chainsafe/bls-keystore": "3.0.0", "@noble/hashes": "^1.2.0", - "@waku/core": "0.0.34-5c50ed7.0", - "@waku/utils": "0.0.22-5c50ed7.0", + "@waku/core": "0.0.35-6198efb.0", + "@waku/utils": "0.0.23-6198efb.0", "@waku/zerokit-rln-wasm": "^0.0.13", + "chai": "^5.1.2", + "chai-as-promised": "^8.0.1", + "chai-spies": "^1.1.0", + "chai-subset": "^1.6.0", "ethereum-cryptography": "^3.1.0", "ethers": "^5.7.2", "lodash": "^4.17.21", + "sinon": "^19.0.2", "uuid": "^11.0.5" }, "engines": { @@ -2383,13 +2432,13 @@ } }, "node_modules/@waku/utils": { - "version": "0.0.22-5c50ed7.0", - "resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.22-5c50ed7.0.tgz", - "integrity": "sha512-hD+XO7lZ86OL9zFEzZvBy6pmgZlmNw7g1xeoui3FOauv6+zL///tarjLrmkPWh+eLJHdalVvTF9D/zkUWbK9SA==", + "version": "0.0.23-6198efb.0", + "resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.23-6198efb.0.tgz", + "integrity": "sha512-K7RNG8ngHYpyVwIHeXaIAi52FO1fJM/h4gurxII331segXrnSXSYtnv3jwO3HDnoTMX+o21ddM0VfyzxilM3NQ==", "license": "MIT OR Apache-2.0", "dependencies": { "@noble/hashes": "^1.3.2", - "@waku/interfaces": "0.0.29-5c50ed7.0", + "@waku/interfaces": "0.0.30-6198efb.0", "chai": "^4.3.10", "debug": "^4.3.4", "uint8arrays": "^5.0.1" @@ -2398,12 +2447,96 @@ "node": ">=20" } }, + "node_modules/@waku/utils/node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@waku/utils/node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@waku/utils/node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@waku/utils/node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@waku/utils/node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/@waku/utils/node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@waku/utils/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/@waku/zerokit-rln-wasm": { "version": "0.0.13", "resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.0.13.tgz", "integrity": "sha512-x7CRIIslmfCmTZc7yVp3dhLlKeLUs8ILIm9kv7+wVJ23H4pPw0Z+uH0ueLIYYfwODI6fDiwJj3S1vdFzM8D1zA==", "license": "MIT or Apache2" }, + "node_modules/abort-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/abort-error/-/abort-error-1.0.1.tgz", + "integrity": "sha512-fxqCblJiIPdSXIUrxI0PL+eJG49QdP9SQ70qtB65MVAoMr2rASlOyAbJFOylfB467F/f+5BCLJJq58RYi7mGfg==", + "license": "Apache-2.0 OR MIT" + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -2692,12 +2825,12 @@ } }, "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "license": "MIT", "engines": { - "node": "*" + "node": ">=12" } }, "node_modules/ast-types-flow": { @@ -2961,19 +3094,50 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", - "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", "license": "MIT", "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.1.0" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" }, + "engines": { + "node": ">=12" + } + }, + "node_modules/chai-as-promised": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-8.0.1.tgz", + "integrity": "sha512-OIEJtOL8xxJSH8JJWbIoRjybbzR52iFuDHuF8eb+nTPD6tgXLjRqsgnUGqQfFODxYvq5QdirT0pN9dZ0+Gz6rA==", + "license": "MIT", + "dependencies": { + "check-error": "^2.0.0" + }, + "peerDependencies": { + "chai": ">= 2.1.2 < 6" + } + }, + "node_modules/chai-spies": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/chai-spies/-/chai-spies-1.1.0.tgz", + "integrity": "sha512-ikaUhQvQWchRYj2K54itFp3nrcxaFRpSDQxDlRzSn9aWgu9Pi7lD8yFxTso4WnQ39+WZ69oB/qOvqp+isJIIWA==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + }, + "peerDependencies": { + "chai": "*" + } + }, + "node_modules/chai-subset": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/chai-subset/-/chai-subset-1.6.0.tgz", + "integrity": "sha512-K3d+KmqdS5XKW5DWPd5sgNffL3uxdDe+6GdnJh3AYPhwnBGRY5urfvfcbRtWIvvpz+KxkL9FeBB6MZewLUNwug==", + "license": "MIT", "engines": { "node": ">=4" } @@ -2996,15 +3160,12 @@ } }, "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.2" - }, "engines": { - "node": "*" + "node": ">= 16" } }, "node_modules/chokidar": { @@ -3227,13 +3388,10 @@ } }, "node_modules/deep-eql": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", - "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "license": "MIT", - "dependencies": { - "type-detect": "^4.0.0" - }, "engines": { "node": ">=6" } @@ -3310,6 +3468,15 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -4522,7 +4689,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5197,12 +5363,12 @@ } }, "node_modules/it-merge": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/it-merge/-/it-merge-3.0.7.tgz", - "integrity": "sha512-4Jzk4ysLW9D6EP6tLCQF253X/ZLO/ELjG9S+Ax79Peo3GQvV4Czux8s3gFS3N/t7/mFhG17ouPvuwY9reFIf+w==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/it-merge/-/it-merge-3.0.9.tgz", + "integrity": "sha512-TjY4WTiwe4ONmaKScNvHDAJj6Tw0UeQFp4JrtC/3Mq7DTyhytes7mnv5OpZV4gItpZcs0AgRntpT2vAy2cnXUw==", "license": "Apache-2.0 OR MIT", "dependencies": { - "it-pushable": "^3.2.3" + "it-queueless-pushable": "^2.0.0" } }, "node_modules/it-peekable": { @@ -5235,6 +5401,17 @@ "p-defer": "^4.0.0" } }, + "node_modules/it-queueless-pushable": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/it-queueless-pushable/-/it-queueless-pushable-2.0.0.tgz", + "integrity": "sha512-MlNnefWT/ntv5fesrHpxwVIu6ZdtlkN0A4aaJiE5wnmPMBv9ttiwX3UEMf78dFwIj5ZNaU9usYXg4swMEpUNJQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "abort-error": "^1.0.1", + "p-defer": "^4.0.1", + "race-signal": "^1.1.3" + } + }, "node_modules/it-reader": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/it-reader/-/it-reader-6.0.4.tgz", @@ -5375,6 +5552,12 @@ "node": ">=4.0" } }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "license": "MIT" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5461,6 +5644,13 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5482,13 +5672,10 @@ } }, "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.1" - } + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "license": "MIT" }, "node_modules/lru-cache": { "version": "10.4.3", @@ -5716,6 +5903,19 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/nise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", + "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.1", + "@sinonjs/text-encoding": "^0.7.3", + "just-extend": "^6.2.0", + "path-to-regexp": "^8.1.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -6030,13 +6230,22 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", "license": "MIT", "engines": { - "node": "*" + "node": ">=16" + } + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "license": "MIT", + "engines": { + "node": ">= 14.16" } }, "node_modules/picocolors": { @@ -6782,6 +6991,24 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/sinon": { + "version": "19.0.5", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.5.tgz", + "integrity": "sha512-r15s9/s+ub/d4bxNXqIUmwp6imVSdTorIRaxoecYjqTVLZ8RuoXr/4EDGwIBo6Waxn7f2gnURX9zuhAfCwaF6Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.5", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "nise": "^6.1.1", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -7096,7 +7323,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -7330,9 +7556,9 @@ } }, "node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "license": "MIT", "engines": { "node": ">=4" diff --git a/examples/keystore-management/package.json b/examples/keystore-management/package.json index be054d1..018c6aa 100644 --- a/examples/keystore-management/package.json +++ b/examples/keystore-management/package.json @@ -10,7 +10,7 @@ "lint": "next lint" }, "dependencies": { - "@waku/rln": "0.0.2-5c50ed7.0", + "@waku/rln": "0.1.5-6198efb.0", "next": "15.1.7", "react": "^19.0.0", "react-dom": "^19.0.0" diff --git a/examples/keystore-management/src/app/layout.tsx b/examples/keystore-management/src/app/layout.tsx index 7c6a855..b9a21d5 100644 --- a/examples/keystore-management/src/app/layout.tsx +++ b/examples/keystore-management/src/app/layout.tsx @@ -4,6 +4,7 @@ import "./globals.css"; import { WalletProvider } from "../contexts/WalletContext"; import { RLNUnifiedProvider } from "../contexts/RLNUnifiedContext2"; import { RLNImplementationProvider } from "../contexts/RLNImplementationContext"; +import { KeystoreProvider } from "../contexts/KeystoreContext"; import { Header } from "../components/Header"; const geistSans = Geist({ @@ -33,14 +34,16 @@ export default function RootLayout({ > - -
-
-
- {children} -
-
-
+ + +
+
+
+ {children} +
+
+
+
diff --git a/examples/keystore-management/src/app/page.tsx b/examples/keystore-management/src/app/page.tsx index c6eb0fa..d4b743f 100644 --- a/examples/keystore-management/src/app/page.tsx +++ b/examples/keystore-management/src/app/page.tsx @@ -1,6 +1,7 @@ import RLNMembershipRegistration from '../components/RLNMembershipRegistration'; import { WalletInfo } from '../components/WalletInfo'; import { RLNImplementationToggle } from '../components/RLNImplementationToggle'; +import KeystoreManager from '../components/KeystoreManager'; export default function Home() { return ( @@ -31,6 +32,16 @@ export default function Home() {

+ + {/* Keystore Management Section */} +
+

Keystore Management

+

+ Export your keystore credentials to use them with your Waku node or import existing credentials. + Keep your keystores safe as they contain your membership information. +

+ +
diff --git a/examples/keystore-management/src/components/KeystoreManager.tsx b/examples/keystore-management/src/components/KeystoreManager.tsx new file mode 100644 index 0000000..2ef1bc8 --- /dev/null +++ b/examples/keystore-management/src/components/KeystoreManager.tsx @@ -0,0 +1,123 @@ +"use client"; + +import { useState } from 'react'; +import { useKeystore } from '../contexts/KeystoreContext'; +import { saveKeystoreToFile, readKeystoreFromFile } from '../utils/fileUtils'; + +export default function KeystoreManager() { + const { + isInitialized: isKeystoreInitialized, + hasStoredCredentials, + storedCredentialsHashes, + exportKeystore, + importKeystore + } = useKeystore(); + + const [error, setError] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + + const handleExport = () => { + try { + const keystoreJson = exportKeystore(); + saveKeystoreToFile(keystoreJson); + setSuccessMessage('Keystore exported successfully'); + setTimeout(() => setSuccessMessage(null), 3000); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to export keystore'); + setTimeout(() => setError(null), 3000); + } + }; + + const handleImport = async () => { + try { + const keystoreJson = await readKeystoreFromFile(); + const success = importKeystore(keystoreJson); + + if (success) { + setSuccessMessage('Keystore imported successfully'); + } else { + setError('Failed to import keystore'); + } + + setTimeout(() => { + setSuccessMessage(null); + setError(null); + }, 3000); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to import keystore'); + setTimeout(() => setError(null), 3000); + } + }; + + if (!isKeystoreInitialized) { + return ( +
+

Initializing keystore...

+
+ ); + } + + return ( +
+

Keystore Management

+ + {/* Status */} +
+

+ Status: {hasStoredCredentials ? 'Credentials found' : 'No credentials stored'} +

+ {hasStoredCredentials && ( +

+ Stored credentials: {storedCredentialsHashes.length} +

+ )} +
+ + {/* Notifications */} + {error && ( +
+

{error}

+
+ )} + + {successMessage && ( +
+

{successMessage}

+
+ )} + + {/* Import/Export Buttons */} +
+ {/* Export Keystore */} +
+ + {!hasStoredCredentials && ( +

+ No credentials to export +

+ )} +
+ + {/* Import Keystore */} +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/examples/keystore-management/src/components/RLNMembershipRegistration.tsx b/examples/keystore-management/src/components/RLNMembershipRegistration.tsx index bbfe1f7..57d2a15 100644 --- a/examples/keystore-management/src/components/RLNMembershipRegistration.tsx +++ b/examples/keystore-management/src/components/RLNMembershipRegistration.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import { useRLN } from '../contexts/RLNUnifiedContext2'; import { useWallet } from '../contexts/WalletContext'; -import { DecryptedCredentials } from '@waku/rln'; +import { KeystoreEntity } from '@waku/rln'; export default function RLNMembershipRegistration() { const { registerMembership, isInitialized, isStarted, rateMinLimit, rateMaxLimit, error, initializeRLN } = useRLN(); @@ -12,12 +12,15 @@ export default function RLNMembershipRegistration() { const [rateLimit, setRateLimit] = useState(rateMinLimit); const [isRegistering, setIsRegistering] = useState(false); const [isInitializing, setIsInitializing] = useState(false); + const [saveToKeystore, setSaveToKeystore] = useState(true); + const [keystorePassword, setKeystorePassword] = useState(''); const [registrationResult, setRegistrationResult] = useState<{ success?: boolean; error?: string; txHash?: string; warning?: string; - credentials?: DecryptedCredentials; + credentials?: KeystoreEntity; + keystoreHash?: string; }>({}); const isLineaSepolia = chainId === 59141; @@ -56,6 +59,15 @@ export default function RLNMembershipRegistration() { return; } + // Validate keystore password if saving to keystore + if (saveToKeystore && keystorePassword.length < 8) { + setRegistrationResult({ + success: false, + error: 'Keystore password must be at least 8 characters long' + }); + return; + } + setIsRegistering(true); setRegistrationResult({}); @@ -65,11 +77,20 @@ export default function RLNMembershipRegistration() { warning: 'Please check your wallet to sign the registration message.' }); - const result = await registerMembership(rateLimit); + // Pass save options if saving to keystore + const saveOptions = saveToKeystore ? { password: keystorePassword } : undefined; + + const result = await registerMembership(rateLimit, saveOptions); + setRegistrationResult({ ...result, credentials: result.credentials }); + + // Clear password field after successful registration + if (result.success) { + setKeystorePassword(''); + } } catch (error) { setRegistrationResult({ success: false, @@ -189,6 +210,50 @@ export default function RLNMembershipRegistration() {

+ {/* Keystore Options */} +
+
+ setSaveToKeystore(e.target.checked)} + className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600" + /> + +
+ + {saveToKeystore && ( +
+ + setKeystorePassword(e.target.value)} + placeholder="Enter password to encrypt your keystore" + className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" + minLength={8} + /> +

+ The password will be used to encrypt your RLN credentials in the keystore. + You will need this password to decrypt your credentials later. +

+
+ )} +
+ {address && (

Registration Details:

@@ -199,9 +264,9 @@ export default function RLNMembershipRegistration() {
)} @@ -315,6 +391,7 @@ export default function RLNMembershipRegistration() {

RLN Initialized: {isInitialized ? "Yes" : "No"}

RLN Started: {isStarted ? "Yes" : "No"}

Min Rate: {rateMinLimit}, Max Rate: {rateMaxLimit}

+

Current Rate Limit: {rateLimit}

); diff --git a/examples/keystore-management/src/contexts/KeystoreContext.tsx b/examples/keystore-management/src/contexts/KeystoreContext.tsx new file mode 100644 index 0000000..98746e4 --- /dev/null +++ b/examples/keystore-management/src/contexts/KeystoreContext.tsx @@ -0,0 +1,160 @@ +"use client"; + +import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import { Keystore, KeystoreEntity } from '@waku/rln'; + +// Define types for the context +interface KeystoreContextType { + keystore: Keystore | null; + isInitialized: boolean; + error: string | null; + hasStoredCredentials: boolean; + storedCredentialsHashes: string[]; + saveCredentials: (credentials: KeystoreEntity, password: string) => Promise; + loadCredential: (hash: string, password: string) => Promise; + exportKeystore: () => string; + importKeystore: (keystoreJson: string) => boolean; + removeCredential: (hash: string) => void; +} + +// Create the context +const KeystoreContext = createContext(undefined); + +// Provider component +export function KeystoreProvider({ children }: { children: ReactNode }) { + const [keystore, setKeystore] = useState(null); + const [isInitialized, setIsInitialized] = useState(false); + const [error, setError] = useState(null); + const [storedCredentialsHashes, setStoredCredentialsHashes] = useState([]); + + // Initialize keystore + useEffect(() => { + try { + const storedKeystore = localStorage.getItem('waku-rln-keystore'); + let keystoreInstance: Keystore; + + if (storedKeystore) { + const loaded = Keystore.fromString(storedKeystore); + if (loaded) { + keystoreInstance = loaded; + } else { + keystoreInstance = Keystore.create(); + } + } else { + keystoreInstance = Keystore.create(); + } + + setKeystore(keystoreInstance); + setStoredCredentialsHashes(keystoreInstance.keys()); + setIsInitialized(true); + } catch (err) { + console.error("Error initializing keystore:", err); + setError(err instanceof Error ? err.message : "Failed to initialize keystore"); + } + }, []); + + // Save keystore to localStorage whenever it changes + useEffect(() => { + if (keystore && isInitialized) { + try { + localStorage.setItem('waku-rln-keystore', keystore.toString()); + } catch (err) { + console.warn("Could not save keystore to localStorage:", err); + } + } + }, [keystore, isInitialized]); + + const saveCredentials = async (credentials: KeystoreEntity, password: string): Promise => { + if (!keystore) { + throw new Error("Keystore not initialized"); + } + + try { + const hash = await keystore.addCredential(credentials, password); + + localStorage.setItem('waku-rln-keystore', keystore.toString()); + + setStoredCredentialsHashes(keystore.keys()); + + return hash; + } catch (err) { + console.error("Error saving credentials:", err); + throw err; + } + }; + + const loadCredential = async (hash: string, password: string): Promise => { + if (!keystore) { + throw new Error("Keystore not initialized"); + } + + try { + return await keystore.readCredential(hash, password); + } catch (err) { + console.error("Error loading credential:", err); + throw err; + } + }; + + const exportKeystore = (): string => { + if (!keystore) { + throw new Error("Keystore not initialized"); + } + + return keystore.toString(); + }; + + const importKeystore = (keystoreJson: string): boolean => { + try { + const imported = Keystore.fromString(keystoreJson); + if (imported) { + setKeystore(imported); + setStoredCredentialsHashes(imported.keys()); + localStorage.setItem('waku-rln-keystore', keystoreJson); + return true; + } + return false; + } catch (err) { + console.error("Error importing keystore:", err); + setError(err instanceof Error ? err.message : "Failed to import keystore"); + return false; + } + }; + + const removeCredential = (hash: string): void => { + if (!keystore) { + throw new Error("Keystore not initialized"); + } + + keystore.removeCredential(hash); + setStoredCredentialsHashes(keystore.keys()); + localStorage.setItem('waku-rln-keystore', keystore.toString()); + }; + + const contextValue: KeystoreContextType = { + keystore, + isInitialized, + error, + hasStoredCredentials: storedCredentialsHashes.length > 0, + storedCredentialsHashes, + saveCredentials, + loadCredential, + exportKeystore, + importKeystore, + removeCredential + }; + + return ( + + {children} + + ); +} + +export function useKeystore() { + const context = useContext(KeystoreContext); + if (context === undefined) { + throw new Error('useKeystore must be used within a KeystoreProvider'); + } + return context; +} \ No newline at end of file diff --git a/examples/keystore-management/src/contexts/RLNFactory.tsx b/examples/keystore-management/src/contexts/RLNFactory.tsx index 6b7c9b8..48a689f 100644 --- a/examples/keystore-management/src/contexts/RLNFactory.tsx +++ b/examples/keystore-management/src/contexts/RLNFactory.tsx @@ -1,6 +1,6 @@ "use client"; -import { createRLN, RLNLightInstance } from '@waku/rln'; +import { createRLN, MembershipInfo, RLNLightInstance } from '@waku/rln'; import { ethers } from 'ethers'; // Define a unified interface that both implementations must support @@ -8,6 +8,18 @@ export interface UnifiedRLNInstance { contract: { address: string; membershipFee?: () => Promise; + getRateLimit: () => number; + getMinRateLimit: () => Promise; + getMaxRateLimit: () => Promise; + getMaxTotalRateLimit: () => Promise; + getCurrentTotalRateLimit: () => Promise; + getRemainingTotalRateLimit: () => Promise; + setRateLimit: (newRateLimit: number) => Promise; + getRemainingMessages: (membershipId: number) => Promise; + getMembershipInfo: (idCommitment: string) => Promise; + extendMembership: (idCommitment: string) => Promise; + eraseMembership: (idCommitment: string, eraseFromMembershipSet?: boolean) => Promise; + registerMembership: (idCommitment: string, rateLimit?: number) => Promise; }; start: (options: { signer: ethers.Signer }) => Promise; // Both implementations use registerMembership but with different parameters diff --git a/examples/keystore-management/src/contexts/RLNLightContext.tsx b/examples/keystore-management/src/contexts/RLNLightContext.tsx index cb08aed..bd83846 100644 --- a/examples/keystore-management/src/contexts/RLNLightContext.tsx +++ b/examples/keystore-management/src/contexts/RLNLightContext.tsx @@ -1,6 +1,6 @@ "use client"; -import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react'; import { DecryptedCredentials, RLNInstance, RLNLightInstance } from '@waku/rln'; import { useWallet } from './WalletContext'; import { ethers } from 'ethers'; @@ -38,8 +38,8 @@ export function RLNProvider({ children }: { children: ReactNode }) { const [isInitialized, setIsInitialized] = useState(false); const [isStarted, setIsStarted] = useState(false); const [error, setError] = useState(null); - const [rateMinLimit, setRateMinLimit] = useState(20); - const [rateMaxLimit, setRateMaxLimit] = useState(600); + const [rateMinLimit, setRateMinLimit] = useState(0); + const [rateMaxLimit, setRateMaxLimit] = useState(0); const ensureLineaSepoliaNetwork = async (): Promise => { try { @@ -88,7 +88,7 @@ export function RLNProvider({ children }: { children: ReactNode }) { } }; - const initializeRLN = async () => { + const initializeRLN = useCallback(async () => { console.log("InitializeRLN called. Connected:", isConnected, "Signer available:", !!signer); try { @@ -105,10 +105,6 @@ export function RLNProvider({ children }: { children: ReactNode }) { setIsInitialized(true); console.log("isInitialized set to true"); - - // Update rate limits to match contract requirements - setRateMinLimit(20); // Contract minimum (RATE_LIMIT_PARAMS.MIN_RATE) - setRateMaxLimit(600); // Contract maximum (RATE_LIMIT_PARAMS.MAX_RATE) } catch (createErr) { console.error("Error creating RLN instance:", createErr); throw createErr; @@ -121,15 +117,21 @@ export function RLNProvider({ children }: { children: ReactNode }) { if (isConnected && signer && rln && !isStarted) { console.log("Starting RLN with signer..."); try { - // Initialize with localKeystore if available (just for reference in localStorage) - const localKeystore = localStorage.getItem("rln-keystore") || ""; - console.log("Local keystore available:", !!localKeystore); - - // Start RLN with signer await rln.start({ signer }); setIsStarted(true); console.log("RLN started successfully, isStarted set to true"); + + const minRate = await rln.contract?.getMinRateLimit(); + const maxRate = await rln.contract?.getMaxRateLimit(); + if (!minRate || !maxRate) { + throw new Error("Failed to get rate limits from contract"); + } + + setRateMinLimit(minRate); + setRateMaxLimit(maxRate); + console.log("Min rate:", minRate); + console.log("Max rate:", maxRate); } catch (startErr) { console.error("Error starting RLN:", startErr); throw startErr; @@ -146,7 +148,7 @@ export function RLNProvider({ children }: { children: ReactNode }) { console.error('Error in initializeRLN:', err); setError(err instanceof Error ? err.message : 'Failed to initialize RLN'); } - }; + }, [isConnected, signer, rln, isStarted]); const registerMembership = async (rateLimit: number) => { console.log("registerMembership called with rate limit:", rateLimit); @@ -167,6 +169,7 @@ export function RLNProvider({ children }: { children: ReactNode }) { error: `Rate limit must be between ${rateMinLimit} and ${rateMaxLimit}` }; } + await rln.contract?.setRateLimit(rateLimit); // Ensure we're on the correct network const isOnLineaSepolia = await ensureLineaSepoliaNetwork(); @@ -258,7 +261,7 @@ export function RLNProvider({ children }: { children: ReactNode }) { } else { console.log("Wallet not connected or no signer available, skipping RLN initialization"); } - }, [isConnected, signer]); + }, [initializeRLN, isConnected, signer]); // Debug log for state changes useEffect(() => { diff --git a/examples/keystore-management/src/contexts/RLNUnifiedContext.tsx b/examples/keystore-management/src/contexts/RLNUnifiedContext.tsx index 2412857..6c96150 100644 --- a/examples/keystore-management/src/contexts/RLNUnifiedContext.tsx +++ b/examples/keystore-management/src/contexts/RLNUnifiedContext.tsx @@ -1,7 +1,7 @@ "use client"; import { ReactNode } from 'react'; -import { RLNProvider as StandardRLNProvider } from './RLNContext'; +import { RLNProvider as StandardRLNProvider } from './RLNZerokitContext'; import { RLNProvider as LightRLNProvider } from './RLNLightContext'; import { useRLNImplementation } from './RLNImplementationContext'; diff --git a/examples/keystore-management/src/contexts/RLNUnifiedContext2.tsx b/examples/keystore-management/src/contexts/RLNUnifiedContext2.tsx index 7419ca4..c7e2141 100644 --- a/examples/keystore-management/src/contexts/RLNUnifiedContext2.tsx +++ b/examples/keystore-management/src/contexts/RLNUnifiedContext2.tsx @@ -6,6 +6,7 @@ import { UnifiedRLNInstance } from './RLNFactory'; import { useRLNImplementation } from './RLNImplementationContext'; import { createRLNImplementation } from './RLNFactory'; import { ethers } from 'ethers'; +import { useKeystore } from './KeystoreContext'; // Constants for RLN membership registration const ERC20_ABI = [ @@ -27,9 +28,17 @@ interface RLNContextType { isStarted: boolean; error: string | null; initializeRLN: () => Promise; - registerMembership: (rateLimit: number) => Promise<{ success: boolean; error?: string; credentials?: KeystoreEntity }>; + registerMembership: (rateLimit: number, saveOptions?: { password: string }) => Promise<{ + success: boolean; + error?: string; + credentials?: KeystoreEntity; + keystoreHash?: string; + }>; rateMinLimit: number; rateMaxLimit: number; + getCurrentRateLimit: () => Promise; + getRateLimitsBounds: () => Promise<{ success: boolean; rateMinLimit: number; rateMaxLimit: number; error?: string }>; + saveCredentialsToKeystore: (credentials: KeystoreEntity, password: string) => Promise; } // Create the context @@ -42,12 +51,14 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) { const [isInitialized, setIsInitialized] = useState(false); const [isStarted, setIsStarted] = useState(false); const [error, setError] = useState(null); - const [rateMinLimit, setRateMinLimit] = useState(20); - const [rateMaxLimit, setRateMaxLimit] = useState(600); // Get the signer from window.ethereum const [signer, setSigner] = useState(null); const [isConnected, setIsConnected] = useState(false); + const [rateMinLimit, setRateMinLimit] = useState(0); + const [rateMaxLimit, setRateMaxLimit] = useState(0); + + const { saveCredentials: saveToKeystore } = useKeystore(); // Listen for wallet connection useEffect(() => { @@ -163,10 +174,6 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) { setIsInitialized(true); console.log("isInitialized set to true"); - - // Update rate limits to match contract requirements - setRateMinLimit(20); // Contract minimum (RATE_LIMIT_PARAMS.MIN_RATE) - setRateMaxLimit(600); // Contract maximum (RATE_LIMIT_PARAMS.MAX_RATE) } catch (createErr) { console.error("Error creating RLN instance:", createErr); throw createErr; @@ -178,16 +185,21 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) { // Start RLN if wallet is connected if (isConnected && signer && rln && !isStarted) { console.log("Starting RLN with signer..."); - try { - // Initialize with localKeystore if available (just for reference in localStorage) - const localKeystore = localStorage.getItem("rln-keystore") || ""; - console.log("Local keystore available:", !!localKeystore); - - // Start RLN with signer + try { await rln.start({ signer }); setIsStarted(true); console.log("RLN started successfully, isStarted set to true"); + + try { + const minLimit = await rln.contract.getMinRateLimit(); + const maxLimit = await rln.contract.getMaxRateLimit(); + setRateMinLimit(minLimit); + setRateMaxLimit(maxLimit); + console.log("Rate limits fetched:", { min: minLimit, max: maxLimit }); + } catch (limitErr) { + console.warn("Could not fetch rate limits:", limitErr); + } } catch (startErr) { console.error("Error starting RLN:", startErr); throw startErr; @@ -206,7 +218,72 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) { } }; - const registerMembership = async (rateLimit: number) => { + const getCurrentRateLimit = async (): Promise => { + try { + if (!rln || !rln.contract || !isStarted) { + console.log("Cannot get rate limit: RLN not initialized or started"); + return null; + } + + const rateLimit = rln.contract.getRateLimit(); + console.log("Current rate limit:", rateLimit); + return rateLimit; + } catch (err) { + console.error("Error getting current rate limit:", err); + return null; + } + }; + + const getRateLimitsBounds = async () => { + try { + if (!rln || !isStarted) { + return { + success: false, + rateMinLimit: 0, + rateMaxLimit: 0, + error: 'RLN not initialized or not started' + }; + } + const minLimit = await rln.contract.getMinRateLimit(); + const maxLimit = await rln.contract.getMaxRateLimit(); + + // Update state + setRateMinLimit(minLimit); + setRateMaxLimit(maxLimit); + + return { + success: true, + rateMinLimit: minLimit, + rateMaxLimit: maxLimit + }; + } catch (error) { + console.error("Error getting rate limits bounds:", error); + return { + success: false, + rateMinLimit: 0, + rateMaxLimit: 0, + error: 'Failed to get rate limits bounds' + }; + } + } + + // Save credentials to keystore + const saveCredentialsToKeystore = async (credentials: KeystoreEntity, password: string): Promise => { + try { + return await saveToKeystore(credentials, password); + } catch (err) { + console.error("Error saving credentials to keystore:", err); + throw err; + } + }; + + // Update registerMembership to optionally save credentials to keystore + const registerMembership = async (rateLimit: number, saveOptions?: { password: string }): Promise<{ + success: boolean; + error?: string; + credentials?: KeystoreEntity; + keystoreHash?: string; + }> => { console.log("registerMembership called with rate limit:", rateLimit); if (!rln || !isStarted) { @@ -218,6 +295,13 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) { } try { + console.log("im here") + const rateMinLimit = await rln.contract.getMinRateLimit(); + const rateMaxLimit = await rln.contract.getMaxRateLimit(); + console.log({ + rateMinLimit, + rateMaxLimit + }) // Validate rate limit if (rateLimit < rateMinLimit || rateLimit > rateMaxLimit) { return { @@ -225,6 +309,8 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) { error: `Rate limit must be between ${rateMinLimit} and ${rateMaxLimit}` }; } + rln.contract.setRateLimit(rateLimit); + console.log("Rate limit set to:", rateLimit); // Ensure we're on the correct network const isOnLineaSepolia = await ensureLineaSepoliaNetwork(); @@ -302,16 +388,21 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) { console.log("Membership registered successfully"); - // Store credentials in localStorage for reference - try { - localStorage.setItem("rln-keystore", JSON.stringify(credentials)); - } catch (storageErr) { - console.warn("Could not store credentials in localStorage:", storageErr); + // If saveOptions provided, save to keystore + let keystoreHash: string | undefined; + if (saveOptions?.password) { + try { + keystoreHash = await saveCredentialsToKeystore(credentials, saveOptions.password); + console.log("Credentials saved to keystore with hash:", keystoreHash); + } catch (keystoreErr) { + console.warn("Could not save credentials to keystore:", keystoreErr); + } } return { success: true, - credentials: credentials + credentials, + keystoreHash }; } catch (registerErr) { console.error("Error registering membership:", registerErr); @@ -337,8 +428,11 @@ export function RLNUnifiedProvider({ children }: { children: ReactNode }) { error, initializeRLN, registerMembership, + getCurrentRateLimit, + getRateLimitsBounds, rateMinLimit, - rateMaxLimit + rateMaxLimit, + saveCredentialsToKeystore, }; return ( @@ -356,3 +450,4 @@ export function useRLN() { } return context; } + diff --git a/examples/keystore-management/src/contexts/RLNUnifiedHook.tsx b/examples/keystore-management/src/contexts/RLNUnifiedHook.tsx index 9d1533a..7449120 100644 --- a/examples/keystore-management/src/contexts/RLNUnifiedHook.tsx +++ b/examples/keystore-management/src/contexts/RLNUnifiedHook.tsx @@ -43,7 +43,7 @@ export function UnifiedRLNProvider({ children }: { children: ReactNode }) { try { if (implementation === 'standard') { // Import the standard RLN hook - const standardModule = await import('./RLNContext'); + const standardModule = await import('./RLNZerokitContext'); const { useRLN: useStandardRLN } = standardModule; // Create a temporary component to access the context diff --git a/examples/keystore-management/src/contexts/RLNContext.tsx b/examples/keystore-management/src/contexts/RLNZerokitContext.tsx similarity index 88% rename from examples/keystore-management/src/contexts/RLNContext.tsx rename to examples/keystore-management/src/contexts/RLNZerokitContext.tsx index 5ffda15..fcec774 100644 --- a/examples/keystore-management/src/contexts/RLNContext.tsx +++ b/examples/keystore-management/src/contexts/RLNZerokitContext.tsx @@ -1,7 +1,7 @@ "use client"; import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; -import { createRLN, DecryptedCredentials, RLNInstance } from '@waku/rln'; +import { createRLN, DecryptedCredentials, LINEA_CONTRACT, RLNInstance } from '@waku/rln'; import { useWallet } from './WalletContext'; import { ethers } from 'ethers'; @@ -13,11 +13,7 @@ const ERC20_ABI = [ "function balanceOf(address account) view returns (uint256)" ]; -// Linea Sepolia configuration -const LINEA_SEPOLIA_CONFIG = { - chainId: 59141, - tokenAddress: '0x185A0015aC462a0aECb81beCc0497b649a64B9ea' -}; + interface RLNContextType { rln: RLNInstance | null; @@ -38,15 +34,15 @@ export function RLNProvider({ children }: { children: ReactNode }) { const [isInitialized, setIsInitialized] = useState(false); const [isStarted, setIsStarted] = useState(false); const [error, setError] = useState(null); - const [rateMinLimit, setRateMinLimit] = useState(20); - const [rateMaxLimit, setRateMaxLimit] = useState(600); + const [rateMinLimit, setRateMinLimit] = useState(0); + const [rateMaxLimit, setRateMaxLimit] = useState(0); const ensureLineaSepoliaNetwork = async (): Promise => { try { console.log("Current network: unknown", await signer?.getChainId()); // Check if already on Linea Sepolia - if (await signer?.getChainId() === LINEA_SEPOLIA_CONFIG.chainId) { + if (await signer?.getChainId() === LINEA_CONTRACT.chainId) { console.log("Already on Linea Sepolia network"); return true; } @@ -73,7 +69,7 @@ export function RLNProvider({ children }: { children: ReactNode }) { // Request network switch await provider.request({ method: 'wallet_switchEthereumChain', - params: [{ chainId: `0x${LINEA_SEPOLIA_CONFIG.chainId.toString(16)}` }], + params: [{ chainId: `0x${LINEA_CONTRACT.chainId.toString(16)}` }], }); console.log("Successfully switched to Linea Sepolia"); @@ -105,10 +101,6 @@ export function RLNProvider({ children }: { children: ReactNode }) { setIsInitialized(true); console.log("isInitialized set to true"); - - // Update rate limits to match contract requirements - setRateMinLimit(20); // Contract minimum (RATE_LIMIT_PARAMS.MIN_RATE) - setRateMaxLimit(600); // Contract maximum (RATE_LIMIT_PARAMS.MAX_RATE) } catch (createErr) { console.error("Error creating RLN instance:", createErr); throw createErr; @@ -117,19 +109,20 @@ export function RLNProvider({ children }: { children: ReactNode }) { console.log("RLN instance already exists, skipping creation"); } - // Start RLN if wallet is connected if (isConnected && signer && rln && !isStarted) { console.log("Starting RLN with signer..."); try { - // Initialize with localKeystore if available (just for reference in localStorage) - const localKeystore = localStorage.getItem("rln-keystore") || ""; - console.log("Local keystore available:", !!localKeystore); - - // Start RLN with signer await rln.start({ signer }); setIsStarted(true); console.log("RLN started successfully, isStarted set to true"); + + const minRate = await rln.contract?.getMinRateLimit(); + const maxRate = await rln.contract?.getMaxRateLimit(); + setRateMinLimit(minRate || 0); + setRateMaxLimit(maxRate || 0); + console.log("Min rate:", minRate); + console.log("Max rate:", maxRate); } catch (startErr) { console.error("Error starting RLN:", startErr); throw startErr; @@ -167,6 +160,7 @@ export function RLNProvider({ children }: { children: ReactNode }) { error: `Rate limit must be between ${rateMinLimit} and ${rateMaxLimit}` }; } + rln.contract?.setRateLimit(rateLimit); // Ensure we're on the correct network const isOnLineaSepolia = await ensureLineaSepoliaNetwork(); @@ -182,7 +176,7 @@ export function RLNProvider({ children }: { children: ReactNode }) { } const contractAddress = rln.contract.address; - const tokenAddress = LINEA_SEPOLIA_CONFIG.tokenAddress; + const tokenAddress = LINEA_CONTRACT.address; // Create token contract instance const tokenContract = new ethers.Contract( diff --git a/examples/keystore-management/src/utils/fileUtils.ts b/examples/keystore-management/src/utils/fileUtils.ts new file mode 100644 index 0000000..dc7d158 --- /dev/null +++ b/examples/keystore-management/src/utils/fileUtils.ts @@ -0,0 +1,62 @@ +/** + * Utility functions for handling keystore file operations + */ + +/** + * Save a keystore JSON string to a file + * @param keystoreJson The keystore JSON as a string + * @param filename Optional filename (defaults to 'waku-rln-keystore.json') + */ +export const saveKeystoreToFile = (keystoreJson: string, filename: string = 'waku-rln-keystore.json'): void => { + const blob = new Blob([keystoreJson], { type: 'application/json' }); + + const url = URL.createObjectURL(blob); + + const link = document.createElement('a'); + link.href = url; + link.download = filename; + + document.body.appendChild(link); + + link.click(); + + URL.revokeObjectURL(url); + document.body.removeChild(link); +}; + +/** + * Read a keystore file and return its content as a string + * @returns Promise resolving to the file content as a string + */ +export const readKeystoreFromFile = (): Promise => { + return new Promise((resolve, reject) => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'application/json,.json'; + + input.onchange = (event) => { + const target = event.target as HTMLInputElement; + const file = target.files?.[0]; + + if (!file) { + reject(new Error('No file selected')); + return; + } + + const reader = new FileReader(); + + reader.onload = () => { + const content = reader.result as string; + resolve(content); + }; + + reader.onerror = () => { + reject(new Error('Failed to read file')); + }; + + reader.readAsText(file); + }; + + input.click(); + }); +}; \ No newline at end of file