From 509faae6c9f558ffeb6e7c0d807c80af704d448f Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Tue, 5 Aug 2025 09:56:32 +0530 Subject: [PATCH] wip: multichain wallet support --- .gitignore | 4 +- package-lock.json | 341 +++++++++++++++++++++++++++- package.json | 2 + src/App.tsx | 47 ++-- src/components/Header.tsx | 188 ++++++++------- src/components/ui/wallet-dialog.tsx | 208 ++++++++++++++--- src/contexts/AuthContext.tsx | 115 +++++----- src/lib/identity/wallets/appkit.ts | 49 ++++ src/lib/identity/wallets/index.ts | 216 ------------------ src/lib/identity/wallets/phantom.ts | 216 ------------------ src/types/global.d.ts | 4 + src/types/index.ts | 15 +- 12 files changed, 766 insertions(+), 639 deletions(-) create mode 100644 src/lib/identity/wallets/appkit.ts delete mode 100644 src/lib/identity/wallets/index.ts delete mode 100644 src/lib/identity/wallets/phantom.ts create mode 100644 src/types/global.d.ts diff --git a/.gitignore b/.gitignore index 6c9f2eb..5eed1ff 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,6 @@ node_modules/ # OS specific # Task files tasks.json -tasks/ \ No newline at end of file +tasks/ + +IMPLEMENTATION_PLAN.md \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9ba3c25..42346f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,9 @@ "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.4", "@reown/appkit": "^1.7.17", + "@reown/appkit-adapter-bitcoin": "^1.7.17", "@reown/appkit-adapter-wagmi": "^1.7.17", + "@reown/appkit-wallet-button": "^1.7.17", "@tanstack/react-query": "^5.84.1", "@waku/sdk": "^0.0.30", "class-variance-authority": "^0.7.1", @@ -1134,6 +1136,56 @@ "@ethersproject/logger": "^5.8.0" } }, + "node_modules/@exodus/bitcoin-wallet-standard": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@exodus/bitcoin-wallet-standard/-/bitcoin-wallet-standard-0.0.0.tgz", + "integrity": "sha512-b/BE2VNLy7CEe3ziOTIjveSBzC4E71OxKIcAyguEQnPizNZ/cC27WQdJhfGeeubOy48+uxCFT1fZq2T53I2mpg==", + "license": "Apache-2.0", + "dependencies": { + "@exodus/bitcoin-wallet-standard-core": "^0.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@exodus/bitcoin-wallet-standard-chains": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@exodus/bitcoin-wallet-standard-chains/-/bitcoin-wallet-standard-chains-0.0.0.tgz", + "integrity": "sha512-D9ddBdkZZCyX4lMLirqdRchrkDhRzlqhAwmKE1IC/N2mndY0Sz2PayJrhpxvS+vlVbU54ZlBGfSwOhPUQekP/w==", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/base": "^1.0.1" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@exodus/bitcoin-wallet-standard-core": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@exodus/bitcoin-wallet-standard-core/-/bitcoin-wallet-standard-core-0.0.0.tgz", + "integrity": "sha512-E9sJ4taqZl72fFU8pUAe15dxf92XzgVTPpPLE5XDHcqz1TkaIUAZ2zbOjvns469vxUj9bDHbBxNAPLlIgohasA==", + "license": "Apache-2.0", + "dependencies": { + "@exodus/bitcoin-wallet-standard-chains": "^0.0.0", + "@exodus/bitcoin-wallet-standard-features": "^0.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@exodus/bitcoin-wallet-standard-features": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@exodus/bitcoin-wallet-standard-features/-/bitcoin-wallet-standard-features-0.0.0.tgz", + "integrity": "sha512-IXt07cl0A+7QuhfB/FiXtGdtTdLN7LyKZjE0ZvU/7msI3D39OsjQMj0qLxLiKj8n+jzAcO2/ULZcrOv5e/jT5A==", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/base": "^1.0.1", + "@wallet-standard/features": "^1.0.3" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@floating-ui/core": { "version": "1.6.8", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", @@ -3576,6 +3628,25 @@ "@reown/appkit-siwx": "1.7.17" } }, + "node_modules/@reown/appkit-adapter-bitcoin": { + "version": "1.7.17", + "resolved": "https://registry.npmjs.org/@reown/appkit-adapter-bitcoin/-/appkit-adapter-bitcoin-1.7.17.tgz", + "integrity": "sha512-X/KCzGee4Y0w9FTn9qU7UGrKsldm1zVmJ4qKJz7xLQSkCD07UG6JoHGyySpJ7j5JzkRnqnQtXeH57IDnJpcN8g==", + "license": "Apache-2.0", + "dependencies": { + "@exodus/bitcoin-wallet-standard": "0.0.0", + "@reown/appkit": "1.7.17", + "@reown/appkit-common": "1.7.17", + "@reown/appkit-controllers": "1.7.17", + "@reown/appkit-polyfills": "1.7.17", + "@reown/appkit-utils": "1.7.17", + "@wallet-standard/app": "1.1.0", + "@wallet-standard/base": "1.1.0", + "@walletconnect/universal-provider": "2.21.5", + "bitcoinjs-lib": "6.1.7", + "sats-connect": "3.5.0" + } + }, "node_modules/@reown/appkit-adapter-wagmi": { "version": "1.7.17", "resolved": "https://registry.npmjs.org/@reown/appkit-adapter-wagmi/-/appkit-adapter-wagmi-1.7.17.tgz", @@ -3861,6 +3932,23 @@ "zod": "3.22.4" } }, + "node_modules/@reown/appkit-wallet-button": { + "version": "1.7.17", + "resolved": "https://registry.npmjs.org/@reown/appkit-wallet-button/-/appkit-wallet-button-1.7.17.tgz", + "integrity": "sha512-4W/Joqyqkwyj3VtGkDgyLvZXwcuwLQpcJ76KuKPIYqRwjv6VvvpN3FLr3C0KXu6hYQ/o9DXCOxe03/SckAu3cw==", + "license": "Apache-2.0", + "dependencies": { + "@reown/appkit-common": "1.7.17", + "@reown/appkit-controllers": "1.7.17", + "@reown/appkit-ui": "1.7.17", + "@reown/appkit-utils": "1.7.17", + "lit": "3.3.0", + "valtio": "2.1.5" + }, + "optionalDependencies": { + "@lit/react": "1.0.8" + } + }, "node_modules/@reown/appkit-wallet/node_modules/zod": { "version": "3.22.4", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", @@ -4123,6 +4211,42 @@ "node": ">=16" } }, + "node_modules/@sats-connect/core": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.6.5.tgz", + "integrity": "sha512-jltPdG3RzY6NPSp/ldoVTu7hOmMiuXr90osTHFPoQzprxcOs/1U4sZt/qAyGhfOM6B+ZXb7DF686BmFkUpPCJA==", + "license": "ISC", + "dependencies": { + "axios": "1.8.4", + "bitcoin-address-validation": "2.2.3", + "buffer": "6.0.3", + "jsontokens": "4.0.1", + "valibot": "0.42.1" + } + }, + "node_modules/@sats-connect/make-default-provider-config": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@sats-connect/make-default-provider-config/-/make-default-provider-config-0.0.10.tgz", + "integrity": "sha512-BBot3Ofa2J7OwXprgYPD4C8dppX4nnPxj4FXWq1H7fDsvwJmW4sAnfmnAIzwmyWZJOR2uZqtTkXAA08sVkoN5g==", + "dependencies": { + "@sats-connect/ui": "^0.0.6", + "bowser": "^2.11.0" + }, + "peerDependencies": { + "@sats-connect/core": "*", + "typescript": "^5.0.0" + } + }, + "node_modules/@sats-connect/make-default-provider-config/node_modules/@sats-connect/ui": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@sats-connect/ui/-/ui-0.0.6.tgz", + "integrity": "sha512-H3bFFhr9CcY1oNosNi/QJszmMHSht4U19bUWfM3vzayAKgV4ebY6iUnRK5g3p2rVLLWVzlpaw1J9m+7JWwyBfA==" + }, + "node_modules/@sats-connect/ui": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@sats-connect/ui/-/ui-0.0.7.tgz", + "integrity": "sha512-dq02JxvTSAkfgFzEz4iWDSamm6Dte1omzxK0F1yytRZbIrbjjz1KmlMHM+uuxnFN9+EzHIHNsA4aS2dEDwh0xw==" + }, "node_modules/@scure/base": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", @@ -5519,6 +5643,18 @@ "node": ">=20" } }, + "node_modules/@wallet-standard/app": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@wallet-standard/app/-/app-1.1.0.tgz", + "integrity": "sha512-3CijvrO9utx598kjr45hTbbeeykQrQfKmSnxeWOgU25TOEpvcipD/bYDQWIqUv1Oc6KK4YStokSMu/FBNecGUQ==", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/base": "^1.1.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@wallet-standard/base": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@wallet-standard/base/-/base-1.1.0.tgz", @@ -5528,6 +5664,18 @@ "node": ">=16" } }, + "node_modules/@wallet-standard/features": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@wallet-standard/features/-/features-1.1.0.tgz", + "integrity": "sha512-hiEivWNztx73s+7iLxsuD1sOJ28xtRix58W7Xnz4XzzA/pF0+aicnWgjOdA10doVDEDZdUuZCIIqG96SFNlDUg==", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/base": "^1.1.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@wallet-standard/wallet": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@wallet-standard/wallet/-/wallet-1.1.0.tgz", @@ -8026,6 +8174,12 @@ "tslib": "^2.0.0" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -8088,6 +8242,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -8100,6 +8265,15 @@ "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", "license": "MIT" }, + "node_modules/base58-js": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/base58-js/-/base58-js-1.0.5.tgz", + "integrity": "sha512-LkkAPP8Zu+c0SVNRTRVDyMfKVORThX+rCViget00xdgLRrKkClCTz1T7cIrpr69ShwV5XJuuoZvMvJ43yURwkA==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -8124,8 +8298,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/big.js": { "version": "6.2.2", @@ -8167,7 +8340,6 @@ "resolved": "https://registry.npmjs.org/bip174/-/bip174-2.1.1.tgz", "integrity": "sha512-mdFV5+/v0XyNYXjBS6CQPLo9ekCx4gtKZFnJm5PMto7Fs9hTTDpkkzOB7/FtluRI6JbUUAu+snTYfJRgHLZbZQ==", "license": "MIT", - "optional": true, "engines": { "node": ">=8.0.0" } @@ -8198,12 +8370,22 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/bitcoin-address-validation": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/bitcoin-address-validation/-/bitcoin-address-validation-2.2.3.tgz", + "integrity": "sha512-1uGCGl26Ye8JG5qcExtFLQfuib6qEZWNDo1ZlLlwp/z7ygUFby3IxolgEfgMGaC+LG9csbVASLcH8fRLv7DIOg==", + "license": "MIT", + "dependencies": { + "base58-js": "^1.0.0", + "bech32": "^2.0.0", + "sha256-uint8array": "^0.10.3" + } + }, "node_modules/bitcoinjs-lib": { "version": "6.1.7", "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.7.tgz", "integrity": "sha512-tlf/r2DGMbF7ky1MgUqXHzypYHakkEnm0SZP23CJKIqNY/5uNAnMbFhMJdhjrL/7anfb/U8+AlpdjPWjPnAalg==", "license": "MIT", - "optional": true, "dependencies": { "@noble/hashes": "^1.2.0", "bech32": "^2.0.0", @@ -8411,7 +8593,6 @@ "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", "license": "MIT", - "optional": true, "dependencies": { "@noble/hashes": "^1.2.0", "bs58": "^5.0.0" @@ -8421,15 +8602,13 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/bs58check/node_modules/bs58": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", "license": "MIT", - "optional": true, "dependencies": { "base-x": "^4.0.0" } @@ -9206,6 +9385,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -9617,6 +9808,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -10021,6 +10221,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-toolkit": { "version": "1.33.0", "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.33.0.tgz", @@ -10640,6 +10855,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -10671,6 +10906,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -11808,6 +12059,17 @@ "dev": true, "license": "MIT" }, + "node_modules/jsontokens": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsontokens/-/jsontokens-4.0.1.tgz", + "integrity": "sha512-+MO415LEN6M+3FGsRz4wU20g7N2JA+2j9d9+pGaNJHviG4L8N0qzavGyENw6fJqsq9CcrHOIL6iWX5yeTZ86+Q==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.2", + "@noble/secp256k1": "^1.6.3", + "base64-js": "^1.5.1" + } + }, "node_modules/keccak": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz", @@ -12544,6 +12806,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -13562,6 +13845,12 @@ "integrity": "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==", "license": "MIT" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -14169,6 +14458,17 @@ "dev": true, "license": "MIT" }, + "node_modules/sats-connect": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/sats-connect/-/sats-connect-3.5.0.tgz", + "integrity": "sha512-/Czx9XcBV57ubAMrII3WaQG4kq7imdgGUOU9IRg0/8AWdjFczOq9wu1i7vCnxENm8MoRLuCfhMIHbekxUn23HA==", + "license": "MIT", + "dependencies": { + "@sats-connect/core": "0.6.5", + "@sats-connect/make-default-provider-config": "0.0.10", + "@sats-connect/ui": "0.0.7" + } + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -14269,6 +14569,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sha256-uint8array": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/sha256-uint8array/-/sha256-uint8array-0.10.7.tgz", + "integrity": "sha512-1Q6JQU4tX9NqsDGodej6pkrUVQVNapLZnvkwIhddH/JqzBZF1fSaxSWNY6sziXBE8aEa2twtGkXUrwzGeZCMpQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -15017,14 +15323,12 @@ "version": "1.18.0", "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/typescript": { "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -15242,6 +15546,20 @@ "uuid": "dist/esm/bin/uuid" } }, + "node_modules/valibot": { + "version": "0.42.1", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.42.1.tgz", + "integrity": "sha512-3keXV29Ar5b//Hqi4MbSdV7lfVp6zuYLZuA9V1PvQUsXqogr+u5lvLPLk3A4f74VUXDnf/JfWMN6sB+koJ/FFw==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/valtio": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/valtio/-/valtio-2.1.5.tgz", @@ -15271,7 +15589,6 @@ "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz", "integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==", "license": "MIT", - "optional": true, "dependencies": { "safe-buffer": "^5.1.1" } diff --git a/package.json b/package.json index 3f5ff52..80e4193 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,9 @@ "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.4", "@reown/appkit": "^1.7.17", + "@reown/appkit-adapter-bitcoin": "^1.7.17", "@reown/appkit-adapter-wagmi": "^1.7.17", + "@reown/appkit-wallet-button": "^1.7.17", "@tanstack/react-query": "^5.84.1", "@waku/sdk": "^0.0.30", "class-variance-authority": "^0.7.1", diff --git a/src/App.tsx b/src/App.tsx index 7cc4bd7..f522a7e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,29 +22,40 @@ import CellPage from "./pages/CellPage"; import PostPage from "./pages/PostPage"; import NotFound from "./pages/NotFound"; import Dashboard from "./pages/Dashboard"; +import { appkitConfig } from "./lib/identity/wallets/appkit"; +import { createAppKit } from "@reown/appkit"; +import { WagmiProvider } from "wagmi"; +import { config } from "./lib/identity/wallets/appkit"; +import { AppKitProvider } from "@reown/appkit/react"; // Create a client const queryClient = new QueryClient(); +createAppKit(appkitConfig); + const App = () => ( - - - - - - - - - } /> - } /> - } /> - } /> - - - - - - + + + + + + + + + + + } /> + } /> + } /> + } /> + + + + + + + + ); export default App; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 26f7ed2..d8da40d 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import { useState } from 'react'; import { Link } from 'react-router-dom'; import { useAuth } from '@/contexts/useAuth'; import { useForum } from '@/contexts/useForum'; @@ -7,14 +7,14 @@ import { Badge } from '@/components/ui/badge'; import { LogOut, Terminal, Wifi, WifiOff, AlertTriangle, CheckCircle, Key, RefreshCw, CircleSlash } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { useToast } from '@/components/ui/use-toast'; +import { useAppKitAccount, useDisconnect } from '@reown/appkit/react'; +import { WalletConnectionDialog } from '@/components/ui/wallet-dialog'; const Header = () => { const { currentUser, isAuthenticated, verificationStatus, - connectWallet, - disconnectWallet, verifyOrdinal, delegateKey, isDelegationValid, @@ -23,13 +23,29 @@ const Header = () => { } = useAuth(); const { isNetworkConnected, isRefreshing } = useForum(); const { toast } = useToast(); + // Use AppKit hooks for multi-chain support + const bitcoinAccount = useAppKitAccount({ namespace: "bip122" }); + const ethereumAccount = useAppKitAccount({ namespace: "eip155" }); + const { disconnect } = useDisconnect(); + + // Determine which account is connected + const isBitcoinConnected = bitcoinAccount.isConnected; + const isEthereumConnected = ethereumAccount.isConnected; + const isConnected = isBitcoinConnected || isEthereumConnected; + const address = isConnected ? (isBitcoinConnected ? bitcoinAccount.address : ethereumAccount.address) : undefined; + + const [walletDialogOpen, setWalletDialogOpen] = useState(false); const handleConnect = async () => { - await connectWallet(); + setWalletDialogOpen(true); }; - const handleDisconnect = () => { - disconnectWallet(); + const handleDisconnect = async () => { + await disconnect(); + toast({ + title: "Wallet Disconnected", + description: "Your wallet has been disconnected successfully.", + }); }; const handleVerify = async () => { @@ -41,7 +57,7 @@ const Header = () => { if (!isWalletAvailable()) { toast({ title: "Wallet Not Available", - description: "Phantom wallet is not installed or not available. Please install Phantom wallet and try again.", + description: "Wallet is not installed or not available. Please install a compatible wallet and try again.", variant: "destructive", }); return; @@ -177,83 +193,91 @@ const Header = () => { }; return ( -
-
-
- - - OpChan - - -
- -
- - - - {isNetworkConnected ? ( - <> - - WAKU: Connected - - ) : ( - <> - - WAKU: Offline - - )} - - - -

