diff --git a/package-lock.json b/package-lock.json index 8b88c9366..63820e3a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "xverse-web-extension", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "xverse-web-extension", - "version": "0.3.0", + "version": "0.4.0", "dependencies": { "@react-spring/web": "^9.6.1", - "@secretkeylabs/xverse-core": "0.8.0", + "@secretkeylabs/xverse-core": "0.10.0", "@stacks/connect": "^6.10.2", "@stacks/encryption": "4.3.5", "@stacks/stacks-blockchain-api-types": "^6.1.1", @@ -50,7 +50,7 @@ "react-tooltip": "^5.4.0", "redux": "^4.0.5", "redux-persist": "^6.0.0", - "redux-saga": "^1.1.3", + "sats-connect": "^0.1.11", "stream-browserify": "^3.0.0", "string-to-color": "^2.2.2", "styled-components": "^5.3.5", @@ -110,6 +110,43 @@ "webpack-dev-server": "^4.11.0" } }, + "../../../../desktop/work/skl/xverse-core": { + "name": "@secretkeylabs/xverse-core", + "version": "0.8.0", + "extraneous": true, + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "@noble/secp256k1": "^1.7.1", + "@scure/base": "^1.1.1", + "@stacks/encryption": "6.1.1", + "@stacks/network": "4.3.5", + "@stacks/storage": "^6.0.0", + "@stacks/transactions": "4.3.5", + "@stacks/wallet-sdk": "^5.0.2", + "axios": "0.27.2", + "bignumber.js": "9.1.0", + "bip32": "^2.0.6", + "bip39": "3.0.3", + "bitcoin-address-validation": "^2.2.1", + "bitcoinjs-lib": "5.2.0", + "bn.js": "^5.1.3", + "buffer": "6.0.3", + "ecpair": "^2.1.0", + "jsontokens": "^4.0.1", + "micro-btc-signer": "^0.4.2", + "process": "^0.11.10", + "util": "^0.12.4" + }, + "devDependencies": { + "rimraf": "^3.0.2", + "ts-loader": "^9.4.1", + "typescript": "^4.8.3", + "vitest": "^0.28.5", + "webpack": "^5.74.0", + "webpack-cli": "^4.10.0" + } + }, "node_modules/@adobe/css-tools": { "version": "4.1.0", "license": "MIT" @@ -1684,7 +1721,8 @@ }, "node_modules/@redux-saga/core": { "version": "1.2.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.2.2.tgz", + "integrity": "sha512-0qr5oleOAmI5WoZLRA6FEa30M4qKZcvx+ZQOQw+RqFeH8t20bvhE329XSPsNfTVP8C6qyDsXOSjuoV+g3+8zkg==", "dependencies": { "@babel/runtime": "^7.6.3", "@redux-saga/deferred": "^1.2.1", @@ -1702,18 +1740,21 @@ }, "node_modules/@redux-saga/deferred": { "version": "1.2.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.2.1.tgz", + "integrity": "sha512-cmin3IuuzMdfQjA0lG4B+jX+9HdTgHZZ+6u3jRAOwGUxy77GSlTi4Qp2d6PM1PUoTmQUR5aijlA39scWWPF31g==" }, "node_modules/@redux-saga/delay-p": { "version": "1.2.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.2.1.tgz", + "integrity": "sha512-MdiDxZdvb1m+Y0s4/hgdcAXntpUytr9g0hpcOO1XFVyyzkrDu3SKPgBFOtHn7lhu7n24ZKIAT1qtKyQjHqRd+w==", "dependencies": { "@redux-saga/symbols": "^1.1.3" } }, "node_modules/@redux-saga/is": { "version": "1.1.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.3.tgz", + "integrity": "sha512-naXrkETG1jLRfVfhOx/ZdLj0EyAzHYbgJWkXbB3qFliPcHKiWbv/ULQryOAEKyjrhiclmr6AMdgsXFyx7/yE6Q==", "dependencies": { "@redux-saga/symbols": "^1.1.3", "@redux-saga/types": "^1.2.1" @@ -1721,11 +1762,13 @@ }, "node_modules/@redux-saga/symbols": { "version": "1.1.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.3.tgz", + "integrity": "sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg==" }, "node_modules/@redux-saga/types": { "version": "1.2.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz", + "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==" }, "node_modules/@remix-run/router": { "version": "1.3.2", @@ -1760,6 +1803,17 @@ "@scure/base": "~1.1.0" } }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz", + "integrity": "sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, "node_modules/@scure/bip39": { "version": "1.1.1", "funding": [ @@ -1775,10 +1829,9 @@ } }, "node_modules/@secretkeylabs/xverse-core": { - "version": "0.8.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/0.8.0/9faae631820759cbbfa7f010af3498a7f46363df", - "integrity": "sha512-824kwKdHPuuOsiw42rRf7o9W98y7u5dODLBWORKxxzLEF/BbaXC4r6Wk/f5TS+ImPzOTiPvTSzfD47LKorBFGA==", - "hasInstallScript": true, + "version": "0.10.0", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/0.10.0/cfce803b0cfde5c4e01d1df94cfdbed6f85c6bfc", + "integrity": "sha512-TBsKHeMjX4nqYD5dnCIiBkPJAuLbsU6hv/DEc0qoxhmX5V+lDxzuGwnrvxrJfeW00FQpBPM9dwYGf3Z4YqilsQ==", "license": "ISC", "dependencies": { "@noble/secp256k1": "^1.7.1", @@ -10245,6 +10298,7 @@ "version": "0.4.2", "resolved": "https://registry.npmjs.org/micro-btc-signer/-/micro-btc-signer-0.4.2.tgz", "integrity": "sha512-8u5ieZF2DbJcTM6h0OceVU3ysvyiAjQvPSGke/waPyUOkRI5V2ifTqJQypO158svudq/8RHDVJ/v37ox9BUZWQ==", + "deprecated": "Switch to @scure/btc-signer for security updates and support", "funding": [ { "type": "individual", @@ -11865,7 +11919,8 @@ }, "node_modules/redux-saga": { "version": "1.2.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.2.2.tgz", + "integrity": "sha512-6xAHWgOqRP75MFuLq88waKK9/+6dCdMQjii2TohDMARVHeQ6HZrZoJ9HZ3dLqMWCZ9kj4iuS6CDsujgnovn11A==", "dependencies": { "@redux-saga/core": "^1.2.2" } @@ -12158,6 +12213,16 @@ "version": "2.1.2", "license": "MIT" }, + "node_modules/sats-connect": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/sats-connect/-/sats-connect-0.1.11.tgz", + "integrity": "sha512-8o6TJ+loIjrtzl002xFwbGfFlZYWcN7FDE/6alHTRUOFgFLKQ+ICcslxEg1tSqzQNRYpBnMv2kJlP6Zz9BAj0g==", + "dependencies": { + "jsontokens": "^4.0.1", + "process": "^0.11.10", + "util": "^0.12.4" + } + }, "node_modules/saxes": { "version": "5.0.1", "dev": true, @@ -13393,18 +13458,21 @@ }, "node_modules/typescript-compare": { "version": "0.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", + "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==", "dependencies": { "typescript-logic": "^0.0.0" } }, "node_modules/typescript-logic": { "version": "0.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz", + "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==" }, "node_modules/typescript-tuple": { "version": "2.2.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz", + "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==", "dependencies": { "typescript-compare": "^0.0.2" } @@ -15251,6 +15319,8 @@ }, "@redux-saga/core": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.2.2.tgz", + "integrity": "sha512-0qr5oleOAmI5WoZLRA6FEa30M4qKZcvx+ZQOQw+RqFeH8t20bvhE329XSPsNfTVP8C6qyDsXOSjuoV+g3+8zkg==", "requires": { "@babel/runtime": "^7.6.3", "@redux-saga/deferred": "^1.2.1", @@ -15263,26 +15333,36 @@ } }, "@redux-saga/deferred": { - "version": "1.2.1" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.2.1.tgz", + "integrity": "sha512-cmin3IuuzMdfQjA0lG4B+jX+9HdTgHZZ+6u3jRAOwGUxy77GSlTi4Qp2d6PM1PUoTmQUR5aijlA39scWWPF31g==" }, "@redux-saga/delay-p": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.2.1.tgz", + "integrity": "sha512-MdiDxZdvb1m+Y0s4/hgdcAXntpUytr9g0hpcOO1XFVyyzkrDu3SKPgBFOtHn7lhu7n24ZKIAT1qtKyQjHqRd+w==", "requires": { "@redux-saga/symbols": "^1.1.3" } }, "@redux-saga/is": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.3.tgz", + "integrity": "sha512-naXrkETG1jLRfVfhOx/ZdLj0EyAzHYbgJWkXbB3qFliPcHKiWbv/ULQryOAEKyjrhiclmr6AMdgsXFyx7/yE6Q==", "requires": { "@redux-saga/symbols": "^1.1.3", "@redux-saga/types": "^1.2.1" } }, "@redux-saga/symbols": { - "version": "1.1.3" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.3.tgz", + "integrity": "sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg==" }, "@redux-saga/types": { - "version": "1.2.1" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz", + "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==" }, "@remix-run/router": { "version": "1.3.2" @@ -15298,6 +15378,13 @@ "@noble/hashes": "~1.2.0", "@noble/secp256k1": "~1.7.0", "@scure/base": "~1.1.0" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz", + "integrity": "sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==" + } } }, "@scure/bip39": { @@ -15308,9 +15395,9 @@ } }, "@secretkeylabs/xverse-core": { - "version": "0.8.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/0.8.0/9faae631820759cbbfa7f010af3498a7f46363df", - "integrity": "sha512-824kwKdHPuuOsiw42rRf7o9W98y7u5dODLBWORKxxzLEF/BbaXC4r6Wk/f5TS+ImPzOTiPvTSzfD47LKorBFGA==", + "version": "0.10.0", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/0.10.0/cfce803b0cfde5c4e01d1df94cfdbed6f85c6bfc", + "integrity": "sha512-TBsKHeMjX4nqYD5dnCIiBkPJAuLbsU6hv/DEc0qoxhmX5V+lDxzuGwnrvxrJfeW00FQpBPM9dwYGf3Z4YqilsQ==", "requires": { "@noble/secp256k1": "^1.7.1", "@scure/base": "^1.1.1", @@ -22225,6 +22312,8 @@ }, "redux-saga": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.2.2.tgz", + "integrity": "sha512-6xAHWgOqRP75MFuLq88waKK9/+6dCdMQjii2TohDMARVHeQ6HZrZoJ9HZ3dLqMWCZ9kj4iuS6CDsujgnovn11A==", "requires": { "@redux-saga/core": "^1.2.2" } @@ -22395,6 +22484,16 @@ "safer-buffer": { "version": "2.1.2" }, + "sats-connect": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/sats-connect/-/sats-connect-0.1.11.tgz", + "integrity": "sha512-8o6TJ+loIjrtzl002xFwbGfFlZYWcN7FDE/6alHTRUOFgFLKQ+ICcslxEg1tSqzQNRYpBnMv2kJlP6Zz9BAj0g==", + "requires": { + "jsontokens": "^4.0.1", + "process": "^0.11.10", + "util": "^0.12.4" + } + }, "saxes": { "version": "5.0.1", "dev": true, @@ -23200,15 +23299,21 @@ }, "typescript-compare": { "version": "0.0.2", + "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", + "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==", "requires": { "typescript-logic": "^0.0.0" } }, "typescript-logic": { - "version": "0.0.0" + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz", + "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==" }, "typescript-tuple": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz", + "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==", "requires": { "typescript-compare": "^0.0.2" } diff --git a/package.json b/package.json index 8b8d18658..cde5dd54f 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "xverse-web-extension", "description": "A Bitcoin wallet for Web3", - "version": "0.3.0", + "version": "0.5.0", "private": true, "dependencies": { "@react-spring/web": "^9.6.1", - "@secretkeylabs/xverse-core": "0.8.0", + "@secretkeylabs/xverse-core": "0.10.0", "@stacks/connect": "^6.10.2", "@stacks/encryption": "4.3.5", "@stacks/stacks-blockchain-api-types": "^6.1.1", @@ -46,7 +46,7 @@ "react-tooltip": "^5.4.0", "redux": "^4.0.5", "redux-persist": "^6.0.0", - "redux-saga": "^1.1.3", + "sats-connect": "^0.1.11", "stream-browserify": "^3.0.0", "string-to-color": "^2.2.2", "styled-components": "^5.3.5", diff --git a/src/app/components/accountHeader/index.tsx b/src/app/components/accountHeader/index.tsx index be2de83c0..0b9de552a 100644 --- a/src/app/components/accountHeader/index.tsx +++ b/src/app/components/accountHeader/index.tsx @@ -49,9 +49,10 @@ const OptionsButton = styled.button((props) => ({ interface AccountHeaderComponentProps { disableMenuOption?: boolean; disableAccountSwitch?: boolean; + disableCopy?: boolean; } -function AccountHeaderComponent({ disableMenuOption, disableAccountSwitch = false }:AccountHeaderComponentProps) { +function AccountHeaderComponent({ disableMenuOption, disableAccountSwitch = false, disableCopy = false }:AccountHeaderComponentProps) { const navigate = useNavigate(); const { selectedAccount, @@ -112,36 +113,44 @@ function AccountHeaderComponent({ disableMenuOption, disableAccountSwitch = fals return ( <> - { showResetWalletDisplay - && ( - - - + {showResetWalletDisplay && ( + + + )} - + {!disableMenuOption && ( - - Options - + + Options + + )} + {showOptionsDialog && ( + )} - {showOptionsDialog && } - ); } diff --git a/src/app/components/accountRow/index.tsx b/src/app/components/accountRow/index.tsx index 0d304fd46..e569f3b03 100644 --- a/src/app/components/accountRow/index.tsx +++ b/src/app/components/accountRow/index.tsx @@ -2,6 +2,7 @@ import styled from 'styled-components'; import { getAccountGradient } from '@utils/gradient'; import { Tooltip } from 'react-tooltip'; import 'react-tooltip/dist/react-tooltip.css'; +import OrdinalsIcon from '@assets/img/nftDashboard/white_ordinals_icon.svg'; import { useTranslation } from 'react-i18next'; import { getTruncatedAddress, getAddressDetail } from '@utils/helper'; import BarLoader from '@components/barLoader'; @@ -79,6 +80,13 @@ const StyledToolTip = styled(Tooltip)` padding: 7px; `; +const AddressContainer = styled.div({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', +}); + const Button = styled.button` background: transparent; `; @@ -102,10 +110,31 @@ const CopyButton = styled.button` } `; +const OrdinalImage = styled.img({ + width: 12, + height: 12, + marginRight: 4, +}); + +const AddressText = styled.h1((props) => ({ + ...props.theme.body_m, + marginTop: props.theme.spacing(1), + color: props.theme.colors.white['400'], +})); + +const BitcoinDot = styled.div((props) => ({ + borderRadius: 20, + background: props.theme.colors.feedback.caution, + width: 10, + marginRight: 4, + marginLeft: 4, + height: 10, +})); interface Props { account: Account | null; isSelected: boolean; allowCopyAddress?: boolean; + showOrdinalAddress?: boolean; onAccountSelected: (account: Account) => void; } @@ -114,6 +143,7 @@ function AccountRow({ isSelected, onAccountSelected, allowCopyAddress, + showOrdinalAddress, }: Props) { const { t } = useTranslation('translation', { keyPrefix: 'DASHBOARD_SCREEN' }); const { @@ -151,6 +181,19 @@ function AccountRow({ onAccountSelected(account!); }; + const showOrdinalBtcAddress = ( + + + + {`${getTruncatedAddress(account?.ordinalsAddress!)} / `} + + + + {`${getTruncatedAddress(account?.btcAddress!)}`} + + + ); + const displayAddress = allowCopyAddress ? ( @@ -182,7 +225,7 @@ function AccountRow({ /> ) : ( - {getAddressDetail(account!)} + {showOrdinalAddress ? showOrdinalBtcAddress : getAddressDetail(account!)} ); return ( diff --git a/src/app/components/confirmBtcTransactionComponent/btcRecipientComponent.tsx b/src/app/components/confirmBtcTransactionComponent/btcRecipientComponent.tsx new file mode 100644 index 000000000..3ef0c39f3 --- /dev/null +++ b/src/app/components/confirmBtcTransactionComponent/btcRecipientComponent.tsx @@ -0,0 +1,136 @@ +import TransferDetailView from '@components/transferDetailView'; +import OutputIcon from '@assets/img/transactions/output.svg'; +import { currencySymbolMap } from '@secretkeylabs/xverse-core/types/currency'; +import { StoreState } from '@stores/index'; +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 Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + background: props.theme.colors.background.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: 16, +})); + +const RowContainer = styled.div({ + display: 'flex', + flexDirection: 'row', + width: '100%', + alignItems: 'center', +}); + +const AddressContainer = styled.div({ + marginTop: 22, +}); + +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 ColumnContainer = styled.div({ + display: 'flex', + flexDirection: 'column', + flex: 1, + justifyContent: 'flex-end', + alignItems: 'flex-end', +}); + +interface Props { + recipientIndex?: number; + address?: string; + value: string; + subValue?: BigNumber; + totalRecipient?: number; + icon: string; + title: string; + heading?: string; +} +function BtcRecipientComponent({ + recipientIndex, + address, + value, + totalRecipient, + subValue, + icon, + title, + heading, +}: Props) { + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + const { fiatCurrency } = useSelector((state: StoreState) => state.walletState); + + const getFiatAmountString = (fiatAmount: BigNumber) => { + if (fiatAmount) { + if (fiatAmount.isLessThan(0.01)) { + return `<${currencySymbolMap[fiatCurrency]}0.01 ${fiatCurrency}`; + } + return ( + {text}} + /> + ); + } + return ''; + }; + + return ( + + {recipientIndex && totalRecipient && ( + {`${t( + 'RECIPIENT' + )} ${recipientIndex}/${totalRecipient}`} + )} + {heading && {heading}} + + + {title} + + {value} + {subValue && {getFiatAmountString(subValue)}} + + + {address && ( + + + + )} + + ); +} + +export default BtcRecipientComponent; diff --git a/src/app/screens/confrimBtcTransaction/confirmBtcTransactionComponent/index.tsx b/src/app/components/confirmBtcTransactionComponent/index.tsx similarity index 69% rename from src/app/screens/confrimBtcTransaction/confirmBtcTransactionComponent/index.tsx rename to src/app/components/confirmBtcTransactionComponent/index.tsx index 5c6af734d..607e8d5e8 100644 --- a/src/app/screens/confrimBtcTransaction/confirmBtcTransactionComponent/index.tsx +++ b/src/app/components/confirmBtcTransactionComponent/index.tsx @@ -4,18 +4,37 @@ import styled from 'styled-components'; import { ReactNode, useEffect, useState } from 'react'; import BigNumber from 'bignumber.js'; import ActionButton from '@components/button'; +import AssetIcon from '@assets/img/transactions/Assets.svg'; import SettingIcon from '@assets/img/dashboard/faders_horizontal.svg'; import TransactionSettingAlert from '@components/transactionSetting'; import { useSelector } from 'react-redux'; +import IconBitcoin from '@assets/img/dashboard/bitcoin_icon.svg'; import { StoreState } from '@stores/index'; import { signBtcTransaction } from '@secretkeylabs/xverse-core/transactions'; import { useMutation } from '@tanstack/react-query'; -import { Recipient, SignedBtcTx, signOrdinalSendTransaction } from '@secretkeylabs/xverse-core/transactions/btc'; -import TransferAmountView from '@components/transferAmountView'; -import TransferFeeView from '@components/transferFeeView'; import { - btcToSats, BtcUtxoDataResponse, ErrorCodes, ResponseError, + Recipient, + SignedBtcTx, + signOrdinalSendTransaction, +} from '@secretkeylabs/xverse-core/transactions/btc'; +import { + BtcUtxoDataResponse, + ErrorCodes, + getBtcFiatEquivalent, + ResponseError, + satsToBtc, } from '@secretkeylabs/xverse-core'; +import TransactionDetailComponent from '../transactionDetailComponent'; +import BtcRecipientComponent from './btcRecipientComponent'; + +const OuterContainer = styled.div` + display: flex; + flex-direction: column; + overflow-y: auto; + &::-webkit-scrollbar { + display: none; + } +`; const Container = styled.div((props) => ({ display: 'flex', @@ -73,14 +92,24 @@ const ErrorText = styled.h1((props) => ({ color: props.theme.colors.feedback.error, })); +interface ReviewTransactionTitleProps { + isOridnalTx: boolean; +} +const ReviewTransactionText = styled.h1((props) => ({ + ...props.theme.headline_s, + color: props.theme.colors.white[0], + marginBottom: props.theme.spacing(16), + textAlign: props.isOridnalTx ? 'center' : 'left', +})); + interface Props { fee: BigNumber; - children: ReactNode; loadingBroadcastedTx: boolean; - amount: BigNumber; - recipientAddress: string; signedTxHex: string; ordinalTxUtxo?: BtcUtxoDataResponse; + recipients: Recipient[]; + children?: ReactNode; + assetDetail?: string; onConfirmClick: (signedTxHex: string) => void; onCancelClick: () => void; onBackButtonClick: () => void; @@ -88,12 +117,12 @@ interface Props { function ConfirmBtcTransactionComponent({ fee, - children, loadingBroadcastedTx, - amount, - recipientAddress, signedTxHex, ordinalTxUtxo, + recipients, + children, + assetDetail, onConfirmClick, onCancelClick, onBackButtonClick, @@ -103,7 +132,7 @@ function ConfirmBtcTransactionComponent({ const [loading, setLoading] = useState(false); const [openTransactionSettingModal, setOpenTransactionSettingModal] = useState(false); const { - btcAddress, selectedAccount, seedPhrase, network, + btcAddress, selectedAccount, seedPhrase, network, btcFiatRate, } = useSelector( (state: StoreState) => state.walletState, ); @@ -111,7 +140,10 @@ function ConfirmBtcTransactionComponent({ const [error, setError] = useState(''); const [signedTx, setSignedTx] = useState(signedTxHex); const { - isLoading, data, error: txError, mutate, + isLoading, + data, + error: txError, + mutate, } = useMutation< SignedBtcTx, ResponseError, @@ -119,16 +151,14 @@ function ConfirmBtcTransactionComponent({ recipients: Recipient[]; txFee: string; } - >( - async ({ recipients, txFee }) => signBtcTransaction( - recipients, - btcAddress, - selectedAccount?.id ?? 0, - seedPhrase, - network.type, - new BigNumber(txFee), - ), - ); + >(async ({ recipients, txFee }) => signBtcTransaction( + recipients, + btcAddress, + selectedAccount?.id ?? 0, + seedPhrase, + network.type, + new BigNumber(txFee), + )); const { isLoading: isLoadingOrdData, @@ -137,7 +167,7 @@ function ConfirmBtcTransactionComponent({ mutate: ordinalMutate, } = useMutation(async (txFee) => { const signedTx = await signOrdinalSendTransaction( - recipientAddress, + recipients[0]?.address, ordinalTxUtxo!, btcAddress, Number(selectedAccount?.id), @@ -174,13 +204,6 @@ function ConfirmBtcTransactionComponent({ const onApplyClick = (modifiedFee: string) => { setCurrentFee(new BigNumber(modifiedFee)); - const recipients: Recipient[] = [ - { - address: recipientAddress, - amountSats: btcToSats(new BigNumber(amount)), - }, - ]; - if (ordinalTxUtxo) ordinalMutate(modifiedFee); else mutate({ recipients, txFee: modifiedFee }); setLoading(true); @@ -191,7 +214,7 @@ function ConfirmBtcTransactionComponent({ }; useEffect(() => { - if (recipientAddress && amount && txError) { + if (recipients && txError) { setOpenTransactionSettingModal(false); if (Number(txError) === ErrorCodes.InSufficientBalance) { setError(t('TX_ERRORS.INSUFFICIENT_BALANCE')); @@ -202,7 +225,7 @@ function ConfirmBtcTransactionComponent({ }, [txError]); useEffect(() => { - if (recipientAddress && amount && ordinalError) { + if (recipients && ordinalError) { setOpenTransactionSettingModal(false); if (Number(txError) === ErrorCodes.InSufficientBalance) { setError(t('TX_ERRORS.INSUFFICIENT_BALANCE')); @@ -213,12 +236,46 @@ function ConfirmBtcTransactionComponent({ }, [ordinalError]); return ( - <> - {!isGalleryOpen && } + + {!isGalleryOpen && ( + + )} - {amount && } {children} - + + {t('CONFIRM_TRANSACTION.REVIEW_TRNSACTION')} + + + {ordinalTxUtxo ? ( + + ) : ( + recipients?.map((recipient, index) => ( + + )) + )} + + + + + + + {isExpanded && ( + + {parsedPsbt?.inputs.map((input, index) => ( + + + {renderSubValue(input, address[index])} + + + ))} + + {t('OUTPUT')} + {parsedPsbt?.outputs.map((output) => ( + + + { + output.address === btcAddress || output.address === ordinalsAddress + ? ( + + (Your Address) + {getTruncatedAddress(output.address)} + + ) + : {getTruncatedAddress(output.address)} + } + + + + ))} + + )} + + ); +} + +export default InputOutputComponent; diff --git a/src/app/components/copyButton/index.tsx b/src/app/components/copyButton/index.tsx new file mode 100644 index 000000000..52679e6e5 --- /dev/null +++ b/src/app/components/copyButton/index.tsx @@ -0,0 +1,70 @@ +import styled from 'styled-components'; +import Copy from '@assets/img/nftDashboard/Copy.svg'; +import Tick from '@assets/img/tick.svg'; +import { useEffect, useState } from 'react'; +import { Tooltip } from 'react-tooltip'; +import { useTranslation } from 'react-i18next'; + +const Button = styled.button((props) => ({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + background: 'transparent', + marginLeft: props.theme.spacing(3), + padding: 3, + ':hover': { + background: props.theme.colors.white[900], + borderRadius: 24, + }, +})); + +const Img = styled.img({ + width: 20, + height: 20, + +}); + +const StyledToolTip = styled(Tooltip)` + background-color: #ffffff; + color: #12151e; + border-radius: 8px; + padding: 7px; +`; + +interface Props { + text: string; +} +function CopyButton({ text }: Props) { + const [isCopied, setIsCopied] = useState(false); + const { t } = useTranslation('translation', { keyPrefix: 'NFT_DASHBOARD_SCREEN' }); + + const onCopyClick = () => { + navigator.clipboard.writeText(text); + setIsCopied(true); + }; + + useEffect(() => { + if (isCopied) { + setTimeout(() => { + setIsCopied(false); + }, 5000); + } + }, [isCopied]); + + return ( + <> + + + + ); +} + +export default CopyButton; diff --git a/src/app/components/postCondition/postConditionView/index.tsx b/src/app/components/postCondition/postConditionView/index.tsx index 81feecf5e..8d58889da 100644 --- a/src/app/components/postCondition/postConditionView/index.tsx +++ b/src/app/components/postCondition/postConditionView/index.tsx @@ -101,7 +101,7 @@ function PostConditionsView({ postCondition, amount }: Props) { ? t('CONTRACT_ADDRESS') : isSending ? t('MY_ADDRESS') - : t('RECEPIENT_ADDRESS')}`} + : t('RECIPIENT_ADDRESS')}`} /> diff --git a/src/app/screens/nftDashboard/receiveNft/receiveCardComponent/index.tsx b/src/app/components/receiveCardComponent/index.tsx similarity index 88% rename from src/app/screens/nftDashboard/receiveNft/receiveCardComponent/index.tsx rename to src/app/components/receiveCardComponent/index.tsx index 726f9556d..a224fcd0a 100644 --- a/src/app/screens/nftDashboard/receiveNft/receiveCardComponent/index.tsx +++ b/src/app/components/receiveCardComponent/index.tsx @@ -5,15 +5,16 @@ import { getShortTruncatedAddress } from '@utils/helper'; import Copy from '@assets/img/nftDashboard/Copy.svg'; import QrCode from '@assets/img/nftDashboard/QrCode.svg'; import { useTranslation } from 'react-i18next'; -import { ChangeShowOrdinalReceiveAlertAction } from '@stores/wallet/actions/actionCreators'; +import { ReactNode } from 'react'; +import { ChangeShowBtcReceiveAlertAction, ChangeShowOrdinalReceiveAlertAction } from '@stores/wallet/actions/actionCreators'; import { useDispatch } from 'react-redux'; import useWalletSelector from '@hooks/useWalletSelector'; interface Props { - icon: string; title: string; address: string; onQrAddressClick: () => void; + children: ReactNode; } const ReceiveCard = styled.div((props) => ({ @@ -53,11 +54,6 @@ const RowContainer = styled.div({ flex: 2, }); -const Icon = styled.img({ - width: 24, - height: 24, -}); - const ButtonIcon = styled.img({ width: 22, height: 22, @@ -83,23 +79,26 @@ const StyledToolTip = styled(Tooltip)` `; function ReceiveCardComponent({ - icon, title, address, onQrAddressClick, + children, title, address, onQrAddressClick, }: Props) { const { t } = useTranslation('translation', { keyPrefix: 'NFT_DASHBOARD_SCREEN' }); const dispatch = useDispatch(); const { ordinalsAddress, + btcAddress, showOrdinalReceiveAlert, + showBtcReceiveAlert, } = useWalletSelector(); const onCopyClick = () => { if (ordinalsAddress === address && showOrdinalReceiveAlert !== null) { dispatch(ChangeShowOrdinalReceiveAlertAction(true)); } + if (btcAddress === address && showBtcReceiveAlert !== null) { dispatch(ChangeShowBtcReceiveAlertAction(true)); } navigator.clipboard.writeText(address); }; return ( - + {children} {title} {getShortTruncatedAddress(address)} diff --git a/src/app/components/recipinetAddressView/index.tsx b/src/app/components/recipinetAddressView/index.tsx index 74e069188..c7f5b7d8b 100644 --- a/src/app/components/recipinetAddressView/index.tsx +++ b/src/app/components/recipinetAddressView/index.tsx @@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import ArrowSquareOut from '@assets/img/arrow_square_out.svg'; import { getExplorerUrl } from '@utils/helper'; -import { useBnsName } from '@hooks/useBnsName'; +import { useBnsName } from '@hooks/queries/useBnsName'; import useNetworkSelector from '@hooks/useNetwork'; const InfoContainer = styled.div((props) => ({ @@ -65,7 +65,7 @@ function RecipientAddressView({ recipient }: Props) { return ( - {t('RECEPIENT_ADDRESS')} + {t('RECIPIENT_ADDRESS')} {bnsName} diff --git a/src/app/components/sendForm/index.tsx b/src/app/components/sendForm/index.tsx index c8e1171ef..cd5aea25e 100644 --- a/src/app/components/sendForm/index.tsx +++ b/src/app/components/sendForm/index.tsx @@ -12,7 +12,7 @@ import Info from '@assets/img/info.svg'; import Switch from '@assets/img/send/switch.svg'; import ActionButton from '@components/button'; import { useNavigate } from 'react-router-dom'; -import { useBnsName, useBNSResolver, useDebounce } from '@hooks/useBnsName'; +import { useBnsName, useBNSResolver, useDebounce } from '@hooks/queries/useBnsName'; import { getFiatEquivalent } from '@secretkeylabs/xverse-core/transactions'; import InfoContainer from '@components/infoContainer'; import useNetworkSelector from '@hooks/useNetwork'; @@ -402,14 +402,14 @@ function SendForm({ setRecipientAddress(e.target.value); }; - const renderEnterRecepientSection = ( + const renderEnterRecipientSection = ( - {t('RECEPIENT')} + {t('RECIPIENT')} @@ -475,7 +475,7 @@ function SendForm({ {buyCryptoMessage} {children} - {renderEnterRecepientSection} + {renderEnterRecipientSection} {addressError} diff --git a/src/app/components/transactionDetailComponent/index.tsx b/src/app/components/transactionDetailComponent/index.tsx new file mode 100644 index 000000000..166c35a2e --- /dev/null +++ b/src/app/components/transactionDetailComponent/index.tsx @@ -0,0 +1,92 @@ +import styled from 'styled-components'; +import { NumericFormat } from 'react-number-format'; +import { currencySymbolMap } from '@secretkeylabs/xverse-core/types/currency'; +import { useSelector } from 'react-redux'; +import BigNumber from 'bignumber.js'; +import { StoreState } from '@stores/index'; + +const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'row', + background: props.theme.colors.background.elevation1, + borderRadius: 12, + padding: '12px 16px', + justifyContent: 'center', + alignItems: 'center', + marginBottom: 12, +})); + +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 ColumnContainer = styled.div({ + display: 'flex', + flexDirection: 'column', + flex: 1, + alignItems: 'flex-end', +}); + +const TitleContainer = styled.div({ + display: 'flex', + flexDirection: 'column', +}); + +interface Props { + title: string; + subTitle?: string; + value?: string; + subValue?: BigNumber; +} + +function TransactionDetailComponent({ + title, subTitle, value, subValue, +}: Props) { + const { + fiatCurrency, + } = useSelector((state: StoreState) => state.walletState); + + const getFiatAmountString = (fiatAmount: BigNumber) => { + if (fiatAmount) { + if (fiatAmount.isLessThan(0.01)) { + return `<${currencySymbolMap[fiatCurrency]}0.01 ${fiatCurrency}`; + } + return ( + + ); + } + return ''; + }; + return ( + + + {title} + {subTitle && {subTitle}} + + + {value && {value}} + {subValue && {getFiatAmountString(subValue)}} + + + ); +} + +export default TransactionDetailComponent; diff --git a/src/app/components/transactionSetting/index.tsx b/src/app/components/transactionSetting/index.tsx index 652a878ce..bb9127326 100644 --- a/src/app/components/transactionSetting/index.tsx +++ b/src/app/components/transactionSetting/index.tsx @@ -16,7 +16,7 @@ import { } from '@secretkeylabs/xverse-core/currency'; import { useSelector } from 'react-redux'; import { StoreState } from '@stores/index'; -import { getBtcFees, getBtcFeesForOrdinalSend, isCustomFeesAllowed } from '@secretkeylabs/xverse-core/transactions/btc'; +import { getBtcFees, getBtcFeesForOrdinalSend, isCustomFeesAllowed, Recipient } from '@secretkeylabs/xverse-core/transactions/btc'; import { btcToSats, BtcUtxoDataResponse, ErrorCodes } from '@secretkeylabs/xverse-core'; const Text = styled.h1((props) => ({ @@ -139,8 +139,7 @@ interface Props { availableBalance?: BigNumber; allowEditNonce?: boolean; type?: TxType; - btcRecepientAddress?: string; - amount?: BigNumber; + btcRecipients?: Recipient[]; ordinalTxUtxo?: BtcUtxoDataResponse; } type TxType = 'STX' | 'BTC' | 'Ordinals'; @@ -157,8 +156,7 @@ function TransactionSettingAlert({ availableBalance, allowEditNonce = true, type = 'STX', - btcRecepientAddress, - amount, + btcRecipients, ordinalTxUtxo, }:Props) { const { t } = useTranslation('translation'); @@ -279,14 +277,9 @@ function TransactionSettingAlert({ setIsLoading(true); if (mode?.value === 'custom') inputRef?.current?.focus(); else if (type === 'BTC') { - if (amount && selectedAccount && btcRecepientAddress) { + if (btcRecipients && selectedAccount) { const btcFee = await getBtcFees( - [ - { - address: btcRecepientAddress, - amountSats: btcToSats(new BigNumber(amount)), - }, - ], + btcRecipients, btcAddress, network.type, mode?.value, @@ -294,9 +287,9 @@ function TransactionSettingAlert({ setFeeInput(btcFee.toString()); } } else if (type === 'Ordinals') { - if (btcRecepientAddress && ordinalTxUtxo) { + if (btcRecipients && ordinalTxUtxo) { const txFees = await getBtcFeesForOrdinalSend( - btcRecepientAddress, + btcRecipients[0].address, ordinalTxUtxo, btcAddress, network.type, diff --git a/src/app/components/transactions/transactionAmount.tsx b/src/app/components/transactions/transactionAmount.tsx index 259749bca..686946978 100644 --- a/src/app/components/transactions/transactionAmount.tsx +++ b/src/app/components/transactions/transactionAmount.tsx @@ -41,7 +41,7 @@ export default function TransactionAmount(props: TransactionAmountProps): JSX.El if (transaction.txType === 'contract_call') { if (transaction.tokenType === 'fungible') { const token = coinsList?.find( - (cn) => cn.principal === transaction.contractCall?.contract_id + (cn) => cn.principal === transaction.contractCall?.contract_id, ); const prefix = transaction.incoming ? '' : '-'; return ( @@ -59,17 +59,19 @@ export default function TransactionAmount(props: TransactionAmountProps): JSX.El } } else if (coin === 'BTC') { const prefix = transaction.incoming ? '' : '-'; - return ( - ( - {`${prefix}${value} BTC`} - )} - /> - ); + if (!new BigNumber(transaction.amount).isEqualTo(0)) { + return ( + ( + {`${prefix}${value} BTC`} + )} + /> + ); + } } return null; } diff --git a/src/app/components/transactions/transactionStatusIcon.tsx b/src/app/components/transactions/transactionStatusIcon.tsx index 32f9fcd36..5d1850b4e 100644 --- a/src/app/components/transactions/transactionStatusIcon.tsx +++ b/src/app/components/transactions/transactionStatusIcon.tsx @@ -5,6 +5,7 @@ import SendIcon from '@assets/img/transactions/sent.svg'; import PendingIcon from '@assets/img/transactions/pending.svg'; import ContractIcon from '@assets/img/transactions/contract.svg'; import FailedIcon from '@assets/img/transactions/failed.svg'; +import OrdinalsIcon from '@assets/img/transactions/ordinal.svg'; interface TransactionStatusIconPros { transaction: StxTransactionData | BtcTransactionData; @@ -34,6 +35,9 @@ function TransactionStatusIcon(props: TransactionStatusIconPros) { } if (currency === 'BTC') { const tx = transaction as BtcTransactionData; + if (tx.isOrdinal) { + return ordinals-transfer; + } if (tx.txStatus === 'pending') { return pending; } diff --git a/src/app/components/transactionsRequests/utils.ts b/src/app/components/transactionsRequests/utils.ts index 513e30898..ed24cd582 100644 --- a/src/app/components/transactionsRequests/utils.ts +++ b/src/app/components/transactionsRequests/utils.ts @@ -1,6 +1,6 @@ import { ExternalMethods, MESSAGE_SOURCE, TransactionResponseMessage, TxResult, -} from 'content-scripts/message-types'; +} from '@common/types/message-types'; interface FormatTxSignatureResponseArgs { payload: string; diff --git a/src/app/components/transferAmountComponent/index.tsx b/src/app/components/transferAmountComponent/index.tsx new file mode 100644 index 000000000..bfaf08ace --- /dev/null +++ b/src/app/components/transferAmountComponent/index.tsx @@ -0,0 +1,185 @@ +import styled from 'styled-components'; +import DropDownIcon from '@assets/img/transactions/dropDownIcon.svg'; +import AddressIcon from '@assets/img/transactions/address.svg'; +import { useTranslation } from 'react-i18next'; +import { NumericFormat } from 'react-number-format'; +import { currencySymbolMap } from '@secretkeylabs/xverse-core/types/currency'; +import { useSelector } from 'react-redux'; +import BigNumber from 'bignumber.js'; +import { + animated, config, useSpring, +} from '@react-spring/web'; +import { StoreState } from '@stores/index'; +import TransferDetailView from '@components/transferDetailView'; + +const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + background: props.theme.colors.background.elevation1, + borderRadius: 12, + padding: '12px 16px', + justifyContent: 'center', + marginBottom: 12, +})); + +const FromContainer = styled.div((props) => ({ + marginTop: props.theme.spacing(13), +})); + +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 DescriptionText = styled.h1((props) => ({ + ...props.theme.body_medium_m, + marginTop: props.theme.spacing(6), + marginBottom: props.theme.spacing(4), + color: props.theme.colors.white[400], +})); + +const SubValueText = styled.h1((props) => ({ + ...props.theme.body_m, + fontSize: 12, + color: props.theme.colors.white[400], +})); + +const Icon = styled.img((props) => ({ + marginRight: props.theme.spacing(4), + width: 32, + height: 32, + borderRadius: 30, +})); + +interface ColumnProps { + isExpanded: boolean; +} + +const ColumnContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: props.isExpanded ? 'column' : 'row', + flex: 1, + justifyContent: 'flex-end', + alignItems: props.isExpanded ? 'flex-end' : 'center', +})); + +const RowContainer = styled.div({ + display: 'flex', + flexDirection: 'row', + width: '100%', + alignItems: 'center', +}); + +const ExpandedContainer = styled(animated.div)({ + display: 'flex', + flexDirection: 'column', + marginTop: 16, +}); + +const Button = styled.button((props) => ({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + background: 'transparent', + marginLeft: props.theme.spacing(4), +})); + +interface Props { + title: string; + address: string; + description?: string; + value: string; + subValue: string; + icon: string; + isExpanded?: boolean; + onArrowClick: () => void; + +} + +function TransferAmountComponent({ + title, address, value, subValue, description, icon, isExpanded = false, onArrowClick, +}: Props) { + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + const { + fiatCurrency, + } = useSelector((state: StoreState) => state.walletState); + + const slideInStyles = useSpring({ + config: { ...config.gentle, duration: 400 }, + from: { opacity: 0, height: 0 }, + to: { + opacity: isExpanded ? 1 : 0, + height: isExpanded ? 80 : 0, + }, + }); + + const arrowRotation = useSpring({ + transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)', + config: { ...config.stiff }, + }); + + const getFiatAmountString = (fiatAmount: BigNumber) => { + if (fiatAmount) { + if (fiatAmount.isLessThan(0.01)) { + return `<${currencySymbolMap[fiatCurrency]}0.01 ${fiatCurrency}`; + } + return ( + {text}} + /> + ); + } + return ''; + }; + + const renderAmount = value && ( + <> + {value} + {isExpanded && {getFiatAmountString(subValue)}} + + ); + + return ( + + + {icon && !isExpanded && } + {title} + + {!isExpanded && renderAmount} + + + + + {isExpanded && ( + + {description} + + + {t('AMOUNT')} + + {renderAmount} + + + {/* + {t('FROM')} + + */} + + )} + + ); +} + +export default TransferAmountComponent; diff --git a/src/app/components/transferDetailView/index.tsx b/src/app/components/transferDetailView/index.tsx new file mode 100644 index 000000000..cee092486 --- /dev/null +++ b/src/app/components/transferDetailView/index.tsx @@ -0,0 +1,80 @@ +import styled from 'styled-components'; +import { getTruncatedAddress } from '@utils/helper'; +import { ReactNode } from 'react'; +import CopyButton from '@components/copyButton'; +import useWalletSelector from '@hooks/useWalletSelector'; + +const RowContainer = styled.div({ + display: 'flex', + flexDirection: 'row', + width: '100%', + alignItems: 'center', +}); + +const Icon = styled.img((props) => ({ + marginRight: props.theme.spacing(4), + width: 32, + height: 32, + borderRadius: 30, +})); + +const AddressContainer = styled.div({ + display: 'flex', + flexDirection: 'row', + flex: 1, + alignItems: 'center', + justifyContent: 'flex-end', +}); + +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 AmountText = styled.h1((props) => ({ + ...props.theme.body_medium_m, + color: props.theme.colors.white[0], +})); + +const ColumnContainer = styled.div({ + display: 'flex', + flexDirection: 'column', +}); + +interface Props { + icon: string; + title?: string; + amount?: string; + children?: ReactNode; + address: string; + hideAddress?: boolean; + hideCopyButton?: boolean; +} + +function TransferDetailView({ + icon, title, amount, children, address, hideAddress, hideCopyButton, +}: Props) { + return ( + + + {amount ? ( + + {amount} + {children} + + ) + : {title}} + + {!hideAddress && {getTruncatedAddress(address)}} + {!hideCopyButton && } + + + ); +} + +export default TransferDetailView; diff --git a/src/app/hooks/useBnsName.ts b/src/app/hooks/queries/useBnsName.ts similarity index 94% rename from src/app/hooks/useBnsName.ts rename to src/app/hooks/queries/useBnsName.ts index 8234051c9..870b47d39 100644 --- a/src/app/hooks/useBnsName.ts +++ b/src/app/hooks/queries/useBnsName.ts @@ -3,8 +3,8 @@ import { StacksNetwork, validateStxAddress } from '@secretkeylabs/xverse-core'; import { fetchAddressOfBnsName, getBnsName, } from '@secretkeylabs/xverse-core/api'; -import useWalletSelector from './useWalletSelector'; -import useNetworkSelector from './useNetwork'; +import useWalletSelector from '../useWalletSelector'; +import useNetworkSelector from '../useNetwork'; export const useBnsName = (walletAddress: string, network: StacksNetwork) => { const [bnsName, setBnsName] = useState(''); diff --git a/src/app/hooks/queries/useBtcWalletData.ts b/src/app/hooks/queries/useBtcWalletData.ts new file mode 100644 index 000000000..87c3b7334 --- /dev/null +++ b/src/app/hooks/queries/useBtcWalletData.ts @@ -0,0 +1,33 @@ +import BigNumber from 'bignumber.js'; +import { useDispatch } from 'react-redux'; +import { useQuery } from '@tanstack/react-query'; +import { getBtcWalletData } from '@secretkeylabs/xverse-core/api/btc'; +import { BtcAddressData } from '@secretkeylabs/xverse-core/types'; +import { SetBtcWalletDataAction } from '@stores/wallet/actions/actionCreators'; +import useWalletSelector from '../useWalletSelector'; + +export const useBtcWalletData = () => { + const dispatch = useDispatch(); + const { btcAddress, network } = useWalletSelector(); + + const fetchBtcWalletData = async () => { + try { + const btcData: BtcAddressData = await getBtcWalletData(btcAddress, network.type); + console.log("🚀 ~ file: useBtcWalletData.ts:16 ~ fetchBtcWalletData ~ btcData:", btcData) + + const btcBalance = new BigNumber(btcData.finalBalance); + dispatch(SetBtcWalletDataAction(btcBalance)); + return btcData; + } catch (error) { + return Promise.reject(error); + } + }; + + return useQuery({ + queryKey: [`wallet-data-${btcAddress}`], + queryFn: fetchBtcWalletData, + refetchOnMount: false, + }); +}; + +export default useBtcWalletData; diff --git a/src/app/hooks/queries/useCoinData.ts b/src/app/hooks/queries/useCoinData.ts new file mode 100644 index 000000000..bac4e0cdd --- /dev/null +++ b/src/app/hooks/queries/useCoinData.ts @@ -0,0 +1,86 @@ +import useWalletSelector from '@hooks/useWalletSelector'; +import { useDispatch } from 'react-redux'; +import { useQuery } from '@tanstack/react-query'; +import { CoinsResponse, FungibleToken } from '@secretkeylabs/xverse-core/types'; +import { getCoinsInfo, getFtData } from '@secretkeylabs/xverse-core/api'; +import useNetworkSelector from '@hooks/useNetwork'; +import { setCoinDataAction } from '@stores/wallet/actions/actionCreators'; + +export const useCoinsData = () => { + const dispatch = useDispatch(); + const { stxAddress, coinsList, fiatCurrency } = useWalletSelector(); + const currentNetworkInstance = useNetworkSelector(); + + const fetchCoinData = async () => { + try { + const fungibleTokenList: Array = await getFtData( + stxAddress, + currentNetworkInstance, + ); + const visibleCoins: FungibleToken[] | null = coinsList; + if (visibleCoins) { + visibleCoins.forEach((visibleCoin) => { + const coinToBeUpdated = fungibleTokenList.find( + (ft) => ft.principal === visibleCoin.principal, + ); + if (coinToBeUpdated) coinToBeUpdated.visible = visibleCoin.visible; + else if (visibleCoin.visible) { + visibleCoin.balance = '0'; + fungibleTokenList.push(visibleCoin); + } + }); + } else { + fungibleTokenList.forEach((ft) => { + ft.visible = true; + }); + } + + const contractids: string[] = []; + // getting contract ids of all fts + fungibleTokenList.forEach((ft) => { + contractids.push(ft.principal); + }); + const coinsReponse: CoinsResponse = await getCoinsInfo(contractids, fiatCurrency); + coinsReponse.forEach((coin) => { + if (!coin.name) { + coin.name = coin.contract.split('.')[1]; + } + }); + + // update attributes of fungible token list + fungibleTokenList.forEach((ft) => { + coinsReponse.forEach((coin) => { + if (ft.principal === coin.contract) { + ft.ticker = coin.ticker; + ft.decimals = coin.decimals; + ft.supported = coin.supported; + ft.image = coin.image; + ft.name = coin.name; + ft.tokenFiatRate = coin.tokenFiatRate; + coin.visible = ft.visible; + } + }); + }); + + // sorting the list - moving supported to the top + const supportedFts: FungibleToken[] = []; + const unSupportedFts: FungibleToken[] = []; + fungibleTokenList.forEach((ft) => { + if (ft.supported) supportedFts.push(ft); + else unSupportedFts.push(ft); + }); + const sortedFtList: FungibleToken[] = [...supportedFts, ...unSupportedFts]; + dispatch(setCoinDataAction(sortedFtList, coinsReponse)); + return { sortedFtList, coinsReponse}; + } catch (error: any) { + return Promise.reject(error); + } + }; + + return useQuery({ + queryKey: ['coins_data'], + queryFn: fetchCoinData, + }); +}; + +export default useCoinsData; diff --git a/src/app/hooks/queries/useCoinRates.ts b/src/app/hooks/queries/useCoinRates.ts new file mode 100644 index 000000000..a995e671e --- /dev/null +++ b/src/app/hooks/queries/useCoinRates.ts @@ -0,0 +1,32 @@ +import useWalletSelector from '@hooks/useWalletSelector'; +import { useDispatch } from 'react-redux'; +import { useQuery } from '@tanstack/react-query'; +import { setCoinRatesAction } from '@stores/wallet/actions/actionCreators'; +import { fetchBtcToCurrencyRate, fetchStxToBtcRate } from '@secretkeylabs/xverse-core/api'; + +export const useCoinRates = () => { + const dispatch = useDispatch(); + const { + fiatCurrency, + } = useWalletSelector(); + + const fetchCoinRates = async () => { + try { + const btcFiatRate = await fetchBtcToCurrencyRate({ + fiatCurrency, + }); + const stxBtcRate = await fetchStxToBtcRate(); + dispatch(setCoinRatesAction(stxBtcRate, btcFiatRate)); + return { stxBtcRate, btcFiatRate }; + } catch (e: any) { + return Promise.reject(e); + } + }; + + return useQuery({ + queryKey: ['coin_rates'], + queryFn: fetchCoinRates, + }); +}; + +export default useCoinRates; diff --git a/src/app/hooks/queries/useFeeMultipliers.ts b/src/app/hooks/queries/useFeeMultipliers.ts new file mode 100644 index 000000000..5f0652db6 --- /dev/null +++ b/src/app/hooks/queries/useFeeMultipliers.ts @@ -0,0 +1,26 @@ +import { useDispatch } from 'react-redux'; +import { useQuery } from '@tanstack/react-query'; +import { FeesMultipliers } from '@secretkeylabs/xverse-core/types'; +import { fetchAppInfo } from '@secretkeylabs/xverse-core/api'; +import { setFeeMultiplierAction } from '@stores/wallet/actions/actionCreators'; + +export const useFeeMultipliers = () => { + const dispatch = useDispatch(); + + const fetchFeeMultiplierData = async (): Promise => { + try { + const response: FeesMultipliers = await fetchAppInfo(); + dispatch(setFeeMultiplierAction(response)); + return response; + } catch (err) { + return Promise.reject(err); + } + }; + + return useQuery({ + queryKey: ['fee_multipliers'], + queryFn: fetchFeeMultiplierData, + }); +}; + +export default useFeeMultipliers; diff --git a/src/app/hooks/queries/usePendingOrdinalTx.ts b/src/app/hooks/queries/usePendingOrdinalTx.ts new file mode 100644 index 000000000..c7f1810a7 --- /dev/null +++ b/src/app/hooks/queries/usePendingOrdinalTx.ts @@ -0,0 +1,28 @@ +import { BtcAddressMempool } from '@secretkeylabs/xverse-core/types/api/blockstream/transactions'; +import { fetchPendingOrdinalsTransactions } from '@secretkeylabs/xverse-core/api/btc'; +import { useQuery } from '@tanstack/react-query'; +import useWalletSelector from '../useWalletSelector'; + +const usePendingOrdinalTxs = (ordinalUtxoHash: string | undefined) => { + const { ordinalsAddress, network } = useWalletSelector(); + + const fetchOrdinalsMempoolTxs = async (): Promise => fetchPendingOrdinalsTransactions(ordinalsAddress, network.type); + + let isPending: boolean | undefined = false; + let pendingTxHash: string | undefined; + + const response = useQuery(['ordinal-pending-transactions'], fetchOrdinalsMempoolTxs); + + if (response.data) { + response.data.forEach((tx) => { + tx.vin.forEach((v) => { if (v.txid === ordinalUtxoHash) isPending = true; pendingTxHash = tx.txid; }); + }); + } + + return { + isPending, + pendingTxHash, + }; +}; + +export default usePendingOrdinalTxs; diff --git a/src/app/hooks/useStackingData.tsx b/src/app/hooks/queries/useStackingData.ts similarity index 94% rename from src/app/hooks/useStackingData.tsx rename to src/app/hooks/queries/useStackingData.ts index ad5075b52..bcbed0385 100644 --- a/src/app/hooks/useStackingData.tsx +++ b/src/app/hooks/queries/useStackingData.ts @@ -3,8 +3,8 @@ import { useCallback } from 'react'; import { fetchDelegationState, fetchPoolStackerInfo, fetchStackingPoolInfo, getStacksInfo, StackingData, } from '@secretkeylabs/xverse-core'; -import useWalletSelector from './useWalletSelector'; -import useNetworkSelector from './useNetwork'; +import useWalletSelector from '../useWalletSelector'; +import useNetworkSelector from '../useNetwork'; const useStackingData = () => { const { stxAddress, network } = useWalletSelector(); diff --git a/src/app/hooks/useStxPendingTxData.tsx b/src/app/hooks/queries/useStxPendingTxData.ts similarity index 92% rename from src/app/hooks/useStxPendingTxData.tsx rename to src/app/hooks/queries/useStxPendingTxData.ts index a405882fd..f4cbdaf08 100644 --- a/src/app/hooks/useStxPendingTxData.tsx +++ b/src/app/hooks/queries/useStxPendingTxData.ts @@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { useSelector } from 'react-redux'; import { fetchStxPendingTxData } from '@secretkeylabs/xverse-core/api'; import { StoreState } from '@stores/index'; -import useNetworkSelector from './useNetwork'; +import useNetworkSelector from '../useNetwork'; const useStxPendingTxData = () => { const { stxAddress } = useSelector( diff --git a/src/app/hooks/queries/useStxWalletData.ts b/src/app/hooks/queries/useStxWalletData.ts new file mode 100644 index 000000000..845062077 --- /dev/null +++ b/src/app/hooks/queries/useStxWalletData.ts @@ -0,0 +1,45 @@ +import { useDispatch } from 'react-redux'; +import { useQuery } from '@tanstack/react-query'; +import { StxAddressData } from '@secretkeylabs/xverse-core/types'; +import { fetchStxAddressData } from '@secretkeylabs/xverse-core/api'; +import { PAGINATION_LIMIT } from '@utils/constants'; +import { setStxWalletDataAction } from '@stores/wallet/actions/actionCreators'; +import useWalletSelector from '../useWalletSelector'; +import useNetworkSelector from '../useNetwork'; + +export const useStxWalletData = () => { + const dispatch = useDispatch(); + const { stxAddress } = useWalletSelector(); + const currentNetworkInstance = useNetworkSelector(); + + const fetchStxWalletData = async (): Promise => { + try { + const stxData: StxAddressData = await fetchStxAddressData( + stxAddress, + currentNetworkInstance, + 0, + PAGINATION_LIMIT, + ); + dispatch( + setStxWalletDataAction( + stxData.balance, + stxData.availableBalance, + stxData.locked, + stxData.transactions, + stxData.nonce, + ), + ); + return stxData; + } catch (error) { + return Promise.reject(error); + } + }; + + return useQuery({ + queryKey: [`wallet-data-${stxAddress}`], + queryFn: fetchStxWalletData, + refetchOnMount: false, + }); +}; + +export default useStxWalletData; diff --git a/src/app/hooks/useTransactions.ts b/src/app/hooks/queries/useTransactions.ts similarity index 78% rename from src/app/hooks/useTransactions.ts rename to src/app/hooks/queries/useTransactions.ts index e35eea434..8ee6bbbd2 100644 --- a/src/app/hooks/useTransactions.ts +++ b/src/app/hooks/queries/useTransactions.ts @@ -10,11 +10,11 @@ import { import { useQuery } from '@tanstack/react-query'; import { CurrencyTypes, PAGINATION_LIMIT } from '@utils/constants'; import { getStxAddressTransactions } from '@utils/transactions/transactions'; -import useNetworkSelector from './useNetwork'; +import useNetworkSelector from '../useNetwork'; export default function useTransactions(coinType: CurrencyTypes) { const { - network, stxAddress, btcAddress, + network, stxAddress, btcAddress, ordinalsAddress, hasActivatedOrdinalsKey } = useWalletSelector(); const selectedNetwork = useNetworkSelector(); const fetchTransactions = async (): Promise< @@ -25,8 +25,13 @@ export default function useTransactions(coinType: CurrencyTypes) { return await getStxAddressTransactions(stxAddress, selectedNetwork, 0, PAGINATION_LIMIT); } if (coinType === 'BTC') { - const btcData = await fetchBtcTransactionsData(btcAddress, network.type); - return btcData.transactions; + const btcData = await fetchBtcTransactionsData( + btcAddress, + ordinalsAddress, + network.type, + hasActivatedOrdinalsKey as boolean, + ); + return btcData; } return []; } catch (err) { diff --git a/src/app/hooks/useBtcAddressRequest.ts b/src/app/hooks/useBtcAddressRequest.ts new file mode 100644 index 000000000..6193cb7f4 --- /dev/null +++ b/src/app/hooks/useBtcAddressRequest.ts @@ -0,0 +1,66 @@ +import { decodeToken } from 'jsontokens'; +import { useLocation } from 'react-router-dom'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { ExternalSatsMethods, MESSAGE_SOURCE } from '@common/types/message-types'; +import { + GetAddressOptions, + AddressPurposes, + GetAddressResponse, + Address, +} from 'sats-connect'; + +const useBtcAddressRequest = () => { + const { + btcAddress, ordinalsAddress, btcPublicKey, ordinalsPublicKey, + } = useWalletSelector(); + const { search } = useLocation(); + const params = new URLSearchParams(search); + const requestToken = params.get('addressRequest') ?? ''; + const request = decodeToken(requestToken) as any as GetAddressOptions; + const tabId = params.get('tabId') ?? '0'; + + const approveBtcAddressRequest = () => { + const addressesResponse: Address[] = request.payload.purposes.map((purpose: AddressPurposes) => { + if (purpose === AddressPurposes.ORDINALS) { + return { + address: ordinalsAddress, + publicKey: ordinalsPublicKey, + purpose: AddressPurposes.ORDINALS, + }; + } + return { + address: btcAddress, + publicKey: btcPublicKey, + purpose: AddressPurposes.PAYMENT, + }; + }); + const response: GetAddressResponse = { + addresses: addressesResponse, + }; + const addressMessage = { + source: MESSAGE_SOURCE, + method: ExternalSatsMethods.getAddressResponse, + payload: { addressRequest: requestToken, addressResponse: response }, + }; + chrome.tabs.sendMessage(+tabId, addressMessage); + }; + + const cancelAddressRequest = () => { + const addressMessage = { + source: MESSAGE_SOURCE, + method: ExternalSatsMethods.getAddressResponse, + payload: { addressRequest: requestToken, addressResponse: 'cancel' }, + }; + chrome.tabs.sendMessage(+tabId, addressMessage); + }; + + return { + payload: request.payload, + tabId, + requestToken, + approveBtcAddressRequest, + cancelAddressRequest, + }; +}; + +export default useBtcAddressRequest; diff --git a/src/app/hooks/useOnTabClosed.ts b/src/app/hooks/useOnTabClosed.ts index 0da2a2260..279d90919 100644 --- a/src/app/hooks/useOnTabClosed.ts +++ b/src/app/hooks/useOnTabClosed.ts @@ -1,5 +1,5 @@ -import { InternalMethods } from 'content-scripts/message-types'; -import { BackgroundMessages } from 'content-scripts/messages'; +import { InternalMethods } from '@common/types/message-types'; +import { BackgroundMessages } from '@common/types/messages'; import { useEffect } from 'react'; export default function useOnOriginTabClose( diff --git a/src/app/hooks/useSignPsbtTx.ts b/src/app/hooks/useSignPsbtTx.ts new file mode 100644 index 000000000..40b1842a2 --- /dev/null +++ b/src/app/hooks/useSignPsbtTx.ts @@ -0,0 +1,81 @@ +import { decodeToken } from 'jsontokens'; +import { useLocation } from 'react-router-dom'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { SignTransactionOptions } from 'sats-connect'; +import { InputToSign, signPsbt, psbtBase64ToHex } from '@secretkeylabs/xverse-core/transactions/psbt'; +import { broadcastRawBtcOrdinalTransaction } from '@secretkeylabs/xverse-core/api'; + +import { ExternalSatsMethods, MESSAGE_SOURCE } from '@common/types/message-types'; + +const useSignPsbtTx = () => { + const { + seedPhrase, accountsList, network, + } = useWalletSelector(); + const { search } = useLocation(); + const params = new URLSearchParams(search); + const requestToken = params.get('signPsbtRequest') ?? ''; + const request = decodeToken(requestToken) as any as SignTransactionOptions; + const tabId = params.get('tabId') ?? '0'; + + const confirmSignPsbt = async () => { + const 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) + txId = await broadcastRawBtcOrdinalTransaction(txHex, network.type); + } + const signingMessage = { + source: MESSAGE_SOURCE, + method: ExternalSatsMethods.signPsbtResponse, + payload: { + signPsbtRequest: requestToken, + signPsbtResponse: { + psbtBase64: signingResponse, + txId, + }, + }, + }; + chrome.tabs.sendMessage(+tabId, signingMessage); + return { + txId, + signingResponse, + }; + }; + + const cancelSignPsbt = () => { + const signingMessage = { + source: MESSAGE_SOURCE, + method: ExternalSatsMethods.signPsbtResponse, + payload: { signPsbtRequest: requestToken, signPsbtResponse: 'cancel' }, + }; + chrome.tabs.sendMessage(+tabId, signingMessage); + }; + + const getSigningAddresses = (inputsToSign: Array) => { + const signingAddresses : Array = []; + inputsToSign.forEach((inputToSign) => { + inputToSign.signingIndexes.forEach((signingIndex) => { + signingAddresses[signingIndex] = inputToSign.address; + }); + }); + return signingAddresses; + }; + + return { + payload: request.payload, + tabId, + requestToken, + getSigningAddresses, + confirmSignPsbt, + cancelSignPsbt, + }; +}; + +export default useSignPsbtTx; \ No newline at end of file diff --git a/src/app/hooks/useWalletReducer.ts b/src/app/hooks/useWalletReducer.ts index 79329d9f4..10dd8b57b 100644 --- a/src/app/hooks/useWalletReducer.ts +++ b/src/app/hooks/useWalletReducer.ts @@ -18,10 +18,12 @@ import { unlockWalletAction, } from '@stores/wallet/actions/actionCreators'; import { decryptSeedPhrase, encryptSeedPhrase } from '@utils/encryptionUtils'; -import { InternalMethods } from 'content-scripts/message-types'; -import { sendMessage } from 'content-scripts/messages'; +import { InternalMethods } from '@common/types/message-types'; +import { sendMessage } from '@common/types/messages'; import { useSelector, useDispatch } from 'react-redux'; -import useNetworkSelector from './useNetwork'; +import useNetworkSelector from '@hooks/useNetwork'; +import useBtcWalletData from '@hooks/queries/useBtcWalletData'; +import useStxWalletData from '@hooks/queries/useStxWalletData'; const useWalletReducer = () => { const { @@ -33,6 +35,8 @@ const useWalletReducer = () => { ); const selectedNetwork = useNetworkSelector(); const dispatch = useDispatch(); + const { refetch: refetchStxData } = useStxWalletData(); + const { refetch: refetchBtcData } = useBtcWalletData(); const loadActiveAccounts = async (secretKey: string, currentNetwork: SettingsNetwork, currentNetworkObject: StacksNetwork, currentAccounts: Account[]) => { const walletAccounts = await restoreWalletWithAccounts(secretKey, currentNetwork, currentNetworkObject, currentAccounts); @@ -51,7 +55,7 @@ const useWalletReducer = () => { try { const decrypted = await decryptSeedPhrase(encryptedSeed, password); await loadActiveAccounts(decrypted, network, selectedNetwork, accountsList); - sendMessage({ + await sendMessage({ method: InternalMethods.ShareInMemoryKeyToBackground, payload: { secretKey: decrypted, @@ -88,7 +92,7 @@ const useWalletReducer = () => { network: 'Mainnet', }); const encryptSeed = await encryptSeedPhrase(seed, password); - sendMessage({ + await sendMessage({ method: InternalMethods.ShareInMemoryKeyToBackground, payload: { secretKey: wallet.seedPhrase, @@ -108,7 +112,7 @@ const useWalletReducer = () => { }; dispatch(setWalletAction(wallet)); dispatch(fetchAccountAction(account, [account])); - sendMessage({ + await sendMessage({ method: InternalMethods.ShareInMemoryKeyToBackground, payload: { secretKey: wallet.seedPhrase, @@ -140,6 +144,7 @@ const useWalletReducer = () => { account.masterPubKey, account.stxPublicKey, account.btcPublicKey, + account.ordinalsPublicKey, network, ), ); @@ -155,6 +160,8 @@ const useWalletReducer = () => { }); dispatch(setWalletAction(wallet)); await loadActiveAccounts(wallet.seedPhrase, changedNetwork, networkObject, { ...wallet, id: 0 }); + await refetchStxData(); + await refetchBtcData(); }; return { diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx index 65e4016ad..2dca5b9e5 100644 --- a/src/app/routes/index.tsx +++ b/src/app/routes/index.tsx @@ -42,6 +42,9 @@ import ErrorBoundary from '@screens/error'; import OrdinalDetailScreen from '@screens/ordinalDetail'; import SendOrdinal from '@screens/sendOrdinal'; import ConfirmOrdinalTransaction from '@screens/confirmOrdinalTransaction'; +import BtcSelectAddressScreen from '@screens/btcSelectAddressScreen'; +import SignPsbtRequest from '@screens/signPsbtRequest'; +import RestoreFunds from '@screens/restoreFunds'; const router = createHashRouter([ { @@ -133,6 +136,22 @@ const router = createHashRouter([ ), }, + { + path: 'btc-select-address-request', + element: ( + + + + ), + }, + { + path: 'psbt-signing-request', + element: ( + + + + ), + }, { path: 'login', element: , @@ -161,6 +180,10 @@ const router = createHashRouter([ path: 'settings', element: , }, + { + path: 'nft-dashboard/restore-funds', + element: , + }, { path: 'fiat-currency', element: , @@ -225,7 +248,7 @@ const router = createHashRouter([ element: , }, { - path: 'nft-dashboard/ordinal-detail/:id', + path: 'nft-dashboard/ordinal-detail/:id/:txHash', element: , }, { @@ -241,7 +264,7 @@ const router = createHashRouter([ element: , }, { - path: 'nft-dashboard/ordinal-detail/:id/send-ordinal', + path: 'nft-dashboard/ordinal-detail/:id/:txHash/send-ordinal', element: ( diff --git a/src/app/screens/accountList/index.tsx b/src/app/screens/accountList/index.tsx index d5bfbb031..6e1a20405 100644 --- a/src/app/screens/accountList/index.tsx +++ b/src/app/screens/accountList/index.tsx @@ -76,6 +76,7 @@ function AccountList(): JSX.Element { account.masterPubKey, account.stxPublicKey, account.btcPublicKey, + account.ordinalsPublicKey, network, ), ); diff --git a/src/app/screens/authenticationRequest/index.tsx b/src/app/screens/authenticationRequest/index.tsx index c81431914..7e21723d4 100644 --- a/src/app/screens/authenticationRequest/index.tsx +++ b/src/app/screens/authenticationRequest/index.tsx @@ -4,7 +4,7 @@ import ConfirmScreen from '@components/confirmScreen'; import { decodeToken } from 'jsontokens'; import { useTranslation } from 'react-i18next'; import { createAuthResponse } from '@secretkeylabs/xverse-core'; -import { MESSAGE_SOURCE } from 'content-scripts/message-types'; +import { MESSAGE_SOURCE } from '@common/types/message-types'; import { useState } from 'react'; import useWalletSelector from '@hooks/useWalletSelector'; import DappPlaceholderIcon from '@assets/img/webInteractions/authPlaceholder.svg'; diff --git a/src/app/screens/btcSelectAddressScreen/accountView.tsx b/src/app/screens/btcSelectAddressScreen/accountView.tsx new file mode 100644 index 000000000..87fb05382 --- /dev/null +++ b/src/app/screens/btcSelectAddressScreen/accountView.tsx @@ -0,0 +1,109 @@ +import { Account } from '@secretkeylabs/xverse-core'; +import { getAccountGradient } from '@utils/gradient'; +import { getTruncatedAddress } from '@utils/helper'; +import { useTranslation } from 'react-i18next'; +import OrdinalsIcon from '@assets/img/nftDashboard/white_ordinals_icon.svg'; +import styled from 'styled-components'; + +interface GradientCircleProps { + firstGradient: string; + secondGradient: string; + thirdGradient: string; +} +const GradientCircle = styled.div((props) => ({ + height: 40, + width: 40, + borderRadius: 25, + marginRight: 9, + background: `linear-gradient(to bottom,${props.firstGradient}, ${props.secondGradient},${props.thirdGradient} )`, +})); + +const Container = styled.div({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', +}); + +const ColumnContainer = styled.div({ + display: 'flex', + flexDirection: 'column', +}); + +const AddressContainer = styled.div({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', +}); + +const RowContainer = styled.div({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', +}); + +const CurrentSelectedAccountText = styled.h1((props) => ({ + ...props.theme.body_bold_m, + color: props.theme.colors.white['0'], + textAlign: 'start', +})); + +const AddressText = styled.h1((props) => ({ + ...props.theme.body_m, + marginTop: props.theme.spacing(1), + color: props.theme.colors.white['400'], +})); + +const BitcoinDot = styled.div((props) => ({ + borderRadius: 20, + background: props.theme.colors.feedback.caution, + width: 10, + marginRight: 4, + marginLeft: 4, + height: 10, +})); + +const OrdinalImage = styled.img({ + width: 12, + height: 12, + marginRight: 4, +}); + +interface Props { + account: Account; + isBitcoinTx: boolean; +} +function AccountView({ account, isBitcoinTx }: Props) { + const gradient = getAccountGradient(account?.stxAddress!); + const { t } = useTranslation('translation', { keyPrefix: 'DASHBOARD_SCREEN' }); + + function getName() { + return account?.bnsName ?? `${t('ACCOUNT_NAME')} ${`${(account?.id ?? 0) + 1}`}`; + } + return ( + + + + + {getName()} + + + + {`${getTruncatedAddress(account?.ordinalsAddress)} / `} + + + + {`${getTruncatedAddress(account?.btcAddress)}`} + + + + + ); +} + +export default AccountView; diff --git a/src/app/screens/btcSelectAddressScreen/index.tsx b/src/app/screens/btcSelectAddressScreen/index.tsx new file mode 100644 index 000000000..0294d1593 --- /dev/null +++ b/src/app/screens/btcSelectAddressScreen/index.tsx @@ -0,0 +1,338 @@ +import styled from 'styled-components'; +import XverseLogo from '@assets/img/settings/logo.svg'; +import DropDownIcon from '@assets/img/transactions/dropDownIcon.svg'; +import { useTranslation } from 'react-i18next'; +import { useEffect, useState } from 'react'; +import DappPlaceholderIcon from '@assets/img/webInteractions/authPlaceholder.svg'; +import useWalletSelector from '@hooks/useWalletSelector'; +import AccountRow from '@components/accountRow'; +import { animated, useSpring } from '@react-spring/web'; +import Seperator from '@components/seperator'; +import { Account } from '@secretkeylabs/xverse-core'; +import { useDispatch } from 'react-redux'; +import { selectAccount } from '@stores/wallet/actions/actionCreators'; +import OrdinalsIcon from '@assets/img/nftDashboard/white_ordinals_icon.svg'; +import ActionButton from '@components/button'; +import useBtcAddressRequest from '@hooks/useBtcAddressRequest'; +import { AddressPurposes } from 'sats-connect'; +import { useNavigate } from 'react-router-dom'; +import AccountView from './accountView'; + +const TitleContainer = styled.div({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', + marginLeft: 30, + marginRight: 30, +}); + +const DropDownContainer = styled.div({ + display: 'flex', + flexDirection: 'row', + flex: 1, + height: '100%', + alignItems: 'center', + justifyContent: 'flex-end', +}); + +const LogoContainer = styled.div((props) => ({ + padding: props.theme.spacing(11), + marginBottom: props.theme.spacing(16), + borderBottom: `1px solid ${props.theme.colors.background.elevation3}`, +})); + +const AddressContainer = styled.div((props) => ({ + background: props.theme.colors.background.elevation2, + borderRadius: 40, + height: 24, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + padding: '3px 10px 3px 6px', + marginTop: props.theme.spacing(4), + marginRight: props.theme.spacing(2), +})); + +const AccountListContainer = styled(animated.div)((props) => ({ + paddingBottom: 20, + width: '100%', + borderRadius: 12, + height: 214, + marginTop: props.theme.spacing(9.5), + boxShadow: '0px 8px 104px rgba(0, 0, 0, 0.5)', + background: props.theme.colors.background.elevation2, + '&::-webkit-scrollbar': { + display: 'none', + }, + overflowY: 'auto', +})); + +const TopImage = styled.img({ + aspectRatio: 1, + height: 88, + borderWidth: 10, + borderColor: 'white', +}); + +const FunctionTitle = styled.h1((props) => ({ + ...props.theme.body_bold_l, + color: props.theme.colors.white['0'], + marginTop: 16, +})); + +const AccountContainer = styled.button((props) => ({ + background: props.theme.colors.background.elevation1, + border: `1px solid ${props.theme.colors.background.elevation3}`, + borderRadius: 8, + width: '100%', + padding: '12px 16px', + display: 'flex', + flexDirection: 'row', + marginTop: props.theme.spacing(4), + ':hover': { + background: props.theme.colors.background.elevation2, + }, +})); + +const AccountText = styled.h1((props) => ({ + ...props.theme.body_medium_m, + color: props.theme.colors.white['400'], + marginTop: 24, +})); + +const DappTitle = styled.h2((props) => ({ + ...props.theme.body_m, + color: props.theme.colors.white['200'], + marginTop: 12, + textAlign: 'center', +})); + +const AddressTextTitle = styled.h1((props) => ({ + ...props.theme.body_medium_l, + color: props.theme.colors.white['0'], + fontSize: 10, + textAlign: 'center', +})); + +const OuterContainer = styled(animated.div)({ + display: 'flex', + flexDirection: 'column', + marginLeft: 16, + marginRight: 16, +}); + +const ButtonsContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-end', + marginBottom: props.theme.spacing(20), + marginTop: 82, +})); + +const BitcoinDot = styled.div((props) => ({ + borderRadius: 20, + background: props.theme.colors.feedback.caution, + width: 6, + marginRight: props.theme.spacing(3), + height: 6, +})); + +const AccountListRow = styled.div((props) => ({ + paddingLeft: 16, + paddingRight: 16, + ':hover': { + background: props.theme.colors.background.elevation3, + }, +})); + +const ConfirmButton = styled.button((props) => ({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + borderRadius: props.theme.radius(1), + backgroundColor: props.theme.colors.action.classic, + color: props.theme.colors.white['0'], + width: '50%', + height: 44, + marginLeft: 6, +})); + +const TransparentButtonContainer = styled.div((props) => ({ + marginLeft: props.theme.spacing(2), + marginRight: props.theme.spacing(2), + width: '100%', +})); + +const OrdinalImage = styled.img({ + width: 12, + height: 12, + marginRight: 8, +}); + +const CancelButton = styled.button((props) => ({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + borderRadius: props.theme.radius(1), + backgroundColor: props.theme.colors.background.elevation0, + border: `1px solid ${props.theme.colors.background.elevation2}`, + color: props.theme.colors.white['0'], + width: '50%', + height: 44, + marginRight: 6, +})); + +function BtcSelectAddressScreen() { + const [loading, setLoading] = useState(false); + const [showAccountList, setShowAccountList] = useState(false); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { t } = useTranslation('translation', { keyPrefix: 'SELECT_BTC_ADDRESS_SCREEN' }); + const { + selectedAccount, + accountsList, + network, + } = useWalletSelector(); + const { payload, approveBtcAddressRequest, cancelAddressRequest } = useBtcAddressRequest(); + const springProps = useSpring({ + transform: showAccountList ? 'translateY(0%)' : 'translateY(100%)', + opacity: showAccountList ? 1 : 0, + config: { + tension: 160, + friction: 25, + }, + }); + const styles = useSpring({ + from: { + opacity: 0, + y: 24, + }, + to: { + y: 0, + opacity: 1, + }, + }); + + const confirmCallback = async () => { + setLoading(true); + approveBtcAddressRequest(); + window.close(); + }; + + const cancelCallback = () => { + cancelAddressRequest(); + window.close(); + }; + + const onChangeAccount = () => { + setShowAccountList(true); + }; + + const isAccountSelected = (account: Account) => account.id === selectedAccount?.id; + + const handleAccountSelect = (account: Account) => { + dispatch( + selectAccount( + account, + account.stxAddress, + account.btcAddress, + account.ordinalsAddress, + account.masterPubKey, + account.stxPublicKey, + account.btcPublicKey, + account.ordinalsPublicKey, + network, + ), + ); + setShowAccountList(false); + }; + + const switchAccountBasedOnRequest = () => { + if (payload.network.type !== network.type) { + navigate('/tx-status', { + state: { + txid: '', + currency: 'STX', + error: t('NETWORK_MISMATCH'), + browserTx: true, + }, + }); + } + }; + + useEffect(() => { + switchAccountBasedOnRequest(); + }, []); + + return ( + <> + + xverse logo + + + + + {t('TITLE')} +
+ {payload.purposes.map((purpose) => (purpose === AddressPurposes.PAYMENT ? ( + + + {t('BITCOIN_ADDRESS')} + + ) : ( + + + {t('ORDINAL_ADDRESS')} + + )))} +
+ {payload.message} +
+ {showAccountList ? ( + + {accountsList.map((account) => ( + + + + + ))} + + ) : ( + <> + {t('ACCOUNT')} + + + + Drop Down + + + + + + + + + + )} +
+ + ); +} + +export default BtcSelectAddressScreen; diff --git a/src/app/screens/coinDashboard/coinHeader.tsx b/src/app/screens/coinDashboard/coinHeader.tsx index 97075ea16..cdcec41fc 100644 --- a/src/app/screens/coinDashboard/coinHeader.tsx +++ b/src/app/screens/coinDashboard/coinHeader.tsx @@ -192,7 +192,6 @@ export default function CoinHeader(props: CoinBalanceProps) { btcFiatRate, stxLockedBalance, stxAvailableBalance, - loadingWalletData, } = useWalletSelector(); const navigate = useNavigate(); const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' }); @@ -294,7 +293,7 @@ export default function CoinHeader(props: CoinBalanceProps) { ); const renderStackingBalances = () => { - if (!loadingWalletData && !new BigNumber(stxLockedBalance).eq(0, 10) && coin === 'STX') { + if (stxLockedBalance && !new BigNumber(stxLockedBalance).eq(0, 10) && coin === 'STX') { return ( <> diff --git a/src/app/screens/coinDashboard/transactionsHistoryList.tsx b/src/app/screens/coinDashboard/transactionsHistoryList.tsx index c69f29948..7f7108a75 100644 --- a/src/app/screens/coinDashboard/transactionsHistoryList.tsx +++ b/src/app/screens/coinDashboard/transactionsHistoryList.tsx @@ -1,7 +1,7 @@ import styled from 'styled-components'; import { BtcTransactionData } from '@secretkeylabs/xverse-core/types'; import { CurrencyTypes } from '@utils/constants'; -import useTransactions from '@hooks/useTransactions'; +import useTransactions from '@hooks/queries/useTransactions'; import { MoonLoader } from 'react-spinners'; import { useTranslation } from 'react-i18next'; import { formatDate } from '@utils/date'; @@ -95,6 +95,11 @@ const groupBtcTxsByDate = ( } } else { all[txDate].push(transaction); + all[txDate].sort((txA, txB) => { + if (txB.blockHeight > txA.blockHeight) { + return 1; + } return -1; + }); } return all; }, @@ -161,7 +166,6 @@ export default function TransactionsHistoryList(props: TransactionsHistoryListPr return groupedTxsByDateMap(data); } }, [data, isLoading, isFetching]); - return ( {t('TRANSACTION_HISTORY_TITLE')} diff --git a/src/app/screens/confirmFtTransaction/index.tsx b/src/app/screens/confirmFtTransaction/index.tsx index 57275a0ea..e7c39ae47 100644 --- a/src/app/screens/confirmFtTransaction/index.tsx +++ b/src/app/screens/confirmFtTransaction/index.tsx @@ -1,21 +1,19 @@ import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; - import { useMutation } from '@tanstack/react-query'; import { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; import { useLocation, useNavigate } from 'react-router-dom'; import { StacksTransaction } from '@secretkeylabs/xverse-core/types'; import { broadcastSignedTransaction } from '@secretkeylabs/xverse-core/transactions'; -import { StoreState } from '@stores/index'; import BottomBar from '@components/tabBar'; -import { fetchStxWalletDataRequestAction } from '@stores/wallet/actions/actionCreators'; import RecipientAddressView from '@components/recipinetAddressView'; import TransferAmountView from '@components/transferAmountView'; import ConfirmStxTransationComponent from '@components/confirmStxTransactionComponent'; import TopRow from '@components/topRow'; import BigNumber from 'bignumber.js'; import useNetworkSelector from '@hooks/useNetwork'; +import useStxWalletData from '@hooks/queries/useStxWalletData'; +import useWalletSelector from '@hooks/useWalletSelector'; const InfoContainer = styled.div((props) => ({ display: 'flex', @@ -40,18 +38,18 @@ const ValueText = styled.h1((props) => ({ function ConfirmFtTransaction() { const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const navigate = useNavigate(); - const dispatch = useDispatch(); const selectedNetwork = useNetworkSelector(); const location = useLocation(); const { unsignedTx, amount, fungibleToken, memo, recepientAddress, } = location.state; + const { + refetch, + } = useStxWalletData(); const { - stxBtcRate, network, stxAddress, fiatCurrency, - } = useSelector( - (state: StoreState) => state.walletState, - ); + network, + } = useWalletSelector(); const { isLoading, @@ -73,7 +71,7 @@ function ConfirmFtTransaction() { }, }); setTimeout(() => { - dispatch(fetchStxWalletDataRequestAction(stxAddress, selectedNetwork, fiatCurrency, stxBtcRate)); + refetch(); }, 1000); } }, [stxTxBroadcastData]); diff --git a/src/app/screens/confirmNftTransaction/index.tsx b/src/app/screens/confirmNftTransaction/index.tsx index 43e859540..601ab9c15 100644 --- a/src/app/screens/confirmNftTransaction/index.tsx +++ b/src/app/screens/confirmNftTransaction/index.tsx @@ -2,15 +2,12 @@ import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import { useMutation } from '@tanstack/react-query'; import { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { StacksTransaction } from '@secretkeylabs/xverse-core/types'; import { broadcastSignedTransaction } from '@secretkeylabs/xverse-core/transactions'; import ArrowLeft from '@assets/img/dashboard/arrow_left.svg'; import Seperator from '@components/seperator'; -import { StoreState } from '@stores/index'; import BottomBar from '@components/tabBar'; -import { fetchStxWalletDataRequestAction } from '@stores/wallet/actions/actionCreators'; import RecipientAddressView from '@components/recipinetAddressView'; import ConfirmStxTransationComponent from '@components/confirmStxTransactionComponent'; import useNftDataSelector from '@hooks/useNftDataSelector'; @@ -18,6 +15,8 @@ import NftImage from '@screens/nftDashboard/nftImage'; import AccountHeaderComponent from '@components/accountHeader'; import TopRow from '@components/topRow'; import useNetworkSelector from '@hooks/useNetwork'; +import useWalletSelector from '@hooks/useWalletSelector'; +import useStxWalletData from '@hooks/queries/useStxWalletData'; const ScrollContainer = styled.div` display: flex; @@ -119,7 +118,6 @@ function ConfirmNftTransaction() { const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const isGalleryOpen: boolean = document.documentElement.clientWidth > 360; const navigate = useNavigate(); - const dispatch = useDispatch(); const location = useLocation(); const { id } = useParams(); const { nftData } = useNftDataSelector(); @@ -127,11 +125,13 @@ function ConfirmNftTransaction() { const nft = nftData.find((nftItem) => nftItem?.asset_id === nftIdDetails[1]); const { unsignedTx, recipientAddress } = location.state; const { - stxBtcRate, network, stxAddress, fiatCurrency, - } = useSelector( - (state: StoreState) => state.walletState, - ); + network, + } = useWalletSelector(); + const { + refetch, + } = useStxWalletData(); const selectedNetwork = useNetworkSelector(); + const { isLoading, error: txError, @@ -153,7 +153,7 @@ function ConfirmNftTransaction() { }, }); setTimeout(() => { - dispatch(fetchStxWalletDataRequestAction(stxAddress, selectedNetwork, fiatCurrency, stxBtcRate)); + refetch(); }, 1000); } }, [stxTxBroadcastData]); diff --git a/src/app/screens/confirmOrdinalTransaction/index.tsx b/src/app/screens/confirmOrdinalTransaction/index.tsx index a21ec379e..626fdcc3d 100644 --- a/src/app/screens/confirmOrdinalTransaction/index.tsx +++ b/src/app/screens/confirmOrdinalTransaction/index.tsx @@ -2,20 +2,17 @@ import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import { useMutation } from '@tanstack/react-query'; import { useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; import ArrowLeft from '@assets/img/dashboard/arrow_left.svg'; -import Seperator from '@components/seperator'; -import { StoreState } from '@stores/index'; import BottomBar from '@components/tabBar'; -import { fetchBtcWalletDataRequestAction } from '@stores/wallet/actions/actionCreators'; -import RecipientAddressView from '@components/recipinetAddressView'; import useNftDataSelector from '@hooks/useNftDataSelector'; import AccountHeaderComponent from '@components/accountHeader'; -import TopRow from '@components/topRow'; -import ConfirmBtcTransactionComponent from '@screens/confrimBtcTransaction/confirmBtcTransactionComponent'; +import ConfirmBtcTransactionComponent from '@components/confirmBtcTransactionComponent'; import { broadcastRawBtcOrdinalTransaction } from '@secretkeylabs/xverse-core'; import OrdinalImage from '@screens/ordinals/ordinalImage'; +import BigNumber from 'bignumber.js'; +import useBtcWalletData from '@hooks/queries/useBtcWalletData'; +import useWalletSelector from '@hooks/useWalletSelector'; const ScrollContainer = styled.div` display: flex; @@ -30,18 +27,6 @@ const ScrollContainer = styled.div` margin: auto; `; -const InfoContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - marginTop: props.theme.spacing(12), -})); - -const TitleText = styled.h1((props) => ({ - ...props.theme.headline_category_s, - color: props.theme.colors.white['400'], - textTransform: 'uppercase', -})); - const ButtonContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'row', @@ -74,19 +59,6 @@ const ButtonImage = styled.img((props) => ({ transform: 'all', })); -const IndicationText = styled.h1((props) => ({ - ...props.theme.headline_category_s, - color: props.theme.colors.white['400'], - textTransform: 'uppercase', - fontSize: 14, -})); - -const ValueText = styled.h1((props) => ({ - ...props.theme.body_m, - marginTop: props.theme.spacing(2), - wordBreak: 'break-all', -})); - const BottomBarContainer = styled.h1((props) => ({ marginTop: props.theme.spacing(3), })); @@ -99,8 +71,8 @@ const Container = styled.div({ }); const NFtContainer = styled.div((props) => ({ - maxWidth: 120, - maxHeight: 120, + maxWidth: 150, + maxHeight: 150, width: '60%', display: 'flex', aspectRatio: 1, @@ -111,26 +83,15 @@ const NFtContainer = styled.div((props) => ({ marginBottom: props.theme.spacing(6), })); -const OrdinalInscriptionNumber = styled.h1((props) => ({ - ...props.theme.headline_s, - color: props.theme.colors.white['0'], - textAlign: 'center', -})); - function ConfirmOrdinalTransaction() { const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const isGalleryOpen: boolean = document.documentElement.clientWidth > 360; const navigate = useNavigate(); - const dispatch = useDispatch(); - const { - network, btcAddress, stxBtcRate, btcFiatRate, - } = useSelector( - (state: StoreState) => state.walletState, - ); + const { network } = useWalletSelector(); const [recipientAddress, setRecipientAddress] = useState(''); const location = useLocation(); const { - fee, amount, signedTxHex, ordinalUtxo, + fee, signedTxHex, ordinalUtxo, } = location.state; const { isLoading, @@ -144,6 +105,7 @@ function ConfirmOrdinalTransaction() { const { ordinalsData } = useNftDataSelector(); const ordinalId = id!.split('::'); const ordinal = ordinalsData.find((inscription) => inscription?.metadata?.id === ordinalId[0]); + const { refetch } = useBtcWalletData(); useEffect(() => { setRecipientAddress(location.state.recipientAddress); @@ -160,9 +122,7 @@ function ConfirmOrdinalTransaction() { }, }); setTimeout(() => { - dispatch( - fetchBtcWalletDataRequestAction(btcAddress, network.type, stxBtcRate, btcFiatRate), - ); + refetch(); }, 1000); } }, [btcTxBroadcastData]); @@ -180,13 +140,6 @@ function ConfirmOrdinalTransaction() { } }, [txError]); - const networkInfoSection = ( - - {t('NETWORK')} - {network.type} - - ); - const handleOnConfirmClick = (txHex: string) => { mutate({ signedTx: txHex }); }; @@ -213,24 +166,20 @@ function ConfirmOrdinalTransaction() { - + - {ordinal?.inscriptionNumber} - - {networkInfoSection} - {!isGalleryOpen diff --git a/src/app/screens/confirmStxTransaction/index.tsx b/src/app/screens/confirmStxTransaction/index.tsx index 9efd4264c..b40002fc0 100644 --- a/src/app/screens/confirmStxTransaction/index.tsx +++ b/src/app/screens/confirmStxTransaction/index.tsx @@ -3,23 +3,22 @@ import styled from 'styled-components'; import { useMutation } from '@tanstack/react-query'; import BigNumber from 'bignumber.js'; import { useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; import { useLocation, useNavigate } from 'react-router-dom'; import { getStxFiatEquivalent, microstacksToStx } from '@secretkeylabs/xverse-core/currency'; import { StacksTransaction, TokenTransferPayload } from '@secretkeylabs/xverse-core/types'; import { addressToString, broadcastSignedTransaction } from '@secretkeylabs/xverse-core/transactions'; import Seperator from '@components/seperator'; -import { StoreState } from '@stores/index'; import BottomBar from '@components/tabBar'; -import { fetchStxWalletDataRequestAction } from '@stores/wallet/actions/actionCreators'; import RecipientAddressView from '@components/recipinetAddressView'; import TransferAmountView from '@components/transferAmountView'; import TopRow from '@components/topRow'; import AccountHeaderComponent from '@components/accountHeader'; import finalizeTxSignature from '@components/transactionsRequests/utils'; -import useOnOriginTabClose from '@hooks/useOnTabClosed'; import InfoContainer from '@components/infoContainer'; +import useOnOriginTabClose from '@hooks/useOnTabClosed'; import useNetworkSelector from '@hooks/useNetwork'; +import useStxWalletData from '@hooks/queries/useStxWalletData'; +import useWalletSelector from '@hooks/useWalletSelector'; import ConfirmStxTransationComponent from '../../components/confirmStxTransactionComponent'; const Container = styled.div((props) => ({ @@ -57,7 +56,6 @@ function ConfirmStxTransaction() { const [txRaw, setTxRaw] = useState(''); const [memo, setMemo] = useState(''); const navigate = useNavigate(); - const dispatch = useDispatch(); const location = useLocation(); const selectedNetwork = useNetworkSelector(); const { @@ -68,10 +66,11 @@ function ConfirmStxTransaction() { window.scrollTo({ top: 0, behavior: 'smooth' }); }); const { - stxBtcRate, btcFiatRate, network, stxAddress, fiatCurrency, - } = useSelector( - (state: StoreState) => state.walletState, - ); + stxBtcRate, btcFiatRate, network, + } = useWalletSelector(); + const { + refetch, + } = useStxWalletData(); const { isLoading, error: txError, @@ -96,7 +95,7 @@ function ConfirmStxTransaction() { }, }); setTimeout(() => { - dispatch(fetchStxWalletDataRequestAction(stxAddress, selectedNetwork, fiatCurrency, stxBtcRate)); + refetch(); }, 1000); } }, [stxTxBroadcastData]); diff --git a/src/app/screens/confrimBtcTransaction/index.tsx b/src/app/screens/confrimBtcTransaction/index.tsx index d57e94d15..bb2e47b91 100644 --- a/src/app/screens/confrimBtcTransaction/index.tsx +++ b/src/app/screens/confrimBtcTransaction/index.tsx @@ -1,49 +1,28 @@ -import { useTranslation } from 'react-i18next'; import { useNavigate, useLocation } from 'react-router-dom'; -import { useDispatch, useSelector } from 'react-redux'; import { useEffect, useState } from 'react'; -import styled from 'styled-components'; import { useMutation } from '@tanstack/react-query'; -import { broadcastRawBtcTransaction } from '@secretkeylabs/xverse-core/api'; +import { broadcastRawBtcOrdinalTransaction, broadcastRawBtcTransaction } from '@secretkeylabs/xverse-core/api'; import { BtcTransactionBroadcastResponse } from '@secretkeylabs/xverse-core/types'; -import { fetchBtcWalletDataRequestAction } from '@stores/wallet/actions/actionCreators'; -import Seperator from '@components/seperator'; -import { StoreState } from '@stores/index'; import BottomBar from '@components/tabBar'; -import RecipientAddressView from '@components/recipinetAddressView'; -import ConfirmBtcTransactionComponent from '@screens/confrimBtcTransaction/confirmBtcTransactionComponent'; - -const InfoContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - marginTop: props.theme.spacing(12), - marginBottom: props.theme.spacing(4), -})); - -const TitleText = styled.h1((props) => ({ - ...props.theme.headline_category_s, - color: props.theme.colors.white['400'], - textTransform: 'uppercase', -})); +import useBtcWalletData from '@hooks/queries/useBtcWalletData'; +import useWalletSelector from '@hooks/useWalletSelector'; +import ConfirmBtcTransactionComponent from '@components/confirmBtcTransactionComponent'; +import styled from 'styled-components'; +import { saveTimeForNonOrdinalTransferTransaction } from '@utils/localStorage'; -const ValueText = styled.h1((props) => ({ - ...props.theme.body_m, - marginTop: props.theme.spacing(2), - wordBreak: 'break-all', +const BottomBarContainer = styled.h1((props) => ({ + marginTop: props.theme.spacing(5), })); function ConfirmBtcTransaction() { - const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const navigate = useNavigate(); - const dispatch = useDispatch(); - const { - network, btcAddress, stxBtcRate, btcFiatRate, - } = useSelector( - (state: StoreState) => state.walletState, - ); + const { network, ordinalsAddress } = useWalletSelector(); const [recipientAddress, setRecipientAddress] = useState(''); const location = useLocation(); - const { fee, amount, signedTxHex } = location.state; + const { refetch } = useBtcWalletData(); + const { + fee, amount, signedTxHex, recipient, isRestoreFundFlow, + } = location.state; const { isLoading, error: txError, @@ -53,6 +32,30 @@ function ConfirmBtcTransaction() { async ({ signedTx }) => broadcastRawBtcTransaction(signedTx, network.type), ); + const { + error: errorBtcOrdinalTransaction, + data: btcOrdinalTxBroadcastData, + mutate: broadcastOrdinalTransaction, + } = useMutation( + async ({ signedTx }) => broadcastRawBtcOrdinalTransaction( + signedTx, + network.type, + ), + ); + + useEffect(() => { + if (errorBtcOrdinalTransaction) { + navigate('/tx-status', { + state: { + txid: '', + currency: 'BTC', + isNft: true, + error: errorBtcOrdinalTransaction.toString(), + }, + }); + } + }, [errorBtcOrdinalTransaction]); + useEffect(() => { setRecipientAddress(location.state.recipientAddress); }, [location]); @@ -67,11 +70,29 @@ function ConfirmBtcTransaction() { }, }); setTimeout(() => { - dispatch(fetchBtcWalletDataRequestAction(btcAddress, network.type, stxBtcRate, btcFiatRate)); + refetch(); }, 1000); } }, [btcTxBroadcastData]); + useEffect(() => { + if (btcOrdinalTxBroadcastData) { + saveTimeForNonOrdinalTransferTransaction(ordinalsAddress).then(() => { + navigate('/tx-status', { + state: { + txid: btcOrdinalTxBroadcastData, + currency: 'BTC', + isNft: true, + error: '', + }, + }); + setTimeout(() => { + refetch(); + }, 1000); + }); + } + }, [btcOrdinalTxBroadcastData]); + useEffect(() => { if (txError) { navigate('/tx-status', { @@ -85,37 +106,40 @@ function ConfirmBtcTransaction() { }, [txError]); const handleOnConfirmClick = (txHex: string) => { - mutate({ signedTx: txHex }); + if (isRestoreFundFlow) { + broadcastOrdinalTransaction({ signedTx: txHex }); + } else { + mutate({ signedTx: txHex }); + } }; const goBackToScreen = () => { - navigate('/send-btc', { - state: { - amount, - recipientAddress, - }, - }); + if (isRestoreFundFlow) { + navigate(-1); + } else { + navigate('/send-btc', { + state: { + amount, + recipientAddress, + }, + }); + } }; return ( <> - - - {t('NETWORK')} - {network.type} - - - - + /> + + + + ); } diff --git a/src/app/screens/home/balanceCard/index.tsx b/src/app/screens/home/balanceCard/index.tsx index a77de07f7..739640a27 100644 --- a/src/app/screens/home/balanceCard/index.tsx +++ b/src/app/screens/home/balanceCard/index.tsx @@ -49,7 +49,11 @@ const CurrencyCard = styled.div((props) => ({ marginLeft: props.theme.spacing(4), })); -function BalanceCard() { +interface BalanceCardProps { + isLoading: boolean, +} + +function BalanceCard(props: BalanceCardProps) { const { t } = useTranslation('translation', { keyPrefix: 'DASHBOARD_SCREEN' }); const { fiatCurrency, @@ -57,9 +61,8 @@ function BalanceCard() { stxBtcRate, stxBalance, btcBalance, - loadingWalletData, - loadingBtcData, } = useSelector((state: StoreState) => state.walletState); + const { isLoading } = props; function calculateTotalBalance() { const stxFiatEquiv = microstacksToStx(new BigNumber(stxBalance)) @@ -80,7 +83,7 @@ function BalanceCard() { {fiatCurrency}
- {loadingWalletData && loadingBtcData ? ( + {isLoading ? ( diff --git a/src/app/screens/home/coinSelectModal/index.tsx b/src/app/screens/home/coinSelectModal/index.tsx index f692cc6a8..0a5d8bd93 100644 --- a/src/app/screens/home/coinSelectModal/index.tsx +++ b/src/app/screens/home/coinSelectModal/index.tsx @@ -5,8 +5,6 @@ import IconBitcoin from '@assets/img/dashboard/bitcoin_icon.svg'; import IconStacks from '@assets/img/dashboard/stack_icon.svg'; import { useTranslation } from 'react-i18next'; import styled, { useTheme } from 'styled-components'; -import { useSelector } from 'react-redux'; -import { StoreState } from '@stores/index'; const Container = styled.div((props) => ({ marginTop: props.theme.spacing(6), @@ -21,6 +19,7 @@ interface Props { onSelectStacks: () => void; onSelectCoin: (coin: FungibleToken) => void; onClose: () => void; + loadingWalletData: boolean; } function CoinSelectModal({ @@ -31,12 +30,9 @@ function CoinSelectModal({ onSelectStacks, onSelectCoin, onClose, + loadingWalletData, }: Props) { const { t } = useTranslation('translation', { keyPrefix: 'DASHBOARD_SCREEN' }); - const { - loadingWalletData, - loadingBtcData, - } = useSelector((state: StoreState) => state.walletState); const theme = useTheme(); const handleOnBitcoinPress = () => { @@ -56,7 +52,7 @@ function CoinSelectModal({ title={t('BITCOIN')} currency="BTC" icon={IconBitcoin} - loading={loadingBtcData} + loading={loadingWalletData} underlayColor={theme.colors.background.elevation2} margin={14} enlargeTicker diff --git a/src/app/screens/home/index.tsx b/src/app/screens/home/index.tsx index 63f98c5fd..bde543114 100644 --- a/src/app/screens/home/index.tsx +++ b/src/app/screens/home/index.tsx @@ -1,11 +1,15 @@ /* eslint-disable no-await-in-loop */ import { useTranslation } from 'react-i18next'; -import { useDispatch } from 'react-redux'; -import { useCallback, useEffect, useState } from 'react'; +import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; -import { fetchAppInfo } from '@secretkeylabs/xverse-core/api'; -import { FeesMultipliers, FungibleToken } from '@secretkeylabs/xverse-core/types'; +import AddToken from '@assets/img/dashboard/add_token.svg'; +import StacksToken from '@assets/img/dashboard/stacks_token.svg'; +import MiaToken from '@assets/img/dashboard/mia_token.svg'; +import NycToken from '@assets/img/dashboard/nyc_token.svg'; +import CoinToken from '@assets/img/dashboard/coin_token.svg'; +import BitcoinToken from '@assets/img/dashboard/bitcoin_token.svg'; +import { FungibleToken } from '@secretkeylabs/xverse-core/types'; import ListDashes from '@assets/img/dashboard/list_dashes.svg'; import CreditCard from '@assets/img/dashboard/credit_card.svg'; import ArrowDownLeft from '@assets/img/dashboard/arrow_down_left.svg'; @@ -16,19 +20,17 @@ import TokenTile from '@components/tokenTile'; import CoinSelectModal from '@screens/home/coinSelectModal'; import Theme from 'theme'; import ActionButton from '@components/button'; -import { - fetchAccountAction, - fetchBtcWalletDataRequestAction, - fetchCoinDataRequestAction, - FetchFeeMultiplierAction, - fetchRatesAction, - fetchStxWalletDataRequestAction, -} from '@stores/wallet/actions/actionCreators'; import BottomBar from '@components/tabBar'; import AccountHeaderComponent from '@components/accountHeader'; import { CurrencyTypes } from '@utils/constants'; import useWalletSelector from '@hooks/useWalletSelector'; -import useNetworkSelector from '@hooks/useNetwork'; +import useStxWalletData from '@hooks/queries/useStxWalletData'; +import useBtcWalletData from '@hooks/queries/useBtcWalletData'; +import useFeeMultipliers from '@hooks/queries/useFeeMultipliers'; +import useCoinRates from '@hooks/queries/useCoinRates'; +import useCoinsData from '@hooks/queries/useCoinData'; +import BottomModal from '@components/bottomModal'; +import ReceiveCardComponent from '@components/receiveCardComponent'; import BalanceCard from './balanceCard'; const Container = styled.div` @@ -51,6 +53,15 @@ const ColumnContainer = styled.div((props) => ({ marginTop: props.theme.spacing(12), })); +const ReceiveContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + marginTop: props.theme.spacing(12), + marginBottom: props.theme.spacing(16), + paddingLeft: props.theme.spacing(8), + paddingRight: props.theme.spacing(8), +})); + const CoinContainer = styled.div({ display: 'flex', flexDirection: 'column', @@ -101,47 +112,30 @@ const TokenListButtonContainer = styled.div((props) => ({ marginTop: props.theme.spacing(12), })); +const Icon = styled.img({ + width: 24, + height: 24, +}); + +const IconRow = styled.div({ + display: 'flex', + width: 140, + flexDirection: 'row', + justifyContent: 'space-between', +}); + function Home() { const { t } = useTranslation('translation', { keyPrefix: 'DASHBOARD_SCREEN' }); const navigate = useNavigate(); - const dispatch = useDispatch(); const [openReceiveModal, setOpenReceiveModal] = useState(false); const [openSendModal, setOpenSendModal] = useState(false); const [openBuyModal, setOpenBuyModal] = useState(false); - const { - stxAddress, - btcAddress, - masterPubKey, - fiatCurrency, - btcFiatRate, - stxBtcRate, - network, - coinsList, - loadingWalletData, - loadingBtcData, - selectedAccount, - accountsList, - } = useWalletSelector(); - const selectedNetwork = useNetworkSelector(); - const fetchFeeMultiplierData = async () => { - const response: FeesMultipliers = await fetchAppInfo(); - dispatch(FetchFeeMultiplierAction(response)); - }; - - const loadInitialData = useCallback(() => { - if (stxAddress && btcAddress) { - fetchFeeMultiplierData(); - dispatch(fetchAccountAction(selectedAccount!, accountsList)); - dispatch(fetchRatesAction(fiatCurrency)); - dispatch(fetchStxWalletDataRequestAction(stxAddress, selectedNetwork, fiatCurrency, stxBtcRate)); - dispatch(fetchBtcWalletDataRequestAction(btcAddress, network.type, stxBtcRate, btcFiatRate)); - dispatch(fetchCoinDataRequestAction(stxAddress, selectedNetwork, fiatCurrency, coinsList)); - } - }, [stxAddress]); - - useEffect(() => { - loadInitialData(); - }, [masterPubKey, stxAddress, btcAddress, loadInitialData]); + const { coinsList, stxAddress, btcAddress } = useWalletSelector(); + const { isLoading: loadingStxWalletData, isRefetching: refetchingStxWalletData } = useStxWalletData(); + const { isLoading: loadingBtcWalletData, isRefetching: refetchingBtcWalletData } = useBtcWalletData(); + const { isLoading: loadingCoinData, isRefetching: refetchingCoinData } = useCoinsData(); + useFeeMultipliers(); + useCoinRates(); const onReceiveModalOpen = () => { setOpenReceiveModal(true); @@ -211,11 +205,38 @@ function Home() { navigate(`/coinDashboard/${token.coin}?ft=${token.ft}`); }; + const receiveContent = ( + + + + + + + + + + + + + + + + + ); + return ( <> - + @@ -242,7 +263,7 @@ function Home() { title={t('BITCOIN')} currency="BTC" icon={IconBitcoin} - loading={loadingBtcData} + loading={loadingBtcWalletData || refetchingBtcWalletData} underlayColor={Theme.colors.background.elevation1} onPress={handleTokenPressed} /> @@ -250,33 +271,29 @@ function Home() { title={t('STACKS')} currency="STX" icon={IconStacks} - loading={loadingWalletData} + loading={loadingStxWalletData || refetchingStxWalletData} underlayColor={Theme.colors.background.elevation1} onPress={handleTokenPressed} /> - {coinsList?.filter((ft) => ft.visible).map((coin) => ( - - ))} + {coinsList + ?.filter((ft) => ft.visible) + .map((coin) => ( + + ))} - + + {receiveContent} + diff --git a/src/app/screens/nftDashboard/index.tsx b/src/app/screens/nftDashboard/index.tsx index 3285673b8..2197b9cd7 100644 --- a/src/app/screens/nftDashboard/index.tsx +++ b/src/app/screens/nftDashboard/index.tsx @@ -3,15 +3,16 @@ import { MoonLoader } from 'react-spinners'; import useWalletSelector from '@hooks/useWalletSelector'; import BottomTabBar from '@components/tabBar'; import { useTranslation } from 'react-i18next'; +import InfoIcon from '@assets/img/info.svg'; import SquaresFour from '@assets/img/nftDashboard/squares_four.svg'; import ArrowDownLeft from '@assets/img/dashboard/arrow_down_left.svg'; import ShareNetwork from '@assets/img/nftDashboard/share_network.svg'; import ActionButton from '@components/button'; -import { getNfts, getOrdinalsByAddress } from '@secretkeylabs/xverse-core/api'; -import { useEffect, useState } from 'react'; +import { getNfts, getNonOrdinalUtxo, getOrdinalsByAddress } from '@secretkeylabs/xverse-core/api'; +import { useCallback, useEffect, useState } from 'react'; import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; import BarLoader from '@components/barLoader'; -import { GAMMA_URL, LoaderSize } from '@utils/constants'; +import { GAMMA_URL, LoaderSize, REFETCH_UNSPENT_UTXO_TIME } from '@utils/constants'; import ShareDialog from '@components/shareNft'; import AccountHeaderComponent from '@components/accountHeader'; import useNetworkSelector from '@hooks/useNetwork'; @@ -20,6 +21,9 @@ import { ChangeActivateOrdinalsAction } from '@stores/wallet/actions/actionCreat import { useDispatch } from 'react-redux'; import { BtcOrdinal, NftsListData } from '@secretkeylabs/xverse-core/types'; import AlertMessage from '@components/alertMessage'; +import { useNavigate } from 'react-router-dom'; +import { getTimeForNonOrdinalTransferTransaction } from '@utils/localStorage'; +import { UnspentOutput } from '@secretkeylabs/xverse-core/transactions/btc'; import Nft from './nft'; import ReceiveNftModal from './receiveNft'; @@ -43,6 +47,7 @@ const GridContainer = styled.div((props) => ({ display: 'grid', columnGap: props.theme.spacing(8), rowGap: props.theme.spacing(6), + marginTop: props.theme.spacing(14), gridTemplateColumns: props.isGalleryOpen ? 'repeat(auto-fill,minmax(300px,1fr))' : 'repeat(auto-fill,minmax(150px,1fr))', gridTemplateRows: props.isGalleryOpen ? 'repeat(minmax(300px,1fr))' : 'minmax(150px,220px)', })); @@ -82,7 +87,6 @@ const ButtonContainer = styled.div((props) => ({ alignItems: 'center', justifyContent: 'space-between', maxWidth: 400, - marginBottom: props.theme.spacing(20), })); const ShareButtonContainer = styled.div((props) => ({ @@ -127,6 +131,45 @@ const BottomBarContainer = styled.div({ marginTop: '5%', }); +const WithdrawAlertContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'row', + borderRadius: 12, + alignItems: 'flex-start', + backgroundColor: 'transparent', + padding: props.theme.spacing(8), + border: '1px solid rgba(255, 255, 255, 0.2)', + marginTop: 24, +})); + +const TextContainer = styled.div((props) => ({ + marginLeft: props.theme.spacing(5), + display: 'flex', + flexDirection: 'column', +})); + +const RedirectButton = styled.button((props) => ({ + ...props.theme.body_medium_m, + background: 'transparent', + display: 'flex', + alignItems: 'flex-start', + justifyContent: 'flex-start', + marginTop: 4, + color: props.theme.colors.white['0'], +})); + +const SubText = styled.h1((props) => ({ + ...props.theme.body_xs, + marginTop: props.theme.spacing(2), + color: props.theme.colors.white['200'], +})); + +const Text = styled.h1((props) => ({ + ...props.theme.body_m, + color: props.theme.colors.white['200'], + lineHeight: 1.4, +})); + const CollectiblesHeadingText = styled.h1((props) => ({ ...props.theme.headline_category_s, color: props.theme.colors.white['200'], @@ -145,7 +188,6 @@ const GalleryCollectiblesHeadingText = styled.h1((props) => ({ const CollectiblesValueText = styled.h1((props) => ({ ...props.theme.headline_l, - marginTop: props.theme.spacing(4), })); const LoadMoreButtonContainer = styled.div((props) => ({ @@ -176,31 +218,54 @@ const LoadMoreButton = styled.button((props) => ({ const NoCollectiblesText = styled.h1((props) => ({ ...props.theme.body_bold_m, color: props.theme.colors.white['200'], - marginTop: props.theme.spacing(4), + marginTop: props.theme.spacing(20), textAlign: 'center', })); const BarLoaderContainer = styled.div((props) => ({ marginTop: props.theme.spacing(5), maxWidth: 300, + display: 'flex', })); function NftDashboard() { const { t } = useTranslation('translation', { keyPrefix: 'NFT_DASHBOARD_SCREEN' }); const selectedNetwork = useNetworkSelector(); + const { network } = useWalletSelector(); const { stxAddress, ordinalsAddress, hasActivatedOrdinalsKey } = useWalletSelector(); const [showShareNftOptions, setShowNftOptions] = useState(false); const [openReceiveModal, setOpenReceiveModal] = useState(false); const [showActivateOrdinalsAlert, setShowActivateOrdinalsAlert] = useState(false); const dispatch = useDispatch(); + const navigate = useNavigate(); function fetchNfts({ pageParam = 0 }): Promise { return getNfts(stxAddress, selectedNetwork, pageParam); } - function fetchOrdinals(): Promise { - return getOrdinalsByAddress(ordinalsAddress); - } + const fetchNonOrdinalUtxo = async () => { + const lastTransactionTime = await getTimeForNonOrdinalTransferTransaction(ordinalsAddress); + if (!lastTransactionTime) { + return getNonOrdinalUtxo(ordinalsAddress, network.type); + } + const diff = new Date().getTime() - Number(lastTransactionTime); + if (diff > REFETCH_UNSPENT_UTXO_TIME) { + return getNonOrdinalUtxo(ordinalsAddress, network.type); + } + return [] as UnspentOutput[]; + }; + + const fetchOrdinals = useCallback(async (): Promise => { + let response = await getOrdinalsByAddress(ordinalsAddress); + response = response.filter((ordinal) => ordinal.id !== undefined); + return response; + }, [ordinalsAddress]); + + const { data: unspentUtxos, refetch: refetchNonOrdinalUtxos } = useQuery({ + keepPreviousData: false, + queryKey: [`getNonOrdinalsUtxo-${ordinalsAddress}`], + queryFn: fetchNonOrdinalUtxo, + }); const { isLoading, data, fetchNextPage, isFetchingNextPage, hasNextPage, refetch, @@ -224,8 +289,14 @@ function NftDashboard() { queryFn: fetchOrdinals, }); + const refetchCollectibles = useCallback(async () => { + await refetch(); + await fetchOrdinals(); + refetchNonOrdinalUtxos(); + }, [refetch, fetchOrdinals]); + useEffect(() => { - refetch(); + refetchCollectibles(); }, [stxAddress, ordinalsAddress]); const nfts = data?.pages.map((page) => page.nftsList).flat(); @@ -303,6 +374,14 @@ function NftDashboard() { setShowNftOptions(false); }; + const onRestoreFundClick = () => { + navigate('restore-funds', { + state: { + unspentUtxos, + }, + }); + }; + const onActivateOrdinalsAlertCrossPress = () => { setShowActivateOrdinalsAlert(false); }; @@ -376,6 +455,15 @@ function NftDashboard() { )} + {!isGalleryOpen && unspentUtxos && unspentUtxos.length > 0 && ( + + alert + + {t('WITHDRAW_BTC_ALERT_TITLE')} + {t('WITHDRAW_BTC_ALERT_DESCRIPTION')} + + + )} {isLoading ? ( diff --git a/src/app/screens/nftDashboard/nft.tsx b/src/app/screens/nftDashboard/nft.tsx index 5e128c648..d6a63238e 100644 --- a/src/app/screens/nftDashboard/nft.tsx +++ b/src/app/screens/nftDashboard/nft.tsx @@ -12,6 +12,7 @@ interface Props { const NftNameText = styled.h1((props) => ({ ...props.theme.body_bold_m, + textAlign: 'left', })); const NftNameTextContainer = styled.h1((props) => ({ @@ -66,7 +67,10 @@ function Nft({ asset }: Props) { const url = `${asset.asset_identifier}::${asset.value.repr}`; function getName() { - if (asset?.data?.token_metadata) return `${asset?.data.token_metadata.name} `; + if (asset?.data?.token_metadata) { + return asset?.data.token_metadata?.name.length <= 35 ? `${asset?.data.token_metadata?.name} ` + : `${asset?.data.token_metadata?.name.substring(0, 35)}...`; + } if (asset.asset_identifier === BNS_CONTRACT) { return getBnsNftName(asset); diff --git a/src/app/screens/nftDashboard/nftImage.tsx b/src/app/screens/nftDashboard/nftImage.tsx index e1aec83b8..a08925599 100644 --- a/src/app/screens/nftDashboard/nftImage.tsx +++ b/src/app/screens/nftDashboard/nftImage.tsx @@ -13,29 +13,38 @@ interface ContainerProps { const ImageContainer = styled.div((props) => ({ display: 'flex', justifyContent: 'center', - alignItems: 'center', + alignItems: props.isGalleryOpen ? 'center' : 'flex-start', width: '100%', - flex: 1, - height: props.isGalleryOpen ? '100%' : 156, + height: props.isGalleryOpen ? '100%' : 150, overflow: 'hidden', position: 'relative', + borderRadius: 8, })); -const LoaderContainer = styled.div({ +const LoaderContainer = styled.div((props) => ({ display: 'flex', justifyContent: 'center', alignItems: 'center', -}); + position: 'absolute', + width: '100%', + left: 0, + bottom: 0, + right: 0, + top: 0, + height: props.isGalleryOpen ? '100%' : 150, +})); const Video = styled.video({ width: '100%', height: '100%', objectFit: 'cover', + borderRadius: 8, }); const StyledImg = styled(Image)` border-radius: 8px; object-fit: contain; + height: 150; `; interface Props { metadata: TokenMetaData; @@ -49,19 +58,18 @@ function NftImage({ metadata }: Props) { + - )} - src={getFetchableUrl(metadata.image_url ?? '', metadata.image_protocol ?? '')} + )} fallback={NftPlaceholderImage} /> ); } - if (metadata?.asset_protocol) { return (
- + diff --git a/src/app/screens/transactionRequest/index.tsx b/src/app/screens/transactionRequest/index.tsx index 0e0f179d9..317b27467 100644 --- a/src/app/screens/transactionRequest/index.tsx +++ b/src/app/screens/transactionRequest/index.tsx @@ -4,7 +4,7 @@ import useWalletSelector from '@hooks/useWalletSelector'; import { useEffect, useState } from 'react'; import { StacksTransaction } from '@stacks/transactions'; import ContractDeployRequest from '@components/transactionsRequests/ContractDeployTransaction'; -import useStxPendingTxData from '@hooks/useStxPendingTxData'; +import useStxPendingTxData from '@hooks/queries/useStxPendingTxData'; import { useNavigate } from 'react-router-dom'; import { MoonLoader } from 'react-spinners'; import styled from 'styled-components'; diff --git a/src/app/stores/index.ts b/src/app/stores/index.ts index 651167def..9418230c9 100644 --- a/src/app/stores/index.ts +++ b/src/app/stores/index.ts @@ -1,9 +1,7 @@ import ChromeStorage from '@utils/storage'; -import { createStore, applyMiddleware, combineReducers } from 'redux'; +import { createStore, combineReducers } from 'redux'; import { persistReducer, persistStore } from 'redux-persist'; -import createSagaMiddleware from 'redux-saga'; -import walletReducer from './wallet/walletReducer'; -import rootSaga from './root/saga'; +import walletReducer from './wallet/reducer'; import NftDataStateReducer from './nftData/reducer'; export const storage = new ChromeStorage(chrome.storage.local, chrome.runtime); @@ -17,7 +15,7 @@ const rootPersistConfig = { const WalletPersistConfig = { key: 'walletState', storage, - blacklist: ['seedPhrase', 'hasRestoredMemoryKey', 'configPrivateKey'], + blacklist: ['seedPhrase'], }; const appReducer = combineReducers({ @@ -32,9 +30,7 @@ const persistedReducer = persistReducer(rootPersistConfig, rootReducer); export type StoreState = ReturnType; const rootStore = (() => { - const sagaMiddleware = createSagaMiddleware(); - const store = createStore(persistedReducer, applyMiddleware(sagaMiddleware)); - sagaMiddleware.run(rootSaga); + const store = createStore(persistedReducer); const persistedStore = persistStore(store); return { store, persistedStore }; })(); diff --git a/src/app/stores/root/saga.ts b/src/app/stores/root/saga.ts deleted file mode 100644 index 63ae39fab..000000000 --- a/src/app/stores/root/saga.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { all } from 'redux-saga/effects'; -import { - fetchBtcWalletSaga, - fetchCoinDataSaga, - fetchRatesSaga, - fetchStxWalletSaga, -} from '@stores/wallet/saga'; - -function* rootSaga() { - const sagasList = [ - fetchRatesSaga(), - fetchStxWalletSaga(), - fetchBtcWalletSaga(), - fetchCoinDataSaga(), - ]; - - yield all(sagasList); -} - -export default rootSaga; diff --git a/src/app/stores/wallet/actions/actionCreators.ts b/src/app/stores/wallet/actions/actionCreators.ts index effe0abbb..89ca3fc2d 100644 --- a/src/app/stores/wallet/actions/actionCreators.ts +++ b/src/app/stores/wallet/actions/actionCreators.ts @@ -1,13 +1,10 @@ import { Account, BaseWallet, - BtcTransactionData, Coin, FeesMultipliers, FungibleToken, - NetworkType, SettingsNetwork, - StacksNetwork, SupportedCurrency, TransactionData, } from '@secretkeylabs/xverse-core/types'; @@ -80,6 +77,7 @@ export function selectAccount( masterPubKey: string, stxPublicKey: string, btcPublicKey: string, + ordinalsPublicKey: string, network: SettingsNetwork, // stackingState: StackingStateData, bnsName?: string, @@ -93,68 +91,40 @@ export function selectAccount( masterPubKey, stxPublicKey, btcPublicKey, + ordinalsPublicKey, network, // stackingState, bnsName, }; } -export function FetchFeeMultiplierAction(feeMultipliers: FeesMultipliers): actions.FetchFeeMultiplier { +export function setFeeMultiplierAction(feeMultipliers: FeesMultipliers): actions.SetFeeMultiplier { return { - type: actions.FetchFeeMultiplierKey, + type: actions.SetFeeMultiplierKey, feeMultipliers, }; } -export function fetchRatesAction(fiatCurrency: SupportedCurrency): actions.FetchRates { - return { - type: actions.FetchRatesKey, - fiatCurrency, - }; -} - -export function fetchRatesSuccessAction( +export function setCoinRatesAction( stxBtcRate: BigNumber, btcFiatRate: BigNumber, -): actions.FetchRatesSuccess { +): actions.SetCoinRates { return { - type: actions.FetchRatesSuccessKey, + type: actions.SetCoinRatesKey, stxBtcRate, btcFiatRate, }; } -export function fetchRatesFailAction(error: string): actions.FetchRatesFail { - return { - type: actions.FetchRatesFailureKey, - error, - }; -} - -export function fetchStxWalletDataRequestAction( - stxAddress: string, - network: StacksNetwork, - fiatCurrency: string, - stxBtcRate: BigNumber, -): actions.FetchStxWalletDataRequest { - return { - type: actions.FetchStxWalletDataRequestKey, - stxAddress, - network, - fiatCurrency, - stxBtcRate, - }; -} - -export function fetchStxWalletDataSuccessAction( +export function setStxWalletDataAction( stxBalance: BigNumber, stxAvailableBalance: BigNumber, stxLockedBalance: BigNumber, stxTransactions: TransactionData[], stxNonce: number, -): actions.FetchStxWalletDataSuccess { +): actions.SetStxWalletData { return { - type: actions.FetchStxWalletDataSuccessKey, + type: actions.SetStxWalletDataKey, stxBalance, stxAvailableBalance, stxLockedBalance, @@ -163,74 +133,24 @@ export function fetchStxWalletDataSuccessAction( }; } -export function fetchStxWalletDataFailureAction() { - return { - type: actions.FetchStxWalletDataFailureKey, - }; -} - -export function fetchBtcWalletDataRequestAction( - btcAddress: string, - network: NetworkType, - stxBtcRate: BigNumber, - btcFiatRate: BigNumber, -): actions.FetchBtcWalletDataRequest { +export function SetBtcWalletDataAction(balance: BigNumber): actions.SetBtcWalletData { return { - type: actions.FetchBtcWalletDataRequestKey, - btcAddress, - network, - stxBtcRate, - btcFiatRate, - }; -} - -export function fetchBtcWalletDataSuccess(balance: BigNumber, btctransactions: BtcTransactionData[]): actions.FetchBtcWalletDataSuccess { - return { - type: actions.FetchBtcWalletDataSuccessKey, + type: actions.SetBtcWalletDataKey, balance, - btctransactions, - }; -} - -export function fetchBtcWalletDataFail(): actions.FetchBtcWalletDataFail { - return { - type: actions.FetchBtcWalletDataFailureKey, }; } -export function fetchCoinDataRequestAction( - stxAddress: string, - network: StacksNetwork, - fiatCurrency: string, - coinsList: FungibleToken[] | null, -): actions.FetchCoinDataRequest { - return { - type: actions.FetchCoinDataRequestKey, - stxAddress, - network, - fiatCurrency, - coinsList, - }; -} - -export function FetchCoinDataSuccessAction( +export function setCoinDataAction( coinsList: FungibleToken[], supportedCoins: Coin[], -): actions.FetchCoinDataSuccess { +): actions.SetCoinData { return { - type: actions.FetchCoinDataSuccessKey, + type: actions.SetCoinDataKey, coinsList, supportedCoins, }; } -export function FetchCoinDataFailureAction(error: string): actions.FetchCoinDataFailure { - return { - type: actions.FetchCoinDataFailureKey, - error, - }; -} - export function FetchUpdatedVisibleCoinListAction( coinsList: FungibleToken[], ): actions.UpdateVisibleCoinList { diff --git a/src/app/stores/wallet/actions/types.ts b/src/app/stores/wallet/actions/types.ts index 740e32bff..216532e56 100644 --- a/src/app/stores/wallet/actions/types.ts +++ b/src/app/stores/wallet/actions/types.ts @@ -3,12 +3,10 @@ import { Coin, FeesMultipliers, FungibleToken, - NetworkType, SupportedCurrency, TransactionData, Account, BaseWallet, - StacksNetwork, SettingsNetwork, } from '@secretkeylabs/xverse-core/types'; import BigNumber from 'bignumber.js'; @@ -22,27 +20,20 @@ export const LockWalletKey = 'LockWallet'; export const StoreEncryptedSeedKey = 'StoreEncryptedSeed'; export const UpdateVisibleCoinListKey = 'UpdateVisibleCoinList'; export const AddAccountKey = 'AddAccount'; -export const FetchFeeMultiplierKey = 'FetchFeeMultiplier'; +export const SetFeeMultiplierKey = 'SetFeeMultiplierKey'; export const ChangeFiatCurrencyKey = 'ChangeFiatCurrency'; export const ChangeNetworkKey = 'ChangeNetwork'; export const GetActiveAccountsKey = 'GetActiveAccounts'; export const SetWalletSeedPhraseKey = 'SetWalletSeed'; export const FetchStxWalletDataRequestKey = 'FetchStxWalletDataRequest'; -export const FetchStxWalletDataSuccessKey = 'FetchStxWalletDataSuccess'; -export const FetchStxWalletDataFailureKey = 'FetchStxWalletDataFailure'; +export const SetStxWalletDataKey = 'SetStxWalletDataKey'; -export const FetchBtcWalletDataRequestKey = 'FetchBtcWalletDataRequest'; -export const FetchBtcWalletDataSuccessKey = 'FetchBtcWalletDataSuccess'; -export const FetchBtcWalletDataFailureKey = 'FetchBtcWalletDataFailure'; +export const SetBtcWalletDataKey = 'SetBtcWalletData'; -export const FetchRatesKey = 'FetchRates'; -export const FetchRatesSuccessKey = 'FetchRatesSuccess'; -export const FetchRatesFailureKey = 'FetchRatesFailure'; +export const SetCoinRatesKey = 'SetCoinRatesKey'; -export const FetchCoinDataRequestKey = 'FetchCoinDataRequest'; -export const FetchCoinDataSuccessKey = 'FetchCoinDataSuccess'; -export const FetchCoinDataFailureKey = 'FetchCoinDataFailure'; +export const SetCoinDataKey = 'SetCoinDataKey'; export const ChangeHasActivatedOrdinalsKey = 'ChangeHasActivatedOrdinalsKey'; @@ -56,14 +47,12 @@ export interface WalletState { masterPubKey: string; stxPublicKey: string; btcPublicKey: string; + ordinalsPublicKey: string; accountsList: Account[]; selectedAccount: Account | null; - hasRestoredMemoryKey: boolean; network: SettingsNetwork; seedPhrase: string; encryptedSeed: string; - loadingWalletData: boolean; - loadingBtcData: boolean; fiatCurrency: SupportedCurrency; btcFiatRate: BigNumber; stxBtcRate: BigNumber; @@ -100,8 +89,8 @@ export interface UnlockWallet { seed: string; } -export interface FetchFeeMultiplier { - type: typeof FetchFeeMultiplierKey; +export interface SetFeeMultiplier { + type: typeof SetFeeMultiplierKey; feeMultipliers: FeesMultipliers; } @@ -131,37 +120,19 @@ export interface SelectAccount { masterPubKey: string; stxPublicKey: string; btcPublicKey: string; + ordinalsPublicKey: string; bnsName?: string; network: SettingsNetwork; // stackingState: StackingStateData; } - -export interface FetchRates { - type: typeof FetchRatesKey; - fiatCurrency: SupportedCurrency; -} - -export interface FetchRatesSuccess { - type: typeof FetchRatesSuccessKey; +export interface SetCoinRates { + type: typeof SetCoinRatesKey; stxBtcRate: BigNumber; btcFiatRate: BigNumber; } -export interface FetchRatesFail { - type: typeof FetchRatesFailureKey; - error: string; -} - -export interface FetchStxWalletDataRequest { - type: typeof FetchStxWalletDataRequestKey; - stxAddress: string; - network: StacksNetwork; - fiatCurrency: string; - stxBtcRate: BigNumber; -} - -export interface FetchStxWalletDataSuccess { - type: typeof FetchStxWalletDataSuccessKey; +export interface SetStxWalletData { + type: typeof SetStxWalletDataKey; stxBalance: BigNumber; stxAvailableBalance: BigNumber; stxLockedBalance: BigNumber; @@ -169,46 +140,16 @@ export interface FetchStxWalletDataSuccess { stxNonce: number; } -export interface FetchStxWalletDataFail { - type: typeof FetchStxWalletDataFailureKey; -} - -export interface FetchBtcWalletDataRequest { - type: typeof FetchBtcWalletDataRequestKey; - btcAddress: string; - network: NetworkType; - stxBtcRate: BigNumber; - btcFiatRate: BigNumber; -} - -export interface FetchBtcWalletDataSuccess { - type: typeof FetchBtcWalletDataSuccessKey; +export interface SetBtcWalletData { + type: typeof SetBtcWalletDataKey; balance: BigNumber; - btctransactions: BtcTransactionData[]; -} - -export interface FetchBtcWalletDataFail { - type: typeof FetchBtcWalletDataFailureKey; } -export interface FetchCoinDataRequest { - type: typeof FetchCoinDataRequestKey; - stxAddress: string; - network: StacksNetwork; - fiatCurrency: string; - coinsList: FungibleToken[] | null; -} - -export interface FetchCoinDataSuccess { - type: typeof FetchCoinDataSuccessKey; +export interface SetCoinData { + type: typeof SetCoinDataKey; coinsList: FungibleToken[]; supportedCoins: Coin[]; } - -export interface FetchCoinDataFailure { - type: typeof FetchCoinDataFailureKey; - error: string; -} export interface UpdateVisibleCoinList { type: typeof UpdateVisibleCoinListKey; coinsList: FungibleToken[]; @@ -254,19 +195,11 @@ export type WalletActions = | SetWalletSeedPhrase | UnlockWallet | LockWallet - | FetchFeeMultiplier - | FetchRates - | FetchRatesSuccess - | FetchRatesFail - | FetchStxWalletDataRequest - | FetchStxWalletDataSuccess - | FetchStxWalletDataFail - | FetchBtcWalletDataFail - | FetchBtcWalletDataSuccess - | FetchBtcWalletDataRequest - | FetchCoinDataRequest - | FetchCoinDataSuccess - | FetchCoinDataFailure + | SetFeeMultiplier + | SetCoinRates + | SetStxWalletData + | SetBtcWalletData + | SetCoinData | UpdateVisibleCoinList | ChangeFiatCurrency | ChangeNetwork diff --git a/src/app/stores/wallet/walletReducer.ts b/src/app/stores/wallet/reducer.ts similarity index 75% rename from src/app/stores/wallet/walletReducer.ts rename to src/app/stores/wallet/reducer.ts index 7136b0291..0c2fa331e 100644 --- a/src/app/stores/wallet/walletReducer.ts +++ b/src/app/stores/wallet/reducer.ts @@ -9,23 +9,17 @@ import { LockWalletKey, FetchAccountKey, SelectAccountKey, - FetchRatesSuccessKey, - FetchStxWalletDataRequestKey, - FetchStxWalletDataSuccessKey, - FetchStxWalletDataFailureKey, - FetchBtcWalletDataRequestKey, - FetchBtcWalletDataSuccessKey, - FetchBtcWalletDataFailureKey, - FetchCoinDataRequestKey, - FetchCoinDataSuccessKey, - FetchCoinDataFailureKey, + SetBtcWalletDataKey, + SetCoinDataKey, AddAccountKey, UpdateVisibleCoinListKey, - FetchFeeMultiplierKey, + SetFeeMultiplierKey, ChangeFiatCurrencyKey, ChangeNetworkKey, GetActiveAccountsKey, SetWalletSeedPhraseKey, + SetStxWalletDataKey, + SetCoinRatesKey, ChangeHasActivatedOrdinalsKey, ChangeShowBtcReceiveAlertKey, ChangeShowOrdinalReceiveAlertKey, @@ -34,10 +28,11 @@ import { const initialWalletState: WalletState = { stxAddress: '', btcAddress: '', - masterPubKey: '', ordinalsAddress: '', + masterPubKey: '', stxPublicKey: '', btcPublicKey: '', + ordinalsPublicKey: '', network: { type: 'Mainnet', address: 'https://stacks-node-api.mainnet.stacks.co', @@ -46,8 +41,6 @@ const initialWalletState: WalletState = { selectedAccount: null, seedPhrase: '', encryptedSeed: '', - loadingWalletData: false, - loadingBtcData: false, fiatCurrency: 'USD', btcFiatRate: new BigNumber(0), stxBtcRate: new BigNumber(0), @@ -59,7 +52,6 @@ const initialWalletState: WalletState = { coinsList: null, coins: [], feeMultipliers: null, - hasRestoredMemoryKey: false, networkAddress: undefined, hasActivatedOrdinalsKey: undefined, showBtcReceiveAlert: false, @@ -97,11 +89,12 @@ const walletReducer = ( ...state, selectedAccount: action.selectedAccount, stxAddress: action.stxAddress, - btcAddress: action.btcAddress, ordinalsAddress: action.ordinalsAddress, + btcAddress: action.btcAddress, masterPubKey: action.masterPubKey, stxPublicKey: action.stxPublicKey, btcPublicKey: action.btcPublicKey, + ordinalsPublicKey: action.ordinalsPublicKey, network: action.network, }; case StoreEncryptedSeedKey: @@ -113,7 +106,6 @@ const walletReducer = ( return { ...state, seedPhrase: action.seedPhrase, - hasRestoredMemoryKey: true, }; case UnlockWalletKey: return { @@ -124,72 +116,38 @@ const walletReducer = ( return { ...state, seedPhrase: '', - hasRestoredMemoryKey: false, }; - case FetchRatesSuccessKey: + case SetCoinRatesKey: return { ...state, btcFiatRate: action.btcFiatRate, stxBtcRate: action.stxBtcRate, }; - case FetchStxWalletDataRequestKey: - return { - ...state, - loadingWalletData: true, - }; - case FetchStxWalletDataSuccessKey: + case SetStxWalletDataKey: return { ...state, stxBalance: action.stxBalance, stxAvailableBalance: action.stxAvailableBalance, stxLockedBalance: action.stxLockedBalance, stxNonce: action.stxNonce, - loadingWalletData: false, - }; - case FetchStxWalletDataFailureKey: - return { - ...state, - loadingWalletData: false, - }; - case FetchBtcWalletDataRequestKey: - return { - ...state, - loadingBtcData: true, }; - case FetchBtcWalletDataSuccessKey: + case SetBtcWalletDataKey: return { ...state, btcBalance: action.balance, - loadingBtcData: false, - }; - case FetchBtcWalletDataFailureKey: - return { - ...state, - loadingBtcData: false, }; - case FetchCoinDataRequestKey: - return { - ...state, - loadingWalletData: true, - }; - case FetchCoinDataSuccessKey: + case SetCoinDataKey: return { ...state, coinsList: action.coinsList, coins: action.supportedCoins, - loadingWalletData: false, - }; - case FetchCoinDataFailureKey: - return { - ...state, - loadingWalletData: false, }; case UpdateVisibleCoinListKey: return { ...state, coinsList: action.coinsList, }; - case FetchFeeMultiplierKey: + case SetFeeMultiplierKey: return { ...state, feeMultipliers: action.feeMultipliers, diff --git a/src/app/stores/wallet/saga.ts b/src/app/stores/wallet/saga.ts deleted file mode 100644 index 9f7b12881..000000000 --- a/src/app/stores/wallet/saga.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { takeEvery, put, call } from 'redux-saga/effects'; -import { - fetchBtcToCurrencyRate, - fetchBtcTransactionsData, - fetchStxAddressData, - fetchStxToBtcRate, - getCoinsInfo, - getFtData, -} from '@secretkeylabs/xverse-core/api'; -import { PAGINATION_LIMIT } from '@utils/constants'; -import BigNumber from 'bignumber.js'; -import { - BtcAddressData, - StxAddressData, - FungibleToken, - CoinsResponse, -} from '@secretkeylabs/xverse-core/types'; -import { - FetchStxWalletDataRequestKey, - FetchRates, - FetchRatesKey, - FetchStxWalletDataRequest, - FetchBtcWalletDataRequest, - FetchBtcWalletDataRequestKey, - FetchCoinDataRequest, - FetchCoinDataRequestKey, -} from './actions/types'; -import { - fetchBtcWalletDataFail, - fetchBtcWalletDataSuccess, - FetchCoinDataFailureAction, - FetchCoinDataSuccessAction, - fetchRatesFailAction, - fetchRatesSuccessAction, - fetchStxWalletDataFailureAction, - fetchStxWalletDataSuccessAction, -} from './actions/actionCreators'; - -function* fetchRates(action: FetchRates) { - try { - const btcFiatRate: BigNumber = yield fetchBtcToCurrencyRate({ - fiatCurrency: action.fiatCurrency, - }); - const stxBtcRate: BigNumber = yield fetchStxToBtcRate(); - yield put(fetchRatesSuccessAction(stxBtcRate, btcFiatRate)); - } catch (e: any) { - yield put(fetchRatesFailAction(e.toString())); - } -} - -function* fetchStxWalletData(action: FetchStxWalletDataRequest) { - try { - const stxData: StxAddressData = yield fetchStxAddressData( - action.stxAddress, - action.network, - 0, - PAGINATION_LIMIT, - ); - const stxBalance = stxData.balance; - const stxAvailableBalance = stxData.availableBalance; - const stxLockedBalance = stxData.locked; - const stxTransactions = stxData.transactions; - const stxNonce = stxData.nonce; - yield put( - fetchStxWalletDataSuccessAction( - stxBalance, - stxAvailableBalance, - stxLockedBalance, - stxTransactions, - stxNonce, - ), - ); - } catch (error) { - yield put(fetchStxWalletDataFailureAction()); - } -} - -function* fetchBtcWalletData(action: FetchBtcWalletDataRequest) { - try { - const btcData: BtcAddressData = yield fetchBtcTransactionsData( - action.btcAddress, - action.network, - ); - const btcBalance = new BigNumber(btcData.finalBalance); - const btcTransactions = btcData.transactions; - yield put(fetchBtcWalletDataSuccess(btcBalance, btcTransactions)); - } catch (error) { - yield put(fetchBtcWalletDataFail()); - } -} - -function* fetchCoinData(action: FetchCoinDataRequest) { - try { - const fungibleTokenList: Array = yield call( - getFtData, - action.stxAddress, - action.network, - ); - const visibleCoins: FungibleToken[] | null = action.coinsList; - if (visibleCoins) { - visibleCoins.forEach((visibleCoin) => { - const coinToBeUpdated = fungibleTokenList.find( - (ft) => ft.principal === visibleCoin.principal, - ); - if (coinToBeUpdated) coinToBeUpdated.visible = visibleCoin.visible; - else if (visibleCoin.visible) { - visibleCoin.balance = '0'; - fungibleTokenList.push(visibleCoin); - } - }); - } else { - fungibleTokenList.forEach((ft) => { - ft.visible = true; - }); - } - - const contractids: string[] = []; - // getting contract ids of all fts - fungibleTokenList.forEach((ft) => { - contractids.push(ft.principal); - }); - const coinsReponse: CoinsResponse = yield call(getCoinsInfo, contractids, action.fiatCurrency); - coinsReponse.forEach((coin) => { - if (!coin.name) { - coin.name = coin.contract.split('.')[1]; - } - }); - - // update attributes of fungible token list - fungibleTokenList.forEach((ft) => { - coinsReponse.forEach((coin) => { - if (ft.principal === coin.contract) { - ft.ticker = coin.ticker; - ft.decimals = coin.decimals; - ft.supported = coin.supported; - ft.image = coin.image; - ft.name = coin.name; - ft.tokenFiatRate = coin.tokenFiatRate; - coin.visible = ft.visible; - } - }); - }); - - // sorting the list - moving supported to the top - const supportedFts: FungibleToken[] = []; - const unSupportedFts: FungibleToken[] = []; - fungibleTokenList.forEach((ft) => { - if (ft.supported) supportedFts.push(ft); - else unSupportedFts.push(ft); - }); - const sortedFtList: FungibleToken[] = [...supportedFts, ...unSupportedFts]; - yield put(FetchCoinDataSuccessAction(sortedFtList, coinsReponse)); - } catch (error: any) { - yield put(FetchCoinDataFailureAction(error.toString())); - } -} - -export function* fetchRatesSaga() { - yield takeEvery(FetchRatesKey, fetchRates); -} - -export function* fetchStxWalletSaga() { - yield takeEvery(FetchStxWalletDataRequestKey, fetchStxWalletData); -} - -export function* fetchBtcWalletSaga() { - yield takeEvery(FetchBtcWalletDataRequestKey, fetchBtcWalletData); -} - -export function* fetchCoinDataSaga() { - yield takeEvery(FetchCoinDataRequestKey, fetchCoinData); -} diff --git a/src/app/utils/constants.ts b/src/app/utils/constants.ts index a89da8e51..a9d2d32e2 100644 --- a/src/app/utils/constants.ts +++ b/src/app/utils/constants.ts @@ -6,7 +6,7 @@ export const TERMS_LINK = 'https://xverse.app/terms'; export const PRIVACY_POLICY_LINK = 'https://xverse.app/privacy'; export const SUPPORT_LINK = 'https://support.xverse.app/hc/en-us'; export const SUPPORT_EMAIL = 'support@xverse.app'; -export const BTC_TRANSACTION_STATUS_URL = 'https://www.blockchain.com/btc/tx/'; +export const BTC_TRANSACTION_STATUS_URL = 'https://mempool.space/tx/'; export const BTC_TRANSACTION_TESTNET_STATUS_URL = 'https://live.blockcypher.com/btc-testnet/tx/'; export const TRANSACTION_STATUS_URL = 'https://explorer.stacks.co/txid/'; export const XVERSE_WEB_POOL_URL = 'https://pool.xverse.app'; @@ -28,6 +28,7 @@ export enum LoaderSize { export const BITCOIN_DUST_AMOUNT_SATS = 5500; export const PAGINATION_LIMIT = 50; +export const REFETCH_UNSPENT_UTXO_TIME = 2 * 60 * 60 * 1000; export const initialNetworksList: SettingsNetwork[] = [ { diff --git a/src/app/utils/localStorage.ts b/src/app/utils/localStorage.ts index 7edd18f67..ff1cf599c 100644 --- a/src/app/utils/localStorage.ts +++ b/src/app/utils/localStorage.ts @@ -2,6 +2,7 @@ const userPrefBackupRemindKey = 'UserPref:BackupRemind'; const isTermsAccepted = 'isTermsAccepted'; const hasFinishedOnboardingKey = 'hasFinishedOnboarding'; const saltKey = 'salt'; +const nonOrdinalTransferTime = 'nonOrdinalTransferTime'; export function saveMultiple(items: { [x: string]: string }) { const itemKeys = Object.keys(items); @@ -49,3 +50,12 @@ export function saveSalt(salt: string) { export function getSalt() { return localStorage.getItem(saltKey); } + +export async function saveTimeForNonOrdinalTransferTransaction(ordinalAddress: string) { + const currentTime = new Date().getTime().toString(); + return localStorage.setItem(nonOrdinalTransferTime + ordinalAddress, currentTime); +} + +export async function getTimeForNonOrdinalTransferTransaction(ordinalAddress: string) { + return localStorage.getItem(nonOrdinalTransferTime + ordinalAddress); +} diff --git a/src/assets/img/dashboard/add_token.svg b/src/assets/img/dashboard/add_token.svg new file mode 100644 index 000000000..57391e16b --- /dev/null +++ b/src/assets/img/dashboard/add_token.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/img/dashboard/bitcoin_token.svg b/src/assets/img/dashboard/bitcoin_token.svg new file mode 100644 index 000000000..a498c97a3 --- /dev/null +++ b/src/assets/img/dashboard/bitcoin_token.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/img/dashboard/coin_token.svg b/src/assets/img/dashboard/coin_token.svg new file mode 100644 index 000000000..1b227221b --- /dev/null +++ b/src/assets/img/dashboard/coin_token.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/img/dashboard/mia_token.svg b/src/assets/img/dashboard/mia_token.svg new file mode 100644 index 000000000..71f641f9d --- /dev/null +++ b/src/assets/img/dashboard/mia_token.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/img/dashboard/nyc_token.svg b/src/assets/img/dashboard/nyc_token.svg new file mode 100644 index 000000000..8b7a254c4 --- /dev/null +++ b/src/assets/img/dashboard/nyc_token.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/img/dashboard/stacks_token.svg b/src/assets/img/dashboard/stacks_token.svg new file mode 100644 index 000000000..468e80753 --- /dev/null +++ b/src/assets/img/dashboard/stacks_token.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/img/tick.svg b/src/assets/img/tick.svg new file mode 100644 index 000000000..50b373869 --- /dev/null +++ b/src/assets/img/tick.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/img/transactions/Assets.svg b/src/assets/img/transactions/Assets.svg new file mode 100644 index 000000000..d1d5c48fd --- /dev/null +++ b/src/assets/img/transactions/Assets.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/img/transactions/address.svg b/src/assets/img/transactions/address.svg new file mode 100644 index 000000000..c97859d45 --- /dev/null +++ b/src/assets/img/transactions/address.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/img/transactions/dropDownIcon.svg b/src/assets/img/transactions/dropDownIcon.svg new file mode 100644 index 000000000..89891e88c --- /dev/null +++ b/src/assets/img/transactions/dropDownIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/img/transactions/ordinal.svg b/src/assets/img/transactions/ordinal.svg new file mode 100644 index 000000000..d326dbb28 --- /dev/null +++ b/src/assets/img/transactions/ordinal.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/img/transactions/output.svg b/src/assets/img/transactions/output.svg new file mode 100644 index 000000000..346a39fe7 --- /dev/null +++ b/src/assets/img/transactions/output.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/background/background.ts b/src/background/background.ts index caa4dc8a6..f3476391f 100755 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -1,11 +1,11 @@ /* eslint-disable no-underscore-dangle */ -import { CONTENT_SCRIPT_PORT } from '../content-scripts/message-types'; -import type { LegacyMessageFromContentScript } from '../content-scripts/message-types'; +import internalBackgroundMessageHandler from '@common/utils/messageHandlers'; import { handleLegacyExternalMethodFormat, inferLegacyMessage, -} from './legacy-external-message-handler'; -import internalBackgroundMessageHandler from './messageHandlers'; +} from '@common/utils/legacy-external-message-handler'; +import { CONTENT_SCRIPT_PORT } from '@common/types/message-types'; +import type { LegacyMessageFromContentScript } from '@common/types/message-types'; function deleteTimer(port) { if (port._timer) { diff --git a/src/content-scripts/inpage-types.ts b/src/common/types/inpage-types.ts similarity index 66% rename from src/content-scripts/inpage-types.ts rename to src/common/types/inpage-types.ts index 48574f277..cf91c1102 100644 --- a/src/content-scripts/inpage-types.ts +++ b/src/common/types/inpage-types.ts @@ -6,6 +6,8 @@ export enum DomEventName { signatureRequest = 'signatureRequest', structuredDataSignatureRequest = 'structuredDataSignatureRequest', transactionRequest = 'stacksTransactionRequest', + getAddressRequest = 'SatsAddressRequest', + signPsbtRequest = 'SatsPsbtRequest', } export interface AuthenticationRequestEventDetails { @@ -25,3 +27,15 @@ export interface TransactionRequestEventDetails { } export type TransactionRequestEvent = CustomEvent; + +export interface GetAddressRequestEventDetails { + btcAddressRequest: string, +} + +export type GetAddressRequestEvent = CustomEvent; + +export interface SignPsbtRequestEventDetails { + signPsbtRequest: string; +} + +export type SignPsbtRequestEvent = CustomEvent; diff --git a/src/content-scripts/message-types.ts b/src/common/types/message-types.ts similarity index 67% rename from src/content-scripts/message-types.ts rename to src/common/types/message-types.ts index b53316adf..3626cdca5 100644 --- a/src/content-scripts/message-types.ts +++ b/src/common/types/message-types.ts @@ -1,9 +1,14 @@ import { FinishedTxPayload, SignatureData, SponsoredFinishedTxPayload } from '@stacks/connect'; +import { GetAddressResponse, SignPsbtResponse } from 'sats-connect'; export const MESSAGE_SOURCE = 'xverse-wallet' as const; export const CONTENT_SCRIPT_PORT = 'xverse-content-script' as const; +/** + * Stacks External Callable Methods + * @enum {string} + */ export enum ExternalMethods { transactionRequest = 'transactionRequest', transactionResponse = 'transactionResponse', @@ -23,7 +28,7 @@ export enum InternalMethods { OriginatingTabClosed = 'OriginatingTabClosed', } -export type ExtensionMethods = ExternalMethods | InternalMethods; +export type ExtensionMethods = ExternalMethods | ExternalSatsMethods | InternalMethods; interface BaseMessage { source: typeof MESSAGE_SOURCE; @@ -86,3 +91,38 @@ export type LegacyMessageToContentScript = | AuthenticationResponseMessage | TransactionResponseMessage | SignatureResponseMessage; + +/** + * Sats External Callable Methods + * @enum {string} + */ +export enum ExternalSatsMethods { + getAddressRequest = 'getAddressRequest', + getAddressResponse = 'getAddressResponse', + signPsbtRequest = 'signPsbtRequest', + signPsbtResponse = 'signPsbtResponse', +} + +type GetAddressRequestMessage = Message; + +export type GetAddressResponseMessage = Message< +ExternalSatsMethods.getAddressResponse, +{ + addressRequest: string + addressResponse: GetAddressResponse | string; +} +>; + +type SignPsbtRequestMessage = Message; + +export type SignPsbtResponseMessage = Message< +ExternalSatsMethods.signPsbtResponse, +{ + signPsbtRequest: string; + signPsbtResponse: SignPsbtResponse | string; +} +>; + +export type SatsConnectMessageFromContentScript = GetAddressRequestMessage | SignPsbtRequestMessage; + +export type SatsConnectMessageToContentScript = GetAddressResponseMessage | SignPsbtResponseMessage; diff --git a/src/content-scripts/messages.ts b/src/common/types/messages.ts similarity index 100% rename from src/content-scripts/messages.ts rename to src/common/types/messages.ts diff --git a/src/content-scripts/get-event-source-window.ts b/src/common/utils/get-event-source-window.ts similarity index 100% rename from src/content-scripts/get-event-source-window.ts rename to src/common/utils/get-event-source-window.ts diff --git a/src/background/legacy-external-message-handler.ts b/src/common/utils/legacy-external-message-handler.ts similarity index 75% rename from src/background/legacy-external-message-handler.ts rename to src/common/utils/legacy-external-message-handler.ts index ca94eb89b..593109439 100644 --- a/src/background/legacy-external-message-handler.ts +++ b/src/common/utils/legacy-external-message-handler.ts @@ -1,14 +1,17 @@ import { SignatureData } from '@stacks/connect'; import { ExternalMethods, + ExternalSatsMethods, InternalMethods, LegacyMessageFromContentScript, LegacyMessageToContentScript, MESSAGE_SOURCE, + SatsConnectMessageFromContentScript, + SatsConnectMessageToContentScript, SignatureResponseMessage, -} from '../content-scripts/message-types'; -import { sendMessage } from '../content-scripts/messages'; -import RequestsRoutes from '../content-scripts/route-urls'; +} from '../types/message-types'; +import { sendMessage } from '../types/messages'; +import RequestsRoutes from './route-urls'; import popupCenter from './popup-center'; export function inferLegacyMessage(message: any): message is LegacyMessageFromContentScript { @@ -46,7 +49,7 @@ interface ListenForPopupCloseArgs { id?: number; // TabID from requesting tab, to which request should be returned tabId?: number; - response: LegacyMessageToContentScript; + response: LegacyMessageToContentScript | SatsConnectMessageToContentScript; } function listenForPopupClose({ id, tabId, response }: ListenForPopupCloseArgs) { chrome.windows.onRemoved.addListener((winId) => { @@ -82,13 +85,12 @@ function listenForOriginTabClose({ tabId }: ListenForOriginTabCloseArgs) { } async function triggerRequstWindowOpen(path: RequestsRoutes, urlParams: URLSearchParams) { - // if (IS_TEST_ENV) return openRequestInFullPage(path, urlParams); console.log(`/popup.html#${path}?${urlParams.toString()}`); return popupCenter({ url: `/popup.html#${path}?${urlParams.toString()}` }); } export async function handleLegacyExternalMethodFormat( - message: LegacyMessageFromContentScript, + message: LegacyMessageFromContentScript | SatsConnectMessageFromContentScript, port: chrome.runtime.Port, ) { const { payload } = message; @@ -163,6 +165,48 @@ export async function handleLegacyExternalMethodFormat( listenForOriginTabClose({ tabId }); break; } + case ExternalSatsMethods.getAddressRequest: { + const { urlParams, tabId } = makeSearchParamsWithDefaults(port, [ + ['addressRequest', payload], + ]); + + const { id } = await triggerRequstWindowOpen(RequestsRoutes.AddressRequest, urlParams); + listenForPopupClose({ + id, + tabId, + response: { + source: MESSAGE_SOURCE, + payload: { + addressRequest: payload, + addressResponse: 'cancel', + }, + method: ExternalSatsMethods.getAddressResponse, + }, + }); + listenForOriginTabClose({ tabId }); + break; + } + case ExternalSatsMethods.signPsbtRequest: { + const { urlParams, tabId } = makeSearchParamsWithDefaults(port, [ + ['signPsbtRequest', payload], + ]); + + const { id } = await triggerRequstWindowOpen(RequestsRoutes.SignBtcTx, urlParams); + listenForPopupClose({ + id, + tabId, + response: { + source: MESSAGE_SOURCE, + payload: { + signPsbtRequest: payload, + signPsbtResponse: 'cancel', + }, + method: ExternalSatsMethods.signPsbtResponse, + }, + }); + listenForOriginTabClose({ tabId }); + break; + } default: { break; } diff --git a/src/background/messageHandlers.ts b/src/common/utils/messageHandlers.ts similarity index 88% rename from src/background/messageHandlers.ts rename to src/common/utils/messageHandlers.ts index f0d37d3b4..4e4bf2c59 100644 --- a/src/background/messageHandlers.ts +++ b/src/common/utils/messageHandlers.ts @@ -1,5 +1,5 @@ -import { InternalMethods } from 'content-scripts/message-types'; -import { BackgroundMessages } from 'content-scripts/messages'; +import { InternalMethods } from '@common/types/message-types'; +import { BackgroundMessages } from 'common/types/messages'; function validateMessagesAreFromExtension(sender: chrome.runtime.MessageSender) { // Only respond to internal messages from our UI, not content scripts in other applications diff --git a/src/background/popup-center.ts b/src/common/utils/popup-center.ts similarity index 100% rename from src/background/popup-center.ts rename to src/common/utils/popup-center.ts diff --git a/src/content-scripts/route-urls.ts b/src/common/utils/route-urls.ts similarity index 70% rename from src/content-scripts/route-urls.ts rename to src/common/utils/route-urls.ts index 90cd8f577..76b365467 100644 --- a/src/content-scripts/route-urls.ts +++ b/src/common/utils/route-urls.ts @@ -3,6 +3,8 @@ enum RequestsRoutes { TransactionRequest = '/transaction-request', AuthenticationRequest = '/authentication-request', SignatureRequest = '/signature-request', + AddressRequest = '/btc-select-address-request', + SignBtcTx = '/psbt-signing-request', } export default RequestsRoutes; diff --git a/src/content-scripts/content-script.ts b/src/content-scripts/content-script.ts index 29e361b66..38a630e6f 100644 --- a/src/content-scripts/content-script.ts +++ b/src/content-scripts/content-script.ts @@ -1,18 +1,22 @@ +import RequestsRoutes from '@common/utils/route-urls'; +import getEventSourceWindow from '@common/utils/get-event-source-window'; import { CONTENT_SCRIPT_PORT, ExternalMethods, + ExternalSatsMethods, LegacyMessageFromContentScript, LegacyMessageToContentScript, MESSAGE_SOURCE, -} from './message-types'; + SatsConnectMessageFromContentScript, +} from '@common/types/message-types'; import { AuthenticationRequestEvent, DomEventName, SignatureRequestEvent, TransactionRequestEvent, -} from './inpage-types'; -import RequestsRoutes from './route-urls'; -import getEventSourceWindow from './get-event-source-window'; + GetAddressRequestEvent, + SignPsbtRequestEvent, +} from '@common/types/inpage-types'; // Legacy messaging to work with older versions of Connect window.addEventListener('message', (event) => { @@ -45,7 +49,9 @@ function connect() { connect(); // Sends message to background script that an event has fired -function sendMessageToBackground(message: LegacyMessageFromContentScript) { +function sendMessageToBackground( + message: LegacyMessageFromContentScript | SatsConnectMessageFromContentScript, +) { backgroundPort.postMessage(message); } @@ -59,7 +65,7 @@ chrome.runtime.onMessage.addListener((message: LegacyMessageToContentScript) => interface ForwardDomEventToBackgroundArgs { payload: string; - method: LegacyMessageFromContentScript['method']; + method: LegacyMessageFromContentScript['method'] | SatsConnectMessageFromContentScript['method']; urlParam: string; path: RequestsRoutes; } @@ -116,6 +122,30 @@ document.addEventListener(DomEventName.structuredDataSignatureRequest, (( }); }) as EventListener); +// Listen for a CustomEvent (BTC Address request) coming from the web app +document.addEventListener(DomEventName.getAddressRequest, (( + event: GetAddressRequestEvent, +) => { + forwardDomEventToBackground({ + path: RequestsRoutes.AddressRequest, + payload: event.detail.btcAddressRequest, + urlParam: 'addressRequest', + method: ExternalSatsMethods.getAddressRequest, + }); +}) as EventListener); + +// Listen for a CustomEvent (PSBT Signing request) coming from the web app +document.addEventListener(DomEventName.signPsbtRequest, (( + event: SignPsbtRequestEvent, +) => { + forwardDomEventToBackground({ + path: RequestsRoutes.SignBtcTx, + payload: event.detail.signPsbtRequest, + urlParam: 'signPsbtRequest', + method: ExternalSatsMethods.signPsbtRequest, + }); +}) as EventListener); + // Inject inpage script (Stacks Provider) const inpage = document.createElement('script'); inpage.src = chrome.runtime.getURL('inpage.js'); diff --git a/src/inpage/index.ts b/src/inpage/index.ts new file mode 100644 index 000000000..57500cf54 --- /dev/null +++ b/src/inpage/index.ts @@ -0,0 +1,5 @@ +import StacksMethodsProvider from './stacks.inpage'; +import SatsMethodsProvider from './sats.inpage'; + +window.StacksProvider = StacksMethodsProvider; +window.BitcoinProvider = SatsMethodsProvider; diff --git a/src/inpage/sats.inpage.ts b/src/inpage/sats.inpage.ts new file mode 100644 index 000000000..29fe3516e --- /dev/null +++ b/src/inpage/sats.inpage.ts @@ -0,0 +1,70 @@ +import { SignPsbtRequestEventDetails } from './../common/types/inpage-types'; +import { BitcoinProvider, GetAddressResponse } from 'sats-connect'; +import { + DomEventName, + GetAddressRequestEventDetails, +} from '@common/types/inpage-types'; +import { + ExternalSatsMethods, + GetAddressResponseMessage, + MESSAGE_SOURCE, + SatsConnectMessageToContentScript, + SignPsbtResponseMessage, +} from '@common/types/message-types'; +import { SignTransactionResponse } from 'sats-connect/src/transactions/signTransaction'; + +const isValidEvent = (event: MessageEvent, method: SatsConnectMessageToContentScript['method']) => { + const { data } = event; + const correctSource = data.source === MESSAGE_SOURCE; + const correctMethod = data.method === method; + return correctSource && correctMethod && !!data.payload; +}; + +const SatsMethodsProvider: BitcoinProvider = { + connect: async (btcAddressRequest): Promise => { + const event = new CustomEvent(DomEventName.getAddressRequest, { + detail: { btcAddressRequest }, + }); + document.dispatchEvent(event); + return new Promise((resolve, reject) => { + const handleMessage = (eventMessage: MessageEvent) => { + if (!isValidEvent(eventMessage, ExternalSatsMethods.getAddressResponse)) return; + if (eventMessage.data.payload?.addressRequest !== btcAddressRequest) return; + window.removeEventListener('message', handleMessage); + if (eventMessage.data.payload.addressResponse === 'cancel') { + reject(eventMessage.data.payload.addressResponse); + return; + } + if (typeof eventMessage.data.payload.addressResponse !== 'string') { + resolve(eventMessage.data.payload.addressResponse); + } + }; + window.addEventListener('message', handleMessage); + }); + }, + signTransaction: async (signPsbtRequest: string): Promise => { + const event = new CustomEvent(DomEventName.signPsbtRequest, { + detail: { signPsbtRequest }, + }); + document.dispatchEvent(event); + return new Promise((resolve, reject) => { + const handleMessage = (eventMessage: MessageEvent) => { + if (!isValidEvent(eventMessage, ExternalSatsMethods.signPsbtResponse)) return; + if (eventMessage.data.payload?.signPsbtRequest !== signPsbtRequest) return; + window.removeEventListener('message', handleMessage); + if (eventMessage.data.payload.signPsbtResponse === 'cancel') { + reject(eventMessage.data.payload.signPsbtResponse); + return; + } + if (typeof eventMessage.data.payload.signPsbtResponse !== 'string') { + resolve(eventMessage.data.payload.signPsbtResponse); + } + }; + window.addEventListener('message', handleMessage); + }); + }, + call(request: string): Promise> { + throw new Error('`call` function is not implemented'); + }, +}; +export default SatsMethodsProvider; diff --git a/src/inpage/inpage.ts b/src/inpage/stacks.inpage.ts similarity index 96% rename from src/inpage/inpage.ts rename to src/inpage/stacks.inpage.ts index 8ebb3571a..ae39f5911 100644 --- a/src/inpage/inpage.ts +++ b/src/inpage/stacks.inpage.ts @@ -4,7 +4,7 @@ import { DomEventName, SignatureRequestEventDetails, TransactionRequestEventDetails, -} from '../content-scripts/inpage-types'; +} from '@common/types/inpage-types'; import { AuthenticationResponseMessage, ExternalMethods, @@ -12,8 +12,9 @@ import { MESSAGE_SOURCE, SignatureResponseMessage, TransactionResponseMessage, -} from '../content-scripts/message-types'; +} from '@common/types/message-types'; +declare const VERSION: string; type CallableMethods = keyof typeof ExternalMethods; interface ExtensionResponse { @@ -58,7 +59,7 @@ const isValidEvent = (event: MessageEvent, method: LegacyMessageToContentScript[ return correctSource && correctMethod && !!data.payload; }; -const provider: StacksProvider = { +const StacksMethodsProvider: StacksProvider = { getURL: async () => { const { url } = await callAndReceive('getURL'); return url; @@ -153,7 +154,7 @@ const provider: StacksProvider = { }, getProductInfo() { return { - version: '0.0.1', + version: VERSION, name: 'Xverse Wallet', }; }, @@ -162,4 +163,4 @@ const provider: StacksProvider = { }, }; -window.StacksProvider = provider; +export default StacksMethodsProvider; diff --git a/src/locales/en.json b/src/locales/en.json index 38d718788..31d6ce806 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -23,7 +23,8 @@ "RECEIVE": "Receive", "SEND": "Send", "BUY": "Buy", - "MANAGE_TOKEN": "Manage token list" + "MANAGE_TOKEN": "Manage token list", + "STACKS_AND_TOKEN": "Stacks and tokens" }, "TOKEN_SCREEN": { "ADD_COINS": "Manage tokens" @@ -47,15 +48,15 @@ "SEND": "Send", "AMOUNT": "Amount", "BALANCE": "Balance", - "RECEPIENT": "Recipient", - "RECEPIENT_PLACEHOLDER": "Address or .btc domain", - "BTC_RECEPIENT_PLACEHOLDER": "Enter Bitcoin address", + "RECIPIENT": "Recipient", + "RECIPIENT_PLACEHOLDER": "STX Address or .btc domain", + "BTC_RECIPIENT_PLACEHOLDER": "Enter Bitcoin address", "MEMO": "Add a memo (optional)", "MEMO_PLACEHOLDER": "Memo", "ASSOCIATED_ADDRESS": "Associated Address", "MEMO_INFO": "Adding a memo can have an impact on the transaction fee", "NEXT": "Next", - "NO_FUNDS": "You don’t have any fund to send.", + "NO_FUNDS": "You don’t have any funds to send.", "BUY_CRYPTO": "Buy crypto →", "MOVE_TO_ASSET_DETAIL": "Back to asset detail", "ERRORS": { @@ -80,7 +81,7 @@ "CONFIRM_TRANSACTION": { "SEND": "Confirm Transaction", "INDICATION": "You will send", - "RECEPIENT_ADDRESS": "recipient address", + "RECIPIENT_ADDRESS": "recipient address", "MEMO": "Attached memo", "NETWORK": "Network", "FEES": "Fees", @@ -91,7 +92,20 @@ "SATS": "SATS", "CONFIRM_TX": "Confirm Transaction", "MOVE_TO_ASSET_DETAIL": "Back to asset detail", - "SPONSORED_TX_INFO": "This is a sponsored transaction, no transaction fees will be deducted from your account." + "SPONSORED_TX_INFO": "This is a sponsored transaction, no transaction fees will be deducted from your account.", + "TOTAL": "Total", + "AMOUNT_PLUS_FEES": "Amount + fees", + "REVIEW_TRNSACTION": "Review transaction", + "AMOUNT": "Amount", + "YOUR_ADDRESS": "Your address", + "FROM": "From", + "INPUT": "Inputs", + "INPUT_AND_OUTPUT": "Inputs & Outputs", + "OUTPUT": "Output", + "RECIPIENT": "Recipient", + "ASSET": "Asset", + "NETWORK_MISMATCH": "There’s a mismatch between your active network and the network you’re logged with.", + "ADDRESS_MISMATCH": "There’s a mismatch between your signing address and the address you’re logged with." }, "TX_ERRORS": { "INSUFFICIENT_BALANCE": "Insufficient balance", @@ -211,7 +225,7 @@ "POOL_FEE": "Pool fee", "MINIMUM_AMOUNT": "Minimum amount", "REWARD_CYCLES": "Reward Cycles", - "START_STACKNG": "Start stacking", + "START_STACKING": "Start stacking", "STACK_FOR_REWARD": "Stack your coins to earn rewards", "XVERSE_POOL": "Xverse delegated pooling allows everyone to participate in Stacking and earn rewards. ", "STACK_STX": "Stack STX", @@ -243,7 +257,18 @@ "ACTIVATE_ORDINALS_INFO":"You have Bitcoin Ordinals in your wallet. Would you like to display them? This is an experimental feature. You can change this setting at anytime.", "ACTIVATE": "Activate", "DENY": "No", - "COPIED": "Copied!" + "COPIED": "Copied!", + "WITHDRAW_BTC_ALERT_TITLE": "You have Bitcoin stored in your ordinal address.", + "WITHDRAW_BTC_ALERT_DESCRIPTION": "Transfer it to my Bitcoin address →" + }, + "RESTORE_FUNDS_SCREEN": { + "TITLE": "Restore funds", + "AMOUNT": "Amount", + "TRANSFER": "Transfer", + "BACK": "Back", + "ORDINAL_ADDRESS": "Ordinal address", + "NO_FUNDS": "You don’t have any funds stored on your ordinal address.", + "DESCRIPTION": "You have Bitcoin stored in your ordinal address. You can transfer them to your payment address so they can be used for payments and are shown in your balance." }, "NFT_DETAIL_SCREEN": { "NFT_DETAIL": "Item detail", @@ -265,8 +290,9 @@ "GAMMA": "Gamma.io", "MOVE_TO_ASSET_DETAIL": "Back to gallery", "ORDINALS": "Ordinal", - "ORDINAL_SEND_TITLE": "Transfers are coming soon!", - "ORDINAL_SEND_DESCRIPTION": "We are working hard to enable transfers for Ordinals.", + "ORDINAL_PENDING_SEND_TITLE": "Transfer Pending", + "ORDINAL_PENDING_SEND_DESCRIPTION": "This Ordinal is already in a pending transfer.", + "ORDINAL_PENDING_SEND_BUTTON": "View in Explorer", "INSCRIPTION": "Inscription", "ID": "Id", "CONTENT_LENGTH": "Content length", @@ -364,7 +390,7 @@ "ANOTHER_ADDRESS": "Another address", "CONTRACT_ADDRESS": "Contract Address", "MY_ADDRESS": "My Address", - "RECEPIENT_ADDRESS": "Recipient address", + "RECIPIENT_ADDRESS": "Recipient address", "EQUAL": "will transfer exactly", "GREATER": " will transfer more than", "GREATER_EQUAL": "will transfer at least", @@ -429,5 +455,14 @@ "RECEIVING_BTC_INFO": "Use this address to receive Bitcoin payments only. Do not use this address to receive Ordinals.", "I_UNDERSTAND": "I understand", "DO_NOT_SHOW_MESSAGE": "Do not show this message again" +}, + "SELECT_BTC_ADDRESS_SCREEN": { + "TITLE": "Bitcoin Address Request", + "ACCOUNT": "Account", + "CONNECT_BUTTON": "Approve", + "CANCEL_BUTTON": "Dismiss", + "BITCOIN_ADDRESS": "Bitcoin address", + "ORDINAL_ADDRESS": "Ordinal address", + "NETWORK_MISMATCH": "There’s a mismatch between your active network and the network you’re logged with." } } diff --git a/src/pages/Options/index.tsx b/src/pages/Options/index.tsx index 01c5d79f2..6d32b94a7 100644 --- a/src/pages/Options/index.tsx +++ b/src/pages/Options/index.tsx @@ -1,4 +1,4 @@ -import { InternalMethods } from 'content-scripts/message-types'; +import { InternalMethods } from '@common/types/message-types'; import rootStore from '@stores/index'; import { setWalletSeedPhraseAction } from '@stores/wallet/actions/actionCreators'; import { createRoot } from 'react-dom/client'; diff --git a/src/pages/Popup/index.tsx b/src/pages/Popup/index.tsx index 71de7bc34..9887c642c 100644 --- a/src/pages/Popup/index.tsx +++ b/src/pages/Popup/index.tsx @@ -1,4 +1,4 @@ -import { InternalMethods } from 'content-scripts/message-types'; +import { InternalMethods } from '@common/types/message-types'; import rootStore from '@stores/index'; import { setWalletSeedPhraseAction } from '@stores/wallet/actions/actionCreators'; import { createRoot } from 'react-dom/client'; diff --git a/tsconfig.json b/tsconfig.json index 04b9c2503..22a6361d1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,7 +24,7 @@ "@hooks/*": ["app/hooks/*"], "@assets/*": ["assets/*"], "@screens/*": ["app/screens/*"], - "@shared/*": ["shared/*"], + "@common/*": ["common/*"], "@stores/*": ["app/stores/*"], "@utils/*": ["app/utils/*"], "@core/*": ["app/core/*"] diff --git a/webpack/webpack.config.js b/webpack/webpack.config.js index f84ccf59b..e45d19435 100644 --- a/webpack/webpack.config.js +++ b/webpack/webpack.config.js @@ -30,7 +30,7 @@ var options = { entry: { background: path.join(SRC_ROOT_PATH, 'background', 'background.ts'), - inpage: path.join(SRC_ROOT_PATH, 'inpage', 'inpage.ts'), + inpage: path.join(SRC_ROOT_PATH, 'inpage', 'index.ts'), 'content-script': path.join(SRC_ROOT_PATH, 'content-scripts', 'content-script.ts'), options: path.join(SRC_ROOT_PATH, 'pages', 'Options', 'index.tsx'), popup: path.join(SRC_ROOT_PATH, 'pages', 'Popup', 'index.tsx'),