diff --git a/package-lock.json b/package-lock.json index 784a9510..3ecd65f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "@radix-ui/react-tabs": "^1.1.11", "@radix-ui/react-toast": "^1.2.13", "@radix-ui/react-tooltip": "^1.2.6", + "@scure/btc-signer": "^1.8.0", "@stacks/connect": "^7.10.2", "@stacks/connect-react": "^22.6.2", "@stacks/connect-ui": "^6.6.0", @@ -59,6 +60,7 @@ "react-markdown": "^9.1.0", "recharts": "^2.15.3", "remark-gfm": "^4.0.1", + "sats-connect": "^3.5.0", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "zustand": "^5.0.4" @@ -885,6 +887,33 @@ "node": ">= 10" } }, + "node_modules/@noble/curves": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.0.tgz", + "integrity": "sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/hashes": { "version": "1.1.5", "funding": [ @@ -2153,6 +2182,42 @@ "dev": true, "license": "MIT" }, + "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.1.9", "license": "MIT", @@ -2191,6 +2256,42 @@ "@scure/base": "~1.1.0" } }, + "node_modules/@scure/btc-signer": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@scure/btc-signer/-/btc-signer-1.8.0.tgz", + "integrity": "sha512-lzf9ugp2hZwP84bdRQuxdX2iib3wyUs7+8+Ph/hanVaXWGOZfSfgEZFaOyocj/Qh0Igt1WHkZh6hdh4KloynNQ==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5", + "micro-packed": "~0.7.3" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/btc-signer/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/btc-signer/node_modules/@scure/base": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.5.tgz", + "integrity": "sha512-9rE6EOVeIQzt5TSu4v+K523F8u6DhBsoZWPGKlnCshhlDhy0kJzUX4V+tr2dWmzF1GdekvThABoEQBGBQI7xZw==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sinclair/typebox": { "version": "0.25.24", "dev": true, @@ -4521,6 +4622,12 @@ "license": "MIT", "peer": true }, + "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/available-typed-arrays": { "version": "1.0.7", "dev": true, @@ -4543,6 +4650,17 @@ "node": ">=4" } }, + "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/axobject-query": { "version": "4.1.0", "dev": true, @@ -4567,6 +4685,15 @@ "version": "4.0.0", "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", "funding": [ @@ -4585,6 +4712,12 @@ ], "license": "MIT" }, + "node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", + "license": "MIT" + }, "node_modules/bin-links": { "version": "5.0.0", "dev": true, @@ -4619,12 +4752,29 @@ "file-uri-to-path": "1.0.0" } }, + "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/blake3-wasm": { "version": "2.1.5", "dev": true, "license": "MIT", "peer": true }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.11", "dev": true, @@ -4651,6 +4801,30 @@ "base-x": "^4.0.0" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-crc32": { "version": "0.2.13", "dev": true, @@ -4708,7 +4882,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.1", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4940,6 +5113,18 @@ "color-support": "bin.js" } }, + "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/comma-separated-tokens": { "version": "2.0.3", "license": "MIT", @@ -5286,6 +5471,15 @@ "license": "MIT", "peer": true }, + "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/delegates": { "version": "1.0.0", "dev": true, @@ -5372,7 +5566,6 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -5531,7 +5724,6 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5539,7 +5731,6 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5579,7 +5770,6 @@ }, "node_modules/es-object-atoms": { "version": "1.0.0", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -5592,7 +5782,6 @@ "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==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6341,6 +6530,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "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.3", "dev": true, @@ -6363,6 +6572,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "dev": true, @@ -6552,7 +6776,6 @@ }, "node_modules/get-intrinsic": { "version": "1.2.6", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -6719,7 +6942,6 @@ }, "node_modules/gopd": { "version": "1.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6797,7 +7019,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6808,7 +7029,6 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -6931,6 +7151,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "dev": true, @@ -7724,7 +7964,6 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8010,6 +8249,27 @@ "node": ">= 8.0.0" } }, + "node_modules/micro-packed": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/micro-packed/-/micro-packed-0.7.3.tgz", + "integrity": "sha512-2Milxs+WNC00TRlem41oRswvw31146GiSaoCT7s3Xi2gMUglW5QBeqlQaZeHr5tJx9nm3i57LNXPqxOOaWtTYg==", + "license": "MIT", + "dependencies": { + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/micro-packed/node_modules/@scure/base": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.5.tgz", + "integrity": "sha512-9rE6EOVeIQzt5TSu4v+K523F8u6DhBsoZWPGKlnCshhlDhy0kJzUX4V+tr2dWmzF1GdekvThABoEQBGBQI7xZw==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/micro/node_modules/arg": { "version": "4.1.0", "dev": true, @@ -8546,6 +8806,27 @@ "node": ">=10.0.0" } }, + "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/mimic-fn": { "version": "2.1.0", "dev": true, @@ -9534,6 +9815,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "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.2", "dev": true, @@ -10175,6 +10462,17 @@ "license": "MIT", "peer": true }, + "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/scheduler": { "version": "0.23.2", "license": "MIT", @@ -10255,6 +10553,12 @@ "license": "ISC", "peer": true }, + "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", "license": "MIT", @@ -11205,7 +11509,6 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -11444,6 +11747,20 @@ "license": "MIT", "peer": true }, + "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/varuint-bitcoin": { "version": "1.1.2", "license": "MIT", diff --git a/package.json b/package.json index 1325c88d..99c0397b 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@radix-ui/react-tabs": "^1.1.11", "@radix-ui/react-toast": "^1.2.13", "@radix-ui/react-tooltip": "^1.2.6", + "@scure/btc-signer": "^1.8.0", "@stacks/connect": "^7.10.2", "@stacks/connect-react": "^22.6.2", "@stacks/connect-ui": "^6.6.0", @@ -63,6 +64,7 @@ "react-markdown": "^9.1.0", "recharts": "^2.15.3", "remark-gfm": "^4.0.1", + "sats-connect": "^3.5.0", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "zustand": "^5.0.4" diff --git a/src/app/deposit/page.tsx b/src/app/deposit/page.tsx new file mode 100644 index 00000000..39e58c8d --- /dev/null +++ b/src/app/deposit/page.tsx @@ -0,0 +1,10 @@ +import BitcoinDeposit from "@/components/btc-deposit"; + +const page = () => { + return ( +
+ +
+ ); +}; +export default page; diff --git a/src/components/btc-deposit/DepositForm.tsx b/src/components/btc-deposit/DepositForm.tsx new file mode 100644 index 00000000..84c419b6 --- /dev/null +++ b/src/components/btc-deposit/DepositForm.tsx @@ -0,0 +1,711 @@ +"use client"; + +import { useState, type ChangeEvent, useEffect } from "react"; +import { getStacksAddress, getBitcoinAddress } from "@/lib/address"; +import { styxSDK } from "@faktoryfun/styx-sdk"; +import type { + FeeEstimates, + PoolStatus, + TransactionPrepareParams, + TransactionPriority, + UTXO, +} from "@faktoryfun/styx-sdk"; +import { MIN_DEPOSIT_SATS, MAX_DEPOSIT_SATS } from "@faktoryfun/styx-sdk"; +import { useToast } from "@/hooks/use-toast"; +import { Bitcoin, Loader2 } from "lucide-react"; +import AuthButton from "@/components/home/auth-button"; +import { useSessionStore } from "@/store/session"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Card, CardContent } from "@/components/ui/card"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { useQuery } from "@tanstack/react-query"; + +interface DepositFormProps { + btcUsdPrice: number | null; + poolStatus: PoolStatus | null; + setConfirmationData: (data: ConfirmationData) => void; + setShowConfirmation: (show: boolean) => void; + activeWalletProvider: "leather" | "xverse" | null; +} + +export interface ConfirmationData { + depositAmount: string; + depositAddress: string; + stxAddress: string; + opReturnHex: string; + isBlaze?: boolean; +} + +export default function DepositForm({ + btcUsdPrice, + poolStatus, + setConfirmationData, + setShowConfirmation, + activeWalletProvider, +}: DepositFormProps) { + const [amount, setAmount] = useState("0.0001"); + const [selectedPreset, setSelectedPreset] = useState(null); + const { toast } = useToast(); + // const [useBlazeSubnet, setUseBlazeSubnet] = useState(false); + const [feeEstimates, setFeeEstimates] = useState<{ + low: { rate: number; fee: number; time: string }; + medium: { rate: number; fee: number; time: string }; + high: { rate: number; fee: number; time: string }; + }>({ + low: { rate: 1, fee: 0, time: "30 min" }, + medium: { rate: 3, fee: 0, time: "~20 min" }, + high: { rate: 5, fee: 0, time: "~10 min" }, + }); + + // Get session state from Zustand store + const { accessToken, isLoading, initialize } = useSessionStore(); + + // Initialize session on component mount + useEffect(() => { + initialize(); + }, [initialize]); + + // Use the activeWalletProvider state with a default value + // const [activeWalletProvider, setActiveWalletProvider] = useState< + // "leather" | "xverse" | null + // >(null); + + // Set the wallet provider based on the session when initialized + // useEffect(() => { + // if (accessToken) { + // // Determine which wallet is being used based on available information + // // This is a placeholder - implement your actual wallet detection logic here + // const detectedProvider = localStorage.getItem("walletProvider") as + // | "leather" + // | "xverse" + // | null; + // setActiveWalletProvider(detectedProvider); + // } + // }, [accessToken]); + + // Get addresses from the lib - only if we have a session + const userAddress = accessToken ? getStacksAddress() : null; + const btcAddress = accessToken ? getBitcoinAddress() : null; + + // Fetch BTC balance using React Query with 40-minute cache + const { data: btcBalance, isLoading: isBalanceLoading } = useQuery< + number | null + >({ + queryKey: ["btcBalance", btcAddress], + queryFn: async () => { + if (!btcAddress) return null; + + const blockstreamUrl = `https://blockstream.info/api/address/${btcAddress}/utxo`; + const response = await fetch(blockstreamUrl); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const utxos = await response.json(); + const totalSats = utxos.reduce( + (sum: number, utxo: UTXO) => sum + utxo.value, + 0 + ); + return totalSats / 100000000; // Convert satoshis to BTC + }, + enabled: !!btcAddress, // Only run query when btcAddress is available + staleTime: 40 * 60 * 1000, // 40 minutes in milliseconds + retry: 2, + refetchOnWindowFocus: false, + }); + + // Fetch fee estimates from mempool.space + const fetchMempoolFeeEstimates = async (): Promise<{ + low: { rate: number; fee: number; time: string }; + medium: { rate: number; fee: number; time: string }; + high: { rate: number; fee: number; time: string }; + }> => { + try { + console.log("Fetching fee estimates directly from mempool.space"); + const response = await fetch( + "https://mempool.space/api/v1/fees/recommended" + ); + const data = await response.json(); + + // Log the raw values to help with debugging + console.log("Raw mempool.space fee data:", data); + + // Map to the correct fee estimate fields + const lowRate = data.hourFee; + const mediumRate = data.halfHourFee; + const highRate = data.fastestFee; + + // Don't modify the rates, use them as-is + return { + low: { + rate: lowRate, + fee: Math.round(lowRate * 148), + time: "~1 hour", + }, + medium: { + rate: mediumRate, + fee: Math.round(mediumRate * 148), + time: "~30 min", + }, + high: { + rate: highRate, + fee: Math.round(highRate * 148), + time: "~10 min", + }, + }; + } catch (error) { + console.error("Error fetching fee estimates from mempool.space:", error); + // Fallback to default values that better reflect current network conditions + return { + low: { rate: 3, fee: 444, time: "~1 hour" }, + medium: { rate: 3, fee: 444, time: "~30 min" }, + high: { rate: 5, fee: 740, time: "~10 min" }, + }; + } + }; + + // Fetch fee estimates on component mount + useEffect(() => { + const getFeeEstimates = async () => { + try { + const estimates = await fetchMempoolFeeEstimates(); + setFeeEstimates(estimates); + } catch (error) { + console.error("Error fetching initial fee estimates:", error); + } + }; + + getFeeEstimates(); + }, []); + + const formatUsdValue = (amount: number): string => { + if (!amount || amount <= 0) return "$0.00"; + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(amount); + }; + + const calculateUsdValue = (btcAmount: string): number => { + if (!btcAmount || !btcUsdPrice) return 0; + const numAmount = Number.parseFloat(btcAmount); + return isNaN(numAmount) ? 0 : numAmount * btcUsdPrice; + }; + + const calculateFee = (btcAmount: string): string => { + if (!btcAmount || Number.parseFloat(btcAmount) <= 0) return "0.00000000"; + const numAmount = Number.parseFloat(btcAmount); + if (isNaN(numAmount)) return "0.00003000"; + + return numAmount <= 0.002 ? "0.00003000" : "0.00006000"; + }; + + const handleAmountChange = (e: ChangeEvent): void => { + const value = e.target.value; + if (value === "" || /^\d*\.?\d*$/.test(value)) { + setAmount(value); + setSelectedPreset(null); + } + }; + + const handlePresetClick = (presetAmount: string): void => { + setAmount(presetAmount); + setSelectedPreset(presetAmount); + }; + + const handleMaxClick = async (): Promise => { + if (btcBalance !== null && btcBalance !== undefined) { + try { + const feeRates = await styxSDK.getFeeEstimates(); + const selectedRate = feeRates.medium; + const estimatedSize = 1 * 70 + 2 * 33 + 12; + const networkFeeSats = estimatedSize * selectedRate; + const networkFee = networkFeeSats / 100000000; + const maxAmount = Math.max(0, btcBalance - networkFee); + const formattedMaxAmount = maxAmount.toFixed(8); + + setAmount(formattedMaxAmount); + setSelectedPreset("max"); + } catch (error) { + console.error("Error calculating max amount:", error); + const networkFee = 0.000006; + const maxAmount = Math.max(0, btcBalance - networkFee); + setAmount(maxAmount.toFixed(8)); + setSelectedPreset("max"); + } + } else { + toast({ + title: "Balance not available", + description: + "Your BTC balance is not available. Please try again later.", + variant: "destructive", + }); + } + }; + + const handleDepositConfirm = async (): Promise => { + if (!amount || Number.parseFloat(amount) <= 0) { + toast({ + title: "Invalid amount", + description: "Please enter a valid BTC amount greater than 0", + variant: "destructive", + }); + return; + } + + if (!accessToken || !userAddress) { + toast({ + title: "Not connected", + description: "Please connect your wallet first", + variant: "destructive", + }); + return; + } + + try { + if (!btcAddress) { + throw new Error("No Bitcoin address found in your wallet"); + } + + // IMPORTANT: Calculate the total amount including service fee + const userInputAmount = Number.parseFloat(amount); + const serviceFee = Number.parseFloat(calculateFee(amount)); + const totalAmount = (userInputAmount + serviceFee).toFixed(8); + + console.log("Transaction amounts:", { + userInputAmount, + serviceFee, + totalAmount, + }); + + // Always fetch fresh fee estimates before transaction + let currentFeeRates: FeeEstimates; + try { + console.log( + "Fetching fresh fee estimates before transaction preparation" + ); + const estimatesResult = await fetchMempoolFeeEstimates(); + currentFeeRates = { + low: estimatesResult.low.rate, + medium: estimatesResult.medium.rate, + high: estimatesResult.high.rate, + }; + + // Update the UI fee display + setFeeEstimates(estimatesResult); + console.log("Using fee rates:", currentFeeRates); + } catch (error) { + console.warn("Error fetching fee estimates, using defaults:", error); + currentFeeRates = { low: 1, medium: 3, high: 5 }; + } + + const amountInSats = Math.round(Number.parseFloat(amount) * 100000000); + + console.log(MIN_DEPOSIT_SATS, MAX_DEPOSIT_SATS); + if (amountInSats < MIN_DEPOSIT_SATS) { + toast({ + title: "Minimum deposit required", + description: `Please deposit at least ${ + MIN_DEPOSIT_SATS / 100000000 + } BTC`, + variant: "destructive", + }); + return; + } + + if (amountInSats > MAX_DEPOSIT_SATS) { + toast({ + title: "Beta limitation", + description: `During beta, the maximum deposit amount is ${ + MAX_DEPOSIT_SATS / 100000000 + } BTC. Thank you for your understanding.`, + variant: "destructive", + }); + return; + } + + if (poolStatus && amountInSats > poolStatus.estimatedAvailable) { + toast({ + title: "Insufficient liquidity", + description: `The pool currently has ${ + poolStatus.estimatedAvailable / 100000000 + } BTC available. Please try a smaller amount.`, + variant: "destructive", + }); + return; + } + + const amountInBTC = Number.parseFloat(amount); + const networkFeeInBTC = 0.000006; + const totalRequiredBTC = amountInBTC + networkFeeInBTC; + + // Check if btcBalance is available and sufficient + const currentBalance = btcBalance ?? 0; + if (currentBalance < totalRequiredBTC) { + const shortfallBTC = totalRequiredBTC - currentBalance; + throw new Error( + `Insufficient funds. You need ${shortfallBTC.toFixed( + 8 + )} BTC more to complete this transaction.` + ); + } + + try { + console.log("Preparing transaction with SDK..."); + + const transactionData = await styxSDK.prepareTransaction({ + amount: totalAmount, // Now includes service fee + userAddress, + btcAddress, + feePriority: "medium" as TransactionPriority, + walletProvider: activeWalletProvider, + feeRates: currentFeeRates, + } as TransactionPrepareParams); + + console.log("Transaction prepared:", transactionData); + + setConfirmationData({ + depositAmount: totalAmount, + depositAddress: transactionData.depositAddress, + stxAddress: userAddress, + opReturnHex: transactionData.opReturnData, + // isBlaze: useBlazeSubnet, + }); + + setShowConfirmation(true); + } catch (err) { + console.error("Error preparing transaction:", err); + + if (err instanceof Error) { + if (isInscriptionError(err)) { + handleInscriptionError(err); + } else if (isUtxoCountError(err)) { + handleUtxoCountError(err); + } else if (isAddressTypeError(err)) { + handleAddressTypeError(err, activeWalletProvider); + } else { + toast({ + title: "Error", + description: + err.message || + "Failed to prepare transaction. Please try again.", + variant: "destructive", + }); + } + } else { + toast({ + title: "Error", + description: "Failed to prepare transaction. Please try again.", + variant: "destructive", + }); + } + } + } catch (err) { + console.error("Error preparing Bitcoin transaction:", err); + + if (err instanceof Error) { + toast({ + title: "Error", + description: + err.message || + "Failed to prepare Bitcoin transaction. Please try again.", + variant: "destructive", + }); + } else { + toast({ + title: "Error", + description: + "Failed to prepare Bitcoin transaction. Please try again.", + variant: "destructive", + }); + } + } + }; + + // Helper functions for error handling + function isAddressTypeError(error: Error): boolean { + return ( + error.message.includes("inputType: sh without redeemScript") || + error.message.includes("P2SH") || + error.message.includes("redeem script") + ); + } + + function handleAddressTypeError( + error: Error, + walletProvider: "leather" | "xverse" | null + ): void { + if (walletProvider === "leather") { + toast({ + title: "Unsupported Address Type", + description: + "Leather wallet does not support P2SH addresses (starting with '3'). Please use a SegWit address (starting with 'bc1') instead.", + variant: "destructive", + }); + } else if (walletProvider === "xverse") { + toast({ + title: "P2SH Address Error", + description: + "There was an issue with the P2SH address. This might be due to wallet limitations. Try using a SegWit address (starting with 'bc1') instead.", + variant: "destructive", + }); + } else { + toast({ + title: "P2SH Address Not Supported", + description: + "Your wallet doesn't provide the necessary information for your P2SH address. Please try using a SegWit address (starting with bc1) instead.", + variant: "destructive", + }); + } + } + + function isInscriptionError(error: Error): boolean { + return error.message.includes("with inscriptions"); + } + + function handleInscriptionError(error: Error): void { + toast({ + title: "Inscriptions Detected", + description: error.message, + variant: "destructive", + }); + } + + function isUtxoCountError(error: Error): boolean { + return error.message.includes("small UTXOs"); + } + + function handleUtxoCountError(error: Error): void { + toast({ + title: "Too Many UTXOs", + description: error.message, + variant: "destructive", + }); + } + + const presetAmounts: string[] = ["0.0001", "0.0002"]; + const presetLabels: string[] = ["0.0001 BTC", "0.0002 BTC"]; + + // Determine button text based on connection state + const getButtonText = () => { + if (!accessToken) return "Connect Wallet"; + return "Confirm Deposit"; + }; + + // Render loading state while initializing session + if (isLoading) { + return ( +
+ +

Loading your session...

+
+ ); + } + + return ( +
+ {/* From: Bitcoin */} +
+
+
+ + Bitcoin +
+ + {formatUsdValue(calculateUsdValue(amount))} + +
+ +
+ + + BTC + +
+ + {/* Display user's BTC balance */} + {accessToken && ( +
+ + Balance:{" "} + {isBalanceLoading + ? "Loading..." + : btcBalance !== null && btcBalance !== undefined + ? `${btcBalance.toFixed(8)} BTC${ + btcUsdPrice + ? ` (${formatUsdValue(btcBalance * (btcUsdPrice || 0))})` + : "" + }` + : "Unable to load balance"} + +
+ )} + + {/* Preset amounts */} +
+ {presetAmounts.map((presetAmount, index) => ( + + ))} + +
+
+ + {/* Fee Information Box */} + + +
+ + Estimated time + + + {feeEstimates.medium.time} + +
+
+ Service fee + + {amount && Number.parseFloat(amount) > 0 && btcUsdPrice + ? formatUsdValue( + Number.parseFloat(calculateFee(amount)) * btcUsdPrice + ) + : "$0.00"}{" "} + ~ {calculateFee(amount)} BTC + +
+ + {/* Add Pool Liquidity information */} + {poolStatus && ( +
+ + Pool liquidity + + + {formatUsdValue( + (poolStatus.estimatedAvailable / 100000000) * + (btcUsdPrice || 0) + )}{" "} + ~ {(poolStatus.estimatedAvailable / 100000000).toFixed(8)} BTC + +
+ )} +
+
+ + {/* Blaze Fast Subnet Option NOT SURE IF I SHOULD ADD IT BUT KEEPING IT FOR LATER JUST IN CASE */} + {/*
setUseBlazeSubnet(!useBlazeSubnet)} + > +
+
+ + {useBlazeSubnet && ( +
+ )} +
+
+ +

+ Near-instant confirmations with high throughput +

+
+
+
+ + BETA + + {useBlazeSubnet && ( +
+ +
+ )} +
+
*/} + + {/* Accordion with Additional Info */} + + +
+ + How it works + +
+ +

+ Your BTC deposit unlocks sBTC via Clarity's direct Bitcoin + state reading. No intermediaries or multi-signature scheme needed. + Trustless. Fast. Secure. +

+
+
+
+ + {/* Action Button */} + {!accessToken ? ( +
+

+ Connect your wallet to continue +

+
+ +
+
+ ) : ( + + )} +
+ ); +} diff --git a/src/components/btc-deposit/TransactionConfirmation.tsx b/src/components/btc-deposit/TransactionConfirmation.tsx new file mode 100644 index 00000000..341aa7d3 --- /dev/null +++ b/src/components/btc-deposit/TransactionConfirmation.tsx @@ -0,0 +1,946 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { hex } from "@scure/base"; +import * as btc from "@scure/btc-signer"; +import { styxSDK, TransactionPriority } from "@faktoryfun/styx-sdk"; +import { + type AddressPurpose, + type AddressType, + request as xverseRequest, +} from "sats-connect"; +import { useToast } from "@/hooks/use-toast"; +import { ArrowLeft, Copy, Check, AlertTriangle, Loader2 } from "lucide-react"; +import { useSessionStore } from "@/store/session"; +import { useClipboard } from "@/helpers/clipboard-utils"; +import type { + QueryObserverResult, + RefetchOptions, +} from "@tanstack/react-query"; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { cn } from "@/lib/utils"; +import type { ConfirmationData } from "./DepositForm"; +import type { + TransactionPrepareParams, + PreparedTransactionData, + DepositStatus, + DepositHistoryResponse, + Deposit, +} from "@faktoryfun/styx-sdk"; + +export interface LeatherSignPsbtRequestParams { + hex: string; + network: string; + broadcast: boolean; + allowedSighash?: number[]; + allowUnknownOutputs?: boolean; +} + +export interface LeatherSignPsbtResponse { + result?: { + hex: string; + }; + error?: { + code?: string; + message?: string; + }; +} + +export interface LeatherProvider { + request( + method: "signPsbt", + params: LeatherSignPsbtRequestParams + ): Promise; +} + +// Add this to fix the window.LeatherProvider type error +declare global { + interface Window { + LeatherProvider?: LeatherProvider; + } +} + +interface TransactionConfirmationProps { + confirmationData: ConfirmationData; + open: boolean; + onClose: () => void; + feePriority: TransactionPriority; + setFeePriority: (priority: TransactionPriority) => void; + userAddress: string; + btcAddress: string; + activeWalletProvider: "leather" | "xverse" | null; + refetchDepositHistory: ( + options?: RefetchOptions + ) => Promise>; + refetchAllDeposits: ( + options?: RefetchOptions + ) => Promise>; + setIsRefetching: (isRefetching: boolean) => void; +} + +interface XverseSignPsbtResponse { + status: "success" | "error"; + result?: { + psbt: string; + txid: string; + }; + error?: { + code?: string; + message?: string; + }; +} + +export default function TransactionConfirmation({ + confirmationData, + open, + onClose, + feePriority, + setFeePriority, + userAddress, + btcAddress, + activeWalletProvider, + refetchDepositHistory, + refetchAllDeposits, + setIsRefetching, +}: TransactionConfirmationProps) { + const { toast } = useToast(); + const [btcTxStatus, setBtcTxStatus] = useState< + "idle" | "pending" | "success" | "error" + >("idle"); + // const [btcTxId, setBtcTxId] = useState(""); + // const [currentDepositId, setCurrentDepositId] = useState(null); + const { copiedText, copyToClipboard } = useClipboard(); + + // Get session state from Zustand store + const { accessToken, isLoading, initialize } = useSessionStore(); + + // Initialize session on component mount + useEffect(() => { + initialize(); + }, [initialize]); + + const [feeEstimates, setFeeEstimates] = useState<{ + low: { rate: number; fee: number; time: string }; + medium: { rate: number; fee: number; time: string }; + high: { rate: number; fee: number; time: string }; + }>({ + low: { rate: 1, fee: 0, time: "30 min" }, + medium: { rate: 3, fee: 0, time: "~20 min" }, + high: { rate: 5, fee: 0, time: "~10 min" }, + }); + + const [loadingFees, setLoadingFees] = useState(true); + + const isP2SHAddress = (address: string): boolean => { + return address.startsWith("3"); + }; + + const calculateFeeEstimate = (rate: number, txSize = 148): number => { + return Math.round(txSize * rate); + }; + + // Fetch fee rates as soon as the modal opens + useEffect(() => { + const fetchFeeEstimates = async () => { + if (open) { + setLoadingFees(true); + try { + // Get fee rate estimates from SDK or API + const feeRates = await styxSDK.getFeeEstimates(); + + // Ensure proper separation between tiers + const lowRate = feeRates.low || 1; + const mediumRate = Math.max(lowRate + 1, feeRates.medium || 2); + const highRate = Math.max(mediumRate + 1, feeRates.high || 5); + + // Calculate fees for a standard transaction (~148 vBytes) + const txSize = 148; + + setFeeEstimates({ + low: { + rate: lowRate, + fee: calculateFeeEstimate(lowRate, txSize), + time: "30 min", + }, + medium: { + rate: mediumRate, + fee: calculateFeeEstimate(mediumRate, txSize), + time: "~20 min", + }, + high: { + rate: highRate, + fee: calculateFeeEstimate(highRate, txSize), + time: "~10 min", + }, + }); + } catch (error) { + console.error("Error fetching fee estimates:", error); + // Fallback to default estimates with proper separation + setFeeEstimates({ + low: { rate: 1, fee: 148, time: "30 min" }, + medium: { rate: 2, fee: 296, time: "~20 min" }, + high: { rate: 5, fee: 740, time: "~10 min" }, + }); + } finally { + setLoadingFees(false); + } + } + }; + + fetchFeeEstimates(); + }, [open]); + + const executeBitcoinTransaction = async (): Promise => { + console.log( + "Starting transaction with activeWalletProvider:", + activeWalletProvider + ); + + // Check if user is authenticated + if (!accessToken) { + toast({ + title: "Authentication required", + description: "Please sign in before proceeding with the transaction", + variant: "destructive", + }); + return; + } + + // Check if wallet is connected + if (!activeWalletProvider) { + toast({ + title: "No wallet connected", + description: "Please connect a wallet before proceeding", + variant: "destructive", + }); + return; + } + + const feeRates = await styxSDK.getFeeEstimates(); + const selectedFeeRate = feeRates[feePriority]; + console.log( + `Using ${feePriority} priority fee rate: ${selectedFeeRate} sat/vB` + ); + + if (!confirmationData) { + toast({ + title: "Error", + description: "Missing transaction data", + variant: "destructive", + }); + return; + } + + // Begin Bitcoin transaction process + setBtcTxStatus("pending"); + + try { + // Create deposit record + console.log("Creating deposit with data:", { + btcAmount: Number.parseFloat(confirmationData.depositAmount), + stxReceiver: userAddress || "", + btcSender: btcAddress || "", + }); + + // Create deposit record which will update pool status (reduce estimated available) + const depositId = await styxSDK.createDeposit({ + btcAmount: Number.parseFloat(confirmationData.depositAmount), + stxReceiver: userAddress || "", + btcSender: btcAddress || "", + isBlaze: confirmationData.isBlaze || false, + }); + console.log("Create deposit depositId:", depositId); + + // Store deposit ID for later use + // setCurrentDepositId(depositId); + + try { + if ( + activeWalletProvider === "leather" && + (typeof window === "undefined" || !window.LeatherProvider) + ) { + throw new Error("Leather wallet is not installed or not accessible"); + } + + console.log( + "Window object has LeatherProvider:", + !!window.LeatherProvider + ); + console.log("Full window object keys:", Object.keys(window)); + + if (!userAddress) { + throw new Error("STX address is missing or invalid"); + } + + if (activeWalletProvider === "leather") { + console.log("About to use LeatherProvider:", window.LeatherProvider); + } + + // Use the BTC address from context + if (!btcAddress) { + throw new Error("Could not find a valid BTC address in wallet"); + } + + const senderBtcAddress = btcAddress; + console.log("Using BTC address from context:", senderBtcAddress); + + // Get a transaction prepared for signing + console.log("Getting prepared transaction from SDK..."); + const preparedTransaction = await styxSDK.prepareTransaction({ + amount: confirmationData.depositAmount, + userAddress, + btcAddress, + feePriority, + walletProvider: activeWalletProvider, + } as TransactionPrepareParams); + + // Here, update fee estimates from the prepared transaction + setFeeEstimates({ + low: { + rate: preparedTransaction.feeRate, + fee: preparedTransaction.fee, + time: "30 min", + }, + medium: { + rate: preparedTransaction.feeRate, + fee: preparedTransaction.fee, + time: "~20 min", + }, + high: { + rate: preparedTransaction.feeRate, + fee: preparedTransaction.fee, + time: "~10 min", + }, + }); + + // Execute transaction with prepared data + console.log("Creating transaction with SDK..."); + const transactionData = await styxSDK.executeTransaction({ + depositId, + preparedData: { + utxos: preparedTransaction.utxos, + opReturnData: preparedTransaction.opReturnData, + depositAddress: preparedTransaction.depositAddress, + fee: preparedTransaction.fee, + changeAmount: preparedTransaction.changeAmount, + amountInSatoshis: preparedTransaction.amountInSatoshis, + feeRate: preparedTransaction.feeRate, + inputCount: preparedTransaction.inputCount, + outputCount: preparedTransaction.outputCount, + inscriptionCount: preparedTransaction.inscriptionCount, + } as PreparedTransactionData, + walletProvider: activeWalletProvider, + btcAddress: senderBtcAddress, + }); + + console.log("Transaction execution prepared:", transactionData); + + // Create a transaction object from the PSBT + let tx = new btc.Transaction({ + allowUnknownOutputs: true, + allowUnknownInputs: true, + disableScriptCheck: false, + }); + + // Load the base transaction from PSBT + tx = btc.Transaction.fromPSBT(hex.decode(transactionData.txPsbtHex)); + + // Handle P2SH for Xverse which requires frontend handling + const isP2sh = isP2SHAddress(senderBtcAddress); + if ( + isP2sh && + activeWalletProvider === "xverse" && + transactionData.needsFrontendInputHandling + ) { + console.log("Adding P2SH inputs specifically for Xverse"); + + // Only for P2SH + Xverse, do we need to add inputs - in all other cases the backend handled it + for (const utxo of preparedTransaction.utxos) { + try { + // First, try to get the account (which might fail if we don't have permission) + console.log("Trying to get wallet account..."); + let walletAccount = await xverseRequest( + "wallet_getAccount", + null + ); + + // If we get an access denied error, we need to request permissions + if ( + walletAccount.status === "error" && + walletAccount.error.code === -32002 + ) { + console.log("Access denied. Requesting permissions..."); + + // Request permissions using wallet_requestPermissions as shown in the docs + const permissionResponse = await xverseRequest( + "wallet_requestPermissions", + null + ); + console.log("Permission response:", permissionResponse); + + // If the user granted permission, try again to get the account + if (permissionResponse.status === "success") { + console.log( + "Permission granted. Trying to get wallet account again..." + ); + walletAccount = await xverseRequest( + "wallet_getAccount", + null + ); + } else { + throw new Error("User declined to grant permissions"); + } + } + + console.log("Wallet account response:", walletAccount); + + if ( + walletAccount.status === "success" && + walletAccount.result.addresses + ) { + // Find the payment address that matches our sender address + const paymentAddress = walletAccount.result.addresses.find( + (addr: { + address: string; + walletType: "software" | "ledger" | "keystone"; + publicKey: string; + purpose: AddressPurpose; + addressType: AddressType; + }) => + addr.address === senderBtcAddress && + addr.purpose === "payment" + ); + + if (paymentAddress && paymentAddress.publicKey) { + console.log( + "Found matching public key for P2SH address:", + paymentAddress.publicKey + ); + + // Create P2SH-P2WPKH from public key as shown in documentation + const publicKeyBytes = hex.decode(paymentAddress.publicKey); + const p2wpkh = btc.p2wpkh(publicKeyBytes, btc.NETWORK); + const p2sh = btc.p2sh(p2wpkh, btc.NETWORK); + + // Add input with redeemScript + tx.addInput({ + txid: utxo.txid, + index: utxo.vout, + witnessUtxo: { + script: p2sh.script, + amount: BigInt(utxo.value), + }, + redeemScript: p2sh.redeemScript, + }); + } else { + throw new Error( + "Could not find payment address with public key" + ); + } + } else { + throw new Error("Failed to get wallet account info"); + } + } catch (err) { + console.error("Error getting wallet account info:", err); + throw new Error( + "P2SH address requires access to the public key. Please use a SegWit address (starting with 'bc1') or grant necessary permissions." + ); + } + } + } + + // Extract transaction details from response + const { transactionDetails } = transactionData; + console.log("Transaction summary:", transactionDetails); + + // Generate PSBT and request signing + const txPsbt = tx.toPSBT(); + const finalTxPsbtHex = hex.encode(txPsbt); + const finalTxPsbtBase64 = Buffer.from(finalTxPsbtHex, "hex").toString( + "base64" + ); + + let txid; + + console.log("Wallet-specific flow for:", activeWalletProvider); + + if (activeWalletProvider === "leather") { + // Leather wallet flow + const requestParams = { + hex: finalTxPsbtHex, + network: "mainnet", + broadcast: false, + allowedSighash: [btc.SigHash.ALL], + allowUnknownOutputs: true, + }; + + if (!window.LeatherProvider) { + throw new Error( + "Leather wallet provider not found on window object" + ); + } + + // Send the signing request to Leather + const signResponse = await window.LeatherProvider.request( + "signPsbt", + requestParams + ); + + if ( + !signResponse || + !signResponse.result || + !signResponse.result.hex + ) { + throw new Error( + "Leather wallet did not return a valid signed PSBT" + ); + } + + // We get the hex of the signed PSBT back, finalize it + const signedPsbtHex = signResponse.result.hex; + const signedTx = btc.Transaction.fromPSBT(hex.decode(signedPsbtHex)); + signedTx.finalize(); + const finalTxHex = hex.encode(signedTx.extract()); + + // Manually broadcast the transaction + const broadcastResponse = await fetch( + "https://mempool.space/api/tx", + { + method: "POST", + headers: { + "Content-Type": "text/plain", + }, + body: finalTxHex, + } + ); + + if (!broadcastResponse.ok) { + const errorText = await broadcastResponse.text(); + throw new Error(`Failed to broadcast transaction: ${errorText}`); + } + + txid = await broadcastResponse.text(); + } else if (activeWalletProvider === "xverse") { + console.log("Executing Xverse transaction flow"); + console.log("xverseRequest function type:", typeof xverseRequest); + // Xverse wallet flow + try { + console.log("Starting Xverse PSBT signing flow..."); + + // Add all input addresses from our transaction + const inputAddresses: Record = {}; + inputAddresses[senderBtcAddress] = Array.from( + { length: preparedTransaction.utxos.length }, + (_, i) => i + ); + + console.log("Input addresses for Xverse:", inputAddresses); + console.log( + "PSBT Base64 (first 100 chars):", + finalTxPsbtBase64.substring(0, 100) + "..." + ); + + // Prepare request params + const xverseParams = { + psbt: finalTxPsbtBase64, + signInputs: inputAddresses, + broadcast: true, // Let Xverse handle broadcasting + allowedSighash: [ + btc.SigHash.ALL, + btc.SigHash.NONE, + btc.SigHash.SINGLE, + btc.SigHash.DEFAULT_ANYONECANPAY, + ], // More complete set of sighash options + options: { + allowUnknownInputs: true, + allowUnknownOutputs: true, + }, + }; + + // For P2SH addresses with Xverse, we need to add a special note in the logs + if (isP2SHAddress(senderBtcAddress)) { + console.log("Using P2SH-specific params for Xverse"); + console.log( + "P2SH address detected, relying on Xverse's internal handling" + ); + } + + console.log( + "Calling Xverse request with params:", + JSON.stringify(xverseParams, null, 2) + ); + + const response = (await xverseRequest( + "signPsbt", + xverseParams + )) as XverseSignPsbtResponse; + + console.log( + "Full Xverse response:", + JSON.stringify(response, null, 2) + ); + + if (response.status !== "success") { + console.error( + "Xverse signing failed with status:", + response.status + ); + console.error("Xverse error details:", response.error); + throw new Error( + `Xverse signing failed: ${ + response.error?.message || "Unknown error" + }` + ); + } + + // Fix the txid property access + if (!response.result?.txid) { + console.error("No txid in successful Xverse response:", response); + throw new Error("No transaction ID returned from Xverse"); + } + + txid = response.result.txid; + console.log("Successfully got txid from Xverse:", txid); + } catch (err) { + console.error("Detailed error with Xverse signing:", err); + console.error("Error type:", typeof err); + if (err instanceof Error) { + console.error("Error name:", err.name); + console.error("Error message:", err.message); + console.error("Error stack:", err.stack); + } + throw err; + } + } else { + throw new Error("No compatible wallet provider detected"); + } + + console.log("Transaction successfully broadcast with txid:", txid); + + // Update the deposit record with the transaction ID + console.log( + "Attempting to update deposit with ID:", + depositId, + "Type:", + typeof depositId + ); + + try { + console.log( + "About to update deposit with ID:", + depositId, + "and txid:", + txid + ); + console.log("Update data:", { + id: depositId, + data: { btcTxId: txid, status: "broadcast" }, + }); + + const updateResult = await styxSDK.updateDepositStatus({ + id: depositId, + data: { + btcTxId: txid, + status: "broadcast" as DepositStatus, + }, + }); + + console.log( + "Successfully updated deposit:", + JSON.stringify(updateResult, null, 2) + ); + } catch (error) { + console.error("Error updating deposit with ID:", depositId); + console.error("Update error details:", error); + if (error instanceof Error) { + console.error("Error name:", error.name); + console.error("Error message:", error.message); + console.error("Error stack:", error.stack); + } + } + + // Update state with success + setBtcTxStatus("success"); + // setBtcTxId(txid); + + // Show success message + toast({ + title: "Deposit Initiated", + description: `Your Bitcoin transaction has been sent successfully with txid: ${txid.substring( + 0, + 10 + )}...`, + }); + + // Close confirmation dialog + onClose(); + + // Trigger data refetch with loading indicator + setIsRefetching(true); + Promise.all([refetchDepositHistory(), refetchAllDeposits()]).finally( + () => { + setIsRefetching(false); + // Optionally show a toast to confirm refresh + toast({ + title: "Data Refreshed", + description: "Your transaction history has been updated", + }); + } + ); + } catch (err: unknown) { + console.error("Error in Bitcoin transaction process:", err); + setBtcTxStatus("error"); + + // Update deposit as canceled if wallet interaction failed + await styxSDK.updateDepositStatus({ + id: depositId, + data: { + status: "canceled" as DepositStatus, + }, + }); + + const errorMessage = + err instanceof Error + ? err.message + : "Failed to process Bitcoin transaction. Please try again."; + + toast({ + title: "Error", + description: errorMessage, + variant: "destructive", + }); + } + } catch (err: unknown) { + console.error("Error creating deposit record:", err); + setBtcTxStatus("error"); + + toast({ + title: "Error", + description: "Failed to initiate deposit. Please try again.", + variant: "destructive", + }); + } + }; + + // Render loading state while initializing session + if (isLoading) { + return ( + !open && onClose()}> + +
+ +

