diff --git a/package-lock.json b/package-lock.json index 8546451b9..09f7772f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "@ledgerhq/hw-transport-webusb": "^6.27.13", "@phosphor-icons/react": "^2.0.10", "@react-spring/web": "^9.6.1", - "@secretkeylabs/xverse-core": "8.0.0", + "@scure/btc-signer": "^1.1.1", + "@secretkeylabs/xverse-core": "8.0.1", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.9.0", @@ -27,6 +28,7 @@ "axios": "^1.1.3", "bignumber.js": "^9.1.0", "bip39": "^3.0.3", + "buffer": "6.0.3", "c32check": "^2.0.0", "classnames": "^2.3.2", "crypto-browserify": "^3.12.0", @@ -1658,9 +1660,9 @@ ] }, "node_modules/@scure/base": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", - "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.4.tgz", + "integrity": "sha512-wznebWtt+ejH8el87yuD4i9xLSbYZXf1Pe4DY0o/zq/eg5I0VQVXVbFs6XIM0pNVCJ/uE3t5wI9kh90mdLUxtw==", "funding": { "url": "https://paulmillr.com/funding/" } @@ -1732,10 +1734,32 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/btc-signer/node_modules/@noble/curves": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", + "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "dependencies": { + "@noble/hashes": "1.3.3" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/btc-signer/node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@secretkeylabs/xverse-core": { - "version": "8.0.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/8.0.0/1847cfb905bcbed1164961685131936c01892ac0", - "integrity": "sha512-3vz6g7tr5oEeluKii0KHBlR3RqZBuDrI25PLYgEJs1O5tQwlQflK8ad5yWGj+CS3qY84853JSYJEirVvoMzJkw==", + "version": "8.0.1", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/8.0.1/4ad617c75c0435ca61da77d8d0cf0d4b961d8d87", + "integrity": "sha512-Y6qH74fUZ4Wv8CX/7Ax/9DMf7HfW4GYIgdsn2a8mgCMyAxre7MPcomxmzWSvK8CViHim18b6K2P+wNOOE4l70Q==", "license": "ISC", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", @@ -1803,22 +1827,58 @@ "dev": true }, "node_modules/@stacks/auth": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@stacks/auth/-/auth-6.9.0.tgz", - "integrity": "sha512-tBOB+H/96TUNK9pKmr1YQoiIItUFp2ms5RCNYPSjy3/lbIYYJYtw/O2fOS78fVQvCCpuObhhO65AVsrE/IzQeg==", - "dependencies": { - "@stacks/common": "^6.8.1", - "@stacks/encryption": "^6.9.0", - "@stacks/network": "^6.8.1", - "@stacks/profile": "^6.9.0", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/auth/-/auth-6.10.0.tgz", + "integrity": "sha512-5/FdD1btPovJTckVfaNdD+J/JxtKwn1jVOcyiuDwOABMOMGykgH8ZdXc0Kho2POZoOwKTIrG8sYH5TolSoH7BA==", + "dependencies": { + "@stacks/common": "^6.10.0", + "@stacks/encryption": "^6.10.0", + "@stacks/network": "^6.10.0", + "@stacks/profile": "^6.10.0", "cross-fetch": "^3.1.5", "jsontokens": "^4.0.1" } }, + "node_modules/@stacks/auth/node_modules/@noble/hashes": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", + "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@stacks/auth/node_modules/@stacks/encryption": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.11.0.tgz", + "integrity": "sha512-VfBkrwmCRppCasJo+R/hWfC7vgS6GmfPyoTeDsoYlfRRXz/auFbEdRaaruFPtAda/1nKdDOZ9UZEMOp5AIw0IQ==", + "dependencies": { + "@noble/hashes": "1.1.5", + "@noble/secp256k1": "1.7.1", + "@scure/bip39": "1.1.0", + "@stacks/common": "^6.10.0", + "@types/node": "^18.0.4", + "base64-js": "^1.5.1", + "bs58": "^5.0.0", + "ripemd160-min": "^0.0.6", + "varuint-bitcoin": "^1.1.2" + } + }, + "node_modules/@stacks/auth/node_modules/@stacks/network": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.10.0.tgz", + "integrity": "sha512-mbiZ8nlsyy77ndmBdaqhHXii22IFdK4ThRcOQs9j/O00DkAr04jCM4GV5Q+VLUnZ9OBoJq7yOV7Pf6jglh+0hw==", + "dependencies": { + "@stacks/common": "^6.10.0", + "cross-fetch": "^3.1.5" + } + }, "node_modules/@stacks/common": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.8.1.tgz", - "integrity": "sha512-ewL9GLZNQYa5a/3K4xSHlHIgHkD4rwWW/QEaPId8zQIaL+1O9qCaF4LX9orNQeOmEk8kvG0x2xGV54fXKCZeWQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.10.0.tgz", + "integrity": "sha512-6x5Z7AKd9/kj3+DYE9xIDIkFLHihBH614i2wqrZIjN02WxVo063hWSjIlUxlx8P4gl6olVzlOy5LzhLJD9OP0A==", "dependencies": { "@types/bn.js": "^5.1.0", "@types/node": "^18.0.4" @@ -1853,7 +1913,7 @@ "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.1", "@scure/bip39": "1.1.0", - "@stacks/common": "^6.8.1", + "@stacks/common": "^6.10.0", "@types/node": "^18.0.4", "base64-js": "^1.5.1", "bs58": "^5.0.0", @@ -1882,36 +1942,105 @@ } }, "node_modules/@stacks/profile": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@stacks/profile/-/profile-6.9.0.tgz", - "integrity": "sha512-sIR60DsAHi8C6zGqKqSe1r2hXTMHgwrJkX3fAaP3de40KeplZ2bkE+0B83yismEeU2baNc+AukyVvWJv0PfP0A==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/profile/-/profile-6.10.0.tgz", + "integrity": "sha512-n2H1Imuu7UfwIvUv7xgHiSb3CKfbP4H+Jzy1+w73njYX7glt60uQ1SjWef7gvMgMFQNTAPELqitweyA+UII6Hg==", "dependencies": { - "@stacks/common": "^6.8.1", - "@stacks/network": "^6.8.1", - "@stacks/transactions": "^6.9.0", + "@stacks/common": "^6.10.0", + "@stacks/network": "^6.10.0", + "@stacks/transactions": "^6.10.0", "jsontokens": "^4.0.1", "schema-inspector": "^2.0.2", "zone-file": "^2.0.0-beta.3" } }, + "node_modules/@stacks/profile/node_modules/@noble/hashes": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", + "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@stacks/profile/node_modules/@stacks/network": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.10.0.tgz", + "integrity": "sha512-mbiZ8nlsyy77ndmBdaqhHXii22IFdK4ThRcOQs9j/O00DkAr04jCM4GV5Q+VLUnZ9OBoJq7yOV7Pf6jglh+0hw==", + "dependencies": { + "@stacks/common": "^6.10.0", + "cross-fetch": "^3.1.5" + } + }, + "node_modules/@stacks/profile/node_modules/@stacks/transactions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.11.0.tgz", + "integrity": "sha512-+zIDqn9j4H/+o1ER8C9rFpig1fyrQcj2hVGNIrp+YbpPyja+cxv3fPk6kI/gePzwggzxRgUkIWhBc+mZAXuXyQ==", + "dependencies": { + "@noble/hashes": "1.1.5", + "@noble/secp256k1": "1.7.1", + "@stacks/common": "^6.10.0", + "@stacks/network": "^6.10.0", + "c32check": "^2.0.0", + "lodash.clonedeep": "^4.5.0" + } + }, "node_modules/@stacks/stacks-blockchain-api-types": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-6.1.1.tgz", "integrity": "sha512-Mw5dBPx3DySPupwaq0iBdm1WdEVXIfhjUVaTjI2iSyzWz4Fgs3U7JCaAezLbgNu7Q69c/ZN4JUDWuo9FVjy7oA==" }, "node_modules/@stacks/storage": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.9.0.tgz", - "integrity": "sha512-aZ3tOnwRSk5cHQh9ButhfHDvAylNVxPRQzeSB8PydHfyib4XL7fSAJwizzEWNgJV4dovqW2Nsy8gm/4rM/oFKQ==", - "dependencies": { - "@stacks/auth": "^6.9.0", - "@stacks/common": "^6.8.1", - "@stacks/encryption": "^6.9.0", - "@stacks/network": "^6.8.1", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.10.0.tgz", + "integrity": "sha512-DLinjJkCN9Q7Yu5yelcXfP89CIDZ4TqXisjJYRqRlFfQdoDWDFZT5vpM2C8U2xDmgzVxfjg90HmQpIjTeIMSnw==", + "dependencies": { + "@stacks/auth": "^6.10.0", + "@stacks/common": "^6.10.0", + "@stacks/encryption": "^6.10.0", + "@stacks/network": "^6.10.0", "base64-js": "^1.5.1", "jsontokens": "^4.0.1" } }, + "node_modules/@stacks/storage/node_modules/@noble/hashes": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", + "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@stacks/storage/node_modules/@stacks/encryption": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.11.0.tgz", + "integrity": "sha512-VfBkrwmCRppCasJo+R/hWfC7vgS6GmfPyoTeDsoYlfRRXz/auFbEdRaaruFPtAda/1nKdDOZ9UZEMOp5AIw0IQ==", + "dependencies": { + "@noble/hashes": "1.1.5", + "@noble/secp256k1": "1.7.1", + "@scure/bip39": "1.1.0", + "@stacks/common": "^6.10.0", + "@types/node": "^18.0.4", + "base64-js": "^1.5.1", + "bs58": "^5.0.0", + "ripemd160-min": "^0.0.6", + "varuint-bitcoin": "^1.1.2" + } + }, + "node_modules/@stacks/storage/node_modules/@stacks/network": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.10.0.tgz", + "integrity": "sha512-mbiZ8nlsyy77ndmBdaqhHXii22IFdK4ThRcOQs9j/O00DkAr04jCM4GV5Q+VLUnZ9OBoJq7yOV7Pf6jglh+0hw==", + "dependencies": { + "@stacks/common": "^6.10.0", + "cross-fetch": "^3.1.5" + } + }, "node_modules/@stacks/transactions": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.9.0.tgz", @@ -3496,29 +3625,6 @@ "node": ">=8" } }, - "node_modules/@zondax/ledger-stacks/node_modules/c32check/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -5290,29 +5396,6 @@ "buffer": "^5.6.0" } }, - "node_modules/cross-sha256/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -15341,9 +15424,9 @@ "optional": true }, "@scure/base": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", - "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.4.tgz", + "integrity": "sha512-wznebWtt+ejH8el87yuD4i9xLSbYZXf1Pe4DY0o/zq/eg5I0VQVXVbFs6XIM0pNVCJ/uE3t5wI9kh90mdLUxtw==" }, "@scure/bip32": { "version": "1.1.3", @@ -15387,12 +15470,27 @@ "@noble/hashes": "~1.3.3", "@scure/base": "~1.1.4", "micro-packed": "~0.4.0" + }, + "dependencies": { + "@noble/curves": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", + "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "requires": { + "@noble/hashes": "1.3.3" + } + }, + "@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==" + } } }, "@secretkeylabs/xverse-core": { - "version": "8.0.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/8.0.0/1847cfb905bcbed1164961685131936c01892ac0", - "integrity": "sha512-3vz6g7tr5oEeluKii0KHBlR3RqZBuDrI25PLYgEJs1O5tQwlQflK8ad5yWGj+CS3qY84853JSYJEirVvoMzJkw==", + "version": "8.0.1", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/8.0.1/4ad617c75c0435ca61da77d8d0cf0d4b961d8d87", + "integrity": "sha512-Y6qH74fUZ4Wv8CX/7Ax/9DMf7HfW4GYIgdsn2a8mgCMyAxre7MPcomxmzWSvK8CViHim18b6K2P+wNOOE4l70Q==", "requires": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", @@ -15454,22 +15552,54 @@ "dev": true }, "@stacks/auth": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@stacks/auth/-/auth-6.9.0.tgz", - "integrity": "sha512-tBOB+H/96TUNK9pKmr1YQoiIItUFp2ms5RCNYPSjy3/lbIYYJYtw/O2fOS78fVQvCCpuObhhO65AVsrE/IzQeg==", - "requires": { - "@stacks/common": "^6.8.1", - "@stacks/encryption": "^6.9.0", - "@stacks/network": "^6.8.1", - "@stacks/profile": "^6.9.0", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/auth/-/auth-6.10.0.tgz", + "integrity": "sha512-5/FdD1btPovJTckVfaNdD+J/JxtKwn1jVOcyiuDwOABMOMGykgH8ZdXc0Kho2POZoOwKTIrG8sYH5TolSoH7BA==", + "requires": { + "@stacks/common": "^6.10.0", + "@stacks/encryption": "^6.10.0", + "@stacks/network": "^6.10.0", + "@stacks/profile": "^6.10.0", "cross-fetch": "^3.1.5", "jsontokens": "^4.0.1" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", + "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==" + }, + "@stacks/encryption": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.11.0.tgz", + "integrity": "sha512-VfBkrwmCRppCasJo+R/hWfC7vgS6GmfPyoTeDsoYlfRRXz/auFbEdRaaruFPtAda/1nKdDOZ9UZEMOp5AIw0IQ==", + "requires": { + "@noble/hashes": "1.1.5", + "@noble/secp256k1": "1.7.1", + "@scure/bip39": "1.1.0", + "@stacks/common": "^6.10.0", + "@types/node": "^18.0.4", + "base64-js": "^1.5.1", + "bs58": "^5.0.0", + "ripemd160-min": "^0.0.6", + "varuint-bitcoin": "^1.1.2" + } + }, + "@stacks/network": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.10.0.tgz", + "integrity": "sha512-mbiZ8nlsyy77ndmBdaqhHXii22IFdK4ThRcOQs9j/O00DkAr04jCM4GV5Q+VLUnZ9OBoJq7yOV7Pf6jglh+0hw==", + "requires": { + "@stacks/common": "^6.10.0", + "cross-fetch": "^3.1.5" + } + } } }, "@stacks/common": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.8.1.tgz", - "integrity": "sha512-ewL9GLZNQYa5a/3K4xSHlHIgHkD4rwWW/QEaPId8zQIaL+1O9qCaF4LX9orNQeOmEk8kvG0x2xGV54fXKCZeWQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.10.0.tgz", + "integrity": "sha512-6x5Z7AKd9/kj3+DYE9xIDIkFLHihBH614i2wqrZIjN02WxVo063hWSjIlUxlx8P4gl6olVzlOy5LzhLJD9OP0A==", "requires": { "@types/bn.js": "^5.1.0", "@types/node": "^18.0.4" @@ -15504,7 +15634,7 @@ "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.1", "@scure/bip39": "1.1.0", - "@stacks/common": "^6.8.1", + "@stacks/common": "^6.10.0", "@types/node": "^18.0.4", "base64-js": "^1.5.1", "bs58": "^5.0.0", @@ -15529,16 +15659,45 @@ } }, "@stacks/profile": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@stacks/profile/-/profile-6.9.0.tgz", - "integrity": "sha512-sIR60DsAHi8C6zGqKqSe1r2hXTMHgwrJkX3fAaP3de40KeplZ2bkE+0B83yismEeU2baNc+AukyVvWJv0PfP0A==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/profile/-/profile-6.10.0.tgz", + "integrity": "sha512-n2H1Imuu7UfwIvUv7xgHiSb3CKfbP4H+Jzy1+w73njYX7glt60uQ1SjWef7gvMgMFQNTAPELqitweyA+UII6Hg==", "requires": { - "@stacks/common": "^6.8.1", - "@stacks/network": "^6.8.1", - "@stacks/transactions": "^6.9.0", + "@stacks/common": "^6.10.0", + "@stacks/network": "^6.10.0", + "@stacks/transactions": "^6.10.0", "jsontokens": "^4.0.1", "schema-inspector": "^2.0.2", "zone-file": "^2.0.0-beta.3" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", + "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==" + }, + "@stacks/network": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.10.0.tgz", + "integrity": "sha512-mbiZ8nlsyy77ndmBdaqhHXii22IFdK4ThRcOQs9j/O00DkAr04jCM4GV5Q+VLUnZ9OBoJq7yOV7Pf6jglh+0hw==", + "requires": { + "@stacks/common": "^6.10.0", + "cross-fetch": "^3.1.5" + } + }, + "@stacks/transactions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.11.0.tgz", + "integrity": "sha512-+zIDqn9j4H/+o1ER8C9rFpig1fyrQcj2hVGNIrp+YbpPyja+cxv3fPk6kI/gePzwggzxRgUkIWhBc+mZAXuXyQ==", + "requires": { + "@noble/hashes": "1.1.5", + "@noble/secp256k1": "1.7.1", + "@stacks/common": "^6.10.0", + "@stacks/network": "^6.10.0", + "c32check": "^2.0.0", + "lodash.clonedeep": "^4.5.0" + } + } } }, "@stacks/stacks-blockchain-api-types": { @@ -15547,16 +15706,48 @@ "integrity": "sha512-Mw5dBPx3DySPupwaq0iBdm1WdEVXIfhjUVaTjI2iSyzWz4Fgs3U7JCaAezLbgNu7Q69c/ZN4JUDWuo9FVjy7oA==" }, "@stacks/storage": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.9.0.tgz", - "integrity": "sha512-aZ3tOnwRSk5cHQh9ButhfHDvAylNVxPRQzeSB8PydHfyib4XL7fSAJwizzEWNgJV4dovqW2Nsy8gm/4rM/oFKQ==", - "requires": { - "@stacks/auth": "^6.9.0", - "@stacks/common": "^6.8.1", - "@stacks/encryption": "^6.9.0", - "@stacks/network": "^6.8.1", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.10.0.tgz", + "integrity": "sha512-DLinjJkCN9Q7Yu5yelcXfP89CIDZ4TqXisjJYRqRlFfQdoDWDFZT5vpM2C8U2xDmgzVxfjg90HmQpIjTeIMSnw==", + "requires": { + "@stacks/auth": "^6.10.0", + "@stacks/common": "^6.10.0", + "@stacks/encryption": "^6.10.0", + "@stacks/network": "^6.10.0", "base64-js": "^1.5.1", "jsontokens": "^4.0.1" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", + "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==" + }, + "@stacks/encryption": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.11.0.tgz", + "integrity": "sha512-VfBkrwmCRppCasJo+R/hWfC7vgS6GmfPyoTeDsoYlfRRXz/auFbEdRaaruFPtAda/1nKdDOZ9UZEMOp5AIw0IQ==", + "requires": { + "@noble/hashes": "1.1.5", + "@noble/secp256k1": "1.7.1", + "@scure/bip39": "1.1.0", + "@stacks/common": "^6.10.0", + "@types/node": "^18.0.4", + "base64-js": "^1.5.1", + "bs58": "^5.0.0", + "ripemd160-min": "^0.0.6", + "varuint-bitcoin": "^1.1.2" + } + }, + "@stacks/network": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.10.0.tgz", + "integrity": "sha512-mbiZ8nlsyy77ndmBdaqhHXii22IFdK4ThRcOQs9j/O00DkAr04jCM4GV5Q+VLUnZ9OBoJq7yOV7Pf6jglh+0hw==", + "requires": { + "@stacks/common": "^6.10.0", + "cross-fetch": "^3.1.5" + } + } } }, "@stacks/transactions": { @@ -15593,7 +15784,7 @@ "@stacks/profile": "^6.9.0", "@stacks/storage": "^6.9.0", "@stacks/transactions": "^6.9.0", - "buffer": "^6.0.3", + "buffer": "6.0.3", "c32check": "^2.0.0", "jsontokens": "^4.0.1", "triplesec": "^4.0.3", @@ -16781,7 +16972,7 @@ "requires": { "@types/bn.js": "^5.1.0", "@types/node": "^18.0.4", - "buffer": "^6.0.3" + "buffer": "6.0.3" } }, "@stacks/network": { @@ -16825,19 +17016,8 @@ "integrity": "sha512-ADADE/PjAbJRlwpG3ShaOMbBUlJJZO7xaYSRD5Tub6PixQlgR4s36y9cvMf/YRGpkqX+QOxIdMw216iC320q9A==", "requires": { "base-x": "^3.0.8", - "buffer": "^5.6.0", + "buffer": "6.0.3", "cross-sha256": "^1.2.0" - }, - "dependencies": { - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - } } } } @@ -18217,18 +18397,7 @@ "resolved": "https://registry.npmjs.org/cross-sha256/-/cross-sha256-1.2.0.tgz", "integrity": "sha512-KViLNMDZKV7jwFqjFx+rNhG26amnFYYQ0S+VaFlVvpk8tM+2XbFia/don/SjGHg9WQxnFVi6z64CGPuF3T+nNw==", "requires": { - "buffer": "^5.6.0" - }, - "dependencies": { - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - } + "buffer": "6.0.3" } }, "cross-spawn": { diff --git a/package.json b/package.json index 849c5ef66..6364bfe4c 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "@ledgerhq/hw-transport-webusb": "^6.27.13", "@phosphor-icons/react": "^2.0.10", "@react-spring/web": "^9.6.1", - "@secretkeylabs/xverse-core": "8.0.0", + "@scure/btc-signer": "^1.1.1", + "@secretkeylabs/xverse-core": "8.0.1", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.9.0", @@ -26,6 +27,7 @@ "axios": "^1.1.3", "bignumber.js": "^9.1.0", "bip39": "^3.0.3", + "buffer": "6.0.3", "c32check": "^2.0.0", "classnames": "^2.3.2", "crypto-browserify": "^3.12.0", @@ -76,8 +78,7 @@ "style": "prettier --write \"src/**/*.{ts,tsx}\"", "prepare": "husky install" }, - "resolutions": { - "styled-components": "^5", + "overrides": { "buffer": "6.0.3" }, "lint-staged": { diff --git a/src/app/components/confirmBtcTransaction/index.tsx b/src/app/components/confirmBtcTransaction/index.tsx new file mode 100644 index 000000000..c042ca43f --- /dev/null +++ b/src/app/components/confirmBtcTransaction/index.tsx @@ -0,0 +1,229 @@ +import ledgerConnectDefaultIcon from '@assets/img/ledger/ledger_connect_default.svg'; +import ledgerConnectBtcIcon from '@assets/img/ledger/ledger_import_connect_btc.svg'; +import { delay } from '@common/utils/ledger'; +import BottomModal from '@components/bottomModal'; +import ActionButton from '@components/button'; +import LedgerConnectionView from '@components/ledger/connectLedgerView'; +import useWalletSelector from '@hooks/useWalletSelector'; +import TransportFactory from '@ledgerhq/hw-transport-webusb'; +import { btcTransaction, Transport } from '@secretkeylabs/xverse-core'; +import Callout from '@ui-library/callout'; +import { StickyHorizontalSplitButtonContainer, StyledP } from '@ui-library/common.styled'; +import { isLedgerAccount } from '@utils/helper'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { MoonLoader } from 'react-spinners'; +import styled from 'styled-components'; +import SendLayout from '../../layouts/sendLayout'; +import TransactionSummary from './transactionSummary'; + +const LoaderContainer = styled.div(() => ({ + display: 'flex', + flex: 1, + justifyContent: 'center', + alignItems: 'center', +})); + +const ReviewTransactionText = styled(StyledP)` + text-align: left; + margin-bottom: ${(props) => props.theme.space.l}; +`; + +const BroadcastCallout = styled(Callout)` + margin-bottom: ${(props) => props.theme.space.m}; +`; + +const SuccessActionsContainer = styled.div((props) => ({ + width: '100%', + display: 'flex', + flexDirection: 'column', + gap: props.theme.space.s, + paddingLeft: props.theme.space.m, + paddingRight: props.theme.space.m, + marginBottom: props.theme.space.xxl, + marginTop: props.theme.space.xxl, +})); + +type Props = { + inputs: btcTransaction.EnhancedInput[]; + outputs: btcTransaction.EnhancedOutput[]; + feeOutput?: btcTransaction.TransactionFeeOutput; + isLoading: boolean; + isSubmitting: boolean; + isBroadcast?: boolean; + isError?: boolean; + showAccountHeader?: boolean; + hideBottomBar?: boolean; + cancelText: string; + confirmText: string; + onConfirm: (ledgerTransport?: Transport) => void; + onCancel: () => void; + onBackClick?: () => void; + confirmDisabled?: boolean; + getFeeForFeeRate?: (feeRate: number, useEffectiveFeeRate?: boolean) => Promise; + onFeeRateSet?: (feeRate: number) => void; +}; + +function ConfirmBtcTransaction({ + inputs, + outputs, + feeOutput, + isLoading, + isSubmitting, + isBroadcast, + isError = false, + cancelText, + confirmText, + onConfirm, + onCancel, + onBackClick, + showAccountHeader, + hideBottomBar, + confirmDisabled = false, + getFeeForFeeRate, + onFeeRateSet, +}: Props) { + const [isModalVisible, setIsModalVisible] = useState(false); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isButtonDisabled, setIsButtonDisabled] = useState(false); + const [isConnectSuccess, setIsConnectSuccess] = useState(false); + const [isConnectFailed, setIsConnectFailed] = useState(false); + const [isTxRejected, setIsTxRejected] = useState(false); + + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + const { t: signatureRequestTranslate } = useTranslation('translation', { + keyPrefix: 'SIGNATURE_REQUEST', + }); + const { selectedAccount } = useWalletSelector(); + + const hideBackButton = !onBackClick; + + const onConfirmPress = async () => { + if (!isLedgerAccount(selectedAccount)) { + return onConfirm(); + } + + // show ledger connection screens + setIsModalVisible(true); + }; + + const handleConnectAndConfirm = async () => { + if (!selectedAccount) { + console.error('No account selected'); + return; + } + setIsButtonDisabled(true); + + const transport = await TransportFactory.create(); + + if (!transport) { + setIsConnectSuccess(false); + setIsConnectFailed(true); + setIsButtonDisabled(false); + return; + } + + setIsConnectSuccess(true); + await delay(1500); + setCurrentStepIndex(1); + + try { + onConfirm(transport); + } catch (err) { + console.error(err); + setIsTxRejected(true); + } + }; + + const handleRetry = async () => { + setIsTxRejected(false); + setIsConnectSuccess(false); + setCurrentStepIndex(0); + }; + + // TODO: this is a bit naive, but should be correct. We may want to look at the sig hash types of the inputs instead + const isPartialTransaction = !feeOutput; + + return isLoading ? ( + + + + ) : ( + <> + + + {t('REVIEW_TRANSACTION')} + + {!isBroadcast && } + + {!isLoading && ( + + + + + )} + + setIsModalVisible(false)}> + {currentStepIndex === 0 && ( + + )} + {currentStepIndex === 1 && ( + + )} + + + + + + + ); +} + +export default ConfirmBtcTransaction; diff --git a/src/app/components/confirmBtcTransaction/itemRow/amount.tsx b/src/app/components/confirmBtcTransaction/itemRow/amount.tsx new file mode 100644 index 000000000..23b7a8894 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/itemRow/amount.tsx @@ -0,0 +1,86 @@ +import TokenImage from '@components/tokenImage'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { currencySymbolMap, getBtcFiatEquivalent, satsToBtc } from '@secretkeylabs/xverse-core'; +import Avatar from '@ui-library/avatar'; +import { StyledP } from '@ui-library/common.styled'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { NumericFormat } from 'react-number-format'; +import styled from 'styled-components'; + +type Props = { + amount: number; +}; + +const RowCenter = styled.div<{ spaceBetween?: boolean }>((props) => ({ + display: 'flex', + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: props.spaceBetween ? 'space-between' : 'initial', +})); + +const NumberTypeContainer = styled.div` + text-align: right; +`; + +const AvatarContainer = styled.div` + margin-right: ${(props) => props.theme.space.xs}; +`; + +export default function Amount({ amount }: Props) { + const { btcFiatRate, fiatCurrency } = useWalletSelector(); + const { t } = useTranslation('translation'); + + const getFiatAmountString = (amountParam: number, btcFiatRateParam: string) => { + const fiatAmount = getBtcFiatEquivalent( + new BigNumber(amountParam), + BigNumber(btcFiatRateParam), + ); + if (!fiatAmount) { + return ''; + } + + if (fiatAmount.isLessThan(0.01)) { + return `<${currencySymbolMap[fiatCurrency]}0.01 ${fiatCurrency}`; + } + + return ( + `~ ${value}`} + /> + ); + }; + + return ( + + + } /> + + +
+ + {t('CONFIRM_TRANSACTION.AMOUNT')} + +
+ + {value}} + /> + + {getFiatAmountString(amount, btcFiatRate)} + + +
+
+ ); +} diff --git a/src/app/components/confirmBtcTransaction/itemRow/amountWithInscriptionSatribute.tsx b/src/app/components/confirmBtcTransaction/itemRow/amountWithInscriptionSatribute.tsx new file mode 100644 index 000000000..b7f790d4c --- /dev/null +++ b/src/app/components/confirmBtcTransaction/itemRow/amountWithInscriptionSatribute.tsx @@ -0,0 +1,128 @@ +import DropDownIcon from '@assets/img/transactions/dropDownIcon.svg'; +import BundleItem from '@components/confirmBtcTransactionComponent/bundleItem'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { WarningOctagon } from '@phosphor-icons/react'; +import { animated, config, useSpring } from '@react-spring/web'; +import { btcTransaction } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import Divider from '@ui-library/divider'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import Theme from 'theme'; +import { mapTxSatributeInfoToBundleInfo } from '../utils'; +import Inscription from './inscription'; + +const WarningContainer = styled.div` + display: flex; + flex-direction: column; + border-radius: ${(props) => props.theme.space.s}; +`; + +const WarningButton = styled.button` + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + background-color: ${(props) => props.theme.colors.elevation1}; + padding-top: ${(props) => props.theme.space.m}; +`; + +const ItemsContainer = styled(animated.div)((props) => ({ + borderRadius: props.theme.space.s, + backgroundColor: props.theme.colors.elevation2, + padding: `${props.theme.space.s} 0`, + marginTop: props.theme.space.m, +})); + +const Range = styled.div` + padding: 0 ${(props) => props.theme.space.s}; +`; + +const Row = styled.div` + display: flex; + flex-direction: row; + align-items: center; +`; + +const Title = styled(StyledP)` + margin-left: ${(props) => props.theme.space.xxs}; +`; + +export default function AmountWithInscriptionSatribute({ + satributes, + inscriptions, + onShowInscription, +}: { + satributes: btcTransaction.IOSatribute[]; + inscriptions: btcTransaction.IOInscription[]; + onShowInscription: (inscription: btcTransaction.IOInscription) => void; +}) { + const [showBundleDetail, setShowBundleDetail] = useState(false); + + const { t } = useTranslation('translation'); + const { hasActivatedRareSatsKey } = useWalletSelector(); + + const slideInStyles = useSpring({ + config: { ...config.gentle, duration: 400 }, + from: { opacity: 0, height: 0 }, + to: { + opacity: showBundleDetail ? 1 : 0, + height: showBundleDetail ? 'auto' : 0, + }, + }); + + const arrowRotation = useSpring({ + transform: showBundleDetail ? 'rotate(180deg)' : 'rotate(0deg)', + config: { ...config.stiff }, + }); + + // we only show the satributes if the user has activated the rare sats key + const satributesArray = hasActivatedRareSatsKey ? satributes : []; + const amountOfAssets = satributesArray.length + inscriptions.length; + + return amountOfAssets > 0 ? ( + + setShowBundleDetail((prevState) => !prevState)}> + + + + {t( + `CONFIRM_TRANSACTION.${ + satributesArray.length ? 'INSCRIBED_RARE_SATS' : 'INSCRIBED_SATS' + }`, + )} + + + + + + {showBundleDetail && ( + + {inscriptions.map((item: btcTransaction.IOInscription, index: number) => ( +
+ + + + {(satributesArray.length || inscriptions.length > index + 1) && ( + + )} +
+ ))} + {satributesArray.map((item: btcTransaction.IOSatribute, index: number) => ( +
+ + + + {satributesArray.length > index + 1 && } +
+ ))} +
+ )} +
+ ) : null; +} diff --git a/src/app/components/confirmBtcTransaction/itemRow/bundleTxView.tsx b/src/app/components/confirmBtcTransaction/itemRow/bundleTxView.tsx new file mode 100644 index 000000000..da3b22b90 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/itemRow/bundleTxView.tsx @@ -0,0 +1,97 @@ +import Link from '@assets/img/rareSats/link.svg'; +import { CubeTransparent } from '@phosphor-icons/react'; +import { btcTransaction } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { useTranslation } from 'react-i18next'; +import { NumericFormat } from 'react-number-format'; +import styled from 'styled-components'; +import Theme from 'theme'; +import Avatar from '../../../ui-library/avatar'; +import { SatRangeTx } from '../utils'; +import Inscription from './inscription'; +import RareSats from './rareSats'; + +type Props = { + inscriptions: btcTransaction.IOInscription[]; + satributesInfo: { satRanges: SatRangeTx[]; totalExoticSats: number }; + bundleSize: number; + isRareSatsEnabled?: boolean; + onShowInscription: (inscription: btcTransaction.IOInscription) => void; +}; + +const Header = styled.div((props) => ({ + display: 'flex', + flex: 1, + flexDirection: 'row', + alignItems: 'center', + marginBottom: props.theme.space.m, +})); + +const AvatarContainer = styled.div` + margin-right: ${(props) => props.theme.space.xs}; +`; + +const RowsContainer = styled.div` + padding-left: ${(props) => props.theme.space.m}; +`; +const LinkContainer = styled.div` + display: flex; + width: 32px; + justify-content: center; + margin: ${(props) => props.theme.space.xxxs} 0; +`; + +export default function BundleTxView({ + inscriptions, + satributesInfo, + bundleSize, + isRareSatsEnabled, + onShowInscription, +}: Props) { + const { t } = useTranslation('translation'); + + // we only show rare sats if there are any and the user has enabled the feature + const showRareSats = satributesInfo.totalExoticSats > 0 && isRareSatsEnabled; + + return ( + <> +
+ + } /> + +
+ + {t('COMMON.BUNDLE')} + + {bundleSize && ( + ( + + {value} + + )} + /> + )} +
+
+ + {inscriptions.map((inscription, index) => ( +
+ + {!!(inscriptions.length > index + 1 || showRareSats) && ( + + link + + )} +
+ ))} + {showRareSats && } +
+ + ); +} diff --git a/src/app/components/confirmBtcTransaction/itemRow/inscription.tsx b/src/app/components/confirmBtcTransaction/itemRow/inscription.tsx new file mode 100644 index 000000000..400575026 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/itemRow/inscription.tsx @@ -0,0 +1,117 @@ +import OrdinalIcon from '@assets/img/rareSats/ic_ordinal_small_over_card.svg'; +import { Eye } from '@phosphor-icons/react'; +import OrdinalImage from '@screens/ordinals/ordinalImage'; +import { btcTransaction } from '@secretkeylabs/xverse-core'; +import Avatar from '@ui-library/avatar'; +import { StyledP } from '@ui-library/common.styled'; +import { useTranslation } from 'react-i18next'; +import { NumericFormat } from 'react-number-format'; +import styled from 'styled-components'; +import Theme from 'theme'; + +type Props = { + inscription: btcTransaction.IOInscription; + bundleSize?: number; + hideTypeSizeInfo?: boolean; + onShowInscription: (inscription: btcTransaction.IOInscription) => void; +}; + +const RowCenter = styled.div<{ spaceBetween?: boolean; gap?: boolean }>((props) => ({ + display: 'flex', + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: props.spaceBetween ? 'space-between' : 'initial', + gap: props.gap ? props.theme.space.xs : 0, +})); + +const InscriptionNumberContainer = styled.button` + background-color: transparent; +`; + +const NumberTypeContainer = styled.div` + text-align: right; +`; + +const AvatarContainer = styled.div` + margin-right: ${(props) => props.theme.space.xs}; +`; + +const InscriptionNumber = styled(StyledP)` + margin-right: ${(props) => props.theme.space.xs}; +`; +const ContentType = styled(StyledP)` + word-break: break-word; +`; + +export default function Inscription({ + inscription, + bundleSize, + hideTypeSizeInfo = false, + onShowInscription, +}: Props) { + const { t } = useTranslation('translation'); + + return ( + + + + } + /> + + +
+ {!hideTypeSizeInfo && ( + <> + + {t('COMMON.INSCRIPTION')} + + {bundleSize && ( + ( + + {value} + + )} + /> + )} + + )} +
+ + { + onShowInscription(inscription); + }} + > + + + {inscription.number} + + + + + + {inscription.contentType} + + +
+
+ ); +} diff --git a/src/app/components/confirmBtcTransaction/itemRow/inscriptionSatributeRow.tsx b/src/app/components/confirmBtcTransaction/itemRow/inscriptionSatributeRow.tsx new file mode 100644 index 000000000..beca09ac1 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/itemRow/inscriptionSatributeRow.tsx @@ -0,0 +1,80 @@ +import useWalletSelector from '@hooks/useWalletSelector'; +import { btcTransaction } from '@secretkeylabs/xverse-core'; +import Divider from '@ui-library/divider'; +import styled from 'styled-components'; +import { getSatRangesWithInscriptions } from '../utils'; +import Amount from './amount'; +import BundleTxView from './bundleTxView'; +import Inscription from './inscription'; +import RareSats from './rareSats'; + +const RowContainer = styled.div((props) => ({ + padding: `0 ${props.theme.space.m}`, +})); + +type Props = { + inscriptions: btcTransaction.IOInscription[]; + satributes: btcTransaction.IOSatribute[]; + amount: number; + showBottomDivider?: boolean; + showTopDivider?: boolean; + onShowInscription: (inscription: btcTransaction.IOInscription) => void; +}; + +function InscriptionSatributeRow({ + inscriptions, + satributes, + amount, + showBottomDivider, + showTopDivider, + onShowInscription, +}: Props) { + const { hasActivatedRareSatsKey } = useWalletSelector(); + + const satributesInfo = getSatRangesWithInscriptions({ + satributes, + inscriptions, + amount, + }); + + const getRow = () => { + if (inscriptions.length > 0 && inscriptions.length + satributes.length > 1) { + return ( + + ); + } + + if (inscriptions.length) { + return ( + + ); + } + + // if rare sats is disabled we show the amount of btc + if (!hasActivatedRareSatsKey) { + return ; + } + + return ; + }; + + return ( + <> + {showTopDivider && } + {getRow()} + {showBottomDivider && } + + ); +} + +export default InscriptionSatributeRow; diff --git a/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx b/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx new file mode 100644 index 000000000..ee362347c --- /dev/null +++ b/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx @@ -0,0 +1,125 @@ +import DropDownIcon from '@assets/img/transactions/dropDownIcon.svg'; +import BundleItem from '@components/confirmBtcTransactionComponent/bundleItem'; +import { Butterfly } from '@phosphor-icons/react'; +import { animated, config, useSpring } from '@react-spring/web'; +import { StyledP } from '@ui-library/common.styled'; +import Divider from '@ui-library/divider'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { NumericFormat } from 'react-number-format'; +import styled from 'styled-components'; +import Theme from 'theme'; +import Avatar from '../../../ui-library/avatar'; +import { SatRangeTx, mapTxSatributeInfoToBundleInfo } from '../utils'; + +const SatsBundleContainer = styled.div` + display: flex; + flex-direction: column; + border-radius: ${(props) => props.theme.space.s}; +`; + +const SatsBundleButton = styled.button` + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + background-color: ${(props) => props.theme.colors.elevation1}; +`; + +const RangesContainer = styled(animated.div)((props) => ({ + borderRadius: props.theme.space.s, + backgroundColor: props.theme.colors.elevation2, + padding: `${props.theme.space.s} 0`, + marginTop: props.theme.space.m, +})); + +const Range = styled.div` + padding: 0 ${(props) => props.theme.space.s}; +`; + +const Row = styled.div` + display: flex; + flex-direction: row; + align-items: center; +`; + +const BundleInfo = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + margin-left: ${(props) => props.theme.space.s}; +`; + +function RareSats({ + satributesInfo, + bundleSize, +}: { + satributesInfo: { satRanges: SatRangeTx[]; totalExoticSats: number }; + bundleSize?: number; +}) { + const [showBundleDetail, setShowBundleDetail] = useState(false); + + const { t } = useTranslation('translation'); + + const slideInStyles = useSpring({ + config: { ...config.gentle, duration: 400 }, + from: { opacity: 0, height: 0 }, + to: { + opacity: showBundleDetail ? 1 : 0, + height: showBundleDetail ? 'auto' : 0, + }, + }); + + const arrowRotation = useSpring({ + transform: showBundleDetail ? 'rotate(180deg)' : 'rotate(0deg)', + config: { ...config.stiff }, + }); + + return ( + + setShowBundleDetail((prevState) => !prevState)} + > + + } /> + + {`${ + satributesInfo.totalExoticSats + } ${t('NFT_DASHBOARD_SCREEN.RARE_SATS')}`} + {bundleSize && ( + ( + + {value} + + )} + /> + )} + + + + + + {showBundleDetail && ( + + {satributesInfo.satRanges.map((item: SatRangeTx, index: number) => ( +
+ + + + {satributesInfo.satRanges.length > index + 1 && } +
+ ))} +
+ )} +
+ ); +} + +export default RareSats; diff --git a/src/app/components/confirmBtcTransaction/receiveSection.tsx b/src/app/components/confirmBtcTransaction/receiveSection.tsx new file mode 100644 index 000000000..9aa8e6923 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/receiveSection.tsx @@ -0,0 +1,126 @@ +import useWalletSelector from '@hooks/useWalletSelector'; +import { ArrowRight } from '@phosphor-icons/react'; +import { btcTransaction } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import Theme from 'theme'; +import Amount from './itemRow/amount'; +import InscriptionSatributeRow from './itemRow/inscriptionSatributeRow'; +import { getOutputsWithAssetsToUserAddress } from './utils'; + +const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + background: props.theme.colors.elevation1, + borderRadius: 12, + padding: `${props.theme.space.m} 0`, + justifyContent: 'center', + marginBottom: props.theme.space.s, +})); + +const RowCenter = styled.div<{ spaceBetween?: boolean }>((props) => ({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: props.spaceBetween ? 'space-between' : 'initial', +})); + +const Header = styled(RowCenter)((props) => ({ + marginBottom: props.theme.space.m, + padding: `0 ${props.theme.space.m}`, +})); + +const RowContainer = styled.div((props) => ({ + padding: `0 ${props.theme.space.m}`, +})); +const AddressLabel = styled(StyledP)((props) => ({ + marginLeft: props.theme.space.xxs, +})); + +type Props = { + outputs: btcTransaction.EnhancedOutput[]; + netAmount: number; + onShowInscription: (inscription: btcTransaction.IOInscription) => void; +}; +function ReceiveSection({ outputs, netAmount, onShowInscription }: Props) { + const { btcAddress, ordinalsAddress } = useWalletSelector(); + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + + const { outputsToPayment, outputsToOrdinal } = getOutputsWithAssetsToUserAddress({ + outputs, + btcAddress, + ordinalsAddress, + }); + + const inscriptionsRareSatsInPayment = outputsToPayment.filter( + (output) => output.inscriptions.length > 0 || output.satributes.length > 0, + ); + const areInscriptionsRareSatsInPayment = inscriptionsRareSatsInPayment.length > 0; + const showPaymentSection = areInscriptionsRareSatsInPayment || netAmount > 0; + + return ( + <> + {!!outputsToOrdinal.length && ( + +
+ + {t('YOU_WILL_RECEIVE')} + + + + {t('YOUR_ORDINAL_ADDRESS')} + +
+ {outputsToOrdinal + .sort((a, b) => b.inscriptions.length - a.inscriptions.length) + .map((output, index) => ( + index + 1} + /> + ))} +
+ )} + {showPaymentSection && ( + +
+ + {t('YOU_WILL_RECEIVE')} + + + + {t('YOUR_PAYMENT_ADDRESS')} + +
+ {netAmount > 0 && ( + + + + )} + {inscriptionsRareSatsInPayment + .sort((a, b) => b.inscriptions.length - a.inscriptions.length) + .map((output, index) => ( + index + 1} + /> + ))} +
+ )} + + ); +} + +export default ReceiveSection; diff --git a/src/app/components/confirmBtcTransaction/transactionSummary.tsx b/src/app/components/confirmBtcTransaction/transactionSummary.tsx new file mode 100644 index 000000000..013071812 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/transactionSummary.tsx @@ -0,0 +1,147 @@ +import TransactionDetailComponent from '@components/transactionDetailComponent'; +import useWalletSelector from '@hooks/useWalletSelector'; + +import AssetModal from '@components/assetModal'; +import TransferFeeView from '@components/transferFeeView'; +import { btcTransaction } from '@secretkeylabs/xverse-core'; +import Callout from '@ui-library/callout'; +import { BLOG_LINK } from '@utils/constants'; +import BigNumber from 'bignumber.js'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import ReceiveSection from './receiveSection'; +import TransferSection from './transferSection'; +import TxInOutput from './txInOutput/txInOutput'; +import { getNetAmount, isScriptOutput, isSpendOutput } from './utils'; + +const ScriptCallout = styled(Callout)` + margin-bottom: ${(props) => props.theme.space.s}; +`; +const InscribedRareSatWarning = styled(Callout)` + margin-bottom: ${(props) => props.theme.space.m}; +`; + +const UnconfirmedInputCallout = styled(Callout)` + margin-bottom: ${(props) => props.theme.space.m}; +`; + +type Props = { + isPartialTransaction: boolean; + + inputs: btcTransaction.EnhancedInput[]; + outputs: btcTransaction.EnhancedOutput[]; + feeOutput?: btcTransaction.TransactionFeeOutput; + + // TODO: these are for txn screens which we will tackle next + // TODO: By having these as generic props here, we can use the generic set fee rate component for all use cases + getFeeForFeeRate?: (feeRate: number, useEffectiveFeeRate?: boolean) => Promise; + onFeeRateSet?: (feeRate: number) => void; + // TODO: use this to disable the edit fee component when it is created + isSubmitting?: boolean; +}; + +function TransactionSummary({ + inputs, + outputs, + feeOutput, + isPartialTransaction, + isSubmitting, + getFeeForFeeRate, + onFeeRateSet, +}: Props) { + const [inscriptionToShow, setInscriptionToShow] = useState< + btcTransaction.IOInscription | undefined + >(undefined); + + const { network } = useWalletSelector(); + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + const { t: rareSatsT } = useTranslation('translation', { keyPrefix: 'RARE_SATS' }); + const { btcAddress, ordinalsAddress } = useWalletSelector(); + + const hasOutputScript = outputs.some((output) => isScriptOutput(output)); + + const netAmount = getNetAmount({ + inputs, + outputs, + btcAddress, + ordinalsAddress, + }); + + const isUnConfirmedInput = inputs.some((input) => !input.extendedUtxo.utxo.status.confirmed); + + const paymentHasInscribedRareSats = isPartialTransaction + ? inputs.some( + (input) => + input.extendedUtxo.address === btcAddress && + (input.inscriptions.length || input.satributes.length), + ) + : outputs.some( + (output) => + isSpendOutput(output) && + (output.inscriptions.some((inscription) => inscription.fromAddress === btcAddress) || + output.satributes.some((satribute) => satribute.fromAddress === btcAddress)), + ); + const feesHaveInscribedRareSats = feeOutput?.inscriptions.length || feeOutput?.satributes.length; + const showInscribeRareSatWarning = paymentHasInscribedRareSats || feesHaveInscribedRareSats; + + return ( + <> + {inscriptionToShow && ( + setInscriptionToShow(undefined)} + inscription={{ + content_type: inscriptionToShow.contentType, + id: inscriptionToShow.id, + inscription_number: inscriptionToShow.number, + }} + /> + )} + + {!!showInscribeRareSatWarning && ( + + )} + + {isUnConfirmedInput && ( + + )} + + + + + + + + {hasOutputScript && } + + + + {feeOutput && ( + + )} + + ); +} + +export default TransactionSummary; diff --git a/src/app/components/confirmBtcTransaction/transferSection.tsx b/src/app/components/confirmBtcTransaction/transferSection.tsx new file mode 100644 index 000000000..ab3b917d9 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/transferSection.tsx @@ -0,0 +1,122 @@ +import useWalletSelector from '@hooks/useWalletSelector'; +import { btcTransaction } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import Amount from './itemRow/amount'; +import AmountWithInscriptionSatribute from './itemRow/amountWithInscriptionSatribute'; +import InscriptionSatributeRow from './itemRow/inscriptionSatributeRow'; +import { getInputsWitAssetsFromUserAddress, getOutputsWithAssetsFromUserAddress } from './utils'; + +const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + background: props.theme.colors.elevation1, + borderRadius: 12, + padding: `${props.theme.space.m} 0`, + justifyContent: 'center', + marginBottom: props.theme.space.s, +})); + +const RowContainer = styled.div((props) => ({ + padding: `0 ${props.theme.space.m}`, +})); + +const RowCenter = styled.div<{ spaceBetween?: boolean }>((props) => ({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: props.spaceBetween ? 'space-between' : 'initial', +})); + +const Header = styled(RowCenter)((props) => ({ + marginBottom: props.theme.space.m, + padding: `0 ${props.theme.space.m}`, +})); + +type Props = { + outputs: btcTransaction.EnhancedOutput[]; + inputs: btcTransaction.EnhancedInput[]; + isPartialTransaction: boolean; + netAmount: number; + onShowInscription: (inscription: btcTransaction.IOInscription) => void; +}; + +// if isPartialTransaction, we use inputs instead of outputs +function TransferSection({ + outputs, + inputs, + isPartialTransaction, + netAmount, + onShowInscription, +}: Props) { + const { btcAddress, ordinalsAddress } = useWalletSelector(); + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + + const { inputFromPayment, inputFromOrdinal } = getInputsWitAssetsFromUserAddress({ + inputs, + btcAddress, + ordinalsAddress, + }); + const { outputsFromPayment, outputsFromOrdinal } = getOutputsWithAssetsFromUserAddress({ + outputs, + btcAddress, + ordinalsAddress, + }); + + const showAmount = netAmount > 0; + + const inscriptionsFromPayment: btcTransaction.IOInscription[] = []; + const satributesFromPayment: btcTransaction.IOSatribute[] = []; + (isPartialTransaction ? inputFromPayment : outputsFromPayment).forEach((item) => { + inscriptionsFromPayment.push(...item.inscriptions); + satributesFromPayment.push(...item.satributes); + }); + + return ( + +
+ + {t('YOU_WILL_TRANSFER')} + +
+ {showAmount && ( + + + + + )} + {isPartialTransaction + ? inputFromOrdinal.map((input, index) => ( + index + 1} + /> + )) + : outputsFromOrdinal.map((output, index) => ( + index + 1} + /> + ))} +
+ ); +} + +export default TransferSection; diff --git a/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx b/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx new file mode 100644 index 000000000..20e9469c1 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx @@ -0,0 +1,85 @@ +import IconBitcoin from '@assets/img/dashboard/bitcoin_icon.svg'; +import TransferDetailView from '@components/transferDetailView'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { btcTransaction, satsToBtc } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { getTruncatedAddress } from '@utils/helper'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; + +const TransferDetailContainer = styled.div((props) => ({ + paddingBottom: props.theme.space.m, +})); + +const SubValueText = styled(StyledP)((props) => ({ + color: props.theme.colors.white_400, +})); + +const TxIdText = styled(StyledP)((props) => ({ + marginLeft: props.theme.space.xxs, +})); + +const YourAddressText = styled(StyledP)((props) => ({ + marginRight: props.theme.space.xxs, +})); + +const TxIdContainer = styled.div({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', +}); + +type Props = { + input: btcTransaction.EnhancedInput; +}; + +function TransactionInput({ input }: Props) { + const { btcAddress, ordinalsAddress } = useWalletSelector(); + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + + const isPaymentsAddress = input.extendedUtxo.address === btcAddress; + const isOrdinalsAddress = input.extendedUtxo.address === ordinalsAddress; + const isExternalInput = !isPaymentsAddress && !isOrdinalsAddress; + + // TODO: show this in the UI? + // const insecureInput = + // input.sigHash === btc.SigHash.NONE || input.sigHash === btc.SigHash.NONE_ANYONECANPAY; + + const renderAddress = (addressToBeDisplayed: string) => + addressToBeDisplayed === btcAddress || addressToBeDisplayed === ordinalsAddress ? ( + + ({t('YOUR_ADDRESS')}) + {getTruncatedAddress(addressToBeDisplayed)} + + ) : ( + {getTruncatedAddress(addressToBeDisplayed)} + ); + + return ( + + + {isExternalInput ? ( + + + {getTruncatedAddress(input.extendedUtxo.utxo.txid)} + + (txid) + + ) : ( + renderAddress(input.extendedUtxo.address) + )} + + + ); +} + +export default TransactionInput; diff --git a/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx b/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx new file mode 100644 index 000000000..43c8ab099 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx @@ -0,0 +1,78 @@ +import ScriptIcon from '@assets/img/transactions/ScriptIcon.svg'; +import OutputIcon from '@assets/img/transactions/output.svg'; +import TransferDetailView from '@components/transferDetailView'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { btcTransaction, satsToBtc } from '@secretkeylabs/xverse-core'; +import { getTruncatedAddress } from '@utils/helper'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import { isScriptOutput, isSpendOutput } from '../utils'; + +const TransferDetailContainer = styled.div((props) => ({ + paddingBottom: props.theme.spacing(8), +})); + +const SubValueText = styled.h1((props) => ({ + ...props.theme.typography.body_m, + fontSize: 12, + color: props.theme.colors.white_400, +})); + +const YourAddressText = styled.h1((props) => ({ + ...props.theme.typography.body_m, + fontSize: 12, + color: props.theme.colors.white_0, + marginRight: props.theme.spacing(2), +})); + +const TxIdContainer = styled.div({ + display: 'flex', + flexDirection: 'row', +}); + +type Props = { + output: btcTransaction.EnhancedOutput; + scriptOutputCount?: number; +}; + +function TransactionOutput({ output, scriptOutputCount }: Props) { + const { btcAddress, ordinalsAddress } = useWalletSelector(); + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + const outputWithScript = isScriptOutput(output); + + const detailViewIcon = outputWithScript ? ScriptIcon : OutputIcon; + const detailViewHideCopyButton = outputWithScript + ? true + : btcAddress === output.address || ordinalsAddress === output.address; + const detailViewValue = outputWithScript ? ( + {`${t('SCRIPT_OUTPUT')} #${scriptOutputCount}`} + ) : output.address === btcAddress || output.address === ordinalsAddress ? ( + + ({t('YOUR_ADDRESS')}) + {getTruncatedAddress(output.address)} + + ) : ( + {getTruncatedAddress(output.address)} + ); + + return ( + + + {detailViewValue} + + + ); +} + +export default TransactionOutput; diff --git a/src/app/components/confirmBtcTransaction/txInOutput/txInOutput.tsx b/src/app/components/confirmBtcTransaction/txInOutput/txInOutput.tsx new file mode 100644 index 000000000..2ff4e71f0 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/txInOutput/txInOutput.tsx @@ -0,0 +1,103 @@ +import DropDownIcon from '@assets/img/transactions/dropDownIcon.svg'; +import { animated, config, useSpring } from '@react-spring/web'; +import { btcTransaction } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import { isScriptOutput } from '../utils'; +import TransactionInput from './transactionInput'; +import TransactionOutput from './transactionOutput'; + +const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + borderRadius: props.theme.space.s, + background: props.theme.colors.elevation1, + padding: `${props.theme.space.s} ${props.theme.space.m}`, + marginBottom: props.theme.space.s, +})); + +const Button = styled.button` + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + background-color: ${(props) => props.theme.colors.elevation1}; +`; + +const Row = styled.div` + display: flex; + flex-direction: row; + align-items: center; +`; + +const OutputTitleText = styled(StyledP)((props) => ({ + marginBottom: props.theme.space.s, +})); + +const ExpandedContainer = styled(animated.div)({ + display: 'flex', + flexDirection: 'column', + marginTop: 16, +}); + +type Props = { + inputs: btcTransaction.EnhancedInput[]; + outputs: btcTransaction.EnhancedOutput[]; +}; + +function TxInOutput({ inputs, outputs }: Props) { + const [isExpanded, setIsExpanded] = useState(false); + + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + + const slideInStyles = useSpring({ + config: { ...config.gentle, duration: 400 }, + from: { opacity: 0, height: 0 }, + to: { + opacity: isExpanded ? 1 : 0, + height: isExpanded ? 'auto' : 0, + }, + }); + + const arrowRotation = useSpring({ + transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)', + config: { ...config.stiff }, + }); + + let scriptOutputCount = 1; + + return ( + + + {isExpanded && ( + + {inputs.map((input) => ( + + ))} + + {t('OUTPUT')} + + {outputs.map((output, index) => ( + + ))} + + )} + + ); +} + +export default TxInOutput; diff --git a/src/app/components/confirmBtcTransaction/utils.ts b/src/app/components/confirmBtcTransaction/utils.ts new file mode 100644 index 000000000..ededa2dbb --- /dev/null +++ b/src/app/components/confirmBtcTransaction/utils.ts @@ -0,0 +1,269 @@ +import { btcTransaction, BundleSatRange } from '@secretkeylabs/xverse-core'; + +export type SatRangeTx = { + totalSats: number; + offset: number; + fromAddress: string; + inscriptions: (Omit & { + content_type: string; + inscription_number: number; + })[]; + satributes: btcTransaction.IOSatribute['types']; +}; + +const DUMMY_OFFSET = -1; + +export const isScriptOutput = ( + output: btcTransaction.EnhancedOutput, +): output is btcTransaction.TransactionScriptOutput => + (output as btcTransaction.TransactionScriptOutput).script !== undefined; + +export const isSpendOutput = ( + output: btcTransaction.EnhancedOutput, +): output is btcTransaction.TransactionOutput => + (output as btcTransaction.TransactionOutput).address !== undefined; + +type CommonInputOutputUtilProps = { + inputs: btcTransaction.EnhancedInput[]; + outputs: btcTransaction.EnhancedOutput[]; + btcAddress: string; + ordinalsAddress: string; +}; + +export const getNetAmount = ({ + inputs, + outputs, + btcAddress, + ordinalsAddress, +}: CommonInputOutputUtilProps) => { + const initialValue = 0; + + const totalUserSpend = inputs.reduce((accumulator: number, input) => { + const isFromUserAddress = [btcAddress, ordinalsAddress].includes(input.extendedUtxo.address); + if (isFromUserAddress) { + return accumulator + input.extendedUtxo.utxo.value; + } + return accumulator; + }, initialValue); + + const totalUserReceive = outputs.reduce((accumulator: number, output) => { + const isToUserAddress = + isSpendOutput(output) && [btcAddress, ordinalsAddress].includes(output.address); + if (isToUserAddress) { + return accumulator + output.amount; + } + return accumulator; + }, initialValue); + + return totalUserReceive - totalUserSpend; +}; + +export const getOutputsWithAssetsFromUserAddress = ({ + btcAddress, + ordinalsAddress, + outputs, +}: Omit) => { + // we want to discard outputs that are script, are not from user address and do not have inscriptions or satributes + const outputsFromPayment: btcTransaction.TransactionOutput[] = []; + const outputsFromOrdinal: btcTransaction.TransactionOutput[] = []; + outputs.forEach((output) => { + if (isScriptOutput(output)) { + return; + } + + const itemsFromPayment: (btcTransaction.IOInscription | btcTransaction.IOSatribute)[] = []; + const itemsFromOrdinal: (btcTransaction.IOInscription | btcTransaction.IOSatribute)[] = []; + [...output.inscriptions, ...output.satributes].forEach((item) => { + if (item.fromAddress === btcAddress) { + return itemsFromPayment.push(item); + } + if (item.fromAddress === ordinalsAddress) { + itemsFromOrdinal.push(item); + } + }); + + if (itemsFromOrdinal.length > 0) { + outputsFromOrdinal.push(output); + } + if (itemsFromPayment.length > 0) { + outputsFromPayment.push(output); + } + }); + + return { outputsFromPayment, outputsFromOrdinal }; +}; + +export const getInputsWitAssetsFromUserAddress = ({ + btcAddress, + ordinalsAddress, + inputs, +}: Omit) => { + // we want to discard inputs that are not from user address and do not have inscriptions or satributes + const inputFromPayment: btcTransaction.EnhancedInput[] = []; + const inputFromOrdinal: btcTransaction.EnhancedInput[] = []; + inputs.forEach((input) => { + if (!input.inscriptions.length && !input.satributes.length) { + return; + } + + if (input.extendedUtxo.address === btcAddress) { + return inputFromPayment.push(input); + } + if (input.extendedUtxo.address === ordinalsAddress) { + inputFromOrdinal.push(input); + } + }); + + return { inputFromPayment, inputFromOrdinal }; +}; + +export const getOutputsWithAssetsToUserAddress = ({ + btcAddress, + ordinalsAddress, + outputs, +}: Omit) => { + const outputsToPayment: btcTransaction.TransactionOutput[] = []; + const outputsToOrdinal: btcTransaction.TransactionOutput[] = []; + outputs.forEach((output) => { + // we want to discard outputs that are not spendable or are not to user address + if (isScriptOutput(output) || ![btcAddress, ordinalsAddress].includes(output.address)) { + return; + } + + if (output.address === btcAddress) { + return outputsToPayment.push(output); + } + + // we don't want to show amount to ordinals address, because it's not spendable + if ( + output.address === ordinalsAddress && + (output.inscriptions.length > 0 || output.satributes.length > 0) + ) { + outputsToOrdinal.push(output); + } + }); + + return { outputsToPayment, outputsToOrdinal }; +}; + +export const mapTxSatributeInfoToBundleInfo = (item: btcTransaction.IOSatribute | SatRangeTx) => { + const commonProps = { + offset: item.offset, + block: 0, + range: { + start: '0', + end: '0', + }, + yearMined: 0, + }; + + // SatRangeTx + if ('totalSats' in item) { + return { + ...commonProps, + totalSats: item.totalSats, + inscriptions: item.inscriptions, + satributes: item.satributes, + } as BundleSatRange; + } + + // btcTransaction.IOSatribute + return { + ...commonProps, + totalSats: item.amount, + inscriptions: [], + satributes: item.types, + } as BundleSatRange; +}; + +export const getSatRangesWithInscriptions = ({ + satributes, + inscriptions, + amount, +}: { + inscriptions: btcTransaction.IOInscription[]; + satributes: btcTransaction.IOSatribute[]; + amount: number; +}) => { + const satRanges: { + [offset: number]: SatRangeTx; + } = {}; + + satributes.forEach((satribute) => { + const { types, amount: totalSats, ...rest } = satribute; + satRanges[rest.offset] = { ...rest, satributes: types, totalSats, inscriptions: [] }; + }); + + inscriptions.forEach((inscription) => { + const { contentType, number, ...inscriptionRest } = inscription; + const mappedInscription = { + ...inscriptionRest, + content_type: contentType, + inscription_number: number, + }; + if (satRanges[inscription.offset]) { + satRanges[inscription.offset] = { + ...satRanges[inscription.offset], + inscriptions: [...satRanges[inscription.offset].inscriptions, mappedInscription], + }; + return; + } + + satRanges[inscription.offset] = { + totalSats: 1, + offset: inscription.offset, + fromAddress: inscription.fromAddress, + inscriptions: [mappedInscription], + satributes: ['COMMON'], + }; + }); + + const { amountOfExoticsOrInscribedSats, totalExoticSats } = Object.values(satRanges).reduce( + (acc, range) => ({ + amountOfExoticsOrInscribedSats: acc.amountOfExoticsOrInscribedSats + range.totalSats, + totalExoticSats: + acc.totalExoticSats + (!range.satributes.includes('COMMON') ? range.totalSats : 0), + }), + { + amountOfExoticsOrInscribedSats: 0, + totalExoticSats: 0, + }, + ); + + if (amountOfExoticsOrInscribedSats < amount) { + satRanges[DUMMY_OFFSET] = { + totalSats: amount - amountOfExoticsOrInscribedSats, + offset: DUMMY_OFFSET, + fromAddress: '', + inscriptions: [], + satributes: ['COMMON'], + }; + } + + // sort should be: inscribed rare, rare, inscribed common, common + const satRangesArray = Object.values(satRanges).sort((a, b) => { + // Check conditions for each category + const aHasInscriptions = a.inscriptions.length > 0; + const bHasInscriptions = b.inscriptions.length > 0; + const aHasRareSatributes = a.satributes.some((s) => s !== 'COMMON'); + const bHasRareSatributes = b.satributes.some((s) => s !== 'COMMON'); + + // sats not rare and not inscribed at bottom + if (!aHasInscriptions && !aHasRareSatributes) return 1; + + // sats inscribed and rare at top + if (aHasInscriptions && aHasRareSatributes) return -1; + + // sats not inscribed and rare below inscribed and rare + if (bHasInscriptions && bHasRareSatributes) return 1; + + // sats inscribed and not rare above sats not inscribed and not rare + if (aHasRareSatributes) return -1; + if (bHasRareSatributes) return 1; + + // equal ranges + return 0; + }); + + return { satRanges: satRangesArray, totalExoticSats }; +}; diff --git a/src/app/components/confirmBtcTransactionComponent/bundle.tsx b/src/app/components/confirmBtcTransactionComponent/bundle.tsx index 84b728b71..eeabbce28 100644 --- a/src/app/components/confirmBtcTransactionComponent/bundle.tsx +++ b/src/app/components/confirmBtcTransactionComponent/bundle.tsx @@ -3,6 +3,7 @@ import AssetModal from '@components/assetModal'; import { CaretDown } from '@phosphor-icons/react'; import { Bundle, BundleSatRange, SatRangeInscription } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; +import Divider from '@ui-library/divider'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; @@ -97,8 +98,8 @@ function SatsBundle({ bundle, title }: { bundle: Bundle; title?: string }) { // show ordinal modal to show asset setInscriptionToShow(inscription); }} - showDivider={index !== bundle.satRanges.length - 1} /> + {bundle.satRanges.length > index + 1 && } ))} diff --git a/src/app/components/confirmBtcTransactionComponent/bundleItem.tsx b/src/app/components/confirmBtcTransactionComponent/bundleItem.tsx index 009220d0b..8bb38bd77 100644 --- a/src/app/components/confirmBtcTransactionComponent/bundleItem.tsx +++ b/src/app/components/confirmBtcTransactionComponent/bundleItem.tsx @@ -20,19 +20,11 @@ const Range = styled.div` align-items: center; `; -interface ComponentWithDividerProps { - showDivider: boolean; -} - -const Container = styled.div` - padding-top:${(props) => props.theme.space.s}; - padding-bottom:${(props) => props.theme.space.s}; +const Container = styled.div` display: flex; flex-direction: row; justify-content: space-between; align-items: center; - border-bottom: ${(props) => - props.showDivider ? `1px solid ${props.theme.colors.white_900}` : 'transparent'}; width: 100%; }`; @@ -52,6 +44,7 @@ const InscriptionRow = styled.button` flex-direction: row; align-items: center; background-color: transparent; + cursor: ${(props) => (props.disabled ? 'default' : 'pointer')}; `; const InscriptionText = styled(StyledP)` text-wrap: nowrap; @@ -69,11 +62,9 @@ const BundleText = styled(StyledP)` function BundleItem({ item, ordinalEyePressed, - showDivider, }: { item: BundleSatRange; - ordinalEyePressed: (inscription: SatRangeInscription) => void; - showDivider?: boolean; + ordinalEyePressed?: (inscription: SatRangeInscription) => void; }) { const renderedIcons = () => ( @@ -90,7 +81,7 @@ function BundleItem({ ); return ( - + {renderedIcons()} {getSatLabel(item.satributes)} @@ -110,14 +101,15 @@ function BundleItem({ type="button" key={inscription.id} onClick={() => { - ordinalEyePressed(inscription); + ordinalEyePressed?.(inscription); }} + disabled={!ordinalEyePressed} > ordinal {inscription.inscription_number} - + {ordinalEyePressed && } ))} diff --git a/src/app/components/screenContainer/index.tsx b/src/app/components/screenContainer/index.tsx index 233f2df2a..7152b206c 100644 --- a/src/app/components/screenContainer/index.tsx +++ b/src/app/components/screenContainer/index.tsx @@ -8,7 +8,8 @@ const RouteContainer = styled.div` // any route should default to the chrome extension window size display: flex; flex-direction: column; - height: 600px; + height: 100%; + max-height: 600px; width: 360px; margin: auto; background-color: ${(props) => props.theme.colors.elevation0}; diff --git a/src/app/components/transferFeeView/index.tsx b/src/app/components/transferFeeView/index.tsx index 5f0848a2e..75ade94fa 100644 --- a/src/app/components/transferFeeView/index.tsx +++ b/src/app/components/transferFeeView/index.tsx @@ -1,29 +1,30 @@ +import AmountWithInscriptionSatribute from '@components/confirmBtcTransaction/itemRow/amountWithInscriptionSatribute'; import { + btcTransaction, currencySymbolMap, getBtcFiatEquivalent, getFiatEquivalent, } from '@secretkeylabs/xverse-core'; import { StoreState } from '@stores/index'; +import { StyledP } from '@ui-library/common.styled'; import BigNumber from 'bignumber.js'; import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import { useSelector } from 'react-redux'; import styled from 'styled-components'; -const RowContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', +const Container = styled.div((props) => ({ background: props.theme.colors.elevation1, borderRadius: 12, padding: '12px 16px', - justifyContent: 'center', marginBottom: 12, })); -const FeeText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_0, -})); +const Row = styled.div({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', +}); const FeeTitleContainer = styled.div({ display: 'flex', @@ -37,24 +38,24 @@ const FeeContainer = styled.div({ alignItems: 'flex-end', }); -const TitleText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_200, -})); - -const FiatAmountText = styled.h1((props) => ({ - ...props.theme.body_m, - fontSize: 12, - color: props.theme.colors.white_400, -})); - interface Props { feePerVByte?: BigNumber; fee: BigNumber; currency: string; title?: string; + inscriptions?: btcTransaction.IOInscription[]; + satributes?: btcTransaction.IOSatribute[]; + onShowInscription?: (inscription: btcTransaction.IOInscription) => void; } -function TransferFeeView({ feePerVByte, fee, currency, title }: Props) { +function TransferFeeView({ + feePerVByte, + fee, + currency, + title, + inscriptions = [], + satributes = [], + onShowInscription = () => {}, +}: Props) { const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const { btcFiatRate, stxBtcRate, fiatCurrency } = useSelector( (state: StoreState) => state.walletState, @@ -81,42 +82,59 @@ function TransferFeeView({ feePerVByte, fee, currency, title }: Props) { thousandSeparator prefix={`${currencySymbolMap[fiatCurrency]} `} suffix={` ${fiatCurrency}`} - renderText={(value: string) => {`~ ${value}`}} + renderText={(value: string) => `~ ${value}`} /> ); }; return ( - - - {title ?? t('FEES')} - - - {value}} - /> - {currency === 'sats' && ( + + + + + {title ?? t('FEES')} + + + {value}} + suffix={` ${currency}`} + renderText={(value: string) => ( + + {value} + + )} /> - )} - - {getFiatAmountString( - currency === 'sats' - ? getBtcFiatEquivalent(new BigNumber(fee), BigNumber(btcFiatRate)) - : new BigNumber(fiatRate!), + {currency === 'sats' && feePerVByte && ( + ( + + {value} + + )} + /> )} - - - + + {getFiatAmountString( + currency === 'sats' + ? getBtcFiatEquivalent(new BigNumber(fee), BigNumber(btcFiatRate)) + : new BigNumber(fiatRate!), + )} + + + + + ); } diff --git a/src/app/hooks/queries/useAppConfig.ts b/src/app/hooks/queries/useAppConfig.ts index 733d26ece..1a4c53521 100644 --- a/src/app/hooks/queries/useAppConfig.ts +++ b/src/app/hooks/queries/useAppConfig.ts @@ -1,21 +1,14 @@ import useWalletSelector from '@hooks/useWalletSelector'; import { getAppConfig } from '@secretkeylabs/xverse-core'; -import { ChangeNetworkAction } from '@stores/wallet/actions/actionCreators'; import { useQuery } from '@tanstack/react-query'; -import { useDispatch } from 'react-redux'; const useAppConfig = () => { - const { network, networkAddress, btcApiUrl } = useWalletSelector(); - const dispatch = useDispatch(); + const { network } = useWalletSelector(); return useQuery({ - queryKey: ['app-config', network.type, btcApiUrl], + queryKey: ['app-config', network.type], queryFn: async () => { const response = await getAppConfig(network.type); - if (response.data.btcApiURL && network.type === 'Mainnet' && !btcApiUrl) { - const updatedNetwork = { ...network, btcApiUrl: response.data.btcApiURL }; - dispatch(ChangeNetworkAction(updatedNetwork, networkAddress, '')); - } return response; }, }); diff --git a/src/app/hooks/useBtcClient.ts b/src/app/hooks/useBtcClient.ts index 33993a117..18320c299 100644 --- a/src/app/hooks/useBtcClient.ts +++ b/src/app/hooks/useBtcClient.ts @@ -3,17 +3,18 @@ import { useMemo } from 'react'; import useWalletSelector from './useWalletSelector'; const useBtcClient = () => { - const { network, btcApiUrl } = useWalletSelector(); - const { type, btcApiUrl: remoteBtcApiURL } = network; - const url = btcApiUrl || remoteBtcApiURL; + const { network } = useWalletSelector(); + const { type, btcApiUrl, fallbackBtcApiUrl } = network; + const url = btcApiUrl; const esploraInstance = useMemo( () => new BitcoinEsploraApiProvider({ url, + fallbackUrl: fallbackBtcApiUrl, network: type, }), - [url, type], + [url, fallbackBtcApiUrl, type], ); return esploraInstance; diff --git a/src/app/hooks/useNetwork.ts b/src/app/hooks/useNetwork.ts index 0f757c0cc..c3ce19150 100644 --- a/src/app/hooks/useNetwork.ts +++ b/src/app/hooks/useNetwork.ts @@ -3,14 +3,14 @@ import { useMemo } from 'react'; import useWalletSelector from './useWalletSelector'; const useNetworkSelector = () => { - const { network, networkAddress } = useWalletSelector(); + const { network } = useWalletSelector(); const selectedNetwork = useMemo( () => network.type === 'Mainnet' - ? new StacksMainnet({ url: networkAddress }) - : new StacksTestnet({ url: networkAddress }), - [network.type, networkAddress], + ? new StacksMainnet({ url: network.address }) + : new StacksTestnet({ url: network.address }), + [network.type, network.address], ); return selectedNetwork; }; diff --git a/src/app/hooks/useSignPsbtTx.ts b/src/app/hooks/useSignPsbtTx.ts index 30cf0d0bf..88412b685 100644 --- a/src/app/hooks/useSignPsbtTx.ts +++ b/src/app/hooks/useSignPsbtTx.ts @@ -17,16 +17,21 @@ const useSignPsbtTx = () => { const tabId = params.get('tabId') ?? '0'; const btcClient = useBtcClient(); - const confirmSignPsbt = async () => { - const seedPhrase = await getSeed(); - const signingResponse = await signPsbt( - seedPhrase, - accountsList, - request.payload.inputsToSign, - request.payload.psbtBase64, - request.payload.broadcast, - network.type, - ); + const confirmSignPsbt = async (signingResponseOverride?: string) => { + let signingResponse = signingResponseOverride; + + if (!signingResponse) { + const seedPhrase = await getSeed(); + signingResponse = await signPsbt( + seedPhrase, + accountsList, + request.payload.inputsToSign, + request.payload.psbtBase64, + request.payload.broadcast, + network.type, + ); + } + let txId: string = ''; if (request.payload.broadcast) { const txHex = psbtBase64ToHex(signingResponse); diff --git a/src/app/hooks/useTransactionContext.ts b/src/app/hooks/useTransactionContext.ts new file mode 100644 index 000000000..58fd225c1 --- /dev/null +++ b/src/app/hooks/useTransactionContext.ts @@ -0,0 +1,49 @@ +import { btcTransaction, UtxoCache } from '@secretkeylabs/xverse-core'; +import { useMemo } from 'react'; +import useBtcClient from './useBtcClient'; +import useSeedVault from './useSeedVault'; +import useWalletSelector from './useWalletSelector'; + +const useTransactionContext = () => { + const { selectedAccount, network } = useWalletSelector(); + const seedVault = useSeedVault(); + const btcClient = useBtcClient(); + + const utxoCache = useMemo( + () => + new UtxoCache({ + cacheStorageController: { + get: async (key: string) => { + const value = localStorage.getItem(key); + return value; + }, + set: async (key: string, value: string) => { + localStorage.setItem(key, value); + }, + remove: async (key: string) => { + localStorage.removeItem(key); + }, + }, + network: network.type, + }), + [network.type], + ); + + const transactionContext = useMemo(() => { + if (selectedAccount?.id === undefined) { + throw new Error('No account selected'); + } + + return btcTransaction.createTransactionContext({ + account: selectedAccount, + seedVault, + utxoCache, + network: network.type, + esploraApiProvider: btcClient, + }); + }, [utxoCache, selectedAccount, network, seedVault, btcClient]); + + return transactionContext; +}; + +export default useTransactionContext; diff --git a/src/app/hooks/useWalletReducer.ts b/src/app/hooks/useWalletReducer.ts index 7f642674f..a0a1ad269 100644 --- a/src/app/hooks/useWalletReducer.ts +++ b/src/app/hooks/useWalletReducer.ts @@ -1,22 +1,24 @@ -import { getDeviceAccountIndex } from '@common/utils/ledger'; +import { filterLedgerAccounts, getDeviceAccountIndex } from '@common/utils/ledger'; import useBtcWalletData from '@hooks/queries/useBtcWalletData'; import useStxWalletData from '@hooks/queries/useStxWalletData'; import useNetworkSelector from '@hooks/useNetwork'; import { Account, AnalyticsEvents, + SettingsNetwork, + StacksMainnet, + StacksNetwork, + StacksTestnet, createWalletAccount, decryptSeedPhraseCBC, getBnsName, newWallet, restoreWalletWithAccounts, - SettingsNetwork, - StacksNetwork, walletFromSeedPhrase, } from '@secretkeylabs/xverse-core'; import { - addAccountAction, ChangeNetworkAction, + addAccountAction, fetchAccountAction, getActiveAccountsAction, resetWalletAction, @@ -49,7 +51,8 @@ const useWalletReducer = () => { const dispatch = useDispatch(); const { refetch: refetchStxData } = useStxWalletData(); const { refetch: refetchBtcData } = useBtcWalletData(); - const { setSessionStartTime, clearSessionTime } = useWalletSession(); + const { setSessionStartTime, clearSessionTime, setSessionStartTimeAndMigrate } = + useWalletSession(); const queryClient = useQueryClient(); const loadActiveAccounts = async ( @@ -82,7 +85,16 @@ const useWalletReducer = () => { if (!selectedAccount) { [selectedAccountData] = walletAccounts; } else if (isLedgerAccount(selectedAccount)) { - selectedAccountData = ledgerAccountsList.find((a) => a.id === selectedAccount.id); + const networkLedgerAccounts = filterLedgerAccounts(ledgerAccountsList, currentNetwork.type); + const selectedAccountDataInNetwork = networkLedgerAccounts.find( + (a) => a.id === selectedAccount.id, + ); + + // we try find the specific matching ledger account + // If we can't find it, we default to the first ledger account in the selected network + // If we can't find that, we default to the first software account in the wallet + selectedAccountData = + selectedAccountDataInNetwork ?? networkLedgerAccounts[0] ?? walletAccounts[0]; } else { selectedAccountData = walletAccounts.find((a) => a.id === selectedAccount.id); } @@ -103,6 +115,9 @@ const useWalletReducer = () => { dispatch(fetchAccountAction(selectedAccountData, walletAccounts)); + // ledger accounts initially didn't have a deviceAccountIndex + // this is a migration to add the deviceAccountIndex to the ledger accounts without them + // it should only fire once if ever if (ledgerAccountsList.some((account) => account.deviceAccountIndex === undefined)) { const newLedgerAccountsList = ledgerAccountsList.map((account) => ({ ...account, @@ -142,7 +157,7 @@ const useWalletReducer = () => { dispatch(fetchAccountAction(accountsList[0], accountsList)); dispatch(getActiveAccountsAction(accountsList)); } finally { - setSessionStartTime(); + setSessionStartTimeAndMigrate(); } }; @@ -281,14 +296,9 @@ const useWalletReducer = () => { dispatch(fetchAccountAction(account, accountsList)); }; - const changeNetwork = async ( - changedNetwork: SettingsNetwork, - networkObject: StacksNetwork, - networkAddress: string, - btcApiUrl: string, - ) => { + const changeNetwork = async (changedNetwork: SettingsNetwork) => { const seedPhrase = await seedVault.getSeed(); - dispatch(ChangeNetworkAction(changedNetwork, networkAddress, btcApiUrl)); + dispatch(ChangeNetworkAction(changedNetwork)); const wallet = await walletFromSeedPhrase({ mnemonic: seedPhrase, index: 0n, @@ -305,6 +315,10 @@ const useWalletReducer = () => { stxPublicKey: wallet.stxPublicKey, }; dispatch(setWalletAction(wallet)); + const networkObject = + changedNetwork.type === 'Mainnet' + ? new StacksMainnet({ url: changedNetwork.address }) + : new StacksTestnet({ url: changedNetwork.address }); try { await loadActiveAccounts(wallet.seedPhrase, changedNetwork, networkObject, [account]); } catch (err) { diff --git a/src/app/hooks/useWalletSession.ts b/src/app/hooks/useWalletSession.ts index 95ceb0d5d..730d77b41 100644 --- a/src/app/hooks/useWalletSession.ts +++ b/src/app/hooks/useWalletSession.ts @@ -1,9 +1,9 @@ import useWalletSelector from '@hooks/useWalletSelector'; import { setWalletLockPeriodAction } from '@stores/wallet/actions/actionCreators'; import { WalletSessionPeriods } from '@stores/wallet/actions/types'; +import { chromeSessionStorage } from '@utils/chromeStorage'; import { addMinutes } from 'date-fns'; import { useDispatch } from 'react-redux'; -import { chromeSessionStorage } from '@utils/chromeStorage'; import useSeedVault from './useSeedVault'; const SESSION_START_TIME_KEY = 'sessionStartTime'; @@ -36,11 +36,20 @@ const useWalletSession = () => { setSessionStartTime(); }; + const setSessionStartTimeAndMigrate = () => { + if (walletLockPeriod < WalletSessionPeriods.LOW) { + return setWalletLockPeriod(WalletSessionPeriods.LOW); + } + + setSessionStartTime(); + }; + return { setSessionStartTime, setWalletLockPeriod, shouldLock, clearSessionTime, + setSessionStartTimeAndMigrate, }; }; diff --git a/src/app/screens/accountList/index.tsx b/src/app/screens/accountList/index.tsx index f86106f2d..9e50a792c 100644 --- a/src/app/screens/accountList/index.tsx +++ b/src/app/screens/accountList/index.tsx @@ -1,5 +1,6 @@ import ConnectLedger from '@assets/img/dashboard/connect_ledger.svg'; import Plus from '@assets/img/dashboard/plus.svg'; +import { filterLedgerAccounts } from '@common/utils/ledger'; import AccountRow from '@components/accountRow'; import Separator from '@components/separator'; import TopRow from '@components/topRow'; @@ -85,10 +86,8 @@ function AccountList(): JSX.Element { const { createAccount, switchAccount } = useWalletReducer(); const displayedAccountsList = useMemo(() => { - if (network.type === 'Mainnet') { - return [...ledgerAccountsList, ...accountsList]; - } - return accountsList; + const networkLedgerAccounts = filterLedgerAccounts(ledgerAccountsList, network.type); + return [...networkLedgerAccounts, ...accountsList]; }, [accountsList, ledgerAccountsList, network]); const handleAccountSelect = async (account: Account, goBack = true) => { @@ -140,14 +139,12 @@ function AccountList(): JSX.Element { {t('NEW_ACCOUNT')} - {network.type === 'Mainnet' && ( - - - - - {t('LEDGER_ACCOUNT')} - - )} + + + + + {t('LEDGER_ACCOUNT')} + ); diff --git a/src/app/screens/btcSelectAddressScreen/index.tsx b/src/app/screens/btcSelectAddressScreen/index.tsx index bf2ce535c..5eb6b86a3 100644 --- a/src/app/screens/btcSelectAddressScreen/index.tsx +++ b/src/app/screens/btcSelectAddressScreen/index.tsx @@ -2,6 +2,7 @@ import OrdinalsIcon from '@assets/img/nftDashboard/white_ordinals_icon.svg'; import XverseLogo from '@assets/img/settings/logo.svg'; import DropDownIcon from '@assets/img/transactions/dropDownIcon.svg'; import DappPlaceholderIcon from '@assets/img/webInteractions/authPlaceholder.svg'; +import { filterLedgerAccounts } from '@common/utils/ledger'; import AccountRow from '@components/accountRow'; import ActionButton from '@components/button'; import Separator from '@components/separator'; @@ -234,6 +235,8 @@ function BtcSelectAddressScreen() { switchAccountBasedOnRequest(); }, []); + const networkLedgerAccounts = filterLedgerAccounts(ledgerAccountsList, network.type); + return ( <> @@ -262,7 +265,7 @@ function BtcSelectAddressScreen() { {showAccountList ? ( - {[...ledgerAccountsList, ...accountsList].map((account) => ( + {[...networkLedgerAccounts, ...accountsList].map((account) => ( props.theme.colors.white_0}; `; export const StyledBarLoader = styled(BetterBarLoader)((props) => ({ @@ -132,6 +143,8 @@ interface Props { isSmallImage?: boolean; withoutSizeIncrease?: boolean; withoutTitles?: boolean; + placeholderIcon?: string; + showContentTypeThumbnail?: boolean; } function OrdinalImage({ @@ -142,6 +155,8 @@ function OrdinalImage({ isSmallImage = false, withoutSizeIncrease = false, withoutTitles = false, + placeholderIcon, + showContentTypeThumbnail = false, }: Props) { const isGalleryOpen: boolean = document.documentElement.clientWidth > 360 && !withoutSizeIncrease; const textContent = useTextOrdinalContent(ordinal); @@ -268,6 +283,16 @@ function OrdinalImage({ ); } + if (showContentTypeThumbnail) { + return ( + + + + + + ); + } + return ( - ordinal + ordinal ); } diff --git a/src/app/screens/settings/changeNetwork/index.tsx b/src/app/screens/settings/changeNetwork/index.tsx index 312c2b1ed..98485d348 100644 --- a/src/app/screens/settings/changeNetwork/index.tsx +++ b/src/app/screens/settings/changeNetwork/index.tsx @@ -1,19 +1,24 @@ -import Cross from '@assets/img/settings/x.svg'; import ActionButton from '@components/button'; import BottomBar from '@components/tabBar'; import TopRow from '@components/topRow'; import useWalletReducer from '@hooks/useWalletReducer'; import useWalletSelector from '@hooks/useWalletSelector'; -import { SettingsNetwork, StacksMainnet, StacksTestnet } from '@secretkeylabs/xverse-core'; -import { initialNetworksList } from '@utils/constants'; +import { + SettingsNetwork, + defaultMainnet, + defaultTestnet, + initialNetworksList, +} from '@secretkeylabs/xverse-core'; import { isValidBtcApi, isValidStacksApi } from '@utils/helper'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; import NetworkRow from './networkRow'; +import NodeInput from './nodeInput'; const Container = styled.div` + ${(props) => props.theme.typography.body_medium_m} display: flex; flex-direction: column; flex: 1; @@ -26,180 +31,155 @@ const Container = styled.div` } `; -const NodeInputHeader = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingLeft: props.theme.spacing(1), - paddingRight: props.theme.spacing(1), -})); - -const NodeText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - marginTop: props.theme.spacing(6), -})); - -const NodeResetButton = styled.button((props) => ({ - background: 'none', - color: props.theme.colors.action.classicLight, -})); - -const InputContainer = styled.div((props) => ({ - display: 'flex', - alignItems: 'center', - width: '100%', - border: `1px solid ${props.theme.colors.elevation3}`, - backgroundColor: props.theme.colors.elevation_n1, - borderRadius: props.theme.radius(1), - paddingLeft: props.theme.spacing(4), - paddingRight: props.theme.spacing(4), - marginTop: props.theme.spacing(4), - marginBottom: props.theme.spacing(3), -})); - -const ButtonContainer = styled.div((props) => ({ - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), - marginBottom: props.theme.spacing(16), -})); - -const ErrorMessage = styled.h2((props) => ({ - ...props.theme.body_medium_m, - textAlign: 'left', - color: props.theme.colors.feedback.error, -})); - -const Input = styled.input((props) => ({ - ...props.theme.body_medium_m, - height: 44, - display: 'flex', - flex: 1, - backgroundColor: props.theme.colors.elevation_n1, - color: props.theme.colors.white_0, - border: 'none', -})); - -const Button = styled.button({ - background: 'none', -}); +const ButtonContainer = styled.div` + margin: ${(props) => props.theme.space.m}; +`; + +const NodeInputsContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${(props) => props.theme.space.s}; + margin-top: ${(props) => props.theme.space.s}; +`; + +type NodeInputKey = keyof Pick; +const nodeInputs: { key: NodeInputKey; labelKey: string }[] = [ + { key: 'address', labelKey: 'STACKS_URL' }, + { key: 'btcApiUrl', labelKey: 'BTC_URL' }, + { key: 'fallbackBtcApiUrl', labelKey: 'FALLBACK_BTC_URL' }, +]; + +type NodeInputErrors = Record; +const initialNodeErrors: NodeInputErrors = { + address: '', + btcApiUrl: '', + fallbackBtcApiUrl: '', +}; function ChangeNetworkScreen() { const { t } = useTranslation('translation', { keyPrefix: 'SETTING_SCREEN' }); - const { network, btcApiUrl, networkAddress } = useWalletSelector(); - const [changedNetwork, setChangedNetwork] = useState(network); - const [stacksUrlError, setStacksUrlError] = useState(''); - const [btcURLError, setBtcURLError] = useState(''); - const [btcUrl, setBtcUrl] = useState(btcApiUrl || network.btcApiUrl); - const [stacksUrl, setStacksUrl] = useState(networkAddress || network.address); - const [isChangingNetwork, setIsChangingNetwork] = useState(false); const navigate = useNavigate(); const { changeNetwork } = useWalletReducer(); + const { network, savedNetworks } = useWalletSelector(); + const [isChangingNetwork, setIsChangingNetwork] = useState(false); + const [formErrors, setFormErrors] = useState(initialNodeErrors); + const [formInputs, setFormInputs] = useState(network); const handleBackButtonClick = () => { navigate('/settings'); }; const onNetworkSelected = (networkSelected: SettingsNetwork) => { - setStacksUrl(networkSelected.address); - setChangedNetwork(networkSelected); - setBtcUrl(networkSelected.btcApiUrl); - setStacksUrlError(''); - setBtcURLError(''); - }; - - const onChangeStacksUrl = (event: React.FormEvent) => { - setStacksUrlError(''); - setStacksUrl(event.currentTarget.value); - }; - - const onChangeBtcApiUrl = (event: React.FormEvent) => { - setBtcURLError(''); - setBtcUrl(event.currentTarget.value); + setFormInputs(networkSelected); + setFormErrors(initialNodeErrors); }; - const onClearStacksUrl = () => { - setStacksUrl(''); + // TODO should validate required fields on change + const onChangeCreator = (key: NodeInputKey) => (event: React.ChangeEvent) => { + setFormErrors((prevErrors) => ({ + ...prevErrors, + [key]: '', + })); + setFormInputs((prevInputs) => ({ + ...prevInputs, + [key]: event.target.value, + })); }; - const onClearBtcUrl = () => { - setBtcUrl(''); + const onClearCreator = (key: NodeInputKey) => () => { + setFormErrors((prevErrors) => ({ + ...prevErrors, + [key]: '', + })); + setFormInputs((prevInputs) => ({ + ...prevInputs, + [key]: '', + })); }; - const onResetBtcUrl = async () => { - setBtcUrl(changedNetwork.btcApiUrl); - setBtcURLError(''); - }; - - const onResetStacks = async () => { - setStacksUrl(changedNetwork.address); - setStacksUrlError(''); + const onResetCreator = (key: NodeInputKey) => () => { + setFormErrors((prevErrors) => ({ + ...prevErrors, + [key]: '', + })); + setFormInputs((prevInputs) => ({ + ...prevInputs, + [key]: initialNetworksList.find((n) => n.type === formInputs.type)?.[key], + })); }; const onSubmit = async () => { setIsChangingNetwork(true); - const [isValidStacksUrl, isValidBtcApiUrl] = await Promise.all([ - isValidStacksApi(stacksUrl, changedNetwork.type), - isValidBtcApi(btcUrl, changedNetwork.type), + if (!formInputs.address) { + setFormErrors((prevErrors) => ({ + ...prevErrors, + address: t('REQUIRED'), + })); + setIsChangingNetwork(false); + return; + } + + if (!formInputs.btcApiUrl) { + setFormErrors((prevErrors) => ({ + ...prevErrors, + btcApiUrl: t('REQUIRED'), + })); + setIsChangingNetwork(false); + return; + } + + const [isValidStacksUrl, isValidBtcApiUrl, isValidFallbackBtcApiUrl] = await Promise.all([ + isValidStacksApi(formInputs.address, formInputs.type), + isValidBtcApi(formInputs.btcApiUrl, formInputs.type), + !formInputs.fallbackBtcApiUrl || isValidBtcApi(formInputs.fallbackBtcApiUrl, formInputs.type), ]); - if (isValidStacksUrl && isValidBtcApiUrl) { - const networkObject = - changedNetwork.type === 'Mainnet' - ? new StacksMainnet({ url: stacksUrl }) - : new StacksTestnet({ url: stacksUrl }); - await changeNetwork(changedNetwork, networkObject, stacksUrl, btcUrl); + if (isValidStacksUrl && isValidBtcApiUrl && isValidFallbackBtcApiUrl) { + await changeNetwork(formInputs); navigate('/settings'); } else { - if (!isValidStacksUrl) { - setStacksUrlError(t('INVALID_URL')); - } - if (!isValidBtcApiUrl) { - setBtcURLError(t('INVALID_URL')); - } + setFormErrors({ + address: !isValidStacksUrl ? t('INVALID_URL') : '', + btcApiUrl: !isValidBtcApiUrl ? t('INVALID_URL') : '', + fallbackBtcApiUrl: !isValidFallbackBtcApiUrl ? t('INVALID_URL') : '', + }); setIsChangingNetwork(false); } }; + const savedMainnet = savedNetworks.find((n) => n.type === 'Mainnet'); + const savedTestnet = savedNetworks.find((n) => n.type === 'Testnet'); + return ( <> - - {t('NODE')} - Reset URL - - - - - - {stacksUrlError} - - BTC API URL - Reset URL - - - - - - {btcURLError} + + {nodeInputs.map(({ key, labelKey }) => ( + + ))} + - ); diff --git a/src/app/screens/settings/changeNetwork/networkRow.tsx b/src/app/screens/settings/changeNetwork/networkRow.tsx index 2169c1354..6f7cf65c6 100644 --- a/src/app/screens/settings/changeNetwork/networkRow.tsx +++ b/src/app/screens/settings/changeNetwork/networkRow.tsx @@ -20,7 +20,7 @@ const Button = styled.button((props) => ({ })); const Text = styled.h1((props) => ({ - ...props.theme.body_medium_m, + ...props.theme.typography.body_medium_m, color: props.color, flex: 1, textAlign: 'left', diff --git a/src/app/screens/settings/changeNetwork/nodeInput.tsx b/src/app/screens/settings/changeNetwork/nodeInput.tsx new file mode 100644 index 000000000..ce1a7b32b --- /dev/null +++ b/src/app/screens/settings/changeNetwork/nodeInput.tsx @@ -0,0 +1,90 @@ +import { XCircle } from '@phosphor-icons/react'; +import InputFeedback from '@ui-library/inputFeedback'; +import { useTranslation } from 'react-i18next'; +import styled, { useTheme } from 'styled-components'; + +const NodeInputHeader = styled.div` + display: flex; + flex-direction: row; + align-items: flex-end; + justify-content: space-between; + padding-left: ${(props) => props.theme.spacing(1)}; + padding-right: ${(props) => props.theme.spacing(1)}; +`; + +const NodeText = styled.label` + ${(props) => props.theme.typography.body_medium_m} + color: ${(props) => props.theme.colors.white_200}; +`; + +const NodeResetButton = styled.button` + ${(props) => props.theme.typography.body_medium_m} + background: none; + color: ${(props) => props.theme.colors.white_200}; +`; + +// TODO create and use a ui-library input with proper input box styling +const InputContainer = styled.div` + display: flex; + flex-direction: row; + align-items: center; + width: 100%; + border: 1px solid ${(props) => props.theme.colors.elevation3}; + background-color: ${(props) => props.theme.colors.elevation_n1}; + border-radius: ${(props) => props.theme.radius(1)}px; + padding-left: ${(props) => props.theme.space.m}; + padding-right: ${(props) => props.theme.space.m}; + margin-top: ${(props) => props.theme.space.s}; + margin-bottom: ${(props) => props.theme.space.s}; +`; + +const Input = styled.input` + ${(props) => props.theme.typography.body_medium_m} + height: 44px; + display: flex; + flex: 1; + background-color: ${(props) => props.theme.colors.elevation_n1}; + color: ${(props) => props.theme.colors.white_200}; + border: none; +`; + +const Button = styled.button` + background: none; +`; + +function NodeInput({ + label, + onChange, + value, + onClear, + onReset, + error, +}: { + label: string; + onChange: (event: React.ChangeEvent) => void; + value: string; + onClear: () => void; + onReset: () => void; + error: string; +}) { + const { t } = useTranslation('translation', { keyPrefix: 'SETTING_SCREEN' }); + const theme = useTheme(); + + return ( +
+ + {label} + {t('RESET_TO_DEFAULT')} + + + + + + +
+ ); +} + +export default NodeInput; diff --git a/src/app/screens/settings/index.tsx b/src/app/screens/settings/index.tsx index ce754c35f..cdf5b42f8 100644 --- a/src/app/screens/settings/index.tsx +++ b/src/app/screens/settings/index.tsx @@ -197,14 +197,11 @@ function Setting() { icon={ArrowIcon} showDivider /> - {!isLedgerAccount(selectedAccount) && ( - - )} - + ((props) => ({ - ...props.theme.body_medium_m, - backgroundColor: 'transparent', - border: `1px solid ${props.selected ? props.theme.colors.white_0 : props.theme.colors.grey}`, - color: props.theme.colors.white_0, + ...props.theme.typography.body_medium_m, + backgroundColor: props.selected ? props.theme.colors.white_900 : 'transparent', + border: `1px solid ${props.theme.colors.white_800}`, + color: props.selected ? props.theme.colors.white_0 : props.theme.colors.white_200, borderRadius: props.theme.radius(1), - height: 44, display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', - marginBottom: props.theme.spacing(12), - paddingLeft: props.theme.spacing(6), - paddingRight: props.theme.spacing(6), + padding: props.theme.space.m, + marginBottom: props.theme.space.s, })); -const TimerIcon = styled.img((props) => ({ +const TimerIcon = styled.img((props) => ({ width: 18, height: 21, - marginRight: props.theme.spacing(12), + marginRight: props.theme.space.l, + opacity: props.selected ? 1 : 0.8, })); +const getLabel = (period: number, t: TFunction<'translation', 'SETTING_SCREEN'>) => { + if (period < 60) { + return t('LOCK_COUNTDOWN_MIN', { count: period }); + } + const hours = period / 60; + return t('LOCK_COUNTDOWN_HS', { count: hours }); +}; + function LockCountdown() { const navigate = useNavigate(); const { t } = useTranslation('translation', { keyPrefix: 'SETTING_SCREEN' }); @@ -72,49 +80,41 @@ function LockCountdown() { navigate(-1); }; - const onChooseLow = () => { - setSelectedTime(WalletSessionPeriods.LOW); - }; - - const onChooseStandard = () => { - setSelectedTime(WalletSessionPeriods.STANDARD); - }; - - const onChooseLong = () => { - setSelectedTime(WalletSessionPeriods.LONG); - }; - const onSave = () => { setWalletLockPeriod(selectedTime); navigate(-1); }; + const periodOptions: number[] = Object.keys(WalletSessionPeriods) + .filter((key) => !Number.isNaN(Number(WalletSessionPeriods[key]))) + .map((key) => WalletSessionPeriods[key]); + + const iconsByPeriod = { + [WalletSessionPeriods.LOW]: Timer15, + [WalletSessionPeriods.STANDARD]: Timer30, + [WalletSessionPeriods.LONG]: Timer1, + [WalletSessionPeriods.VERY_LONG]: Timer3, + }; + return ( <> {t('LOCK_COUNTDOWN_TITLE')} - - - {`${WalletSessionPeriods.LOW} minute`} - - - - {`${WalletSessionPeriods.STANDARD} minutes`} - - - - {`${WalletSessionPeriods.LONG} minutes`} - + {periodOptions.map((period) => ( + setSelectedTime(period)} + > + + {getLabel(period, t)} + + ))} diff --git a/src/app/screens/signBatchPsbtRequest/index.tsx b/src/app/screens/signBatchPsbtRequest/index.tsx index a7ee9f476..116b78127 100644 --- a/src/app/screens/signBatchPsbtRequest/index.tsx +++ b/src/app/screens/signBatchPsbtRequest/index.tsx @@ -1,20 +1,23 @@ import { ExternalSatsMethods, MESSAGE_SOURCE } from '@common/types/message-types'; import { delay } from '@common/utils/ledger'; import AccountHeaderComponent from '@components/accountHeader'; +import AssetModal from '@components/assetModal'; import BottomModal from '@components/bottomModal'; import ActionButton from '@components/button'; -import SatsBundle from '@components/confirmBtcTransactionComponent/bundle'; -import InputOutputComponent from '@components/confirmBtcTransactionComponent/inputOutputComponent'; +import ReceiveSection from '@components/confirmBtcTransaction/receiveSection'; +import TransactionSummary from '@components/confirmBtcTransaction/transactionSummary'; +import TransferSection from '@components/confirmBtcTransaction/transferSection'; +import { getNetAmount, isScriptOutput } from '@components/confirmBtcTransaction/utils'; import InfoContainer from '@components/infoContainer'; import LoadingTransactionStatus from '@components/loadingTransactionStatus'; import { ConfirmationStatus } from '@components/loadingTransactionStatus/circularSvgAnimation'; -import RecipientComponent from '@components/recipientComponent'; import TransactionDetailComponent from '@components/transactionDetailComponent'; -import useDetectOrdinalInSignPsbt, { InputsBundle } from '@hooks/useDetectOrdinalInSignPsbt'; import useSignBatchPsbtTx from '@hooks/useSignBatchPsbtTx'; +import useTransactionContext from '@hooks/useTransactionContext'; import useWalletSelector from '@hooks/useWalletSelector'; import { ArrowLeft, ArrowRight } from '@phosphor-icons/react'; -import { Bundle, parsePsbt, satsToBtc } from '@secretkeylabs/xverse-core'; +import { btcTransaction } from '@secretkeylabs/xverse-core'; +import Callout from '@ui-library/callout'; import { isLedgerAccount } from '@utils/helper'; import BigNumber from 'bignumber.js'; import { useCallback, useEffect, useMemo, useState } from 'react'; @@ -113,33 +116,39 @@ interface TxResponse { psbtBase64: string; } +type PsbtSummary = { + inputs: btcTransaction.EnhancedInput[]; + outputs: btcTransaction.EnhancedOutput[]; + feeOutput?: btcTransaction.TransactionFeeOutput | undefined; + hasSigHashNone: boolean; +}; + function SignBatchPsbtRequest() { const { btcAddress, ordinalsAddress, selectedAccount, network } = useWalletSelector(); const navigate = useNavigate(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const { t: tCommon } = useTranslation('translation', { keyPrefix: 'COMMON' }); - const [expandInputOutputView, setExpandInputOutputView] = useState(false); - const { payload, confirmSignPsbt, cancelSignPsbt, getSigningAddresses, requestToken } = - useSignBatchPsbtTx(); + const { payload, confirmSignPsbt, cancelSignPsbt, requestToken } = useSignBatchPsbtTx(); const [isSigning, setIsSigning] = useState(false); const [isSigningComplete, setIsSigningComplete] = useState(false); const [signingPsbtIndex, setSigningPsbtIndex] = useState(1); - const [hasOutputScript, setHasOutputScript] = useState(false); const [currentPsbtIndex, setCurrentPsbtIndex] = useState(0); const [reviewTransaction, setReviewTransaction] = useState(false); const { search } = useLocation(); const params = new URLSearchParams(search); const tabId = params.get('tabId') ?? '0'; - const handleOrdinalAndOrdinalInfo = useDetectOrdinalInSignPsbt(); - const [userReceivesOrdinalArr, setUserReceivesOrdinalArr] = useState< - { bundleItemsData: InputsBundle; userReceivesOrdinal: boolean }[] - >([]); const [isLoading, setIsLoading] = useState(true); + const txnContext = useTransactionContext(); + const [inscriptionToShow, setInscriptionToShow] = useState< + btcTransaction.IOInscription | undefined + >(undefined); + + const [parsedPsbts, setParsedPsbts] = useState([]); const handlePsbtParsing = useCallback( (psbt: SignMultiplePsbtPayload, index: number) => { try { - return parsePsbt(selectedAccount!, psbt.inputsToSign, psbt.psbtBase64, network.type); + const parsedPsbt = new btcTransaction.EnhancedPsbt(txnContext, psbt.psbtBase64); + return parsedPsbt.getSummary(); } catch (err) { navigate('/tx-status', { state: { @@ -153,13 +162,21 @@ function SignBatchPsbtRequest() { return undefined; } }, - [selectedAccount, network.type], + [txnContext], ); - const parsedPsbts = useMemo( - () => payload.psbts.map(handlePsbtParsing), - [handlePsbtParsing, payload.psbts], - ); + useEffect(() => { + (async () => { + const parsedPsbtsResult = await Promise.all(payload.psbts.map(handlePsbtParsing)); + + if (parsedPsbtsResult.some((item) => item === undefined)) { + return setIsLoading(false); + } + + setParsedPsbts(parsedPsbtsResult as PsbtSummary[]); + setIsLoading(false); + })(); + }, [payload.psbts.length, handlePsbtParsing]); const checkAddressMismatch = (input) => { if (input.address !== btcAddress && input.address !== ordinalsAddress) { @@ -189,51 +206,10 @@ function SignBatchPsbtRequest() { payload.psbts.forEach((psbt) => psbt.inputsToSign.forEach(checkAddressMismatch)); }; - const checkIfUserReceivesOrdinals = async () => { - try { - const results = await Promise.all(parsedPsbts.map(handleOrdinalAndOrdinalInfo)); - setUserReceivesOrdinalArr(results); - } catch { - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - errorTitle: t('PSBT_CANT_PARSE_ERROR_TITLE'), - error: t('PSBT_CANT_PARSE_ERROR_DESCRIPTION'), - browserTx: true, - }, - }); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - checkIfUserReceivesOrdinals(); - }, []); - useEffect(() => { checkIfMismatch(); }, []); - useEffect(() => { - if (parsedPsbts) { - let outputScriptDetected = false; - - parsedPsbts.forEach((psbt) => { - if (!psbt) { - return; - } - - if (psbt.outputs.some((output) => !!output.outputScript)) { - outputScriptDetected = true; - } - }); - - setHasOutputScript(outputScriptDetected); - } - }, [parsedPsbts]); - const onSignPsbtConfirmed = async () => { try { if (isLedgerAccount(selectedAccount)) { @@ -296,28 +272,35 @@ function SignBatchPsbtRequest() { window.close(); }; - const expandInputOutputSection = () => { - setExpandInputOutputView(!expandInputOutputView); - }; - const closeCallback = () => { window.close(); }; const totalNetAmount = parsedPsbts.reduce( - (sum, psbt) => (psbt ? sum.plus(new BigNumber(psbt.netAmount.toString())) : sum), + (sum, psbt) => + psbt + ? sum.plus( + new BigNumber( + getNetAmount({ + inputs: psbt.inputs, + outputs: psbt.outputs, + btcAddress, + ordinalsAddress, + }), + ), + ) + : sum, new BigNumber(0), ); - - const userReceivesOrdinals = userReceivesOrdinalArr - .filter((item) => item.userReceivesOrdinal) - .map((item) => item.bundleItemsData) - .flat(); - - const userTransfersOrdinals = userReceivesOrdinalArr - .filter((item) => !item.userReceivesOrdinal) - .map((item) => item.bundleItemsData) - .flat(); + const totalFeeAmount = parsedPsbts.reduce((sum, psbt) => { + const feeAmount = psbt.feeOutput?.amount ?? 0; + return sum.plus(new BigNumber(feeAmount)); + }, new BigNumber(0)); + + const hasOutputScript = useMemo( + () => parsedPsbts.some((psbt) => psbt.outputs.some((output) => isScriptOutput(output))), + [parsedPsbts.length], + ); const signingStatus: ConfirmationStatus = isSigningComplete ? 'SUCCESS' : 'LOADING'; @@ -364,51 +347,30 @@ function SignBatchPsbtRequest() { {t('REVIEW_ALL')} - - {userTransfersOrdinals.length > 0 && - userTransfersOrdinals.map((item, index) => ( - - ))} - - {userReceivesOrdinals.length > 0 && - userTransfersOrdinals.map((item, index) => ( - - ))} - - setInscriptionToShow(undefined)} + inscription={{ + content_type: inscriptionToShow.contentType, + id: inscriptionToShow.id, + inscription_number: inscriptionToShow.number, + }} + /> + )} + psbt.inputs).flat()} + outputs={parsedPsbts.map((psbt) => psbt.outputs).flat()} + netAmount={(totalNetAmount.toNumber() + totalFeeAmount.toNumber()) * -1} + isPartialTransaction={parsedPsbts.some((psbt) => !psbt.feeOutput)} + onShowInscription={setInscriptionToShow} + /> + psbt.outputs).flat()} + onShowInscription={setInscriptionToShow} + netAmount={totalNetAmount.toNumber()} /> - - - {hasOutputScript && } + {hasOutputScript && } )} @@ -439,46 +401,14 @@ function SignBatchPsbtRequest() { {t('TRANSACTION')} {currentPsbtIndex + 1}/{parsedPsbts.length} - {Array.isArray(userReceivesOrdinalArr[currentPsbtIndex]?.bundleItemsData) && - userReceivesOrdinalArr[currentPsbtIndex].bundleItemsData.map((bundle, index) => ( - - ))} - - - - - - {hasOutputScript && } + {!!parsedPsbts[currentPsbtIndex] && ( + + )} diff --git a/src/app/screens/signPsbtRequest/bundleItemsComponent.tsx b/src/app/screens/signPsbtRequest/bundleItemsComponent.tsx deleted file mode 100644 index 82d0de52d..000000000 --- a/src/app/screens/signPsbtRequest/bundleItemsComponent.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import Eye from '@assets/img/createPassword/Eye.svg'; -import Cross from '@assets/img/dashboard/X.svg'; -import IconOrdinal from '@assets/img/transactions/ordinal.svg'; -import RareSatAsset from '@components/rareSatAsset/rareSatAsset'; -import { animated, useSpring } from '@react-spring/web'; -import { getTruncatedAddress } from '@utils/helper'; -import { BundleItem, getBundleItemSubText } from '@utils/rareSats'; -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - background: props.theme.colors.elevation1, - borderRadius: 12, - padding: '16px 16px', - justifyContent: 'center', - marginBottom: 12, -})); - -const RecipientTitleText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_200, - marginBottom: 10, -})); - -const RowContainer = styled.div({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', -}); - -const TransparentButton = styled.button({ - background: 'transparent', - display: 'flex', - alignItems: 'center', - marginLeft: 10, -}); - -const Icon = styled.img((props) => ({ - marginRight: props.theme.spacing(4), - width: 32, - height: 32, - borderRadius: 30, -})); - -const TitleText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_200, -})); - -const ValueText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_0, -})); - -const SubValueText = styled.h1((props) => ({ - ...props.theme.body_m, - fontSize: 12, - color: props.theme.colors.white_400, -})); - -const InscriptionText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - fontSize: 21, - marginTop: 24, - textAlign: 'center', - color: props.theme.colors.white[0], - overflowWrap: 'break-word', - wordWrap: 'break-word', - wordBreak: 'break-word', -})); - -const ColumnContainer = styled.div({ - display: 'flex', - flexDirection: 'column', - flex: 1, - justifyContent: 'flex-end', - alignItems: 'flex-end', - marginTop: 12, -}); - -const CrossContainer = styled.div({ - display: 'flex', - marginTop: 10, - justifyContent: 'flex-end', - alignItems: 'flex-end', -}); - -const OrdinalOuterImageContainer = styled.div({ - justifyContent: 'center', - alignItems: 'center', - borderRadius: 2, - display: 'flex', - flexDirection: 'column', - flex: 1, -}); - -const OrdinalImageContainer = styled.div({ - width: '50%', -}); - -const OrdinalBackgroundContainer = styled(animated.div)({ - width: '100%', - height: '100%', - top: 0, - left: 0, - bottom: 0, - right: 0, - position: 'fixed', - zIndex: 10, - background: 'rgba(18, 21, 30, 0.8)', - backdropFilter: 'blur(16px)', - padding: 16, - display: 'flex', - flexDirection: 'column', -}); - -const EyeIcon = styled.img({ - width: 20, - height: 20, -}); - -interface Props { - item: BundleItem; - userReceivesOrdinal: boolean; -} -function BundleItemsComponent({ item, userReceivesOrdinal }: Props) { - const { t } = useTranslation('translation'); - const [showOrdinal, setShowOrdinal] = useState(false); - const styles = useSpring({ - from: { - opacity: 0, - y: 24, - }, - to: { - y: 0, - opacity: 1, - }, - delay: 100, - }); - const onButtonClick = () => { - setShowOrdinal(true); - }; - - const onCrossClick = () => { - setShowOrdinal(false); - }; - const getItemId = () => { - if (item.type === 'inscription') { - return item.inscription.id; - } - if (item.type === 'inscribed-sat' || item.type === 'rare-sat') { - return item.number; - } - return ''; - }; - const itemSubText = getBundleItemSubText({ - satType: item.type, - rareSatsType: item.rarity_ranking as any, - }); - const getDetail = () => { - if (item.type === 'inscription' || item.type === 'inscribed-sat') { - return item.inscription.content_type; - } - return itemSubText; - }; - const getTitle = () => { - if (item.type === 'inscription') { - return t('COMMON.INSCRIPTION'); - } - if (item.type === 'inscribed-sat') { - return t('RARE_SATS.INSCRIBED_SAT'); - } - return t('RARE_SATS.RARE_SAT'); - }; - return ( - <> - {showOrdinal && ( - - - - cross - - - - - - - {`${getTitle()} ${getItemId()} `} - - - )} - - - {userReceivesOrdinal - ? t('CONFIRM_TRANSACTION.YOU_WILL_RECEIVE') - : t('CONFIRM_TRANSACTION.YOU_WILL_TRANSFER')} - - - - {getTitle()} - - - {getTruncatedAddress(String(getItemId()))} - - - - - {getDetail()} - - - - - ); -} - -export default BundleItemsComponent; diff --git a/src/app/screens/signPsbtRequest/index.tsx b/src/app/screens/signPsbtRequest/index.tsx index e322c6006..3e662ce82 100644 --- a/src/app/screens/signPsbtRequest/index.tsx +++ b/src/app/screens/signPsbtRequest/index.tsx @@ -1,231 +1,89 @@ -import ledgerConnectDefaultIcon from '@assets/img/ledger/ledger_connect_default.svg'; -import ledgerConnectBtcIcon from '@assets/img/ledger/ledger_import_connect_btc.svg'; -import { ExternalSatsMethods, MESSAGE_SOURCE } from '@common/types/message-types'; -import { delay } from '@common/utils/ledger'; -import AccountHeaderComponent from '@components/accountHeader'; -import BottomModal from '@components/bottomModal'; -import ActionButton from '@components/button'; -import InputOutputComponent from '@components/confirmBtcTransactionComponent/inputOutputComponent'; -import InfoContainer from '@components/infoContainer'; -import LedgerConnectionView from '@components/ledger/connectLedgerView'; -import RecipientComponent from '@components/recipientComponent'; -import TransactionDetailComponent from '@components/transactionDetailComponent'; -import useBtcClient from '@hooks/useBtcClient'; -import useDetectOrdinalInSignPsbt from '@hooks/useDetectOrdinalInSignPsbt'; +import ConfirmBitcoinTransaction from '@components/confirmBtcTransaction'; import useSignPsbtTx from '@hooks/useSignPsbtTx'; +import useTransactionContext from '@hooks/useTransactionContext'; import useWalletSelector from '@hooks/useWalletSelector'; -import Transport from '@ledgerhq/hw-transport-webusb'; -import { - getBtcFiatEquivalent, - parsePsbt, - psbtBase64ToHex, - satsToBtc, - signLedgerPSBT, - Transport as TransportType, -} from '@secretkeylabs/xverse-core'; -import { isLedgerAccount } from '@utils/helper'; -import { BundleItem, convertV2ToV1Bundle } from '@utils/rareSats'; -import BigNumber from 'bignumber.js'; -import { decodeToken } from 'jsontokens'; +import { btcTransaction, Transport } from '@secretkeylabs/xverse-core'; import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { NumericFormat } from 'react-number-format'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { MoonLoader } from 'react-spinners'; -import { SignTransactionOptions } from 'sats-connect'; -import styled from 'styled-components'; -import BundleItemsComponent from './bundleItemsComponent'; - -const OuterContainer = styled.div` - display: flex; - flex: 1; - flex-direction: column; - overflow-y: auto; - &::-webkit-scrollbar { - display: none; - } -`; - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - flex: 1, - marginTop: props.theme.spacing(11), - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), -})); - -const LoaderContainer = styled.div((props) => ({ - display: 'flex', - flex: 1, - justifyContent: 'center', - alignItems: 'center', - marginTop: props.theme.spacing(12), -})); - -const ButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), - marginBottom: props.theme.spacing(20), - marginTop: props.theme.spacing(12), -})); - -const TransparentButtonContainer = styled.div((props) => ({ - marginRight: props.theme.spacing(6), - width: '100%', -})); - -const ReviewTransactionText = styled.h1((props) => ({ - ...props.theme.headline_s, - color: props.theme.colors.white_0, - marginBottom: props.theme.spacing(12), - textAlign: 'left', -})); - -const SuccessActionsContainer = styled.div((props) => ({ - width: '100%', - display: 'flex', - flexDirection: 'column', - gap: props.theme.spacing(6), - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), - marginBottom: props.theme.spacing(20), - marginTop: props.theme.spacing(20), -})); +import { useNavigate } from 'react-router-dom'; +import getPsbtDataWithMocks from './tempMockDataUtil'; +import useSignPsbtValidationGate from './useSignPsbtValidationGate'; function SignPsbtRequest() { - const { - btcAddress, - btcPublicKey, - ordinalsAddress, - ordinalsPublicKey, - selectedAccount, - network, - btcFiatRate, - } = useWalletSelector(); const navigate = useNavigate(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const { t: signatureRequestTranslate } = useTranslation('translation', { - keyPrefix: 'SIGNATURE_REQUEST', - }); - const [expandInputOutputView, setExpandInputOutputView] = useState(false); - const { payload, confirmSignPsbt, cancelSignPsbt, getSigningAddresses } = useSignPsbtTx(); + + const [isLoading, setIsLoading] = useState(true); const [isSigning, setIsSigning] = useState(false); - const [hasOutputScript, setHasOutputScript] = useState(false); - const [isModalVisible, setIsModalVisible] = useState(false); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isButtonDisabled, setIsButtonDisabled] = useState(false); - const [isConnectSuccess, setIsConnectSuccess] = useState(false); - const [isConnectFailed, setIsConnectFailed] = useState(false); - const [isTxRejected, setIsTxRejected] = useState(false); - const { search } = useLocation(); - const params = new URLSearchParams(search); - const tabId = params.get('tabId') ?? '0'; - const requestToken = params.get('signPsbtRequest') ?? ''; - const request = decodeToken(requestToken) as any as SignTransactionOptions; - const btcClient = useBtcClient(); + const [inputs, setInputs] = useState([]); + const [outputs, setOutputs] = useState([]); + const [feeOutput, setFeeOutput] = useState(); + const { payload, confirmSignPsbt, cancelSignPsbt } = useSignPsbtTx(); + const txnContext = useTransactionContext(); const parsedPsbt = useMemo(() => { try { - return parsePsbt(selectedAccount!, payload.inputsToSign, payload.psbtBase64, network.type); + return new btcTransaction.EnhancedPsbt(txnContext, payload.psbtBase64, payload.inputsToSign); } catch (err) { return undefined; } - }, [selectedAccount, payload.inputsToSign, payload.psbtBase64, network.type]); + }, [txnContext, payload.psbtBase64]); - const handleOrdinalAndOrdinalInfo = useDetectOrdinalInSignPsbt(); - const [isLoading, setIsLoading] = useState(true); - const [userReceivesOrdinal, setUserReceivesOrdinal] = useState(false); - const [bundleItemsData, setBundleItemsData] = useState([]); - const signingAddresses = useMemo( - () => getSigningAddresses(payload.inputsToSign), - [payload.inputsToSign], - ); + useSignPsbtValidationGate({ payload, parsedPsbt }); - const checkIfMismatch = () => { - if (!parsedPsbt) { - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - errorTitle: t('PSBT_CANT_PARSE_ERROR_TITLE'), - error: t('PSBT_CANT_PARSE_ERROR_DESCRIPTION'), - browserTx: true, - }, - }); - } - if (payload.network.type !== network.type) { - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - error: t('NETWORK_MISMATCH'), - browserTx: true, - }, - }); - } - if (payload.inputsToSign) { - payload.inputsToSign.forEach((input) => { - if (input.address !== btcAddress && input.address !== ordinalsAddress) { - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - error: t('ADDRESS_MISMATCH'), - browserTx: true, - }, - }); - } + const { btcAddress, ordinalsAddress } = useWalletSelector(); + useEffect(() => { + if (!parsedPsbt) return; + + parsedPsbt + .getSummary() + .then((summary) => { + const { feeOutput: psbtFeeOutput, inputs: psbtInputs, outputs: psbtOutputs } = summary; + // TODO: remove this section, this is only for testing + const { inputsWithMocks, outputsWithMocks, feeOutputWithMocks } = getPsbtDataWithMocks( + btcAddress, + ordinalsAddress, + psbtInputs, + psbtOutputs, + !psbtFeeOutput, + psbtFeeOutput, + ); + setFeeOutput(feeOutputWithMocks); + setInputs(inputsWithMocks); + setOutputs(outputsWithMocks); + + // setFeeOutput(psbtFeeOutput); + // setInputs(psbtInputs); + // setOutputs(psbtOutputs); + setIsLoading(false); + }) + .catch((error) => { + console.error(error); + navigate('/tx-status', { + state: { + txid: '', + currency: 'BTC', + errorTitle: t('PSBT_CANT_PARSE_ERROR_TITLE'), + error: t('PSBT_CANT_PARSE_ERROR_DESCRIPTION'), + browserTx: true, + }, + }); }); - } - }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [parsedPsbt]); - const checkIfUserReceivesOrdinal = async () => { + const onConfirm = async (ledgerTransport?: Transport) => { + setIsSigning(true); try { - const result = await handleOrdinalAndOrdinalInfo(parsedPsbt); - setBundleItemsData(convertV2ToV1Bundle(result.bundleItemsData)); - setUserReceivesOrdinal(result.userReceivesOrdinal); - } catch { - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - errorTitle: t('PSBT_CANT_PARSE_ERROR_TITLE'), - error: t('PSBT_CANT_PARSE_ERROR_DESCRIPTION'), - browserTx: true, - }, + const signedPsbt = await parsedPsbt?.getSignedPsbtBase64({ + finalize: payload.broadcast, + ledgerTransport, }); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - checkIfUserReceivesOrdinal(); - }, []); - - useEffect(() => { - checkIfMismatch(); - }, []); - useEffect(() => { - if (parsedPsbt) { - const outputScriptDetected = parsedPsbt.outputs.some((output) => !!output.outputScript); - setHasOutputScript(outputScriptDetected); - } - }, [parsedPsbt]); - - const onSignPsbtConfirmed = async () => { - try { - if (isLedgerAccount(selectedAccount)) { - setIsModalVisible(true); - return; + const response = await confirmSignPsbt(signedPsbt); + if (ledgerTransport) { + await ledgerTransport?.close(); } - - setIsSigning(true); - const response = await confirmSignPsbt(); setIsSigning(false); if (payload.broadcast) { navigate('/tx-status', { @@ -240,6 +98,7 @@ function SignPsbtRequest() { window.close(); } } catch (err) { + setIsSigning(false); if (err instanceof Error) { navigate('/tx-status', { state: { @@ -254,226 +113,26 @@ function SignPsbtRequest() { } }; - const onCancelClick = async () => { + const onCancel = () => { cancelSignPsbt(); window.close(); }; - const expandInputOutputSection = () => { - setExpandInputOutputView(!expandInputOutputView); - }; - - const handleLedgerPsbtSigning = async (transport: TransportType) => { - const addressIndex = selectedAccount?.deviceAccountIndex; - const { psbtBase64, broadcast } = payload; - - if (addressIndex === undefined) { - throw new Error('Account not found'); - } - - const signingResponse = await signLedgerPSBT({ - transport, - esploraProvider: btcClient, - network: network.type, - addressIndex, - psbtInputBase64: psbtBase64, - finalize: broadcast ?? false, - nativeSegwitPubKey: btcPublicKey, - taprootPubKey: ordinalsPublicKey, - }); - - let txId: string = ''; - if (request.payload.broadcast) { - const txHex = psbtBase64ToHex(signingResponse); - const response = await btcClient.sendRawTransaction(txHex); - txId = response.tx.hash; - } - - const signingMessage = { - source: MESSAGE_SOURCE, - method: ExternalSatsMethods.signPsbtResponse, - payload: { - signPsbtRequest: requestToken, - signPsbtResponse: { - psbtBase64: signingResponse, - txId, - }, - }, - }; - chrome.tabs.sendMessage(+tabId, signingMessage); - - return { - txId, - signingResponse, - }; - }; - - const handleConnectAndConfirm = async () => { - if (!selectedAccount) { - console.error('No account selected'); - return; - } - setIsButtonDisabled(true); - - const transport = await Transport.create(); - - if (!transport) { - setIsConnectSuccess(false); - setIsConnectFailed(true); - setIsButtonDisabled(false); - return; - } - - setIsConnectSuccess(true); - await delay(1500); - setCurrentStepIndex(1); - - try { - const response = await handleLedgerPsbtSigning(transport); - - if (payload.broadcast) { - navigate('/tx-status', { - state: { - txid: response.txId, - currency: 'BTC', - error: '', - browserTx: true, - }, - }); - } else { - window.close(); - } - } catch (err) { - console.error(err); - setIsTxRejected(true); - } finally { - await transport.close(); - setIsButtonDisabled(false); - } - }; - - const handleRetry = async () => { - setIsTxRejected(false); - setIsConnectSuccess(false); - setCurrentStepIndex(0); - }; - - const cancelCallback = () => { - window.close(); - }; - - const getSatsAmountString = (sats: BigNumber) => ( - - ); - return ( - <> - - {isLoading ? ( - - - - ) : ( - <> - - - {t('REVIEW_TRANSACTION')} - {!payload.broadcast && } - {bundleItemsData && - bundleItemsData.map((bundleItem, index) => ( - - ))} - - - - - {payload.broadcast ? ( - - ) : null} - {hasOutputScript && } - - - - - - - - - - )} - setIsModalVisible(false)}> - {currentStepIndex === 0 && ( - - )} - {currentStepIndex === 1 && ( - - )} - - - - - - + ); } diff --git a/src/app/screens/signPsbtRequest/tempMockDataUtil.ts b/src/app/screens/signPsbtRequest/tempMockDataUtil.ts new file mode 100644 index 000000000..b2af92428 --- /dev/null +++ b/src/app/screens/signPsbtRequest/tempMockDataUtil.ts @@ -0,0 +1,143 @@ +import { btcTransaction } from '@secretkeylabs/xverse-core'; + +// TODO: remove after testing +const getPsbtDataWithMocks = ( + btcAddress: string, + ordinalsAddress: string, + inputs: btcTransaction.EnhancedInput[], + outputs: btcTransaction.EnhancedOutput[], + isPartialTransaction: boolean, + feeOutput?: btcTransaction.TransactionFeeOutput, +) => { + const outputsWithMocks = [...outputs]; + const inputsWithMocks = [...inputs]; + const feeOutputWithMocks = feeOutput ? { ...feeOutput } : undefined; + + if (localStorage.getItem('assetsInPayment') === 'true') { + // TODO: mock data for items spend in payment address + if (isPartialTransaction) { + inputsWithMocks.push({ + // @ts-ignore + extendedUtxo: { + address: btcAddress, + outpoint: '9851e0a32f6fd352dd763624025cb55cead8954c7bdde4430c290f7f9e3bcfeb:0', + // @ts-ignore + utxo: { + value: 100000, + status: { + confirmed: false, // to test the unconfirmed utxo warning callout + }, + }, + }, + inscriptions: [ + { + contentType: 'image/png', + fromAddress: btcAddress, + id: 'f115397bbaf139f0daa954bf26e9a986b5468a2332628b0c0a9a9f6af34d1b3di0', + number: 10951686, + offset: 0, + }, + ], + satributes: [], + sigHash: 131, + }); + } + outputsWithMocks.push( + { + address: 'bc1p6rh39e6s6utyc8adtlt3q09d9tnrwlwynngdwj9jse2uysekynxscnwfh7', + amount: 10000, + inscriptions: [ + { + contentType: 'image/png', + fromAddress: btcAddress, + id: 'f115397bbaf139f0daa954bf26e9a986b5468a2332628b0c0a9a9f6af34d1b3di0', + number: 10951686, + offset: 0, + }, + ], + satributes: [], + }, + { + address: 'bc1p6rh39e6s6utyc8adtlt3q09d9tnrwlwynngdwj9jse2uysekynxscnwfh7', + amount: 546, + inscriptions: [], + satributes: [ + { + amount: 1, + fromAddress: btcAddress, + offset: 0, + types: ['PALINDROME'], + }, + ], + }, + ); + } + + if (localStorage.getItem('bundleInOrdinal') === 'true') { + // TODO: mock data for bundle item in ordinal address + outputsWithMocks.push({ + address: ordinalsAddress, + amount: 20, + inscriptions: [ + { + contentType: 'image/png', + fromAddress: 'bc1prnplwl27eedudpvl9cjhd2pysudk0gzze08wjhsyy0998mfcgmcsaz4nse', + id: '9f0a1ea3ca2e2431242350b63cf53708f0f3e560638eb26b1255d4e5dd766fc4i0', + number: 10987226, + offset: 0, + }, + { + contentType: 'image/png', + fromAddress: 'bc1pmz88ylp258alrgeqsy7jn99u20ylkc4fuqcgwva3eef8s92ye9squunk5r', + id: '2237248523bc923a7844b47cb7e2552c1666032ed54ab153a00fba1f5c3e1e22i0', + number: 10878824, + offset: 2, + }, + ], + satributes: [ + { + amount: 1, + fromAddress: 'bc1pugy3kp2zeuntlw649vse3eyy9zr6rwd2lfchdasx9pa7nvm2555qfeepyt', + offset: 3, + types: ['FIRST_TRANSACTION', 'VINTAGE', 'BLOCK9', 'NAKAMOTO'], + }, + { + amount: 1, + fromAddress: 'bc1pugy3kp2zeuntlw649vse3eyy9zr6rwd2lfchdasx9pa7nvm2555qfeepyt', + offset: 0, + types: ['PIZZA'], + }, + ], + }); + } + + if (localStorage.getItem('assetsInFees') === 'true' && feeOutputWithMocks) { + feeOutputWithMocks.satributes = [ + ...feeOutputWithMocks.satributes, + { + amount: 1, + fromAddress: '38NMchWMVXBokHicGrs9nWimzJPfjJYhZ8', + offset: 0, + types: ['PALINDROME'], + }, + ]; + feeOutputWithMocks.inscriptions = [ + ...feeOutputWithMocks.inscriptions, + { + contentType: 'image/png', + fromAddress: '38NMchWMVXBokHicGrs9nWimzJPfjJYhZ8', + id: 'f115397bbaf139f0daa954bf26e9a986b5468a2332628b0c0a9a9f6af34d1b3di0', + number: 10951686, + offset: 0, + }, + ]; + } + + return { + inputsWithMocks, + outputsWithMocks, + feeOutputWithMocks, + }; +}; + +export default getPsbtDataWithMocks; diff --git a/src/app/screens/signPsbtRequest/useSignPsbtValidationGate.ts b/src/app/screens/signPsbtRequest/useSignPsbtValidationGate.ts new file mode 100644 index 000000000..cd81c775a --- /dev/null +++ b/src/app/screens/signPsbtRequest/useSignPsbtValidationGate.ts @@ -0,0 +1,56 @@ +import useWalletSelector from '@hooks/useWalletSelector'; +import { btcTransaction } from '@secretkeylabs/xverse-core'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { SignTransactionPayload } from 'sats-connect'; + +type Props = { + payload: SignTransactionPayload; + parsedPsbt: btcTransaction.EnhancedPsbt | undefined; +}; +const useSignPsbtValidationGate = ({ payload, parsedPsbt }: Props) => { + const { btcAddress, ordinalsAddress, network } = useWalletSelector(); + const navigate = useNavigate(); + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + + useEffect(() => { + if (!parsedPsbt) { + navigate('/tx-status', { + state: { + txid: '', + currency: 'BTC', + errorTitle: t('PSBT_CANT_PARSE_ERROR_TITLE'), + error: t('PSBT_CANT_PARSE_ERROR_DESCRIPTION'), + browserTx: true, + }, + }); + } + if (payload.network.type !== network.type) { + navigate('/tx-status', { + state: { + txid: '', + currency: 'BTC', + error: t('NETWORK_MISMATCH'), + browserTx: true, + }, + }); + } + if (payload.inputsToSign) { + payload.inputsToSign.forEach((input) => { + if (input.address !== btcAddress && input.address !== ordinalsAddress) { + navigate('/tx-status', { + state: { + txid: '', + currency: 'BTC', + error: t('ADDRESS_MISMATCH'), + browserTx: true, + }, + }); + } + }); + } + }); +}; + +export default useSignPsbtValidationGate; diff --git a/src/app/screens/speedUpTransaction/index.tsx b/src/app/screens/speedUpTransaction/index.tsx index 8a885b742..ae567f408 100644 --- a/src/app/screens/speedUpTransaction/index.tsx +++ b/src/app/screens/speedUpTransaction/index.tsx @@ -21,12 +21,12 @@ import useWalletSelector from '@hooks/useWalletSelector'; import Transport from '@ledgerhq/hw-transport-webusb'; import { CarProfile, Lightning, RocketLaunch, ShootingStar } from '@phosphor-icons/react'; import { + StacksTransaction, + Transport as TransportType, broadcastSignedTransaction, signLedgerStxTransaction, signTransaction, - StacksTransaction, stxToMicrostacks, - Transport as TransportType, } from '@secretkeylabs/xverse-core'; import { deserializeTransaction } from '@stacks/transactions'; import { EMPTY_LABEL } from '@utils/constants'; diff --git a/src/app/stores/wallet/actions/actionCreators.ts b/src/app/stores/wallet/actions/actionCreators.ts index 587dbf1d6..7ad9cc88d 100644 --- a/src/app/stores/wallet/actions/actionCreators.ts +++ b/src/app/stores/wallet/actions/actionCreators.ts @@ -176,16 +176,10 @@ export function ChangeFiatCurrencyAction( }; } -export function ChangeNetworkAction( - network: SettingsNetwork, - networkAddress: string | undefined, - btcApiUrl: string, -): actions.ChangeNetwork { +export function ChangeNetworkAction(network: SettingsNetwork): actions.ChangeNetwork { return { type: actions.ChangeNetworkKey, network, - networkAddress, - btcApiUrl, }; } diff --git a/src/app/stores/wallet/actions/types.ts b/src/app/stores/wallet/actions/types.ts index 734210049..bec02f74e 100644 --- a/src/app/stores/wallet/actions/types.ts +++ b/src/app/stores/wallet/actions/types.ts @@ -21,36 +21,28 @@ export const SetFeeMultiplierKey = 'SetFeeMultiplierKey'; export const ChangeFiatCurrencyKey = 'ChangeFiatCurrency'; export const ChangeNetworkKey = 'ChangeNetwork'; export const GetActiveAccountsKey = 'GetActiveAccounts'; - export const FetchStxWalletDataRequestKey = 'FetchStxWalletDataRequest'; export const SetStxWalletDataKey = 'SetStxWalletDataKey'; - export const SetBtcWalletDataKey = 'SetBtcWalletData'; - export const SetCoinRatesKey = 'SetCoinRatesKey'; - export const SetCoinDataKey = 'SetCoinDataKey'; - export const ChangeHasActivatedOrdinalsKey = 'ChangeHasActivatedOrdinalsKey'; export const RareSatsNoticeDismissedKey = 'RareSatsNoticeDismissedKey'; export const ChangeHasActivatedRareSatsKey = 'ChangeHasActivatedRareSatsKey'; export const ChangeHasActivatedRBFKey = 'ChangeHasActivatedRBFKey'; - export const ChangeShowBtcReceiveAlertKey = 'ChangeShowBtcReceiveAlertKey'; export const ChangeShowOrdinalReceiveAlertKey = 'ChangeShowOrdinalReceiveAlertKey'; export const ChangeShowDataCollectionAlertKey = 'ChangeShowDataCollectionAlertKey'; export const UpdateLedgerAccountsKey = 'UpdateLedgerAccountsKey'; - export const SetBrcCoinsListKey = 'SetBrcCoinsList'; - export const SetWalletLockPeriodKey = 'SetWalletLockPeriod'; - export const SetWalletUnlockedKey = 'SetWalletUnlocked'; export enum WalletSessionPeriods { - LOW = 1, - STANDARD = 10, - LONG = 30, + LOW = 15, + STANDARD = 30, + LONG = 60, + VERY_LONG = 180, } export interface WalletState { @@ -64,7 +56,8 @@ export interface WalletState { accountsList: Account[]; ledgerAccountsList: Account[]; selectedAccount: Account | null; - network: SettingsNetwork; + network: SettingsNetwork; // currently selected network urls and type + savedNetworks: SettingsNetwork[]; // previously set network urls for type encryptedSeed: string; fiatCurrency: SupportedCurrency; btcFiatRate: string; @@ -78,7 +71,6 @@ export interface WalletState { coins: Coin[]; brcCoinsList: FungibleToken[] | null; feeMultipliers: AppInfo | null; - networkAddress: string | undefined; hasActivatedOrdinalsKey: boolean | undefined; hasActivatedRareSatsKey: boolean | undefined; hasActivatedRBFKey: boolean | undefined; @@ -88,7 +80,6 @@ export interface WalletState { showDataCollectionAlert: boolean | null; accountType: AccountType | undefined; accountName: string | undefined; - btcApiUrl: string; walletLockPeriod: WalletSessionPeriods; isUnlocked: boolean; } @@ -178,8 +169,6 @@ export interface ChangeFiatCurrency { export interface ChangeNetwork { type: typeof ChangeNetworkKey; network: SettingsNetwork; - networkAddress: string | undefined; - btcApiUrl: string; } export interface GetActiveAccounts { diff --git a/src/app/stores/wallet/reducer.ts b/src/app/stores/wallet/reducer.ts index 6e626e48c..b09f5a4b2 100644 --- a/src/app/stores/wallet/reducer.ts +++ b/src/app/stores/wallet/reducer.ts @@ -1,10 +1,10 @@ -import { initialNetworksList } from '@utils/constants'; +import { defaultMainnet, initialNetworksList } from '@secretkeylabs/xverse-core'; import { AddAccountKey, ChangeFiatCurrencyKey, ChangeHasActivatedOrdinalsKey, - ChangeHasActivatedRareSatsKey, ChangeHasActivatedRBFKey, + ChangeHasActivatedRareSatsKey, ChangeNetworkKey, ChangeShowBtcReceiveAlertKey, ChangeShowDataCollectionAlertKey, @@ -70,7 +70,8 @@ const initialWalletState: WalletState = { stxPublicKey: '', btcPublicKey: '', ordinalsPublicKey: '', - network: initialNetworksList[0], + network: { ...defaultMainnet }, + savedNetworks: initialNetworksList, accountsList: [], ledgerAccountsList: [], selectedAccount: null, @@ -87,8 +88,6 @@ const initialWalletState: WalletState = { coins: [], brcCoinsList: [], feeMultipliers: null, - networkAddress: undefined, - btcApiUrl: '', hasActivatedOrdinalsKey: undefined, hasActivatedRareSatsKey: undefined, hasActivatedRBFKey: true, @@ -204,8 +203,10 @@ const walletReducer = ( return { ...state, network: action.network, - networkAddress: action.networkAddress, - btcApiUrl: action.btcApiUrl, + savedNetworks: [ + ...state.savedNetworks.filter((n) => n.type !== action.network.type), + action.network, + ], }; case GetActiveAccountsKey: return { diff --git a/src/app/ui-library/avatar.tsx b/src/app/ui-library/avatar.tsx new file mode 100644 index 000000000..e1350f457 --- /dev/null +++ b/src/app/ui-library/avatar.tsx @@ -0,0 +1,33 @@ +import styled from 'styled-components'; + +type Props = { + icon?: React.ReactNode; + src?: React.ReactNode; + size?: number; +}; + +const ImageContainer = styled.div<{ size: number }>((props) => ({ + height: props.size, + width: props.size, + borderRadius: '50%', + backgroundColor: props.color, + overflow: 'hidden', +})); + +const IconContainer = styled.div((props) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '100%', + width: '100%', + backgroundColor: props.theme.colors.white_0, +})); + +export default function Avatar({ icon, src, size = 32 }: Props) { + return ( + + {icon && {icon}} + {src && src} + + ); +} diff --git a/src/app/ui-library/callout.tsx b/src/app/ui-library/callout.tsx index 4428021e4..2d7399e61 100644 --- a/src/app/ui-library/callout.tsx +++ b/src/app/ui-library/callout.tsx @@ -56,6 +56,14 @@ const RedirectButton = styled.button` text-transform: capitalize; `; +export const AnchorLink = styled.a((props) => ({ + display: 'inline-flex', + alignItems: 'center', + gap: props.theme.space.xxxs, + textTransform: 'capitalize', + color: props.theme.colors.white_0, +})); + /** * ref: https://zeroheight.com/0683c9fa7/p/051ca8-callout/t/7814dc */ @@ -66,6 +74,7 @@ export type CalloutProps = { variant?: CalloutVariant; redirectText?: string; onClickRedirect?: () => void; + anchorRedirect?: string; }; export function Callout({ className, @@ -73,7 +82,8 @@ export function Callout({ bodyText, variant = 'info', redirectText, - onClickRedirect = () => {}, + onClickRedirect, + anchorRedirect, }: CalloutProps) { const StyledIcon = icons[variant]; return ( @@ -89,12 +99,22 @@ export function Callout({ {bodyText} {redirectText && ( - - - {redirectText} - - - + <> + {onClickRedirect && ( + + + {redirectText} + + + + )} + {anchorRedirect && ( + + {redirectText} + + + )} + )} diff --git a/src/app/ui-library/divider.tsx b/src/app/ui-library/divider.tsx new file mode 100644 index 000000000..fd852ffd7 --- /dev/null +++ b/src/app/ui-library/divider.tsx @@ -0,0 +1,11 @@ +import styled from 'styled-components'; +import Theme from 'theme'; + +const Divider = styled.div<{ verticalMargin: keyof typeof Theme.space }>((props) => ({ + display: 'flex', + width: '100%', + height: 1, + backgroundColor: props.theme.colors.white_900, + margin: `${props.theme.space[props.verticalMargin]} 0`, +})); +export default Divider; diff --git a/src/app/utils/constants.ts b/src/app/utils/constants.ts index cd9b5254f..cbc11f1e7 100644 --- a/src/app/utils/constants.ts +++ b/src/app/utils/constants.ts @@ -1,11 +1,5 @@ /* eslint-disable prefer-destructuring */ -import type { NetworkType, SettingsNetwork } from '@secretkeylabs/xverse-core'; -import { - BTC_BASE_URI_MAINNET, - BTC_BASE_URI_TESTNET, - HIRO_MAINNET_DEFAULT, - HIRO_TESTNET_DEFAULT, -} from '@secretkeylabs/xverse-core'; +import type { NetworkType } from '@secretkeylabs/xverse-core'; export const GAMMA_URL = 'https://gamma.io/'; export const TERMS_LINK = 'https://xverse.app/terms'; @@ -47,21 +41,6 @@ export const BITCOIN_DUST_AMOUNT_SATS = 1500; export const PAGINATION_LIMIT = 50; export const REFETCH_UNSPENT_UTXO_TIME = 2 * 60 * 60 * 1000; -export const initialNetworksList: SettingsNetwork[] = [ - { - type: 'Mainnet', - address: HIRO_MAINNET_DEFAULT, - btcApiUrl: BTC_BASE_URI_MAINNET, - fallbackBtcApiUrl: '', - }, - { - type: 'Testnet', - address: HIRO_TESTNET_DEFAULT, - btcApiUrl: BTC_BASE_URI_TESTNET, - fallbackBtcApiUrl: '', - }, -]; - /** * contract id of send_many transaction type */ diff --git a/src/assets/img/rareSats/ic_ordinal_small_over_card.svg b/src/assets/img/rareSats/ic_ordinal_small_over_card.svg new file mode 100644 index 000000000..5ffa566eb --- /dev/null +++ b/src/assets/img/rareSats/ic_ordinal_small_over_card.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/img/rareSats/link.svg b/src/assets/img/rareSats/link.svg new file mode 100644 index 000000000..354bf93cb --- /dev/null +++ b/src/assets/img/rareSats/link.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/img/settings/Timer.svg b/src/assets/img/settings/Timer.svg deleted file mode 100644 index 504fd5ff2..000000000 --- a/src/assets/img/settings/Timer.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/img/settings/Timer15m.svg b/src/assets/img/settings/Timer15m.svg new file mode 100644 index 000000000..b7bbd5c34 --- /dev/null +++ b/src/assets/img/settings/Timer15m.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/img/settings/Timer1h.svg b/src/assets/img/settings/Timer1h.svg new file mode 100644 index 000000000..dfdc84e87 --- /dev/null +++ b/src/assets/img/settings/Timer1h.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/img/settings/Timer30m.svg b/src/assets/img/settings/Timer30m.svg new file mode 100644 index 000000000..333786f7d --- /dev/null +++ b/src/assets/img/settings/Timer30m.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/img/settings/Timer3h.svg b/src/assets/img/settings/Timer3h.svg new file mode 100644 index 000000000..687b5e489 --- /dev/null +++ b/src/assets/img/settings/Timer3h.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/img/settings/TimerFull.svg b/src/assets/img/settings/TimerFull.svg deleted file mode 100644 index 5a8b51d50..000000000 --- a/src/assets/img/settings/TimerFull.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/img/settings/TimerHalf.svg b/src/assets/img/settings/TimerHalf.svg deleted file mode 100644 index 914f48708..000000000 --- a/src/assets/img/settings/TimerHalf.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/common/utils/ledger.ts b/src/common/utils/ledger.ts index 3095b9ce3..2eb0de284 100644 --- a/src/common/utils/ledger.ts +++ b/src/common/utils/ledger.ts @@ -1,10 +1,17 @@ -import { Account } from '@secretkeylabs/xverse-core'; +import { Account, NetworkType } from '@secretkeylabs/xverse-core'; export const delay = (ms: number) => new Promise((res) => { setTimeout(res, ms); }); +export const filterLedgerAccounts = (accounts: Account[], network: NetworkType) => + accounts.filter((account) => + account.ordinalsAddress?.startsWith(network === 'Mainnet' ? 'bc1' : 'tb1'), + ); + +// this is used for migrating the old ledger accounts to the new format +// it returns the index of the account in the list, which now maps to the deviceAccountIndex export const getDeviceAccountIndex = ( ledgerAccountsList: Account[], id: number, @@ -31,10 +38,17 @@ export const getNewAccountId = (ledgerAccountsList: Account[]) => { return ledgerAccountsList[ledgerAccountsList.length - 1].id + 1; }; -export const getDeviceNewAccountIndex = (ledgerAccountsList: Account[], masterKey?: string) => { - const ledgerAccountsIndexList = ledgerAccountsList +export const getDeviceNewAccountIndex = ( + ledgerAccountsList: Account[], + network: NetworkType, + masterKey?: string, +) => { + const networkLedgerAccounts = filterLedgerAccounts(ledgerAccountsList, network); + + const ledgerAccountsIndexList = networkLedgerAccounts .filter((account) => masterKey === account.masterPubKey) .map((account, key) => + // ledger accounts initially didn't have deviceAccountIndex, so we map to their list index as as the initial behaviour account.deviceAccountIndex !== undefined ? account.deviceAccountIndex : key, ) .sort((a, b) => a - b); diff --git a/src/locales/en.json b/src/locales/en.json index 4eea75b3d..dde66a67e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -5,7 +5,9 @@ "COMBO": "Combo", "SATS": "Sats", "SATTRIBUTES": "Sattributes", - "INPUT": "Input" + "INPUT": "Input", + "SIZE": "Size", + "BUNDLE": "Bundle" }, "LANDING_SCREEN": { "SCREEN_TITLE": "Wallet for Stacks & Bitcoin", @@ -376,7 +378,13 @@ "CANCEL_BUTTON": "Cancel", "SEE_TRANSACTION_BUTTON": "See transaction", "RETRY_BUTTON": "Try again" - } + }, + "YOUR_ORDINAL_ADDRESS": "Your ordinals address", + "YOUR_PAYMENT_ADDRESS": "Your payment address", + "INSCRIBED_SATS": "Some of these sats are inscribed", + "INSCRIBED_RARE_SATS": "Some of these sats are rare or inscribed", + "UNCONFIRMED_UTXO_WARNING": "You are spending unconfirmed outputs in this transaction. This may lower the effective fee rate causing delays in transaction confirmation", + "INSCRIBED_RARE_SATS_WARNING": "Your payment wallet holds rare or inscribed sats. To avoid spending them in your transactions and fees, transfer them to your ordinals wallet" }, "TX_ERRORS": { "INSUFFICIENT_BALANCE": "The requested transaction cannot be created due to insufficient balance", @@ -703,6 +711,11 @@ "DESCRIPTION": "Help improve the app experience, by allowing Xverse to collect anonymized usage data. This data cannot be used to identify your wallet individually.", "AUTHORIZE_DATA_COLLECTION": "Authorize data collection" }, + "BTC_URL": "BTC URL", + "STACKS_URL": "Stacks URL", + "FALLBACK_BTC_URL": "Fallback BTC URL", + "RESET_TO_DEFAULT": "Reset to default", + "REQUIRED": "Required", "NETWORK": "Network", "SECURITY": "Security", "UPDATE_PASSWORD": "Update Password", @@ -733,6 +746,9 @@ "RECOVER_ASSETS": "Recover assets", "LOCK_COUNTDOWN": "Auto-lock Timer", "LOCK_COUNTDOWN_TITLE": "Select the time duration before the wallet locks automatically.", + "LOCK_COUNTDOWN_MIN": "{{count}} minutes", + "LOCK_COUNTDOWN_HS_one": "{{count}} hour", + "LOCK_COUNTDOWN_HS_other": "{{count}} hours", "ENTER_YOUR_NEW_PASSWORD": "Enter your new password", "CONFIRM_YOUR_NEW_PASSWORD": "Confirm your new password", "TEXT_INPUT_NEW_PASSWORD_LABEL": "New Password", diff --git a/src/pages/Popup/index.css b/src/pages/Popup/index.css index f9908ff52..f74e9369a 100644 --- a/src/pages/Popup/index.css +++ b/src/pages/Popup/index.css @@ -6,7 +6,7 @@ body { background-color: #181818; } -#app-container { +#app { height: 600px; width: 360px; display: flex; @@ -19,11 +19,6 @@ body { } } -#app { - min-height: 600px; - min-width: 360px; -} - ::-webkit-scrollbar { display: none; } diff --git a/src/pages/Popup/index.html b/src/pages/Popup/index.html index 7b41ffc81..1aba6f4c3 100644 --- a/src/pages/Popup/index.html +++ b/src/pages/Popup/index.html @@ -5,8 +5,6 @@ Xverse Wallet -
-
-
+
diff --git a/tsconfig.json b/tsconfig.json index baba9fda5..e3bbeac91 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,6 @@ "isolatedModules": true, "noEmit": false, "jsx": "react-jsx", - "rootDir": "src", "outDir": "build/js", "baseUrl": "./src", "paths": {