├── .env.example
├── .eslintrc.cjs
├── .firebaserc
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── build-and-release.yml
│ ├── firebase-hosting-merge.yml
│ └── firebase-hosting-pull-request.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── SUPPORT.md
├── firebase.json
├── index.html
├── package-lock.json
├── package.json
├── patches
└── @cmdcode+tapscript+1.4.3.patch
├── public
├── background.js
├── chromestore.png
├── content.js
├── inject.js
├── logo.png
├── logo.svg
└── manifest.json
├── src
├── app
│ ├── app-context.tsx
│ ├── app.hook.ts
│ ├── app.tsx
│ ├── index.ts
│ └── settings.ts
├── bitcoin
│ ├── fees.ts
│ ├── helpers.ts
│ ├── lib
│ │ └── bitcoin-lib.ts
│ ├── node.ts
│ ├── wallet-storage.ts
│ └── wallet.ts
├── components
│ ├── app-layout.tsx
│ ├── dollar-balance.tsx
│ ├── icon.tsx
│ ├── index.ts
│ ├── layout.tsx
│ └── set-fees.tsx
├── constants
│ └── constants.ts
├── env.d.ts
├── hooks
│ ├── address.hook.ts
│ ├── create-wallet.hook.ts
│ ├── get-balances.hook.ts
│ ├── index.ts
│ ├── restore-wallet.hook.ts
│ ├── safe-balances.hook.ts
│ ├── send-sats.hook.ts
│ ├── send-tokens.hook.ts
│ └── show-transactions.hook.ts
├── main.tsx
├── router
│ ├── index.ts
│ ├── route-path.ts
│ ├── router-provider.tsx
│ └── router.tsx
├── screens
│ ├── addresses.tsx
│ ├── balances.tsx
│ ├── confirm-transaction.tsx
│ ├── connect-wallet.tsx
│ ├── create-wallet.tsx
│ ├── decode-and-sign-psbt.tsx
│ ├── explore.tsx
│ ├── index.ts
│ ├── mnemonic.tsx
│ ├── modals
│ │ ├── reset-storage.tsx
│ │ ├── show-address.tsx
│ │ └── show-mnemonic.tsx
│ ├── password.tsx
│ ├── restore-wallet.tsx
│ ├── send.tsx
│ ├── settings.tsx
│ ├── sign-message.tsx
│ ├── switch-network.tsx
│ └── verify-sign.tsx
├── theme
│ ├── index.ts
│ ├── theme-constants.ts
│ ├── theme-provider.tsx
│ └── theme.ts
├── transfer
│ ├── get-ordinals-unspents.ts
│ ├── get-pipe-unspents.ts
│ ├── get-unspents.ts
│ ├── prepare-transfer-pipe.ts
│ ├── prepare-transfer-sats.ts
│ ├── select-all-ordinals-unspents.ts
│ ├── select-all-pipe-unspents.ts
│ ├── select-all-unspents.ts
│ ├── select-unspents.ts
│ ├── transfer-pipe.ts
│ └── transfer-sats.ts
├── utils
│ ├── bitcoin-price.ts
│ ├── calculate-fee.ts
│ ├── clean-float.ts
│ ├── crypto.ts
│ ├── fingerprint.ts
│ ├── hash-to-string.ts
│ ├── hex-to-bytes.ts
│ ├── sats-to-btc.ts
│ ├── sats-to-dollars.ts
│ └── truncate-in-middle.ts
└── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.env.example:
--------------------------------------------------------------------------------
1 | VITE_SERVER_HOST=""
2 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended',
7 | 'plugin:react-hooks/recommended',
8 | ],
9 | ignorePatterns: ['dist', '.eslintrc.cjs'],
10 | parser: '@typescript-eslint/parser',
11 | plugins: ['react-refresh'],
12 | rules: {
13 | 'react-refresh/only-export-components': [
14 | 'warn',
15 | { allowConstantExport: true },
16 | ],
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "inspip-wallet"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | custom: []
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **System (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Wallet version [e.g. 22]
30 |
31 | **Additional context**
32 | Add any other context about the problem here.
33 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/build-and-release.yml:
--------------------------------------------------------------------------------
1 | name: Build and Release
2 |
3 | on:
4 | # push:
5 | # tags:
6 | # - 'v*'
7 | push:
8 | branches:
9 | - "main"
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | permissions:
15 | contents: write
16 |
17 | steps:
18 | - name: Checkout code
19 | uses: actions/checkout@v3
20 |
21 | - name: Setup Node.js
22 | uses: actions/setup-node@v3
23 | with:
24 | node-version: '19'
25 |
26 | - name: Install dependencies
27 | run: npm install
28 |
29 | - name: Build project
30 | run: npm run build
31 | env:
32 | VITE_SERVER_HOST: ${{ secrets.VITE_SERVER_HOST }}
33 |
34 | - name: Package /dist directory
35 | run: zip -r dist.zip dist/
36 |
37 | - name: Create Release
38 | uses: ncipollo/release-action@v1
39 | with:
40 | artifacts: dist.zip
41 | tag: "v0.1.12"
42 | allowUpdates: true
43 | artifactContentType: application/zip
44 |
45 | - name: Upload Release Asset
46 | uses: softprops/action-gh-release@v1
47 | with:
48 | files: dist.zip
49 | tag_name: "v0.1.12"
50 |
--------------------------------------------------------------------------------
/.github/workflows/firebase-hosting-merge.yml:
--------------------------------------------------------------------------------
1 | # This file was auto-generated by the Firebase CLI
2 | # https://github.com/firebase/firebase-tools
3 |
4 | name: Deploy to Firebase Hosting on merge
5 | 'on':
6 | push:
7 | branches:
8 | - main
9 | jobs:
10 | build_and_deploy:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | - run: npm ci && npm run build
15 | - uses: FirebaseExtended/action-hosting-deploy@v0
16 | with:
17 | repoToken: '${{ secrets.GITHUB_TOKEN }}'
18 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_INSPIP_WALLET }}'
19 | channelId: live
20 | projectId: inspip-wallet
21 |
--------------------------------------------------------------------------------
/.github/workflows/firebase-hosting-pull-request.yml:
--------------------------------------------------------------------------------
1 | # This file was auto-generated by the Firebase CLI
2 | # https://github.com/firebase/firebase-tools
3 |
4 | name: Deploy to Firebase Hosting on PR
5 | 'on': pull_request
6 | jobs:
7 | build_and_preview:
8 | if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}'
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v3
12 | - run: npm ci && npm run build
13 | - uses: FirebaseExtended/action-hosting-deploy@v0
14 | with:
15 | repoToken: '${{ secrets.GITHUB_TOKEN }}'
16 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_INSPIP_WALLET }}'
17 | projectId: inspip-wallet
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | .yarn.lock
5 | .env
6 | npm-debug.log*
7 | yarn-debug.log*
8 | yarn-error.log*
9 | pnpm-debug.log*
10 | lerna-debug.log*
11 |
12 | node_modules
13 | dist
14 | dist-ssr
15 | *.local
16 | # Editor directories and files
17 | .vscode/*
18 | !.vscode/extensions.json
19 | .idea
20 | .DS_Store
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
26 | .
27 | DS_Store
28 | .firebase
29 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | [version] - 0.1.12
2 |
3 | - [x] Wif fix
4 | - [x] Injection improvements
5 |
6 | [version] - 0.1.11
7 |
8 | - [x] Chrome extension size fix
9 |
10 | [version] - 0.1.10
11 |
12 | - [x] Protection for ordinals
13 | - [x] Transfer bitcoin refactor
14 | - [x] Transfer pipe refactor
15 | - [x] Testnet improvements
16 |
17 | [version] - 0.1.9
18 |
19 | - [x] UX & UI improvements
20 | - [x] Fix Sending Transactions vin issue
21 |
22 | [version] - 0.1.8
23 |
24 | - [x] Browser Injection:
25 | - [x] Connect
26 | - [x] Send bitcoin
27 | - [x] Hot Reload for Extension
28 | - [x] Mnemo click to copy
29 | - [x] Editor settings
30 | - [x] Wallet Settings
31 | - [x] Switch Testnet
32 | - [x] Fee calculator with vsize
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Business Source License 1.1
2 |
3 | License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
4 | "Business Source License" is a trademark of MariaDB Corporation Ab.
5 |
6 | -----------------------------------------------------------------------------
7 |
8 | Parameters
9 |
10 | Licensor: Inscrib3
11 |
12 | Licensed Work: Inscrib3
13 | The Licensed Work is (c) 2023 Inscrib3
14 |
15 | Additional Use Grant: Research and auditing
16 |
17 | Change Date: The earlier of 2025-01-01
18 |
19 | Change License: GNU General Public License v2.0 or later
20 |
21 | -----------------------------------------------------------------------------
22 |
23 | Terms
24 |
25 | The Licensor hereby grants you the right to copy, modify, create derivative
26 | works, redistribute, and make non-production use of the Licensed Work. The
27 | Licensor may make an Additional Use Grant, above, permitting limited
28 | production use.
29 |
30 | Effective on the Change Date, or the fourth anniversary of the first publicly
31 | available distribution of a specific version of the Licensed Work under this
32 | License, whichever comes first, the Licensor hereby grants you rights under
33 | the terms of the Change License, and the rights granted in the paragraph
34 | above terminate.
35 |
36 | If your use of the Licensed Work does not comply with the requirements
37 | currently in effect as described in this License, you must purchase a
38 | commercial license from the Licensor, its affiliated entities, or authorized
39 | resellers, or you must refrain from using the Licensed Work.
40 |
41 | All copies of the original and modified Licensed Work, and derivative works
42 | of the Licensed Work, are subject to this License. This License applies
43 | separately for each version of the Licensed Work and the Change Date may vary
44 | for each version of the Licensed Work released by Licensor.
45 |
46 | You must conspicuously display this License on each original or modified copy
47 | of the Licensed Work. If you receive the Licensed Work in original or
48 | modified form from a third party, the terms and conditions set forth in this
49 | License apply to your use of that work.
50 |
51 | Any use of the Licensed Work in violation of this License will automatically
52 | terminate your rights under this License for the current and all other
53 | versions of the Licensed Work.
54 |
55 | This License does not grant you any right in any trademark or logo of
56 | Licensor or its affiliates (provided that you may use a trademark or logo of
57 | Licensor as expressly required by this License).
58 |
59 | TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
60 | AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
61 | EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
62 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
63 | TITLE.
64 |
65 | MariaDB hereby grants you permission to use this License's text to license
66 | your works, and to refer to it using the trademark "Business Source License",
67 | as long as you comply with the Covenants of Licensor below.
68 |
69 | -----------------------------------------------------------------------------
70 |
71 | Covenants of Licensor
72 |
73 | In consideration of the right to use this License’s text and the "Business
74 | Source License" name and trademark, Licensor covenants to MariaDB, and to all
75 | other recipients of the licensed work to be provided by Licensor:
76 |
77 | 1. To specify as the Change License the GPL Version 2.0 or any later version,
78 | or a license that is compatible with GPL Version 2.0 or a later version,
79 | where "compatible" means that software provided under the Change License can
80 | be included in a program with software provided under GPL Version 2.0 or a
81 | later version. Licensor may specify additional Change Licenses without
82 | limitation.
83 |
84 | 2. To either: (a) specify an additional grant of rights to use that does not
85 | impose any additional restriction on the right granted in this License, as
86 | the Additional Use Grant; or (b) insert the text "None".
87 |
88 | 3. To specify a Change Date.
89 |
90 | 4. Not to modify this License in any other way.
91 |
92 | -----------------------------------------------------------------------------
93 |
94 | Notice
95 |
96 | The Business Source License (this document, or the "License") is not an Open
97 | Source license. However, the Licensed Work will eventually be made available
98 | under an Open Source License, as stated in this License.
99 |
100 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Inspip | Pipe Wallet Extension
2 |
3 | ## Experimental Public Beta: Use at your own risk, no refunds, no warranties.
4 |
5 | Easily manage and interact with PIPE tokens on the Bitcoin network right from your browser. The Pipe Wallet extension provides a simple interface to *Deploy*, *Mint*, and *Transfer* tokens adhering to the PIPE protocol specifications.
6 |
7 | ## Features
8 | - Fully non-custodial: **you control your private keys** and they never leave your device. Don't screw up, we cannot recover lost keys!
9 | - Seamless token management: Deploy, Mint, Transfer.
10 | - Securely sign transactions.
11 | - View transaction history and token balances.
12 |
13 | ## Installation
14 | Make sure to use Chrome, Brave or Chromium browser. Firefox and Safari are not supported at this time.
15 |
16 | 1. Download the extension from the *Release* page
17 | 2. Go to `chrome://extensions/` or `brave://extensions/`
18 | 3. Click *Load unpacked* and select the downloaded folder
19 |
20 | ## Support
21 | For support or any inquiries, please visit our [Discord](https://discord.gg/gpFGS4UJ5f).
22 |
23 | ## How to Contribute
24 | Any PR is welcome. You can also tip us some spare sats if you like the project.
25 | Your donations will contribute to the ongoing development, maintenance, and customer support. Find more [here](SUPPORT.md).
26 |
27 | ## PIPE Protocol Overview
28 | [PIPE](https://github.com/BennyTheDev/pipe-specs) is a Bitcoin-native token protocol with three main functions:
29 |
30 | **Deploy**, **Mint**, and **Transfer** (DMT):
31 |
32 | - **Deploy**: Initiates a new token with defined attributes like ticker name, maximum supply, and minting limits.
33 | - **Mint**: Allows the creation of new tokens within the defined limits set during deployment.
34 | - **Transfer**: Facilitates the sending of tokens to selected recipients.
35 |
36 | The PIPE protocol, by introducing a structured way to deploy, mint, and transfer tokens on the Bitcoin network, provides a framework for tokenized assets and applications which isn't natively supported by Bitcoin. This potentially allows for a variety of decentralized applications (dApps), tokenized assets, and smart contract-like behaviors on Bitcoin, which are features more commonly associated with platforms like Ethereum.
37 | In essence, it can bring a new level of functionality to Bitcoin while still utilizing Bitcoin's robust and secure blockchain.
38 |
--------------------------------------------------------------------------------
/SUPPORT.md:
--------------------------------------------------------------------------------
1 | # Support Pipe Wallet Development
2 | We're committed to making the Pipe Wallet the best it can be. Your contributions can help us achieve this goal.
3 |
4 | ## How to Contribute
5 | Your donations will contribute to the ongoing development, maintenance, and customer support.
6 |
7 | **Bitcoin Address for Donations**: `3GbeGZnnricNemsneqhjEWa9r2JbBRzdyu`
8 |
9 | Any amount is appreciated. Thank you for your support!
10 |
11 | ## Contact
12 | For more information on how your donation will be used, feel free to contact on [Discord](https://discord.gg/gpFGS4UJ5f) or email [inscrib3@proton.me](mailto:inscrib3@proton.me).
13 |
14 | ## Supporters
15 | We are grateful to these wonderful individuals who helped support the Pipe wallet project:
16 |
17 | - Your name here...
18 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "dist",
4 | "ignore": [
5 | "firebase.json",
6 | "**/.*",
7 | "**/node_modules/**"
8 | ]
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 | Inscrib3 | Pipe Wallet Extension
13 |
14 |
15 |
16 |
17 |
32 |
33 |
34 |
35 |
36 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "inspip",
3 | "private": true,
4 | "version": "0.1.12",
5 | "type": "module",
6 | "license": "BUSL-1.1",
7 | "scripts": {
8 | "dev": "vite",
9 | "build": "tsc && vite build",
10 | "build:watch": "vite build --watch",
11 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
12 | "preview": "vite preview"
13 | },
14 | "dependencies": {
15 | "@bitcoin-js/tiny-secp256k1-asmjs": "^2.2.3",
16 | "@cmdcode/tapscript": "^1.4.3",
17 | "@types/chrome": "^0.0.258",
18 | "bip32": "^4.0.0",
19 | "bip322-js": "^1.1.0",
20 | "bip39": "^3.1.0",
21 | "bitcoinjs-lib": "^6.1.5",
22 | "bs58": "^5.0.0",
23 | "buffer": "^6.0.3",
24 | "crypto-js": "^4.1.1",
25 | "ecpair": "^2.1.0",
26 | "firebase": "^10.8.0",
27 | "grommet": "^2.33.2",
28 | "grommet-icons": "^4.11.0",
29 | "localforage": "^1.10.0",
30 | "match-sorter": "^6.3.1",
31 | "murmurhash-js": "^1.0.0",
32 | "qrcode.react": "^3.1.0",
33 | "react": "^18.2.0",
34 | "react-dom": "^18.2.0",
35 | "react-router-dom": "^6.16.0",
36 | "sort-by": "^1.2.0",
37 | "styled-components": "^5.1.0",
38 | "typescript": "^5.2.2"
39 | },
40 | "devDependencies": {
41 | "@rollup/plugin-inject": "^5.0.5",
42 | "@types/crypto-js": "^4.1.3",
43 | "@types/murmurhash-js": "^1.0.5",
44 | "@types/node": "^20.8.9",
45 | "@types/react": "^18.2.15",
46 | "@types/react-dom": "^18.2.7",
47 | "@typescript-eslint/eslint-plugin": "^6.0.0",
48 | "@typescript-eslint/parser": "^6.0.0",
49 | "@vitejs/plugin-react": "^4.0.3",
50 | "eslint": "^8.45.0",
51 | "eslint-plugin-react-hooks": "^4.6.0",
52 | "eslint-plugin-react-refresh": "^0.4.3",
53 | "hot-reload-extension-vite": "^1.0.13",
54 | "patch-package": "^7.0.0",
55 | "vite": "^4.4.5",
56 | "vite-plugin-node-polyfills": "^0.15.0"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/patches/@cmdcode+tapscript+1.4.3.patch:
--------------------------------------------------------------------------------
1 | diff --git a/node_modules/@cmdcode/tapscript/package.json b/node_modules/@cmdcode/tapscript/package.json
2 | index d3b3491..2d5ed53 100644
3 | --- a/node_modules/@cmdcode/tapscript/package.json
4 | +++ b/node_modules/@cmdcode/tapscript/package.json
5 | @@ -15,7 +15,8 @@
6 | "require": {
7 | "types": "./dist/types/index.d.ts",
8 | "default": "./dist/main.cjs"
9 | - }
10 | + },
11 | + "types": "./dist/types/index.d.ts"
12 | }
13 | },
14 | "scripts": {
15 |
--------------------------------------------------------------------------------
/public/background.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-undef
2 | chrome.runtime.onMessage.addListener(async (request) => {
3 | if (typeof request.message !== 'string') {
4 | if (request.message.action === 'SendBitcoin') {
5 | const params = request.message.params;
6 | // eslint-disable-next-line no-undef
7 | chrome.windows.create({
8 | type: 'popup',
9 | url: `index.html#?action=SendBitcoin&feerate=${params.feerate}&toAddress=${params.toAddress}&satoshi=${params.satoshi}`,
10 | width: 400,
11 | height: 600,
12 | });
13 | }
14 | if (request.message.action === 'SendTokens') {
15 | const params = request.message.params;
16 | // eslint-disable-next-line no-undef
17 | chrome.windows.create({
18 | type: 'popup',
19 | url: `index.html#?action=SendTokens&feerate=${params.feerate}&toAddress=${params.toAddress}&amount=${params.amount}&id=${params.id}&ticker=${params.ticker}`,
20 | width: 400,
21 | height: 600,
22 | });
23 | }
24 | if (request.message.action === 'SignPsbt') {
25 | const params = request.message.params;
26 | // eslint-disable-next-line no-undef
27 | chrome.windows.create({
28 | type: 'popup',
29 | url: `index.html#?action=SignPsbt&psbt=${params.psbt}&toSignInputs=${params.toSignInputs}&autoFinalized=${params.autoFinalized}`,
30 | width: 400,
31 | height: 600,
32 | });
33 | }
34 | if (request.message.action === 'SignMessage') {
35 | const params = request.message.params;
36 | // eslint-disable-next-line no-undef
37 | chrome.windows.create({
38 | type: 'popup',
39 | url: `index.html#?action=SignMessage&msg=${params.msg}`,
40 | width: 400,
41 | height: 600,
42 | });
43 | }
44 | if (request.message.action === 'VerifySign') {
45 | const params = request.message.params;
46 | // eslint-disable-next-line no-undef
47 | chrome.windows.create({
48 | type: 'popup',
49 | url: `index.html#?action=VerifySign&msg=${params.msg}&signature=${params.signature}`,
50 | width: 400,
51 | height: 600,
52 | });
53 | }
54 | if (request.message.action === 'ConnectWallet') {
55 | // eslint-disable-next-line no-undef
56 | chrome.windows.create({
57 | type: 'popup',
58 | url: `index.html#?action=ConnectWallet`,
59 | width: 400,
60 | height: 600,
61 | });
62 | }
63 | } else {
64 | if (request.message.includes('ReturnSendTokens')) {
65 | // eslint-disable-next-line no-undef
66 | chrome.tabs.query({ active: true }, function(tabs) {
67 | // eslint-disable-next-line no-undef
68 | chrome.tabs.sendMessage(tabs[0].id, { message: request.message });
69 | });
70 | }
71 | if (request.message.includes('ReturnConnectWalletInfo')) {
72 | // eslint-disable-next-line no-undef
73 | chrome.tabs.query({ active: true }, function(tabs) {
74 | // eslint-disable-next-line no-undef
75 | chrome.tabs.sendMessage(tabs[0].id, { message: request.message });
76 | });
77 | }
78 | if (request.message.includes('ClientRejectConnectWalletInfo')) {
79 | // eslint-disable-next-line no-undef
80 | chrome.tabs.query({ active: true }, function(tabs) {
81 | // eslint-disable-next-line no-undef
82 | chrome.tabs.sendMessage(tabs[0].id, { message: request.message });
83 | });
84 | }
85 | if (request.message.includes('ReturnSendBitcoin')) {
86 | // eslint-disable-next-line no-undef
87 | chrome.tabs.query({ active: true }, function(tabs) {
88 | // eslint-disable-next-line no-undef
89 | chrome.tabs.sendMessage(tabs[0].id, { message: request.message });
90 | });
91 | }
92 | if (request.message.includes('ReturnSignPsbt')) {
93 | // eslint-disable-next-line no-undef
94 | chrome.tabs.query({ active: true }, function(tabs) {
95 | // eslint-disable-next-line no-undef
96 | chrome.tabs.sendMessage(tabs[0].id, { message: request.message });
97 | });
98 | }
99 | if (request.message.includes('ReturnErrorOnSignPsbt')) {
100 | // eslint-disable-next-line no-undef
101 | chrome.tabs.query({ active: true }, function(tabs) {
102 | // eslint-disable-next-line no-undef
103 | chrome.tabs.sendMessage(tabs[0].id, { message: request.message });
104 | });
105 | }
106 | if (request.message.includes('ClientRejectSignPsbt')) {
107 | // eslint-disable-next-line no-undef
108 | chrome.tabs.query({ active: true }, function(tabs) {
109 | // eslint-disable-next-line no-undef
110 | chrome.tabs.sendMessage(tabs[0].id, { message: request.message });
111 | });
112 | }
113 | if (request.message.includes('ReturnSignMessage')) {
114 | // eslint-disable-next-line no-undef
115 | chrome.tabs.query({ active: true }, function(tabs) {
116 | // eslint-disable-next-line no-undef
117 | chrome.tabs.sendMessage(tabs[0].id, { message: request.message });
118 | });
119 | }
120 | if (request.message.includes('ReturnErrorOnSignMessage')) {
121 | // eslint-disable-next-line no-undef
122 | chrome.tabs.query({ active: true }, function(tabs) {
123 | // eslint-disable-next-line no-undef
124 | chrome.tabs.sendMessage(tabs[0].id, { message: request.message });
125 | });
126 | }
127 | if (request.message.includes('ClientRejectSignMessage')) {
128 | // eslint-disable-next-line no-undef
129 | chrome.tabs.query({ active: true }, function(tabs) {
130 | // eslint-disable-next-line no-undef
131 | chrome.tabs.sendMessage(tabs[0].id, { message: request.message });
132 | });
133 | }
134 | if (request.message.includes('ReturnVerifySign')) {
135 | // eslint-disable-next-line no-undef
136 | chrome.tabs.query({ active: true }, function(tabs) {
137 | // eslint-disable-next-line no-undef
138 | chrome.tabs.sendMessage(tabs[0].id, { message: request.message });
139 | });
140 | }
141 | if (request.message.includes('ReturnErrorOnVerifySign')) {
142 | // eslint-disable-next-line no-undef
143 | chrome.tabs.query({ active: true }, function(tabs) {
144 | // eslint-disable-next-line no-undef
145 | chrome.tabs.sendMessage(tabs[0].id, { message: request.message });
146 | });
147 | }
148 | }
149 | });
--------------------------------------------------------------------------------
/public/chromestore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inscrib3/inspip/3671b68eb56adc743fe3f4498b3306932b5340da/public/chromestore.png
--------------------------------------------------------------------------------
/public/content.js:
--------------------------------------------------------------------------------
1 | var scriptElement = document.createElement('script');
2 |
3 | // eslint-disable-next-line no-undef
4 | scriptElement.src = chrome.runtime.getURL('inject.js');
5 |
6 | document.head.appendChild(scriptElement);
7 |
8 | window.addEventListener('SendBitcoin', function (event) {
9 | // eslint-disable-next-line no-undef
10 | chrome.runtime.sendMessage(
11 | {
12 | message: {action:'SendBitcoin',params:event.detail},
13 | },
14 | function () {})
15 | })
16 |
17 | window.addEventListener('SendTokens', function (event) {
18 | // eslint-disable-next-line no-undef
19 | chrome.runtime.sendMessage(
20 | {
21 | message: {action:'SendTokens',params:event.detail},
22 | },
23 | function () {})
24 | })
25 |
26 | window.addEventListener('SignPsbt', function (event) {
27 | // eslint-disable-next-line no-undef
28 | chrome.runtime.sendMessage(
29 | {
30 | message: {action:'SignPsbt',params:event.detail},
31 | },
32 | function () {})
33 | })
34 |
35 | window.addEventListener('SignMessage', function (event) {
36 | // eslint-disable-next-line no-undef
37 | chrome.runtime.sendMessage(
38 | {
39 | message: {action:'SignMessage',params:event.detail},
40 | },
41 | function () {})
42 | })
43 |
44 | window.addEventListener('ConnectWallet', function (event) {
45 | // eslint-disable-next-line no-undef
46 | chrome.runtime.sendMessage(
47 | {
48 | message: {action:'ConnectWallet',params:event.detail},
49 | },
50 | function () {})
51 | })
52 |
53 | window.addEventListener('VerifySign', function (event) {
54 | // eslint-disable-next-line no-undef
55 | chrome.runtime.sendMessage(
56 | {
57 | message: {action:'VerifySign',params:event.detail},
58 | },
59 | function () {})
60 | })
61 |
62 | // eslint-disable-next-line no-undef
63 | chrome.runtime.onMessage.addListener(
64 | function(request) {
65 | // Handle the message from background.js
66 | if(request.message.includes("ReturnConnectWalletInfo")){
67 | const customEvent = new CustomEvent("ReturnConnectWalletInfo", {
68 | detail: { message: request.message }
69 | });
70 | window.dispatchEvent(customEvent);
71 | }
72 | }
73 | );
74 |
75 | // eslint-disable-next-line no-undef
76 | chrome.runtime.onMessage.addListener(
77 | function(request) {
78 | // Handle the message from background.js
79 | if(request.message.includes("ClientRejectConnectWalletInfo")){
80 | const customEvent = new CustomEvent("ClientRejectConnectWalletInfo", {
81 | detail: { message: request.message }
82 | });
83 | window.dispatchEvent(customEvent);
84 | }
85 | }
86 | );
87 |
88 | // eslint-disable-next-line no-undef
89 | chrome.runtime.onMessage.addListener(
90 | function(request) {
91 | if(request.message.includes("ReturnSendBitcoin")){
92 | const customEvent = new CustomEvent("ReturnSendBitcoin", {
93 | detail: { message: request.message }
94 | });
95 | window.dispatchEvent(customEvent);
96 | }
97 | }
98 | );
99 |
100 | // eslint-disable-next-line no-undef
101 | chrome.runtime.onMessage.addListener(
102 | function(request) {
103 | if(request.message.includes("ReturnSendTokens")){
104 | const customEvent = new CustomEvent("ReturnSendTokens", {
105 | detail: { message: request.message }
106 | });
107 | window.dispatchEvent(customEvent);
108 | }
109 | }
110 | );
111 |
112 | // eslint-disable-next-line no-undef
113 | chrome.runtime.onMessage.addListener(
114 | function(request) {
115 | if(request.message.includes("ReturnSignPsbt")){
116 | const customEvent = new CustomEvent("ReturnSignPsbt", {
117 | detail: { message: request.message }
118 | });
119 | window.dispatchEvent(customEvent);
120 | }
121 | }
122 | );
123 |
124 | // eslint-disable-next-line no-undef
125 | chrome.runtime.onMessage.addListener(
126 | function(request) {
127 | if(request.message.includes("ReturnErrorOnSignPsbt")){
128 | const customEvent = new CustomEvent("ReturnErrorOnSignPsbt", {
129 | detail: { message: request.message }
130 | });
131 | window.dispatchEvent(customEvent);
132 | }
133 | }
134 | );
135 |
136 | // eslint-disable-next-line no-undef
137 | chrome.runtime.onMessage.addListener(
138 | function(request) {
139 | if(request.message.includes("ClientRejectSignPsbt")){
140 | const customEvent = new CustomEvent("ClientRejectSignPsbt", {
141 | detail: { message: request.message }
142 | });
143 | window.dispatchEvent(customEvent);
144 | }
145 | }
146 | );
147 |
148 | // eslint-disable-next-line no-undef
149 | chrome.runtime.onMessage.addListener(
150 | function(request) {
151 | if(request.message.includes("ReturnSignMessage")){
152 | const customEvent = new CustomEvent("ReturnSignMessage", {
153 | detail: { message: request.message }
154 | });
155 | window.dispatchEvent(customEvent);
156 | }
157 | }
158 | );
159 |
160 | // eslint-disable-next-line no-undef
161 | chrome.runtime.onMessage.addListener(
162 | function(request) {
163 | if(request.message.includes("ReturnErrorOnSignMessage")){
164 | const customEvent = new CustomEvent("ReturnErrorOnSignMessage", {
165 | detail: { message: request.message }
166 | });
167 | window.dispatchEvent(customEvent);
168 | }
169 | }
170 | );
171 |
172 | // eslint-disable-next-line no-undef
173 | chrome.runtime.onMessage.addListener(
174 | function(request) {
175 | if(request.message.includes("ClientRejectSignMessage")){
176 | const customEvent = new CustomEvent("ClientRejectSignMessage", {
177 | detail: { message: request.message }
178 | });
179 | window.dispatchEvent(customEvent);
180 | }
181 | }
182 | );
183 |
184 | // eslint-disable-next-line no-undef
185 | chrome.runtime.onMessage.addListener(
186 | function(request) {
187 | if(request.message.includes("ReturnVerifySign")){
188 | const customEvent = new CustomEvent("ReturnVerifySign", {
189 | detail: { message: request.message }
190 | });
191 | window.dispatchEvent(customEvent);
192 | }
193 | }
194 | );
195 |
196 | // eslint-disable-next-line no-undef
197 | chrome.runtime.onMessage.addListener(
198 | function(request) {
199 | if(request.message.includes("ReturnErrorOnVerifySign")){
200 | const customEvent = new CustomEvent("ReturnErrorOnVerifySign", {
201 | detail: { message: request.message }
202 | });
203 | window.dispatchEvent(customEvent);
204 | }
205 | }
206 | );
--------------------------------------------------------------------------------
/public/inject.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | window.inspip = window.inspip || {};
3 |
4 | window.inspip.connect = function () {
5 | const event = new CustomEvent("ConnectWallet");
6 | window.dispatchEvent(event);
7 | return new Promise((resolve,reject) => {
8 | const eventListener = (event) => {
9 | if (event.type === "ReturnConnectWalletInfo") {
10 | window.removeEventListener("ReturnConnectWalletInfo", eventListener);
11 | const address = event.detail.message.split(';')[1];
12 | const pubkey = event.detail.message.split(';')[2];
13 | resolve({address,pubkey});
14 | }
15 | if (event.type === "ClientRejectConnectWalletInfo") {
16 | window.removeEventListener("ClientRejectConnectWalletInfo", eventListener);
17 | reject(new Error("ConnectWallet rejected by client"));
18 | }
19 | };
20 |
21 | window.addEventListener("ReturnConnectWalletInfo", eventListener);
22 | window.addEventListener("ClientRejectConnectWalletInfo", eventListener);
23 | });
24 | };
25 |
26 | window.inspip.sendBitcoin = function (toAddress, satoshi, feerate) {
27 | const message = {toAddress,satoshi:satoshi.toString(),feerate:feerate.toString()};
28 | const event = new CustomEvent("SendBitcoin", {detail: message});
29 | window.dispatchEvent(event);
30 |
31 | return new Promise((resolve) => {
32 | const eventListener = (event) => {
33 | if (event.type === "ReturnSendBitcoin") {
34 | window.removeEventListener("ReturnSendBitcoin", eventListener);
35 | const txId = event.detail.message.split(";")[1];
36 | resolve(txId);
37 | }
38 | };
39 |
40 | window.addEventListener("ReturnSendBitcoin", eventListener);
41 | });
42 | };
43 |
44 | window.inspip.sendTokens = function (ticker, id, toAddress, amount, feerate) {
45 | const message = {ticker, id, toAddress, amount:amount.toString(), feerate:feerate.toString()};
46 | const event = new CustomEvent("SendTokens", {detail: message});
47 | window.dispatchEvent(event);
48 |
49 | return new Promise((resolve) => {
50 | const eventListener = (event) => {
51 | if (event.type === "ReturnSendTokens") {
52 | window.removeEventListener("ReturnSendTokens", eventListener);
53 | const txId = event.detail.message.split(";")[1];
54 | resolve(txId);
55 | }
56 | };
57 |
58 | window.addEventListener("ReturnSendTokens", eventListener);
59 | });
60 | };
61 |
62 | window.inspip.signPsbt = function (psbt, toSignInputs, autoFinalized) {
63 | const message = {psbt, toSignInputs:JSON.stringify(toSignInputs), autoFinalized:autoFinalized.toString()};
64 | const event = new CustomEvent("SignPsbt", {detail: message});
65 | window.dispatchEvent(event);
66 |
67 | return new Promise((resolve,reject) => {
68 | const eventListener = (event) => {
69 | if (event.type === "ReturnSignPsbt") {
70 | window.removeEventListener("ReturnSignPsbt", eventListener);
71 | const signed = event.detail.message.split(';')[1];
72 | resolve(signed);
73 | }
74 | if (event.type === "ReturnErrorOnSignPsbt") {
75 | window.removeEventListener("ReturnErrorOnSignPsbt", eventListener);
76 | const error = event.detail.message.split(';')[1];
77 | reject(new Error(error));
78 | }
79 | if (event.type === "ClientRejectSignPsbt") {
80 | window.removeEventListener("ClientRejectSignPsbt", eventListener);
81 | reject(new Error("SignPsbt rejected by client"));
82 | }
83 | };
84 |
85 | window.addEventListener("ReturnSignPsbt", eventListener);
86 | window.addEventListener("ReturnErrorOnSignPsbt", eventListener);
87 | window.addEventListener("ClientRejectSignPsbt", eventListener);
88 | });
89 | };
90 |
91 | window.inspip.signMessage = function (msg) {
92 | const message = {msg};
93 | const event = new CustomEvent("SignMessage", {detail: message});
94 | window.dispatchEvent(event);
95 |
96 | return new Promise((resolve,reject) => {
97 | const eventListener = (event) => {
98 | if (event.type === "ReturnSignMessage") {
99 | window.removeEventListener("ReturnSignMessage", eventListener);
100 | const signature = event.detail.message.split(';')[1];
101 | resolve(signature);
102 | }
103 | if (event.type === "ReturnErrorOnSignMessage") {
104 | window.removeEventListener("ReturnErrorOnSignMessage", eventListener);
105 | reject(new Error("Error on SignMessage event occurred."));
106 | }
107 | if (event.type === "ClientRejectSignMessage") {
108 | window.removeEventListener("ClientRejectSignMessage", eventListener);
109 | reject(new Error("SignMessage rejected by client"));
110 | }
111 | };
112 |
113 | window.addEventListener("ReturnSignMessage", eventListener);
114 | window.addEventListener("ReturnErrorOnSignMessage", eventListener);
115 | window.addEventListener("ClientRejectSignMessage", eventListener);
116 | });
117 | };
118 |
119 | window.inspip.verifySign = function (msg, signature) {
120 | const message = {msg, signature};
121 | const event = new CustomEvent("VerifySign", {detail: message});
122 | window.dispatchEvent(event);
123 |
124 | return new Promise((resolve,reject) => {
125 | const eventListener = (event) => {
126 | if (event.type === "ReturnVerifySign") {
127 | window.removeEventListener("ReturnVerifySign", eventListener);
128 | const result = event.detail.message.split(';')[1] === "true" ? true : false;
129 | resolve(result);
130 | }
131 | if (event.type === "ReturnErrorOnVerifySign") {
132 | window.removeEventListener("ReturnErrorOnVerifySign", eventListener);
133 | reject(new Error("Error on VerifySign event occurred."));
134 | }
135 | };
136 |
137 | window.addEventListener("ReturnVerifySign", eventListener);
138 | window.addEventListener("ReturnErrorOnVerifySign", eventListener);
139 | });
140 | };
141 | })();
142 |
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inscrib3/inspip/3671b68eb56adc743fe3f4498b3306932b5340da/public/logo.png
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "Inspip",
4 | "permissions": [
5 | "activeTab"
6 | ],
7 | "content_scripts": [
8 | {
9 | "run_at": "document_end",
10 | "matches": ["http://*/*", "https://*/*"],
11 | "js": ["content.js"]
12 | }
13 | ],
14 | "web_accessible_resources": [
15 | {
16 | "resources": ["inject.js"],
17 | "matches": [""]
18 | }
19 | ],
20 | "background": {
21 | "service_worker": "background.js",
22 | "type": "module"
23 | },
24 | "description": "Inspip | Pipe Wallet Extension",
25 | "version": "0.1.12",
26 | "action": {
27 | "default_popup": "index.html",
28 | "default_icon": "logo.png"
29 | }
30 | }
--------------------------------------------------------------------------------
/src/app/app-context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useCallback, useEffect, useRef, useState } from 'react';
2 | import { editWallet, updateStoredWallet } from '../bitcoin/wallet-storage';
3 |
4 | export type Utxo = {
5 | protocol?: "pipe" | "ordinals";
6 | txid: string,
7 | hex?: string,
8 | status: {
9 | confirmed: boolean,
10 | },
11 | vout: number,
12 | value: number,
13 | tick?: string,
14 | id?: number,
15 | dec?: number,
16 | amt?: string,
17 | };
18 |
19 | export const AppContext = createContext<{
20 | network: string,
21 | setNetwork: (network: string) => void,
22 | account: any,
23 | setAccount: (account: any) => void,
24 | currentAddress: string,
25 | setCurrentAddress: (address: string, index: number) => void,
26 | addresses: number[],
27 | setAddresses: (addresses: number[]) => void,
28 | loading: boolean,
29 | feerate: number,
30 | setFeerate: (feerate: number) => void,
31 | tokens: { tick: string, id: number, dec: number }[],
32 | setTokens: (tokens: { tick: string, id: number, dec: number }[]) => void,
33 | fetchUtxos: () => Promise,
34 | signPsbt: any,
35 | setSignPsbt: any,
36 | signMessage: any,
37 | setSignMessage: any,
38 | verifySign: any,
39 | setVerifySign: any
40 | }>({
41 | account: {},
42 | loading: false,
43 | setAccount: () => undefined,
44 | network: 'mainnet',
45 | setNetwork: () => undefined,
46 | currentAddress: '',
47 | setCurrentAddress: () => undefined,
48 | addresses: [],
49 | setAddresses: () => undefined,
50 | feerate: 0,
51 | setFeerate: () => undefined,
52 | tokens: [],
53 | setTokens: () => undefined,
54 | fetchUtxos: async () => [],
55 | signPsbt: {},
56 | setSignPsbt: () => undefined,
57 | signMessage: {},
58 | setSignMessage: () => undefined,
59 | verifySign: {},
60 | setVerifySign: () => undefined,
61 | });
62 |
63 | export type Tx = {
64 | txid: string;
65 | vin: {
66 | txid: string;
67 | vout: number;
68 | }[];
69 | vout: {
70 | index: number;
71 | scriptpubkey_address: string;
72 | value: number;
73 | }[];
74 | status?: {
75 | confirmed: boolean;
76 | }
77 | };
78 |
79 | export type TxsStorage = {
80 | last_txid: string;
81 | data: {
82 | [txid: string]: Tx;
83 | };
84 | };
85 |
86 | export interface IndexerToken {
87 | beneficiaryAddress: string;
88 | block: number;
89 | bvo: number;
90 | collectionAddress: string;
91 | collectionNumber: number;
92 | createdAt: string;
93 | decimals: number;
94 | id: number;
95 | limit: number;
96 | maxSupply: number;
97 | pid: number;
98 | amount?: number;
99 | remaining: number;
100 | ticker: string;
101 | txId: string;
102 | updatedAt: string;
103 | vo: number;
104 | vout: number;
105 | metadata?: string;
106 | mime?: string;
107 | ref?: string;
108 | traits?: string[];
109 | }
110 |
111 | export interface AppProviderProps {
112 | children: React.ReactNode;
113 | }
114 |
115 | // Import the functions you need from the SDKs you need
116 | import { initializeApp, FirebaseApp } from "firebase/app";
117 | import { getAnalytics, Analytics } from "firebase/analytics";
118 | import { selectAllOrdinalsUnspents } from '../transfer/select-all-ordinals-unspents';
119 | import { selectAllPipeUnspents } from '../transfer/select-all-pipe-unspents';
120 | // TODO: Add SDKs for Firebase products that you want to use
121 | // https://firebase.google.com/docs/web/setup#available-libraries
122 |
123 | // Your web app's Firebase configuration
124 | // For Firebase JS SDK v7.20.0 and later, measurementId is optional
125 | const firebaseConfig = {
126 | apiKey: "AIzaSyBC77tDyyuddrsc_Jdm0BcsKNoFl5BXWOQ",
127 | authDomain: "inspip-wallet.firebaseapp.com",
128 | projectId: "inspip-wallet",
129 | storageBucket: "inspip-wallet.appspot.com",
130 | messagingSenderId: "734952055419",
131 | appId: "1:734952055419:web:a5cdb017c0d7423c99f8de",
132 | measurementId: "G-523R85SP1N"
133 | };
134 |
135 | export const AppProvider = (props: AppProviderProps) => {
136 | const [account, setAccount] = useState({});
137 | const [network, _setNetwork] = useState('mainnet');
138 | const [addresses, _setAddresses] = useState([]);
139 | const [currentAddress, _setCurrentAddress] = useState('');
140 | const [feerate, setFeerate] = useState(0);
141 | const [tokens, setTokens] = useState<{ tick: string, id: number, dec: number }[]>([]);
142 |
143 | const [signPsbt, setSignPsbt] = useState({});
144 | const [signMessage, setSignMessage] = useState({});
145 | const [verifySign, setVerifySign] = useState({});
146 | const loading = useRef(false);
147 | const [firebase, setFirebase] = useState(null);
148 | const [, setAnalytics] = useState(null);
149 |
150 | useEffect(() => {
151 | if (firebase) return;
152 | const nextFirebase = initializeApp(firebaseConfig);
153 | setAnalytics(getAnalytics(nextFirebase));
154 | setFirebase(nextFirebase);
155 | }, [firebase]);
156 |
157 | interface TickerData {
158 | tick: string;
159 | id: number;
160 | dec: number;
161 | }
162 |
163 | const getUniqueTickers = (data: any) : TickerData[] =>{
164 | const uniqueTickers = data.reduce((acc:any, curr:any) => {
165 | const exists = acc.find((item:{
166 | tick: string,
167 | id: number,
168 | dec: number,
169 | }) => item.tick === curr.tick && item.id === curr.id && item.dec === curr.dec);
170 | if (!exists) {
171 | acc.push(curr);
172 | }
173 | return acc;
174 | }, []);
175 | return uniqueTickers;
176 | }
177 |
178 | const fetchUtxos = useCallback(async (): Promise => {
179 | if (loading.current || currentAddress === '') return [];
180 |
181 | loading.current = true;
182 |
183 | const pipeUnspents = await selectAllPipeUnspents({
184 | network: network as "mainnet" | "testnet",
185 | address: currentAddress,
186 | });
187 |
188 | const pipeUnspentFormatted = pipeUnspents.map((pipeUnspent) => {
189 | return {
190 | protocol: "pipe",
191 | tick: pipeUnspent.ticker,
192 | id: pipeUnspent.id,
193 | dec: pipeUnspent.decimals,
194 | amt: pipeUnspent.amount,
195 | status: {
196 | confirmed: true,
197 | },
198 | };
199 | });
200 |
201 | const uniqueTickers: TickerData[] = getUniqueTickers(pipeUnspentFormatted);
202 | setTokens(uniqueTickers);
203 |
204 | const ordinalsUnspents = await selectAllOrdinalsUnspents({
205 | network: network as "mainnet" | "testnet",
206 | address: currentAddress,
207 | });
208 |
209 | const ordinalsUnspentFormatted = ordinalsUnspents.map(() => {
210 | return {
211 | protocol: "ordinals",
212 | status: {
213 | confirmed: true,
214 | },
215 | };
216 | });
217 |
218 | loading.current = false;
219 | return [...pipeUnspentFormatted,...ordinalsUnspentFormatted];
220 | }, [currentAddress, network]);
221 |
222 | const setAddresses = (addresses: number[]) => {
223 | _setAddresses(addresses);
224 | editWallet('', addresses);
225 | };
226 |
227 | const setCurrentAddress = (address: string, index: number) => {
228 | _setCurrentAddress(address);
229 | editWallet(address, [], index);
230 | };
231 |
232 | const setNetwork = (network: string) => {
233 | _setNetwork(network);
234 | updateStoredWallet('network', network);
235 | };
236 |
237 | return (
238 |
262 | {props.children}
263 |
264 | );
265 | }
266 |
--------------------------------------------------------------------------------
/src/app/app.hook.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { AppContext } from "./app-context";
3 |
4 | export const useApp = () => useContext(AppContext);
--------------------------------------------------------------------------------
/src/app/app.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Image, Text } from "grommet";
2 | import { useNavigate } from "react-router-dom";
3 | import { RoutePath } from "../router";
4 | import { useEffect } from "react";
5 | import { Layout } from "../components";
6 | import { useSearchParams } from "react-router-dom";
7 | import { useApp } from ".";
8 |
9 | export const App = () => {
10 | const navigate = useNavigate();
11 | const app = useApp();
12 | const [searchParams] = useSearchParams();
13 | const params = {
14 | ticker: searchParams.get("ticker"),
15 | id: searchParams.get("id"),
16 | toAddress: searchParams.get("toAddress"),
17 | satoshi: searchParams.get("satoshi") || searchParams.get("amount"),
18 | feerate: searchParams.get("feerate"),
19 | };
20 |
21 | useEffect(()=>{
22 | if (searchParams.get("action") === "SignPsbt") {
23 | app.setSignPsbt({
24 | psbt:(searchParams.get("psbt") || "").replace(/\s/g,'+'),
25 | toSignInputs:(JSON.parse(searchParams.get("toSignInputs") || "")),
26 | autoFinalized:searchParams.get("autoFinalized") === 'true' ? true : false,
27 | })
28 | }
29 | if (searchParams.get("action") === "SignMessage") {
30 | app.setSignMessage({
31 | msg:searchParams.get("msg"),
32 | type:searchParams.get("type"),
33 | })
34 | }
35 | if (searchParams.get("action") === "VerifySign") {
36 | app.setVerifySign({
37 | msg:searchParams.get("msg"),
38 | signature:(searchParams.get("signature") || "").replace(/\s/g,'+'),
39 | })
40 | }
41 | },[app, searchParams])
42 |
43 | const createWallet = () => navigate(RoutePath.CreateWallet);
44 | const restoreWallet = () => navigate(RoutePath.RestoreWallet);
45 | const connectWallet = () => navigate(RoutePath.ConnectWallet);
46 | const send = (data: any) => navigate(RoutePath.Send, { state: data });
47 |
48 | useEffect(() => {
49 | if (searchParams.get("toAddress")) {
50 | setTimeout(() => {
51 | send(params);
52 | }, 500);
53 | }
54 | if (
55 | searchParams.get("action") &&
56 | searchParams.get("action") === "ConnectWallet"
57 | ) {
58 | setTimeout(() => {
59 | connectWallet();
60 | }, 500);
61 | }
62 | // eslint-disable-next-line react-hooks/exhaustive-deps
63 | }, [searchParams]);
64 |
65 | useEffect(() => {
66 | if (localStorage.getItem("wallet")) {
67 | navigate(RoutePath.Password);
68 | }
69 | }, [navigate]);
70 |
71 | return (
72 |
73 |
74 |
75 |
76 |
77 |
78 | Inspip | Pipe Wallet
79 |
80 |
81 |
82 | Create & Store your Pipe DMT and ART in the world's first Open
83 | Source Chrome wallet for Pipe!
84 |
85 |
86 |
87 |
88 |
89 | {window.origin.includes('http') && (
90 | window.open('https://chromewebstore.google.com/detail/inspip/hgbnkbbibgkjkbgbaicdajneaponhnge', '_blank')}>
91 |
92 |
93 | )}
94 |
95 |
96 |
97 | );
98 | };
99 |
--------------------------------------------------------------------------------
/src/app/index.ts:
--------------------------------------------------------------------------------
1 | export * from './app'
2 | export * from './app-context'
3 | export * from './app.hook'
4 |
--------------------------------------------------------------------------------
/src/app/settings.ts:
--------------------------------------------------------------------------------
1 | import { decrypt, encrypt } from "../utils/crypto";
2 | import { generateFingerprint } from "../utils/fingerprint";
3 |
4 | type Settings = {
5 | password: string;
6 | ttl: number;
7 | language: string;
8 | lastUpdate: number;
9 | };
10 |
11 | const save = (settings: Settings) => {
12 | const fingerprint = generateFingerprint();
13 | if(settings.password.length > 0) settings.password = encrypt(settings.password, fingerprint);
14 | localStorage.setItem("settings", JSON.stringify(settings));
15 | };
16 |
17 | const create = (password: string) => {
18 | const currentSettings: Settings = {
19 | password,
20 | ttl: 1000 * 60 * 60 * 3, // 3 hours
21 | language: navigator.language,
22 | lastUpdate: Date.now(),
23 | };
24 |
25 | save(currentSettings);
26 | }
27 |
28 | const get = (): Settings | null => {
29 | const settings = JSON.parse(localStorage.getItem("settings") || "null");
30 | if (settings && settings.lastUpdate + settings.ttl < Date.now()) {
31 | settings.password = '';
32 | settings.lastUpdate = Date.now();
33 | save(settings);
34 | } else if (settings && settings.password) {
35 | const fingerprint = generateFingerprint();
36 | settings.password = decrypt(settings.password, fingerprint);
37 | }
38 |
39 | return settings;
40 | };
41 |
42 | export const getPasswordFromSettings = () => {
43 | const settings = get();
44 | if (!settings?.language) create('');
45 | return settings?.password || '';
46 | }
47 |
48 | export const savePasswordInSettings = (password: string) => {
49 | const settings = get();
50 | if (!settings?.language) {
51 | create(password);
52 | } else {
53 | settings.password = password;
54 | save(settings);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/bitcoin/fees.ts:
--------------------------------------------------------------------------------
1 | export function estimateFee(vin: bigint, vout: bigint, rate: bigint) {
2 | return (102n + vin * 112n + vout * 33n) * rate;
3 | }
4 |
--------------------------------------------------------------------------------
/src/bitcoin/helpers.ts:
--------------------------------------------------------------------------------
1 | import { Buffer } from 'buffer';
2 | import { bitcoin } from './lib/bitcoin-lib';
3 | import { Address } from '@cmdcode/tapscript';
4 | import { cleanFloat } from '../utils/clean-float';
5 |
6 | export const toXOnly = (pubKey: Buffer) => (pubKey.length === 32 ? pubKey : pubKey.slice(1, 33));
7 |
8 | export function getNetwork(network: string) {
9 | if(network === '' || network === 'mainnet') return bitcoin.networks.bitcoin;
10 | else if(network === 'testnet') return bitcoin.networks.testnet;
11 | else throw new Error('Invalid network');
12 | }
13 |
14 | export function stringFromBigInt(_amount: string, mantissaDecimalPoints: number) {
15 | const amount = cleanFloat(_amount);
16 | if (/[^0-9.]/.test(amount)) {
17 | throw new Error("Invalid character in amount");
18 | }
19 |
20 | const [, decimalPart = ''] = amount.split('.');
21 | const decimalPartLength = decimalPart.length;
22 | if (decimalPartLength > mantissaDecimalPoints) {
23 | throw new Error(`Amount exceeds ${mantissaDecimalPoints} decimals`);
24 | }
25 | const amountBigInt = BigInt(amount.replace('.', ''));
26 | const mantissa = BigInt(10**mantissaDecimalPoints);
27 | const decimals = BigInt(10**decimalPartLength);
28 | const result = amountBigInt * mantissa / decimals;
29 |
30 | return result;
31 | }
32 |
33 | export function parseStringToBigInt(amount: string, mantissaDecimalPoints: number) {
34 | if (/[^0-9.]/.test(amount)) {
35 | throw new Error("Invalid character in amount");
36 | }
37 |
38 | const [, decimalPart = ''] = amount.split('.');
39 | const decimalPartLength = decimalPart.length;
40 | if (decimalPartLength > mantissaDecimalPoints) {
41 | throw new Error(`Amount exceeds ${mantissaDecimalPoints} decimal`);
42 | }
43 | const amountBigInt = BigInt(amount.replace('.', ''));
44 | const mantissa = BigInt(10**mantissaDecimalPoints);
45 | const decimals = BigInt(10**decimalPartLength);
46 | const result = amountBigInt * mantissa / decimals;
47 |
48 | return result;
49 | }
50 |
51 | export function bigIntToString(valueBigInt: bigint, decimalPlaces: number) {
52 | const factor = BigInt(10 ** decimalPlaces);
53 | const wholePart = valueBigInt / factor;
54 | const decimalPart = valueBigInt % factor;
55 | const paddedDecimalPart = decimalPart.toString().padStart(decimalPlaces, '0');
56 |
57 | return `${wholePart}.${paddedDecimalPart}`;
58 | }
59 |
60 | export function addressToScriptPubKey(address: string, _network: "mainnet" | "testnet") {
61 | let _toAddress, _script;
62 |
63 | const network = _network === "testnet" ? bitcoin.networks.testnet : bitcoin.networks.bitcoin;
64 |
65 | if (address.startsWith('tb1q') || address.startsWith('bc1q')) {
66 | _toAddress = bitcoin.address.fromBech32(address);
67 | _script = bitcoin.payments.p2wpkh({ address, network }).output;
68 | } else if (address.startsWith('1') || address.startsWith('m') || address.startsWith('n')) {
69 | _toAddress = bitcoin.address.fromBase58Check(address);
70 | _script = bitcoin.payments.p2pkh({ address, network }).output;
71 | } else if (address.startsWith('3') || address.startsWith('2')) {
72 | _toAddress = bitcoin.address.fromBase58Check(address);
73 | _script = bitcoin.payments.p2sh({ address, network }).output;
74 | } else {
75 | _toAddress = Address.p2tr.decode(address).hex;
76 | _script = [ 'OP_1', _toAddress ];
77 | }
78 |
79 | return _script;
80 | }
81 |
82 | export function validateAddress(address: string, network: any): boolean {
83 | try {
84 | return bitcoin.address.toOutputScript(address, network);
85 | } catch(e) {
86 | return false;
87 | }
88 | }
89 |
90 | function charRange(start: string, stop: string) {
91 | const result: any = [];
92 |
93 | // get all chars from starting char
94 | // to ending char
95 | let i = start.charCodeAt(0);
96 | const last = stop.charCodeAt(0) + 1;
97 | for (i; i < last; i++) {
98 | result.push(String.fromCharCode(i));
99 | }
100 |
101 | return result;
102 | }
103 |
104 | export function toInt26(str: string) {
105 | const alpha = charRange('a', 'z');
106 | let result = 0n;
107 |
108 | // make sure we have a usable string
109 | str = str.toLowerCase();
110 | str = str.replace(/[^a-z]/g, '');
111 |
112 | // we're incrementing j and decrementing i
113 | let j = 0n;
114 | for (let i = str.length - 1; i > -1; i--) {
115 | // get letters in reverse
116 | const char = str[i];
117 |
118 | // get index in alpha and compensate for
119 | // 0 based array
120 | let position = BigInt(''+alpha.indexOf(char));
121 | position++;
122 |
123 | // the power kinda like the 10's or 100's
124 | // etc... position of the letter
125 | // when j is 0 it's 1s
126 | // when j is 1 it's 10s
127 | // etc...
128 | const pow = (base: bigint, exponent: bigint) => base ** exponent;
129 |
130 | const power = pow(26n, j)
131 |
132 | // add the power and index to result
133 | result += power * position;
134 | j++;
135 | }
136 |
137 | return result;
138 | }
139 |
140 | function bitLength(number: bigint) {
141 | if (typeof number !== 'bigint') {
142 | throw new Error("Input must be a BigInt");
143 | }
144 | return number === 0n ? 0 : number.toString(2).length;
145 | }
146 |
147 | function byteLength(number: bigint) {
148 | if (typeof number !== 'bigint') {
149 | throw new Error("Input must be a BigInt");
150 | }
151 | return Math.ceil(bitLength(number) / 8);
152 | }
153 |
154 | export function toBytes(number: bigint) {
155 | if (typeof number !== 'bigint') {
156 | throw new Error("Input must be a BigInt");
157 | }
158 |
159 | if (number < 0n) {
160 | throw new Error("BigInt must be non-negative");
161 | }
162 |
163 | if (number === 0n) {
164 | return new Uint8Array().buffer;
165 | }
166 |
167 | const size = byteLength(number);
168 | const bytes = new Uint8Array(size);
169 | let x = number;
170 | for (let i = size - 1; i >= 0; i--) {
171 | bytes[i] = Number(x & 0xFFn);
172 | x >>= 8n;
173 | }
174 |
175 | return bytes.buffer;
176 | }
177 |
178 | export function textToHex(text: string) {
179 | const encoder = new TextEncoder().encode(text);
180 | return [...new Uint8Array(encoder)]
181 | .map(x => x.toString(16).padStart(2, "0"))
182 | .join("");
183 | }
184 |
--------------------------------------------------------------------------------
/src/bitcoin/lib/bitcoin-lib.ts:
--------------------------------------------------------------------------------
1 | //const _bitcoin = (window as any).bitcoin;
2 | import * as _bitcoin from 'bitcoinjs-lib';
3 |
4 | interface Bitcoin {
5 | address: any;
6 | networks: {
7 | bitcoin: any;
8 | testnet: any;
9 | };
10 | payments: {
11 | p2tr: (options: any) => { address: string, output: any };
12 | p2wpkh: (options: any) => { address: string, output: any };
13 | p2pkh: (options: any) => { address: string, output: any };
14 | p2sh: (options: any) => { address: string, output: any };
15 | };
16 | ECPair: {
17 | fromWIF: (key: string, network: any) => any;
18 | };
19 | Psbt: any;
20 | script: {
21 | fromASM: (asm: string) => any;
22 | };
23 | crypto: any;
24 | initEccLib: (ecc: any) => void;
25 | }
26 | export const bitcoin: Bitcoin = _bitcoin as unknown as Bitcoin;
27 |
28 | export interface Utxo {
29 | txid: string;
30 | value: number;
31 | hex: string;
32 | status: any;
33 | vout: number;
34 | }
35 |
--------------------------------------------------------------------------------
/src/bitcoin/node.ts:
--------------------------------------------------------------------------------
1 | import { Utxo } from "./lib/bitcoin-lib";
2 |
3 | export async function fetchUtxos(address: string, network: string): Promise {
4 | if(address.length === 0) throw new Error('Invalid address provided');
5 |
6 | const response = await fetch(
7 | `https://mempool.space/${network === 'testnet' ? 'testnet/' : ''}api/address/${address}/utxo`
8 | );
9 | let utxos: Utxo[] = await response.json();
10 |
11 | utxos = await Promise.all(
12 | utxos.map(async (utxo) => {
13 | const hex = await fetchHex(utxo.txid, network);
14 | utxo.hex = hex;
15 |
16 | return utxo;
17 | })
18 | );
19 |
20 | utxos = utxos.sort((a, b) => {
21 | return b.value - a.value;
22 | });
23 |
24 | return utxos;
25 | }
26 |
27 | export async function fetchHex(txid: any, network: string) {
28 | const response = await fetch(`https://mempool.space/${network === 'testnet' ? 'testnet/' : ''}api/tx/${txid}/hex`);
29 | return await response.text();
30 | }
31 |
32 | export async function sendTransaction(hexstring: any, network: string) {
33 | try {
34 | const response = await fetch(`https://mempool.space/${network === 'testnet' ? 'testnet/' : ''}api/tx`, {
35 | method: "POST",
36 | body: hexstring,
37 | });
38 | if (!response.ok) {
39 | const error = await response.text();
40 | const _error = new Error(error);
41 | throw new Error(`Something went wrong: '${_error.message}'`);
42 | }
43 | const result = await response.text();
44 | return result;
45 | } catch (error) {
46 | console.error("Error:", error);
47 | throw error;
48 | }
49 | }
50 |
51 | export type FeesResponse = {
52 | fastestFee: number;
53 | halfHourFee: number;
54 | hourFee: number;
55 | economyFee: number;
56 | minimumFee: number;
57 | };
58 |
59 | export const getFees = async () => {
60 | const response = await fetch("https://mempool.space/api/v1/fees/recommended");
61 | const data: FeesResponse = await response.json();
62 | return {
63 | ...data,
64 | economyFee: data.economyFee <= 2 ? 3 : data.economyFee,
65 | halfHourFee: data.halfHourFee <= 2 ? 3 : data.halfHourFee,
66 | };
67 | };
68 |
--------------------------------------------------------------------------------
/src/bitcoin/wallet-storage.ts:
--------------------------------------------------------------------------------
1 | import { decrypt, encrypt } from '../utils/crypto';
2 |
3 | export function editWallet(currentAddress: string = '', addresses: number[] = [], addressIndex: number | undefined = undefined) {
4 | const data = localStorage.getItem('wallet');
5 | if (!data) throw new Error('Wallet not found');
6 |
7 | const parsedData = JSON.parse(data);
8 | if (!parsedData?.mnemonic) throw new Error('Wallet corrupted');
9 |
10 | if (currentAddress !== '') parsedData.currentAddress = currentAddress;
11 | if (addresses.length > 0) parsedData.addresses = addresses;
12 |
13 | if (addressIndex !== undefined && parsedData.addressIndex !== addressIndex) {
14 | parsedData.addressIndex = addressIndex;
15 | }
16 |
17 | localStorage.setItem('wallet', JSON.stringify(parsedData));
18 |
19 | return parsedData;
20 | }
21 |
22 | export const updateStoredWallet = (key: string, value: string) => {
23 | try {
24 | const wallet = localStorage.getItem('wallet');
25 | if (!wallet) return;
26 | const parsedWallet = JSON.parse(wallet);
27 | parsedWallet[key] = value;
28 | localStorage.setItem('wallet', JSON.stringify(parsedWallet));
29 | } catch (e) {
30 | console.error(e);
31 | }
32 | }
33 |
34 | export function saveWallet(mnemonic: string, network: string, currentAddress: string, addresses: number[], password: string) {
35 | const wallet = {
36 | mnemonic: encrypt(mnemonic, password),
37 | network,
38 | currentAddress,
39 | addresses,
40 | }
41 | localStorage.setItem('wallet', JSON.stringify(wallet));
42 | }
43 |
44 | export function loadWallet(password: string) {
45 | const data = localStorage.getItem('wallet');
46 | if (!data) throw new Error('Wallet not found');
47 |
48 | const parsedData = JSON.parse(data);
49 | if (!parsedData?.mnemonic) throw new Error('Wallet corrupted');
50 |
51 | try {
52 | parsedData.mnemonic = decrypt(parsedData.mnemonic, password);
53 | } catch (e) {
54 | throw new Error('Wrong password');
55 | }
56 |
57 | return parsedData;
58 | }
59 |
--------------------------------------------------------------------------------
/src/bitcoin/wallet.ts:
--------------------------------------------------------------------------------
1 | import BIP32Factory from 'bip32';
2 | import * as ecc from '@bitcoin-js/tiny-secp256k1-asmjs';
3 | import * as bip39 from "bip39";
4 | import ECPairFactory, { ECPairAPI, ECPairInterface } from 'ecpair';
5 | import { ScriptData, Signer, Tap, Tx, ValueData, Word } from '@cmdcode/tapscript';
6 | import { addressToScriptPubKey, bigIntToString, parseStringToBigInt, textToHex, toBytes, toInt26, toXOnly } from './helpers';
7 | import { bitcoin } from './lib/bitcoin-lib';
8 | import { Utxo } from '../app/app-context';
9 | import { cleanFloat } from '../utils/clean-float';
10 |
11 | export function generateWallet(network: any) {
12 | bip39.setDefaultWordlist('english');
13 | const mnemonic = bip39.generateMnemonic();
14 |
15 | return importWallet(mnemonic, network);
16 | }
17 |
18 | export function importWallet(mnemonic: string, network: any, index: number = 0) {
19 | bitcoin.initEccLib(ecc);
20 | const bip32 = BIP32Factory(ecc);
21 | const seed = bip39.mnemonicToSeedSync(mnemonic);
22 | const rootKey = bip32.fromSeed(seed, network);
23 |
24 | const data = generateNewAddress(rootKey, network, index);
25 | return { ...data, mnemonic, rootKey };
26 | }
27 |
28 | export function importWalletFromWif(wif: string, network: any, index: number = 0) {
29 | const ECPairInstance: ECPairAPI = ECPairFactory(ecc);
30 | const rootKey: ECPairInterface = ECPairInstance.fromWIF(wif, network);
31 |
32 | const data = generateNewAddress(rootKey, network, index);
33 | return { ...data, mnemonic: wif, rootKey };
34 | }
35 |
36 | export function generateNewAddress(rootKey: any, network: any, index: number = 0) {
37 | if(rootKey?.privateKey === undefined) throw new Error('Invalid private key');
38 |
39 | let path: string;
40 | if(network === bitcoin.networks.testnet) {
41 | path = `m/86'/0'/0'/0/${index}`;
42 | } else {
43 | path = `m/86'/0'/0'/0/${index}`;
44 | }
45 |
46 | let account;
47 | let internalPubkey;
48 | let address: string;
49 | let output;
50 |
51 | if (typeof rootKey.derivePath === 'function') {
52 | account = rootKey.derivePath(path);
53 | internalPubkey = toXOnly(account.publicKey);
54 | const payments = bitcoin.payments.p2tr({ internalPubkey: internalPubkey, network });
55 | address = payments.address;
56 | output = payments.output;
57 | } else {
58 | internalPubkey = toXOnly(rootKey.publicKey);
59 | bitcoin.initEccLib(ecc);
60 | const payments = bitcoin.payments.p2tr({ internalPubkey: internalPubkey, network });
61 | address = payments.address;
62 | output = payments.output;
63 | }
64 |
65 | return { rootKey, account: account ?? rootKey, internalPubkey, address, output, publickey: account ? account.publicKey.toString('hex') : rootKey.publicKey.toString('hex')}
66 | }
67 |
68 | export const sendTokens = (
69 | account: any,
70 | currentAddress: string,
71 | utxos: Utxo[],
72 | to: string,
73 | _ticker: string,
74 | _id: string,
75 | dec: number,
76 | _amount: string,
77 | _rate: string,
78 | network: any
79 | ): {
80 | hex: string;
81 | vin: any[];
82 | vout: any[];
83 | fee: string;
84 | ticker: string;
85 | id: string;
86 | amount: string;
87 | change: string;
88 | sats: string;
89 | sats_change: string;
90 | to: string;
91 | } => {
92 | const ticker = _ticker.trim().toLowerCase();
93 | const id = parseInt(_id.trim());
94 |
95 | const amount = parseStringToBigInt(_amount, dec);
96 | if(amount === 0n) throw new Error('Invalid rate');
97 |
98 | const rate = BigInt(_rate);
99 | if(rate < 2n) throw new Error('Invalid rate');
100 |
101 | let vin = [];
102 | let vout: {
103 | value?: ValueData | undefined;
104 | scriptPubKey?: ScriptData | undefined;
105 | }[] | undefined = [];
106 | let found = 0n;
107 | let sats_found = 0n;
108 | const sats_amount = 1092n;
109 |
110 | for (let i = 0; i < utxos.length; i++) {
111 | if (found >= amount) {
112 | break;
113 | }
114 |
115 | if (utxos[i].status.confirmed) {
116 | try {
117 | if (utxos[i].tick === ticker && utxos[i].id === id) {
118 | vin.push({
119 | txid: utxos[i].txid,
120 | vout: utxos[i].vout,
121 | prevout: {
122 | value: BigInt(utxos[i].value),
123 | scriptPubKey: addressToScriptPubKey(currentAddress, network)
124 | }
125 | });
126 |
127 | found += BigInt(utxos[i].amt || 0);
128 | }
129 | } catch (e) {
130 | console.error(e);
131 | }
132 | }
133 | }
134 |
135 | for (let i = 0; i < utxos.length; i++) {
136 | if (sats_found >= sats_amount + 5_000n) {
137 | break;
138 | }
139 |
140 | if (
141 | !utxos[i].tick &&
142 | utxos[i].status.confirmed &&
143 | utxos[i].value >= 600
144 | ) {
145 | vin.push({
146 | txid: utxos[i].txid,
147 | vout: utxos[i].vout,
148 | prevout: {
149 | value: BigInt(utxos[i].value),
150 | scriptPubKey: addressToScriptPubKey(currentAddress, network)
151 | }
152 | });
153 |
154 | sats_found += BigInt(utxos[i].value);
155 | }
156 | }
157 |
158 | vout.push({
159 | value: 546n,
160 | scriptPubKey: addressToScriptPubKey(to, network)
161 | });
162 |
163 | let ec = new TextEncoder();
164 | let token_change = found - amount;
165 |
166 | let conv_amount = cleanFloat(bigIntToString(amount, dec));
167 | let conv_change = cleanFloat(bigIntToString(token_change, dec));
168 |
169 | if (token_change <= 0n) {
170 | vout.push({
171 | scriptPubKey: [ 'OP_RETURN', ec.encode('P'), ec.encode('T'),
172 | toBytes(toInt26(ticker)) as Word, toBytes(BigInt(id)) as Word, toBytes(0n) as Word, textToHex(conv_amount)
173 | ]
174 | })
175 | } else {
176 | vout.push({
177 | value: 546n,
178 | scriptPubKey: addressToScriptPubKey(currentAddress, network)
179 | });
180 |
181 | vout.push({
182 | scriptPubKey: [ 'OP_RETURN', ec.encode('P'), ec.encode('T'),
183 | toBytes(toInt26(ticker)) as Word, toBytes(BigInt(id)) as Word, toBytes(0n) as Word, textToHex(conv_amount),
184 | toBytes(toInt26(ticker)) as Word, toBytes(BigInt(id)) as Word, toBytes(1n) as Word, textToHex(conv_change)
185 | ]
186 | });
187 | }
188 |
189 | vout.push({
190 | value: sats_found - 5_000n,
191 | scriptPubKey: addressToScriptPubKey(currentAddress, network)
192 | })
193 |
194 | let txdata = Tx.create({
195 | vin: vin,
196 | vout: vout
197 | });
198 |
199 | const txSizeData = Tx.util.getTxSize(txdata);
200 | const vsize = BigInt(Math.floor(txSizeData.vsize * 1.1));
201 | const fee = (vsize * rate);
202 |
203 | vin = [];
204 | vout = [];
205 | found = 0n;
206 | sats_found = 0n;
207 |
208 | for (let i = 0; i < utxos.length; i++) {
209 | if (found >= amount) {
210 | break;
211 | }
212 |
213 | if (utxos[i].status.confirmed) {
214 | try {
215 | if (utxos[i].tick === ticker && utxos[i].id === id) {
216 | vin.push({
217 | txid: utxos[i].txid,
218 | vout: utxos[i].vout,
219 | prevout: {
220 | value: BigInt(utxos[i].value),
221 | scriptPubKey: addressToScriptPubKey(currentAddress, network)
222 | }
223 | });
224 |
225 | found += BigInt(utxos[i].amt || 0);
226 | }
227 | } catch (e) {
228 | console.error(e);
229 | }
230 | }
231 | }
232 |
233 | for (let i = 0; i < utxos.length; i++) {
234 | if (sats_found >= sats_amount + fee) {
235 | break;
236 | }
237 |
238 | if (
239 | !utxos[i].tick &&
240 | utxos[i].status.confirmed &&
241 | utxos[i].value >= 600
242 | ) {
243 | vin.push({
244 | txid: utxos[i].txid,
245 | vout: utxos[i].vout,
246 | prevout: {
247 | value: BigInt(utxos[i].value),
248 | scriptPubKey: addressToScriptPubKey(currentAddress, network)
249 | }
250 | });
251 |
252 | sats_found += BigInt(utxos[i].value);
253 | }
254 | }
255 |
256 | vout.push({
257 | value: 546n,
258 | scriptPubKey: addressToScriptPubKey(to, network)
259 | });
260 |
261 | ec = new TextEncoder();
262 | token_change = found - amount;
263 |
264 | conv_amount = cleanFloat(bigIntToString(amount, dec));
265 | conv_change = cleanFloat(bigIntToString(token_change, dec));
266 |
267 | if (token_change <= 0n) {
268 | vout.push({
269 | scriptPubKey: [ 'OP_RETURN', ec.encode('P'), ec.encode('T'),
270 | toBytes(toInt26(ticker)) as Word, toBytes(BigInt(id)) as Word, toBytes(0n) as Word, textToHex(conv_amount)
271 | ]
272 | })
273 | } else {
274 | vout.push({
275 | value: 546n,
276 | scriptPubKey: addressToScriptPubKey(currentAddress, network)
277 | });
278 |
279 | vout.push({
280 | scriptPubKey: [ 'OP_RETURN', ec.encode('P'), ec.encode('T'),
281 | toBytes(toInt26(ticker)) as Word, toBytes(BigInt(id)) as Word, toBytes(0n) as Word, textToHex(conv_amount),
282 | toBytes(toInt26(ticker)) as Word, toBytes(BigInt(id)) as Word, toBytes(1n) as Word, textToHex(conv_change)
283 | ]
284 | });
285 | }
286 |
287 | vout.push({
288 | value: sats_found - fee,
289 | scriptPubKey: addressToScriptPubKey(currentAddress, network)
290 | })
291 |
292 | if(
293 | found < amount ||
294 | sats_found < sats_amount + fee
295 | ) {
296 | throw new Error('Insufficient token funds, or transaction still unconfirmed');
297 | }
298 |
299 | txdata = Tx.create({
300 | vin : vin,
301 | vout : vout
302 | });
303 |
304 | const [tseckey] = Tap.getSecKey(account.account.privateKey)
305 |
306 | for (let i = 0; i < vin.length; i++) {
307 | const sig = Signer.taproot.sign(tseckey, txdata, i)
308 | txdata.vin[i].witness = [sig]
309 | Signer.taproot.verify(txdata, i, { throws: true })
310 | }
311 |
312 | return {
313 | hex: Tx.encode(txdata).hex,
314 | vin: vin.map((v) => ({
315 | ...v,
316 | prevout: {
317 | ...v.prevout,
318 | value: v.prevout.value.toString(),
319 | }
320 | })),
321 | vout: vout.map((v) => ({
322 | ...v,
323 | value: v.value?.toString(),
324 | })),
325 | fee: fee.toString(),
326 | ticker,
327 | id: _id,
328 | to: to,
329 | amount: conv_amount,
330 | change: conv_change,
331 | sats: sats_found.toString(),
332 | sats_change: (sats_found - fee).toString(),
333 | };
334 | };
335 |
336 | export const sendSats = (
337 | account: any,
338 | currentAddress: string,
339 | utxos: Utxo[],
340 | toAddress: string,
341 | amount: bigint,
342 | rate: bigint,
343 | network: any,
344 | ): {
345 | hex: string;
346 | vin: any[];
347 | vout: any[];
348 | fee: string;
349 | sats: string;
350 | to: string;
351 | sats_amount: string,
352 | sats_change: string;
353 | } => {
354 | let vin = [];
355 | let vout = [];
356 | let found = 0n;
357 |
358 | if(utxos.length === 0) throw new Error("No UTXOs available")
359 |
360 | utxos = utxos.sort((a, b) => b.value - a.value);
361 |
362 | for(let i = 0; i < utxos.length; i++)
363 | {
364 | if(found >= amount + 5_000n) break;
365 |
366 | if(!utxos[i].tick && utxos[i].status.confirmed && utxos[i].value >= 600) {
367 | vin.push({
368 | txid: utxos[i].txid,
369 | vout: utxos[i].vout,
370 | prevout: {
371 | value: BigInt(utxos[i].value),
372 | scriptPubKey: addressToScriptPubKey(currentAddress, network)
373 | }
374 | });
375 |
376 | found += BigInt(utxos[i].value);
377 | }
378 | }
379 |
380 | vout.push({
381 | value: amount,
382 | scriptPubKey: addressToScriptPubKey(toAddress, network)
383 | });
384 |
385 | vout.push({
386 | value: found - amount - 5_000n,
387 | scriptPubKey: addressToScriptPubKey(currentAddress, network)
388 | })
389 |
390 | let txdata = Tx.create({
391 | vin: vin,
392 | vout: vout
393 | });
394 |
395 | const txSizeData = Tx.util.getTxSize(txdata);
396 | const vsize = BigInt(Math.floor(txSizeData.vsize * 1.1));
397 | const fee = (vsize * rate);
398 |
399 | vin = [];
400 | vout = [];
401 | found = 0n;
402 |
403 | for(let i = 0; i < utxos.length; i++)
404 | {
405 | if(found >= amount + fee) break;
406 |
407 | if(!utxos[i].tick && utxos[i].status.confirmed && utxos[i].value >= 600) {
408 | vin.push({
409 | txid: utxos[i].txid,
410 | vout: utxos[i].vout,
411 | prevout: {
412 | value: BigInt(utxos[i].value),
413 | scriptPubKey: addressToScriptPubKey(currentAddress, network)
414 | }
415 | });
416 |
417 | found += BigInt(utxos[i].value);
418 | }
419 | }
420 |
421 | if(found < amount + fee) throw new Error("Insufficient token funds, or transaction still unconfirmed");
422 |
423 | vout.push({
424 | value: amount,
425 | scriptPubKey: addressToScriptPubKey(toAddress, network)
426 | });
427 |
428 | vout.push({
429 | value: found - amount - fee,
430 | scriptPubKey: addressToScriptPubKey(currentAddress, network)
431 | })
432 |
433 | txdata = Tx.create({
434 | vin: vin,
435 | vout: vout
436 | });
437 |
438 | const [tseckey] = Tap.getSecKey(account.account.privateKey)
439 |
440 | for (let i = 0; i < vin.length; i++) {
441 | const sig = Signer.taproot.sign(tseckey, txdata, i)
442 | txdata.vin[i].witness = [sig]
443 | Signer.taproot.verify(txdata, i, { throws: true })
444 | }
445 |
446 | return {
447 | hex: Tx.encode(txdata).hex,
448 | vin: vin.map((v) => ({
449 | ...v,
450 | prevout: {
451 | ...v.prevout,
452 | value: v.prevout.value.toString(),
453 | }
454 | })),
455 | vout: vout.map((v) => ({
456 | ...v,
457 | value: v.value?.toString(),
458 | })),
459 | fee: fee.toString(),
460 | sats: found.toString(),
461 | to: toAddress,
462 | sats_amount: amount.toString(),
463 | sats_change: (found - amount - fee).toString(),
464 | };
465 | }
466 |
--------------------------------------------------------------------------------
/src/components/app-layout.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Text, Image } from "grommet";
2 | import { Icon } from "../components/icon";
3 | import { RoutePath } from "../router";
4 | import { colors } from "../theme";
5 | import { useNavigate } from "react-router-dom";
6 |
7 | export interface AppLayoutProps {
8 | children: React.ReactNode;
9 | activeTab?: number;
10 | showBack?: boolean;
11 | showHeader?: boolean;
12 | }
13 |
14 | export const AppLayout = ({
15 | activeTab = 0,
16 | showBack = false,
17 | showHeader = true,
18 | ...props
19 | }: AppLayoutProps): JSX.Element => {
20 | const navigate = useNavigate();
21 |
22 | return (
23 |
24 | {showHeader && (
25 |
33 | {showBack ? (
34 | navigate(-1)} />
35 | ) : (
36 |
37 | )}
38 |
39 | )}
40 | {props.children}
41 |
42 |
50 | navigate(RoutePath.Root)}>
51 |
57 |
63 | Wallet
64 |
65 |
66 | navigate(RoutePath.Explore)}
69 | >
70 |
76 |
82 | Explore
83 |
84 |
85 | navigate(RoutePath.Settings)}>
86 |
92 |
98 | Settings
99 |
100 |
101 |
102 |
103 |
104 | );
105 | };
106 |
--------------------------------------------------------------------------------
/src/components/dollar-balance.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react"
2 |
3 | export const DollarBalance = (sats: bigint): JSX.Element => {
4 | const [ convertedValue, setConvertedValue ] = useState();
5 |
6 | const getPrice = async () => {
7 | const url = `https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd`;
8 |
9 | try {
10 | const response = await fetch(url, { method: 'GET' });
11 | const rate = await response.json();
12 |
13 | if(!rate.rate.bitcoin) return BigInt(0);
14 | else return BigInt(rate.rate.bitcoin);
15 | } catch (error) {
16 | console.error(error);
17 | }
18 | }
19 |
20 | useEffect(() => {
21 | const convert = async (sats: bigint) => {
22 | const rate = await getPrice()
23 | const val = sats * rate!;
24 | setConvertedValue(val);
25 | }
26 |
27 | convert(1n);
28 | }, [sats])
29 |
30 | return (
31 | <>
32 | $ {convertedValue}
33 | >
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/icon.tsx:
--------------------------------------------------------------------------------
1 | export const Icon = (props: {
2 | name: string;
3 | style?: React.CSSProperties;
4 | onClick?: React.MouseEventHandler | undefined;
5 | }) => (
6 |
11 | {props.name}
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./layout";
2 | export * from "./set-fees";
3 | export * from "./dollar-balance";
4 |
--------------------------------------------------------------------------------
/src/components/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Anchor, Box, Button, Header, Image, Nav } from "grommet";
2 | import { HomeOption, LinkPrevious, SettingsOption } from "grommet-icons";
3 | import { useNavigate } from "react-router-dom";
4 | import { RoutePath } from "../router/route-path";
5 |
6 | export interface LayoutProps {
7 | children: React.ReactNode;
8 | showBack?: boolean;
9 | showLogo?: boolean;
10 | actions?: {
11 | render: () => JSX.Element;
12 | }[];
13 | activeTab?: number;
14 | showTabs?: boolean;
15 | }
16 |
17 | export const Layout = ({
18 | showTabs = false,
19 | ...props
20 | }: LayoutProps): JSX.Element => {
21 | const navigate = useNavigate();
22 |
23 | const goBack = () => {
24 | navigate(-1);
25 | };
26 |
27 | return (
28 |
29 |
30 | {!!props.showBack && (
31 |
32 | } onClick={goBack} />
33 |
34 | )}
35 | {!!props.showLogo && (
36 |
37 |
38 |
39 | )}
40 | {!!props.actions && props.actions.map((action) => action.render())}
41 |
42 | {props.children}
43 | {showTabs && (
44 |
45 | navigate(RoutePath.Balances)} icon={ } label="Wallet" />
46 | navigate(RoutePath.Settings)} icon={ } label="Settings" />
47 |
48 | )}
49 |
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/src/components/set-fees.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect, useState } from "react";
2 | import { Box, Button, RangeInput, ResponsiveContext, TextInput } from "grommet";
3 | import { useApp } from "../app";
4 | import { FeesResponse, getFees } from "../bitcoin/node";
5 |
6 | interface SetFeesProps {
7 | initialFee?:number
8 | }
9 |
10 | export const SetFees:React.FC = ({initialFee}): JSX.Element => {
11 | const app = useApp();
12 |
13 | const [fees, setFees] = useState({
14 | fastestFee: 0,
15 | halfHourFee: 0,
16 | hourFee: 0,
17 | economyFee: 0,
18 | minimumFee: 0,
19 | });
20 |
21 | const [selectedFee, _setSelectedFee] = useState<
22 | "economyFee" | "halfHourFee" | "custom"
23 | >("halfHourFee");
24 |
25 | const [customFee, setCustomFee] = useState(fees.fastestFee);
26 |
27 | const onCustomFee = (event: React.ChangeEvent) => {
28 | if (!event.target.value) return setCustomFee(0);
29 | const value = parseInt(event.target.value, 10);
30 | setCustomFee(value);
31 | app.setFeerate(value);
32 | };
33 |
34 | useEffect(() => {
35 | getFees().then((fees) => {
36 | setFees({
37 | fastestFee: Math.floor(fees.fastestFee * 1.5),
38 | halfHourFee: Math.floor(fees.halfHourFee * 1.5),
39 | hourFee: Math.floor(fees.hourFee * 1.5),
40 | economyFee: Math.floor(fees.economyFee * 1.5),
41 | minimumFee: Math.floor(fees.minimumFee * 1.5),
42 | });
43 | if(initialFee){
44 | setSelectedFee('custom');
45 | app.setFeerate(initialFee);
46 | setCustomFee(initialFee);
47 | return;
48 | }
49 | setCustomFee(Math.floor(fees.fastestFee * 1.5));
50 | if (selectedFee === "custom") return;
51 | app.setFeerate(Math.floor(fees[selectedFee] * 1.5));
52 | });
53 | }, []);
54 |
55 | const size = useContext(ResponsiveContext);
56 |
57 | const setSelectedFee = (fee: "economyFee" | "halfHourFee" | "custom") => {
58 | _setSelectedFee(fee);
59 | if (fee === "custom") {
60 | app.setFeerate(customFee);
61 | return;
62 | }
63 | app.setFeerate(fees[fee]);
64 | }
65 |
66 | return (
67 | <>
68 |
78 | setSelectedFee("economyFee")}
82 | size="small"
83 | />
84 | setSelectedFee("halfHourFee")}
88 | size="small"
89 | />
90 | setSelectedFee("custom")}
94 | size="small"
95 | />
96 |
97 | {selectedFee === "custom" && (
98 |
99 |
105 |
106 |
112 |
113 |
114 | )}
115 | >
116 | );
117 | };
118 |
--------------------------------------------------------------------------------
/src/constants/constants.ts:
--------------------------------------------------------------------------------
1 | export const constants = {
2 | bitcoin_indexer: {
3 | main: "https://api.blockcypher.com/v1/btc/main",
4 | testnet: "https://api.blockcypher.com/v1/btc/test3",
5 | api: {
6 | unspents: "/addrs/:address?unspentOnly=true&before=:before",
7 | },
8 | },
9 | pipe_indexer: {
10 | main: "https://indexer.inspip.com",
11 | testnet: "https://indexer-testnet.inspip.com",
12 | api: {
13 | unspents: "/utxo/by-address/:address?limit=:limit&page=:page"
14 | },
15 | },
16 | ordinals_indexer: {
17 | main: "https://api.hiro.so",
18 | testnet: "https://api.hiro.so",
19 | api: {
20 | unspents: "/ordinals/v1/inscriptions?limit=:limit&offset=:offset&address=:address"
21 | },
22 | },
23 | utxo_dummy_value: 546,
24 | };
--------------------------------------------------------------------------------
/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | interface ImportMetaEnv {
4 | readonly VITE_SERVER_HOST: string
5 | }
6 |
7 | interface ImportMeta {
8 | readonly env: ImportMetaEnv
9 | }
10 |
--------------------------------------------------------------------------------
/src/hooks/address.hook.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 | import { useApp } from "../app";
3 | import { generateNewAddress } from "../bitcoin/wallet";
4 | import { getNetwork } from "../bitcoin/helpers";
5 |
6 | export type Address = {
7 | data: string[];
8 | address: string;
9 | createAddress: () => void;
10 | switchAddress: (address: string, index: number) => void;
11 | }
12 |
13 | export const useAddress = (): Address => {
14 | const app = useApp();
15 |
16 | const data = useMemo(() => {
17 | return app.addresses.map((_index) => {
18 | // @todo check it
19 | const account0 = generateNewAddress(app.account.rootKey, getNetwork(app.network), 0);
20 | return generateNewAddress(account0.rootKey, getNetwork(app.network), _index).address;
21 | });
22 | }, [app.account.rootKey, app.addresses, app.network]);
23 |
24 | const createAddress = async () => {
25 | app.setAddresses(app.addresses.concat([app.addresses.length]));
26 | };
27 |
28 | const switchAddress = (address: string, index: number) => {
29 | // @todo check it
30 | const account0 = generateNewAddress(app.account.rootKey, getNetwork(app.network), 0);
31 | app.setAccount(generateNewAddress(account0.rootKey, getNetwork(app.network), index));
32 | app.setCurrentAddress(address, index);
33 | };
34 |
35 | return {
36 | address: app.currentAddress,
37 | data,
38 | createAddress,
39 | switchAddress,
40 | };
41 | }
42 |
--------------------------------------------------------------------------------
/src/hooks/create-wallet.hook.ts:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from "react";
2 | import { generateNewAddress, generateWallet, importWallet } from "../bitcoin/wallet";
3 | import { saveWallet } from "../bitcoin/wallet-storage";
4 | import { useApp } from "../app";
5 | import { getNetwork } from "../bitcoin/helpers";
6 |
7 | export type CreateWallet = {
8 | dispatch: (password: string) => Promise;
9 | loading: boolean;
10 | data?: { network: string; rootKey: any; mnemonic: string; account: any; internalPubkey: any; address: string; output: any; } | undefined;
11 | };
12 |
13 | export const useCreateWallet = (): CreateWallet => {
14 | const [loading, setLoading] = useState(false);
15 | const [data, setData] = useState<{ network: string; rootKey: any; mnemonic: string; account: any; internalPubkey: any; address: string; output: any; } | undefined>();
16 | const app = useApp();
17 |
18 | const dispatch = useCallback(
19 | async (password: string) => {
20 | if (loading) return;
21 |
22 | setLoading(true);
23 |
24 | const wallet = generateWallet(getNetwork(app.network));
25 | const address = generateNewAddress(wallet.rootKey, getNetwork(app.network), 0);
26 | saveWallet(wallet.mnemonic, app.network, address.address, [0], password);
27 | setData({...wallet, network: app.network});
28 |
29 | app.setAccount(importWallet(wallet.mnemonic, getNetwork(app.network), 0));
30 | app.setNetwork(app.network);
31 | app.setCurrentAddress(address.address, 0)
32 | app.setAddresses([0]);
33 |
34 | setLoading(false);
35 |
36 | return wallet;
37 | },
38 | [loading]
39 | );
40 |
41 | return {
42 | dispatch,
43 | loading,
44 | data,
45 | };
46 | };
47 |
--------------------------------------------------------------------------------
/src/hooks/get-balances.hook.ts:
--------------------------------------------------------------------------------
1 | import { useState, useCallback, useEffect } from "react";
2 | import { useApp } from "../app";
3 | import { bigIntToString, parseStringToBigInt } from "../bitcoin/helpers";
4 | import { cleanFloat } from "../utils/clean-float";
5 | import { getUnspents } from "../transfer/get-unspents";
6 |
7 | export type GetBalances = {
8 | dispatch: () => Promise<{ [key: string]: string } | undefined>;
9 | loading: boolean;
10 | data: { [key: string]: string };
11 | };
12 |
13 | export const useGetBalances = (): GetBalances => {
14 | const app = useApp();
15 | const [loading, setLoading] = useState(false);
16 | const [data, setData] = useState<{
17 | [key: string]: string;
18 | }>({ btc: "0" });
19 |
20 | const dispatch = useCallback(async () => {
21 | if (loading) return;
22 | setLoading(true);
23 |
24 | const nextData: {
25 | [key: string]: string;
26 | } = {};
27 |
28 | const utxos = await app.fetchUtxos();
29 |
30 | const sats = ((await getUnspents({network:app.network as "mainnet" | "testnet",cursor:null,address:app.currentAddress})).balance)/Math.pow(10,8);
31 |
32 | for (const utxo of utxos.filter((u:any) => u.protocol === "pipe")) {
33 | try {
34 | if (typeof nextData[utxo.tick + ":" + utxo.id] === "undefined") {
35 | nextData[utxo.tick + ":" + utxo.id] = "0";
36 | }
37 |
38 | if (typeof utxo.amt === 'undefined') continue;
39 | if (typeof utxo.dec === 'undefined') continue;
40 |
41 | nextData[utxo.tick + ":" + utxo.id] = cleanFloat(bigIntToString(
42 | parseStringToBigInt(
43 | nextData[utxo.tick + ":" + utxo.id],
44 | utxo.dec
45 | ) + BigInt(utxo.amt),
46 | utxo.dec
47 | )
48 | );
49 | } catch (e) {
50 | console.error(e);
51 | }
52 | }
53 |
54 | //nextData["ordinals"] = utxos.filter((u:any) => u.protocol === "ordinals").length;
55 |
56 | nextData["btc"] = sats.toString();
57 |
58 | setData(nextData);
59 | setLoading(false);
60 | return nextData;
61 | }, [app, loading]);
62 |
63 | useEffect(() => {
64 | dispatch();
65 | }, []);
66 |
67 | return {
68 | dispatch,
69 | loading,
70 | data,
71 | };
72 | };
73 |
--------------------------------------------------------------------------------
/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./create-wallet.hook";
2 | export * from "./get-balances.hook";
3 | export * from "./restore-wallet.hook";
4 | export * from "./send-sats.hook";
5 | export * from "./send-tokens.hook";
6 |
--------------------------------------------------------------------------------
/src/hooks/restore-wallet.hook.ts:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from "react";
2 | import { importWallet, importWalletFromWif } from "../bitcoin/wallet";
3 | import { saveWallet } from "../bitcoin/wallet-storage";
4 | import { useApp } from "../app";
5 | import { getNetwork } from "../bitcoin/helpers";
6 |
7 | export type RestoreWallet = {
8 | dispatch: (mnemonic: string, password: string) => Promise;
9 | loading: boolean;
10 | data?: { network: string; rootKey: any; mnemonic: string; account: any; internalPubkey: any; address: string; output: any; };
11 | };
12 |
13 |
14 | export const useRestoreWallet = (): RestoreWallet => {
15 | const [loading, setLoading] = useState(false);
16 | const [data, setData] = useState<{ network: string; rootKey: any; mnemonic: string; account: any; internalPubkey: any; address: string; output: any; } | undefined>();
17 | const app = useApp();
18 |
19 | const dispatch = useCallback(
20 | async (mnemonic: string, password: string) => {
21 | if (loading) return;
22 |
23 | setLoading(true);
24 |
25 | let wallet;
26 | const formattedMnemonic = mnemonic?.split(' ').filter((el:any)=>el !== '');
27 |
28 | const isMnemo = formattedMnemonic.length === 12;
29 |
30 | if (isMnemo) {
31 | wallet = importWallet(formattedMnemonic.join(' '), getNetwork(app.network));
32 | } else {
33 | wallet = importWalletFromWif(mnemonic, getNetwork(app.network));
34 | }
35 |
36 | saveWallet(wallet.mnemonic, app.network, wallet.address, [0], password);
37 | setData({...wallet, network: app.network});
38 |
39 | app.setAccount(wallet);
40 | app.setNetwork(app.network);
41 | app.setCurrentAddress(wallet.address, 0)
42 | app.setAddresses([0]);
43 |
44 | setLoading(false);
45 |
46 | return wallet;
47 | },
48 | [loading]
49 | );
50 |
51 | return {
52 | dispatch,
53 | loading,
54 | data,
55 | };
56 | };
57 |
--------------------------------------------------------------------------------
/src/hooks/safe-balances.hook.ts:
--------------------------------------------------------------------------------
1 | import { useState, useCallback, useEffect } from "react";
2 | import { useApp } from "../app";
3 | import { cleanFloat } from "../utils/clean-float";
4 | import { bigIntToString, parseStringToBigInt } from "../bitcoin/helpers";
5 | import { getUnspents } from "../transfer/get-unspents";
6 |
7 | export type SafeBalances = {
8 | dispatch: () => Promise<{ [key: string]: string } | undefined>;
9 | loading: boolean;
10 | data: { [key: string]: string };
11 | };
12 |
13 | export const useSafeBalances = (): SafeBalances => {
14 | const app = useApp();
15 | const [loading, setLoading] = useState(false);
16 | const [data, setData] = useState<{
17 | [key: string]: string;
18 | }>({ btc: "0" });
19 |
20 | const dispatch = useCallback(async () => {
21 | if (loading) return;
22 | setLoading(true);
23 |
24 | const nextData: {
25 | [key: string]: string;
26 | } = {};
27 |
28 | let utxos = await app.fetchUtxos();
29 | utxos = utxos.filter((u:any) => u.status.confirmed);
30 |
31 | const sats = ((await getUnspents({network:app.network as "mainnet" | "testnet",cursor:null,address:app.currentAddress})).balance)/Math.pow(10,8);
32 |
33 | for (const utxo of utxos.filter((u:any) => u.protocol === "pipe")) {
34 | try {
35 | if (typeof nextData[utxo.tick + ":" + utxo.id] === "undefined") {
36 | nextData[utxo.tick + ":" + utxo.id] = "0";
37 | }
38 |
39 | if (typeof utxo.amt === 'undefined') continue;
40 | if (typeof utxo.dec === 'undefined') continue;
41 |
42 | nextData[utxo.tick + ":" + utxo.id] = cleanFloat(bigIntToString(
43 | parseStringToBigInt(
44 | nextData[utxo.tick + ":" + utxo.id],
45 | utxo.dec
46 | ) + BigInt(utxo.amt),
47 | utxo.dec
48 | )
49 | );
50 | } catch (e) {
51 | console.error(e);
52 | }
53 | }
54 |
55 | //nextData["ordinals"] = utxos.filter((u:any) => u.protocol === "ordinals").length;
56 |
57 | nextData["btc"] = sats.toString();
58 |
59 | setData(nextData);
60 | setLoading(false);
61 | return nextData;
62 | }, [app, loading]);
63 |
64 | useEffect(() => {
65 | dispatch();
66 | }, []);
67 |
68 | return {
69 | dispatch,
70 | loading,
71 | data,
72 | };
73 | };
74 |
--------------------------------------------------------------------------------
/src/hooks/send-sats.hook.ts:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from "react";
2 | import { useApp } from "../app";
3 | import { transferSats } from "../transfer/transfer-sats";
4 | import { parseStringToBigInt } from "../bitcoin/helpers";
5 |
6 | export type SendSats = {
7 | dispatch: (address: string, amount: string, fee_rate: string) => Promise<{
8 | hex: string;
9 | vin: any[];
10 | vout: any[];
11 | fee: string;
12 | sats: string;
13 | sats_change: string;
14 | } | undefined>;
15 | loading: boolean;
16 | data?: {
17 | hex: string;
18 | vin: any[];
19 | vout: any[];
20 | fee: string;
21 | sats: string;
22 | sats_change: string;
23 | };
24 | };
25 |
26 | export const useSendSats = (): SendSats => {
27 | const [loading, setLoading] = useState(false);
28 | const [data, setData] = useState<{
29 | hex: string;
30 | vin: any[];
31 | vout: any[];
32 | fee: string;
33 | sats: string;
34 | sats_change: string;
35 | }>();
36 | const app = useApp();
37 |
38 | const dispatch = useCallback(
39 | async (address: string, amount: string, fee_rate: string) => {
40 | if (loading) return;
41 | setLoading(true);
42 |
43 | try {
44 | const res = await transferSats({
45 | privateKey: app.account.account.privateKey as Uint8Array,
46 | from: app.currentAddress,
47 | to: address,
48 | amount: parseStringToBigInt(amount, 8).toString(),
49 | feerate: fee_rate,
50 | network: app.network as "mainnet" | "testnet",
51 | });
52 |
53 | setData(res);
54 |
55 | return res;
56 | } catch (e) {
57 | setLoading(false);
58 | throw e;
59 | }
60 | },
61 | [app, loading]
62 | );
63 |
64 | return {
65 | dispatch,
66 | loading,
67 | data,
68 | };
69 | };
70 |
--------------------------------------------------------------------------------
/src/hooks/send-tokens.hook.ts:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from "react";
2 | import { useApp } from "../app";
3 | import { transferPipe } from "../transfer/transfer-pipe";
4 | import { stringFromBigInt } from "../bitcoin/helpers";
5 |
6 | export type SendTokens = {
7 | dispatch: (address: string, ticker: string, id: string, amount: string, fee_rate: string) => Promise<{
8 | hex: string;
9 | vin: any[];
10 | vout: any[];
11 | fee: string;
12 | ticker: string;
13 | id: string;
14 | amount: string;
15 | change: string;
16 | sats: string;
17 | sats_change: string;
18 | } | undefined>
19 | loading: boolean;
20 | data?: {
21 | hex: string;
22 | vin: any[];
23 | vout: any[];
24 | fee: string;
25 | ticker: string;
26 | id: string;
27 | amount: string;
28 | change: string;
29 | sats: string;
30 | sats_change: string;
31 | };
32 | };
33 |
34 | export const useSendTokens = (): SendTokens => {
35 | const [loading, setLoading] = useState(false);
36 | const [data, setData] = useState<{
37 | hex: string;
38 | vin: any[];
39 | vout: any[];
40 | fee: string;
41 | ticker: string;
42 | id: string;
43 | amount: string;
44 | change: string;
45 | sats: string;
46 | sats_change: string;
47 | }>();
48 | const app = useApp();
49 |
50 | const dispatch = useCallback(
51 | async (address: string, ticker: string, id: string, amount: string, fee_rate: string) => {
52 | if (loading) return;
53 | setLoading(true);
54 |
55 | const deployment = await app.tokens.filter((token) => token.tick === ticker.toLowerCase() && token.id === parseInt(id))[0];
56 |
57 | try {
58 | const res = await transferPipe({
59 | privateKey: app.account.account.privateKey as Uint8Array,
60 | from: app.currentAddress,
61 | to: address,
62 | amount: stringFromBigInt(amount, deployment.dec).toString(),
63 | feerate: fee_rate,
64 | network: app.network as "mainnet" | "testnet",
65 | ticker,
66 | id,
67 | decimals: deployment.dec.toString(),
68 | });
69 |
70 | setData(res);
71 |
72 | return res;
73 | } catch (e) {
74 | setLoading(false);
75 | throw e;
76 | }
77 | },
78 | [app, loading]
79 | );
80 |
81 | return {
82 | dispatch,
83 | loading,
84 | data,
85 | };
86 | };
87 |
--------------------------------------------------------------------------------
/src/hooks/show-transactions.hook.ts:
--------------------------------------------------------------------------------
1 | export type Transaction = {
2 | txid: string;
3 | from: string;
4 | to: string;
5 | timestamp: number;
6 | confirmed: boolean;
7 | amount: string;
8 | token?: string;
9 | };
10 |
11 | export const save = (transaction: Transaction) => {
12 | const transactionsJSON = localStorage.getItem("transactions");
13 | const transactions: Transaction[] = transactionsJSON ? JSON.parse(transactionsJSON) : [];
14 | transactions.push(transaction);
15 | localStorage.setItem("transactions", JSON.stringify(transactions));
16 | }
17 |
18 | export const load = () => {
19 | const transactionsJSON = localStorage.getItem("transactions");
20 | const transactions: Transaction[] = transactionsJSON ? JSON.parse(transactionsJSON) : [];
21 |
22 | return transactions;
23 | }
24 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import { ThemeProvider } from "./theme";
4 | import { RouterProvider } from "./router";
5 | import { AppProvider } from "./app";
6 |
7 | ReactDOM.createRoot(document.getElementById("root")!).render(
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | );
16 |
--------------------------------------------------------------------------------
/src/router/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./route-path";
2 | export * from "./router";
3 | export * from "./router-provider";
4 |
--------------------------------------------------------------------------------
/src/router/route-path.ts:
--------------------------------------------------------------------------------
1 | export enum RoutePath {
2 | Root = "/",
3 | Explore = "/explore",
4 | CreateWallet = "/create",
5 | RestoreWallet = "/restore",
6 | Balances = "/balances",
7 | Mnemonic = "/mnemonic",
8 | Password = "/password",
9 | Send = "/send",
10 | Addresses = "/addresses",
11 | ConfirmTransaction = "/confirm-transaction",
12 | ConnectWallet = "/connect-wallet",
13 | SendBTC = "/send-btc",
14 | Settings = "/settings",
15 | SwitchNetwork = "/switch-network",
16 | DecodeAndSignPsbt = "/decode-and-sign-psbt",
17 | SignMessage = "/sign-message",
18 | VerifySign = "/verify-message",
19 | }
20 |
--------------------------------------------------------------------------------
/src/router/router-provider.tsx:
--------------------------------------------------------------------------------
1 | import { RouterProvider as DefaultRouterProvider } from "react-router-dom";
2 | import { router } from "./router";
3 |
4 | export const RouterProvider = () => ;
5 |
--------------------------------------------------------------------------------
/src/router/router.tsx:
--------------------------------------------------------------------------------
1 | import { createHashRouter } from "react-router-dom";
2 | import { RoutePath } from "./route-path";
3 | import { App } from "../app";
4 | import { CreateWallet, RestoreWallet, Mnemonic, Balances, Send } from "../screens";
5 | import { Addresses } from "../screens/addresses";
6 | import { Password } from "../screens/password";
7 | import { Settings} from "../screens/settings";
8 | import { ConfirmTransaction } from "../screens/confirm-transaction";
9 | import { ConnectWallet } from "../screens/connect-wallet";
10 | import { SwitchNetwork } from "../screens/switch-network";
11 | import { Explore } from "../screens/explore";
12 | import { DecodeAndSignPsbt } from "../screens/decode-and-sign-psbt";
13 | import { SignMessage } from "../screens/sign-message";
14 | import { VerifySign } from "../screens/verify-sign";
15 |
16 | export const router = createHashRouter([
17 | {
18 | path: RoutePath.Root,
19 | element: ,
20 | },
21 | {
22 | path: RoutePath.Explore,
23 | element: ,
24 | },
25 | {
26 | path: RoutePath.CreateWallet,
27 | element: ,
28 | },
29 | {
30 | path: RoutePath.Mnemonic,
31 | element: ,
32 | },
33 | {
34 | path: RoutePath.RestoreWallet,
35 | element: ,
36 | },
37 | {
38 | path: RoutePath.Password,
39 | element: ,
40 | },
41 | {
42 | path: RoutePath.Balances,
43 | element: ,
44 | },
45 | {
46 | path: RoutePath.Send,
47 | element: ,
48 | },
49 | {
50 | path: RoutePath.Addresses,
51 | element: ,
52 | },
53 | {
54 | path: RoutePath.ConfirmTransaction,
55 | element: ,
56 | },
57 | {
58 | path: RoutePath.ConnectWallet,
59 | element: ,
60 | },
61 | {
62 | path: RoutePath.Settings,
63 | element:
64 | },
65 | {
66 | path: RoutePath.SwitchNetwork,
67 | element: ,
68 | },
69 | {
70 | path: RoutePath.DecodeAndSignPsbt,
71 | element: ,
72 | },
73 | {
74 | path: RoutePath.SignMessage,
75 | element: ,
76 | },
77 | {
78 | path: RoutePath.VerifySign,
79 | element: ,
80 | }
81 | ]);
82 |
--------------------------------------------------------------------------------
/src/screens/addresses.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, InfiniteScroll, Text } from "grommet";
2 | import { useAddress } from "../hooks/address.hook";
3 | import { truncateInMiddle } from "../utils/truncate-in-middle";
4 | import { Add } from "grommet-icons";
5 | import * as bip39 from "bip39";
6 | import { useApp } from "../app";
7 | import { AppLayout } from "../components/app-layout";
8 |
9 | export const Addresses = (): JSX.Element => {
10 | const address = useAddress();
11 | const app = useApp();
12 |
13 | const isMnemo = bip39.validateMnemonic(app.account.mnemonic);
14 |
15 | return (
16 |
17 |
18 |
19 | {isMnemo && (
20 | }
24 | pad="small"
25 | onClick={() => address.createAddress()}
26 | />
27 | )}
28 |
29 | {(item: string, index: number) => (
30 | address.switchAddress(item, index)}
34 | pad="medium"
35 | margin={{ bottom: "medium" }}
36 | >
37 |
38 | Account {index + 1}
39 |
40 | {truncateInMiddle(item, 20)}
41 |
42 | )}
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/src/screens/balances.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Text,
4 | Button,
5 | InfiniteScroll,
6 | Tip,
7 | Avatar,
8 | ResponsiveContext,
9 | } from "grommet";
10 | import { Next } from "grommet-icons";
11 | import { Icon } from "../components/icon";
12 | import { useGetBalances } from "../hooks";
13 | import { IndexerToken, useApp } from "../app";
14 | import { useNavigate } from "react-router-dom";
15 | import { RoutePath } from "../router";
16 | import { truncateInMiddle } from "../utils/truncate-in-middle";
17 | import { useContext, useEffect, useMemo, useRef, useState } from "react";
18 | import { satsToDollars } from "../utils/sats-to-dollars";
19 | import { getBitcoinPrice } from "../utils/bitcoin-price";
20 | import { Transaction, load } from "../hooks/show-transactions.hook";
21 | import { colors } from "../theme";
22 | import { AppLayout } from "../components/app-layout";
23 | import { constants } from "../constants/constants";
24 |
25 | const TipContent = ({ message }: { message: string }) => (
26 |
27 |
33 | {message}
34 |
35 |
42 |
43 |
44 |
45 | );
46 |
47 | const hexToUrl = (metadata: string, mimeType: string) => {
48 | const input = metadata.replace(/[^A-Fa-f0-9]/g, "");
49 |
50 | if (input.length % 2) {
51 | return;
52 | }
53 |
54 | const binary = [];
55 | for (let i = 0; i < input.length / 2; i++) {
56 | const h = input.substr(i * 2, 2);
57 | binary[i] = parseInt(h, 16);
58 | }
59 |
60 | const byteArray = new Uint8Array(binary);
61 | return window.URL.createObjectURL(new Blob([byteArray], { type: mimeType }));
62 | };
63 |
64 | export const Balances = () => {
65 | const size = useContext(ResponsiveContext);
66 | const app = useApp();
67 | const balances = useGetBalances();
68 | const navigate = useNavigate();
69 | const [bitcoinPrice, setBitcoinPrice] = useState(0);
70 | const [transactions, setTransactions] = useState([]);
71 | const [showTransactions, setShowTransactions] = useState(false);
72 | const [tokens, setTokens] = useState([]);
73 |
74 | useEffect(() => {
75 | (async () => {
76 | const tickers = Object.keys(balances.data);
77 |
78 | if (tickers.length === 0) return;
79 |
80 | const tokens: IndexerToken[] = [];
81 |
82 | for (const ticker of tickers) {
83 | if (ticker === "btc" || ticker === "sats") continue;
84 |
85 | try {
86 | const tickerParts = ticker.split(":");
87 | const token: IndexerToken = await (
88 | await fetch(
89 | `${app.network === 'mainnet' ? constants.pipe_indexer.main : constants.pipe_indexer.testnet}/token/get/${
90 | tickerParts[0]
91 | }/${tickerParts[1]}`
92 | )
93 | ).json();
94 | token.amount = Number(balances.data[ticker]);
95 | tokens.push(token);
96 | } catch (e) {
97 | console.error(e);
98 | }
99 | }
100 |
101 | setTokens(tokens);
102 | })();
103 | }, [app.network, balances.data]);
104 |
105 | useEffect(() => {
106 | if (!app.currentAddress) {
107 | navigate(RoutePath.Root);
108 | }
109 | }, []);
110 |
111 | const called = useRef(false);
112 |
113 | useEffect(() => {
114 | if (called.current) return;
115 | called.current = true;
116 | getBitcoinPrice().then((price) => {
117 | setBitcoinPrice(price);
118 | });
119 |
120 | setTransactions(
121 | load()
122 | .filter((t) => t.from === app.currentAddress)
123 | .reverse()
124 | );
125 | }, []);
126 |
127 | const dollars = useMemo(() => {
128 | if (bitcoinPrice === 0 || balances.data.btc === "0") return "";
129 | return (
130 | Math.floor(
131 | satsToDollars(parseFloat(balances.data.btc) * 100000000, bitcoinPrice)
132 | ) + " $"
133 | );
134 | }, [balances.data.btc, bitcoinPrice]);
135 |
136 | const send = (ticker?: string, id?: number) => {
137 | if (ticker && id) {
138 | navigate(RoutePath.Send, {
139 | state: {
140 | ticker: `${ticker.toUpperCase()}:${id}`,
141 | },
142 | });
143 | } else {
144 | navigate(RoutePath.Send);
145 | }
146 | };
147 |
148 | return (
149 |
150 |
157 | navigate(RoutePath.Addresses)}
170 | >
171 | {
175 | e.stopPropagation();
176 | window.navigator.clipboard.writeText(app.currentAddress);
177 | }}
178 | />
179 |
180 |
184 | {truncateInMiddle(app.currentAddress, 30)}
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 | {balances.data.btc}
193 |
194 | BTC
195 |
196 |
197 |
198 | {dollars}
199 |
200 |
201 |
210 |
211 |
220 | navigate(RoutePath.Explore, {
221 | state: {
222 | url: "https://inspip.com/deploy/ticker",
223 | },
224 | })
225 | }
226 | />
227 |
228 |
229 |
238 | navigate(RoutePath.Explore, {
239 | state: {
240 | url: "https://inspip.com/mint/ticker",
241 | },
242 | })
243 | }
244 | />
245 |
246 |
247 | send()}
256 | />
257 |
258 |
259 |
267 | setShowTransactions(false)}
280 | />
281 | setShowTransactions(true)}
294 | />
295 |
296 |
297 |
298 | {showTransactions ? (
299 |
300 | {(item: Transaction) => (
301 |
319 | window.open(
320 | "https://mempool.space/tx/" + item.txid,
321 | "_blank"
322 | )
323 | }
324 | >
325 |
326 |
327 |
336 |
337 | {item.timestamp &&
338 | `${new Date(item.timestamp).toLocaleDateString(
339 | "en-US",
340 | {
341 | month: "numeric",
342 | day: "numeric",
343 | hour: "numeric",
344 | minute: "numeric",
345 | }
346 | )}`}
347 |
348 |
349 |
350 | {truncateInMiddle(item.to, 40)}
351 |
352 |
353 |
354 |
355 | }
358 | plain
359 | >
360 |
366 | {item.token?.toUpperCase() || "BTC"}
367 |
368 | {item.amount}
369 |
370 |
371 |
372 |
373 | )}
374 |
375 | ) : (
376 |
377 | {(item: IndexerToken) => (
378 | send(item.ticker, item.id)}
396 | >
397 |
398 |
418 |
419 | {item.ticker.toUpperCase()[0]}
420 |
421 |
422 |
423 |
424 | {item.ticker.toUpperCase()}:{item.id}
425 |
426 |
427 |
428 |
429 | }
432 | plain
433 | >
434 |
435 | {item.amount}
436 |
437 |
438 |
439 |
440 |
441 | )}
442 |
443 | )}
444 |
445 |
446 |
447 | );
448 | };
449 |
--------------------------------------------------------------------------------
/src/screens/confirm-transaction.tsx:
--------------------------------------------------------------------------------
1 | import { useLocation, useNavigate } from "react-router-dom";
2 | import { Address } from "@cmdcode/tapscript";
3 | import { Layout } from "../components";
4 | import { Accordion, AccordionPanel, Box, Button, Footer, Text } from "grommet";
5 | import { truncateInMiddle } from "../utils/truncate-in-middle";
6 | import { sendTransaction } from "../bitcoin/node";
7 | import { useApp } from "../app";
8 | import { save } from "../hooks/show-transactions.hook";
9 | import { useState } from "react";
10 |
11 | export const ConfirmTransaction = (): JSX.Element => {
12 | const location = useLocation();
13 | const app = useApp();
14 | const navigate = useNavigate();
15 | const [error, setError] = useState("");
16 |
17 | const goBack = () => {
18 | navigate(-1);
19 | };
20 |
21 | const onSend = async () => {
22 | try {
23 | const txid = await sendTransaction(location.state.tx.hex, app.network);
24 | const currSpentsStr = localStorage.getItem("currSpents");
25 | const currSpents = JSON.parse(currSpentsStr || "[]");
26 | const nextCurrSpents = [...currSpents,...location.state.tx.vin.map((vin: { txid: string, vout: number }) => ({txId: vin.txid, vout: vin.vout}))];
27 | localStorage.setItem("currSpents", JSON.stringify(nextCurrSpents));
28 |
29 | if (location.state.tx.ticker && location.state.tx.ticker !== '') {
30 | save({txid, from: app.currentAddress, to: location.state.tx.to, amount: location.state.tx.amount, token: `${location.state.tx.ticker.toUpperCase()}:${location.state.tx.id}`, timestamp: Date.now(), confirmed: false });
31 | } else {
32 | save({txid, from: app.currentAddress, to: location.state.tx.to, amount: (parseInt(location.state.tx.sats_amount) / Math.pow(10, 8)).toString(), timestamp: Date.now(), confirmed: false });
33 | }
34 | if(location.state.fromWeb){
35 | await chrome.runtime.sendMessage({ message: `ReturnSendBitcoin;${txid}`});
36 | window.close();
37 | } else {
38 | navigate(-2);
39 | }
40 | } catch (e) {
41 | setError((e as Error).message);
42 | }
43 | };
44 |
45 | return (
46 |
47 |
48 | {!!error && ({error} )}
49 |
50 | Inputs}
52 | >
53 |
54 | {location.state.tx.vin.map((vin: any, vinIndex: number) => (
55 |
60 |
61 | {truncateInMiddle(
62 | Address.fromScriptPubKey(vin.prevout.scriptPubKey),
63 | 20
64 | )}
65 |
66 | {vin.prevout.value} sats
67 |
68 | ))}
69 |
70 |
71 | Outputs}
73 | >
74 |
75 | {location.state.tx.vout.map((vout: any, voutIndex: number) => (
76 |
81 | {typeof vout.value !== "undefined" ? (
82 | <>
83 |
84 | {truncateInMiddle(
85 | Address.fromScriptPubKey(vout.scriptPubKey),
86 | 20
87 | )}
88 |
89 | {vout.value} sats
90 | >
91 | ) : (
92 |
93 |
94 | Amount out
95 |
96 | {location.state.tx.amount}{" "}
97 | {`${location.state.tx.ticker}:${location.state.tx.id}`}
98 |
99 |
100 |
101 | Amount change
102 |
103 | {location.state.tx.change}{" "}
104 | {`${location.state.tx.ticker}:${location.state.tx.id}`}
105 |
106 |
107 |
108 | )}
109 |
110 | ))}
111 |
112 |
113 |
114 |
115 |
116 |
117 |
125 | Fee
126 | {location.state.tx.fee} sats
127 |
128 |
129 |
135 |
141 |
142 |
143 |
144 |
145 | );
146 | };
--------------------------------------------------------------------------------
/src/screens/connect-wallet.tsx:
--------------------------------------------------------------------------------
1 | import { Text, Image, Box, Button } from "grommet";
2 | import { Layout } from "../components";
3 | import { useNavigate } from "react-router-dom";
4 | import { useApp } from "../app";
5 |
6 | export const ConnectWallet = (): JSX.Element => {
7 | const app = useApp();
8 | const navigate = useNavigate();
9 | const returnConnectInfo = async () => {
10 | await chrome.runtime.sendMessage({ message: `ReturnConnectWalletInfo;${app.currentAddress};${app.account.publickey}`});
11 | window.close();
12 | };
13 | const goBack = async () => {
14 | await chrome.runtime.sendMessage({ message: 'ClientRejectConnectWalletInfo'});
15 | navigate(-1);
16 | };
17 | return (
18 |
19 |
20 |
21 |
22 |
23 | Current address:
24 |
25 |
26 | {app.currentAddress}
27 |
28 |
29 | Connect wallet?
30 |
31 |
32 |
37 |
42 |
43 |
44 |
45 |
46 | );
47 | };
--------------------------------------------------------------------------------
/src/screens/create-wallet.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Text, Image, Box, TextInput, Button, Spinner } from "grommet";
3 |
4 | import { Layout } from "../components";
5 | import { useCreateWallet } from "../hooks";
6 | import { useNavigate } from "react-router-dom";
7 | import { RoutePath } from "../router";
8 | import { savePasswordInSettings } from "../app/settings";
9 |
10 | export const CreateWallet = (): JSX.Element => {
11 | const createWallet = useCreateWallet();
12 | const navigate = useNavigate();
13 |
14 | const [password, setPassword] = useState("");
15 | const [confirmPassword, setConfirmPassword] = useState("");
16 |
17 | const [errors, setErrors] = useState<{
18 | password?: string;
19 | confirmPassword?: string;
20 | error?: string;
21 | }>({});
22 |
23 | const onCreateWallet = async () => {
24 | if (!password) {
25 | setErrors({
26 | password: "Password is required",
27 | });
28 | return;
29 | } else if (password.length < 8) {
30 | setErrors({
31 | password:
32 | "Password must be at least 8 characters long",
33 | });
34 | return;
35 | } else if (password !== confirmPassword) {
36 | setErrors({
37 | confirmPassword: "Passwords do not match",
38 | });
39 | return;
40 | }
41 |
42 | let data: any;
43 |
44 | try {
45 | data = await createWallet.dispatch(password);
46 | } catch (e) {
47 | setErrors({
48 | error: (e as Error).message,
49 | });
50 | return;
51 | }
52 |
53 | savePasswordInSettings(password);
54 |
55 | navigate(RoutePath.Mnemonic, { state: { mnemonic: data.mnemonic } });
56 | };
57 |
58 | return (
59 |
60 |
61 |
62 |
63 | {!!errors.error && (
64 |
65 | {errors.error}
66 |
67 | )}
68 |
69 | setPassword(e.target.value)}
74 | width="100%"
75 | />
76 | {!!errors.password && (
77 |
78 | {errors.password}
79 |
80 | )}
81 |
82 |
83 | setConfirmPassword(e.target.value)}
88 | width="100%"
89 | />
90 | {!!errors.confirmPassword && (
91 |
92 | {errors.confirmPassword}
93 |
94 | )}
95 |
96 | {createWallet.loading && }
97 |
98 |
105 |
106 |
107 |
108 |
109 | );
110 | };
111 |
--------------------------------------------------------------------------------
/src/screens/decode-and-sign-psbt.tsx:
--------------------------------------------------------------------------------
1 | import { Layout } from "../components";
2 | import { Accordion, AccordionPanel, Box, Button, Footer, Text } from "grommet";
3 | import { useApp } from "../app";
4 | import { Address } from "@cmdcode/tapscript";
5 | import { truncateInMiddle } from "../utils/truncate-in-middle";
6 | import { useEffect, useState } from "react";
7 | import * as bitcoin from "bitcoinjs-lib";
8 |
9 | export const DecodeAndSignPsbt = () => {
10 | const app = useApp();
11 | const [psbtToSign, setPsbtToSign] = useState();
12 | const [inputsDetails, setInputsDetails] = useState([]);
13 | const [outputDetails, setOutputDetails] = useState([]);
14 | const [total, setTotal] = useState(0);
15 |
16 | const onSign = async () => {
17 | if (!psbtToSign) return;
18 | try {
19 | const inputs = psbtToSign.data.inputs;
20 | const toSignIndexes = app.signPsbt.toSignInputs.map(
21 | (el: any) => el.index
22 | );
23 | const tweakedChildNode = app.account.account.tweak(
24 | bitcoin.crypto.taggedHash("TapTweak", app.account.internalPubkey)
25 | );
26 | for (let i = 0; i < inputs.length; i++) {
27 | if (!toSignIndexes.includes(i)) continue;
28 | psbtToSign.signInput(i, tweakedChildNode, app.signPsbt.toSignInputs[toSignIndexes.indexOf(i)].sighashTypes);
29 | if (app.signPsbt.autoFinalized) {
30 | psbtToSign.finalizeInput(i);
31 | }
32 | }
33 |
34 | const hex = psbtToSign.toHex();
35 | await chrome.runtime.sendMessage({ message: `ReturnSignPsbt;${hex}` });
36 | window.close();
37 | } catch (e) {
38 | console.log(e as Error);
39 | await chrome.runtime.sendMessage({ message: `ReturnErrorOnSignPsbt;${(e as Error).message}` });
40 | window.close();
41 | }
42 | };
43 |
44 | const onClose = async () => {
45 | await chrome.runtime.sendMessage({ message: `ClientRejectSignPsbt` });
46 | window.close();
47 | };
48 |
49 | useEffect(() => {
50 | if (app.signPsbt.psbt) {
51 | let validPsbt = false;
52 | let newPsbt: bitcoin.Psbt | undefined;
53 |
54 | try {
55 | try {
56 | newPsbt = bitcoin.Psbt.fromBase64(app.signPsbt.psbt, {
57 | network: app.network === "testnet"
58 | ? bitcoin.networks.testnet
59 | : bitcoin.networks.bitcoin,
60 | });
61 | validPsbt = true;
62 | } catch {
63 | try {
64 | newPsbt = bitcoin.Psbt.fromHex(app.signPsbt.psbt, {
65 | network: app.network === "testnet"
66 | ? bitcoin.networks.testnet
67 | : bitcoin.networks.bitcoin,
68 | });
69 | validPsbt = true;
70 | } catch {
71 | try {
72 | newPsbt = bitcoin.Psbt.fromBuffer(app.signPsbt.psbt, {
73 | network: app.network === "testnet"
74 | ? bitcoin.networks.testnet
75 | : bitcoin.networks.bitcoin,
76 | });
77 | validPsbt = true;
78 | } catch {/* empty */}
79 | }
80 | }
81 |
82 | if (!validPsbt || !newPsbt) throw new Error("Invalid Psbt");
83 |
84 | const inputs = [];
85 |
86 | for (const input of newPsbt.data.inputs) {
87 | if (!input.witnessUtxo) return;
88 | const address = Address.fromScriptPubKey(input.witnessUtxo.script);
89 | const value = input.witnessUtxo.value;
90 | inputs.push({ address, value });
91 | }
92 |
93 | const outputs: any[] = [];
94 |
95 | for (const output of newPsbt.txOutputs) {
96 | outputs.push({ address: output.address, value: output.value });
97 | }
98 | const getTotal = (arr: any) =>
99 | arr
100 | .filter((el: any) => el?.address === app.currentAddress)
101 | .reduce((acc: number, el: any) => acc + el.value, 0);
102 |
103 | const myInputsTotal = getTotal(inputs);
104 | const myOutputsTotal = getTotal(outputs);
105 | const total = myOutputsTotal - myInputsTotal;
106 |
107 | setInputsDetails(inputs);
108 | setOutputDetails(outputs);
109 | setTotal(total);
110 | setPsbtToSign(newPsbt);
111 | } catch (error) {
112 | console.log(error as Error);
113 | chrome.runtime.sendMessage({ message: `ReturnErrorOnSignPsbt` });
114 | window.close();
115 | }
116 | }
117 | // eslint-disable-next-line react-hooks/exhaustive-deps
118 | }, [app.signPsbt.psbt]);
119 |
120 | return (
121 |
122 |
123 |
124 |
130 | Sign Transaction
131 |
132 |
139 | {total} sats
140 |
141 | Inputs}
143 | >
144 |
145 | {inputsDetails.map((el: any) => (
146 |
150 |
151 | {el.address && truncateInMiddle(el.address, 20)}
152 |
153 | {el.value} sats
154 |
155 | ))}
156 |
157 |
158 | Outputs}
160 | >
161 |
162 | {outputDetails.map((el: any) => (
163 |
167 |
168 | {el.address && truncateInMiddle(el.address, 20)}
169 |
170 | {el.value} sats
171 |
172 | ))}
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
186 |
192 |
193 |
194 |
195 |
196 | );
197 | };
--------------------------------------------------------------------------------
/src/screens/explore.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from "grommet";
2 | import { AppLayout } from "../components/app-layout";
3 | import { useLocation } from "react-router-dom";
4 |
5 | export const Explore = () => {
6 | const location = useLocation() as { state?: { url?: string } };
7 |
8 | return (
9 |
10 |
11 |
17 |
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/screens/index.ts:
--------------------------------------------------------------------------------
1 | export * from './create-wallet';
2 | export * from './restore-wallet';
3 | export * from './mnemonic';
4 | export * from './send';
5 | export * from './balances';
6 |
--------------------------------------------------------------------------------
/src/screens/mnemonic.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Header, Text } from "grommet";
2 | import * as Icons from "grommet-icons";
3 | import { useLocation, useNavigate } from "react-router-dom";
4 | import { RoutePath } from "../router";
5 |
6 | export const Mnemonic = () => {
7 | const navigate = useNavigate();
8 | const params = useLocation() as { state: { mnemonic: string } };
9 | const mnemonic = params.state?.mnemonic || "";
10 |
11 | const mnemonicGrid = mnemonic.split(" ").reduce((acc, word, index) => {
12 | if (index % 3 === 0) {
13 | acc.push([]);
14 | }
15 | acc[acc.length - 1].push(word);
16 | return acc;
17 | }, [] as string[][]);
18 |
19 | const onBalances = async () => {
20 | navigate(RoutePath.Balances);
21 | };
22 |
23 | const goBack = () => {
24 | navigate(-1);
25 | };
26 |
27 | const copySeed = () => {
28 | try {
29 | navigator.clipboard.writeText(mnemonic);
30 | } catch (e) {
31 | console.error(e);
32 | }
33 | };
34 |
35 | return (
36 |
37 |
38 | } onClick={goBack} />
39 |
40 |
41 |
42 | Click to copy! Save this seed phrase in a secure place
43 | {mnemonicGrid.map((row, rowIndex) => (
44 |
45 | {row.map((word, wordIndex) => (
46 |
53 |
58 | {rowIndex * 3 + wordIndex + 1}{". "}
59 |
60 |
65 | {word}
66 |
67 |
68 | ))}
69 |
70 | ))}
71 |
72 |
78 |
79 |
80 |
81 |
82 | );
83 | };
84 |
--------------------------------------------------------------------------------
/src/screens/modals/reset-storage.tsx:
--------------------------------------------------------------------------------
1 | import { Layer, Box, Button, Text } from 'grommet';
2 | import { useNavigate } from 'react-router-dom';
3 | import { RoutePath } from '../../router';
4 |
5 | export function ResetStorageModal({ onClose}: { onClose: any }) {
6 | const navigate = useNavigate();
7 |
8 | const reset = () => {
9 | localStorage.clear();
10 | navigate(RoutePath.Root);
11 | };
12 |
13 | return (
14 |
20 |
28 | Are you sure you want to reset this wallet storage?
29 | This action cannot be undone.
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/screens/modals/show-address.tsx:
--------------------------------------------------------------------------------
1 | import { Layer, Box, Button, Text } from 'grommet';
2 | import QRCode from 'qrcode.react';
3 | import { useApp } from '../../app';
4 | import { useState } from 'react';
5 |
6 | export function ShowAddressModal({ onClose }: { onClose: any }) {
7 | const app = useApp();
8 | const [copySuccess, setCopySuccess] = useState('');
9 |
10 | const copyToClipboard = async () => {
11 | try {
12 | await navigator.clipboard.writeText(app.currentAddress);
13 | setCopySuccess('Address Copied!');
14 | } catch (error) {
15 | console.error('Failed to copy: ', error);
16 | }
17 | };
18 |
19 | return (
20 |
27 |
36 |
37 |
42 | {app.currentAddress}
43 |
44 | {copySuccess && (
45 |
46 | {copySuccess}
47 |
48 | )}
49 |
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/src/screens/modals/show-mnemonic.tsx:
--------------------------------------------------------------------------------
1 | import { Layer, Box, Text, Button, TextInput } from 'grommet';
2 | import { useState } from 'react';
3 | import { loadWallet } from '../../bitcoin/wallet-storage';
4 |
5 | export function ShowMnemonicModal({ onClose}: { onClose: any }) {
6 | const [mnemonic, setMnemonic] = useState("");
7 | const [password, setPassword] = useState("");
8 |
9 | const mnemonicGrid = () => {
10 | return mnemonic.split(" ").reduce((acc, word, index) => {
11 | if (index % 3 === 0) {
12 | acc.push([]);
13 | }
14 | acc[acc.length - 1].push(word);
15 | return acc;
16 | }, [] as string[][]);
17 | }
18 |
19 | const load = () => {
20 | try {
21 | const wallet = loadWallet(password);
22 | setMnemonic(wallet.mnemonic);
23 | } catch (e) {
24 | alert("Wrong password")
25 | }
26 | }
27 |
28 | return (
29 |
36 |
45 | {mnemonic !== "" ? (
46 | <>
47 | {mnemonicGrid().map((row: any, rowIndex: number) => (
48 |
49 | {row.map((word: string, wordIndex: number) => (
50 |
56 |
61 | {rowIndex * 3 + wordIndex + 1}. {word}
62 |
63 |
64 | ))}
65 |
66 | ))}
67 | >
68 | ) : (
69 |
70 | setPassword(event.target.value)} />
74 | load()} />
75 |
76 | )}
77 |
78 |
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/src/screens/password.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Image, TextInput, Text, Spinner } from "grommet";
2 | import { useEffect, useState } from "react";
3 | import { useNavigate } from "react-router-dom";
4 | import { RoutePath } from "../router";
5 | import { useApp } from "../app";
6 | import { importWallet, importWalletFromWif } from "../bitcoin/wallet";
7 | import { loadWallet } from "../bitcoin/wallet-storage";
8 | import { ResetStorageModal } from "./modals/reset-storage";
9 | import { getPasswordFromSettings, savePasswordInSettings } from "../app/settings";
10 | import { getNetwork } from "../bitcoin/helpers";
11 |
12 | export const Password = () => {
13 | const navigate = useNavigate();
14 | const app = useApp();
15 | const [password, setPassword] = useState("");
16 | const [loading, setLoading] = useState(false);
17 | const [isResetModalOpen, setResetModalOpen] = useState(false);
18 | const [errors, setErrors] = useState<{
19 | password?: string;
20 | error?: string;
21 | }>({});
22 |
23 | useEffect(() => {
24 | const pass = getPasswordFromSettings();
25 | if(pass) {
26 | handleLoadWallet(pass);
27 | }
28 | }, [])
29 |
30 | const handleLoadWallet = (pass: string) => {
31 | setLoading(true);
32 |
33 | try {
34 | const wallet = loadWallet(pass);
35 |
36 | if (wallet.mnemonic === "") {
37 | throw new Error("Invalid password");
38 | }
39 |
40 | const formattedMnemonic = wallet.mnemonic?.split(' ').filter((el: string)=>el !== '');
41 |
42 | if (formattedMnemonic.length === 12) {
43 | const nextAccount = importWallet(wallet.mnemonic, getNetwork(wallet.network), wallet.addressIndex);
44 |
45 | app.setAccount(nextAccount);
46 | app.setNetwork(wallet.network);
47 | app.setCurrentAddress(nextAccount.address, wallet.addressIndex)
48 | app.setAddresses(wallet.addresses);
49 | } else {
50 | const nextAccount = importWalletFromWif(wallet.mnemonic, getNetwork(wallet.network), wallet.addressIndex);
51 |
52 | app.setAccount(nextAccount);
53 | app.setNetwork(wallet.network);
54 | app.setCurrentAddress(nextAccount.address, wallet.addressIndex)
55 | app.setAddresses(wallet.addresses);
56 | }
57 | } catch(e) {
58 | setErrors({
59 | error: (e as Error).message,
60 | });
61 | setLoading(false);
62 | return;
63 | }
64 |
65 | setLoading(false);
66 | if (app.signPsbt.psbt) {
67 | navigate(RoutePath.DecodeAndSignPsbt);
68 | return;
69 | }
70 | if (app.signMessage.msg) {
71 | navigate(RoutePath.SignMessage);
72 | return;
73 | }
74 | if (app.verifySign.signature) {
75 | navigate(RoutePath.VerifySign);
76 | return;
77 | }
78 | navigate(RoutePath.Balances);
79 | }
80 |
81 | const onNext = async () => {
82 | setErrors({});
83 |
84 | if (!password) {
85 | setErrors({
86 | password: "Password is required",
87 | });
88 | return;
89 | }
90 |
91 | savePasswordInSettings(password);
92 |
93 | handleLoadWallet(password);
94 | };
95 |
96 | return (
97 |
98 |
99 |
100 |
101 | {!!errors.error && (
102 |
103 | {errors.error}
104 |
105 | )}
106 |
107 | setPassword(e.target.value)}
112 | width="100%"
113 | />
114 | {!!errors.password && (
115 |
120 | {errors.password}
121 |
122 | )}
123 |
124 | {isResetModalOpen && (
125 | setResetModalOpen(false)} />
126 | )}
127 | {loading && }
128 |
129 |
136 | setResetModalOpen(true)}
141 | disabled={loading}
142 | />
143 |
144 |
145 |
146 |
147 | );
148 | };
149 |
--------------------------------------------------------------------------------
/src/screens/restore-wallet.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Text, Image, Box, TextInput, Button, Spinner } from "grommet";
3 | import { Layout } from "../components";
4 | import { useRestoreWallet } from "../hooks";
5 | import { useNavigate } from "react-router-dom";
6 | import { RoutePath } from "../router";
7 | import { savePasswordInSettings } from "../app/settings";
8 |
9 | export const RestoreWallet = (): JSX.Element => {
10 | const restoreWallet = useRestoreWallet();
11 | const navigate = useNavigate();
12 |
13 | const [password, setPassword] = useState("");
14 | const [confirmPassword, setConfirmPassword] = useState("");
15 | const [seedPhrase, setSeedPhrase] = useState("");
16 |
17 | const [errors, setErrors] = useState<{
18 | password?: string;
19 | confirmPassword?: string;
20 | seedPhrase?: string;
21 | error?: string;
22 | }>({});
23 |
24 | const onRestoreWallet = async () => {
25 | if (!password) {
26 | setErrors({
27 | password: "Password is required",
28 | });
29 | return;
30 | } else if (password.length < 8) {
31 | setErrors({
32 | password:
33 | "Password must be at least 8 characters long",
34 | });
35 | return;
36 | } else if (password !== confirmPassword) {
37 | setErrors({
38 | confirmPassword: "Passwords do not match",
39 | });
40 | return;
41 | }
42 |
43 | try {
44 | await restoreWallet.dispatch(seedPhrase, password);
45 | } catch (e) {
46 | setErrors({
47 | error: (e as Error).message,
48 | });
49 | return;
50 | }
51 |
52 | savePasswordInSettings(password);
53 |
54 | navigate(RoutePath.Balances);
55 | };
56 |
57 | return (
58 |
59 |
60 |
61 |
62 | {!!errors.error && (
63 |
64 | {errors.error}
65 |
66 | )}
67 |
68 | setPassword(e.target.value)}
73 | width="100%"
74 | />
75 | {!!errors.password && (
76 |
77 | {errors.password}
78 |
79 | )}
80 |
81 |
82 | setConfirmPassword(e.target.value)}
87 | width="100%"
88 | />
89 | {!!errors.confirmPassword && (
90 |
91 | {errors.confirmPassword}
92 |
93 | )}
94 |
95 |
96 | setSeedPhrase(e.target.value)}
100 | />
101 |
102 | {!!errors.seedPhrase && (
103 |
104 | {errors.seedPhrase}
105 |
106 | )}
107 | {restoreWallet.loading && }
108 |
109 |
116 |
117 |
118 |
119 |
120 | );
121 | };
122 |
--------------------------------------------------------------------------------
/src/screens/send.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Select, TextInput, Text, Spinner, Anchor } from "grommet";
2 | import { useNavigate, useLocation } from "react-router-dom";
3 | import { useEffect, useMemo, useState } from "react";
4 | import { bigIntToString, getNetwork, parseStringToBigInt, validateAddress } from "../bitcoin/helpers";
5 | import { SetFees } from "../components";
6 | import { useSendSats, useSendTokens } from "../hooks";
7 | import { useApp } from "../app";
8 | import { useSafeBalances } from "../hooks/safe-balances.hook";
9 | import { RoutePath } from "../router";
10 | import { AppLayout } from "../components/app-layout";
11 |
12 | export const Send = () => {
13 | const location = useLocation();
14 | const navigate = useNavigate();
15 | const app = useApp();
16 | const sendSats = useSendSats();
17 | const sendTokens = useSendTokens();
18 | const balances = useSafeBalances();
19 | const [ticker, setTicker] = useState(location?.state?.ticker || '');
20 | const [address, setAddress] = useState('');
21 | const [amount, setAmount] = useState('');
22 | const [error, setError] = useState("");
23 | const [loading, setLoading] = useState(false);
24 |
25 | const tickers = useMemo(
26 | () => Object.keys(balances.data).map((value) => value.toUpperCase()),
27 | [balances.data]
28 | );
29 |
30 | useEffect(() => {
31 | if (!ticker && tickers.length > 0) {
32 | setTicker(tickers[0]);
33 | }
34 | }, [ticker, tickers]);
35 |
36 |
37 | useEffect(() => {
38 | if (location?.state?.satoshi) {
39 | if (location?.state?.ticker && location?.state?.id) {
40 | if (app?.tokens?.length > 0) {
41 | const token = app.tokens.filter(
42 | (t) =>
43 | t.tick === location?.state?.ticker?.toLowerCase() &&
44 | t.id === parseInt(location?.state?.id)
45 | );
46 | if (token) {
47 | const decimals = token[0].dec;
48 | const nextAmount = (
49 | parseInt(location?.state?.satoshi) / Math.pow(10, decimals)
50 | ).toString();
51 | setTicker(
52 | location?.state?.ticker &&
53 | location?.state?.ticker + ":" + location?.state?.id
54 | );
55 | setAddress(location?.state?.toAddress);
56 | setAmount(nextAmount);
57 | } else {
58 | console.error("Invalid token");
59 | }
60 | } else {
61 | console.error("No tokens found");
62 | }
63 | } else {
64 | const nextAmount = (
65 | parseInt(location?.state?.satoshi) / Math.pow(10, 8)
66 | ).toString();
67 | setAddress(location?.state?.toAddress);
68 | setAmount(nextAmount);
69 | }
70 | }
71 | }, [
72 | app.tokens,
73 | location?.state,
74 | location?.state?.id,
75 | location?.state?.satoshi,
76 | location?.state?.ticker,
77 | ]);
78 |
79 | const send = async () => {
80 | setError("");
81 |
82 | if(!validateAddress(address, getNetwork(app.network))) {
83 | setError("Invalid address");
84 | return;
85 | }
86 |
87 | const splittedTicker = ticker.split(":");
88 |
89 | const token = app.tokens.filter((t) => t.tick === splittedTicker[0].toLowerCase() && t.id === parseInt(splittedTicker[1]));
90 |
91 | try {
92 | if (ticker.toLowerCase() === 'btc') {
93 | if (!parseStringToBigInt(amount, 8)) {
94 | throw new Error("Amount exceeds 8 decimals");
95 | }
96 | } else {
97 | if (!parseStringToBigInt(amount, token[0].dec)) {
98 | throw new Error("Amount exceeds 8 decimals");
99 | }
100 | }
101 | } catch (e) {
102 | if (!(e instanceof Error)) {
103 | setError("Unknown error");
104 | return;
105 | }
106 | setError(e.message);
107 | return;
108 | }
109 |
110 | setLoading(true);
111 |
112 | if (ticker.toLowerCase() === "btc") {
113 | try {
114 | const tx = await sendSats.dispatch(address, bigIntToString(parseStringToBigInt(amount, 8), 8), `${app.feerate}`);
115 | if ((tx?.vin?.length || 0) > 0 && (tx?.vout?.length || 0) > 0) {
116 | navigate(RoutePath.ConfirmTransaction, { state: {tx, fromWeb: location?.state?.satoshi ? true : false} })
117 | } else {
118 | setLoading(false);
119 | throw new Error("Something went wrong, please try again");
120 | }
121 | } catch (e) {
122 | setError((e as Error).message);
123 | }
124 | setLoading(false);
125 | return;
126 | }
127 |
128 | const tickerSplit = ticker.split(":");
129 |
130 | try {
131 | const tx = await sendTokens.dispatch(
132 | address,
133 | tickerSplit[0],
134 | tickerSplit[1],
135 | amount,
136 | `${app.feerate}`
137 | );
138 | setLoading(false);
139 | if ((tx?.vin?.length || 0) > 0 && (tx?.vout?.length || 0) > 0) {
140 | navigate(RoutePath.ConfirmTransaction, { state: {tx, fromWeb: location?.state?.satoshi ? true : false} })
141 | } else {
142 | throw new Error("Something went wrong, please try again");
143 | }
144 | // const txid = await sendTransaction(hex, app.network);
145 | // save({txid, from: app.currentAddress, to: address, amount, token: ticker, timestamp: Date.now(), confirmed: false });
146 | } catch (e) {
147 | setError((e as Error).message);
148 | setLoading(false);
149 | return;
150 | }
151 | };
152 |
153 | return (
154 |
155 |
156 | {!!error && ({error} )}
157 | {ticker && balances.data[ticker.toLowerCase()] && (
158 |
159 | MAX SAFE
160 |
161 | {
162 | setAmount(balances.data[ticker.toLowerCase()])
163 | }}>{balances.data[ticker.toLowerCase()]}
164 | {ticker}
165 |
166 |
167 | )}
168 |
169 |
170 | setAmount(e.target.value)}
173 | max={balances.data[ticker?.toLowerCase() || ""]}
174 | value={amount}
175 | />
176 |
177 |
178 | setTicker(option)}
182 | />
183 |
184 |
185 |
186 |
187 | setAddress(e.target.value)}
192 | />
193 |
194 |
195 |
196 | {loading && }
197 |
204 |
205 |
206 | );
207 | };
208 |
--------------------------------------------------------------------------------
/src/screens/settings.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Text } from "grommet";
2 | import { Next } from "grommet-icons";
3 | import { useNavigate } from "react-router-dom";
4 | import { RoutePath } from "../router";
5 | import { useApp } from "../app";
6 | import { AppLayout } from "../components/app-layout";
7 | import { Icon } from "../components/icon";
8 | import { colors } from "../theme";
9 | import { ResetStorageModal } from "./modals/reset-storage";
10 | import { ShowMnemonicModal } from "./modals/show-mnemonic";
11 | import { useState } from "react";
12 |
13 | export const Settings = () => {
14 | const navigate = useNavigate();
15 | const app = useApp();
16 | const [showReset, setShowReset] = useState(false);
17 | const [showMnemo, setShowMnemo] = useState(false);
18 |
19 | return (
20 |
21 | {showMnemo && (
22 | setShowMnemo(false)} />
23 | )}
24 | {showReset && (
25 | setShowReset(false)} />
26 | )}
27 |
35 | navigate(RoutePath.SwitchNetwork)}
43 | >
44 |
45 |
46 | Network
47 |
48 |
49 | {app.network.toUpperCase()}
50 |
51 |
52 |
53 |
54 |
55 |
62 | setShowMnemo(true)}
70 | >
71 |
72 |
73 |
74 | Show Seed or Wif
75 |
76 |
77 |
78 |
79 | setShowReset(true)}
87 | >
88 |
89 |
90 |
91 | Exit Wallet
92 |
93 |
94 |
95 |
96 |
97 |
98 | );
99 | };
100 |
--------------------------------------------------------------------------------
/src/screens/sign-message.tsx:
--------------------------------------------------------------------------------
1 | import { Layout } from "../components";
2 | import { Box, Button, Footer, Text } from "grommet";
3 | import { useApp } from "../app";
4 | import { Signer } from "bip322-js";
5 | import * as bitcoin from "bitcoinjs-lib";
6 | import { colors } from "../theme";
7 |
8 | export const SignMessage = (): JSX.Element => {
9 | const app = useApp();
10 |
11 | const onSign = async () => {
12 | try {
13 | const privateKey = app.account.account.toWIF();
14 | const address = app.currentAddress;
15 | const signature = Signer.sign(
16 | privateKey,
17 | address,
18 | app.signMessage.msg,
19 | app.network === "testnet" ? bitcoin.networks.testnet : bitcoin.networks.bitcoin,
20 | );
21 | await chrome.runtime.sendMessage({ message: `ReturnSignMessage;${signature}` });
22 | window.close();
23 | } catch (e) {
24 | console.log(e as Error);
25 | await chrome.runtime.sendMessage({ message: `ReturnErrorOnSignMessage` });
26 | window.close();
27 | }
28 | };
29 |
30 | const onClose = async () => {
31 | await chrome.runtime.sendMessage({ message: `ClientRejectSignMessage` });
32 | window.close();
33 | };
34 |
35 | return (
36 |
37 |
38 | SIGNATURE REQUEST
39 |
40 | Only sign this message if you fully understand the content and trust
41 | the requesting site.
42 |
43 | You are signing
44 |
45 | {app.signMessage.msg}
46 |
47 |
48 |
49 |
50 |
51 |
57 |
63 |
64 |
65 |
66 |
67 | );
68 | };
--------------------------------------------------------------------------------
/src/screens/switch-network.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Text } from "grommet";
2 | import { Checkmark } from "grommet-icons";
3 | import { useApp } from "../app";
4 | import { RoutePath } from "../router";
5 | import { useNavigate } from "react-router-dom";
6 | import { AppLayout } from "../components/app-layout";
7 |
8 | export const SwitchNetwork = () => {
9 | const app = useApp();
10 | const navigate = useNavigate();
11 |
12 | return (
13 |
14 |
15 |
23 | {
26 | app.setNetwork("mainnet");
27 | navigate(RoutePath.Password);
28 | }}
29 | >
30 |
31 | MAINNET
32 |
33 |
34 | {app.network === "mainnet" && }
35 |
36 |
44 | {
47 | app.setNetwork("testnet");
48 | navigate(RoutePath.Password);
49 | }}
50 | >
51 |
52 | TESTNET
53 |
54 |
55 | {app.network === "testnet" && }
56 |
57 |
58 |
59 | );
60 | };
61 |
--------------------------------------------------------------------------------
/src/screens/verify-sign.tsx:
--------------------------------------------------------------------------------
1 | import { Layout } from "../components";
2 | import { Box, Button, Footer, Text } from "grommet";
3 | import { useApp } from "../app";
4 | import { Verifier } from "bip322-js";
5 | import { colors } from "../theme";
6 | import { useEffect } from "react";
7 |
8 | export const VerifySign = (): JSX.Element => {
9 | const app = useApp();
10 |
11 | const onVerify = async () => {
12 | try {
13 | const validity = Verifier.verifySignature(
14 | app.currentAddress,
15 | app.verifySign.msg,
16 | app.verifySign.signature as string
17 | );
18 | await chrome.runtime.sendMessage({ message: `ReturnVerifySign;${validity}` });
19 | window.close();
20 | } catch (e) {
21 | console.log(e as Error);
22 | await chrome.runtime.sendMessage({ message: `ReturnErrorOnVerifySign` });
23 | window.close();
24 | }
25 | };
26 |
27 | const onClose = () => {
28 | window.close();
29 | };
30 |
31 | useEffect(()=>{
32 | onVerify();
33 | },[])
34 |
35 | return (
36 |
37 |
38 | VERIFY MESSAGE
39 |
40 | Verifying the signature for
41 |
42 |
43 | {app.verifySign.msg}
44 |
45 |
46 |
47 |
48 |
49 |
55 |
56 |
57 |
58 |
59 | );
60 | };
--------------------------------------------------------------------------------
/src/theme/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./theme-constants";
2 | export * from "./theme";
3 | export * from "./theme-provider";
4 |
--------------------------------------------------------------------------------
/src/theme/theme-constants.ts:
--------------------------------------------------------------------------------
1 | import { ThemeType } from "grommet";
2 |
3 | export const colors = {
4 | primary: "#6FFFB0",
5 | secondary: "#D67BFF",
6 | dark: "#1C1917",
7 | inactive: "#858585",
8 | odd: "#272727",
9 | even: "#1C1917",
10 | };
11 |
12 | export const themeConstants: ThemeType = {
13 | global: {
14 | font: {
15 | family: "'Roboto', sans-serif",
16 | },
17 | colors: {
18 | brand: colors.primary
19 | },
20 | },
21 | button: {
22 | size: {
23 | small: {
24 | border: {
25 | radius: "3px",
26 | },
27 | },
28 | medium: {
29 | border: {
30 | radius: "3px",
31 | },
32 | },
33 | large: {
34 | border: {
35 | radius: "3px",
36 | },
37 | }
38 | },
39 | border: {
40 | radius: "3px",
41 | },
42 | },
43 | };
44 |
--------------------------------------------------------------------------------
/src/theme/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | import { Grommet } from "grommet";
2 | import { theme } from "./theme";
3 |
4 | export type ThemeProviderProps = {
5 | children: React.ReactNode;
6 | };
7 |
8 | export const ThemeProvider = (props: ThemeProviderProps) => (
9 |
10 | {props.children}
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/src/theme/theme.ts:
--------------------------------------------------------------------------------
1 | import { grommet } from "grommet";
2 | import { deepMerge } from "grommet/utils";
3 | import { themeConstants } from "./theme-constants";
4 |
5 | export const theme = deepMerge(grommet, themeConstants);
6 |
--------------------------------------------------------------------------------
/src/transfer/get-ordinals-unspents.ts:
--------------------------------------------------------------------------------
1 | import { constants } from "../constants/constants";
2 | import { GetUnspentsParams } from "./get-unspents";
3 |
4 | export type OrdinalsUnspent = {
5 | id: string;
6 | number: number;
7 | address: string;
8 | genesis_address: string;
9 | genesis_block_height: number;
10 | genesis_block_hash: string;
11 | genesis_tx_id: string;
12 | genesis_fee: string;
13 | genesis_timestamp: number;
14 | tx_id: string;
15 | location: string;
16 | output: string;
17 | value: string;
18 | offset: string;
19 | sat_ordinal: string;
20 | sat_rarity: string;
21 | sat_coinbase_height: number;
22 | mime_type: string;
23 | content_type: string;
24 | content_length: number;
25 | timestamp: number;
26 | curse_type: string | null;
27 | recursive: boolean;
28 | recursion_refs: string | null;
29 | };
30 |
31 | export const getOrdinalsUnspents = async ({
32 | network = "mainnet",
33 | cursor = null,
34 | ...params
35 | }: GetUnspentsParams) => {
36 | const url = `${
37 | network === "mainnet"
38 | ? constants.ordinals_indexer.main
39 | : constants.ordinals_indexer.testnet
40 | }${constants.ordinals_indexer.api.unspents
41 | .replace(":limit", "60")
42 | .replace(":address", params.address)
43 | .replace(":offset", cursor ? cursor : "0")
44 | }`;
45 |
46 | const res: { results: OrdinalsUnspent[] } = await (await fetch(url)).json();
47 |
48 | return res.results || [];
49 | };
50 |
--------------------------------------------------------------------------------
/src/transfer/get-pipe-unspents.ts:
--------------------------------------------------------------------------------
1 | import { constants } from "../constants/constants";
2 | import { GetUnspentsParams } from "./get-unspents";
3 |
4 | export type PipeUnspent = {
5 | address: string;
6 | txId: string;
7 | vout: number;
8 | amount: string;
9 | decimals: number;
10 | ticker: string;
11 | id: number;
12 | block: number;
13 | createdAt: string;
14 | };
15 |
16 | export const getPipeUnspents = async ({
17 | network = "mainnet",
18 | cursor = null,
19 | ...params
20 | }: GetUnspentsParams) => {
21 | const url = `${
22 | network === "mainnet"
23 | ? constants.pipe_indexer.main
24 | : constants.pipe_indexer.testnet
25 | }${constants.pipe_indexer.api.unspents
26 | .replace(":limit", "50")
27 | .replace(":address", params.address)
28 | .replace(":page", cursor ? cursor : "")
29 | }`;
30 |
31 | const res: PipeUnspent[] = await (await fetch(url)).json();
32 |
33 | return res;
34 | };
35 |
--------------------------------------------------------------------------------
/src/transfer/get-unspents.ts:
--------------------------------------------------------------------------------
1 | import { constants } from "../constants/constants";
2 |
3 | export type TxRef = {
4 | tx_hash: string;
5 | block_height: number;
6 | tx_input_n: number;
7 | tx_output_n: number;
8 | value: number;
9 | ref_balance: number;
10 | spent: boolean;
11 | confirmations: number;
12 | confirmed: string;
13 | double_spend: boolean;
14 | };
15 |
16 | export type GetUnspentsResponse = {
17 | address: string;
18 | total_received: number;
19 | total_sent: number;
20 | balance: number;
21 | unconfirmed_balance: number;
22 | final_balance: number;
23 | n_tx: number;
24 | unconfirmed_n_tx: number;
25 | final_n_tx: number;
26 | txrefs?: TxRef[];
27 | tx_url: string;
28 | };
29 |
30 | export type GetUnspentsParams = {
31 | address: string;
32 | cursor?: string | null;
33 | network?: "mainnet" | "testnet";
34 | };
35 |
36 | export const getUnspents = async ({
37 | network = "mainnet",
38 | cursor = null,
39 | ...params
40 | }: GetUnspentsParams) => {
41 | const url = `${
42 | network === "mainnet" ? constants.bitcoin_indexer.main : constants.bitcoin_indexer.testnet
43 | }${constants.bitcoin_indexer.api.unspents
44 | .replace(":address", params.address)
45 | .replace(":before", cursor ? cursor : "")
46 | }`;
47 |
48 | const res: GetUnspentsResponse = await (await fetch(url)).json();
49 |
50 | return res;
51 | };
52 |
--------------------------------------------------------------------------------
/src/transfer/prepare-transfer-pipe.ts:
--------------------------------------------------------------------------------
1 | import { Signer, Tap, Tx, TxTemplate, Word } from "@cmdcode/tapscript";
2 | import { addressToScriptPubKey, bigIntToString, textToHex, toBytes, toInt26 } from "../bitcoin/helpers";
3 | import { TxRef } from "./get-unspents";
4 | import { PipeUnspent } from "./get-pipe-unspents";
5 | import { constants } from "../constants/constants";
6 | import { cleanFloat } from "../utils/clean-float";
7 |
8 | export type PrepareTransferPipeParams = {
9 | ticker: string;
10 | id: string;
11 | amount: string;
12 | decimals: string;
13 | from: string;
14 | to: string;
15 | feerate: string;
16 | privateKey: Uint8Array;
17 | fee?: string;
18 | unspents: TxRef[];
19 | pipeUnspents: PipeUnspent[];
20 | network?: "mainnet" | "testnet";
21 | };
22 |
23 | export const prepareTransferPipe = ({
24 | network = "mainnet",
25 | ...params
26 | }: PrepareTransferPipeParams) => {
27 | const ticker = params.ticker;
28 | const id = params.id;
29 | const decimals = params.decimals;
30 | const privateKey = params.privateKey;
31 | const unspents = params.unspents;
32 | const amount = BigInt(params.amount);
33 | const feerate = BigInt(params.feerate);
34 | const from = params.from;
35 | const to = params.to;
36 | let fee = params.fee ? BigInt(params.fee) : 0n;
37 | const pipeUnspents = params.pipeUnspents;
38 |
39 | const vin: TxTemplate["vin"] = [];
40 | const vout: TxTemplate["vout"] = [];
41 |
42 | let pipeAmount = 0n;
43 |
44 | for (const pipeUnspent of pipeUnspents) {
45 | vin.push({
46 | txid: pipeUnspent.txId,
47 | vout: pipeUnspent.vout,
48 | prevout: {
49 | value: BigInt(constants.utxo_dummy_value),
50 | scriptPubKey: addressToScriptPubKey(from, network)
51 | }
52 | });
53 |
54 | pipeAmount += BigInt(pipeUnspent.amount);
55 | }
56 |
57 | const inputAmount = vin.map(v => v.prevout?.value as bigint).reduce((a, b) => a + b, 0n);
58 | let total = 0n;
59 |
60 | for (const unspent of unspents) {
61 | if (total >= inputAmount + fee) {
62 | break;
63 | }
64 |
65 | vin.push({
66 | txid: unspent.tx_hash,
67 | vout: unspent.tx_output_n,
68 | prevout: {
69 | value: BigInt(unspent.value),
70 | scriptPubKey: addressToScriptPubKey(from, network),
71 | },
72 | });
73 | total += BigInt(unspent.value);
74 | }
75 |
76 | vout.push({
77 | value: BigInt(constants.utxo_dummy_value),
78 | scriptPubKey: addressToScriptPubKey(to, network),
79 | });
80 |
81 | const ec = new TextEncoder();
82 | const token_change = pipeAmount - amount;
83 |
84 | const conv_amount = cleanFloat(bigIntToString(amount, Number(decimals)));
85 | const conv_change = cleanFloat(bigIntToString(token_change, Number(decimals)));
86 |
87 | if (token_change <= 0n) {
88 | vout.push({
89 | scriptPubKey: [ 'OP_RETURN', ec.encode('P'), ec.encode('T'),
90 | toBytes(toInt26(ticker)) as Word, toBytes(BigInt(id)) as Word, toBytes(0n) as Word, textToHex(conv_amount)
91 | ]
92 | })
93 | } else {
94 | vout.push({
95 | value: constants.utxo_dummy_value,
96 | scriptPubKey: addressToScriptPubKey(from, network)
97 | });
98 |
99 | vout.push({
100 | scriptPubKey: [ 'OP_RETURN', ec.encode('P'), ec.encode('T'),
101 | toBytes(toInt26(ticker)) as Word, toBytes(BigInt(id)) as Word, toBytes(0n) as Word, textToHex(conv_amount),
102 | toBytes(toInt26(ticker)) as Word, toBytes(BigInt(id)) as Word, toBytes(1n) as Word, textToHex(conv_change)
103 | ]
104 | });
105 | }
106 |
107 | vout.push({
108 | value: total - inputAmount - fee,
109 | scriptPubKey: addressToScriptPubKey(from, network),
110 | });
111 |
112 | const [tseckey] = Tap.getSecKey(privateKey);
113 |
114 | const testTx = Tx.create({
115 | vin: vin,
116 | vout: vout,
117 | });
118 |
119 | for (let i = 0; i < vin.length; i++) {
120 | const sig = Signer.taproot.sign(tseckey, testTx, i);
121 | testTx.vin[i].witness = [sig];
122 | Signer.taproot.verify(testTx, i, { throws: true });
123 | }
124 |
125 | const txSize = Tx.util.getTxSize(testTx);
126 | const vsize = BigInt(txSize.vsize);
127 |
128 | fee = vsize * feerate;
129 |
130 | vout[vout.length - 1].value = total - inputAmount - fee;
131 |
132 | const tx = Tx.create({
133 | vin: vin,
134 | vout: vout,
135 | });
136 |
137 | for (let i = 0; i < vin.length; i++) {
138 | const sig = Signer.taproot.sign(tseckey, tx, i);
139 | tx.vin[i].witness = [sig];
140 | Signer.taproot.verify(tx, i, { throws: true });
141 | }
142 |
143 | return {
144 | tx,
145 | hex: Tx.encode(tx).hex,
146 | vin,
147 | vout,
148 | vsize: vsize.toString(),
149 | fee: fee.toString(),
150 | change: vout[vout.length - 1].value?.toString() || "0",
151 | pipeChange: conv_change,
152 | pipeAmount: conv_amount,
153 | };
154 | };
155 |
--------------------------------------------------------------------------------
/src/transfer/prepare-transfer-sats.ts:
--------------------------------------------------------------------------------
1 | import { Signer, Tap, Tx, TxTemplate } from "@cmdcode/tapscript";
2 | import { addressToScriptPubKey } from "../bitcoin/helpers";
3 | import { TxRef } from "./get-unspents";
4 |
5 | export type PrepareTransferSatsParams = {
6 | amount: string;
7 | from: string;
8 | to: string;
9 | feerate: string;
10 | privateKey: Uint8Array;
11 | fee?: string;
12 | unspents: TxRef[];
13 | network?: "mainnet" | "testnet";
14 | };
15 |
16 | export const prepareTransferSats = ({
17 | network = "mainnet",
18 | ...params
19 | }: PrepareTransferSatsParams) => {
20 | const privateKey = params.privateKey;
21 | const unspents = params.unspents;
22 | const amount = BigInt(params.amount);
23 | const feerate = BigInt(params.feerate);
24 | const from = params.from;
25 | const to = params.to;
26 | let fee = params.fee ? BigInt(params.fee) : 0n;
27 |
28 | const vin: TxTemplate["vin"] = [];
29 | const vout: TxTemplate["vout"] = [];
30 |
31 | let total = 0n;
32 |
33 | for (const unspent of unspents) {
34 | if (total >= amount + fee) {
35 | break;
36 | }
37 |
38 | vin.push({
39 | txid: unspent.tx_hash,
40 | vout: unspent.tx_output_n,
41 | prevout: {
42 | value: BigInt(unspent.value),
43 | scriptPubKey: addressToScriptPubKey(from, network),
44 | },
45 | });
46 | total += BigInt(unspent.value);
47 | }
48 |
49 | vout.push({
50 | value: amount,
51 | scriptPubKey: addressToScriptPubKey(to, network),
52 | });
53 |
54 | vout.push({
55 | value: total - amount - fee,
56 | scriptPubKey: addressToScriptPubKey(from, network),
57 | });
58 |
59 | const [tseckey] = Tap.getSecKey(privateKey);
60 |
61 | const testTx = Tx.create({
62 | vin: vin,
63 | vout: vout,
64 | });
65 |
66 | for (let i = 0; i < vin.length; i++) {
67 | const sig = Signer.taproot.sign(tseckey, testTx, i);
68 | testTx.vin[i].witness = [sig];
69 | Signer.taproot.verify(testTx, i, { throws: true });
70 | }
71 |
72 | const txSize = Tx.util.getTxSize(testTx);
73 | const vsize = BigInt(txSize.vsize);
74 |
75 | fee = vsize * feerate;
76 |
77 | vout[vout.length - 1].value = total - amount - fee;
78 |
79 | const tx = Tx.create({
80 | vin: vin,
81 | vout: vout,
82 | });
83 |
84 | for (let i = 0; i < vin.length; i++) {
85 | const sig = Signer.taproot.sign(tseckey, tx, i);
86 | tx.vin[i].witness = [sig];
87 | Signer.taproot.verify(tx, i, { throws: true });
88 | }
89 |
90 | return {
91 | tx,
92 | hex: Tx.encode(tx).hex,
93 | vin,
94 | vout,
95 | vsize: vsize.toString(),
96 | fee: fee.toString(),
97 | change: vout[vout.length - 1].value?.toString() || "0",
98 | };
99 | };
100 |
--------------------------------------------------------------------------------
/src/transfer/select-all-ordinals-unspents.ts:
--------------------------------------------------------------------------------
1 | import { OrdinalsUnspent, getOrdinalsUnspents } from "./get-ordinals-unspents";
2 | import { SelectAllUnspentsParams } from "./select-all-unspents";
3 |
4 | export const selectAllOrdinalsUnspents = async ({
5 | network = "mainnet",
6 | ...params
7 | }: SelectAllUnspentsParams) => {
8 | const unspents: OrdinalsUnspent[] = [];
9 |
10 | let cursor = 0;
11 | let hasMore = true;
12 | let sleep = 1000;
13 |
14 | do {
15 | try {
16 | const nextUnspents = await getOrdinalsUnspents({ address: params.address, cursor: cursor.toString(), network });
17 |
18 | if (nextUnspents.length === 0) {
19 | hasMore = false;
20 | break;
21 | }
22 |
23 | unspents.push(...nextUnspents);
24 |
25 | cursor += 60;
26 | sleep = 1000;
27 | } catch (e) {
28 | console.error(e);
29 | sleep *= 2;
30 | await new Promise(resolve => setTimeout(resolve, sleep));
31 | }
32 | } while (hasMore);
33 |
34 | return unspents;
35 | };
36 |
--------------------------------------------------------------------------------
/src/transfer/select-all-pipe-unspents.ts:
--------------------------------------------------------------------------------
1 | import { PipeUnspent, getPipeUnspents } from "./get-pipe-unspents";
2 | import { SelectAllUnspentsParams } from "./select-all-unspents";
3 |
4 | export const selectAllPipeUnspents = async ({
5 | network = "mainnet",
6 | ...params
7 | }: SelectAllUnspentsParams) => {
8 | const unspents: PipeUnspent[] = [];
9 |
10 | let cursor = 1;
11 | let hasMore = true;
12 | let sleep = 1000;
13 |
14 | do {
15 | try {
16 | const nextUnspents = await getPipeUnspents({ address: params.address, cursor: cursor.toString(), network });
17 |
18 | if (nextUnspents.length === 0) {
19 | hasMore = false;
20 | break;
21 | }
22 |
23 | unspents.push(...nextUnspents);
24 |
25 | cursor += 1;
26 | sleep = 1000;
27 | } catch (e) {
28 | console.error(e);
29 | sleep *= 2;
30 | await new Promise(resolve => setTimeout(resolve, sleep));
31 | }
32 | } while (hasMore);
33 |
34 | return unspents;
35 | };
36 |
--------------------------------------------------------------------------------
/src/transfer/select-all-unspents.ts:
--------------------------------------------------------------------------------
1 | import { TxRef, getUnspents } from "./get-unspents";
2 |
3 | export type SelectAllUnspentsParams = {
4 | address: string;
5 | network?: "mainnet" | "testnet";
6 | }
7 |
8 | export const selectAllUnspents = async ({
9 | network = "mainnet",
10 | ...params
11 | }: SelectAllUnspentsParams) => {
12 | const unspents: TxRef[] = [];
13 |
14 | let cursor = null;
15 | let hasMore = true;
16 | let sleep = 1000;
17 |
18 | do {
19 | try {
20 | const nextUnspents = await getUnspents({ address: params.address, cursor: cursor?.toString(), network });
21 |
22 | if (!nextUnspents.txrefs || nextUnspents.txrefs.length === 0) {
23 | hasMore = false;
24 | break;
25 | }
26 |
27 | unspents.push(...nextUnspents.txrefs);
28 |
29 | cursor = nextUnspents.txrefs[nextUnspents.txrefs.length - 1].block_height;
30 | sleep = 1000;
31 | } catch (e) {
32 | console.error(e);
33 | sleep *= 2;
34 | await new Promise(resolve => setTimeout(resolve, sleep));
35 | }
36 | } while (hasMore);
37 |
38 | return unspents;
39 | };
40 |
--------------------------------------------------------------------------------
/src/transfer/select-unspents.ts:
--------------------------------------------------------------------------------
1 | import { TxRef, getUnspents } from "./get-unspents";
2 | import { constants } from "../constants/constants";
3 |
4 | export type SelectUnspentsParams = {
5 | address: string;
6 | amount: string;
7 | network?: "mainnet" | "testnet";
8 | exclude?: {
9 | txId: string;
10 | vout: number;
11 | }[];
12 | };
13 |
14 | export const selectUnspents = async ({
15 | network = "mainnet",
16 | ...params
17 | }: SelectUnspentsParams) => {
18 | const amount = BigInt(params.amount);
19 | const unspents: TxRef[] = [];
20 |
21 | let total = 0n;
22 | let cursor = null;
23 | let sleep = 1000;
24 | const currSpentsStr = localStorage.getItem("currSpents");
25 | const currSpents = JSON.parse(currSpentsStr || "[]");
26 | let exclude = params?.exclude || [];
27 | exclude = [...exclude,...currSpents];
28 |
29 | do {
30 | try {
31 | const nextUnspents = await getUnspents({
32 | address: params.address,
33 | cursor,
34 | network,
35 | });
36 |
37 | if (!nextUnspents.txrefs || nextUnspents.txrefs.length === 0) {
38 | break;
39 | }
40 |
41 | for (const utxo of nextUnspents.txrefs) {
42 | if (
43 | exclude?.some(
44 | (exclud) =>
45 | exclud.txId === utxo.tx_hash && exclud.vout === utxo.tx_output_n
46 | )
47 | ) {
48 | continue;
49 | }
50 |
51 | if (
52 | utxo.spent ||
53 | utxo.double_spend ||
54 | utxo.value <= constants.utxo_dummy_value
55 | ) {
56 | continue;
57 | }
58 |
59 | unspents.push(utxo);
60 | total += BigInt(utxo.value);
61 |
62 | if (total >= amount) {
63 | break;
64 | }
65 | }
66 |
67 | cursor =
68 | nextUnspents.txrefs[
69 | nextUnspents.txrefs.length - 1
70 | ].block_height.toString();
71 | sleep = 1000;
72 | } catch (e) {
73 | console.error(e);
74 | sleep *= 2;
75 | await new Promise((resolve) => setTimeout(resolve, sleep));
76 | }
77 | } while (total < amount);
78 |
79 | return {
80 | unspents,
81 | total: total.toString(),
82 | };
83 | };
84 |
--------------------------------------------------------------------------------
/src/transfer/transfer-pipe.ts:
--------------------------------------------------------------------------------
1 | import { selectAllOrdinalsUnspents } from "./select-all-ordinals-unspents";
2 | import { selectAllPipeUnspents } from "./select-all-pipe-unspents";
3 | import { selectUnspents } from "./select-unspents";
4 | import { prepareTransferPipe } from "./prepare-transfer-pipe";
5 | import { PipeUnspent } from "./get-pipe-unspents";
6 | import { constants } from "../constants/constants";
7 |
8 | export type TransferPipeParams = {
9 | privateKey: Uint8Array;
10 | from: string;
11 | to: string;
12 | amount: string; // in btc
13 | ticker: string;
14 | id: string;
15 | decimals: string;
16 | feerate: string;
17 | network?: "mainnet" | "testnet";
18 | };
19 |
20 | export const transferPipe = async (params: TransferPipeParams) => {
21 | const privateKey = params.privateKey;
22 | const from = params.from;
23 | const to = params.to;
24 | const amount = BigInt(params.amount);
25 | const ticker = params.ticker.toLowerCase();
26 | const id = params.id;
27 | const decimals = params.decimals;
28 | const feerate = params.feerate;
29 | const network = params.network || "mainnet";
30 |
31 | const pipeUnspents = await selectAllPipeUnspents({
32 | network,
33 | address: from,
34 | });
35 |
36 | let selectedPipeSatsAmount = 0n;
37 | let selectedPipeAmount = 0n;
38 |
39 | const selectedPipeUnspents: PipeUnspent[] = [];
40 |
41 | const currSpentsStr = localStorage.getItem("currSpents");
42 | const currSpents = JSON.parse(currSpentsStr || "[]");
43 |
44 | for (const pipeUnspent of pipeUnspents) {
45 | if (selectedPipeAmount >= amount) {
46 | break;
47 | }
48 |
49 | if (pipeUnspent.ticker.toLowerCase() !== ticker || pipeUnspent.id.toString() !== id) {
50 | continue;
51 | }
52 |
53 | if(currSpents.length > 0){
54 | const match = currSpents.find((el: { txId: string; vout: number; })=>(el.txId === pipeUnspent.txId && el.vout === pipeUnspent.vout));
55 | if(match) continue;
56 | }
57 |
58 | selectedPipeUnspents.push(pipeUnspent);
59 | selectedPipeAmount += BigInt(pipeUnspent.amount);
60 | selectedPipeSatsAmount += BigInt(constants.utxo_dummy_value);
61 | }
62 |
63 | if (selectedPipeAmount < amount) {
64 | throw new Error("Not enough funds");
65 | }
66 |
67 | const ordinalsUnspents = await selectAllOrdinalsUnspents({
68 | network,
69 | address: from,
70 | });
71 |
72 | const exclude = [
73 | ...pipeUnspents,
74 | ...ordinalsUnspents.map((ordinals) => {
75 | const output = ordinals.output.split(":");
76 | return {
77 | txId: output[0],
78 | vout: parseInt(output[1]),
79 | };
80 | }),
81 | ];
82 |
83 | let selectUnspentsRes;
84 | let prepareTransferPipeRes;
85 | let total = selectedPipeSatsAmount;
86 | let fee = 0n;
87 |
88 | do {
89 | selectUnspentsRes = await selectUnspents({
90 | network,
91 | address: from,
92 | amount: total.toString(),
93 | exclude,
94 | });
95 |
96 | if (BigInt(selectUnspentsRes.total) < total) {
97 | throw new Error("Not enough funds");
98 | }
99 |
100 | prepareTransferPipeRes = prepareTransferPipe({
101 | privateKey,
102 | network,
103 | from,
104 | to,
105 | ticker,
106 | id,
107 | decimals: decimals.toString(),
108 | fee: fee.toString(),
109 | amount: amount.toString(),
110 | feerate: feerate.toString(),
111 | unspents: selectUnspentsRes.unspents,
112 | pipeUnspents: selectedPipeUnspents,
113 | });
114 |
115 | fee = BigInt(prepareTransferPipeRes.fee);
116 | total = selectedPipeSatsAmount + fee;
117 | } while (BigInt(prepareTransferPipeRes.change) < 0n);
118 |
119 | return {
120 | hex: prepareTransferPipeRes.hex,
121 | vin: prepareTransferPipeRes.vin.map((v) => ({
122 | ...v,
123 | prevout: {
124 | ...v.prevout,
125 | value: v.prevout?.value.toString(),
126 | }
127 | })),
128 | vout: prepareTransferPipeRes.vout.map((v) => ({
129 | ...v,
130 | value: v.value?.toString(),
131 | })),
132 | fee: prepareTransferPipeRes.fee,
133 | ticker,
134 | id,
135 | amount: prepareTransferPipeRes.pipeAmount,
136 | to,
137 | sats: total.toString(),
138 | sats_amount: total.toString(),
139 | change: prepareTransferPipeRes.pipeChange.toString(),
140 | sats_change: prepareTransferPipeRes.change.toString(),
141 | };
142 | };
143 |
--------------------------------------------------------------------------------
/src/transfer/transfer-sats.ts:
--------------------------------------------------------------------------------
1 | import { selectAllOrdinalsUnspents } from "./select-all-ordinals-unspents";
2 | import { selectAllPipeUnspents } from "./select-all-pipe-unspents";
3 | import { selectUnspents } from "./select-unspents";
4 | import { prepareTransferSats } from "./prepare-transfer-sats";
5 |
6 | export type TransferSatsParams = {
7 | privateKey: Uint8Array;
8 | from: string;
9 | to: string;
10 | amount: string;
11 | feerate: string;
12 | network?: "mainnet" | "testnet";
13 | };
14 |
15 | export const transferSats = async ({
16 | network = "mainnet",
17 | ...params
18 | }: TransferSatsParams) => {
19 | const privateKey = params.privateKey;
20 | const from = params.from;
21 | const to = params.to;
22 | const amount = BigInt(params.amount);
23 | const feerate = BigInt(params.feerate);
24 |
25 | const pipeUnspents = await selectAllPipeUnspents({
26 | network,
27 | address: from,
28 | });
29 |
30 | const ordinalsUnspents = await selectAllOrdinalsUnspents({
31 | network,
32 | address: from,
33 | });
34 |
35 | const exclude = [
36 | ...pipeUnspents,
37 | ...ordinalsUnspents.map((ordinals) => {
38 | const output = ordinals.output.split(":");
39 | return {
40 | txId: output[0],
41 | vout: parseInt(output[1]),
42 | };
43 | }),
44 | ];
45 |
46 | let selectUnspentsRes;
47 | let prepareTransferSatsRes;
48 | let total = amount;
49 | let fee = 0n;
50 |
51 | do {
52 | selectUnspentsRes = await selectUnspents({
53 | network,
54 | address: from,
55 | amount: total.toString(),
56 | exclude,
57 | });
58 |
59 | if (BigInt(selectUnspentsRes.total) < total) {
60 | throw new Error("Not enough funds");
61 | }
62 |
63 | prepareTransferSatsRes = prepareTransferSats({
64 | privateKey,
65 | network,
66 | from,
67 | to,
68 | fee: fee.toString(),
69 | amount: amount.toString(),
70 | feerate: feerate.toString(),
71 | unspents: selectUnspentsRes.unspents,
72 | });
73 |
74 | fee = BigInt(prepareTransferSatsRes.fee);
75 | total = amount + fee;
76 | } while (BigInt(prepareTransferSatsRes.change) < 0n);
77 |
78 | return {
79 | hex: prepareTransferSatsRes.hex,
80 | vin: prepareTransferSatsRes.vin.map((v) => ({
81 | ...v,
82 | prevout: {
83 | ...v.prevout,
84 | value: v.prevout?.value.toString(),
85 | }
86 | })),
87 | vout: prepareTransferSatsRes.vout.map((v) => ({
88 | ...v,
89 | value: v.value?.toString(),
90 | })),
91 | fee: prepareTransferSatsRes.fee,
92 | sats: selectUnspentsRes.total,
93 | to,
94 | sats_amount: amount.toString(),
95 | sats_change: prepareTransferSatsRes.change.toString(),
96 | };
97 | };
98 |
--------------------------------------------------------------------------------
/src/utils/bitcoin-price.ts:
--------------------------------------------------------------------------------
1 | export const getBitcoinPriceFromCoinbase = async () => {
2 | const request = await fetch(
3 | "https://api.coinbase.com/v2/prices/BTC-USD/spot"
4 | );
5 | const data: {
6 | data: {
7 | amount: string;
8 | };
9 | } = await request.json();
10 | const price = data.data.amount;
11 | return price;
12 | };
13 |
14 | export const getBitcoinPriceFromKraken = async () => {
15 | const request = await fetch(
16 | "https://api.kraken.com/0/public/Ticker?pair=XBTUSD"
17 | );
18 | const data: {
19 | result: {
20 | XXBTZUSD: {
21 | a: string[];
22 | };
23 | };
24 | } = await request.json();
25 | const price = data.result.XXBTZUSD.a[0];
26 | return price;
27 | };
28 |
29 | export const getBitcoinPriceFromCoindesk = async () => {
30 | const request = await fetch(
31 | "https://api.coindesk.com/v1/bpi/currentprice.json"
32 | );
33 | const data: {
34 | bpi: {
35 | USD: {
36 | rate_float: string;
37 | };
38 | };
39 | } = await request.json();
40 | const price = data.bpi.USD.rate_float;
41 | return price;
42 | };
43 |
44 | export const getBitcoinPriceFromGemini = async () => {
45 | const request = await fetch("https://api.gemini.com/v2/ticker/BTCUSD");
46 | const data: {
47 | bid: string;
48 | } = await request.json();
49 | const price = data.bid;
50 | return price;
51 | };
52 |
53 | export const getBitcoinPriceFromBybit = async () => {
54 | const request = await fetch(
55 | "https://api-testnet.bybit.com/derivatives/v3/public/order-book/L2?category=linear&symbol=BTCUSDT"
56 | );
57 | const data: {
58 | result: {
59 | b: string[][];
60 | };
61 | } = await request.json();
62 | const price = data.result.b[0][0];
63 | return price;
64 | };
65 |
66 | export const getBitcoinPrice = async () => {
67 | const cbprice = await getBitcoinPriceFromCoinbase();
68 | const kprice = await getBitcoinPriceFromKraken();
69 | const cdprice = await getBitcoinPriceFromCoindesk();
70 | const gprice = await getBitcoinPriceFromGemini();
71 | const bprice = await getBitcoinPriceFromBybit();
72 |
73 | let prices = [
74 | parseFloat(cbprice),
75 | parseFloat(kprice),
76 | parseFloat(cdprice),
77 | parseFloat(gprice),
78 | parseFloat(bprice)
79 | ];
80 | prices = prices.filter((value) => !Number.isNaN(value) && value > 0);
81 | const avg = prices.reduce((a, b) => a + b, 0) / prices.length;
82 |
83 | return avg;
84 | };
85 |
--------------------------------------------------------------------------------
/src/utils/calculate-fee.ts:
--------------------------------------------------------------------------------
1 | export const calculateFee = (
2 | vinsLength: number,
3 | voutsLength: number,
4 | feeRate: number,
5 | includeChangeOutput: 0 | 1 = 1,
6 | ): number => {
7 | const baseTxSize = 10;
8 | const inSize = 180;
9 | const outSize = 34;
10 |
11 | const txSize =
12 | baseTxSize +
13 | vinsLength * inSize +
14 | voutsLength * outSize +
15 | includeChangeOutput * outSize;
16 | const fee = txSize * feeRate;
17 | return fee;
18 | }
--------------------------------------------------------------------------------
/src/utils/clean-float.ts:
--------------------------------------------------------------------------------
1 | export const cleanFloat = (input: string) => {
2 | // Check if the input contains a comma and remove it
3 | input = input.replace(/,/g, '');
4 |
5 | // Regular expression to match and clean the float format with optional trailing zeros and an optional decimal point
6 | const regex = /^0*(\d+)\.?(\d*?)0*$/;
7 |
8 | // Check if the input matches the regex pattern
9 | const match = input.match(regex);
10 |
11 | // If there's a match, return the cleaned float, otherwise return "0"
12 | if (match) {
13 | const integerPart = match[1];
14 | const decimalPart = match[2] || '';
15 | return decimalPart ? `${integerPart}.${decimalPart}` : integerPart;
16 | } else {
17 | throw new Error('Invalid float to clean');
18 | }
19 | }
--------------------------------------------------------------------------------
/src/utils/crypto.ts:
--------------------------------------------------------------------------------
1 | import * as CryptoJS from 'crypto-js';
2 |
3 | export function encrypt(text: string, password: string): string {
4 | const ciphertext = CryptoJS.AES.encrypt(text, password);
5 | return ciphertext.toString();
6 | }
7 |
8 | export function decrypt(encryptedText: string, password: string): string {
9 | const bytes = CryptoJS.AES.decrypt(encryptedText, password);
10 | return bytes.toString(CryptoJS.enc.Utf8);
11 | }
12 |
--------------------------------------------------------------------------------
/src/utils/fingerprint.ts:
--------------------------------------------------------------------------------
1 | import murmurhash3_32_gc from "murmurhash-js/murmurhash3_gc";
2 |
3 | // Function to get color depth. Return a string containing the color depth.
4 | function getColorDepth(): string {
5 | return window.screen.colorDepth.toString();
6 | }
7 |
8 | // PLUGIN METHODS
9 |
10 | // Function to get plugins. Return a string containing a list of installed plugins.
11 | function getPlugins(): string {
12 | const pluginsList = Array.from(navigator.plugins).map(plugin => plugin.name).join(', ');
13 | return pluginsList;
14 | }
15 |
16 | // STORAGE METHODS
17 |
18 | // Check if local storage is enabled.
19 | function isLocalStorage(): boolean {
20 | try {
21 | return !!localStorage;
22 | } catch (e) {
23 | return true; // SecurityError when referencing it means it exists
24 | }
25 | }
26 |
27 | // Check if session storage is enabled.
28 | function isSessionStorage(): boolean {
29 | try {
30 | return !!sessionStorage;
31 | } catch (e) {
32 | return true; // SecurityError when referencing it means it exists
33 | }
34 | }
35 |
36 | // Check if cookies are enabled.
37 | function isCookie(): boolean {
38 | return navigator.cookieEnabled;
39 | }
40 |
41 | // TIME METHODS
42 |
43 | // Function to get time zone. Return a string containing the time zone.
44 | function getTimeZone(): string {
45 | const offset = -new Date().getTimezoneOffset() / 60;
46 | const formattedNumber = (`0${Math.abs(offset)}`).slice(-2);
47 | const result = offset < 0 ? `-${formattedNumber}` : `+${formattedNumber}`;
48 | return result;
49 | }
50 |
51 | // LANGUAGE METHODS
52 |
53 | // Function to get language. Return a string containing the user language.
54 | function getLanguage(): string {
55 | return navigator.language;
56 | }
57 |
58 | // Function to get system language. Return a string containing the system language.
59 | function getSystemLanguage(): string {
60 | return navigator.language || window.navigator.language;
61 | }
62 |
63 | // Function to get canvas print. Return a string containing the canvas URI data.
64 | function getCanvasPrint(): string {
65 | const canvas = document.createElement("canvas");
66 | try {
67 | canvas.getContext("2d");
68 | } catch (e) {
69 | return "";
70 | }
71 | return canvas.toDataURL();
72 | }
73 |
74 | // Function to generate fingerprint.
75 | export function generateFingerprint(): string {
76 | const bar = "|";
77 | const pieces = [
78 | navigator.userAgent,
79 | window.location.hostname,
80 | getColorDepth(),
81 | getPlugins(),
82 | isLocalStorage(),
83 | isSessionStorage(),
84 | getTimeZone(),
85 | getLanguage(),
86 | getSystemLanguage(),
87 | isCookie(),
88 | getCanvasPrint()
89 | ];
90 | const key = pieces.join(bar);
91 | const seed = 256;
92 |
93 | return String(murmurhash3_32_gc(key, seed));
94 | }
95 |
--------------------------------------------------------------------------------
/src/utils/hash-to-string.ts:
--------------------------------------------------------------------------------
1 | export const hashToString = (arrayBuffer: ArrayBuffer) => {
2 | return Array.from(new Uint8Array(arrayBuffer))
3 | .map((b) => b.toString(16).padStart(2, "0"))
4 | .join("");
5 | };
6 |
--------------------------------------------------------------------------------
/src/utils/hex-to-bytes.ts:
--------------------------------------------------------------------------------
1 | export const hexToBytes = (hex: string) => {
2 | const bytes = hex.match(/.{1,2}/g)?.map((byte) => parseInt(byte, 16));
3 | return bytes ? Uint8Array.from(bytes) : new Uint8Array();
4 | };
5 |
--------------------------------------------------------------------------------
/src/utils/sats-to-btc.ts:
--------------------------------------------------------------------------------
1 | export const satsToBtc = (sats: number) => {
2 | return sats / 100000000
3 | };
--------------------------------------------------------------------------------
/src/utils/sats-to-dollars.ts:
--------------------------------------------------------------------------------
1 | export const satsToDollars = (sats: number, bitcoinPrice: number) => {
2 | if (sats >= 100000000) sats = sats * 10;
3 | let dollars =
4 | Number(
5 | String(Math.floor(sats)).padStart(8, "0").slice(0, -9) +
6 | "." +
7 | String(Math.floor(sats)).padStart(8, "0").slice(-9)
8 | ) * bitcoinPrice;
9 | dollars = Math.round((dollars + Number.EPSILON) * 100) / 100;
10 | return dollars;
11 | };
12 |
--------------------------------------------------------------------------------
/src/utils/truncate-in-middle.ts:
--------------------------------------------------------------------------------
1 | export const truncateInMiddle = (inputString: string, maxLength: number) => {
2 | // Check if the input string is already shorter than the maxLength
3 | if (inputString.length <= maxLength) {
4 | return inputString;
5 | }
6 |
7 | // Calculate the length of the string to keep on each side of the truncation
8 | const sideLength = Math.floor((maxLength - 3) / 2); // 3 is for the ellipsis (...)
9 |
10 | // Create the truncated string by taking a portion from the start and end of the input string
11 | const truncatedString =
12 | inputString.substring(0, sideLength) +
13 | "..." +
14 | inputString.substring(inputString.length - sideLength);
15 |
16 | return truncatedString;
17 | };
18 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 | "allowJs": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true
23 | },
24 | "include": ["src", "src/bitcoin/bitcoinjs-lib-standalone.js"],
25 | "references": [{ "path": "./tsconfig.node.json" }]
26 | }
27 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import { nodePolyfills } from 'vite-plugin-node-polyfills'
3 | import hotReloadExtension from 'hot-reload-extension-vite'
4 |
5 | export default defineConfig({
6 | plugins: [
7 | nodePolyfills({
8 | globals: {
9 | Buffer: true,
10 | global: true,
11 | process: true,
12 | },
13 | protocolImports: true,
14 | }),
15 | hotReloadExtension({
16 | log: true,
17 | backgroundPath: 'background.js'
18 | }),
19 | ],
20 | server: {
21 | port: 3333
22 | },
23 | })
24 |
--------------------------------------------------------------------------------