Loading your session...

+
+
+
+ ); + } + + return ( + !open && onClose()}> + + +
+ + Confirm Transaction Data +
+
+ +
+ {/* Authentication status */} + {!accessToken && ( + + + + Authentication required. Please sign in before proceeding with + the transaction. + + + )} + + {/* Wallet connection status */} + {!activeWalletProvider && ( + + + + No wallet connected. Please connect a wallet before proceeding + with the transaction. + + + )} + + {/* Transaction details */} +
+
+
Amount:
+
+
+ {confirmationData.depositAmount} BTC +
+
+ +
+ STX Address: +
+
+
+ {confirmationData.stxAddress} +
+
+ +
+ OP_RETURN: +
+
+
+ {confirmationData.opReturnHex} +
+ +
+
+
+ + {/* Wallet provider info */} +
+

Wallet Provider

+
+
+ {activeWalletProvider + ? activeWalletProvider.charAt(0).toUpperCase() + + activeWalletProvider.slice(1) + : "Not Connected"} +
+
+
+ + {/* Fee selection */} +
+

Select priority

+ +
+ setFeePriority(TransactionPriority.Low)} + > + +

Low

+

+ {loadingFees ? ( + + ) : ( + `${feeEstimates.low.fee} sats` + )} +

+

+ ({feeEstimates.low.rate} sat/vB) +

