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) => (
+ handlePresetClick(presetAmount)}
+ >
+ {presetLabels[index]}
+
+ ))}
+
+ MAX
+
+
+
+
+ {/* 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 && (
+
+ )}
+
+
+
+ Use Blaze Fast Subnet
+
+
+ Near-instant confirmations with high throughput
+
+
+
+
+
+ BETA
+
+ {useBlazeSubnet && (
+
+
+
+ )}
+
+
*/}
+
+ {/* Accordion with Additional Info */}
+
+
+
+
+
+ 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
+
+
+
+ ) : (
+
+ {getButtonText()}
+
+ )}
+
+ );
+}
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}
+
+
copyToClipboard(confirmationData.opReturnHex)}
+ >
+ {copiedText === 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.
+
+
+
+
+
+
+ Cancel
+
+
+ {btcTxStatus === "pending" ? "Processing..." : "Proceed to Wallet"}
+
+
+
+
+ );
+}
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.
setIsDepositModalOpen(false)}>Close
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
+}