chore: improve signatures - bind w1/s1 with delegated keypairs

This commit is contained in:
Danish Arora 2025-09-02 14:27:54 +05:30
parent 5e0622183e
commit 414747f396
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
22 changed files with 645 additions and 394 deletions

106
package-lock.json generated
View File

@ -44,6 +44,7 @@
"@reown/appkit-wallet-button": "^1.7.17",
"@tanstack/react-query": "^5.84.1",
"@waku/sdk": "^0.0.35-67a7287.0",
"bitcoinjs-message": "^2.2.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
@ -64,6 +65,7 @@
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.1.0",
"vaul": "^0.9.3",
"viem": "^2.37.1",
"wagmi": "^2.16.1",
"zod": "^3.23.8"
},
@ -8249,7 +8251,6 @@
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"file-uri-to-path": "1.0.0"
}
@ -8284,7 +8285,6 @@
"resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz",
"integrity": "sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw==",
"license": "MIT",
"optional": true,
"dependencies": {
"safe-buffer": "^5.0.1"
}
@ -8322,7 +8322,6 @@
"resolved": "https://registry.npmjs.org/bitcoinjs-message/-/bitcoinjs-message-2.2.0.tgz",
"integrity": "sha512-103Wy3xg8Y9o+pdhGP4M3/mtQQuUWs6sPuOp1mYphSUoSMHjHTlkj32K4zxU8qMH0Ckv23emfkGlFWtoWZ7YFA==",
"license": "MIT",
"optional": true,
"dependencies": {
"bech32": "^1.1.3",
"bs58check": "^2.1.2",
@ -8340,7 +8339,6 @@
"resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz",
"integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==",
"license": "MIT",
"optional": true,
"dependencies": {
"safe-buffer": "^5.0.1"
}
@ -8349,22 +8347,19 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz",
"integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/bitcoinjs-message/node_modules/bn.js": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/bitcoinjs-message/node_modules/bs58": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
"license": "MIT",
"optional": true,
"dependencies": {
"base-x": "^3.0.2"
}
@ -8374,7 +8369,6 @@
"resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz",
"integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==",
"license": "MIT",
"optional": true,
"dependencies": {
"bs58": "^4.0.0",
"create-hash": "^1.1.0",
@ -8387,7 +8381,6 @@
"integrity": "sha512-tArjQw2P0RTdY7QmkNehgp6TVvQXq6ulIhxv8gaH6YubKG/wxxAoNKcbuXjDhybbc+b2Ihc7e0xxiGN744UIiQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"bindings": "^1.5.0",
"bip66": "^1.1.5",
@ -8447,15 +8440,13 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/browserify-aes": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
"integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
"license": "MIT",
"optional": true,
"dependencies": {
"buffer-xor": "^1.0.3",
"cipher-base": "^1.0.0",
@ -8561,7 +8552,6 @@
"resolved": "https://registry.npmjs.org/buffer-equals/-/buffer-equals-1.0.4.tgz",
"integrity": "sha512-99MsCq0j5+RhubVEtKQgKaD6EM+UP3xJgIvQqwJ3SOLDUekzxMX1ylXBng+Wa2sh7mGT0W6RUly8ojjr1Tt6nA==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
@ -8570,8 +8560,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
"integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/bufferutil": {
"version": "4.0.9",
@ -8808,7 +8797,6 @@
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz",
"integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==",
"license": "MIT",
"optional": true,
"dependencies": {
"inherits": "^2.0.4",
"safe-buffer": "^5.2.1"
@ -9356,7 +9344,6 @@
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
"license": "MIT",
"optional": true,
"dependencies": {
"cipher-base": "^1.0.1",
"inherits": "^2.0.1",
@ -9370,7 +9357,6 @@
"resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
"license": "MIT",
"optional": true,
"dependencies": {
"cipher-base": "^1.0.3",
"create-hash": "^1.1.0",
@ -9827,7 +9813,6 @@
"resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz",
"integrity": "sha512-F4wZ06PvqxYLFEZKkFxTDcns9oFNk34hvmJSEwdzsxVQ8YI5YaxtACgQatkYgv2VI2CFkUd2Y+xosPQnHv809g==",
"license": "MIT",
"optional": true,
"dependencies": {
"browserify-aes": "^1.0.6",
"create-hash": "^1.1.2",
@ -9942,7 +9927,6 @@
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz",
"integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==",
"license": "MIT",
"optional": true,
"dependencies": {
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
@ -9957,8 +9941,7 @@
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/embla-carousel": {
"version": "8.3.0",
@ -10550,7 +10533,6 @@
"resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
"integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
"license": "MIT",
"optional": true,
"dependencies": {
"md5.js": "^1.3.4",
"safe-buffer": "^5.1.1"
@ -10691,8 +10673,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/fill-range": {
"version": "7.1.1",
@ -11086,7 +11067,6 @@
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
"integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==",
"license": "MIT",
"optional": true,
"dependencies": {
"inherits": "^2.0.4",
"readable-stream": "^3.6.0",
@ -11101,7 +11081,6 @@
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"license": "MIT",
"optional": true,
"dependencies": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
@ -11136,7 +11115,6 @@
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
"integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
"license": "MIT",
"optional": true,
"dependencies": {
"hash.js": "^1.0.3",
"minimalistic-assert": "^1.0.0",
@ -12178,7 +12156,6 @@
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
"integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
"license": "MIT",
"optional": true,
"dependencies": {
"hash-base": "^3.0.0",
"inherits": "^2.0.1",
@ -12248,15 +12225,13 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"license": "ISC",
"optional": true
"license": "ISC"
},
"node_modules/minimalistic-crypto-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/minimatch": {
"version": "3.1.2",
@ -12348,8 +12323,7 @@
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz",
"integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==",
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.7",
@ -13749,7 +13723,6 @@
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
"integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
"license": "MIT",
"optional": true,
"dependencies": {
"hash-base": "^3.0.0",
"inherits": "^2.0.1"
@ -15045,9 +15018,9 @@
}
},
"node_modules/viem": {
"version": "2.33.2",
"resolved": "https://registry.npmjs.org/viem/-/viem-2.33.2.tgz",
"integrity": "sha512-/720OaM4dHWs8vXwNpyet+PRERhPaW+n/1UVSCzyb9jkmwwVfaiy/R6YfCFb4v+XXbo8s3Fapa3DM5yCRSkulA==",
"version": "2.37.1",
"resolved": "https://registry.npmjs.org/viem/-/viem-2.37.1.tgz",
"integrity": "sha512-IzacdIXYlOvzDJwNKIVa53LP/LaP70qvBGAIoGH6R+n06S/ru/nnQxLNZ6+JImvIcxwNwgAl0jUA6FZEIQQWSw==",
"funding": [
{
"type": "github",
@ -15056,14 +15029,14 @@
],
"license": "MIT",
"dependencies": {
"@noble/curves": "1.9.2",
"@noble/curves": "1.9.1",
"@noble/hashes": "1.8.0",
"@scure/bip32": "1.7.0",
"@scure/bip39": "1.6.0",
"abitype": "1.0.8",
"isows": "1.0.7",
"ox": "0.8.6",
"ws": "8.18.2"
"ox": "0.9.3",
"ws": "8.18.3"
},
"peerDependencies": {
"typescript": ">=5.0.4"
@ -15074,6 +15047,21 @@
}
}
},
"node_modules/viem/node_modules/@noble/curves": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz",
"integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.8.0"
},
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/viem/node_modules/@scure/bip32": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz",
@ -15108,9 +15096,9 @@
"license": "MIT"
},
"node_modules/viem/node_modules/ox": {
"version": "0.8.6",
"resolved": "https://registry.npmjs.org/ox/-/ox-0.8.6.tgz",
"integrity": "sha512-eiKcgiVVEGDtEpEdFi1EGoVVI48j6icXHce9nFwCNM7CKG3uoCXKdr4TPhS00Iy1TR2aWSF1ltPD0x/YgqIL9w==",
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/ox/-/ox-0.9.3.tgz",
"integrity": "sha512-KzyJP+fPV4uhuuqrTZyok4DC7vFzi7HLUFiUNEmpbyh59htKWkOC98IONC1zgXJPbHAhQgqs6B0Z6StCGhmQvg==",
"funding": [
{
"type": "github",
@ -15121,11 +15109,11 @@
"dependencies": {
"@adraffy/ens-normalize": "^1.11.0",
"@noble/ciphers": "^1.3.0",
"@noble/curves": "^1.9.1",
"@noble/curves": "1.9.1",
"@noble/hashes": "^1.8.0",
"@scure/bip32": "^1.7.0",
"@scure/bip39": "^1.6.0",
"abitype": "^1.0.8",
"abitype": "^1.0.9",
"eventemitter3": "5.0.1"
},
"peerDependencies": {
@ -15137,23 +15125,23 @@
}
}
},
"node_modules/viem/node_modules/ws": {
"version": "8.18.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
"node_modules/viem/node_modules/ox/node_modules/abitype": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.9.tgz",
"integrity": "sha512-oN0S++TQmlwWuB+rkA6aiEefLv3SP+2l/tC5mux/TLj6qdA6rF15Vbpex4fHovLsMkwLwTIRj8/Q8vXCS3GfOg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
"funding": {
"url": "https://github.com/sponsors/wevm"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
"typescript": ">=5.0.4",
"zod": "^3 >=3.22.0"
},
"peerDependenciesMeta": {
"bufferutil": {
"typescript": {
"optional": true
},
"utf-8-validate": {
"zod": {
"optional": true
}
}

View File

@ -51,6 +51,7 @@
"@reown/appkit-wallet-button": "^1.7.17",
"@tanstack/react-query": "^5.84.1",
"@waku/sdk": "^0.0.35-67a7287.0",
"bitcoinjs-message": "^2.2.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
@ -71,6 +72,7 @@
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.1.0",
"vaul": "^0.9.3",
"viem": "^2.37.1",
"wagmi": "^2.16.1",
"zod": "^3.23.8"
},

View File

@ -27,7 +27,7 @@ import { useAppKitAccount, useDisconnect } from '@reown/appkit/react';
import { WalletWizard } from '@/components/ui/wallet-wizard';
const Header = () => {
const { currentUser, verificationStatus, isDelegationValid } = useAuth();
const { currentUser, verificationStatus, getDelegationStatus } = useAuth();
const { isNetworkConnected, isRefreshing } = useForum();
const location = useLocation();
const { toast } = useToast();
@ -95,9 +95,9 @@ const Header = () => {
case 'verified-none':
return 'Read-Only Access';
case 'verified-basic':
return isDelegationValid() ? 'Full Access' : 'Setup Key';
return getDelegationStatus().isValid ? 'Full Access' : 'Setup Key';
case 'verified-owner':
return isDelegationValid() ? 'Premium Access' : 'Setup Key';
return getDelegationStatus().isValid ? 'Premium Access' : 'Setup Key';
default:
return 'Setup Account';
}
@ -112,13 +112,13 @@ const Header = () => {
case 'verified-none':
return <CircleSlash className="w-3 h-3" />;
case 'verified-basic':
return isDelegationValid() ? (
return getDelegationStatus().isValid ? (
<CheckCircle className="w-3 h-3" />
) : (
<Key className="w-3 h-3" />
<CheckCircle className="w-3 h-3" />
);
case 'verified-owner':
return isDelegationValid() ? (
return getDelegationStatus().isValid ? (
<CheckCircle className="w-3 h-3" />
) : (
<Key className="w-3 h-3" />
@ -137,9 +137,9 @@ const Header = () => {
case 'verified-none':
return 'secondary';
case 'verified-basic':
return isDelegationValid() ? 'default' : 'outline';
return getDelegationStatus().isValid ? 'default' : 'outline';
case 'verified-owner':
return isDelegationValid() ? 'default' : 'outline';
return getDelegationStatus().isValid ? 'default' : 'outline';
default:
return 'outline';
}

View File

@ -20,8 +20,7 @@ export function DelegationStep({
const {
currentUser,
delegateKey,
isDelegationValid,
delegationTimeRemaining,
getDelegationStatus,
isAuthenticating,
clearDelegation,
} = useAuth();
@ -162,23 +161,30 @@ export function DelegationStep({
<div className="space-y-3">
{/* Status */}
<div className="flex items-center gap-2">
{isDelegationValid() ? (
{getDelegationStatus().isValid ? (
<CheckCircle className="h-4 w-4 text-green-500" />
) : (
<AlertCircle className="h-4 w-4 text-yellow-500" />
)}
<span
className={`text-sm font-medium ${
isDelegationValid() ? 'text-green-400' : 'text-yellow-400'
getDelegationStatus().isValid
? 'text-green-400'
: 'text-yellow-400'
}`}
>
{isDelegationValid() ? 'Delegated' : 'Required'}
{getDelegationStatus().isValid ? 'Delegated' : 'Required'}
</span>
{isDelegationValid() && (
{getDelegationStatus().isValid && (
<span className="text-xs text-neutral-400">
{Math.floor(delegationTimeRemaining() / (1000 * 60 * 60))}h{' '}
{Math.floor(
(delegationTimeRemaining() % (1000 * 60 * 60)) / (1000 * 60)
(getDelegationStatus().timeRemaining || 0) / (1000 * 60 * 60)
)}
h{' '}
{Math.floor(
((getDelegationStatus().timeRemaining || 0) %
(1000 * 60 * 60)) /
(1000 * 60)
)}
m remaining
</span>
@ -186,7 +192,7 @@ export function DelegationStep({
</div>
{/* Duration Selection */}
{!isDelegationValid() && (
{!getDelegationStatus().isValid && (
<div className="space-y-3">
<label className="text-sm font-medium text-neutral-300">
Delegation Duration:
@ -223,7 +229,7 @@ export function DelegationStep({
)}
{/* Delegated Browser Public Key */}
{isDelegationValid() && currentUser?.browserPubKey && (
{getDelegationStatus().isValid && currentUser?.browserPubKey && (
<div className="text-xs text-neutral-400">
<div className="font-mono break-all bg-neutral-800 p-2 rounded">
{currentUser.browserPubKey}
@ -239,7 +245,7 @@ export function DelegationStep({
)}
{/* Delete Button for Active Delegations */}
{isDelegationValid() && (
{getDelegationStatus().isValid && (
<div className="flex justify-end">
<Button
onClick={clearDelegation}
@ -257,7 +263,7 @@ export function DelegationStep({
{/* Action Buttons */}
<div className="mt-auto space-y-2">
{isDelegationValid() ? (
{getDelegationStatus().isValid ? (
<Button
onClick={handleComplete}
className="w-full bg-green-600 hover:bg-green-700 text-white"

View File

@ -28,7 +28,8 @@ export function WalletWizard({
}: WalletWizardProps) {
const [currentStep, setCurrentStep] = React.useState<WizardStep>(1);
const [isLoading, setIsLoading] = React.useState(false);
const { isAuthenticated, verificationStatus, isDelegationValid } = useAuth();
const { isAuthenticated, verificationStatus, getDelegationStatus } =
useAuth();
const hasInitialized = React.useRef(false);
// Reset wizard when opened and determine starting step
@ -48,7 +49,7 @@ export function WalletWizard({
(verificationStatus === 'verified-owner' ||
verificationStatus === 'verified-basic' ||
verificationStatus === 'verified-none') &&
!isDelegationValid()
!getDelegationStatus().isValid
) {
setCurrentStep(3); // Start at delegation step if verified but no valid delegation
} else {
@ -59,7 +60,7 @@ export function WalletWizard({
} else if (!open) {
hasInitialized.current = false;
}
}, [open, isAuthenticated, verificationStatus, isDelegationValid]);
}, [open, isAuthenticated, verificationStatus, getDelegationStatus]);
const handleStepComplete = (step: WizardStep) => {
if (step < 3) {
@ -100,7 +101,7 @@ export function WalletWizard({
verificationStatus !== 'verified-none')
)
return 'disabled';
if (isDelegationValid()) return 'complete';
if (getDelegationStatus().isValid) return 'complete';
return 'current';
}
return 'disabled';

View File

@ -3,7 +3,11 @@ import { useToast } from '@/components/ui/use-toast';
import { OpchanMessage } from '@/types/forum';
import { User, EVerificationStatus, DisplayPreference } from '@/types/identity';
import { WalletManager } from '@/lib/wallet';
import { DelegationManager, DelegationDuration, DelegationFullStatus } from '@/lib/delegation';
import {
DelegationManager,
DelegationDuration,
DelegationFullStatus,
} from '@/lib/delegation';
import { useAppKitAccount, useDisconnect, modal } from '@reown/appkit/react';
export type VerificationStatus =
@ -25,7 +29,7 @@ interface AuthContextType {
getDelegationStatus: () => DelegationFullStatus;
clearDelegation: () => void;
signMessage: (message: OpchanMessage) => Promise<OpchanMessage | null>;
verifyMessage: (message: OpchanMessage) => boolean;
verifyMessage: (message: OpchanMessage) => Promise<boolean>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
@ -170,7 +174,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
user.address,
user.walletType,
duration,
(message) => walletManager.signMessage(message)
message => walletManager.signMessage(message)
);
} catch (error) {
console.error(
@ -447,7 +451,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
};
const getDelegationStatus = (): DelegationFullStatus => {
return delegationManager.getStatus(currentUser?.address, currentUser?.walletType);
return delegationManager.getStatus(
currentUser?.address,
currentUser?.walletType
);
};
const clearDelegation = (): void => {
@ -477,8 +484,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
): Promise<OpchanMessage | null> => {
return delegationManager.signMessage(message);
},
verifyMessage: (message: OpchanMessage): boolean => {
return delegationManager.verify(message);
verifyMessage: async (message: OpchanMessage): Promise<boolean> => {
return await delegationManager.verify(message);
},
};

View File

@ -105,10 +105,11 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
);
// Transform message cache data to the expected types
const updateStateFromCache = useCallback(() => {
const updateStateFromCache = useCallback(async () => {
// Use the verifyMessage function from delegationManager if available
const verifyFn = isAuthenticated
? (message: OpchanMessage) => delegationManager.verifyMessage(message)
? async (message: OpchanMessage) =>
await delegationManager.verify(message)
: undefined;
// Build user verification status for relevance calculation
@ -162,7 +163,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
relevanceCalculator.buildUserVerificationStatus(allUsers);
// Transform data with relevance calculation (initial pass)
const { cells, posts, comments } = getDataFromCache(
const { cells, posts, comments } = await getDataFromCache(
verifyFn,
initialStatus
);
@ -208,7 +209,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
});
const enrichedStatus =
relevanceCalculator.buildUserVerificationStatus(enrichedUsers);
const transformed = getDataFromCache(verifyFn, enrichedStatus);
const transformed = await getDataFromCache(verifyFn, enrichedStatus);
setCells(transformed.cells);
setPosts(transformed.posts);
setComments(transformed.comments);
@ -220,7 +221,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
setIsRefreshing(true);
try {
// SDS handles message syncing automatically, just update UI
updateStateFromCache();
await updateStateFromCache();
toast({
title: 'Data Refreshed',
description: 'Your view has been updated.',

View File

@ -34,8 +34,9 @@ export const useDelegation = () => {
hasDelegation: status.hasDelegation,
isValid: status.isValid,
timeRemaining: status.timeRemaining,
expiresAt:
status.timeRemaining ? new Date(Date.now() + status.timeRemaining) : undefined,
expiresAt: status.timeRemaining
? new Date(Date.now() + status.timeRemaining)
: undefined,
publicKey: status.publicKey,
address: status.address,
walletType: status.walletType,

View File

@ -12,32 +12,32 @@ export const useMessageSigning = () => {
const {
signMessage: contextSignMessage,
verifyMessage: contextVerifyMessage,
isDelegationValid,
getDelegationStatus,
} = context;
const signMessage = useCallback(
async (message: OpchanMessage): Promise<OpchanMessage | null> => {
// Check if we have a valid delegation before attempting to sign
if (!isDelegationValid()) {
if (!getDelegationStatus().isValid) {
console.warn('No valid delegation found. Cannot sign message.');
return null;
}
return contextSignMessage(message);
},
[contextSignMessage, isDelegationValid]
[contextSignMessage, getDelegationStatus]
);
const verifyMessage = useCallback(
(message: OpchanMessage): boolean => {
return contextVerifyMessage(message);
async (message: OpchanMessage): Promise<boolean> => {
return await contextVerifyMessage(message);
},
[contextVerifyMessage]
);
const canSignMessages = useCallback((): boolean => {
return isDelegationValid();
}, [isDelegationValid]);
return getDelegationStatus().isValid;
}, [getDelegationStatus]);
return {
// Message signing

View File

@ -0,0 +1,117 @@
import * as ed from '@noble/ed25519';
import { sha512 } from '@noble/hashes/sha512';
import { bytesToHex, hexToBytes } from '@/lib/utils';
import { WalletManager } from '@/lib/wallet';
// Set up ed25519 with sha512
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
/**
* Delegation-specific cryptographic utilities
* Handles all cryptographic operations: key generation, signing, verification
*/
export class DelegationCrypto {
/**
* Create a standardized delegation authorization message
* @param browserPublicKey - The browser public key being authorized
* @param walletAddress - The wallet address doing the authorization
* @param expiryTimestamp - When the delegation expires
* @param nonce - Unique nonce for replay protection
* @returns string - The authorization message to be signed
*/
static createAuthMessage(
browserPublicKey: string,
walletAddress: string,
expiryTimestamp: number,
nonce: string
): string {
return `I, ${walletAddress}, authorize browser key ${browserPublicKey} until ${expiryTimestamp} (nonce: ${nonce})`;
}
/**
* Verify a wallet signature using WalletManager
* @param authMessage - The message that was signed
* @param walletSignature - The signature to verify
* @param walletAddress - The wallet address that signed
* @param walletType - The type of wallet
* @returns Promise<boolean> - True if signature is valid
*/
static async verifyWalletSignature(
authMessage: string,
walletSignature: string,
walletAddress: string,
walletType: 'bitcoin' | 'ethereum'
): Promise<boolean> {
try {
return await WalletManager.verifySignature(
authMessage,
walletSignature,
walletAddress,
walletType
);
} catch (error) {
console.error('Error verifying wallet signature:', error);
return false;
}
}
/**
* Generate a new browser-based keypair for signing messages
* @returns Object with public and private keys in hex format
*/
static generateKeypair(): { publicKey: string; privateKey: string } {
const privateKey = ed.utils.randomPrivateKey();
const privateKeyHex = bytesToHex(privateKey);
const publicKey = ed.getPublicKey(privateKey);
const publicKeyHex = bytesToHex(publicKey);
return {
privateKey: privateKeyHex,
publicKey: publicKeyHex,
};
}
/**
* Sign a raw string message using a private key
* @param message - The message to sign
* @param privateKeyHex - The private key in hex format
* @returns The signature in hex format or null if signing fails
*/
static signRaw(message: string, privateKeyHex: string): string | null {
try {
const privateKeyBytes = hexToBytes(privateKeyHex);
const messageBytes = new TextEncoder().encode(message);
const signature = ed.sign(messageBytes, privateKeyBytes);
return bytesToHex(signature);
} catch (error) {
console.error('Error signing with private key:', error);
return null;
}
}
/**
* Verify a signature made with a public key
* @param message - The original message
* @param signature - The signature to verify in hex format
* @param publicKey - The public key in hex format
* @returns True if signature is valid
*/
static verifyRaw(
message: string,
signature: string,
publicKey: string
): boolean {
try {
const messageBytes = new TextEncoder().encode(message);
const signatureBytes = hexToBytes(signature);
const publicKeyBytes = hexToBytes(publicKey);
return ed.verify(signatureBytes, messageBytes, publicKeyBytes);
} catch (error) {
console.error('Error verifying signature:', error);
return false;
}
}
}

View File

@ -1,15 +1,14 @@
import * as ed from '@noble/ed25519';
import { sha512 } from '@noble/hashes/sha512';
import { bytesToHex, hexToBytes } from '@/lib/utils';
import { OpchanMessage } from '@/types/forum';
import { UnsignedMessage } from '@/types/waku';
import { DelegationDuration, DelegationInfo, DelegationStatus } from './types';
import {
DelegationDuration,
DelegationInfo,
DelegationStatus,
DelegationProof,
} from './types';
import { DelegationStorage } from './storage';
import { DelegationCrypto } from './crypto';
// Set up ed25519 with sha512
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
// Enhanced status interface that consolidates all delegation information
export interface DelegationFullStatus extends DelegationStatus {
publicKey?: string;
address?: string;
@ -17,15 +16,11 @@ export interface DelegationFullStatus extends DelegationStatus {
}
export class DelegationManager {
// Duration options in hours
private static readonly DURATION_HOURS = {
'7days': 24 * 7, // 168 hours
'30days': 24 * 30, // 720 hours
'7days': 24 * 7,
'30days': 24 * 30,
} as const;
/**
* Get the number of hours for a given duration
*/
static getDurationHours(duration: DelegationDuration): number {
return DelegationManager.DURATION_HOURS[duration];
}
@ -35,12 +30,7 @@ export class DelegationManager {
// ============================================================================
/**
* Create a complete delegation with a single method call
* @param address - Wallet address to delegate from
* @param walletType - Type of wallet (bitcoin/ethereum)
* @param duration - How long the delegation should last
* @param signFunction - Function to sign the delegation message with the wallet
* @returns Promise<boolean> - Success status
* Create a delegation with cryptographic proof
*/
async delegate(
address: string,
@ -49,29 +39,34 @@ export class DelegationManager {
signFunction: (message: string) => Promise<string>
): Promise<boolean> {
try {
// Generate new keypair
const keypair = this.generateKeypair();
// Create delegation message with expiry
const expiryHours = DelegationManager.getDurationHours(duration);
const expiryTimestamp = Date.now() + expiryHours * 60 * 60 * 1000;
const delegationMessage = this.createDelegationMessage(
// Generate browser keypair
const keypair = DelegationCrypto.generateKeypair();
// Create expiry and nonce
const expiryTimestamp =
Date.now() +
DelegationManager.getDurationHours(duration) * 60 * 60 * 1000;
const nonce = crypto.randomUUID();
// Create and sign authorization message
const authMessage = DelegationCrypto.createAuthMessage(
keypair.publicKey,
address,
expiryTimestamp
);
// Sign the delegation message with wallet
const signature = await signFunction(delegationMessage);
// Create and store the delegation
const delegationInfo: DelegationInfo = {
signature,
expiryTimestamp,
browserPublicKey: keypair.publicKey,
browserPrivateKey: keypair.privateKey,
nonce
);
const walletSignature = await signFunction(authMessage);
// Store delegation
const delegationInfo: DelegationInfo = {
authMessage,
walletSignature,
expiryTimestamp,
walletAddress: address,
walletType,
browserPublicKey: keypair.publicKey,
browserPrivateKey: keypair.privateKey,
nonce,
};
DelegationStorage.store(delegationInfo);
@ -83,117 +78,115 @@ export class DelegationManager {
}
/**
* Get comprehensive delegation status
* @param currentAddress - Optional address to validate against
* @param currentWalletType - Optional wallet type to validate against
* @returns Complete delegation status information
*/
getStatus(
currentAddress?: string,
currentWalletType?: 'bitcoin' | 'ethereum'
): DelegationFullStatus {
const delegation = DelegationStorage.retrieve();
if (!delegation) {
return {
hasDelegation: false,
isValid: false,
};
}
// Check if delegation has expired
const now = Date.now();
const hasExpired = now >= delegation.expiryTimestamp;
// Check address/wallet type matching if provided
const addressMatches = !currentAddress || delegation.walletAddress === currentAddress;
const walletTypeMatches = !currentWalletType || delegation.walletType === currentWalletType;
const isValid = !hasExpired && addressMatches && walletTypeMatches;
const timeRemaining = Math.max(0, delegation.expiryTimestamp - now);
return {
hasDelegation: true,
isValid,
timeRemaining: timeRemaining > 0 ? timeRemaining : undefined,
publicKey: delegation.browserPublicKey,
address: delegation.walletAddress,
walletType: delegation.walletType,
};
}
/**
* Clear the stored delegation
*/
clear(): void {
DelegationStorage.clear();
}
/**
* Sign a message with the delegated browser key
* @param message - Unsigned message to sign
* @returns Signed message or null if delegation invalid
* Sign a message with delegated key
*/
signMessage(message: UnsignedMessage): OpchanMessage | null {
const status = this.getStatus();
if (!status.isValid) {
console.error('No valid key delegation found. Cannot sign message.');
const delegation = DelegationStorage.retrieve();
if (!delegation || Date.now() >= delegation.expiryTimestamp) {
return null;
}
const delegation = DelegationStorage.retrieve();
if (!delegation) return null;
// Create the message content to sign (without signature fields)
// Sign message content
const messageToSign = JSON.stringify({
...message,
signature: undefined,
browserPubKey: undefined,
delegationProof: undefined,
});
const signature = this.signRaw(messageToSign);
const signature = DelegationCrypto.signRaw(
messageToSign,
delegation.browserPrivateKey
);
if (!signature) return null;
return {
...message,
signature,
browserPubKey: delegation.browserPublicKey,
delegationProof: this.createProof(delegation),
} as OpchanMessage;
}
/**
* Verify a message signature
* @param message - Signed message to verify
* @returns True if signature is valid
* Verify a signed message
*/
verify(message: OpchanMessage): boolean {
// Check for required signature fields
if (!message.signature || !message.browserPubKey) {
const messageId = message.id || `${message.type}-${message.timestamp}`;
console.warn('Message is missing signature information', messageId);
async verify(message: OpchanMessage): Promise<boolean> {
// Check required fields
if (
!message.signature ||
!message.browserPubKey ||
!message.delegationProof ||
!message.author
) {
return false;
}
// Reconstruct the original signed content
// Verify message signature
const signedContent = JSON.stringify({
...message,
signature: undefined,
browserPubKey: undefined,
delegationProof: undefined,
});
// Verify the signature
const isValid = this.verifyRaw(
signedContent,
message.signature,
message.browserPubKey
);
if (!isValid) {
const messageId = message.id || `${message.type}-${message.timestamp}`;
console.warn(`Invalid signature for message ${messageId}`);
if (
!DelegationCrypto.verifyRaw(
signedContent,
message.signature,
message.browserPubKey
)
) {
return false;
}
return isValid;
// Verify delegation proof
return await this.verifyProof(
message.delegationProof,
message.browserPubKey,
message.author
);
}
/**
* Get delegation status
*/
getStatus(
currentAddress?: string,
currentWalletType?: 'bitcoin' | 'ethereum'
): DelegationFullStatus {
const delegation = DelegationStorage.retrieve();
if (!delegation) {
return { hasDelegation: false, isValid: false };
}
// Check validity
const now = Date.now();
const hasExpired = now >= delegation.expiryTimestamp;
const addressMatches =
!currentAddress || delegation.walletAddress === currentAddress;
const walletTypeMatches =
!currentWalletType || delegation.walletType === currentWalletType;
const isValid = !hasExpired && addressMatches && walletTypeMatches;
return {
hasDelegation: true,
isValid,
timeRemaining: isValid
? Math.max(0, delegation.expiryTimestamp - now)
: undefined,
publicKey: delegation.browserPublicKey,
address: delegation.walletAddress,
walletType: delegation.walletType,
proof: isValid ? this.createProof(delegation) : undefined,
};
}
/**
* Clear stored delegation
*/
clear(): void {
DelegationStorage.clear();
}
// ============================================================================
@ -201,69 +194,53 @@ export class DelegationManager {
// ============================================================================
/**
* Generate a new browser-based keypair for signing messages
* Create delegation proof from stored info
*/
private generateKeypair(): { publicKey: string; privateKey: string } {
const privateKey = ed.utils.randomPrivateKey();
const privateKeyHex = bytesToHex(privateKey);
const publicKey = ed.getPublicKey(privateKey);
const publicKeyHex = bytesToHex(publicKey);
private createProof(delegation: DelegationInfo): DelegationProof {
return {
privateKey: privateKeyHex,
publicKey: publicKeyHex,
authMessage: delegation.authMessage,
walletSignature: delegation.walletSignature,
expiryTimestamp: delegation.expiryTimestamp,
walletAddress: delegation.walletAddress,
walletType: delegation.walletType,
};
}
/**
* Create a delegation message to be signed by the wallet
* Verify delegation proof
*/
private createDelegationMessage(
browserPublicKey: string,
walletAddress: string,
expiryTimestamp: number
): string {
return `I, ${walletAddress}, delegate authority to this pubkey: ${browserPublicKey} until ${expiryTimestamp}`;
}
/**
* Sign a raw string message using the browser-generated private key
*/
private signRaw(message: string): string | null {
const delegation = DelegationStorage.retrieve();
if (!delegation) return null;
try {
const privateKeyBytes = hexToBytes(delegation.browserPrivateKey);
const messageBytes = new TextEncoder().encode(message);
const signature = ed.sign(messageBytes, privateKeyBytes);
return bytesToHex(signature);
} catch (error) {
console.error('Error signing with browser key:', error);
return null;
}
}
/**
* Verify a signature made with the browser key
*/
private verifyRaw(
message: string,
signature: string,
publicKey: string
): boolean {
try {
const messageBytes = new TextEncoder().encode(message);
const signatureBytes = hexToBytes(signature);
const publicKeyBytes = hexToBytes(publicKey);
return ed.verify(signatureBytes, messageBytes, publicKeyBytes);
} catch (error) {
console.error('Error verifying signature:', error);
private async verifyProof(
proof: DelegationProof,
expectedBrowserKey: string,
expectedWalletAddress: string
): Promise<boolean> {
// Basic validation
if (
!proof?.walletAddress ||
!proof?.authMessage ||
proof?.expiryTimestamp === undefined ||
proof.walletAddress !== expectedWalletAddress ||
Date.now() >= proof.expiryTimestamp
) {
return false;
}
// Verify auth message format
if (
!proof.authMessage.includes(expectedWalletAddress) ||
!proof.authMessage.includes(expectedBrowserKey) ||
!proof.authMessage.includes(proof.expiryTimestamp.toString())
) {
return false;
}
// Verify wallet signature
return await DelegationCrypto.verifyWalletSignature(
proof.authMessage,
proof.walletSignature,
proof.walletAddress,
proof.walletType
);
}
}

View File

@ -8,6 +8,24 @@ export class DelegationStorage {
* Store delegation information in localStorage
*/
static store(delegation: DelegationInfo): void {
console.log('DelegationStorage.store - storing delegation:', {
hasAuthMessage: !!delegation.authMessage,
hasWalletSignature: !!delegation.walletSignature,
hasExpiryTimestamp: delegation.expiryTimestamp !== undefined,
hasWalletAddress: !!delegation.walletAddress,
hasWalletType: !!delegation.walletType,
hasBrowserPublicKey: !!delegation.browserPublicKey,
hasBrowserPrivateKey: !!delegation.browserPrivateKey,
hasNonce: !!delegation.nonce,
authMessage: delegation.authMessage,
walletSignature: delegation.walletSignature,
expiryTimestamp: delegation.expiryTimestamp,
walletAddress: delegation.walletAddress,
walletType: delegation.walletType,
browserPublicKey: delegation.browserPublicKey,
nonce: delegation.nonce,
});
localStorage.setItem(
DelegationStorage.STORAGE_KEY,
JSON.stringify(delegation)
@ -22,7 +40,25 @@ export class DelegationStorage {
if (!delegationJson) return null;
try {
return JSON.parse(delegationJson);
const delegation = JSON.parse(delegationJson);
console.log('DelegationStorage.retrieve - retrieved delegation:', {
hasAuthMessage: !!delegation.authMessage,
hasWalletSignature: !!delegation.walletSignature,
hasExpiryTimestamp: delegation.expiryTimestamp !== undefined,
hasWalletAddress: !!delegation.walletAddress,
hasWalletType: !!delegation.walletType,
hasBrowserPublicKey: !!delegation.browserPublicKey,
hasBrowserPrivateKey: !!delegation.browserPrivateKey,
hasNonce: !!delegation.nonce,
authMessage: delegation.authMessage,
walletSignature: delegation.walletSignature,
expiryTimestamp: delegation.expiryTimestamp,
walletAddress: delegation.walletAddress,
walletType: delegation.walletType,
browserPublicKey: delegation.browserPublicKey,
nonce: delegation.nonce,
});
return delegation;
} catch (e) {
console.error('Failed to parse delegation information', e);
return null;

View File

@ -1,19 +1,31 @@
export type DelegationDuration = '7days' | '30days';
export interface DelegationSignature {
signature: string; // Signature from wallet
/**
* Cryptographic proof that a wallet authorized a browser key
*/
export interface DelegationProof {
authMessage: string; // "I authorize browser key: 0xabc... until 1640995200"
walletSignature: string; // Wallet's signature of authMessage
expiryTimestamp: number; // When this delegation expires
browserPublicKey: string; // Browser-generated public key that was delegated to
walletAddress: string; // Wallet address that signed the delegation
walletType: 'bitcoin' | 'ethereum'; // Type of wallet that created the delegation
}
export interface DelegationInfo extends DelegationSignature {
browserPrivateKey: string;
/**
* Complete delegation information including private key (stored locally)
*/
export interface DelegationInfo extends DelegationProof {
browserPublicKey: string; // Browser-generated public key
browserPrivateKey: string; // Browser-generated private key (never shared)
nonce: string; // Unique nonce to prevent replay attacks
}
/**
* Status of current delegation
*/
export interface DelegationStatus {
hasDelegation: boolean;
isValid: boolean;
timeRemaining?: number;
proof?: DelegationProof; // Include proof for verification
}

View File

@ -91,7 +91,9 @@ export class ForumActions {
}
updateStateFromCache();
const transformedPost = transformPost(result.message! as PostMessage);
const transformedPost = await transformPost(
result.message! as PostMessage
);
if (!transformedPost) {
return {
success: false,
@ -166,7 +168,7 @@ export class ForumActions {
}
updateStateFromCache();
const transformedComment = transformComment(
const transformedComment = await transformComment(
result.message! as CommentMessage
);
if (!transformedComment) {
@ -223,7 +225,9 @@ export class ForumActions {
}
updateStateFromCache();
const transformedCell = transformCell(result.message! as CellMessage);
const transformedCell = await transformCell(
result.message! as CellMessage
);
if (!transformedCell) {
return {
success: false,

View File

@ -13,12 +13,12 @@ import { MessageValidator } from '@/lib/utils/MessageValidator';
// Global validator instance for transformers
const messageValidator = new MessageValidator();
export const transformCell = (
export const transformCell = async (
cellMessage: CellMessage,
_verifyMessage?: unknown, // Deprecated parameter, kept for compatibility
userVerificationStatus?: UserVerificationStatus,
posts?: Post[]
): Cell | null => {
): Promise<Cell | null> => {
// MANDATORY: All messages must have valid signatures
// Since CellMessage extends BaseMessage, it already has required signature fields
// But we still need to verify the signature cryptographically
@ -30,7 +30,8 @@ export const transformCell = (
}
// Verify signature using the message validator's crypto service
const validationReport = messageValidator.getValidationReport(cellMessage);
const validationReport =
await messageValidator.getValidationReport(cellMessage);
if (!validationReport.hasValidSignature) {
console.warn(
`Cell message ${cellMessage.id} failed signature validation:`,
@ -78,11 +79,11 @@ export const transformCell = (
return transformedCell;
};
export const transformPost = (
export const transformPost = async (
postMessage: PostMessage,
_verifyMessage?: unknown, // Deprecated parameter, kept for compatibility
userVerificationStatus?: UserVerificationStatus
): Post | null => {
): Promise<Post | null> => {
// MANDATORY: All messages must have valid signatures
if (!postMessage.signature || !postMessage.browserPubKey) {
console.warn(
@ -92,7 +93,8 @@ export const transformPost = (
}
// Verify signature using the message validator's crypto service
const validationReport = messageValidator.getValidationReport(postMessage);
const validationReport =
await messageValidator.getValidationReport(postMessage);
if (!validationReport.hasValidSignature) {
console.warn(
`Post message ${postMessage.id} failed signature validation:`,
@ -105,23 +107,29 @@ export const transformPost = (
vote => vote.targetId === postMessage.id
);
// MANDATORY: Filter out votes with invalid signatures
const filteredVotes = votes.filter(vote => {
if (!vote.signature || !vote.browserPubKey) {
console.warn(`Vote ${vote.id} missing signature fields`);
return false;
}
const voteValidation = messageValidator.getValidationReport(vote);
if (!voteValidation.hasValidSignature) {
console.warn(
`Vote ${vote.id} failed signature validation:`,
voteValidation.errors
);
return false;
}
return true;
});
const upvotes = filteredVotes.filter(vote => vote.value === 1);
const downvotes = filteredVotes.filter(vote => vote.value === -1);
const filteredVotes = await Promise.all(
votes.map(async vote => {
if (!vote.signature || !vote.browserPubKey) {
console.warn(`Vote ${vote.id} missing signature fields`);
return null;
}
const voteValidation = await messageValidator.getValidationReport(vote);
if (!voteValidation.hasValidSignature) {
console.warn(
`Vote ${vote.id} failed signature validation:`,
voteValidation.errors
);
return null;
}
return vote;
})
).then(votes => votes.filter((vote): vote is VoteMessage => vote !== null));
const upvotes = filteredVotes.filter(
(vote): vote is VoteMessage => vote !== null && vote.value === 1
);
const downvotes = filteredVotes.filter(
(vote): vote is VoteMessage => vote !== null && vote.value === -1
);
const modMsg = messageManager.messageCache.moderations[postMessage.id];
const isPostModerated = !!modMsg && modMsg.targetType === 'post';
@ -171,11 +179,13 @@ export const transformPost = (
const relevanceCalculator = new RelevanceCalculator();
// Get comments for this post
const comments = Object.values(messageManager.messageCache.comments)
.map(comment =>
const comments = await Promise.all(
Object.values(messageManager.messageCache.comments).map(comment =>
transformComment(comment, undefined, userVerificationStatus)
)
.filter(Boolean) as Comment[];
).then(comments =>
comments.filter((comment): comment is Comment => comment !== null)
);
const postComments = comments.filter(
comment => comment.postId === postMessage.id
);
@ -215,11 +225,11 @@ export const transformPost = (
return transformedPost;
};
export const transformComment = (
export const transformComment = async (
commentMessage: CommentMessage,
_verifyMessage?: unknown, // Deprecated parameter, kept for compatibility
userVerificationStatus?: UserVerificationStatus
): Comment | null => {
): Promise<Comment | null> => {
// MANDATORY: All messages must have valid signatures
if (!commentMessage.signature || !commentMessage.browserPubKey) {
console.warn(
@ -229,7 +239,8 @@ export const transformComment = (
}
// Verify signature using the message validator's crypto service
const validationReport = messageValidator.getValidationReport(commentMessage);
const validationReport =
await messageValidator.getValidationReport(commentMessage);
if (!validationReport.hasValidSignature) {
console.warn(
`Comment message ${commentMessage.id} failed signature validation:`,
@ -241,23 +252,29 @@ export const transformComment = (
vote => vote.targetId === commentMessage.id
);
// MANDATORY: Filter out votes with invalid signatures
const filteredVotes = votes.filter(vote => {
if (!vote.signature || !vote.browserPubKey) {
console.warn(`Vote ${vote.id} missing signature fields`);
return false;
}
const voteValidation = messageValidator.getValidationReport(vote);
if (!voteValidation.hasValidSignature) {
console.warn(
`Vote ${vote.id} failed signature validation:`,
voteValidation.errors
);
return false;
}
return true;
});
const upvotes = filteredVotes.filter(vote => vote.value === 1);
const downvotes = filteredVotes.filter(vote => vote.value === -1);
const filteredVotes = await Promise.all(
votes.map(async vote => {
if (!vote.signature || !vote.browserPubKey) {
console.warn(`Vote ${vote.id} missing signature fields`);
return null;
}
const voteValidation = await messageValidator.getValidationReport(vote);
if (!voteValidation.hasValidSignature) {
console.warn(
`Vote ${vote.id} failed signature validation:`,
voteValidation.errors
);
return null;
}
return vote;
})
).then(votes => votes.filter((vote): vote is typeof vote => vote !== null));
const upvotes = filteredVotes.filter(
(vote): vote is VoteMessage => vote !== null && vote.value === 1
);
const downvotes = filteredVotes.filter(
(vote): vote is VoteMessage => vote !== null && vote.value === -1
);
const modMsg = messageManager.messageCache.moderations[commentMessage.id];
const isCommentModerated = !!modMsg && modMsg.targetType === 'comment';
@ -307,7 +324,7 @@ export const transformComment = (
const relevanceResult = relevanceCalculator.calculateCommentScore(
transformedComment,
filteredVotes,
filteredVotes.filter((vote): vote is VoteMessage => vote !== null),
userVerificationStatus
);
@ -321,10 +338,10 @@ export const transformComment = (
return transformedComment;
};
export const transformVote = (
export const transformVote = async (
voteMessage: VoteMessage,
_verifyMessage?: unknown // Deprecated parameter, kept for compatibility
): VoteMessage | null => {
): Promise<VoteMessage | null> => {
// MANDATORY: All messages must have valid signatures
if (!voteMessage.signature || !voteMessage.browserPubKey) {
console.warn(
@ -334,7 +351,8 @@ export const transformVote = (
}
// Verify signature using the message validator's crypto service
const validationReport = messageValidator.getValidationReport(voteMessage);
const validationReport =
await messageValidator.getValidationReport(voteMessage);
if (!validationReport.hasValidSignature) {
console.warn(
`Vote message ${voteMessage.id} failed signature validation:`,
@ -346,24 +364,32 @@ export const transformVote = (
return voteMessage;
};
export const getDataFromCache = (
export const getDataFromCache = async (
_verifyMessage?: unknown, // Deprecated parameter, kept for compatibility
userVerificationStatus?: UserVerificationStatus
) => {
): Promise<{ cells: Cell[]; posts: Post[]; comments: Comment[] }> => {
// First transform posts and comments to get relevance scores
// All validation is now handled internally by the transform functions
const posts = Object.values(messageManager.messageCache.posts)
.map(post => transformPost(post, undefined, userVerificationStatus))
.filter(Boolean) as Post[];
const posts = await Promise.all(
Object.values(messageManager.messageCache.posts).map(post =>
transformPost(post, undefined, userVerificationStatus)
)
).then(posts => posts.filter((post): post is Post => post !== null));
const comments = Object.values(messageManager.messageCache.comments)
.map(c => transformComment(c, undefined, userVerificationStatus))
.filter(Boolean) as Comment[];
const comments = await Promise.all(
Object.values(messageManager.messageCache.comments).map(c =>
transformComment(c, undefined, userVerificationStatus)
)
).then(comments =>
comments.filter((comment): comment is Comment => comment !== null)
);
// Then transform cells with posts for relevance calculation
const cells = Object.values(messageManager.messageCache.cells)
.map(cell => transformCell(cell, undefined, userVerificationStatus, posts))
.filter(Boolean) as Cell[];
const cells = await Promise.all(
Object.values(messageManager.messageCache.cells).map(cell =>
transformCell(cell, undefined, userVerificationStatus, posts)
)
).then(cells => cells.filter((cell): cell is Cell => cell !== null));
return { cells, posts, comments };
};

View File

@ -11,7 +11,7 @@ export interface MessageResult {
export interface MessageServiceInterface {
sendMessage(message: UnsignedMessage): Promise<MessageResult>;
verifyMessage(message: OpchanMessage): boolean;
verifyMessage(message: OpchanMessage): Promise<boolean>;
}
export class MessageService implements MessageServiceInterface {
@ -26,13 +26,12 @@ export class MessageService implements MessageServiceInterface {
*/
async sendMessage(message: UnsignedMessage): Promise<MessageResult> {
try {
const signedMessage =
this.delegationManager.signMessageWithDelegatedKey(message);
const signedMessage = this.delegationManager.signMessage(message);
if (!signedMessage) {
// Check if delegation exists but is expired
const isDelegationExpired =
this.delegationManager.isDelegationValid() === false;
const delegationStatus = this.delegationManager.getStatus();
const isDelegationExpired = !delegationStatus.isValid;
return {
success: false,
@ -81,7 +80,7 @@ export class MessageService implements MessageServiceInterface {
/**
* Verify a message signature
*/
verifyMessage(message: OpchanMessage): boolean {
return this.delegationManager.verifyMessage(message);
async verifyMessage(message: OpchanMessage): Promise<boolean> {
return await this.delegationManager.verify(message);
}
}

View File

@ -27,14 +27,14 @@ export class MessageValidator {
/**
* Validates that a message has required signature fields and valid signature
*/
isValidMessage(message: unknown): message is OpchanMessage {
async isValidMessage(message: unknown): Promise<boolean> {
// Check basic structure
if (!this.hasRequiredFields(message)) {
return false;
}
// Verify signature - we know it's safe to cast here since hasRequiredFields passed
return this.delegationManager.verifyMessage(message as OpchanMessage);
// Verify signature and delegation proof - we know it's safe to cast here since hasRequiredFields passed
return await this.delegationManager.verify(message as OpchanMessage);
}
/**
@ -108,7 +108,7 @@ export class MessageValidator {
/**
* Validates a batch of messages and returns only valid ones
*/
filterValidMessages(messages: unknown[]): OpchanMessage[] {
async filterValidMessages(messages: unknown[]): Promise<OpchanMessage[]> {
const validMessages: OpchanMessage[] = [];
const invalidCount = {
missingFields: 0,
@ -123,7 +123,7 @@ export class MessageValidator {
continue;
}
if (!this.delegationManager.verifyMessage(message as OpchanMessage)) {
if (!(await this.delegationManager.verify(message as OpchanMessage))) {
invalidCount.invalidSignature++;
continue;
}
@ -158,7 +158,7 @@ export class MessageValidator {
/**
* Strict validation that throws errors for invalid messages
*/
validateMessage(message: unknown): OpchanMessage {
async validateMessage(message: unknown): Promise<OpchanMessage> {
if (!this.hasRequiredFields(message)) {
const partialMsg = message as PartialMessage;
throw new Error(
@ -166,7 +166,7 @@ export class MessageValidator {
);
}
if (!this.delegationManager.verifyMessage(message as OpchanMessage)) {
if (!(await this.delegationManager.verify(message as OpchanMessage))) {
const partialMsg = message as PartialMessage;
throw new Error(
`Message validation failed: Invalid signature (messageId: ${partialMsg?.id})`
@ -223,12 +223,12 @@ export class MessageValidator {
/**
* Creates a validation report for debugging
*/
getValidationReport(message: unknown): {
async getValidationReport(message: unknown): Promise<{
isValid: boolean;
hasRequiredFields: boolean;
hasValidSignature: boolean;
errors: string[];
} {
}> {
const errors: string[] = [];
let hasRequiredFields = false;
let hasValidSignature = false;
@ -242,7 +242,7 @@ export class MessageValidator {
}
if (hasRequiredFields) {
hasValidSignature = this.delegationManager.verifyMessage(
hasValidSignature = await this.delegationManager.verify(
message as OpchanMessage
);
if (!hasValidSignature) {
@ -271,11 +271,10 @@ export const messageValidator = new MessageValidator();
/**
* Type guard function for convenient usage
* Note: This is not a true type guard since it's async
*/
export function isValidOpchanMessage(
message: unknown
): message is OpchanMessage {
return messageValidator.isValidMessage(message);
export async function isValidOpchanMessage(message: unknown): Promise<boolean> {
return await messageValidator.isValidMessage(message);
}
/**

View File

@ -4,11 +4,11 @@ import { MessageType } from '../../types/waku';
* Content topics for different message types
*/
export const CONTENT_TOPICS: Record<MessageType, string> = {
[MessageType.CELL]: '/opchan-sds/1/cell/proto',
[MessageType.POST]: '/opchan-sds/1/post/proto',
[MessageType.COMMENT]: '/opchan-sds/1/comment/proto',
[MessageType.VOTE]: '/opchan-sds/1/vote/proto',
[MessageType.MODERATE]: '/opchan-sds/1/moderate/proto',
[MessageType.CELL]: '/opchan-sds-ab/1/cell/proto',
[MessageType.POST]: '/opchan-sds-ab/1/post/proto',
[MessageType.COMMENT]: '/opchan-ab-xyz/1/comment/proto',
[MessageType.VOTE]: '/opchan-sds-ab/1/vote/proto',
[MessageType.MODERATE]: '/opchan-sds-ab/1/moderate/proto',
};
/**

View File

@ -33,8 +33,8 @@ export class CacheService {
this.validator = new MessageValidator();
}
public updateCache(message: unknown): boolean {
if (!this.validator.isValidMessage(message)) {
public async updateCache(message: unknown): Promise<boolean> {
if (!(await this.validator.isValidMessage(message))) {
const partialMsg = message as {
id?: unknown;
type?: unknown;

View File

@ -22,8 +22,8 @@ export class MessageService {
private setupMessageHandling(): void {
if (this.reliableMessaging) {
this.reliableMessaging.onMessage(message => {
const isNew = this.cacheService.updateCache(message);
this.reliableMessaging.onMessage(async message => {
const isNew = await this.cacheService.updateCache(message);
if (isNew) {
this.messageReceivedCallbacks.forEach(callback => callback(message));
}
@ -34,33 +34,43 @@ export class MessageService {
public async sendMessage(
message: OpchanMessage,
statusCallback?: MessageStatusCallback
): Promise<void> {
): Promise<{ success: boolean; message?: OpchanMessage; error?: string }> {
if (!this.reliableMessaging) {
throw new Error('Reliable messaging not initialized');
return { success: false, error: 'Reliable messaging not initialized' };
}
if (!this.nodeManager.isReady) {
throw new Error('Network not ready');
return { success: false, error: 'Network not ready' };
}
// Update cache optimistically
this.cacheService.updateCache(message);
try {
// Update cache optimistically
await this.cacheService.updateCache(message);
// Send via reliable messaging with status tracking
await this.reliableMessaging.sendMessage(message, {
onSent: id => {
console.log(`Message ${id} sent`);
statusCallback?.onSent?.(id);
},
onAcknowledged: id => {
console.log(`Message ${id} acknowledged`);
statusCallback?.onAcknowledged?.(id);
},
onError: (id, error) => {
console.error(`Message ${id} failed:`, error);
statusCallback?.onError?.(id, error);
},
});
// Send via reliable messaging with status tracking
await this.reliableMessaging.sendMessage(message, {
onSent: id => {
console.log(`Message ${id} sent`);
statusCallback?.onSent?.(id);
},
onAcknowledged: id => {
console.log(`Message ${id} acknowledged`);
statusCallback?.onAcknowledged?.(id);
},
onError: (id, error) => {
console.error(`Message ${id} failed:`, error);
statusCallback?.onError?.(id, error);
},
});
return { success: true, message };
} catch (error) {
console.error('Error sending message:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
public onMessageReceived(callback: MessageReceivedCallback): () => void {

View File

@ -1,10 +1,14 @@
import { UseAppKitAccountReturn } from '@reown/appkit/react';
import { AppKit } from '@reown/appkit';
import { getEnsName } from '@wagmi/core';
import {
getEnsName,
verifyMessage as verifyEthereumMessage,
} from '@wagmi/core';
import { ChainNamespace } from '@reown/appkit-common';
import { config } from './config';
import { Provider } from '@reown/appkit-controllers';
import { WalletInfo, ActiveWallet } from './types';
import * as bitcoinMessage from 'bitcoinjs-message';
export class WalletManager {
private static instance: WalletManager | null = null;
@ -167,6 +171,64 @@ export class WalletManager {
}
}
/**
* Verify a message signature against a wallet address
* @param message - The original message that was signed
* @param signature - The signature to verify
* @param walletAddress - The expected signer's address
* @param walletType - The type of wallet (bitcoin/ethereum)
* @returns Promise<boolean> - True if signature is valid
*/
static async verifySignature(
message: string,
signature: string,
walletAddress: string,
walletType: 'bitcoin' | 'ethereum'
): Promise<boolean> {
try {
console.log('WalletManager.verifySignature - verifying signature:', {
message,
signature,
walletAddress,
walletType,
});
if (walletType === 'ethereum') {
return await verifyEthereumMessage(config, {
address: walletAddress as `0x${string}`,
message,
signature: signature as `0x${string}`,
});
} else if (walletType === 'bitcoin') {
console.log(
'WalletManager.verifySignature - verifying bitcoin signature:',
{
message,
walletAddress,
signature,
}
);
const result = bitcoinMessage.verify(message, walletAddress, signature);
console.log(
'WalletManager.verifySignature - bitcoin signature result:',
result
);
return result;
}
console.error(
'WalletManager.verifySignature - unknown wallet type:',
walletType
);
return false;
} catch (error) {
console.error(
'WalletManager.verifySignature - error verifying signature:',
error
);
return false;
}
}
/**
* Get comprehensive wallet info including ENS resolution for Ethereum
*/
@ -208,6 +270,7 @@ export const walletManager = {
hasInstance: WalletManager.hasInstance,
clear: WalletManager.clear,
resolveENS: WalletManager.resolveENS,
verifySignature: WalletManager.verifySignature,
};
export * from './types';

View File

@ -6,6 +6,7 @@ import {
ModerateMessage,
} from '@/types/waku';
import { EVerificationStatus } from './identity';
import { DelegationProof } from '@/lib/delegation/types';
/**
* Union type of all message types
@ -92,6 +93,7 @@ export interface Comment extends CommentMessage {
export interface SignedMessage {
signature: string;
browserPubKey: string;
delegationProof?: DelegationProof; // Cryptographic proof that browser key was authorized
}
/**