+

30 min

+
+
+ + setFeePriority(TransactionPriority.Medium)} + > + +

Medium

+

+ {loadingFees ? ( + + ) : ( + `${feeEstimates.medium.fee} sats` + )} +

+

+ ({feeEstimates.medium.rate} sat/vB) +

+

~20 min

+
+
+ + setFeePriority(TransactionPriority.High)} + > + +

High

+

+ {loadingFees ? ( + + ) : ( + `${feeEstimates.high.fee} sats` + )} +

+

+ ({feeEstimates.high.rate} sat/vB) +

+

~10 min

+
+
+
+ +

+ Fees are estimated based on current network conditions. +

+
+
+ + + + + +
+
+ ); +} diff --git a/src/components/btc-deposit/all-deposits.tsx b/src/components/btc-deposit/all-deposits.tsx new file mode 100644 index 00000000..a937468b --- /dev/null +++ b/src/components/btc-deposit/all-deposits.tsx @@ -0,0 +1,277 @@ +"use client"; + +import { ExternalLink, Loader2 } from "lucide-react"; +import type { Deposit } from "@faktoryfun/styx-sdk"; +import { formatDistanceToNow } from "date-fns"; +import { Card } from "@/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Badge } from "@/components/ui/badge"; +import useResolveBnsOrAddress from "@/hooks/deposit/useResolveBnsOrAddress"; + +interface AllDepositsProps { + allDepositsHistory: + | { + aggregateData: { + totalDeposits: number; + totalVolume: string; + uniqueUsers: number; + }; + recentDeposits: Deposit[]; + } + | undefined; + isLoading: boolean; + btcUsdPrice: number | null | undefined; + isRefetching?: boolean; +} + +// AddressCell Component +const AddressCell = ({ address }: { address: string }) => { + const { data } = useResolveBnsOrAddress(address); + + const displayAddress = + data?.resolvedValue && !data.resolvedValue.startsWith("SP") + ? data.resolvedValue // Show BNS names + : formatAddress(address); + + const bgColor = getBackgroundColor(address); + + const handleAddressClick = () => { + window.open( + `https://explorer.hiro.so/address/${address}?chain=mainnet`, + "_blank" + ); + }; + + return ( +
+ {displayAddress} +
+ ); +}; + +// Helper functions +const formatAddress = (address: string): string => { + if (!address) return "Unknown"; + if (address.length <= 10) return address; + return `${address.substring(0, 5)}...${address.substring( + address.length - 5 + )}`; +}; + +const getBackgroundColor = (address: string): string => { + if (!address) return "#3f3f46"; // zinc-700 + + // Simple hash function to generate a color + let hash = 0; + for (let i = 0; i < address.length; i++) { + hash = address.charCodeAt(i) + ((hash << 5) - hash); + } + + // Generate HSL color with fixed saturation and lightness + const h = Math.abs(hash % 360); + return `hsl(${h}, 70%, 30%)`; +}; + +export default function AllDeposits({ + allDepositsHistory, + isLoading, + btcUsdPrice, + isRefetching = false, +}: AllDepositsProps) { + // Format BTC amount for display + const formatBtcAmount = (amount: number | null): string => { + if (amount === null || amount === undefined) return "0.00000000"; + return amount.toFixed(8); + }; + + // Format USD value + const formatUsdValue = (amount: number): string => { + if (!amount || amount <= 0) return "$0.00"; + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(amount); + }; + + // Get status badge variant + const getStatusVariant = ( + status: string + ): "default" | "secondary" | "destructive" | "outline" => { + switch (status) { + case "broadcast": + return "secondary"; // yellow equivalent + case "processing": + return "default"; // blue equivalent + case "confirmed": + return "outline"; // green equivalent + default: + return "destructive"; // gray equivalent + } + }; + + // Get truncated tx id for display + const getTruncatedTxId = (txId: string | null): string => { + if (!txId) return "N/A"; + return `${txId.substring(0, 6)}...${txId.substring(txId.length - 4)}`; + }; + + // Format time using date-fns with timestamp (number) handling + const formatTimeAgo = (timestamp: number | null): string => { + if (timestamp === null || timestamp === undefined) return "Unknown"; + + try { + // Convert timestamp to Date object + const date = new Date(timestamp); + + // Check if date is valid + if (isNaN(date.getTime())) { + return "Invalid date"; + } + + return formatDistanceToNow(date, { addSuffix: true }); + } catch (error) { + console.error("Error formatting date:", error); + return "Invalid date"; + } + }; + + // Check if we have data to display + const hasData = + allDepositsHistory && + allDepositsHistory.recentDeposits && + allDepositsHistory.recentDeposits.length > 0; + + return ( +
+