{isNetworkConnected ? "Waku network connection active." : "Waku network connection lost."}

- {isRefreshing &&

Refreshing data...

} -
-
+ <> +
+
+
+ + + OpChan + + +
- {!currentUser ? ( - - ) : ( -
- {renderAccessBadge()} - {renderDelegationButton()} - - - - {currentUser.address.slice(0, 5)}...{currentUser.address.slice(-4)} - - - -

{currentUser.address}

-
-
- - - - - Disconnect Wallet - -
- )} +
+ + + + {isNetworkConnected ? ( + <> + + WAKU: Connected + + ) : ( + <> + + WAKU: Offline + + )} + + + +

{isNetworkConnected ? "Waku network connection active." : "Waku network connection lost."}

+ {isRefreshing &&

Refreshing data...

} +
+
+ + {!isConnected ? ( + + ) : ( +
+ {renderAccessBadge()} + {renderDelegationButton()} + + + + {address?.slice(0, 5)}...{address?.slice(-4)} + + + +

{address}

+
+
+ + + + + Disconnect Wallet + +
+ )} +
-
-
+ + + + ); }; diff --git a/src/components/ui/wallet-dialog.tsx b/src/components/ui/wallet-dialog.tsx index 256b701..eed00a1 100644 --- a/src/components/ui/wallet-dialog.tsx +++ b/src/components/ui/wallet-dialog.tsx @@ -8,60 +8,212 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; -import { WalletConnectionStatus } from "@/lib/identity/wallets/phantom"; +import { Badge } from "@/components/ui/badge"; +import { Bitcoin, Coins } from "lucide-react"; +import { + useAppKit, + useAppKitAccount, + useDisconnect, + useAppKitState +} from "@reown/appkit/react"; interface WalletDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - onConnectPhantom: () => void; - onInstallPhantom: () => void; - status: WalletConnectionStatus; - isAuthenticating: boolean; + onConnect: () => void; } export function WalletConnectionDialog({ open, onOpenChange, - onConnectPhantom, - onInstallPhantom, - status, - isAuthenticating, + onConnect, }: WalletDialogProps) { + // Always call hooks to follow React rules + const { initialized } = useAppKitState(); + const appKit = useAppKit(); + const { disconnect } = useDisconnect(); + + // Get account info for different chains + const bitcoinAccount = useAppKitAccount({ namespace: "bip122" }); + const ethereumAccount = useAppKitAccount({ namespace: "eip155" }); + + // Determine which account is connected + const isBitcoinConnected = bitcoinAccount.isConnected; + const isEthereumConnected = ethereumAccount.isConnected; + const isConnected = isBitcoinConnected || isEthereumConnected; + + // Get the active account info + const activeAccount = isBitcoinConnected ? bitcoinAccount : ethereumAccount; + const activeAddress = activeAccount.address; + const activeChain = isBitcoinConnected ? "Bitcoin" : "Ethereum"; + + const handleDisconnect = async () => { + await disconnect(); + onOpenChange(false); + }; + + const handleBitcoinConnect = () => { + if (!initialized || !appKit) { + console.error('AppKit not initialized'); + return; + } + + appKit.open({ + view: "Connect", + namespace: "bip122" + }); + onConnect(); + onOpenChange(false); + }; + + const handleEthereumConnect = () => { + if (!initialized || !appKit) { + console.error('AppKit not initialized'); + return; + } + + appKit.open({ + view: "Connect", + namespace: "eip155" + }); + onConnect(); + onOpenChange(false); + }; + + // Show loading state if AppKit is not initialized + if (!initialized) { + return ( + + + + Connect Wallet + + Initializing wallet connection... + + +
+
+
+
+
+ ); + } + return ( Connect Wallet - {status === WalletConnectionStatus.NotDetected - ? "Phantom wallet not detected. Please install it to continue." - : "Choose a wallet connection method to continue"} + {isConnected + ? `Connected to ${activeChain} with ${activeAddress?.slice(0, 6)}...${activeAddress?.slice(-4)}` + : "Choose a network and wallet to connect to OpChan" + }
- {status !== WalletConnectionStatus.NotDetected ? ( - + {!isConnected ? ( +
+ {/* Bitcoin Section */} +
+
+ +

Bitcoin

+ + Ordinal Verification Required + +
+
+ +
+
+ + {/* Divider */} +
+
+ +
+
+ or +
+
+ + {/* Ethereum Section */} +
+
+ +

Ethereum

+ + ENS Ownership Required + +
+
+ +
+
+
) : ( - +
+
+

Connected Network:

+

{activeChain}

+

Address:

+

+ {activeAddress} +

+
+ + +
)}
-

Phantom wallet is required to use OpChan's features

+

Connect your wallet to use OpChan's features

+ {isConnected && ( +

✓ Wallet connected to {activeChain}

+ )}
); -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index a156b25..00a1015 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -3,6 +3,7 @@ import { useToast } from '@/components/ui/use-toast'; import { User } from '@/types'; import { AuthService, AuthResult } from '@/lib/identity/services/AuthService'; import { OpchanMessage } from '@/types'; +import { useAppKitAccount, useDisconnect } from '@reown/appkit/react'; export type VerificationStatus = 'unverified' | 'verified-none' | 'verified-owner' | 'verifying'; @@ -11,8 +12,6 @@ interface AuthContextType { isAuthenticated: boolean; isAuthenticating: boolean; verificationStatus: VerificationStatus; - connectWallet: () => Promise; - disconnectWallet: () => void; verifyOrdinal: () => Promise; delegateKey: () => Promise; isDelegationValid: () => boolean; @@ -34,76 +33,66 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const [verificationStatus, setVerificationStatus] = useState('unverified'); const { toast } = useToast(); + // Use AppKit hooks for multi-chain support + const bitcoinAccount = useAppKitAccount({ namespace: "bip122" }); + const ethereumAccount = useAppKitAccount({ namespace: "eip155" }); + + // Determine which account is connected + const isBitcoinConnected = bitcoinAccount.isConnected; + const isEthereumConnected = ethereumAccount.isConnected; + const isConnected = isBitcoinConnected || isEthereumConnected; + + // Get the active account info + const activeAccount = isBitcoinConnected ? bitcoinAccount : ethereumAccount; + const address = activeAccount.address; + // Create ref for AuthService so it persists between renders const authServiceRef = useRef(new AuthService()); + // Sync with AppKit wallet state useEffect(() => { - const storedUser = authServiceRef.current.loadStoredUser(); - if (storedUser) { - setCurrentUser(storedUser); + if (isConnected && address) { + // Check if we have a stored user for this address + const storedUser = authServiceRef.current.loadStoredUser(); - if ('ordinalOwnership' in storedUser) { - setVerificationStatus(storedUser.ordinalOwnership ? 'verified-owner' : 'verified-none'); + if (storedUser && storedUser.address === address) { + // Use stored user data + setCurrentUser(storedUser); + if ('ordinalOwnership' in storedUser) { + setVerificationStatus(storedUser.ordinalOwnership ? 'verified-owner' : 'verified-none'); + } else { + setVerificationStatus('unverified'); + } } else { + // Create new user from AppKit wallet + const newUser: User = { + address, + walletType: isBitcoinConnected ? 'bitcoin' : 'ethereum', + ordinalOwnership: false, + delegationExpiry: null, + verificationStatus: 'unverified', + }; + setCurrentUser(newUser); setVerificationStatus('unverified'); - } - } - }, []); - - const connectWallet = async () => { - setIsAuthenticating(true); - try { - const result: AuthResult = await authServiceRef.current.connectWallet(); - - if (!result.success) { + authServiceRef.current.saveUser(newUser); + + const chainName = isBitcoinConnected ? 'Bitcoin' : 'Ethereum'; toast({ - title: "Connection Failed", - description: result.error || "Failed to connect to wallet. Please try again.", - variant: "destructive", + title: "Wallet Connected", + description: `Connected to ${chainName} with address ${address.slice(0, 6)}...${address.slice(-4)}`, + }); + + toast({ + title: "Action Required", + description: "Please verify your Ordinal ownership and delegate a signing key for better UX.", }); - throw new Error(result.error); } - - const newUser = result.user!; - setCurrentUser(newUser); - authServiceRef.current.saveUser(newUser); + } else { + // Wallet disconnected + setCurrentUser(null); setVerificationStatus('unverified'); - - toast({ - title: "Wallet Connected", - description: `Connected with address ${newUser.address.slice(0, 6)}...${newUser.address.slice(-4)}`, - }); - - toast({ - title: "Action Required", - description: "Please verify your Ordinal ownership and delegate a signing key for better UX.", - }); - - } catch (error) { - console.error("Error connecting wallet:", error); - toast({ - title: "Connection Failed", - description: "Failed to connect to wallet. Please try again.", - variant: "destructive", - }); - throw error; - } finally { - setIsAuthenticating(false); } - }; - - const disconnectWallet = () => { - authServiceRef.current.disconnectWallet(); - authServiceRef.current.clearStoredUser(); - - setCurrentUser(null); - setVerificationStatus('unverified'); - - toast({ - title: "Disconnected", - description: "Your wallet has been disconnected.", - }); - }; + }, [isConnected, address, isBitcoinConnected, toast]); const verifyOrdinal = async (): Promise => { if (!currentUser || !currentUser.address) { @@ -222,7 +211,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } else if (error.message.includes("timeout")) { errorMessage = "Wallet request timed out. Please try again and approve the signature promptly."; } else if (error.message.includes("Failed to connect wallet")) { - errorMessage = "Unable to connect to Phantom wallet. Please ensure it's installed and unlocked, then try again."; + errorMessage = "Unable to connect to wallet. Please ensure it's installed and unlocked, then try again."; } else if (error.message.includes("Wallet is not connected")) { errorMessage = "Wallet connection was lost. Please reconnect your wallet and try again."; } else { @@ -251,7 +240,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { }; const isWalletAvailable = (): boolean => { - return authServiceRef.current.getWalletInfo()?.type === 'phantom'; + return isConnected && !!address; }; return ( @@ -261,8 +250,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { isAuthenticated: !!currentUser?.ordinalOwnership, isAuthenticating, verificationStatus, - connectWallet, - disconnectWallet, verifyOrdinal, delegateKey, isDelegationValid, diff --git a/src/lib/identity/wallets/appkit.ts b/src/lib/identity/wallets/appkit.ts new file mode 100644 index 0000000..751ed66 --- /dev/null +++ b/src/lib/identity/wallets/appkit.ts @@ -0,0 +1,49 @@ +import { AppKitOptions } from '@reown/appkit' +import { BitcoinAdapter } from '@reown/appkit-adapter-bitcoin' +import { WagmiAdapter } from '@reown/appkit-adapter-wagmi' +import { createStorage } from 'wagmi' +import { mainnet, bitcoin, AppKitNetwork } from '@reown/appkit/networks' + +const networks: [AppKitNetwork, ...AppKitNetwork[]] = [mainnet, bitcoin] + +const projectId = process.env.VITE_REOWN_SECRET || '2ead96ea166a03e5ab50e5c190532e72' + +if (!projectId) { + throw new Error('VITE_REOWN_SECRET is not defined. Please set it in your .env file') +} + +export const wagmiAdapter = new WagmiAdapter({ + storage: createStorage({ storage: localStorage }), + ssr: false, // Set to false for Vite/React apps + projectId, + networks +}) + +// Export the Wagmi config for the provider +export const config = wagmiAdapter.wagmiConfig + +const bitcoinAdapter = new BitcoinAdapter({ + projectId + +}) + +const metadata = { + name: 'OpChan', + description: 'Decentralized forum powered by Bitcoin Ordinals', + url: process.env.NODE_ENV === 'production' ? 'https://opchan.app' : 'http://localhost:8080', + icons: ['https://opchan.com/logo.png'] +} + +export const appkitConfig: AppKitOptions = { + adapters: [wagmiAdapter, bitcoinAdapter], + networks, + metadata, + projectId, + features: { + analytics: false, + socials: false, + allWallets: false, + }, + enableWalletConnect: false + +} diff --git a/src/lib/identity/wallets/index.ts b/src/lib/identity/wallets/index.ts deleted file mode 100644 index 9899d2f..0000000 --- a/src/lib/identity/wallets/index.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { PhantomWalletAdapter } from './phantom'; -import { KeyDelegation } from '../signatures/key-delegation'; -import { DelegationInfo } from '../signatures/types'; - -export type WalletType = 'phantom'; - -export interface WalletInfo { - address: string; - type: WalletType; - delegated: boolean; - delegationExpiry?: number; -} - -/** - * Service for managing wallet connections and key delegation - */ -export class WalletService { - // Default delegation validity period: 24 hours - private static readonly DEFAULT_DELEGATION_PERIOD = 24 * 60 * 60 * 1000; - - private keyDelegation: KeyDelegation; - private phantomAdapter: PhantomWalletAdapter; - - constructor() { - this.keyDelegation = new KeyDelegation(); - this.phantomAdapter = new PhantomWalletAdapter(); - } - - /** - * Checks if a specific wallet type is available in the browser - */ - public isWalletAvailable(type: WalletType): boolean { - return this.phantomAdapter.isInstalled(); - } - - /** - * Check if wallet is available and can be connected - */ - public async canConnectWallet(type: WalletType = 'phantom'): Promise { - if (!this.isWalletAvailable(type)) { - return false; - } - - try { - const isConnected = await this.phantomAdapter.isConnected(); - return isConnected; - } catch (error) { - console.debug('WalletService: Cannot connect wallet:', error); - return false; - } - } - - /** - * Connect to a specific wallet type - * @param type The wallet type to connect to - * @returns Promise resolving to the wallet's address - */ - public async connectWallet(type: WalletType = 'phantom'): Promise { - return await this.phantomAdapter.connect(); - } - - /** - * Disconnect the current wallet - * @param type The wallet type to disconnect - */ - public async disconnectWallet(type: WalletType): Promise { - this.keyDelegation.clearDelegation(); // Clear any delegation - await this.phantomAdapter.disconnect(); - } - - /** - * Get the current wallet information from local storage - * @returns The current wallet info or null if not connected - */ - public getWalletInfo(): WalletInfo | null { - const userJson = localStorage.getItem('opchan-user'); - if (!userJson) return null; - - try { - const user = JSON.parse(userJson); - const delegation = this.keyDelegation.retrieveDelegation(); - - return { - address: user.address, - type: 'phantom', - delegated: !!delegation && this.keyDelegation.isDelegationValid(), - delegationExpiry: delegation?.expiryTimestamp - }; - } catch (e) { - console.error('Failed to parse user data', e); - return null; - } - } - - /** - * Set up key delegation for the connected wallet - * @param bitcoinAddress The Bitcoin address to delegate from - * @param walletType The wallet type - * @param validityPeriod Milliseconds the delegation should be valid for - * @returns Promise resolving to the delegation info - */ - public async setupKeyDelegation( - bitcoinAddress: string, - walletType: WalletType, - validityPeriod: number = WalletService.DEFAULT_DELEGATION_PERIOD - ): Promise> { - console.debug('WalletService: Starting key delegation for address:', bitcoinAddress); - - let isConnected = await this.phantomAdapter.isConnected(); - console.debug('WalletService: Initial wallet connection check result:', isConnected); - - if (!isConnected) { - console.debug('WalletService: Wallet not connected, attempting to connect automatically'); - try { - await this.phantomAdapter.connect(); - isConnected = await this.phantomAdapter.isConnected(); - console.debug('WalletService: Auto-connection result:', isConnected); - } catch (error) { - console.error('WalletService: Failed to auto-connect wallet:', error); - throw new Error('Failed to connect wallet. Please ensure Phantom wallet is installed and try again.'); - } - } - - if (!isConnected) { - console.error('WalletService: Wallet is still not connected after auto-connection attempt'); - throw new Error('Wallet is not connected. Please connect your wallet first.'); - } - - // Generate browser keypair - const keypair = this.keyDelegation.generateKeypair(); - console.debug('WalletService: Generated browser keypair'); - - // Calculate expiry in hours - const expiryHours = validityPeriod / (60 * 60 * 1000); - - // Create delegation message - const delegationMessage = this.keyDelegation.createDelegationMessage( - keypair.publicKey, - bitcoinAddress, - Date.now() + validityPeriod - ); - console.debug('WalletService: Created delegation message'); - - // Sign the delegation message with the Bitcoin wallet - console.debug('WalletService: Requesting signature from wallet'); - const signature = await this.phantomAdapter.signMessage(delegationMessage); - console.debug('WalletService: Received signature from wallet'); - - // Create and store the delegation - const delegationInfo = this.keyDelegation.createDelegation( - bitcoinAddress, - signature, - keypair.publicKey, - keypair.privateKey, - expiryHours - ); - - this.keyDelegation.storeDelegation(delegationInfo); - console.debug('WalletService: Stored delegation info'); - - // Return delegation info (excluding private key) - return { - signature, - expiryTimestamp: delegationInfo.expiryTimestamp, - browserPublicKey: keypair.publicKey, - bitcoinAddress - }; - } - - /** - * Signs a message using the delegated browser key - * @param message The message to sign - * @returns Promise resolving to the signature or null if no valid delegation - */ - public async signMessage(message: string): Promise { - return this.keyDelegation.signMessage(message); - } - - /** - * Verifies a message signature against a public key - * @param message The original message - * @param signature The signature to verify - * @param publicKey The public key to verify against - * @returns Promise resolving to a boolean indicating if the signature is valid - */ - public async verifySignature( - message: string, - signature: string, - publicKey: string - ): Promise { - return this.keyDelegation.verifySignature(message, signature, publicKey); - } - - /** - * Checks if the current key delegation is valid - * @returns boolean indicating if the delegation is valid - */ - public isDelegationValid(): boolean { - return this.keyDelegation.isDelegationValid(); - } - - /** - * Gets the time remaining on the current delegation - * @returns Time remaining in milliseconds, or 0 if expired/no delegation - */ - public getDelegationTimeRemaining(): number { - return this.keyDelegation.getDelegationTimeRemaining(); - } - - /** - * Clears the stored delegation - */ - public clearDelegation(): void { - this.keyDelegation.clearDelegation(); - } -} \ No newline at end of file diff --git a/src/lib/identity/wallets/phantom.ts b/src/lib/identity/wallets/phantom.ts deleted file mode 100644 index 0f8bd48..0000000 --- a/src/lib/identity/wallets/phantom.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { sha512 } from '@noble/hashes/sha2'; -import * as ed from '@noble/ed25519'; -import { PhantomBitcoinProvider, PhantomWallet, WalletConnectionStatus } from './types'; - -ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m)); - -/** - * PhantomWalletAdapter provides methods for connecting to and interacting with - * the Phantom wallet for Bitcoin operations. - */ -export class PhantomWalletAdapter { - private provider: PhantomWallet | null = null; - private btcProvider: PhantomBitcoinProvider | null = null; - private connectionStatus: WalletConnectionStatus = WalletConnectionStatus.Disconnected; - private currentAccount: string | null = null; - - constructor() { - this.checkWalletAvailability(); - this.restoreConnectionState(); - } - - /** - * Restore connection state from existing wallet connection - */ - private async restoreConnectionState(): Promise { - if (typeof window === 'undefined' || !window?.phantom?.bitcoin) { - console.debug('PhantomWalletAdapter: No wallet available for connection restoration'); - return; - } - - try { - console.debug('PhantomWalletAdapter: Attempting to restore connection state'); - this.provider = window.phantom; - this.btcProvider = window.phantom.bitcoin; - - // Check if wallet is already connected by trying to get accounts - if (this.btcProvider?.requestAccounts) { - const btcAccounts = await this.btcProvider.requestAccounts(); - - if (btcAccounts && btcAccounts.length > 0) { - const ordinalAccount = btcAccounts.find(acc => acc.purpose === 'ordinals'); - const account = ordinalAccount || btcAccounts[0]; - - this.currentAccount = account.address; - this.connectionStatus = WalletConnectionStatus.Connected; - console.debug('PhantomWalletAdapter: Successfully restored connection for account:', account.address); - } else { - console.debug('PhantomWalletAdapter: No accounts found during connection restoration'); - } - } else { - console.debug('PhantomWalletAdapter: requestAccounts method not available'); - } - } catch (error) { - // If we can't restore the connection, that's okay - user will need to reconnect - console.debug('PhantomWalletAdapter: Could not restore existing wallet connection:', error); - this.connectionStatus = WalletConnectionStatus.Disconnected; - } - } - - public getStatus(): WalletConnectionStatus { - return this.connectionStatus; - } - - /** - * Check if the wallet is actually connected by attempting to get accounts - */ - public async isConnected(): Promise { - if (!this.btcProvider) { - return false; - } - - try { - if (this.btcProvider.requestAccounts) { - const accounts = await this.btcProvider.requestAccounts(); - return accounts && accounts.length > 0; - } - return false; - } catch (error) { - console.debug('Error checking wallet connection:', error); - return false; - } - } - - public isInstalled(): boolean { - if (typeof window === 'undefined') { - return false; - } - return !!(window?.phantom?.bitcoin?.isPhantom); - } - - async connect(): Promise { - this.connectionStatus = WalletConnectionStatus.Connecting; - - try { - if (!window?.phantom?.bitcoin) { - this.connectionStatus = WalletConnectionStatus.NotDetected; - return Promise.reject(new Error('Phantom wallet not detected. Please install Phantom wallet.')); - } - - this.provider = window.phantom; - this.btcProvider = window.phantom.bitcoin; - - if (this.btcProvider?.connect) { - await this.btcProvider.connect(); - } - - if (this.btcProvider?.requestAccounts) { - const btcAccounts = await this.btcProvider.requestAccounts(); - - if (!btcAccounts || btcAccounts.length === 0) { - this.connectionStatus = WalletConnectionStatus.Disconnected; - throw new Error('No accounts found'); - } - - const ordinalAccount = btcAccounts.find(acc => acc.purpose === 'ordinals'); - const account = ordinalAccount || btcAccounts[0]; - - this.currentAccount = account.address; - this.connectionStatus = WalletConnectionStatus.Connected; - return account.address; - } else { - throw new Error('requestAccounts method not available on wallet provider'); - } - } catch (error) { - this.connectionStatus = window?.phantom?.bitcoin - ? WalletConnectionStatus.Disconnected - : WalletConnectionStatus.NotDetected; - - throw error; - } - } - - async disconnect(): Promise { - if (this.btcProvider && this.btcProvider.disconnect) { - try { - await this.btcProvider.disconnect(); - } catch (error) { - console.error('Error disconnecting from Phantom wallet:', error); - } - } - - this.provider = null; - this.btcProvider = null; - this.currentAccount = null; - this.connectionStatus = WalletConnectionStatus.Disconnected; - } - - async signMessage(message: string): Promise { - console.debug('PhantomWalletAdapter: signMessage called, btcProvider:', !!this.btcProvider, 'currentAccount:', this.currentAccount); - - if (!this.btcProvider && window?.phantom?.bitcoin) { - console.debug('PhantomWalletAdapter: Attempting to restore connection before signing'); - await this.restoreConnectionState(); - } - - if (!this.btcProvider || !this.currentAccount) { - console.debug('PhantomWalletAdapter: Wallet not connected, attempting to connect automatically'); - try { - await this.connect(); - } catch (error) { - console.error('PhantomWalletAdapter: Failed to auto-connect wallet:', error); - throw new Error('Failed to connect wallet. Please ensure Phantom wallet is installed and try again.'); - } - } - - if (!this.btcProvider) { - console.error('PhantomWalletAdapter: Wallet is not connected - no btcProvider'); - throw new Error('Wallet is not connected'); - } - - if (!this.currentAccount) { - console.error('PhantomWalletAdapter: No active account to sign with'); - throw new Error('No active account to sign with'); - } - - try { - if (!this.btcProvider.signMessage) { - throw new Error('signMessage method not available on wallet provider'); - } - - console.debug('PhantomWalletAdapter: Signing message for account:', this.currentAccount); - const messageBytes = new TextEncoder().encode(message); - - const { signature } = await this.btcProvider.signMessage( - this.currentAccount, - messageBytes - ); - - if (signature instanceof Uint8Array) { - const binString = String.fromCodePoint(...signature); - return btoa(binString); - } - - return String(signature); - } catch (error) { - console.error('PhantomWalletAdapter: Error signing message:', error); - throw error; - } - } - - private checkWalletAvailability(): void { - if (typeof window === 'undefined') { - this.connectionStatus = WalletConnectionStatus.NotDetected; - return; - } - - const isPhantomInstalled = window?.phantom?.bitcoin || window?.phantom; - - if (!isPhantomInstalled) { - this.connectionStatus = WalletConnectionStatus.NotDetected; - } else { - this.connectionStatus = WalletConnectionStatus.Disconnected; - } - } - -} diff --git a/src/types/global.d.ts b/src/types/global.d.ts new file mode 100644 index 0000000..fd8ebf5 --- /dev/null +++ b/src/types/global.d.ts @@ -0,0 +1,4 @@ +import 'react'; + +// Ensures file is treated as a module +export {}; \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 95a2d01..0999a79 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -3,12 +3,23 @@ import { CellMessage, CommentMessage, PostMessage, VoteMessage, ModerateMessage export type OpchanMessage = CellMessage | PostMessage | CommentMessage | VoteMessage | ModerateMessage; export interface User { - address: string; + address: string; + walletType: 'bitcoin' | 'ethereum'; + + // Bitcoin-specific ordinalOwnership?: boolean | { id: string; details: string }; + + // Ethereum-specific + ensName?: string; + ensAvatar?: string; + ensOwnership?: boolean; + + verificationStatus: 'verified' | 'unverified'; + signature?: string; lastChecked?: number; browserPubKey?: string; // Browser-generated public key for key delegation - delegationSignature?: string; // Signature from Bitcoin wallet for delegation + delegationSignature?: string; // Signature from Bitcoin/Ethereum wallet for delegation delegationExpiry?: number; // When the delegation expires }