Recent Network Activity

+ + {/* Stats summary box */} + {!isLoading && hasData && ( + +
+
+

Total Deposits

+

+ {allDepositsHistory.aggregateData.totalDeposits} +

+
+
+

Total Volume

+

+ {Number.parseFloat( + allDepositsHistory.aggregateData.totalVolume + ).toFixed(8)} +

+

BTC

+
+
+

Unique Users

+

+ {allDepositsHistory.aggregateData.uniqueUsers} +

+
+
+
+ )} + + {isLoading ? ( +
+ +

+ Loading network activity... +

+
+ ) : hasData ? ( + + {/* Add refetching overlay */} + {isRefetching && ( +
+
+ +

Updating history...

+
+
+ )} +
+ + + + Time + Amount + Status + User + Tx ID + + + + {allDepositsHistory.recentDeposits.map((deposit: Deposit) => ( + + + {formatTimeAgo(deposit.createdAt)} + + +
+ {formatBtcAmount(deposit.btcAmount)} +
+
+ {formatUsdValue(deposit.btcAmount * (btcUsdPrice || 0))} +
+
+ + + {deposit.status} + + + + + + + {deposit.btcTxId ? ( + + {getTruncatedTxId(deposit.btcTxId)} + + + ) : ( + + N/A + + )} + +
+ ))} +
+
+
+
+ ) : ( + +

No network activity found

+

+ Network activity will appear here once deposits are made +

+
+ )} +
+ ); +} diff --git a/src/components/btc-deposit/index.tsx b/src/components/btc-deposit/index.tsx new file mode 100644 index 00000000..e215794c --- /dev/null +++ b/src/components/btc-deposit/index.tsx @@ -0,0 +1,238 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { TransactionPriority } from "@faktoryfun/styx-sdk"; +import { Card } from "@/components/ui/card"; +import { Loader2 } from "lucide-react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import DepositForm from "./DepositForm"; +import TransactionConfirmation from "./TransactionConfirmation"; +import MyHistory from "./my-history"; +import AllDeposits from "./all-deposits"; +import { getStacksAddress, getBitcoinAddress } from "@/lib/address"; +import { useSessionStore } from "@/store/session"; +import AuthButton from "@/components/home/auth-button"; +import { useFormattedBtcPrice } from "@/hooks/deposit/useSdkBtcPrice"; +import useSdkPoolStatus from "@/hooks/deposit/useSdkPoolStatus"; +import useSdkDepositHistory from "@/hooks/deposit/useSdkDepositHistory"; +import useSdkAllDepositsHistory from "@/hooks/deposit/useSdkAllDepositsHistory"; + +// Define the ConfirmationData type +export type ConfirmationData = { + depositAmount: string; + depositAddress: string; + stxAddress: string; + opReturnHex: string; + isBlaze?: boolean; +}; + +export default function BitcoinDeposit() { + // Get session state from Zustand store + const { accessToken } = useSessionStore(); + + // State management + const [showConfirmation, setShowConfirmation] = useState(false); + const [confirmationData, setConfirmationData] = + useState(null); + const [feePriority, setFeePriority] = useState( + TransactionPriority.Medium + ); + const [activeWalletProvider, setActiveWalletProvider] = useState< + "leather" | "xverse" | null + >(null); + const [activeTab, setActiveTab] = useState("deposit"); + const [isRefetching, setIsRefetching] = useState(false); + console.log(activeWalletProvider); + // Add this useEffect hook after the state declarations + useEffect(() => { + if (accessToken) { + // Detect wallet provider based on the structure of btcAddress + let detectedWalletProvider: "xverse" | "leather" | null = null; + + // Get user data from localStorage + const blockstackSession = JSON.parse( + localStorage.getItem("blockstack-session") || "{}" + ); + const userData = blockstackSession.userData; + + if (userData?.profile) { + // Check structure of btcAddress to determine wallet type + if (typeof userData.profile.btcAddress === "string") { + // Xverse stores btcAddress as a direct string + detectedWalletProvider = "xverse"; + } else if ( + userData.profile.btcAddress?.p2wpkh?.mainnet || + userData.profile.btcAddress?.p2tr?.mainnet + ) { + // Leather stores addresses in a structured object + detectedWalletProvider = "leather"; + } else { + // If no BTC address in profile, check localStorage + const storedBtcAddress = localStorage.getItem("btcAddress"); + if (storedBtcAddress) { + detectedWalletProvider = "leather"; // Assume Leather if using localStorage + } + } + + // Update the wallet provider if detected + if (detectedWalletProvider !== activeWalletProvider) { + setActiveWalletProvider(detectedWalletProvider); + } + } + } + }, [accessToken, activeWalletProvider]); + + // Get addresses directly + const userAddress = accessToken ? getStacksAddress() : null; + const btcAddress = accessToken ? getBitcoinAddress() : null; + + // Data fetching hooks + const { + price: btcUsdPrice, + isLoading: isBtcPriceLoading, + error: btcPriceError, + } = useFormattedBtcPrice(); + const { data: poolStatus, isLoading: isPoolStatusLoading } = + useSdkPoolStatus(); + + // User's deposit history + const { + data: depositHistory, + isLoading: isHistoryLoading, + isRefetching: isHistoryRefetching, + refetch: refetchDepositHistory, + } = useSdkDepositHistory(userAddress); + + // All network deposits - using the provided hook + const { + data: allDepositsHistory, + isLoading: isAllDepositsLoading, + isRefetching: isAllDepositsRefetching, + refetch: refetchAllDeposits, + } = useSdkAllDepositsHistory(); + + // Determine if we're still loading critical data + const isDataLoading = + isBtcPriceLoading || isPoolStatusLoading || btcUsdPrice === undefined; + + // Render authentication prompt if not connected + if (!accessToken) { + return ( +
+
+

+ Deposit BTC in just 1 Bitcoin block +

+

+ Fast, secure, and trustless +

+
+ + +

+ Please connect your wallet to access the deposit feature +

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

+ Deposit BTC in just 1 Bitcoin block +

+

+ Fast, secure, and trustless +

+ {btcUsdPrice && ( +

+ Current BTC price: ${btcUsdPrice.toLocaleString()} +

+ )} +
+ + + + Deposit + My History + All Deposits + + + + + {isDataLoading ? ( +
+ +

+ Loading deposit data... +

+
+ ) : btcPriceError ? ( +
+

+ Error loading BTC price data. Please try again later. +

+
+ ) : ( + + )} +
+
+ + + + + + + + +
+ + {showConfirmation && confirmationData && ( + setShowConfirmation(false)} + feePriority={feePriority} + setFeePriority={setFeePriority} + userAddress={userAddress || ""} + btcAddress={btcAddress || ""} + activeWalletProvider={activeWalletProvider} + refetchDepositHistory={refetchDepositHistory} + refetchAllDeposits={refetchAllDeposits} + setIsRefetching={setIsRefetching} + /> + )} +
+ ); +} diff --git a/src/components/btc-deposit/my-history.tsx b/src/components/btc-deposit/my-history.tsx new file mode 100644 index 00000000..02158326 --- /dev/null +++ b/src/components/btc-deposit/my-history.tsx @@ -0,0 +1,184 @@ +"use client"; +import { ExternalLink, Loader2 } from "lucide-react"; +import type { Deposit } from "@faktoryfun/styx-sdk"; +import { useSessionStore } from "@/store/session"; +import { formatDistanceToNow } from "date-fns"; +import { Card } from "@/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Badge } from "@/components/ui/badge"; +import AuthButton from "@/components/home/auth-button"; + +interface MyHistoryProps { + depositHistory: Deposit[] | undefined; + isLoading: boolean; + btcUsdPrice: number | null | undefined; + isRefetching?: boolean; +} + +export default function MyHistory({ + depositHistory, + isLoading, + btcUsdPrice, + isRefetching = false, +}: MyHistoryProps) { + const { accessToken } = useSessionStore(); + + // Format BTC amount for display + const formatBtcAmount = (amount: number | null): string => { + if (amount === null || amount === undefined) return "0.00000000"; + return amount.toFixed(8); + }; + + // Format USD value + const formatUsdValue = (amount: number): string => { + if (!amount || amount <= 0) return "$0.00"; + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(amount); + }; + + // Get status badge variant + const getStatusVariant = ( + status: string + ): "default" | "secondary" | "destructive" | "outline" => { + switch (status) { + case "broadcast": + return "secondary"; + case "processing": + return "default"; + case "confirmed": + return "outline"; + default: + return "destructive"; + } + }; + + // Get truncated tx id for display + const getTruncatedTxId = (txId: string | null): string => { + if (!txId) return "N/A"; + return `${txId.substring(0, 6)}...${txId.substring(txId.length - 4)}`; + }; + + // Format time using date-fns with timestamp (number) handling + const formatTimeAgo = (timestamp: number | null): string => { + if (timestamp === null || timestamp === undefined) return "Unknown"; + + try { + // Convert timestamp to Date object + const date = new Date(timestamp); + + // Check if date is valid + if (isNaN(date.getTime())) { + return "Invalid date"; + } + + return formatDistanceToNow(date, { addSuffix: true }); + } catch (error) { + console.error("Error formatting date:", error); + return "Invalid date"; + } + }; + + return ( +
+ {!accessToken ? ( + +

+ Connect your wallet to view your deposit history +

+ +
+ ) : isLoading ? ( +
+ +

+ Loading deposit history... +

+
+ ) : depositHistory && depositHistory.length > 0 ? ( + + {/* Add refetching overlay */} + {isRefetching && ( +
+
+ +

Updating history...

+
+
+ )} +
+ + + + Time + Amount + Status + Tx ID + + + + {depositHistory.map((deposit: Deposit) => ( + + + {formatTimeAgo(deposit.createdAt)} + + +
+ {formatBtcAmount(deposit.btcAmount)} +
+
+ {formatUsdValue(deposit.btcAmount * (btcUsdPrice || 0))} +
+
+ + + {deposit.status} + + + + {deposit.btcTxId ? ( + + {getTruncatedTxId(deposit.btcTxId)} + + + ) : ( + + N/A + + )} + +
+ ))} +
+
+
+
+ ) : ( + +

No deposit history found

+

+ Make your first deposit to see it here +

+
+ )} +
+ ); +} diff --git a/src/components/reusables/asset-tracker.tsx b/src/components/reusables/asset-tracker.tsx index 21e969a1..388fdd1c 100644 --- a/src/components/reusables/asset-tracker.tsx +++ b/src/components/reusables/asset-tracker.tsx @@ -106,7 +106,7 @@ const AssetTracker = () => {
{!isLoaded && (

- Checking your sBTC status... + Checking your BTC status...

)} @@ -115,7 +115,7 @@ const AssetTracker = () => { className="text-center text-primary font-medium cursor-pointer hover:underline" onClick={openDepositModal} > - You have sBTC in your wallet! Click here to deposit it in your smart + You have BTC in your wallet! Click here to deposit it in your smart wallet.

)} @@ -129,13 +129,13 @@ const AssetTracker = () => { > Bitflow or Velar {" "} - to deposite sBTC in your wallet. + to deposite BTC in your wallet.

)} {isLoaded && hasSbtc === null && currentAddress && (

- Unable to check your sBTC status. Visit{" "} + Unable to check your BTC status. Visit{" "} Bitflow {" "} @@ -145,7 +145,7 @@ const AssetTracker = () => { {isLoaded && !currentAddress && (

- No wallet connected. Connect your wallet to check for sBTC. + No wallet connected. Connect your wallet to check for BTC.

)}
@@ -159,13 +159,13 @@ const AssetTracker = () => { > - Deposit sBTC + Deposit BTC Coming soon

Feature Coming Soon

- The ability to deposit sBTC into your smart wallet will be + The ability to deposit BTC into your smart wallet will be available in a future update.

diff --git a/src/hooks/deposit/useResolveBnsOrAddress.ts b/src/hooks/deposit/useResolveBnsOrAddress.ts new file mode 100644 index 00000000..9b74766c --- /dev/null +++ b/src/hooks/deposit/useResolveBnsOrAddress.ts @@ -0,0 +1,19 @@ +// hooks/useResolveBnsOrAddress.ts +import { useQuery } from "@tanstack/react-query"; + +// In a real application, this would call an API to resolve BNS names +const useResolveBnsOrAddress = (address: string) => { + return useQuery({ + queryKey: ["resolveAddress", address], + queryFn: async () => { + // Mock implementation + return { + resolvedValue: address.startsWith("SP") ? address : null, + }; + }, + enabled: !!address, + staleTime: 1000 * 60 * 60, // 1 hour + }); +}; + +export default useResolveBnsOrAddress; diff --git a/src/hooks/deposit/useSdkAllDepositsHistory.ts b/src/hooks/deposit/useSdkAllDepositsHistory.ts new file mode 100644 index 00000000..371395f4 --- /dev/null +++ b/src/hooks/deposit/useSdkAllDepositsHistory.ts @@ -0,0 +1,18 @@ +// hooks/useSdkAllDepositsHistory.ts +import { useQuery } from "@tanstack/react-query"; +import { styxSDK } from "@faktoryfun/styx-sdk"; + +const useSdkAllDepositsHistory = () => { + return useQuery({ + queryKey: ["allDepositsHistory"], + queryFn: async () => { + console.log("Fetching all deposits history..."); + const data = await styxSDK.getAllDepositsHistory(); + console.log("Received all deposits history:", data); + return data || []; + }, + staleTime: 60000, // 1 minute + }); +}; + +export default useSdkAllDepositsHistory; diff --git a/src/hooks/deposit/useSdkBtcPrice.ts b/src/hooks/deposit/useSdkBtcPrice.ts new file mode 100644 index 00000000..1ee937ae --- /dev/null +++ b/src/hooks/deposit/useSdkBtcPrice.ts @@ -0,0 +1,28 @@ +// hooks/useSdkBtcPrice.ts +import { useQuery } from "@tanstack/react-query"; +import { styxSDK } from "@faktoryfun/styx-sdk"; + +export const useSdkBtcPrice = () => { + return useQuery({ + queryKey: ["btcPrice"], + queryFn: async () => { + console.log("Fetching BTC price from SDK..."); + const price = await styxSDK.getBTCPrice(); + console.log("Received BTC price:", price); + return price; + }, + staleTime: 5 * 60 * 1000, // 5 minutes + }); +}; + +export const useFormattedBtcPrice = () => { + const query = useSdkBtcPrice(); + return { + price: query.data, + error: query.error, + isLoading: query.isLoading, + refetch: query.refetch, + }; +}; + +export default useSdkBtcPrice; diff --git a/src/hooks/deposit/useSdkDepositHistory.ts b/src/hooks/deposit/useSdkDepositHistory.ts new file mode 100644 index 00000000..b57782e1 --- /dev/null +++ b/src/hooks/deposit/useSdkDepositHistory.ts @@ -0,0 +1,20 @@ +// hooks/useSdkDepositHistory.ts +import { useQuery } from "@tanstack/react-query"; +import { styxSDK } from "@faktoryfun/styx-sdk"; + +const useSdkDepositHistory = (userAddress: string | null) => { + return useQuery({ + queryKey: ["depositHistory", userAddress], + queryFn: async () => { + if (!userAddress) return []; + console.log("Fetching deposit history for:", userAddress); + const data = await styxSDK.getDepositHistory(userAddress); + console.log("Received deposit history:", data); + return data || []; + }, + enabled: !!userAddress, + staleTime: 60000, // 1 minute + }); +}; + +export default useSdkDepositHistory; diff --git a/src/hooks/deposit/useSdkPoolStatus.ts b/src/hooks/deposit/useSdkPoolStatus.ts new file mode 100644 index 00000000..77d8c240 --- /dev/null +++ b/src/hooks/deposit/useSdkPoolStatus.ts @@ -0,0 +1,18 @@ +// hooks/useSdkPoolStatus.ts +import { useQuery } from "@tanstack/react-query"; +import { styxSDK } from "@faktoryfun/styx-sdk"; + +const useSdkPoolStatus = () => { + return useQuery({ + queryKey: ["poolStatus"], + queryFn: async () => { + console.log("Fetching pool status from SDK..."); + const data = await styxSDK.getPoolStatus(); + console.log("Received pool status:", data); + return data; + }, + staleTime: 60000, // 1 minute + }); +}; + +export default useSdkPoolStatus; diff --git a/src/lib/address.ts b/src/lib/address.ts index 73d39424..70a7d1e2 100644 --- a/src/lib/address.ts +++ b/src/lib/address.ts @@ -13,15 +13,31 @@ export function getStacksAddress(): string | null { return address || null } -export function getBitcoinAddress(): string | null { +export function getBitcoinAddress(network: "mainnet" | "testnet" = "mainnet"): string | null { if (typeof window === "undefined") { return null } const blockstackSession = JSON.parse(localStorage.getItem("blockstack-session") || "{}") - - // Access the Bitcoin address from the profile const btcAddress = blockstackSession.userData?.profile?.btcAddress - return btcAddress || null -} \ No newline at end of file + if (!btcAddress) { + // Check if there's a stored address in localStorage as fallback + const storedBtcAddress = localStorage.getItem("btcAddress") + return storedBtcAddress || null + } + + // Handle Leather wallet's structured address format + if (typeof btcAddress === "object") { + // Leather wallet stores addresses in a structured object + if (network === "mainnet") { + // Try p2wpkh (segwit) first, then p2tr (taproot), then p2pkh (legacy) + return btcAddress.p2wpkh?.mainnet || btcAddress.p2tr?.mainnet || btcAddress.p2pkh?.mainnet || null + } else { + // For testnet, try the same address types but for testnet + return btcAddress.p2wpkh?.testnet || btcAddress.p2tr?.testnet || btcAddress.p2pkh?.testnet || null + } + } + + return typeof btcAddress === "string" ? btcAddress : null +}