├── .editorconfig ├── .env ├── .gitignore ├── .prettierrc ├── .vscode └── launch.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── README.md ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── modules.js ├── paths.js ├── pnpTs.js ├── webpack.config.js └── webpackDevServer.config.js ├── manifest.json ├── package-lock.json ├── package.json ├── public ├── android-icon-144x144.png ├── android-icon-192x192.png ├── android-icon-36x36.png ├── android-icon-48x48.png ├── android-icon-72x72.png ├── android-icon-96x96.png ├── apple-icon-114x114.png ├── apple-icon-120x120.png ├── apple-icon-144x144.png ├── apple-icon-152x152.png ├── apple-icon-180x180.png ├── apple-icon-57x57.png ├── apple-icon-60x60.png ├── apple-icon-72x72.png ├── apple-icon-76x76.png ├── apple-icon-precomposed.png ├── apple-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── favicon.ico ├── favicon.png ├── index.html ├── manifest.json ├── ms-icon-144x144.png ├── ms-icon-150x150.png ├── ms-icon-310x310.png ├── ms-icon-70x70.png └── robots.txt ├── screenshots ├── ss01.png ├── ss02.png ├── ss03.png └── ss04.jpg ├── scripts ├── build.js ├── start.js └── test.js └── src ├── assets ├── 4-bitcoin-cash-logo-flag.png ├── badger-icon.png ├── bch-full.png ├── bch-icon-qrcode.png ├── bch-logo-2.png ├── bch-logo-3.png ├── bch-logo.png ├── bch-qrcode.png ├── bitcoin-com-wallet-icon.png ├── bitcoin-wallet.png ├── hammer-solid.svg ├── icon.png ├── ios-paperplane.svg ├── logo.png ├── pixel-square-icon.png ├── slp-logo-2.png ├── slp-logo.png ├── slp-oval.png ├── slp-qrcode.png └── slp-sticker.jpg ├── components ├── App.css ├── App.js ├── App.test.js ├── Audit │ └── Audit.js ├── Common │ ├── CustomIcons.js │ ├── QRCode.js │ ├── StyledCollapse.js │ ├── StyledOnBoarding.js │ └── StyledPage.js ├── Configure │ └── Configure.js ├── Create │ └── Create.js ├── DividendHistory │ └── DividendHistory.jsx ├── Dividends │ └── Dividends.js ├── Icons │ └── Icons.js ├── NotFound.js ├── OnBoarding │ └── OnBoarding.js ├── Portfolio │ ├── Burn │ │ └── Burn.js │ ├── EnhancedCard.js │ ├── EnhancedInputs.js │ ├── EnhancedModal.js │ ├── Mint │ │ └── Mint.js │ ├── MoreCardOptions.js │ ├── PayDividends │ │ ├── AdvancedOptions.js │ │ ├── PayDividends.js │ │ └── useDividendsStats.js │ ├── PayDividendsOption.js │ ├── Portfolio.js │ ├── ScanQRCode.js │ ├── SendBCH │ │ └── SendBCH.js │ └── Transfer │ │ └── Transfer.js └── SatoshiDice │ └── SatoshiDice.js ├── index.css ├── index.js ├── serviceWorker.js └── utils ├── broadcastTransaction.js ├── context.js ├── createWallet.js ├── cropImage.js ├── debounce.js ├── decodeRawSlpTransactions.js ├── dividends ├── dividends-manager.js └── dividends.js ├── getSlpBanlancesAndUtxos.js ├── getTokenTransactionHistory.js ├── getTransactionHistory.js ├── getWalletDetails.js ├── isPiticoTokenHolder.js ├── resizeImage.js ├── retry.js ├── roundImage.js ├── satoshiDice.js ├── sendBch.js ├── sendDividends.js ├── useAsyncTimeout.js ├── useInnerScroll.js ├── useInterval.js ├── usePrevious.js ├── useWallet.js └── withSLP.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | max_line_length = 100 10 | trim_trailing_whitespace = true 11 | [*.md] 12 | max_line_length = 0 13 | trim_trailing_whitespace = false 14 | [{Makefile,**.mk}] 15 | # Use tabs for indentation (Makefiles require tabs) 16 | indent_style = tab 17 | [*.scss] 18 | indent_size = 2 19 | indent_style = space -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_NETWORK=mainnet 2 | REACT_APP_IPSTACK_KEY=e31adcf791ad1494cf5265d94bc059ef 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | “printWidth”: 100, 3 | “trailingComma”: “all”, 4 | “tabWidth”: 2, 5 | “semi”: true, 6 | “singleQuote”: true 7 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Launch via NPM", 8 | "runtimeExecutable": "npm", 9 | "runtimeArgs": [ 10 | "run-script", 11 | "debug" 12 | ], 13 | "port": 9229, 14 | "skipFiles": [ 15 | "/**" 16 | ] 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog - Bitcoin.com Mint 2 | 3 | ## Oct 5, 2020 4 | 5 | - Add new feature: Satoshi Dice 6 | - Add Country Ban for select features 7 | - Remove Badger and Bitcoin.com wallet imports 8 | 9 | ## Oct 1, 2020 10 | 11 | - Correct error handling for user attempt to send SLP to BCH address 12 | - Correct error handling for user attempt to send SLP to invalid address 13 | - Introduce semantic versioning at 0.1.1 14 | 15 | ## Apr 29, 2020 16 | 17 | - Add icons with token creation 18 | 19 | ## Fev 18, 2020 20 | 21 | - [#61:](https://github.com/Bitcoin-com/mint/pull/61) Production Release 22 | - [#62:](https://github.com/Bitcoin-com/mint/pull/62) Fix/delay lock on dividends page 23 | - [#60:](https://github.com/Bitcoin-com/mint/pull/60) [feat] fixed_supply_tokens 24 | - [#59:](https://github.com/Bitcoin-com/mint/pull/59) Fix/side bar scroll and audit menu item 25 | - [#57:](https://github.com/Bitcoin-com/mint/pull/57) [hotfix] Default error message when slp-sdk throws an unhandled exception 26 | - [#56:](https://github.com/Bitcoin-com/mint/pull/56) [fix] Fix dividends scroll 27 | - [#55:](https://github.com/Bitcoin-com/mint/pull/55) Fix/token burn adjustments 28 | - [#54:](https://github.com/Bitcoin-com/mint/pull/54) Feat/dividends page 29 | - [#53:](https://github.com/Bitcoin-com/mint/pull/53) [fix] document hash code review adjustments 30 | - [#52:](https://github.com/Bitcoin-com/mint/pull/52) [fix] Better tooltip of how users are eligible for dividends 31 | - [#51:](https://github.com/Bitcoin-com/mint/pull/51) [fix] Additional fix to the fee calculation of dividends payment 32 | - [#50:](https://github.com/Bitcoin-com/mint/pull/50) [feat] document hash on file upload and info linking to notary 33 | - [#49:](https://github.com/Bitcoin-com/mint/pull/49) [feat] burn with multiple funding addresses 34 | - [#48:](https://github.com/Bitcoin-com/mint/pull/48) [fix] sidebar collapse 35 | - [#47:](https://github.com/Bitcoin-com/mint/pull/47) Fix/replace transaction history slpdb queries and Dividends History 36 | - [#46:](https://github.com/Bitcoin-com/mint/pull/46) [feat] OP_RETURN added to dividend payments 37 | - [#45:](https://github.com/Bitcoin-com/mint/pull/45) [fix] Create token should not appear to users without a wallet 38 | 39 | ## Jan 28, 2020 40 | 41 | - [#43:](https://github.com/Bitcoin-com/mint/pull/43) Production release 42 | - [#42:](https://github.com/Bitcoin-com/mint/pull/42) [fix] Get wallet from localstorage should have a getWalletDetails call 43 | - [#41:](https://github.com/Bitcoin-com/mint/pull/41) [feat] Addresses removal for paying dividends 44 | - [#40:](https://github.com/Bitcoin-com/mint/pull/40) [feat] Add Faucet Link and instructions for adding token icons 45 | - [#39:](https://github.com/Bitcoin-com/mint/pull/39) Fix screen flickering on update 46 | - [#38:](https://github.com/Bitcoin-com/mint/pull/38) Fix slow updates issue caused by multiples calls of getWalletDetails 47 | 48 | ## Jan 21, 2020 49 | 50 | - [#37:](https://github.com/Bitcoin-com/mint/pull/37) Production release 51 | - Service workers for caching API calls 52 | - Bug fixes 53 | 54 | ## Jan 15, 2020 55 | 56 | - [#27:](https://github.com/Bitcoin-com/mint/pull/27) First production release to https://mint.bitcoin.com 57 | - Pay BCH dividends to holders of SLP tokens in user's portfolio 58 | - Send and receive BCH 59 | - Transaction history for BCH and SLP transactions 60 | - Token icons matched to [Badger Mobile wallet](https://github.com/bitcoin-com/badger-mobile) icons for tokens without set icons 61 | 62 | ## Sept 2019 63 | 64 | - 3rd place in the [Simple Ledger Virtual Hackathon (SLPVH) 2019](https://simpleledger.info/slpvh/) as Pitico.cash 65 | - Create SLP tokens 66 | - Mint SLP tokens 67 | - Send and receive SLP tokens 68 | - Receive BCH 69 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issues. 4 | If you have more specific questions, please get in touch via email (andre@cabrera.pw), telegram or twitter (alcipir). 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 13 | do not have permission to do that, you may request the second reviewer to merge it for you. 14 | 15 | ## Code of Conduct 16 | 17 | ### Our Pledge 18 | 19 | In the interest of fostering an open and welcoming environment, we as 20 | contributors and maintainers pledge to making participation in our project and 21 | our community a harassment-free experience for everyone, regardless of age, body 22 | size, disability, ethnicity, gender identity and expression, level of experience, 23 | nationality, personal appearance, race, religion, or sexual identity and 24 | orientation. 25 | 26 | ### Our Standards 27 | 28 | Examples of behavior that contributes to creating a positive environment 29 | include: 30 | 31 | * Using welcoming and inclusive language 32 | * Being respectful of differing viewpoints and experiences 33 | * Gracefully accepting constructive criticism 34 | * Focusing on what is best for the community 35 | * Showing empathy towards other community members 36 | 37 | Examples of unacceptable behavior by participants include: 38 | 39 | * The use of sexualized language or imagery and unwelcome sexual attention or 40 | advances 41 | * Trolling, insulting/derogatory comments, and personal or political attacks 42 | * Public or private harassment 43 | * Publishing others' private information, such as a physical or electronic 44 | address, without explicit permission 45 | * Other conduct which could reasonably be considered inappropriate in a 46 | professional setting 47 | 48 | ### Our Responsibilities 49 | 50 | Project maintainers are responsible for clarifying the standards of acceptable 51 | behavior and are expected to take appropriate and fair corrective action in 52 | response to any instances of unacceptable behavior. 53 | 54 | Project maintainers have the right and responsibility to remove, edit, or 55 | reject comments, commits, code, wiki edits, issues, and other contributions 56 | that are not aligned to this Code of Conduct, or to ban temporarily or 57 | permanently any contributor for other behaviors that they deem inappropriate, 58 | threatening, offensive, or harmful. 59 | 60 | ### Scope 61 | 62 | This Code of Conduct applies both within project spaces and in public spaces 63 | when an individual is representing the project or its community. Examples of 64 | representing a project or community include using an official project e-mail 65 | address, posting via an official social media account, or acting as an appointed 66 | representative at an online or offline event. Representation of a project may be 67 | further defined and clarified by project maintainers. 68 | 69 | ### Enforcement 70 | 71 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 72 | reported by contacting the project maintainer: andre@cabrera.pw. All 73 | complaints will be reviewed and investigated and will result in a response that 74 | is deemed necessary and appropriate to the circumstances. The project team is 75 | obligated to maintain confidentiality with regard to the reporter of an incident. 76 | Further details of specific enforcement policies may be posted separately. 77 | 78 | Project maintainers who do not follow or enforce the Code of Conduct in good 79 | faith may face temporary or permanent repercussions as determined by other 80 | members of the project's leadership. 81 | 82 | ### Attribution 83 | 84 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 85 | available at [http://contributor-covenant.org/version/1/4][version] 86 | 87 | [homepage]: http://contributor-covenant.org 88 | [version]: http://contributor-covenant.org/version/1/4/ 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mint, an SLP Management Suite 2 | 3 | ## Create and control your Bitcoin Cash SLP Tokens in your browser 4 | 5 | _Mint is client-only: the app runs in your web browser. Mint uses rest.bitcoin.com as the default back-end, but the user may select any backend. Mint is non-custodial and does not have access to your private keys. You must back up your wallet to recover your funds._ 6 | 7 | ### Features 8 | 9 | - Create your own SLP token 10 | - Pay BCH dividends to SLP token holders 11 | - Mint (create additional token supply for tokens without fixed supply) 12 | - Send & Receive BCH and SLP tokens 13 | - Import existing wallets 14 | - Choose your own REST API (default: rest.bitcoin.com) 15 | - Hosted online at https://mint.bitcoin.com or run locally with `npm start` 16 | 17 | ### Learn more about the Simple Ledger Protocol: https://simpleledger.cash/ 18 | 19 | ## Development 20 | 21 | In the project directory, run: 22 | 23 | ### `npm start` 24 | 25 | Runs the app in the development mode.
26 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 27 | 28 | The page will reload if you make edits.
29 | You will also see any lint errors in the console. 30 | 31 | ## Production 32 | 33 | In the project directory, run: 34 | 35 | ### `npm run build` 36 | 37 | Builds the app for production to the `build` folder.
38 | It correctly bundles React in production mode and optimizes the build for the best performance. 39 | 40 | The build is minified and the filenames include the hashes.
41 | Your app is ready to be deployed! 42 | 43 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 44 | 45 | ## Mint Project Roadmap 46 | 47 | Mint will continue adding new features to best support user-friendly SLP token management. If you have an idea for a feature, please create an issue in this repository. 48 | 49 | The following features are under active development: 50 | 51 | - Custom OP_RETURN notes on user-created dividend transactions 52 | - SLP Airdrops (send any user-specified SLP token to holders of any user specified SLP token) up to 10,000 recipients 53 | - HD wallet support, including seed importing from [the Bitcoin.com mobile wallet](https://wallet.bitcoin.com/) 54 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | const dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. Variable expansion is supported in .env files. 31 | // https://github.com/motdotla/dotenv 32 | // https://github.com/motdotla/dotenv-expand 33 | dotenvFiles.forEach(dotenvFile => { 34 | if (fs.existsSync(dotenvFile)) { 35 | require('dotenv-expand')( 36 | require('dotenv').config({ 37 | path: dotenvFile, 38 | }) 39 | ); 40 | } 41 | }); 42 | 43 | // We support resolving modules according to `NODE_PATH`. 44 | // This lets you use absolute paths in imports inside large monorepos: 45 | // https://github.com/facebook/create-react-app/issues/253. 46 | // It works similar to `NODE_PATH` in Node itself: 47 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 48 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 49 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 50 | // https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 51 | // We also resolve them to make sure all tools using them work consistently. 52 | const appDirectory = fs.realpathSync(process.cwd()); 53 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 54 | .split(path.delimiter) 55 | .filter(folder => folder && !path.isAbsolute(folder)) 56 | .map(folder => path.resolve(appDirectory, folder)) 57 | .join(path.delimiter); 58 | 59 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 60 | // injected into the application via DefinePlugin in Webpack configuration. 61 | const REACT_APP = /^REACT_APP_/i; 62 | 63 | function getClientEnvironment(publicUrl) { 64 | const raw = Object.keys(process.env) 65 | .filter(key => REACT_APP.test(key)) 66 | .reduce( 67 | (env, key) => { 68 | env[key] = process.env[key]; 69 | return env; 70 | }, 71 | { 72 | // Useful for determining whether we’re running in production mode. 73 | // Most importantly, it switches React into the correct mode. 74 | NODE_ENV: process.env.NODE_ENV || 'development', 75 | // Useful for resolving the correct path to static assets in `public`. 76 | // For example, . 77 | // This should only be used as an escape hatch. Normally you would put 78 | // images into the `src` and `import` them in code to get their paths. 79 | PUBLIC_URL: publicUrl, 80 | } 81 | ); 82 | // Stringify all values so we can feed into Webpack DefinePlugin 83 | const stringified = { 84 | 'process.env': Object.keys(raw).reduce((env, key) => { 85 | env[key] = JSON.stringify(raw[key]); 86 | return env; 87 | }, {}), 88 | }; 89 | 90 | return { raw, stringified }; 91 | } 92 | 93 | module.exports = getClientEnvironment; 94 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const camelcase = require('camelcase'); 5 | 6 | // This is a custom Jest transformer turning file imports into filenames. 7 | // http://facebook.github.io/jest/docs/en/webpack.html 8 | 9 | module.exports = { 10 | process(src, filename) { 11 | const assetFilename = JSON.stringify(path.basename(filename)); 12 | 13 | if (filename.match(/\.svg$/)) { 14 | // Based on how SVGR generates a component name: 15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 16 | const pascalCaseFileName = camelcase(path.parse(filename).name, { 17 | pascalCase: true, 18 | }); 19 | const componentName = `Svg${pascalCaseFileName}`; 20 | return `const React = require('react'); 21 | module.exports = { 22 | __esModule: true, 23 | default: ${assetFilename}, 24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) { 25 | return { 26 | $$typeof: Symbol.for('react.element'), 27 | type: 'svg', 28 | ref: ref, 29 | key: null, 30 | props: Object.assign({}, props, { 31 | children: ${assetFilename} 32 | }) 33 | }; 34 | }), 35 | };`; 36 | } 37 | 38 | return `module.exports = ${assetFilename};`; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /config/modules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | const resolve = require('resolve'); 8 | 9 | /** 10 | * Get the baseUrl of a compilerOptions object. 11 | * 12 | * @param {Object} options 13 | */ 14 | function getAdditionalModulePaths(options = {}) { 15 | const baseUrl = options.baseUrl; 16 | 17 | // We need to explicitly check for null and undefined (and not a falsy value) because 18 | // TypeScript treats an empty string as `.`. 19 | if (baseUrl == null) { 20 | // If there's no baseUrl set we respect NODE_PATH 21 | // Note that NODE_PATH is deprecated and will be removed 22 | // in the next major release of create-react-app. 23 | 24 | const nodePath = process.env.NODE_PATH || ''; 25 | return nodePath.split(path.delimiter).filter(Boolean); 26 | } 27 | 28 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 29 | 30 | // We don't need to do anything if `baseUrl` is set to `node_modules`. This is 31 | // the default behavior. 32 | if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { 33 | return null; 34 | } 35 | 36 | // Allow the user set the `baseUrl` to `appSrc`. 37 | if (path.relative(paths.appSrc, baseUrlResolved) === '') { 38 | return [paths.appSrc]; 39 | } 40 | 41 | // Otherwise, throw an error. 42 | throw new Error( 43 | chalk.red.bold( 44 | "Your project's `baseUrl` can only be set to `src` or `node_modules`." + 45 | ' Create React App does not support other values at this time.' 46 | ) 47 | ); 48 | } 49 | 50 | function getModules() { 51 | // Check if TypeScript is setup 52 | const hasTsConfig = fs.existsSync(paths.appTsConfig); 53 | const hasJsConfig = fs.existsSync(paths.appJsConfig); 54 | 55 | if (hasTsConfig && hasJsConfig) { 56 | throw new Error( 57 | 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' 58 | ); 59 | } 60 | 61 | let config; 62 | 63 | // If there's a tsconfig.json we assume it's a 64 | // TypeScript project and set up the config 65 | // based on tsconfig.json 66 | if (hasTsConfig) { 67 | const ts = require(resolve.sync('typescript', { 68 | basedir: paths.appNodeModules, 69 | })); 70 | config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config; 71 | // Otherwise we'll check if there is jsconfig.json 72 | // for non TS projects. 73 | } else if (hasJsConfig) { 74 | config = require(paths.appJsConfig); 75 | } 76 | 77 | config = config || {}; 78 | const options = config.compilerOptions || {}; 79 | 80 | const additionalModulePaths = getAdditionalModulePaths(options); 81 | 82 | return { 83 | additionalModulePaths: additionalModulePaths, 84 | hasTsConfig, 85 | }; 86 | } 87 | 88 | module.exports = getModules(); 89 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(inputPath, needsSlash) { 15 | const hasSlash = inputPath.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return inputPath.substr(0, inputPath.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${inputPath}/`; 20 | } else { 21 | return inputPath; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right 18 | 19 | 20 | 24 | 29 | 34 | 39 | 44 | 49 | 54 | 59 | 64 | 69 | 70 | 71 | 72 | 73 | 74 | 78 | 79 | 88 | Mint | Bitcoin.com 89 | 90 | 91 | 92 | 100 | 101 | 102 |
103 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/public/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/public/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/public/ms-icon-310x310.png -------------------------------------------------------------------------------- /public/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/public/ms-icon-70x70.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /screenshots/ss01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/screenshots/ss01.png -------------------------------------------------------------------------------- /screenshots/ss02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/screenshots/ss02.png -------------------------------------------------------------------------------- /screenshots/ss03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/screenshots/ss03.png -------------------------------------------------------------------------------- /screenshots/ss04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/screenshots/ss04.jpg -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'production'; 5 | process.env.NODE_ENV = 'production'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | 18 | const path = require('path'); 19 | const chalk = require('react-dev-utils/chalk'); 20 | const fs = require('fs-extra'); 21 | const webpack = require('webpack'); 22 | const configFactory = require('../config/webpack.config'); 23 | const paths = require('../config/paths'); 24 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 25 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 26 | const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); 27 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 28 | const printBuildError = require('react-dev-utils/printBuildError'); 29 | 30 | const measureFileSizesBeforeBuild = 31 | FileSizeReporter.measureFileSizesBeforeBuild; 32 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 33 | const useYarn = fs.existsSync(paths.yarnLockFile); 34 | 35 | // These sizes are pretty large. We'll warn for bundles exceeding them. 36 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 37 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 38 | 39 | const isInteractive = process.stdout.isTTY; 40 | 41 | // Warn and crash if required files are missing 42 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 43 | process.exit(1); 44 | } 45 | 46 | // Generate configuration 47 | const config = configFactory('production'); 48 | 49 | // We require that you explicitly set browsers and do not fall back to 50 | // browserslist defaults. 51 | const { checkBrowsers } = require('react-dev-utils/browsersHelper'); 52 | checkBrowsers(paths.appPath, isInteractive) 53 | .then(() => { 54 | // First, read the current file sizes in build directory. 55 | // This lets us display how much they changed later. 56 | return measureFileSizesBeforeBuild(paths.appBuild); 57 | }) 58 | .then(previousFileSizes => { 59 | // Remove all content but keep the directory so that 60 | // if you're in it, you don't end up in Trash 61 | fs.emptyDirSync(paths.appBuild); 62 | // Merge with the public folder 63 | copyPublicFolder(); 64 | // Start the webpack build 65 | return build(previousFileSizes); 66 | }) 67 | .then( 68 | ({ stats, previousFileSizes, warnings }) => { 69 | if (warnings.length) { 70 | console.log(chalk.yellow('Compiled with warnings.\n')); 71 | console.log(warnings.join('\n\n')); 72 | console.log( 73 | '\nSearch for the ' + 74 | chalk.underline(chalk.yellow('keywords')) + 75 | ' to learn more about each warning.' 76 | ); 77 | console.log( 78 | 'To ignore, add ' + 79 | chalk.cyan('// eslint-disable-next-line') + 80 | ' to the line before.\n' 81 | ); 82 | } else { 83 | console.log(chalk.green('Compiled successfully.\n')); 84 | } 85 | 86 | console.log('File sizes after gzip:\n'); 87 | printFileSizesAfterBuild( 88 | stats, 89 | previousFileSizes, 90 | paths.appBuild, 91 | WARN_AFTER_BUNDLE_GZIP_SIZE, 92 | WARN_AFTER_CHUNK_GZIP_SIZE 93 | ); 94 | console.log(); 95 | 96 | const appPackage = require(paths.appPackageJson); 97 | const publicUrl = paths.publicUrl; 98 | const publicPath = config.output.publicPath; 99 | const buildFolder = path.relative(process.cwd(), paths.appBuild); 100 | printHostingInstructions( 101 | appPackage, 102 | publicUrl, 103 | publicPath, 104 | buildFolder, 105 | useYarn 106 | ); 107 | }, 108 | err => { 109 | console.log(chalk.red('Failed to compile.\n')); 110 | printBuildError(err); 111 | process.exit(1); 112 | } 113 | ) 114 | .catch(err => { 115 | if (err && err.message) { 116 | console.log(err.message); 117 | } 118 | process.exit(1); 119 | }); 120 | 121 | // Create the production build and print the deployment instructions. 122 | function build(previousFileSizes) { 123 | // We used to support resolving modules according to `NODE_PATH`. 124 | // This now has been deprecated in favor of jsconfig/tsconfig.json 125 | // This lets you use absolute paths in imports inside large monorepos: 126 | if (process.env.NODE_PATH) { 127 | console.log( 128 | chalk.yellow( 129 | 'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.' 130 | ) 131 | ); 132 | console.log(); 133 | } 134 | 135 | console.log('Creating an optimized production build...'); 136 | 137 | const compiler = webpack(config); 138 | return new Promise((resolve, reject) => { 139 | compiler.run((err, stats) => { 140 | let messages; 141 | if (err) { 142 | if (!err.message) { 143 | return reject(err); 144 | } 145 | messages = formatWebpackMessages({ 146 | errors: [err.message], 147 | warnings: [], 148 | }); 149 | } else { 150 | messages = formatWebpackMessages( 151 | stats.toJson({ all: false, warnings: true, errors: true }) 152 | ); 153 | } 154 | if (messages.errors.length) { 155 | // Only keep the first error. Others are often indicative 156 | // of the same problem, but confuse the reader with noise. 157 | if (messages.errors.length > 1) { 158 | messages.errors.length = 1; 159 | } 160 | return reject(new Error(messages.errors.join('\n\n'))); 161 | } 162 | if ( 163 | process.env.CI && 164 | (typeof process.env.CI !== 'string' || 165 | process.env.CI.toLowerCase() !== 'false') && 166 | messages.warnings.length 167 | ) { 168 | console.log( 169 | chalk.yellow( 170 | '\nTreating warnings as errors because process.env.CI = true.\n' + 171 | 'Most CI servers set it automatically.\n' 172 | ) 173 | ); 174 | return reject(new Error(messages.warnings.join('\n\n'))); 175 | } 176 | 177 | return resolve({ 178 | stats, 179 | previousFileSizes, 180 | warnings: messages.warnings, 181 | }); 182 | }); 183 | }); 184 | } 185 | 186 | function copyPublicFolder() { 187 | fs.copySync(paths.appPublic, paths.appBuild, { 188 | dereference: true, 189 | filter: file => file !== paths.appHtml, 190 | }); 191 | } 192 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'development'; 5 | process.env.NODE_ENV = 'development'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | 18 | const fs = require('fs'); 19 | const chalk = require('react-dev-utils/chalk'); 20 | const webpack = require('webpack'); 21 | const WebpackDevServer = require('webpack-dev-server'); 22 | const clearConsole = require('react-dev-utils/clearConsole'); 23 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 24 | const { 25 | choosePort, 26 | createCompiler, 27 | prepareProxy, 28 | prepareUrls, 29 | } = require('react-dev-utils/WebpackDevServerUtils'); 30 | const openBrowser = require('react-dev-utils/openBrowser'); 31 | const paths = require('../config/paths'); 32 | const configFactory = require('../config/webpack.config'); 33 | const createDevServerConfig = require('../config/webpackDevServer.config'); 34 | 35 | const useYarn = fs.existsSync(paths.yarnLockFile); 36 | const isInteractive = process.stdout.isTTY; 37 | 38 | // Warn and crash if required files are missing 39 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 40 | process.exit(1); 41 | } 42 | 43 | // Tools like Cloud9 rely on this. 44 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 45 | const HOST = process.env.HOST || '0.0.0.0'; 46 | 47 | if (process.env.HOST) { 48 | console.log( 49 | chalk.cyan( 50 | `Attempting to bind to HOST environment variable: ${chalk.yellow( 51 | chalk.bold(process.env.HOST) 52 | )}` 53 | ) 54 | ); 55 | console.log( 56 | `If this was unintentional, check that you haven't mistakenly set it in your shell.` 57 | ); 58 | console.log( 59 | `Learn more here: ${chalk.yellow('https://bit.ly/CRA-advanced-config')}` 60 | ); 61 | console.log(); 62 | } 63 | 64 | // We require that you explicitly set browsers and do not fall back to 65 | // browserslist defaults. 66 | const { checkBrowsers } = require('react-dev-utils/browsersHelper'); 67 | checkBrowsers(paths.appPath, isInteractive) 68 | .then(() => { 69 | // We attempt to use the default port but if it is busy, we offer the user to 70 | // run on a different port. `choosePort()` Promise resolves to the next free port. 71 | return choosePort(HOST, DEFAULT_PORT); 72 | }) 73 | .then(port => { 74 | if (port == null) { 75 | // We have not found a port. 76 | return; 77 | } 78 | const config = configFactory('development'); 79 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 80 | const appName = require(paths.appPackageJson).name; 81 | const useTypeScript = fs.existsSync(paths.appTsConfig); 82 | const urls = prepareUrls(protocol, HOST, port); 83 | const devSocket = { 84 | warnings: warnings => 85 | devServer.sockWrite(devServer.sockets, 'warnings', warnings), 86 | errors: errors => 87 | devServer.sockWrite(devServer.sockets, 'errors', errors), 88 | }; 89 | // Create a webpack compiler that is configured with custom messages. 90 | const compiler = createCompiler({ 91 | appName, 92 | config, 93 | devSocket, 94 | urls, 95 | useYarn, 96 | useTypeScript, 97 | webpack, 98 | }); 99 | // Load proxy config 100 | const proxySetting = require(paths.appPackageJson).proxy; 101 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic); 102 | // Serve webpack assets generated by the compiler over a web server. 103 | const serverConfig = createDevServerConfig( 104 | proxyConfig, 105 | urls.lanUrlForConfig 106 | ); 107 | const devServer = new WebpackDevServer(compiler, serverConfig); 108 | // Launch WebpackDevServer. 109 | devServer.listen(port, HOST, err => { 110 | if (err) { 111 | return console.log(err); 112 | } 113 | if (isInteractive) { 114 | clearConsole(); 115 | } 116 | 117 | // We used to support resolving modules according to `NODE_PATH`. 118 | // This now has been deprecated in favor of jsconfig/tsconfig.json 119 | // This lets you use absolute paths in imports inside large monorepos: 120 | if (process.env.NODE_PATH) { 121 | console.log( 122 | chalk.yellow( 123 | 'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.' 124 | ) 125 | ); 126 | console.log(); 127 | } 128 | 129 | console.log(chalk.cyan('Starting the development server...\n')); 130 | openBrowser(urls.localUrlForBrowser); 131 | }); 132 | 133 | ['SIGINT', 'SIGTERM'].forEach(function(sig) { 134 | process.on(sig, function() { 135 | devServer.close(); 136 | process.exit(); 137 | }); 138 | }); 139 | }) 140 | .catch(err => { 141 | if (err && err.message) { 142 | console.log(err.message); 143 | } 144 | process.exit(1); 145 | }); 146 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const jest = require('jest'); 20 | const execSync = require('child_process').execSync; 21 | let argv = process.argv.slice(2); 22 | 23 | function isInGitRepository() { 24 | try { 25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 26 | return true; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | function isInMercurialRepository() { 33 | try { 34 | execSync('hg --cwd . root', { stdio: 'ignore' }); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Watch unless on CI or explicitly running all tests 42 | if ( 43 | !process.env.CI && 44 | argv.indexOf('--watchAll') === -1 && 45 | argv.indexOf('--watchAll=false') === -1 46 | ) { 47 | // https://github.com/facebook/create-react-app/issues/5210 48 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 49 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 50 | } 51 | 52 | 53 | jest.run(argv); 54 | -------------------------------------------------------------------------------- /src/assets/4-bitcoin-cash-logo-flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/src/assets/4-bitcoin-cash-logo-flag.png -------------------------------------------------------------------------------- /src/assets/badger-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/src/assets/badger-icon.png -------------------------------------------------------------------------------- /src/assets/bch-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/src/assets/bch-full.png -------------------------------------------------------------------------------- /src/assets/bch-icon-qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/src/assets/bch-icon-qrcode.png -------------------------------------------------------------------------------- /src/assets/bch-logo-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/src/assets/bch-logo-2.png -------------------------------------------------------------------------------- /src/assets/bch-logo-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/src/assets/bch-logo-3.png -------------------------------------------------------------------------------- /src/assets/bch-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/src/assets/bch-logo.png -------------------------------------------------------------------------------- /src/assets/bch-qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/src/assets/bch-qrcode.png -------------------------------------------------------------------------------- /src/assets/bitcoin-com-wallet-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/src/assets/bitcoin-com-wallet-icon.png -------------------------------------------------------------------------------- /src/assets/bitcoin-wallet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/src/assets/bitcoin-wallet.png -------------------------------------------------------------------------------- /src/assets/hammer-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/src/assets/icon.png -------------------------------------------------------------------------------- /src/assets/ios-paperplane.svg: -------------------------------------------------------------------------------- 1 | 5 | 7 | 10 | 13 | 20 | 21 | 24 | 31 | 32 | 33 | 36 | 42 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/pixel-square-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/src/assets/pixel-square-icon.png -------------------------------------------------------------------------------- /src/assets/slp-logo-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/src/assets/slp-logo-2.png -------------------------------------------------------------------------------- /src/assets/slp-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/src/assets/slp-logo.png -------------------------------------------------------------------------------- /src/assets/slp-oval.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/src/assets/slp-oval.png -------------------------------------------------------------------------------- /src/assets/slp-qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/src/assets/slp-qrcode.png -------------------------------------------------------------------------------- /src/assets/slp-sticker.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/mint/a1ef4189becaf10da758b2a6ae5e946d92d69e6f/src/assets/slp-sticker.jpg -------------------------------------------------------------------------------- /src/components/App.css: -------------------------------------------------------------------------------- 1 | @import "~antd/dist/antd.less"; 2 | @import "~@fortawesome/fontawesome-free/css/all.css"; 3 | @import url("https://fonts.googleapis.com/css?family=Khula&display=swap&.css"); 4 | 5 | aside::-webkit-scrollbar { 6 | width: 0.3em; 7 | } 8 | aside::-webkit-scrollbar-track { 9 | -webkit-box-shadow: inset 0 0 6px #13171f; 10 | } 11 | aside::-webkit-scrollbar-thumb { 12 | background-color: darkgrey; 13 | outline: 1px solid slategrey; 14 | } 15 | 16 | .ant-modal-wrap.ant-modal-centered::-webkit-scrollbar { 17 | display: none; 18 | } 19 | 20 | .App { 21 | text-align: center; 22 | font-family: "Gilroy", sans-serif; 23 | background-color: #fbfbfd; 24 | } 25 | .App-logo { 26 | width: 100%; 27 | display: block; 28 | } 29 | 30 | .logo img { 31 | width: 100%; 32 | min-width: 193px; 33 | display: block; 34 | padding-left: 24px; 35 | padding-right: 34px; 36 | padding-top: 24px; 37 | } 38 | .ant-list-item-meta .ant-list-item-meta-content { 39 | display: flex; 40 | } 41 | 42 | #react-qrcode-logo { 43 | border-radius: 8px; 44 | } 45 | .App-header { 46 | background-color: #282c34; 47 | min-height: 100vh; 48 | display: flex; 49 | flex-direction: column; 50 | align-items: center; 51 | justify-content: center; 52 | font-size: calc(10px + 2vmin); 53 | color: white; 54 | } 55 | 56 | .App-link { 57 | color: #09d3ac; 58 | } 59 | .ant-menu-item-group-title { 60 | color: rgba(255, 255, 255, 0.45) !important; 61 | padding-left: 30px; 62 | font-size: 10px !important; 63 | font-weight: 500 !important; 64 | } 65 | 66 | .ant-menu-item > span { 67 | font-size: 14px !important; 68 | font-weight: 500 !important; 69 | } 70 | 71 | .ant-card-actions > li > span:hover, 72 | .ant-btn:hover, 73 | .ant-btn:focus { 74 | color: #6e6e6e; 75 | transition: color 0.3s; 76 | background-color: white; 77 | } 78 | 79 | .ant-card-actions > li { 80 | color: #3e3f42; 81 | } 82 | .anticon { 83 | color: #3e3f42; 84 | } 85 | .ant-list-item-meta-description, 86 | .ant-list-item-meta-title { 87 | color: #3e3f42; 88 | } 89 | 90 | .ant-list-item-meta-description > :first-child { 91 | right: 20px !important; 92 | position: absolute; 93 | } 94 | 95 | .ant-modal-body .ant-list-item-meta { 96 | height: 85px; 97 | width: 85px; 98 | padding-left: 10px; 99 | padding-top: 10px; 100 | padding-bottom: 20px; 101 | overflow: visible !important; 102 | } 103 | 104 | .ant-radio-group-solid .ant-radio-button-wrapper { 105 | padding-top: 10px; 106 | padding-left: 20px; 107 | margin-top: 0px; 108 | } 109 | 110 | .ant-radio-group-solid .ant-radio-button-wrapper-checked { 111 | border: 1px solid rgb(0, 0, 0) !important; 112 | box-shadow: inset 0px 1px 3px 0px rgba(0, 0, 0, 0.81) !important; 113 | } 114 | .identicon { 115 | border-radius: 50%; 116 | width: 200px; 117 | height: 200px; 118 | margin-top: -75px; 119 | margin-left: -75px; 120 | margin-bottom: 20px; 121 | box-shadow: 1px 1px 2px 1px #444; 122 | } 123 | .ant-list-item-meta { 124 | width: 40px; 125 | height: 40px; 126 | } 127 | 128 | .ant-radio-group-solid .ant-radio-button-wrapper-checked { 129 | background: #2b3240 !important; 130 | } 131 | 132 | .ant-radio-group.ant-radio-group-solid.ant-radio-group-small { 133 | font-size: 12px !important; 134 | font-weight: 600 !important; 135 | vertical-align: middle; 136 | border-radius: 19.5px; 137 | overflow: auto; 138 | border: 1px solid rgb(0, 0, 0) !important; 139 | box-shadow: inset 0px 1px 3px 0px rgba(0, 0, 0, 0.81) !important; 140 | background: #10131b !important; 141 | margin-top: 14px; 142 | margin-bottom: 10px; 143 | cursor: pointer; 144 | } 145 | 146 | input.ant-input { 147 | background-color: #fff !important; 148 | height: 42px; 149 | box-shadow: none !important; 150 | border: 1px solid #eaedf3; 151 | border-radius: 8px; 152 | font-weight: bold; 153 | color: rgb(62, 63, 66); 154 | opacity: 1; 155 | } 156 | 157 | .ant-checkbox-inner { 158 | border: 1px solid #eaedf3 !important; 159 | background: white; 160 | } 161 | 162 | .ant-checkbox-inner::after { 163 | border-color: white !important; 164 | } 165 | 166 | .ant-card-bordered { 167 | border: 1px solid rgb(234, 237, 243); 168 | border-radius: 8px; 169 | } 170 | 171 | .ant-card-actions { 172 | border-top: 1px solid rgb(234, 237, 243); 173 | border-bottom: 1px solid rgb(234, 237, 243); 174 | border-bottom-left-radius: 8px; 175 | border-bottom-right-radius: 8px; 176 | box-shadow: 0px 5px 8px rgba(0, 0, 0, 0.35); 177 | } 178 | 179 | .ant-input-group-addon { 180 | background-color: #f4f4f4 !important; 181 | border: 1px solid rgb(234, 237, 243); 182 | color: #3e3f42 !important; 183 | 184 | * { 185 | color: #3e3f42 !important; 186 | } 187 | } 188 | 189 | .ant-menu-item.ant-menu-item-selected > * { 190 | color: #5daa7e !important; 191 | } 192 | 193 | .ant-menu-item.ant-menu-item-selected { 194 | border-radius: 6px; 195 | border: 0; 196 | overflow: hidden; 197 | margin-left: 14px !important; 198 | margin-right: 14px !important; 199 | text-align: left; 200 | padding-left: 12px; 201 | } 202 | 203 | .ant-btn { 204 | border-radius: 8px; 205 | background-color: #fff; 206 | color: rgb(62, 63, 66); 207 | font-weight: bold; 208 | } 209 | 210 | .ant-card-actions > li:not(:last-child) { 211 | border-right: 0; 212 | } 213 | .ant-list-item-meta-avatar > img { 214 | margin-left: -12px; 215 | transform: translate(0, -6px); 216 | } 217 | 218 | .ant-list-item-meta-avatar > svg { 219 | margin-right: -70px; 220 | } 221 | 222 | .ant-alert-warning { 223 | background-color: #20242d; 224 | border: 1px solid #17171f; 225 | border-radius: 0; 226 | } 227 | 228 | .ant-alert-message { 229 | color: #fff; 230 | } 231 | 232 | .ant-layout-sider-zero-width-trigger.ant-layout-sider-zero-width-trigger-left 233 | .anticon.anticon-bars { 234 | color: #fff; 235 | transform: scale(1.3); 236 | } 237 | 238 | .ant-layout-sider-zero-width-trigger.ant-layout-sider-zero-width-trigger-left { 239 | background: #3e3f42; 240 | border-radius: 0 8px 8px 0; 241 | } 242 | 243 | .ant-btn-group .ant-btn-primary:first-child:not(:last-child) { 244 | border-right-color: transparent !important; 245 | } 246 | 247 | .ant-btn-group .ant-btn-primary:last-child:not(:first-child), 248 | .ant-btn-group .ant-btn-primary + .ant-btn-primary { 249 | border-left-color: #20242d !important; 250 | } 251 | 252 | .audit { 253 | a, 254 | a:active { 255 | color: #46464a; 256 | } 257 | 258 | a:hover { 259 | color: #111117; 260 | } 261 | } 262 | 263 | .dividends { 264 | a, 265 | a:active { 266 | color: #111117; 267 | } 268 | 269 | a:hover { 270 | color: #46464a; 271 | } 272 | } 273 | 274 | .ant-popover-inner-content { 275 | color: white; 276 | } 277 | 278 | .ant-modal-body .ant-card { 279 | max-width: 100%; 280 | } 281 | 282 | .ant-upload.ant-upload-drag { 283 | border: 1px solid #eaedf3; 284 | border-radius: 8px; 285 | background: #d3d3d3; 286 | } 287 | 288 | .ant-upload-list-item:hover .ant-upload-list-item-info { 289 | background-color: #ffffff; 290 | } 291 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "antd/dist/antd.less"; 3 | import "../index.css"; 4 | import styled from "styled-components"; 5 | import { useSwipeable } from "react-swipeable"; 6 | import { Layout, Menu, Radio, Tabs, Icon } from "antd"; 7 | import Portfolio from "./Portfolio/Portfolio"; 8 | import Icons from "./Icons/Icons"; 9 | import Create from "./Create/Create"; 10 | import Dividends from "./Dividends/Dividends"; 11 | import Configure from "./Configure/Configure"; 12 | import Audit from "./Audit/Audit"; 13 | import SatoshiDice from "./SatoshiDice/SatoshiDice"; 14 | import NotFound from "./NotFound"; 15 | import "./App.css"; 16 | import { WalletContext } from "../utils/context"; 17 | import logo from "../assets/logo.png"; 18 | import { Route, Redirect, Link, Switch, useLocation, useHistory } from "react-router-dom"; 19 | import { QRCode } from "./Common/QRCode"; 20 | import DividendHistory from "./DividendHistory/DividendHistory"; 21 | 22 | const { Header, Content, Sider, Footer } = Layout; 23 | const { TabPane } = Tabs; 24 | 25 | const StyledTabsMenu = styled.div` 26 | .ant-layout-footer { 27 | position: fixed; 28 | bottom: 0; 29 | width: 100%; 30 | padding: 0; 31 | } 32 | .ant-tabs-bar.ant-tabs-bottom-bar { 33 | margin-top: 0; 34 | border-top: 1px solid #ddd; 35 | } 36 | 37 | .ant-tabs-tab { 38 | span { 39 | font-size: 10px; 40 | display: grid; 41 | font-weight: bold; 42 | } 43 | .anticon { 44 | color: rgb(148, 148, 148); 45 | font-size: 24px; 46 | margin-left: 8px; 47 | margin-bottom: 3px; 48 | } 49 | } 50 | 51 | .ant-tabs-tab:hover { 52 | color: #4ab290 !important; 53 | .anticon { 54 | color: #4ab290; 55 | } 56 | } 57 | 58 | .ant-tabs-tab-active.ant-tabs-tab { 59 | color: #4ab290; 60 | .anticon { 61 | color: #4ab290; 62 | } 63 | } 64 | 65 | .ant-tabs-tab-active.ant-tabs-tab { 66 | color: #4ab290; 67 | .anticon { 68 | color: #4ab290; 69 | } 70 | } 71 | .ant-tabs-tab-active:active { 72 | color: #4ab290 !important; 73 | } 74 | .ant-tabs-ink-bar { 75 | display: none !important; 76 | } 77 | 78 | .ant-tabs-nav { 79 | margin: -3.5px 0 0 0; 80 | } 81 | `; 82 | 83 | const App = () => { 84 | const [collapsed, setCollapsed] = React.useState(window.innerWidth < 768); 85 | const [mobile, setMobile] = React.useState(false); 86 | const [address, setAddress] = React.useState("slpAddress"); 87 | const [pixelRatio, setPixelRatio] = React.useState(1); 88 | const [isCountryBanned, setIsCountryBanned] = React.useState(false); 89 | 90 | const ContextValue = React.useContext(WalletContext); 91 | const { wallet } = ContextValue; 92 | const radio = React.useRef(null); 93 | const location = useLocation(); 94 | const history = useHistory(); 95 | const selectedKey = location && location.pathname ? location.pathname.substr(1) : ""; 96 | const handleChange = e => { 97 | window.scrollTo(0, 0); 98 | setTimeout(() => { 99 | if (mobile) { 100 | setCollapsed(true); 101 | document.body.style.overflow = ""; 102 | } 103 | }, 100); 104 | }; 105 | 106 | const handleChangeAddress = e => { 107 | setAddress(address === "cashAddress" ? "slpAddress" : "cashAddress"); 108 | }; 109 | 110 | const handleResize = () => { 111 | setMobile(window.innerWidth < 768); 112 | setPixelRatio(window.devicePixelRatio); 113 | }; 114 | 115 | const handleClickTrigger = e => (document.body.style.overflow = "hidden"); 116 | 117 | const checkIsCountryBanned = async () => { 118 | const bannedCountries = ["United States"]; 119 | let isBanned = false; 120 | try { 121 | const result = await fetch( 122 | `https://cors-anywhere.herokuapp.com/https://api.ipify.org/?format=json` 123 | ); 124 | const { ip } = await result.json(); 125 | 126 | console.log(`IP Fetching from ipify`, ip); 127 | const ipData = await fetch( 128 | `https://cors-anywhere.herokuapp.com/http://api.ipstack.com/${ip}?access_key=${process.env.REACT_APP_IPSTACK_KEY}` 129 | ); 130 | const { country_name } = await ipData.json(); 131 | if (bannedCountries.includes(country_name) || typeof country_name == "undefined") { 132 | setIsCountryBanned(true); 133 | } 134 | } catch (e) { 135 | console.error(e); 136 | } 137 | return isBanned; 138 | }; 139 | 140 | React.useEffect(() => { 141 | if (mobile && pixelRatio === 1) { 142 | const triggerElement = document.getElementsByTagName("aside")[0].children[1]; 143 | 144 | triggerElement.addEventListener("click", handleClickTrigger); 145 | 146 | return () => triggerElement.removeEventListener("click", handleClickTrigger); 147 | } 148 | 149 | // eslint-disable-next-line 150 | }, [mobile]); 151 | 152 | React.useEffect(() => { 153 | handleResize(); 154 | window.addEventListener("resize", handleResize); 155 | checkIsCountryBanned(setIsCountryBanned); 156 | return () => window.removeEventListener("resize", handleResize); 157 | }, []); 158 | 159 | const handleSwipe = useSwipeable({ 160 | trackMouse: mobile, 161 | onSwipedRight: () => { 162 | if (mobile) { 163 | setCollapsed(false); 164 | document.body.style.overflow = "hidden"; 165 | } 166 | }, 167 | onSwipedLeft: () => { 168 | if (mobile) { 169 | setCollapsed(true); 170 | document.body.style.overflow = ""; 171 | } 172 | } 173 | }); 174 | 175 | return ( 176 |
177 | 178 |
198 | setCollapsed(!collapsed)} 203 | width="256" 204 | style={ 205 | mobile 206 | ? { 207 | zIndex: "100", 208 | position: "fixed", 209 | top: 0, 210 | bottom: 0, 211 | overflowY: `${collapsed ? "" : "scroll"}`, 212 | overflowX: `${collapsed ? "" : "hidden"}` 213 | } 214 | : { height: "100%" } 215 | } 216 | > 217 |
218 | Bitcoin.com Mint 219 |
220 |
229 | handleChange(e)} 232 | selectedKeys={[selectedKey]} 233 | style={{ textAlign: "left" }} 234 | > 235 | 236 | Links}> 237 | 238 | {" "} 239 | 240 | Send BCH by Email 241 | 242 | 243 | 244 | 245 | Faucet (Free BCH) 246 | 247 | 248 | 249 | 254 | Exchange 255 | 256 | 257 | 258 | {" "} 259 | 264 | Games 265 | 266 | 267 | 268 | {" "} 269 | 270 | Trade Locally 271 | 272 | 273 | 274 | 275 | 276 | {wallet ? ( 277 | 278 |
284 |
285 | 294 |
295 | 296 | 303 | handleChangeAddress(e)} 311 | > 312 | SLP Tokens 313 | 314 | handleChangeAddress(e)} 322 | > 323 | Bitcoin Cash 324 | 325 | 326 |
327 |
328 | ) : null} 329 |
330 | 331 |
332 | 333 |
340 |
347 |
348 | 349 |
355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 |
363 |
364 | {mobile && } 365 |
366 | 367 |
368 | ); 369 | }; 370 | 371 | export default App; 372 | -------------------------------------------------------------------------------- /src/components/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div"); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/Audit/Audit.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Row, Col, Card, Icon, Typography } from "antd"; 3 | import StyledAudit from "../Common/StyledPage"; 4 | const { Text, Title } = Typography; 5 | 6 | export default () => { 7 | return ( 8 | 9 | 10 | 11 | 15 | Audit 16 | 17 | } 18 | bordered={true} 19 | > 20 | Never trust, always verify. 21 | 22 | Check the open source code{" "} 23 | 28 | here 29 | 30 | . 31 | 32 |
33 | Check and/or change the REST API in Configure. 34 |
35 | 36 | Install, build and run Bitcoin.com Mint{" "} 37 | 42 | locally 43 | 44 | .{" "} 45 | 46 |
47 | 48 | Join our{" "} 49 | 50 | public telegram group 51 | 52 | . 53 | 54 |
55 | 56 |
57 |
58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /src/components/Common/CustomIcons.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Icon } from "antd"; 3 | 4 | const hammer = () => ( 5 | 6 | 10 | 11 | ); 12 | 13 | const plane = () => ( 14 | 15 | 16 | 17 | 18 | 26 | 27 | 28 | 36 | 37 | 38 | 39 | 46 | 53 | 54 | 55 | ); 56 | 57 | const fire = () => ( 58 | 70 | ); 71 | 72 | export const HammerIcon = props => ; 73 | 74 | export const PlaneIcon = props => ; 75 | 76 | export const FireIcon = props => ; 77 | -------------------------------------------------------------------------------- /src/components/Common/QRCode.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components"; 3 | import RawQRCode from "qrcode.react"; 4 | import slpLogo from "../../assets/slp-logo-2.png"; 5 | import bchLogo from "../../assets/bch-icon-qrcode.png"; 6 | import { CopyToClipboard } from "react-copy-to-clipboard"; 7 | import { Input, Button } from "antd"; 8 | 9 | export const StyledRawQRCode = styled(RawQRCode)` 10 | cursor: pointer; 11 | border-radius: 8px; 12 | `; 13 | 14 | const AddrHolder = styled.textarea` 15 | font-size: 18px; 16 | font-weight: bold; 17 | resize: none; 18 | border-radius: 6px !important; 19 | width: 180px; 20 | border-radius: 0; 21 | height: 40px !important; 22 | border-width: 0; 23 | // padding: 18px 0; 24 | padding: 20px 0px 0px 0px !important; 25 | text-align: center; 26 | background: #8eaaaf; 27 | margin-top: 90px; 28 | margin-left: 15px; 29 | overflow: hidden; 30 | line-height: 0 !important; 31 | `; 32 | 33 | const StyledInput = styled.div` 34 | font-size: 12px; 35 | line-height: 14px; 36 | resize: none; 37 | width: 209px; 38 | border-radius: 5px; 39 | margin-top: 12px; 40 | color: black; 41 | height: 50px; 42 | .ant-input:hover { 43 | border-color: rgba(127, 127, 127, 0.1); 44 | border-right-width: 1px !important; 45 | } 46 | .ant-input-disabled:hover { 47 | border-color: rgba(127, 127, 127, 0.1); 48 | border-right-width: 1px !important; 49 | } 50 | .ant-input[disabled]:hover { 51 | border-color: rgba(127, 127, 127, 0.1); 52 | border-right-width: 1px !important; 53 | } 54 | .ant-input { 55 | font-size: 10px; 56 | } 57 | `; 58 | 59 | export const QRCode = ({ address, size = 210, onClick = () => null, ...otherProps }) => { 60 | const [visible, setVisible] = useState(false); 61 | 62 | const txtRef = React.useRef(null); 63 | 64 | const handleOnClick = evt => { 65 | setVisible(true); 66 | setTimeout(() => { 67 | setVisible(false); 68 | }, 1500); 69 | onClick(evt); 70 | }; 71 | 72 | const handleOnCopy = () => { 73 | setVisible(true); 74 | setTimeout(() => { 75 | txtRef.current.select(); 76 | }, 100); 77 | }; 78 | 79 | return ( 80 | 85 |
86 | 96 | 97 | {/**/} 112 | 127 | 128 | 129 | 138 | } 139 | value={visible ? address : null} 140 | placeholder={address} 141 | disabled={!visible} 142 | autoComplete="off" 143 | type="text" 144 | spellCheck="false" 145 | addonAfter={
149 |
150 | ); 151 | }; 152 | -------------------------------------------------------------------------------- /src/components/Common/StyledCollapse.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { Collapse } from "antd"; 3 | 4 | export const StyledCollapse = styled(Collapse)` 5 | background: #fbfcfd !important; 6 | border: 1px solid #eaedf3 !important; 7 | 8 | .ant-collapse-content { 9 | border: 1px solid #eaedf3; 10 | border-top: none; 11 | } 12 | 13 | .ant-collapse-item { 14 | border-bottom: none !important; 15 | } 16 | 17 | * { 18 | color: rgb(62, 63, 66) !important; 19 | } 20 | `; 21 | -------------------------------------------------------------------------------- /src/components/Common/StyledOnBoarding.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledOnBoarding = styled.div` 4 | .ant-card { 5 | background: #ffffff; 6 | box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.04); 7 | overflow: hidden; 8 | 9 | &:hover { 10 | box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1); 11 | } 12 | 13 | * { 14 | color: rgb(62, 63, 66); 15 | } 16 | 17 | .ant-card-head { 18 | color: #6e6e6e !important; 19 | background: #fbfcfd; 20 | border-bottom: 1px solid #eaedf3; 21 | } 22 | 23 | .ant-alert { 24 | background: #fbfcfd; 25 | border: 1px solid #eaedf3; 26 | } 27 | } 28 | .ant-card-body { 29 | border: none; 30 | } 31 | .ant-collapse { 32 | background: #fbfcfd; 33 | border: 1px solid #eaedf3; 34 | 35 | .ant-collapse-content { 36 | border: 1px solid #eaedf3; 37 | border-top: none; 38 | 39 | .ant-collapse-content-box { 40 | padding: 6px; 41 | .ant-row.ant-form-item { 42 | margin-bottom: 0px; 43 | } 44 | } 45 | } 46 | 47 | .ant-collapse-item { 48 | border-bottom: 1px solid #eaedf3; 49 | } 50 | 51 | * { 52 | color: rgb(62, 63, 66) !important; 53 | } 54 | } 55 | `; 56 | 57 | export default StyledOnBoarding; 58 | -------------------------------------------------------------------------------- /src/components/Common/StyledPage.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledPage = styled.div` 4 | .ant-card { 5 | background: #ffffff; 6 | box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.04); 7 | overflow: hidden; 8 | 9 | &:hover { 10 | box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1); 11 | } 12 | 13 | a { 14 | color: rgb(93, 170, 126); 15 | } 16 | 17 | * { 18 | color: rgb(62, 63, 66); 19 | } 20 | 21 | .ant-card-head { 22 | color: #6e6e6e !important; 23 | background: #fbfcfd; 24 | border-bottom: 1px solid #eaedf3; 25 | } 26 | 27 | .ant-alert { 28 | background: #fbfcfd; 29 | border: 1px solid #eaedf3; 30 | } 31 | } 32 | .ant-card-body { 33 | border: none; 34 | } 35 | .ant-collapse { 36 | background: #fbfcfd; 37 | border: 1px solid #eaedf3; 38 | 39 | .ant-collapse-content { 40 | border: 1px solid #eaedf3; 41 | border-top: none; 42 | } 43 | 44 | .ant-collapse-item { 45 | border-bottom: 1px solid #eaedf3; 46 | } 47 | 48 | * { 49 | color: rgb(62, 63, 66) !important; 50 | } 51 | } 52 | `; 53 | 54 | export default StyledPage; 55 | -------------------------------------------------------------------------------- /src/components/Configure/Configure.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { 3 | Row, 4 | Col, 5 | Card, 6 | Icon, 7 | Alert, 8 | Typography, 9 | Form, 10 | Input, 11 | Button, 12 | Collapse, 13 | Select 14 | } from "antd"; 15 | import StyledConfigure from "../Common/StyledPage"; 16 | import { WalletContext } from "../../utils/context"; 17 | import { StyledCollapse } from "../Common/StyledCollapse"; 18 | import { getRestUrl } from "../../utils/withSLP"; 19 | const { Paragraph } = Typography; 20 | const { Panel } = Collapse; 21 | const { Option } = Select; 22 | 23 | const selectBefore = (protocol, handleChangeProcotol) => ( 24 | 28 | ); 29 | 30 | export default () => { 31 | const ContextValue = React.useContext(WalletContext); 32 | const { wallet } = ContextValue; 33 | const [visible, setVisible] = useState(true); 34 | const [option, setOption] = useState(getRestUrl()); 35 | const [protocol, setProtocol] = useState("https://"); 36 | 37 | const handleClose = () => setVisible(false); 38 | const [isConfigUpdated, setIsConfigUpdated] = React.useState(false); 39 | const [data, setData] = React.useState({ 40 | dirty: true, 41 | restAPI: window.localStorage.getItem("restAPI") 42 | }); 43 | const defaultRestUrl = "https://rest.bch.actorforth.org/v2"; 44 | 45 | const newRestApiUrl = (protocol, restAPI) => protocol.concat(restAPI); 46 | const handleChangeProcotol = protocol => setProtocol(protocol); 47 | const isValidCustomRest = (option, protocol, restAPI) => 48 | option === "custom" && 49 | // eslint-disable-next-line 50 | /^(?:http(s)?:\/\/)[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/.test( 51 | newRestApiUrl(protocol, restAPI) 52 | ); 53 | 54 | const handleConfigure = () => { 55 | setData(p => ({ ...p, dirty: false })); 56 | if ( 57 | (option === "custom" && !isValidCustomRest(option, protocol, data.restAPI)) || 58 | (option !== "custom" && getRestUrl() !== defaultRestUrl && option !== defaultRestUrl) || 59 | option === getRestUrl() 60 | ) { 61 | return; 62 | } else { 63 | window.localStorage.setItem( 64 | "restAPI", 65 | option === "custom" ? newRestApiUrl(protocol, data.restAPI) : defaultRestUrl 66 | ); 67 | setIsConfigUpdated(true); 68 | window.localStorage.setItem("wallet", null); 69 | setTimeout(() => { 70 | window.location.reload(); 71 | }, 3000); 72 | } 73 | }; 74 | const handleChange = e => { 75 | const { value, name } = e.target; 76 | setData(p => ({ ...p, [name]: value })); 77 | }; 78 | 79 | return ( 80 | 81 | 82 | 83 | 86 | Maintenance 87 | 88 | } 89 | bordered={true} 90 | > 91 | {visible ? ( 92 | 96 | 97 | Announcement 98 | 99 | 100 | Mint will soon become Pitico 2.0, a revamped and improved version of the 101 | original project that became Mint. 102 | 103 | All features are disabled for now. 104 | 105 | If you already have a wallet, backup your seed below. Your funds are safe. 106 | 107 | 108 | For further questions and updates, join our telegram group: 109 | https://t.me/piticocash 110 | 111 | 112 | } 113 | type="warning" 114 | closable 115 | afterClose={handleClose} 116 | /> 117 | ) : null} 118 | {wallet && wallet.mnemonic && ( 119 | 120 | 121 |

{wallet && wallet.mnemonic ? wallet.mnemonic : ""}

122 |
123 |
124 | )} 125 |
126 | 127 |
128 |
129 | ); 130 | }; 131 | -------------------------------------------------------------------------------- /src/components/DividendHistory/DividendHistory.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Img from "react-image"; 3 | import makeBlockie from "ethereum-blockies-base64"; 4 | import { WalletContext } from "../../utils/context"; 5 | import { Icon, Row, Col, Empty, Progress, Descriptions, Button, Alert } from "antd"; 6 | import styled, { createGlobalStyle } from "styled-components"; 7 | import moment from "moment"; 8 | import { useEffect } from "react"; 9 | import Dividends from "../../utils/dividends/dividends"; 10 | import { SLP_TOKEN_ICONS_URL } from "../Portfolio/Portfolio"; 11 | import { EnhancedCard } from "../Portfolio/EnhancedCard"; 12 | import bchFlagLogo from "../../assets/4-bitcoin-cash-logo-flag.png"; 13 | import { getEncodedOpReturnMessage } from "../../utils/sendDividends"; 14 | import ButtonGroup from "antd/lib/button/button-group"; 15 | 16 | const StyledCol = styled(Col)` 17 | margin-top: 8px; 18 | `; 19 | 20 | const StyledSummary = styled.div` 21 | display: flex; 22 | flex-direction: column; 23 | justify-content: space-between; 24 | `; 25 | 26 | const GlobalStyle = createGlobalStyle` 27 | .ant-modal-body ${StyledSummary} { 28 | padding-top: 50px; 29 | } 30 | `; 31 | 32 | const StyledSummaryIcons = styled.div` 33 | display: flex; 34 | align-items: center; 35 | justify-content: space-between; 36 | `; 37 | 38 | const StyledProgressAndIcon = styled.div` 39 | position: relative; 40 | `; 41 | 42 | const StyledProgress = styled(Progress)` 43 | position: absolute; 44 | top: 0; 45 | left: 0; 46 | transform: scale(0.8, 0.8) translate(-35%, -120%); 47 | 48 | .ant-progress-text { 49 | color: ${props => props.strokeColor} !important; 50 | } 51 | `; 52 | 53 | const StyledDescriptions = styled(Descriptions)` 54 | margin-top: 6px; 55 | overflow: visible; 56 | 57 | .ant-descriptions-item-content, 58 | .ant-descriptions-item-content * { 59 | font-size: 14px; 60 | color: rgba(127, 127, 127, 0.65) !important; 61 | } 62 | 63 | .ant-descriptions-row > th, 64 | .ant-descriptions-row > td { 65 | padding-bottom: 4px; 66 | } 67 | 68 | .ant-descriptions-item-label { 69 | color: black; 70 | 71 | &::after { 72 | margin-right: 4px; 73 | } 74 | } 75 | `; 76 | 77 | const StyledSummaryIconArrow = styled(Icon)` 78 | font-size: 20px; 79 | `; 80 | 81 | const StyledCard = styled(EnhancedCard)` 82 | margin-top: 8px; 83 | text-align: left; 84 | overflow hidden; 85 | 86 | .ant-modal-body ${StyledSummary} { 87 | padding-top: 50px; 88 | } 89 | `; 90 | 91 | const DividendHistory = () => { 92 | const { balances } = React.useContext(WalletContext); 93 | const [dividends, setDividends] = useState(null); 94 | const [selected, setSelected] = useState(null); 95 | 96 | useEffect(() => { 97 | setDividends(Object.values(Dividends.getAll()).sort((a, b) => b.startDate - a.startDate)); 98 | }, [balances]); 99 | 100 | const isEmpty = !dividends || dividends.length === 0; 101 | 102 | const getProgressColor = dividend => { 103 | if (dividend.status === Dividends.Status.PAUSED) { 104 | return "orange"; 105 | } else if ( 106 | dividend.status === Dividends.Status.CANCELED || 107 | dividend.status === Dividends.Status.CRASHED 108 | ) { 109 | return "red"; 110 | } else { 111 | return "#00c389"; 112 | } 113 | }; 114 | 115 | const updateDividendStatus = (dividend, status) => { 116 | const div = dividends.find(d => d.startDate === dividend.startDate); 117 | div.status = status; 118 | setDividends([...dividends]); 119 | Dividends.save(dividend); 120 | }; 121 | 122 | return ( 123 | <> 124 | 125 | {isEmpty ? ( 126 | 127 | ) : ( 128 | 129 | {dividends.map(dividend => ( 130 | 131 | setSelected(dividend)} 133 | expand={selected && selected.startDate === dividend.startDate} 134 | renderExpanded={() => ( 135 | <> 136 |
137 | {dividend.progress === 1 && ( 138 | 142 | 143 | Completed 144 | 145 | } 146 | type="info" 147 | closable={false} 148 | /> 149 | )} 150 | {dividend.status === Dividends.Status.CANCELED && ( 151 | 155 | 156 | Canceled 157 | 158 | } 159 | type="info" 160 | closable={false} 161 | /> 162 | )} 163 | {dividend.status === Dividends.Status.CRASHED && ( 164 | 168 | 169 | Crashed {dividend.error ? `Cause: ${dividend.error}` : ""} 170 | 171 | } 172 | type="error" 173 | closable={false} 174 | /> 175 | )} 176 | {dividend.progress < 1 && dividend.status !== Dividends.Status.CANCELED ? ( 177 | 178 | {dividend.status !== Dividends.Status.PAUSED && 179 | dividend.status !== Dividends.Status.CRASHED ? ( 180 | 187 | ) : ( 188 | 197 | )} 198 | 199 | ) : null} 200 | 201 | 202 | {dividend.totalRecipients} 203 | 204 | 205 | { 206 | getEncodedOpReturnMessage(dividend.opReturn, dividend.token.tokenId) 207 | .decodedOpReturn 208 | } 209 | 210 | 211 | {moment(dividend.startDate).format("LL LTS")} 212 | 213 | {dividend.endDate ? ( 214 | 215 | {moment(dividend.endDate).format("LL LTS")} 216 | 217 | ) : null} 218 | {dividend.txs.map((tx, index) => ( 219 | 220 | 225 | {tx} 226 | 227 | 228 | ))} 229 | 230 |
231 | 232 | )} 233 | onClose={() => setSelected(null)} 234 | > 235 | 236 | 237 | 238 | 239 | 246 | 247 | 248 | {`identicon 259 | } 260 | /> 261 | 262 | 263 | {dividend.totalValue} 264 | 265 | {dividend.token.info.symbol} 266 | 267 | 268 | 269 |
270 |
271 | ))} 272 |
273 | )} 274 | 275 | ); 276 | }; 277 | 278 | export default DividendHistory; 279 | -------------------------------------------------------------------------------- /src/components/Dividends/Dividends.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Row, Col } from "antd"; 3 | import PayDividends from "../Portfolio/PayDividends/PayDividends"; 4 | import StyledDividends from "../Common/StyledPage"; 5 | 6 | export default () => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Row, Col } from "antd"; 3 | 4 | export default () => ( 5 | 6 | 7 |

Page not found

8 | 9 |
10 | ); 11 | -------------------------------------------------------------------------------- /src/components/OnBoarding/OnBoarding.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { WalletContext } from "../../utils/context"; 3 | import { Input, Button, Icon, Row, Col, Card, Form, Collapse } from "antd"; 4 | import Img from "react-image"; 5 | import StyledOnboarding from "../Common/StyledOnBoarding"; 6 | import bitcoinWalletLogo from "../../assets/bitcoin-com-wallet-icon.png"; 7 | import badgerWalletLogo from "../../assets/badger-icon.png"; 8 | import pixelSquareLogo from "../../assets/pixel-square-icon.png"; 9 | 10 | export const OnBoarding = ({ history }) => { 11 | const ContextValue = React.useContext(WalletContext); 12 | const { createWallet } = ContextValue; 13 | const [formData, setFormData] = useState({ 14 | dirty: true, 15 | mnemonic: "" 16 | }); 17 | const [openKey, setOpenKey] = useState(""); 18 | const [warningRead, setWarningRead] = useState(false); 19 | 20 | async function submit() { 21 | setFormData({ 22 | ...formData, 23 | dirty: false 24 | }); 25 | 26 | if (!formData.mnemonic) { 27 | return; 28 | } 29 | 30 | createWallet(formData.mnemonic); 31 | } 32 | 33 | const handleChange = e => { 34 | const { value, name } = e.target; 35 | 36 | setFormData(p => ({ ...p, [name]: value })); 37 | }; 38 | 39 | const handleWarning = () => setWarningRead(true); 40 | 41 | const handleCollapseChange = key => { 42 | setOpenKey(key); 43 | setFormData(p => ({ ...p, mnemonic: "" })); 44 | if (key !== "2") setWarningRead(false); 45 | }; 46 | 47 | return ( 48 | 49 | 50 | 51 | 54 | New Wallet 55 | 56 | } 57 | style={{ height: "100%" }} 58 | bordered={false} 59 | > 60 |
61 | 64 |
65 |
66 | 67 | 68 | 71 | Import Wallet 72 | 73 | } 74 | bordered={false} 75 | > 76 |
77 | handleCollapseChange(key)}> 78 | 81 | {" "} 87 | mint.bitcoin.com wallet 88 | 89 | } 90 | key="1" 91 | style={{ textAlign: "left" }} 92 | > 93 | {openKey === "1" && ( 94 | 98 | } 100 | placeholder="mnemonic (seed phrase)" 101 | name="mnemonic" 102 | onChange={e => handleChange(e)} 103 | required 104 | /> 105 | 106 | )} 107 | 108 | 109 | 110 | 111 | 112 |
113 | 116 |
117 |
118 |
119 | 120 |
121 | 122 | 123 | 126 | Web Wallets 127 | 128 | } 129 | style={{ height: "100%" }} 130 | bordered={false} 131 | > 132 |
133 |

134 | Bitcoin.com Mint is an{" "} 135 | 140 | open source, 141 | {" "} 142 | non-custodial web wallet supporting SLP and BCH.{" "} 143 |

144 |

145 | {" "} 146 | Web wallets offer user convenience, but storing large amounts of money on a web 147 | wallet is not recommended. 148 |

149 |

Creating your own SLP tokens only costs a few cents worth of BCH.

150 |
151 |
152 | 153 |
154 |
155 | ); 156 | }; 157 | -------------------------------------------------------------------------------- /src/components/Portfolio/EnhancedCard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Card, Modal } from "antd"; 4 | import { createGlobalStyle } from "styled-components"; 5 | 6 | const GlobalStyle = createGlobalStyle` 7 | .ant-modal-mask { 8 | background-color: transparent; 9 | } 10 | 11 | .ant-modal-content { 12 | background-color: transparent !important; 13 | } 14 | 15 | .ant-modal-body { 16 | font-family: "Gilroy", sans-serif; 17 | padding: 0 !important; 18 | background-color: transparent; 19 | } 20 | 21 | .ant-modal-close { 22 | top: 20px !important; 23 | right: 20px !important; 24 | } 25 | 26 | .ant-alert { 27 | background: #fbfcfd !important; 28 | border: 1px solid #eaedf3 !important; 29 | // padding-left: 40px !important; 30 | 31 | * { 32 | color: rgb(62, 63, 66) !important; 33 | } 34 | } 35 | `; 36 | 37 | const StyledWrapper = styled.div` 38 | @media (max-width: 768px) { 39 | text-align: -webkit-center; 40 | text-align: -moz-center; 41 | } 42 | `; 43 | 44 | const StyledEnhancedCard = styled(Card)` 45 | border-radius: 8px; 46 | background: #fff; 47 | border: 1px solid #eaedf3; 48 | box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.04); 49 | height: 173px; 50 | max-width: 321px; 51 | width: auto; 52 | cursor: pointer; 53 | will-change: width, height, box-shadow; 54 | transition: all 300ms ease-in-out; 55 | display: flex; 56 | flex-direction: column; 57 | height: 150px; 58 | 59 | .ant-card-body { 60 | height: calc(100% - 46px); 61 | } 62 | 63 | .ant-card-actions { 64 | box-shadow: none; 65 | border-bottom: 0; 66 | white-space: nowrap; 67 | padding-left: 23px; 68 | padding-right: 23px; 69 | 70 | li { 71 | text-align: left; 72 | } 73 | 74 | li:last-child > span { 75 | text-align: ${p => { 76 | if (p.token) { 77 | if (p.token.info && p.token.info.hasBaton) { 78 | return "left"; 79 | } 80 | return "right"; 81 | } 82 | return "left"; 83 | }}; 84 | } 85 | } 86 | 87 | .ant-card-bordered { 88 | border: 1px solid #eaedf3; 89 | border-radius: 8px; 90 | } 91 | 92 | &:hover { 93 | box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1); 94 | } 95 | 96 | .ant-input-group-addon { 97 | padding: 0; 98 | width: 48px; 99 | line-height: 40px; 100 | } 101 | 102 | .anticon { 103 | margin-right: 3px; 104 | vertical-align: sub; 105 | } 106 | `; 107 | 108 | export const StyledModal = styled(Modal)` 109 | .anticon { 110 | vertical-align: middle; 111 | } 112 | font-family: "Gilroy", sans-serif; 113 | 114 | ${StyledEnhancedCard} { 115 | .ant-list-item-meta-description > :first-child { 116 | display: none; 117 | } 118 | 119 | ${props => 120 | props.visible 121 | ? ` 122 | .ant-card-body { 123 | &> * { 124 | overflow-y: auto; 125 | max-height: 85%; 126 | width: 100%; 127 | } 128 | } 129 | ` 130 | : ""} 131 | } 132 | 133 | @media only screen and (max-width: 800px) { 134 | & { 135 | .ant-modal-body { 136 | padding: 0 !important; 137 | } 138 | 139 | ${StyledEnhancedCard} { 140 | margin-top: 0 !important; 141 | height: auto !important; 142 | ${props => 143 | props.visible 144 | ? ` 145 | .ant-card-body { 146 | &> * { 147 | overflow-y: hidden; 148 | } 149 | } 150 | ` 151 | : ""} 152 | } 153 | } 154 | } 155 | `; 156 | 157 | export const StyledExpandedWrapper = styled.div` 158 | .ant-card-head, 159 | .ant-card-body { 160 | padding: 0 !important; 161 | 162 | & > .ant-row-flex { 163 | margin: -8px; 164 | padding: 8px; 165 | } 166 | } 167 | `; 168 | 169 | export const EnhancedCard = ({ 170 | expand, 171 | renderExpanded = () => null, 172 | onClick, 173 | onClose, 174 | children, 175 | style, 176 | ...otherProps 177 | }) => { 178 | return ( 179 | 180 | 181 | 182 | {children} 183 | 184 | 192 | 193 | {children} 194 | {renderExpanded()} 195 | 196 | 197 | 198 | ); 199 | }; 200 | -------------------------------------------------------------------------------- /src/components/Portfolio/EnhancedInputs.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Form, Input, Icon } from "antd"; 3 | import styled from "styled-components"; 4 | import bchLogo from "../../assets/bch-logo-2.png"; 5 | import { ScanQRCode } from "./ScanQRCode"; 6 | import withSLP from "../../utils/withSLP"; 7 | 8 | export const InputAddonText = styled.span` 9 | width: 100%; 10 | height: 100%; 11 | display: block; 12 | 13 | ${props => 14 | props.disabled 15 | ? ` 16 | cursor: not-allowed; 17 | ` 18 | : `cursor: pointer;`} 19 | `; 20 | 21 | export const FormItemWithMaxAddon = ({ onMax, inputProps, ...otherProps }) => { 22 | return ( 23 | 24 | } 26 | addonAfter={ 27 | 31 | max 32 | 33 | } 34 | {...inputProps} 35 | /> 36 | 37 | ); 38 | }; 39 | 40 | export const FormItemWithQRCodeAddon = ({ onScan, inputProps, ...otherProps }) => { 41 | return ( 42 | 43 | } 45 | addonAfter={} 46 | {...inputProps} 47 | /> 48 | 49 | ); 50 | }; 51 | 52 | export const AddressValidators = withSLP(SLP => ({ 53 | safelyDetectAddressFormat: value => { 54 | try { 55 | return SLP.Address.detectAddressFormat(value); 56 | } catch (error) { 57 | return null; 58 | } 59 | }, 60 | isSLPAddress: value => AddressValidators.safelyDetectAddressFormat(value) === "slpaddr", 61 | isBCHAddress: value => AddressValidators.safelyDetectAddressFormat(value) === "cashaddr", 62 | isLegacyAddress: value => AddressValidators.safelyDetectAddressFormat(value) === "legacy" 63 | }))(); 64 | -------------------------------------------------------------------------------- /src/components/Portfolio/EnhancedModal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Card, Modal } from "antd"; 4 | import { createGlobalStyle } from "styled-components"; 5 | 6 | const GlobalStyle = createGlobalStyle` 7 | .ant-modal-mask { 8 | background-color: transparent; 9 | } 10 | 11 | .ant-modal-content { 12 | background-color: transparent !important; 13 | } 14 | 15 | .ant-modal-body { 16 | font-family: "Gilroy", sans-serif; 17 | padding: 0 !important; 18 | background-color: transparent; 19 | } 20 | 21 | .ant-modal-close { 22 | top: 20px !important; 23 | right: 20px !important; 24 | } 25 | 26 | .ant-alert { 27 | background: #fbfcfd !important; 28 | border: 1px solid #eaedf3 !important; 29 | // padding-left: 40px !important; 30 | 31 | * { 32 | color: rgb(62, 63, 66) !important; 33 | } 34 | } 35 | `; 36 | 37 | const StyledWrapper = styled.div` 38 | @media (max-width: 768px) { 39 | text-align: -webkit-center; 40 | text-align: -moz-center; 41 | } 42 | `; 43 | 44 | const StyledEnhancedCard = styled(Card)` 45 | border-radius: 8px; 46 | background: #fff; 47 | border: 1px solid #eaedf3; 48 | box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.04); 49 | height: 173px; 50 | max-width: 321px; 51 | width: auto; 52 | cursor: pointer; 53 | will-change: width, height, box-shadow; 54 | transition: all 300ms ease-in-out; 55 | display: flex; 56 | flex-direction: column; 57 | height: 150px; 58 | 59 | .ant-card-body { 60 | height: 600px; 61 | } 62 | 63 | .ant-card-actions { 64 | box-shadow: none; 65 | border-bottom: 0; 66 | white-space: nowrap; 67 | padding-left: 23px; 68 | padding-right: 23px; 69 | 70 | li { 71 | text-align: left; 72 | } 73 | 74 | li:last-child > span { 75 | text-align: ${p => { 76 | if (p.token) { 77 | if (p.token.info && p.token.info.hasBaton) { 78 | return "left"; 79 | } 80 | return "right"; 81 | } 82 | return "left"; 83 | }}; 84 | } 85 | } 86 | 87 | .ant-card-bordered { 88 | border: 1px solid #eaedf3; 89 | border-radius: 8px; 90 | } 91 | 92 | &:hover { 93 | box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1); 94 | } 95 | 96 | .ant-input-group-addon { 97 | padding: 0; 98 | width: 48px; 99 | line-height: 40px; 100 | } 101 | 102 | .anticon { 103 | margin-right: 3px; 104 | vertical-align: sub; 105 | } 106 | `; 107 | 108 | export const StyledModal = styled(Modal)` 109 | .anticon { 110 | vertical-align: middle; 111 | } 112 | font-family: "Gilroy", sans-serif; 113 | 114 | ${StyledEnhancedCard} { 115 | .ant-list-item-meta-description > :first-child { 116 | display: none; 117 | } 118 | 119 | ${props => 120 | props.visible 121 | ? ` 122 | .ant-card-body { 123 | &> * { 124 | overflow-y: auto; 125 | max-height: 85%; 126 | width: 100%; 127 | } 128 | } 129 | ` 130 | : ""} 131 | } 132 | 133 | @media only screen and (max-width: 800px) { 134 | & { 135 | .ant-modal-body { 136 | padding: 0 !important; 137 | } 138 | 139 | ${StyledEnhancedCard} { 140 | margin-top: 0 !important; 141 | height: auto !important; 142 | ${props => 143 | props.visible 144 | ? ` 145 | .ant-card-body { 146 | &> * { 147 | overflow-y: hidden; 148 | } 149 | } 150 | ` 151 | : ""} 152 | } 153 | } 154 | } 155 | `; 156 | 157 | export const StyledExpandedWrapper = styled.div` 158 | .ant-card-head, 159 | .ant-card-body { 160 | padding: 0 !important; 161 | height: 600px !important; 162 | & > .ant-row-flex { 163 | margin: -8px; 164 | padding: 8px; 165 | } 166 | } 167 | `; 168 | 169 | export const EnhancedModal = ({ 170 | expand, 171 | renderExpanded = () => null, 172 | onClick, 173 | onClose, 174 | children, 175 | style, 176 | ...otherProps 177 | }) => { 178 | return ( 179 | 180 | 181 | 182 | 191 | 192 | {renderExpanded()} 193 | 194 | 195 | 196 | ); 197 | }; 198 | -------------------------------------------------------------------------------- /src/components/Portfolio/Mint/Mint.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components"; 3 | import { ButtonQR } from "badger-components-react"; 4 | import { WalletContext } from "../../../utils/context"; 5 | import mintToken from "../../../utils/broadcastTransaction"; 6 | import { Card, Icon, Form, Input, Button, Spin, notification } from "antd"; 7 | import { Row, Col } from "antd"; 8 | import Paragraph from "antd/lib/typography/Paragraph"; 9 | import { HammerIcon } from "../../Common/CustomIcons"; 10 | import { FormItemWithQRCodeAddon } from "../EnhancedInputs"; 11 | import { getRestUrl } from "../../../utils/withSLP"; 12 | import { QRCode } from "../../Common/QRCode"; 13 | 14 | const StyledButtonWrapper = styled.div` 15 | display: flex; 16 | flex-direction: column; 17 | align-items: center; 18 | justify-content: center; 19 | 20 | ${ButtonQR} { 21 | button { 22 | display: none; 23 | } 24 | } 25 | `; 26 | 27 | const Mint = ({ token, onClose }) => { 28 | const ContextValue = React.useContext(WalletContext); 29 | const { wallet, balances } = ContextValue; 30 | const [formData, setFormData] = useState({ 31 | dirty: true, 32 | quantity: 0, 33 | baton: wallet.Path245.slpAddress 34 | }); 35 | const [loading, setLoading] = useState(false); 36 | 37 | async function submit() { 38 | setFormData({ 39 | ...formData, 40 | dirty: false 41 | }); 42 | 43 | if (!formData.baton || !formData.quantity || Number(formData.quantity) <= 0) { 44 | return; 45 | } 46 | 47 | setLoading(true); 48 | const { quantity, baton } = formData; 49 | 50 | try { 51 | const link = await mintToken(wallet, { 52 | tokenId: token.tokenId, 53 | version: token.version, 54 | additionalTokenQty: quantity, 55 | batonReceiverAddress: baton 56 | }); 57 | 58 | notification.success({ 59 | message: "Success", 60 | description: ( 61 | 62 | Transaction successful. Click or tap here for more details 63 | 64 | ), 65 | duration: 2 66 | }); 67 | 68 | onClose(); 69 | setLoading(false); 70 | } catch (e) { 71 | let message; 72 | 73 | if (/don't have the minting baton/.test(e.message)) { 74 | message = e.message; 75 | } else if (/Invalid BCH address/.test(e.message)) { 76 | message = "Invalid BCH address"; 77 | } else if (/Transaction input BCH amount is too low/.test(e.message)) { 78 | message = "Not enough BCH. Deposit some funds to use this feature."; 79 | } else if (/NFT token types are not yet supported/.test(e.message)) { 80 | message = e.message; 81 | } else if (/is not supported/.test(e.message)) { 82 | message = e.message; 83 | } else if (!e.error) { 84 | message = `Transaction failed: no response from ${getRestUrl()}.`; 85 | } else if (/Could not communicate with full node or other external service/.test(e.error)) { 86 | message = "Could not communicate with API. Please try again."; 87 | } else { 88 | message = e.message || e.error || JSON.stringify(e); 89 | } 90 | 91 | notification.error({ 92 | message: "Error", 93 | description: message, 94 | duration: 2 95 | }); 96 | console.error(e); 97 | setLoading(false); 98 | } 99 | } 100 | 101 | const handleChange = e => { 102 | const { value, name } = e.target; 103 | 104 | setFormData(p => ({ ...p, [name]: value })); 105 | }; 106 | 107 | return ( 108 | 109 | 110 | 111 | 114 | Mint 115 | 116 | } 117 | bordered={false} 118 | > 119 |
120 | {!balances.totalBalance ? ( 121 | 122 | 123 |
124 | 125 | <> 126 | 127 | You currently have 0 BCH. Deposit some funds to use this feature. 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 |
136 | ) : ( 137 | 138 | 139 |
140 | setFormData({ ...formData, address: result })} 146 | inputProps={{ 147 | placeholder: "Baton (slp address)", 148 | name: "baton", 149 | onChange: e => handleChange(e), 150 | required: true, 151 | value: formData.baton 152 | }} 153 | /> 154 | 164 | } 166 | placeholder="Amount" 167 | name="quantity" 168 | onChange={e => handleChange(e)} 169 | required 170 | type="number" 171 | /> 172 | 173 |
174 | 175 |
176 | 177 | 178 |
179 | )} 180 |
181 |
182 | 183 |
184 | ); 185 | }; 186 | 187 | export default Mint; 188 | -------------------------------------------------------------------------------- /src/components/Portfolio/MoreCardOptions.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Popover } from "antd"; 3 | 4 | export default ({ children, hoverContent, clickContent }) => { 5 | const [hovered, setHovered] = useState(false); 6 | const [clicked, setClicked] = useState(false); 7 | 8 | const hide = () => { 9 | setHovered(false); 10 | setClicked(false); 11 | }; 12 | 13 | const handleHoverChange = visible => { 14 | setHovered(visible); 15 | setClicked(false); 16 | }; 17 | 18 | const handleClickChange = visible => { 19 | setHovered(visible); 20 | setClicked(visible); 21 | }; 22 | 23 | return ( 24 | 32 | 35 | {clickContent} 36 | 37 | Close 38 | 39 |
40 | } 41 | trigger="click" 42 | visible={clicked} 43 | onVisibleChange={handleClickChange} 44 | > 45 | {children} 46 | 47 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/components/Portfolio/PayDividends/AdvancedOptions.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import styled from "styled-components"; 3 | import { debounce } from "lodash"; 4 | import { StyledCollapse } from "../../Common/StyledCollapse"; 5 | import { 6 | Collapse, 7 | Row, 8 | Col, 9 | Checkbox, 10 | List, 11 | Badge, 12 | Icon, 13 | Button, 14 | Form, 15 | Input, 16 | Tooltip 17 | } from "antd"; 18 | import { FormItemWithQRCodeAddon, AddressValidators } from "../EnhancedInputs"; 19 | 20 | const StyledButton = styled(Button)` 21 | margin-top: 12px; 22 | `; 23 | 24 | const StyledAdvancedOptions = styled.div` 25 | .ant-badge { 26 | width: 100%; 27 | cursor: pointer; 28 | } 29 | 30 | .ant-form-item { 31 | width: 100%; 32 | margin-bottom: 0; 33 | } 34 | 35 | .ant-divider { 36 | font-size: 14px !important; 37 | margin: 0 !important; 38 | } 39 | 40 | .ant-list-item { 41 | border-bottom: none !important; 42 | } 43 | `; 44 | 45 | export const AdvancedOptions = ({ advancedOptions, setAdvancedOptions, disabled }) => { 46 | const setOpReturnMessage = React.useCallback( 47 | debounce(opReturnMessage => setAdvancedOptions({ ...advancedOptions, opReturnMessage })) 48 | ); 49 | 50 | const updateAddressesToExclude = (address, index) => { 51 | const addresses = [...advancedOptions.addressesToExclude]; 52 | const singleUpdate = (address || "").indexOf(",") === -1; 53 | if (singleUpdate) { 54 | addresses[index] = { 55 | address, 56 | valid: address 57 | ? AddressValidators.isSLPAddress(address) || 58 | AddressValidators.isBCHAddress(address) || 59 | AddressValidators.isLegacyAddress(address) 60 | : null 61 | }; 62 | } else { 63 | const newAddresses = address.split(",").map(addr => ({ 64 | address: addr, 65 | valid: addr 66 | ? AddressValidators.isSLPAddress(addr) || 67 | AddressValidators.isBCHAddress(addr) || 68 | AddressValidators.isLegacyAddress(addr) 69 | : null 70 | })); 71 | addresses.splice(index, 1, ...newAddresses); 72 | } 73 | setAdvancedOptions({ ...advancedOptions, addressesToExclude: addresses }); 74 | }; 75 | 76 | const addAddress = () => { 77 | setAdvancedOptions({ 78 | ...advancedOptions, 79 | addressesToExclude: [ 80 | ...advancedOptions.addressesToExclude, 81 | { 82 | address: "", 83 | valid: null 84 | } 85 | ] 86 | }); 87 | }; 88 | 89 | const removeAddress = index => { 90 | advancedOptions.addressesToExclude.splice(index, 1); 91 | setAdvancedOptions({ 92 | ...advancedOptions, 93 | addressesToExclude: [...advancedOptions.addressesToExclude] 94 | }); 95 | }; 96 | 97 | const opReturnMessageError = 98 | advancedOptions && 99 | advancedOptions.opReturnMessage && 100 | advancedOptions.opReturnMessage.length > 60 101 | ? "OP_RETURN messages on dividend payments with this tool are currently limited to 60 characters." 102 | : ""; 103 | 104 | return ( 105 | 106 | 107 | 108 |
109 | 110 | 111 | 117 | OP_RETURN message 118 | 119 | } 120 | > 121 | setOpReturnMessage(e.target.value)} 124 | /> 125 | 126 | 127 | 128 |
129 | 130 | 131 | 135 | Addresses to exclude 136 | 137 | } 138 | > 139 | 141 | setAdvancedOptions({ 142 | ...advancedOptions, 143 | ignoreOwnAddress: !advancedOptions.ignoreOwnAddress 144 | }) 145 | } 146 | checked={advancedOptions.ignoreOwnAddress} 147 | > 148 | Ignore own address 149 | 150 | 151 | ( 154 | 155 | 0 ? ( 158 | removeAddress(index)} 162 | /> 163 | ) : null 164 | } 165 | > 166 | updateAddressesToExclude(result, index)} 174 | inputProps={{ 175 | placeholder: "BCH or SLP address", 176 | onChange: e => updateAddressesToExclude(e.target.value, index), 177 | required: true, 178 | value: advancedOptions.addressesToExclude[index] 179 | ? advancedOptions.addressesToExclude[index].address 180 | : "" 181 | }} 182 | /> 183 | 184 | 185 | )} 186 | /> 187 | 188 | 189 | Add Address 190 | 191 | 192 | 193 |
194 |
195 |
196 |
197 | ); 198 | }; 199 | -------------------------------------------------------------------------------- /src/components/Portfolio/PayDividends/useDividendsStats.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | 3 | import * as React from "react"; 4 | import { message } from "antd"; 5 | import { getBalancesForToken, getEligibleAddresses } from "../../../utils/sendDividends"; 6 | import retry from "../../../utils/retry"; 7 | import { WalletContext } from "../../../utils/context"; 8 | 9 | export const useDividendsStats = ({ token, amount, setLoading, advancedOptions, disabled }) => { 10 | const { wallet, balances, slpBalancesAndUtxos } = React.useContext(WalletContext); 11 | const [stats, setStats] = React.useState({ 12 | tokens: 0, 13 | holders: 0, 14 | balances: null, 15 | eligibles: 0, 16 | txFee: 0, 17 | maxAmount: 0 18 | }); 19 | 20 | React.useEffect(() => { 21 | if (disabled === true) 22 | setStats({ tokens: 0, holders: 0, balances: null, eligibles: 0, txFee: 0, maxAmount: 0 }); 23 | }, [disabled]); 24 | 25 | React.useEffect(() => { 26 | if (!disabled) { 27 | if (!token) { 28 | return; 29 | } 30 | setLoading(true); 31 | retry(() => getBalancesForToken(token.tokenId)) 32 | .then(balancesForToken => { 33 | setStats({ 34 | ...stats, 35 | tokens: balancesForToken.totalBalance, 36 | holders: balancesForToken.length ? balancesForToken.length : 0, 37 | balances: balancesForToken, 38 | txFee: 0 39 | }); 40 | }) 41 | .catch(() => null) 42 | .finally(() => setLoading(false)); 43 | } 44 | }, [token, disabled]); 45 | 46 | // max amount 47 | React.useEffect(() => { 48 | if (!disabled) { 49 | if (!stats.balances || !balances.totalBalance || !slpBalancesAndUtxos || !token) { 50 | return; 51 | } 52 | 53 | try { 54 | const { txFee } = getEligibleAddresses( 55 | wallet, 56 | stats.balances, 57 | balances.totalBalance, 58 | slpBalancesAndUtxos.nonSlpUtxos, 59 | advancedOptions, 60 | token.tokenId 61 | ); 62 | const maxAmount = (balances.totalBalance - txFee).toFixed(8); 63 | setStats(stats => ({ ...stats, maxAmount })); 64 | } catch (error) {} 65 | } 66 | }, [wallet, balances, stats.balances, slpBalancesAndUtxos, token, advancedOptions, disabled]); 67 | 68 | // eligible addresses to the amount 69 | React.useEffect(() => { 70 | if (!disabled) { 71 | if (!token) { 72 | return; 73 | } 74 | 75 | try { 76 | if (!Number.isNaN(Number(amount)) && amount > 0) { 77 | const { addresses, txFee } = getEligibleAddresses( 78 | wallet, 79 | stats.balances, 80 | amount, 81 | slpBalancesAndUtxos.nonSlpUtxos, 82 | advancedOptions, 83 | token.tokenId 84 | ); 85 | 86 | setStats(stats => ({ ...stats, eligibles: addresses.length, txFee })); 87 | } else { 88 | setStats(stats => ({ ...stats, eligibles: 0, txFee: 0 })); 89 | } 90 | } catch (error) { 91 | console.error(error); 92 | message.error("Unable to calculate eligible addresses due to network errors"); 93 | } 94 | } 95 | }, [ 96 | wallet, 97 | balances, 98 | stats.balances, 99 | slpBalancesAndUtxos, 100 | advancedOptions, 101 | token, 102 | amount, 103 | disabled 104 | ]); 105 | 106 | return { 107 | stats, 108 | setLoading 109 | }; 110 | }; 111 | -------------------------------------------------------------------------------- /src/components/Portfolio/PayDividendsOption.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Icon } from "antd"; 3 | 4 | export default ({ onClick }) => { 5 | return ( 6 |
7 | Pay Dividends 8 |
9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/components/Portfolio/ScanQRCode.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Tooltip, Icon, Modal, message } from "antd"; 3 | import styled from "styled-components"; 4 | import QrReader from "react-qr-reader"; 5 | 6 | const StyledScanQRCode = styled.span` 7 | display: block; 8 | `; 9 | 10 | const StyledModal = styled(Modal)` 11 | width: 400px !important; 12 | height: 400px !important; 13 | 14 | .ant-modal-close { 15 | top: 0 !important; 16 | right: 0 !important; 17 | } 18 | `; 19 | 20 | const StyledQrReader = styled(QrReader)` 21 | width: 100%; 22 | height: 100%; 23 | 24 | img { 25 | background: black; 26 | } 27 | `; 28 | 29 | export const ScanQRCode = ({ width, onScan = () => null, ...otherProps }) => { 30 | const [visible, setVisible] = React.useState(false); 31 | const [error, setError] = React.useState(false); 32 | const ref = React.useRef(); 33 | 34 | React.useEffect(() => { 35 | if (!visible) { 36 | setError(false); 37 | } 38 | }, [visible]); 39 | 40 | return ( 41 | <> 42 | 43 | setVisible(!visible)}> 44 | 45 | 46 | 47 | setVisible(false)} 51 | footer={null} 52 | > 53 | (ref.current ? ref.current.openImageDialog() : null)}> 56 | You need to allow camera access to use this feature, otherwise click here to manually 57 | choose a picture. 58 | 59 | } 60 | visible={visible && error} 61 | placement="bottom" 62 | > 63 | {visible ? ( 64 |
(error && ref.current ? ref.current.openImageDialog() : null)}> 65 | { 70 | setTimeout(() => setError(true), 500); 71 | }} 72 | legacyMode={!!error} 73 | onScan={result => { 74 | if (result) { 75 | setVisible(false); 76 | onScan(result); 77 | } else if (error) { 78 | message.error("No QR Code found, please try another image."); 79 | } 80 | }} 81 | /> 82 |
83 | ) : null} 84 |
85 |
86 | 87 | ); 88 | }; 89 | -------------------------------------------------------------------------------- /src/components/Portfolio/Transfer/Transfer.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { WalletContext } from "../../../utils/context"; 3 | 4 | import { Card, Icon, Form, Button, Spin, notification } from "antd"; 5 | import { Row, Col } from "antd"; 6 | import Paragraph from "antd/lib/typography/Paragraph"; 7 | import sendToken from "../../../utils/broadcastTransaction"; 8 | import { PlaneIcon } from "../../Common/CustomIcons"; 9 | import { FormItemWithMaxAddon, FormItemWithQRCodeAddon } from "../EnhancedInputs"; 10 | import { getRestUrl } from "../../../utils/withSLP"; 11 | import { StyledButtonWrapper } from "../PayDividends/PayDividends"; 12 | import { QRCode } from "../../Common/QRCode"; 13 | 14 | const Transfer = ({ token, onClose }) => { 15 | const { wallet, balances } = React.useContext(WalletContext); 16 | const [formData, setFormData] = useState({ 17 | dirty: false, 18 | quantity: "", 19 | address: "" 20 | }); 21 | const [loading, setLoading] = useState(false); 22 | 23 | async function submit() { 24 | setFormData({ 25 | ...formData, 26 | dirty: false 27 | }); 28 | 29 | if (!formData.address || !formData.quantity || Number(formData.quantity) <= 0) { 30 | return; 31 | } 32 | 33 | setLoading(true); 34 | const { quantity, address } = formData; 35 | 36 | try { 37 | const link = await sendToken(wallet, { 38 | tokenId: token.tokenId, 39 | version: token.version, 40 | amount: quantity, 41 | tokenReceiverAddress: address 42 | }); 43 | 44 | notification.success({ 45 | message: "Success", 46 | description: ( 47 | 48 | Transaction successful. Click or tap here for more details 49 | 50 | ), 51 | duration: 2 52 | }); 53 | 54 | onClose(); 55 | setLoading(false); 56 | } catch (e) { 57 | let message; 58 | 59 | if (/don't have the minting baton/.test(e.message)) { 60 | message = e.message; 61 | } else if (/has no matching Script/.test(e.message)) { 62 | message = "Invalid address"; 63 | } else if (/Transaction input BCH amount is too low/.test(e.message)) { 64 | message = "Not enough BCH. Deposit some funds to use this feature."; 65 | } else if (/Token Receiver Address must be simpleledger format/.test(e.message)) { 66 | message = "Token Receiver Address must be simpleledger format."; 67 | } else if (/Invalid BCH address. Double check your address is valid/.test(e.message)) { 68 | message = "Invalid SLP address. Double check your address is valid."; 69 | } else if (/NFT token types are not yet supported/.test(e.message)) { 70 | message = e.message; 71 | } else if (/is not supported/.test(e.message)) { 72 | message = e.message; 73 | } else if (!e.error) { 74 | message = `Transaction failed: no response from ${getRestUrl()}.`; 75 | } else if (/Could not communicate with full node or other external service/.test(e.error)) { 76 | message = "Could not communicate with API. Please try again."; 77 | } else { 78 | message = e.message || e.error || JSON.stringify(e); 79 | } 80 | 81 | notification.error({ 82 | message: "Error", 83 | description: message, 84 | duration: 2 85 | }); 86 | console.error(e); 87 | setLoading(false); 88 | } 89 | } 90 | 91 | const handleChange = e => { 92 | const { value, name } = e.target; 93 | 94 | setFormData(p => ({ ...p, [name]: value })); 95 | }; 96 | 97 | const onMax = () => { 98 | setFormData({ ...formData, quantity: token.balance || 0 }); 99 | }; 100 | 101 | return ( 102 | 103 | 104 | 105 | 108 | Send 109 | 110 | } 111 | bordered={false} 112 | > 113 |
114 | {!balances.totalBalance ? ( 115 | 116 | 117 |
118 | 119 | <> 120 | 121 | You currently have 0 BCH. Deposit some funds to use this feature. 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 |
130 | ) : ( 131 | 132 | 133 |
134 | setFormData({ ...formData, address: result })} 140 | inputProps={{ 141 | placeholder: "SLP Address", 142 | name: "address", 143 | onChange: e => handleChange(e), 144 | required: true, 145 | value: formData.address 146 | }} 147 | /> 148 | 149 | , 161 | placeholder: "Amount", 162 | name: "quantity", 163 | onChange: e => handleChange(e), 164 | required: true, 165 | type: "number", 166 | value: formData.quantity 167 | }} 168 | /> 169 |
170 | 171 |
172 | 173 | 174 |
175 | )} 176 |
177 |
178 | 179 |
180 | ); 181 | }; 182 | 183 | export default Transfer; 184 | -------------------------------------------------------------------------------- /src/components/SatoshiDice/SatoshiDice.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Row, Col, Card, Icon, Typography, Select, Alert, notification } from "antd"; 3 | import styled from "styled-components"; 4 | import StyledSatoshiDice from "../Common/StyledPage"; 5 | import SendBCH from "../Portfolio/SendBCH/SendBCH"; 6 | import satoshiDice from "../../utils/satoshiDice"; 7 | import { WalletContext } from "../../utils/context"; 8 | 9 | const { Option } = Select; 10 | const { Text, Title, Paragraph } = Typography; 11 | 12 | const StyledSelect = styled(Select)` 13 | width: 200px; 14 | margin-top: 15px !important; 15 | `; 16 | 17 | export default () => { 18 | const [multiplier, setMultiplier] = useState(satoshiDice[`1.1x`]); 19 | const [betTxId, setBetTxId] = useState(false); 20 | const { balances } = React.useContext(WalletContext); 21 | 22 | React.useEffect(() => { 23 | async function checkResultBet() { 24 | if (betTxId) { 25 | console.log(`https://satoshidice.com/api/game?txid=${betTxId}`); 26 | setTimeout(() => { 27 | fetch(`https://satoshidice.com/api/game?txid=${betTxId}`).then(response => response.json()) 28 | .then(data => { 29 | if (data.payload.length > 0) { 30 | if (data.payload[0].win) { 31 | notification.success({ 32 | message: "Congratulations", 33 | description: ( 34 | 35 | You won {data.payload[0].payout} BCH! 36 | 37 | ), 38 | duration: 2 39 | }); 40 | } else { 41 | notification.error({ 42 | message: "You lost", 43 | description: ( 44 | You lost. Try again next time! 45 | 46 | ), 47 | duration: 2 48 | }) 49 | } 50 | } 51 | 52 | 53 | }) 54 | }, 3000); 55 | } 56 | } 57 | 58 | checkResultBet() 59 | }, [betTxId]) 60 | 61 | return ( 62 | 63 | 64 | 65 | 69 | Satoshi Dice 70 | 71 | } 72 | bordered={true} 73 | > 74 | A provably fair on-chain Bitcoin Cash game. 75 | 76 | Check the complete set of rules{" "} 77 | 78 | here 79 | 80 | . 81 |
82 |
83 | setMultiplier(satoshiDice[value])} 86 | > 87 | {Object.keys(satoshiDice).map(s => ( 88 | 91 | ))} 92 | 93 | 94 | 98 | 99 | Be careful. 100 | 101 | 102 | MIN: {multiplier.min} BCH - MAX:{" "} 103 | {multiplier.max} BCH 104 | 105 | 106 | } 107 | /> 108 | {multiplier && setBetTxId(txId)} filledAddress={multiplier.address} onClose={() => null} />} 109 |
110 | 111 |
112 |
113 | ); 114 | }; 115 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./components/App"; 5 | import { WalletProvider } from "./utils/context"; 6 | import { HashRouter as Router } from "react-router-dom"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | 12 | 13 | , 14 | document.getElementById("root") 15 | ); 16 | 17 | if ("serviceWorker" in navigator) { 18 | window.addEventListener("load", () => 19 | navigator.serviceWorker.register("/serviceWorker.js").catch(() => null) 20 | ); 21 | } 22 | 23 | if (module.hot) { 24 | module.hot.accept(); 25 | } 26 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | workbox.setConfig({ 2 | debug: false 3 | }); 4 | 5 | workbox.core.skipWaiting(); 6 | workbox.core.clientsClaim(); 7 | 8 | const cachedPathNames = [ 9 | "/v2/transaction/details", 10 | "/v2/rawtransactions/getRawTransaction", 11 | "/v2/slp/validateTxid" 12 | ]; 13 | 14 | workbox.routing.registerRoute( 15 | ({ url, event }) => cachedPathNames.some(cachedPathName => url.pathname.includes(cachedPathName)), 16 | async ({ event, url }) => { 17 | try { 18 | const cache = await caches.open("api-cache"); 19 | const cacheKeys = await cache.keys(); 20 | if (cacheKeys.length > 100) { 21 | await Promise.all(cacheKeys.map(key => cache.delete(key))); 22 | } 23 | const requestBody = await event.request.clone().text(); 24 | 25 | try { 26 | const response = await cache.match(`${url.pathname}/${requestBody}`); 27 | if (!response) { 28 | throw new Error("SW: Not cached!"); 29 | } 30 | return response; 31 | } catch (error) { 32 | const response = await fetch(event.request.clone()); 33 | if (response.status === 200) { 34 | const body = await response.clone().text(); 35 | cache.put(`${url.pathname}/${requestBody}`, new Response(body, { status: 200 })); 36 | } 37 | return response.clone(); 38 | } 39 | } catch (err) { 40 | return fetch(event.request.clone()); 41 | } 42 | }, 43 | "POST" 44 | ); 45 | -------------------------------------------------------------------------------- /src/utils/broadcastTransaction.js: -------------------------------------------------------------------------------- 1 | import withSLP from "./withSLP"; 2 | 3 | const broadcastTransaction = async (SLPInstance, wallet, { ...args }) => { 4 | try { 5 | // check supported token versions 6 | if (args.version) { 7 | switch (args.version) { 8 | case 0x01: 9 | break; 10 | case 0x41: 11 | case 0x81: 12 | throw new Error("NFT token types are not yet supported."); 13 | default: 14 | throw new Error(`Token type ${args.version} is not supported.`); 15 | } 16 | } 17 | 18 | const NETWORK = process.env.REACT_APP_NETWORK; 19 | 20 | const TRANSACTION_TYPE = 21 | ((args.additionalTokenQty || args.burnBaton) && args.tokenId && "IS_MINTING") || 22 | (args.initialTokenQty && args.symbol && args.name && "IS_CREATING") || 23 | (args.amount && args.tokenId && args.tokenReceiverAddress && "IS_SENDING") || 24 | (args.amount && args.tokenId && "IS_BURNING"); 25 | 26 | const { Path245, Path145 } = wallet; 27 | 28 | const config = args; 29 | config.bchChangeReceiverAddress = Path145.cashAddress; 30 | config.fundingWif = [Path245.fundingWif, Path145.fundingWif]; 31 | config.fundingAddress = [Path245.fundingAddress, Path145.fundingAddress]; 32 | 33 | let createTransaction; 34 | 35 | switch (TRANSACTION_TYPE) { 36 | case "IS_CREATING": 37 | config.batonReceiverAddress = config.fixedSupply === true ? null : Path245.slpAddress; 38 | config.decimals = config.decimals || 0; 39 | config.documentUri = config.docUri; 40 | config.tokenReceiverAddress = Path245.slpAddress; 41 | createTransaction = async config => SLPInstance.TokenType1.create(config); 42 | break; 43 | case "IS_MINTING": 44 | config.batonReceiverAddress = config.batonReceiverAddress || Path245.slpAddress; 45 | config.tokenReceiverAddress = Path245.slpAddress; 46 | createTransaction = async config => SLPInstance.TokenType1.mint(config); 47 | break; 48 | case "IS_SENDING": 49 | config.tokenReceiverAddress = args.tokenReceiverAddress; 50 | createTransaction = async config => SLPInstance.TokenType1.send(config); 51 | break; 52 | case "IS_BURNING": 53 | createTransaction = async config => SLPInstance.TokenType1.burn(config); 54 | break; 55 | default: 56 | break; 57 | } 58 | const broadcastedTransaction = await createTransaction(config); 59 | 60 | let link; 61 | if (NETWORK === `mainnet`) { 62 | link = `https://explorer.bitcoin.com/bch/tx/${broadcastedTransaction}`; 63 | } else { 64 | link = `https://explorer.bitcoin.com/tbch/tx/${broadcastedTransaction}`; 65 | } 66 | 67 | return link; 68 | } catch (err) { 69 | const message = err.message || err.error || JSON.stringify(err); 70 | console.error(`Error in createToken: `, err); 71 | console.log(`Error message: ${message}`); 72 | throw err; 73 | } 74 | }; 75 | 76 | export default withSLP(broadcastTransaction); 77 | -------------------------------------------------------------------------------- /src/utils/context.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useWallet } from "./useWallet"; 3 | export const WalletContext = React.createContext(); 4 | 5 | export const WalletProvider = ({ children }) => { 6 | return {children}; 7 | }; 8 | -------------------------------------------------------------------------------- /src/utils/createWallet.js: -------------------------------------------------------------------------------- 1 | import withSLP from "./withSLP"; 2 | import getWalletDetails from "./getWalletDetails"; 3 | 4 | let wallet; 5 | 6 | export const getWallet = () => { 7 | if (wallet) { 8 | return wallet; 9 | } 10 | 11 | try { 12 | wallet = getWalletDetails(JSON.parse(window.localStorage.getItem("wallet") || undefined)); 13 | window.localStorage.setItem("wallet", JSON.stringify(wallet)); 14 | } catch (error) {} 15 | return wallet; 16 | }; 17 | 18 | export const createWallet = withSLP((SLP, importMnemonic) => { 19 | const lang = "english"; 20 | // create 128 bit BIP39 mnemonic 21 | const Bip39128BitMnemonic = importMnemonic 22 | ? importMnemonic 23 | : SLP.Mnemonic.generate(128, SLP.Mnemonic.wordLists()[lang]); 24 | const wallet = getWalletDetails({ mnemonic: Bip39128BitMnemonic.toString() }); 25 | window.localStorage.setItem("wallet", JSON.stringify(wallet)); 26 | return wallet; 27 | }); 28 | -------------------------------------------------------------------------------- /src/utils/cropImage.js: -------------------------------------------------------------------------------- 1 | const createImage = url => 2 | new Promise((resolve, reject) => { 3 | const image = new Image(); 4 | image.addEventListener("load", () => resolve(image)); 5 | image.addEventListener("error", error => reject(error)); 6 | image.setAttribute("crossOrigin", "anonymous"); 7 | image.src = url; 8 | }); 9 | 10 | function getRadianAngle(degreeValue) { 11 | return (degreeValue * Math.PI) / 180; 12 | } 13 | 14 | export default async function getCroppedImg(imageSrc, pixelCrop, rotation = 0, fileName) { 15 | const image = await createImage(imageSrc); 16 | console.log("image :", image); 17 | const canvas = document.createElement("canvas"); 18 | const ctx = canvas.getContext("2d"); 19 | 20 | const maxSize = Math.max(image.width, image.height); 21 | const safeArea = 2 * ((maxSize / 2) * Math.sqrt(2)); 22 | 23 | canvas.width = safeArea; 24 | canvas.height = safeArea; 25 | 26 | ctx.translate(safeArea / 2, safeArea / 2); 27 | ctx.rotate(getRadianAngle(rotation)); 28 | ctx.translate(-safeArea / 2, -safeArea / 2); 29 | 30 | ctx.drawImage(image, safeArea / 2 - image.width * 0.5, safeArea / 2 - image.height * 0.5); 31 | const data = ctx.getImageData(0, 0, safeArea, safeArea); 32 | 33 | canvas.width = pixelCrop.width; 34 | canvas.height = pixelCrop.height; 35 | 36 | ctx.putImageData( 37 | data, 38 | 0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x, 39 | 0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y 40 | ); 41 | 42 | if (!HTMLCanvasElement.prototype.toBlob) { 43 | Object.defineProperty(HTMLCanvasElement.prototype, "toBlob", { 44 | value: function(callback, type, quality) { 45 | var dataURL = this.toDataURL(type, quality).split(",")[1]; 46 | setTimeout(function() { 47 | var binStr = atob(dataURL), 48 | len = binStr.length, 49 | arr = new Uint8Array(len); 50 | for (var i = 0; i < len; i++) { 51 | arr[i] = binStr.charCodeAt(i); 52 | } 53 | callback(new Blob([arr], { type: type || "image/png" })); 54 | }); 55 | } 56 | }); 57 | } 58 | return new Promise(resolve => { 59 | ctx.canvas.toBlob( 60 | blob => { 61 | const file = new File([blob], fileName, { 62 | type: "image/png" 63 | }); 64 | const resultReader = new FileReader(); 65 | 66 | resultReader.readAsDataURL(file); 67 | 68 | resultReader.addEventListener("load", () => resolve({ file, url: resultReader.result })); 69 | }, 70 | "image/png", 71 | 1 72 | ); 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /src/utils/debounce.js: -------------------------------------------------------------------------------- 1 | export default (routine, timeout = 500) => { 2 | let timeoutId; 3 | 4 | return (...args) => { 5 | return new Promise((resolve, reject) => { 6 | clearTimeout(timeoutId); 7 | timeoutId = setTimeout( 8 | () => Promise.resolve(routine(...args)).then(resolve, reject), 9 | timeout 10 | ); 11 | }); 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /src/utils/decodeRawSlpTransactions.js: -------------------------------------------------------------------------------- 1 | import chunk from "lodash/chunk"; 2 | import BigNumber from "bignumber.js"; 3 | import withSLP from "./withSLP"; 4 | 5 | export const isSlpTx = withSLP((SLP, txDetail) => { 6 | const scriptASMArray = SLP.Script.toASM( 7 | Buffer.from(txDetail.vout[0].scriptPubKey.hex, "hex") 8 | ).split(" "); 9 | if ( 10 | scriptASMArray[0] !== "OP_RETURN" || 11 | scriptASMArray[1] !== "534c5000" || 12 | (scriptASMArray[2] !== "OP_1" && 13 | scriptASMArray[2] !== "OP_1NEGATE" && 14 | scriptASMArray[2] !== "41") 15 | ) { 16 | return false; 17 | } 18 | 19 | return true; 20 | }); 21 | 22 | const revertChunk = chunkedArray => 23 | chunkedArray.reduce((unchunkedArray, chunk) => [...unchunkedArray, ...chunk], []); 24 | 25 | const decodeTokenDetails = withSLP((SLP, txDetail) => { 26 | const script = SLP.Script.toASM(Buffer.from(txDetail.vout[0].scriptPubKey.hex, "hex")).split(" "); 27 | 28 | const tokenDetails = { 29 | isSlpTxid: false, 30 | transactionType: "", 31 | info: {}, 32 | outputs: [], 33 | symbol: "" 34 | }; 35 | const isSlp = isSlpTx(txDetail); 36 | 37 | if (isSlp === true) { 38 | tokenDetails.isSlpTxid = true; 39 | tokenDetails.transactionType = Buffer.from(script[3], "hex") 40 | .toString("ascii") 41 | .toUpperCase(); 42 | 43 | if (tokenDetails.transactionType === "GENESIS") { 44 | const decimals = script[8].startsWith("OP_") 45 | ? parseInt(script[8].slice(3), 10) 46 | : parseInt(script[8], 16); 47 | tokenDetails.info = { 48 | tokenId: txDetail.txid, 49 | symbol: Buffer.from(script[4], "hex").toString("ascii"), 50 | name: Buffer.from(script[5], "hex").toString("ascii"), 51 | decimals, 52 | documentUri: Buffer.from(script[6], "hex").toString("ascii"), 53 | documentHash: script[7].startsWith("OP_0") ? "" : script[7] 54 | }; 55 | tokenDetails.symbol = Buffer.from(script[4], "hex").toString("ascii"); 56 | tokenDetails.outputs = [ 57 | { 58 | address: SLP.Address.toSLPAddress(txDetail.vout[1].scriptPubKey.addresses[0]), 59 | amount: new BigNumber(script[10], 16).div(Math.pow(10, decimals)) 60 | } 61 | ]; 62 | } else if (tokenDetails.transactionType === "MINT") { 63 | tokenDetails.info = { 64 | tokenId: script[4] 65 | }; 66 | tokenDetails.outputs = [ 67 | { 68 | address: SLP.Address.toSLPAddress(txDetail.vout[1].scriptPubKey.addresses[0]), 69 | rawAmount: new BigNumber(script[6], 16) 70 | } 71 | ]; 72 | } else if (tokenDetails.transactionType === "SEND") { 73 | tokenDetails.info = { 74 | tokenId: script[4] 75 | }; 76 | tokenDetails.outputs = script.slice(5, script.length).map((rawBalance, index) => ({ 77 | address: SLP.Address.toSLPAddress(txDetail.vout[index + 1].scriptPubKey.addresses[0]), 78 | rawAmount: new BigNumber(rawBalance, 16) 79 | })); 80 | } 81 | return tokenDetails; 82 | } else { 83 | return false; 84 | } 85 | }); 86 | 87 | const handleTxs = withSLP(async (SLP, txidDetails, tokenInfo) => { 88 | const slpTxidDetails = txidDetails 89 | .map(txDetail => ({ 90 | ...txDetail, 91 | tokenDetails: decodeTokenDetails(txDetail) 92 | })) 93 | .filter(detail => detail.tokenDetails !== false); 94 | 95 | if (slpTxidDetails.lenght === 0) return []; 96 | if (tokenInfo === null || (tokenInfo || {}).tokenId === undefined) { 97 | const tokenIdChunks = chunk( 98 | [...new Set(slpTxidDetails.map(detail => detail.tokenDetails.info.tokenId))], 99 | 20 100 | ); 101 | const tokensInfo = revertChunk( 102 | await Promise.all(tokenIdChunks.map(tokenIdChunk => SLP.Utils.tokenStats(tokenIdChunk))) 103 | ); 104 | 105 | return slpTxidDetails.map(detail => { 106 | const tokenInfo = tokensInfo.find(info => info.id === detail.tokenDetails.info.tokenId); 107 | if (detail.tokenDetails.transactionType !== "GENESIS") { 108 | const { decimals, symbol } = tokenInfo; 109 | return { 110 | ...detail, 111 | tokenDetails: { 112 | ...detail.tokenDetails, 113 | symbol, 114 | info: { ...detail.tokenDetails.info, ...tokenInfo }, 115 | outputs: detail.tokenDetails.outputs.map(output => ({ 116 | ...output, 117 | amount: output.rawAmount.div(Math.pow(10, decimals)) 118 | })) 119 | } 120 | }; 121 | } 122 | return detail; 123 | }); 124 | } else { 125 | const decodedTxs = slpTxidDetails 126 | .filter(detail => detail.tokenDetails.info.tokenId === tokenInfo.tokenId) 127 | .map(tokenTxDetail => { 128 | if (tokenTxDetail.tokenDetails.transactionType !== "GENESIS") { 129 | const { decimals, symbol } = tokenInfo; 130 | return { 131 | ...tokenTxDetail, 132 | tokenDetails: { 133 | ...tokenTxDetail.tokenDetails, 134 | symbol, 135 | info: { ...tokenTxDetail.tokenDetails.info, ...tokenInfo }, 136 | outputs: tokenTxDetail.tokenDetails.outputs.map(output => ({ 137 | ...output, 138 | amount: output.rawAmount.div(Math.pow(10, decimals)) 139 | })) 140 | } 141 | }; 142 | } 143 | return tokenTxDetail; 144 | }); 145 | return decodedTxs; 146 | } 147 | }); 148 | export const decodeRawSlpTrasactionsByTxids = withSLP(async (SLP, txids, tokenInfo = null) => { 149 | const txidChunks = chunk(txids, 20); 150 | const txidDetails = revertChunk( 151 | await Promise.all(txidChunks.map(txidChunk => SLP.Transaction.details(txidChunk))) 152 | ); 153 | return handleTxs(txidDetails, tokenInfo); 154 | }); 155 | 156 | export const decodeRawSlpTrasactionsByTxs = async (txs, tokenInfo = null) => 157 | await handleTxs(txs, tokenInfo); 158 | -------------------------------------------------------------------------------- /src/utils/dividends/dividends-manager.js: -------------------------------------------------------------------------------- 1 | import DividendsPayment from "./dividends"; 2 | import { sendBch, SEND_BCH_ERRORS } from "../sendBch"; 3 | import Dividends from "./dividends"; 4 | import { getEncodedOpReturnMessage } from "../sendDividends"; 5 | 6 | export default class DividendsManager { 7 | static async update({ wallet, utxos }) { 8 | try { 9 | const dividends = Object.values(DividendsPayment.getAll()); 10 | const dividend = dividends.find( 11 | dividend => dividend.progress < 1 && dividend.status === Dividends.Status.IN_PROGRESS 12 | ); 13 | if (dividend && utxos) { 14 | await DividendsManager._update({ wallet, dividend, utxos }); 15 | } 16 | } catch (error) { 17 | console.info("Unable to update dividends", error.message); 18 | } 19 | } 20 | 21 | static async _update({ wallet, dividend, utxos }) { 22 | try { 23 | const addresses = dividend.remainingRecipients.slice(0, Dividends.BATCH_SIZE); 24 | const values = dividend.remainingValues.slice(0, Dividends.BATCH_SIZE); 25 | const { encodedOpReturn } = getEncodedOpReturnMessage( 26 | dividend.opReturn, 27 | dividend.token.tokenId 28 | ); 29 | 30 | const link = await sendBch(wallet, utxos, { 31 | addresses, 32 | values, 33 | encodedOpReturn 34 | }); 35 | const tx = link.match(/([^/]+)$/)[1]; 36 | dividend.txs.push(tx); 37 | dividend.remainingRecipients = dividend.remainingRecipients.slice(Dividends.BATCH_SIZE); 38 | dividend.remainingValues = dividend.remainingValues.slice(Dividends.BATCH_SIZE); 39 | dividend.progress = 1 - dividend.remainingRecipients.length / dividend.totalRecipients; 40 | if (dividend.remainingValues.length === 0) { 41 | dividend.endDate = Date.now(); 42 | } 43 | Dividends.save(dividend); 44 | } catch (error) { 45 | if ( 46 | error.code && 47 | (error.code === SEND_BCH_ERRORS.DOUBLE_SPENDING || 48 | error.code === SEND_BCH_ERRORS.NETWORK_ERROR) 49 | ) { 50 | return; 51 | } 52 | 53 | dividend.error = error.error || error.message; 54 | dividend.status = Dividends.Status.CRASHED; 55 | Dividends.save(dividend); 56 | console.info("Unable to update dividend", error.message); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/utils/dividends/dividends.js: -------------------------------------------------------------------------------- 1 | export default class Dividends { 2 | static BATCH_SIZE = 2500; 3 | 4 | static Status = { 5 | IN_PROGRESS: 0, 6 | PAUSED: 1, 7 | CANCELED: 2, 8 | CRASHED: 3 9 | }; 10 | 11 | constructor({ token, recipients, totalValue, values, opReturn }) { 12 | this.progress = 0; 13 | this.status = Dividends.Status.IN_PROGRESS; 14 | this.token = token; 15 | this.startDate = Date.now(); 16 | this.endDate = null; 17 | this.txs = []; 18 | this.totalRecipients = recipients.length; 19 | this.remainingRecipients = recipients; 20 | this.remainingValues = values; 21 | this.opReturn = opReturn; 22 | this.totalValue = totalValue; 23 | this.error = ""; 24 | } 25 | 26 | static getAll = () => 27 | window.localStorage.getItem("dividends") 28 | ? JSON.parse(window.localStorage.getItem("dividends")) 29 | : {}; 30 | 31 | static save = dividend => { 32 | try { 33 | const storedDividends = Dividends.getAll(); 34 | window.localStorage.setItem( 35 | "dividends", 36 | JSON.stringify({ 37 | ...storedDividends, 38 | [dividend.startDate]: { 39 | ...storedDividends[dividend.startDate], 40 | ...dividend 41 | } 42 | }) 43 | ); 44 | } catch (error) { 45 | console.log("Unable to save setDividends due to: ", error.message); 46 | } 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/getSlpBanlancesAndUtxos.js: -------------------------------------------------------------------------------- 1 | import chunk from "lodash/chunk"; 2 | import BigNumber from "bignumber.js"; 3 | import withSLP from "./withSLP"; 4 | 5 | const getSLPTxType = scriptASMArray => { 6 | if (scriptASMArray[0] !== "OP_RETURN") { 7 | throw new Error("Not an OP_RETURN"); 8 | } 9 | 10 | if (scriptASMArray[1] !== "534c5000") { 11 | throw new Error("Not a SLP OP_RETURN"); 12 | } 13 | 14 | // any token version listed here will display in the portfolio 15 | if ( 16 | scriptASMArray[2] !== "OP_1" && // 0x01 = Fungible token 17 | scriptASMArray[2] !== "OP_1NEGATE" && // 0x81 = NFT Group 18 | scriptASMArray[2] !== "41" // 0x41 = NFT Token 19 | ) { 20 | // NOTE: bitcoincashlib-js converts hex 01 to OP_1 due to BIP62.3 enforcement 21 | throw new Error("Unknown token type"); 22 | } 23 | 24 | var type = Buffer.from(scriptASMArray[3], "hex") 25 | .toString("ascii") 26 | .toLowerCase(); 27 | 28 | // this converts the ASM representation of the version field to a number 29 | var version = scriptASMArray[2] === "OP_1" ? 0x01 : scriptASMArray[2] === "41" ? 0x41 : 0x81; 30 | 31 | return { txType: type, version }; 32 | }; 33 | 34 | const decodeTxOut = withSLP((SLP, txOut) => { 35 | const out = { 36 | tokenId: "", 37 | balance: new BigNumber(0, 16), 38 | hasBaton: false, 39 | version: 0 40 | }; 41 | 42 | const vout = parseInt(txOut.vout, 10); 43 | 44 | const script = SLP.Script.toASM(Buffer.from(txOut.tx.vout[0].scriptPubKey.hex, "hex")).split(" "); 45 | const type = getSLPTxType(script); 46 | out.version = type.version; 47 | 48 | if (type.txType === "genesis") { 49 | if (typeof script[9] === "string" && script[9].startsWith("OP_")) { 50 | script[9] = parseInt(script[9].slice(3), 10).toString(16); 51 | } 52 | if ((script[9] === "OP_2" && vout === 2) || parseInt(script[9], 16) === vout) { 53 | out.tokenId = txOut.txid; 54 | out.hasBaton = true; 55 | return out; 56 | } 57 | if (vout !== 1) { 58 | throw new Error("Not a SLP txout"); 59 | } 60 | out.tokenId = txOut.txid; 61 | out.balance = new BigNumber(script[10], 16); 62 | } else if (type.txType === "mint") { 63 | if (typeof script[5] === "string" && script[5].startsWith("OP_")) { 64 | script[5] = parseInt(script[5].slice(3), 10).toString(16); 65 | } 66 | if ((script[5] === "OP_2" && vout === 2) || parseInt(script[5], 16) === vout) { 67 | out.tokenId = script[4]; 68 | out.hasBaton = true; 69 | return out; 70 | } 71 | 72 | if (txOut.vout !== 1) { 73 | throw new Error("Not a SLP txout"); 74 | } 75 | out.tokenId = script[4]; 76 | 77 | if (typeof script[6] === "string" && script[6].startsWith("OP_")) { 78 | script[6] = parseInt(script[6].slice(3), 10).toString(16); 79 | } 80 | out.balance = new BigNumber(script[6], 16); 81 | } else if (type.txType === "send") { 82 | if (script.length <= vout + 4) { 83 | throw new Error("Not a SLP txout"); 84 | } 85 | 86 | out.tokenId = script[4]; 87 | 88 | if (typeof script[vout + 4] === "string" && script[vout + 4].startsWith("OP_")) { 89 | script[vout + 4] = parseInt(script[vout + 4].slice(3), 10).toString(16); 90 | } 91 | out.balance = new BigNumber(script[vout + 4], 16); 92 | } else { 93 | throw new Error("Invalid tx type"); 94 | } 95 | 96 | return out; 97 | }); 98 | 99 | const decodeTokenMetadata = withSLP((SLP, txDetails) => { 100 | const script = SLP.Script.toASM(Buffer.from(txDetails.vout[0].scriptPubKey.hex, "hex")).split( 101 | " " 102 | ); 103 | 104 | const type = getSLPTxType(script); 105 | 106 | if (type.txType === "genesis") { 107 | return { 108 | tokenId: txDetails.txid, 109 | symbol: Buffer.from(script[4], "hex").toString("ascii"), 110 | name: Buffer.from(script[5], "hex").toString("ascii"), 111 | decimals: script[8].startsWith("OP_") 112 | ? parseInt(script[8].slice(3), 10) 113 | : parseInt(script[8], 16), 114 | documentUri: Buffer.from(script[6], "hex").toString("ascii"), 115 | documentHash: script[7].startsWith("OP_0") ? "" : script[7] 116 | }; 117 | } else { 118 | throw new Error("Invalid tx type"); 119 | } 120 | }); 121 | 122 | const revertChunk = chunkedArray => 123 | chunkedArray.reduce((unchunkedArray, chunk) => [...unchunkedArray, ...chunk], []); 124 | 125 | export default withSLP(async (SLP, addresses) => { 126 | const utxosResponse = await SLP.Address.utxo(addresses); 127 | const utxos = revertChunk( 128 | utxosResponse.map((utxosRespons, i) => 129 | utxosRespons.utxos.map(utxo => ({ ...utxo, address: addresses[i] })) 130 | ) 131 | ); 132 | const utxoChunks = chunk(utxos, 20); 133 | const utxoDetails = revertChunk( 134 | await Promise.all( 135 | utxoChunks.map(utxosChunk => SLP.Transaction.details(utxosChunk.map(utxo => utxo.txid))) 136 | ) 137 | ); 138 | 139 | let tokensByTxId = {}; 140 | utxos.forEach((utxo, i) => { 141 | utxo.tx = utxoDetails[i]; 142 | try { 143 | utxo.slpData = decodeTxOut(utxo); 144 | let token = tokensByTxId[utxo.slpData.tokenId]; 145 | if (token) { 146 | token.balance = token.balance.plus(utxo.slpData.balance); 147 | token.hasBaton = token.hasBaton || utxo.slpData.hasBaton; 148 | } else { 149 | token = utxo.slpData; 150 | tokensByTxId[utxo.slpData.tokenId] = token; 151 | } 152 | } catch (error) {} 153 | }); 154 | 155 | let tokens = Object.values(tokensByTxId); 156 | const tokenIdsChunks = chunk( 157 | tokens.map(token => token.tokenId), 158 | 20 159 | ); 160 | const tokenTxDetails = revertChunk( 161 | await Promise.all(tokenIdsChunks.map(tokenIdsChunk => SLP.Transaction.details(tokenIdsChunk))) 162 | ); 163 | 164 | tokens = tokens 165 | .filter((token, i) => { 166 | const tx = tokenTxDetails[i]; 167 | try { 168 | token.info = decodeTokenMetadata(tx); 169 | token.balance = token.balance.div(Math.pow(10, token.info.decimals)); 170 | return true; 171 | } catch (error) {} 172 | return false; 173 | }) 174 | .sort((t1, t2) => t1.info.name.localeCompare(t2.info.name)); 175 | 176 | const nonSlpUtxos = utxos.filter(utxo => !utxo.slpData && utxo.satoshis !== 546); 177 | const slpUtxos = utxos.filter(utxo => !!utxo.slpData); 178 | 179 | return { tokens, nonSlpUtxos, slpUtxos }; 180 | }); 181 | -------------------------------------------------------------------------------- /src/utils/getWalletDetails.js: -------------------------------------------------------------------------------- 1 | import withSLP from "./withSLP"; 2 | 3 | const deriveAccount = withSLP((SLPInstance, { masterHDNode, path }) => { 4 | const node = SLPInstance.HDNode.derivePath(masterHDNode, path); 5 | const cashAddress = SLPInstance.HDNode.toCashAddress(node); 6 | const slpAddress = SLPInstance.Address.toSLPAddress(cashAddress); 7 | 8 | return { 9 | cashAddress, 10 | slpAddress, 11 | fundingWif: SLPInstance.HDNode.toWIF(node), 12 | fundingAddress: SLPInstance.Address.toSLPAddress(cashAddress), 13 | legacyAddress: SLPInstance.Address.toLegacyAddress(cashAddress) 14 | }; 15 | }); 16 | 17 | const getWalletDetails = (SLPInstance, wallet) => { 18 | const NETWORK = process.env.REACT_APP_NETWORK; 19 | const mnemonic = wallet.mnemonic; 20 | const rootSeedBuffer = SLPInstance.Mnemonic.toSeed(mnemonic); 21 | let masterHDNode; 22 | 23 | if (NETWORK === `mainnet`) masterHDNode = SLPInstance.HDNode.fromSeed(rootSeedBuffer); 24 | else masterHDNode = SLPInstance.HDNode.fromSeed(rootSeedBuffer, "testnet"); 25 | 26 | const Path245 = deriveAccount({ masterHDNode, path: "m/44'/245'/0'/0/0" }); 27 | const Path145 = deriveAccount({ masterHDNode, path: "m/44'/145'/0'/0/0" }); 28 | const PathZero = deriveAccount({ masterHDNode, path: "m/44'/0'/0'/0/0" }); 29 | const Accounts = [Path245, Path145]; 30 | 31 | return { 32 | mnemonic: wallet.mnemonic, 33 | cashAddresses: [Path245.cashAddress, Path145.cashAddress], 34 | slpAddresses: [Path245.slpAddress, Path145.slpAddress], 35 | 36 | Path245, 37 | Path145, 38 | PathZero, 39 | Accounts 40 | }; 41 | }; 42 | 43 | export default withSLP(getWalletDetails); 44 | -------------------------------------------------------------------------------- /src/utils/isPiticoTokenHolder.js: -------------------------------------------------------------------------------- 1 | export default () => true; 2 | -------------------------------------------------------------------------------- /src/utils/resizeImage.js: -------------------------------------------------------------------------------- 1 | const createImage = url => 2 | new Promise((resolve, reject) => { 3 | const image = new Image(); 4 | image.addEventListener("load", () => resolve(image)); 5 | image.addEventListener("error", error => reject(error)); 6 | image.setAttribute("crossOrigin", "anonymous"); 7 | image.src = url; 8 | }); 9 | 10 | export default async function getResizedImg(imageSrc, callback, fileName) { 11 | const image = await createImage(imageSrc); 12 | 13 | const width = 128; 14 | const height = 128; 15 | const canvas = document.createElement("canvas"); 16 | canvas.width = width; 17 | canvas.height = height; 18 | const ctx = canvas.getContext("2d"); 19 | 20 | ctx.drawImage(image, 0, 0, width, height); 21 | if (!HTMLCanvasElement.prototype.toBlob) { 22 | Object.defineProperty(HTMLCanvasElement.prototype, "toBlob", { 23 | value: function(callback, type, quality) { 24 | var dataURL = this.toDataURL(type, quality).split(",")[1]; 25 | setTimeout(function() { 26 | var binStr = atob(dataURL), 27 | len = binStr.length, 28 | arr = new Uint8Array(len); 29 | for (var i = 0; i < len; i++) { 30 | arr[i] = binStr.charCodeAt(i); 31 | } 32 | callback(new Blob([arr], { type: type || "image/png" })); 33 | }); 34 | } 35 | }); 36 | } 37 | 38 | return new Promise(resolve => { 39 | ctx.canvas.toBlob( 40 | blob => { 41 | const file = new File([blob], fileName, { 42 | type: "image/png" 43 | }); 44 | const resultReader = new FileReader(); 45 | 46 | resultReader.readAsDataURL(file); 47 | 48 | resultReader.addEventListener("load", () => callback({ file, url: resultReader.result })); 49 | resolve(); 50 | }, 51 | "image/png", 52 | 1 53 | ); 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/retry.js: -------------------------------------------------------------------------------- 1 | export const retry = async (func, { delay = 100, tries = 3 } = {}) => { 2 | try { 3 | return await Promise.resolve(func()); 4 | } catch (error) { 5 | if (tries === 0) { 6 | throw error; 7 | } 8 | return await retry(func, { delay: delay * 2, tries: tries - 1 }); 9 | } 10 | }; 11 | 12 | export default retry; 13 | -------------------------------------------------------------------------------- /src/utils/roundImage.js: -------------------------------------------------------------------------------- 1 | const createImage = url => 2 | new Promise((resolve, reject) => { 3 | const image = new Image(); 4 | image.addEventListener("load", () => resolve(image)); 5 | image.addEventListener("error", error => reject(error)); 6 | image.setAttribute("crossOrigin", "anonymous"); 7 | image.src = url; 8 | }); 9 | 10 | export default async function getRoundImg(imageSrc, fileName) { 11 | const image = await createImage(imageSrc); 12 | console.log("image :", image); 13 | const canvas = document.createElement("canvas"); 14 | canvas.width = image.width; 15 | canvas.height = image.height; 16 | const ctx = canvas.getContext("2d"); 17 | 18 | ctx.drawImage(image, 0, 0); 19 | ctx.globalCompositeOperation = "destination-in"; 20 | ctx.beginPath(); 21 | ctx.arc(image.width / 2, image.height / 2, image.height / 2, 0, Math.PI * 2); 22 | ctx.closePath(); 23 | ctx.fill(); 24 | if (!HTMLCanvasElement.prototype.toBlob) { 25 | Object.defineProperty(HTMLCanvasElement.prototype, "toBlob", { 26 | value: function(callback, type, quality) { 27 | var dataURL = this.toDataURL(type, quality).split(",")[1]; 28 | setTimeout(function() { 29 | var binStr = atob(dataURL), 30 | len = binStr.length, 31 | arr = new Uint8Array(len); 32 | for (var i = 0; i < len; i++) { 33 | arr[i] = binStr.charCodeAt(i); 34 | } 35 | callback(new Blob([arr], { type: type || "image/png" })); 36 | }); 37 | } 38 | }); 39 | } 40 | return new Promise(resolve => { 41 | ctx.canvas.toBlob( 42 | blob => { 43 | const file = new File([blob], fileName, { 44 | type: "image/png" 45 | }); 46 | const resultReader = new FileReader(); 47 | 48 | resultReader.readAsDataURL(file); 49 | 50 | resultReader.addEventListener("load", () => resolve({ file, url: resultReader.result })); 51 | }, 52 | "image/png", 53 | 1 54 | ); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /src/utils/satoshiDice.js: -------------------------------------------------------------------------------- 1 | export default { 2 | [`1.05x`]: { 3 | [`address`]: `bitcoincash:qz9cq5ylczhld5rm7a4px04zl50v5ahr3scyqudsp9`, 4 | [`description`]: `94.29% Chance`, 5 | min: 0.001, 6 | max: 10 7 | }, 8 | [`1.1x`]: { 9 | [`address`]: `bitcoincash:qz9cq5ytxmejfqqr0l5clkmw0l52pj6n5yc76lup7a`, 10 | [`description`]: `90.00% Chance`, 11 | min: 0.001, 12 | max: 10 13 | }, 14 | [`1.33x`]: { 15 | [`address`]: `bitcoincash:qz9cq5pcexrfnz0a60qz7xtvv8wh5rqf8v6pxd3k74`, 16 | [`description`]: `74.44% Chance`, 17 | min: 0.001, 18 | max: 7.5 19 | }, 20 | [`1.5x`]: { 21 | [`address`]: `bitcoincash:qz9cq5yfkyjpgq6xatlr6veyhmcartkyrg7wev9jzc`, 22 | [`description`]: `66.00% Chance`, 23 | min: 0.001, 24 | max: 7.5 25 | }, 26 | [`2x`]: { 27 | [`address`]: `bitcoincash:qz9cq5rlkdrjy2zkfzqscq847q9n07mu5y7hj8fcge`, 28 | [`description`]: `49.50% Chance`, 29 | min: 0.001, 30 | max: 5 31 | }, 32 | [`3x`]: { 33 | [`address`]: `bitcoincash:qz9cq5r294syv3csh56e4jpyqrpt7gl9lcj7wveruw`, 34 | [`description`]: `33.00% Chance`, 35 | min: 0.001, 36 | max: 3.5 37 | }, 38 | [`10x`]: { 39 | [`address`]: `bitcoincash:qz9cq5pgwgx68wfevx0t78xalkh33xa0v5wlx6nppx`, 40 | [`description`]: `9.90% Chance`, 41 | min: 0.001, 42 | max: 1 43 | }, 44 | [`25x`]: { 45 | [`address`]: `bitcoincash:qz9cq5pryv9hnqwa8q8mccmynk9uf4vlu5nxerpzmc`, 46 | [`description`]: `3.96% Chance`, 47 | min: 0.001, 48 | max: 0.02 49 | }, 50 | [`50x`]: { 51 | [`address`]: `bitcoincash:qz9cq5qa2mfqcxlc4220yh8fatadu4z7pcewq0ns8y`, 52 | [`description`]: `1.98% Chance`, 53 | min: 0.001, 54 | max: 0.02 55 | }, 56 | [`100x`]: { 57 | [`address`]: `bitcoincash:qz9cq5qeguz30nuynwt2ulq6cxt6gfklfv2djqj9lf`, 58 | [`description`]: `0.99% Chance`, 59 | min: 0.001, 60 | max: 0.1 61 | }, 62 | [`1000x`]: { 63 | [`address`]: `bitcoincash:qqpx0wk0hru27l0xk2ek9xulhh269awklyauyuraxk`, 64 | [`description`]: `0.10% Chance`, 65 | min: 0.001, 66 | max: 0.01 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /src/utils/sendBch.js: -------------------------------------------------------------------------------- 1 | import Big from "big.js"; 2 | import withSLP from "./withSLP"; 3 | import { DUST } from "./sendDividends"; 4 | 5 | export const SATOSHIS_PER_BYTE = 1.01; 6 | export const SEND_BCH_ERRORS = { 7 | INSUFICIENT_FUNDS: 0, 8 | NETWORK_ERROR: 1, 9 | INSUFFICIENT_PRIORITY: 66, // ~insufficien fee 10 | DOUBLE_SPENDING: 18, 11 | MAX_UNCONFIRMED_TXS: 64 12 | }; 13 | const NETWORK = process.env.REACT_APP_NETWORK; 14 | 15 | export const sendBch = withSLP( 16 | async (SLP, wallet, utxos, { addresses, values, encodedOpReturn }, callbackTxId ) => { 17 | try { 18 | if (!values.length) { 19 | return null; 20 | } 21 | 22 | const value = values.reduce( 23 | (previous, current) => new Big(current).plus(previous), 24 | new Big(0) 25 | ); 26 | const REMAINDER_ADDR = wallet.Path145.cashAddress; 27 | 28 | const inputUtxos = []; 29 | let transactionBuilder; 30 | 31 | // instance of transaction builder 32 | if (NETWORK === `mainnet`) transactionBuilder = new SLP.TransactionBuilder(); 33 | else transactionBuilder = new SLP.TransactionBuilder("testnet"); 34 | 35 | const satoshisToSend = SLP.BitcoinCash.toSatoshi(value.toFixed(8)); 36 | let originalAmount = new Big(0); 37 | let txFee = 0; 38 | for (let i = 0; i < utxos.length; i++) { 39 | const utxo = utxos[i]; 40 | originalAmount = originalAmount.plus(utxo.satoshis); 41 | const vout = utxo.vout; 42 | const txid = utxo.txid; 43 | // add input with txid and index of vout 44 | transactionBuilder.addInput(txid, vout); 45 | inputUtxos.push(utxo); 46 | 47 | const byteCount = encodedOpReturn 48 | ? SLP.BitcoinCash.getByteCount( 49 | { P2PKH: inputUtxos.length }, 50 | { P2PKH: addresses.length + 2 } 51 | ) 52 | : SLP.BitcoinCash.getByteCount( 53 | { P2PKH: inputUtxos.length }, 54 | { P2PKH: addresses.length + 1 } 55 | ); 56 | const satoshisPerByte = SATOSHIS_PER_BYTE; 57 | txFee = encodedOpReturn 58 | ? Math.floor(satoshisPerByte * (byteCount + encodedOpReturn.length)) 59 | : Math.floor(satoshisPerByte * byteCount); 60 | 61 | if ( 62 | originalAmount 63 | .minus(satoshisToSend) 64 | .minus(txFee) 65 | .gte(0) 66 | ) { 67 | break; 68 | } 69 | } 70 | 71 | // amount to send back to the remainder address. 72 | const remainder = Math.floor(originalAmount.minus(satoshisToSend).minus(txFee)); 73 | if (remainder < 0) { 74 | const error = new Error(`Insufficient funds`); 75 | error.code = SEND_BCH_ERRORS.INSUFICIENT_FUNDS; 76 | throw error; 77 | } 78 | 79 | if (encodedOpReturn) { 80 | transactionBuilder.addOutput(encodedOpReturn, 0); 81 | } 82 | 83 | // add output w/ address and amount to send 84 | for (let i = 0; i < addresses.length; i++) { 85 | const address = addresses[i]; 86 | transactionBuilder.addOutput( 87 | SLP.Address.toCashAddress(address), 88 | SLP.BitcoinCash.toSatoshi(Number(values[i]).toFixed(8)) 89 | ); 90 | } 91 | 92 | if (remainder >= SLP.BitcoinCash.toSatoshi(DUST)) { 93 | transactionBuilder.addOutput(REMAINDER_ADDR, remainder); 94 | } 95 | 96 | // Sign the transactions with the HD node. 97 | for (let i = 0; i < inputUtxos.length; i++) { 98 | const utxo = inputUtxos[i]; 99 | transactionBuilder.sign( 100 | i, 101 | SLP.ECPair.fromWIF(utxo.wif), 102 | undefined, 103 | transactionBuilder.hashTypes.SIGHASH_ALL, 104 | utxo.satoshis 105 | ); 106 | } 107 | 108 | // build tx 109 | const tx = transactionBuilder.build(); 110 | // output rawhex 111 | const hex = tx.toHex(); 112 | 113 | // Broadcast transation to the network 114 | const txidStr = await SLP.RawTransactions.sendRawTransaction([hex]); 115 | let link; 116 | 117 | if (callbackTxId) { 118 | callbackTxId(txidStr) 119 | } 120 | if (NETWORK === `mainnet`) { 121 | link = `https://explorer.bitcoin.com/bch/tx/${txidStr}`; 122 | } else { 123 | link = `https://explorer.bitcoin.com/tbch/tx/${txidStr}`; 124 | } 125 | console.log(link); 126 | 127 | return link; 128 | } catch (err) { 129 | if (err.error === "insufficient priority (code 66)") { 130 | err.code = SEND_BCH_ERRORS.INSUFFICIENT_PRIORITY; 131 | } else if (err.error === "txn-mempool-conflict (code 18)") { 132 | err.code = SEND_BCH_ERRORS.DOUBLE_SPENDING; 133 | } else if (err.error === "Network Error") { 134 | err.code = SEND_BCH_ERRORS.NETWORK_ERROR; 135 | } else if ( 136 | err.error === "too-long-mempool-chain, too many unconfirmed ancestors [limit: 25] (code 64)" 137 | ) { 138 | err.code = SEND_BCH_ERRORS.MAX_UNCONFIRMED_TXS; 139 | } 140 | console.log(`error: `, err); 141 | throw err; 142 | } 143 | } 144 | ); 145 | 146 | export const calcFee = withSLP((SLP, utxos) => { 147 | const byteCount = SLP.BitcoinCash.getByteCount({ P2PKH: utxos.length }, { P2PKH: 2 }); 148 | const satoshisPerByte = SATOSHIS_PER_BYTE; 149 | const txFee = SLP.BitcoinCash.toBitcoinCash(Math.floor(satoshisPerByte * byteCount)); 150 | return txFee; 151 | }); 152 | -------------------------------------------------------------------------------- /src/utils/sendDividends.js: -------------------------------------------------------------------------------- 1 | import Big from "big.js"; 2 | import withSLP from "./withSLP"; 3 | import { SATOSHIS_PER_BYTE } from "./sendBch"; 4 | import Dividends from "./dividends/dividends"; 5 | 6 | export const DUST = 0.00005; 7 | 8 | export const getEncodedOpReturnMessage = withSLP((SLP, opReturnMessage = "", tokenId) => { 9 | const decodedOpReturn = `${tokenId} MintDividend${opReturnMessage ? `: ${opReturnMessage}` : ""}`; 10 | const buf = Buffer.from(decodedOpReturn, "ascii"); 11 | return { 12 | encodedOpReturn: SLP.Script.encodeNullDataOutput(buf), 13 | decodedOpReturn 14 | }; 15 | }); 16 | 17 | export const getBalancesForToken = withSLP(async (SLP, tokenId) => { 18 | try { 19 | const balances = await SLP.Utils.balancesForToken(tokenId); 20 | balances.totalBalance = balances.reduce((p, c) => c.tokenBalance + p, 0); 21 | return balances; 22 | } catch (err) { 23 | console.error(`Error in getTokenInfo: `, err); 24 | throw err; 25 | } 26 | }); 27 | 28 | export const getEligibleAddresses = withSLP( 29 | (SLP, wallet, balancesForToken, value, utxos, advancedOptions, tokenId) => { 30 | const addresses = []; 31 | const values = []; 32 | 33 | const slpAddressesToExclude = advancedOptions.addressesToExclude 34 | .filter(addressToExclude => addressToExclude.valid) 35 | .map(addressToExclude => SLP.Address.toSLPAddress(addressToExclude.address)); 36 | 37 | if (advancedOptions.ignoreOwnAddress) { 38 | slpAddressesToExclude.push(...wallet.slpAddresses); 39 | } 40 | 41 | const eligibleBalances = balancesForToken 42 | .filter(balance => !slpAddressesToExclude.includes(balance.slpAddress)) 43 | .map(eligibleBalance => ({ 44 | ...eligibleBalance, 45 | tokenBalance: new Big(eligibleBalance.tokenBalanceString) 46 | })); 47 | const tokenBalanceSum = eligibleBalances.reduce((p, c) => p.plus(c.tokenBalance), new Big(0)); 48 | const minTokenBalance = tokenBalanceSum.mul(DUST).div(value); 49 | 50 | const filteredEligibleBalances = eligibleBalances.filter(eligibleBalance => 51 | minTokenBalance.lte(eligibleBalance.tokenBalance) 52 | ); 53 | const filteredTokenBalanceSum = filteredEligibleBalances.reduce( 54 | (p, c) => p.plus(c.tokenBalance), 55 | new Big(0) 56 | ); 57 | filteredEligibleBalances.forEach(async eligibleBalance => { 58 | const eligibleValue = eligibleBalance.tokenBalance.div(filteredTokenBalanceSum).mul(value); 59 | values.push(eligibleValue); 60 | addresses.push(eligibleBalance.slpAddress); 61 | }); 62 | const { encodedOpReturn, decodedOpReturn } = getEncodedOpReturnMessage( 63 | advancedOptions.opReturnMessage, 64 | tokenId 65 | ); 66 | let txFee = 0; 67 | for (let i = 0; i < addresses.length; i += Dividends.BATCH_SIZE) { 68 | const byteCount = SLP.BitcoinCash.getByteCount( 69 | { P2PKH: utxos.length }, 70 | { P2PKH: addresses.slice(i, Dividends.BATCH_SIZE).length + 2 } 71 | ); 72 | txFee += SLP.BitcoinCash.toBitcoinCash( 73 | Math.floor(SATOSHIS_PER_BYTE * (byteCount + encodedOpReturn.length)).toFixed(8) 74 | ); 75 | } 76 | 77 | return { 78 | addresses, 79 | values, 80 | txFee, 81 | encodedOpReturn, 82 | decodedOpReturn 83 | }; 84 | } 85 | ); 86 | 87 | export const sendDividends = async (wallet, utxos, advancedOptions, { value, token }) => { 88 | const outputs = await getBalancesForToken(token.tokenId); 89 | 90 | const { addresses, values } = getEligibleAddresses( 91 | wallet, 92 | outputs, 93 | value, 94 | utxos, 95 | advancedOptions, 96 | token.tokenId 97 | ); 98 | 99 | const dividend = new Dividends({ 100 | token, 101 | recipients: addresses, 102 | totalValue: value, 103 | values, 104 | opReturn: advancedOptions.opReturnMessage 105 | }); 106 | 107 | Dividends.save(dividend); 108 | }; 109 | -------------------------------------------------------------------------------- /src/utils/useAsyncTimeout.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | const useAsyncTimeout = (callback, delay) => { 4 | const savedCallback = useRef(callback); 5 | 6 | useEffect(() => { 7 | savedCallback.current = callback; 8 | }); 9 | 10 | useEffect(() => { 11 | let id = null; 12 | const tick = () => { 13 | const promise = savedCallback.current(); 14 | 15 | if (promise instanceof Promise) { 16 | promise.then(() => { 17 | id = setTimeout(tick, delay); 18 | }); 19 | } else { 20 | id = setTimeout(tick, delay); 21 | } 22 | }; 23 | 24 | if (id !== null) { 25 | id = setTimeout(tick, delay); 26 | return () => clearTimeout(id); 27 | } else { 28 | tick(); 29 | return; 30 | } 31 | }, [delay]); 32 | }; 33 | 34 | export default useAsyncTimeout; 35 | -------------------------------------------------------------------------------- /src/utils/useInnerScroll.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | export const useInnerScroll = () => 4 | useEffect(() => { 5 | document.body.style.overflow = "hidden"; 6 | return () => (document.body.style.overflow = ""); 7 | }, []); 8 | -------------------------------------------------------------------------------- /src/utils/useInterval.js: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from "react"; 2 | 3 | const useInterval = (callback, delay) => { 4 | const savedCallback = useRef(); 5 | 6 | useEffect(() => { 7 | savedCallback.current = callback; 8 | }); 9 | 10 | useEffect(() => { 11 | function tick() { 12 | savedCallback.current(); 13 | } 14 | 15 | let id = setInterval(tick, delay); 16 | return () => clearInterval(id); 17 | }, [delay]); 18 | }; 19 | 20 | export default useInterval; 21 | -------------------------------------------------------------------------------- /src/utils/usePrevious.js: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from "react"; 2 | 3 | export const usePrevious = value => { 4 | // The ref object is a generic container whose current property is mutable ... 5 | // ... and can hold any value, similar to an instance property on a class 6 | const ref = useRef(); 7 | 8 | // Store current value in ref 9 | useEffect(() => { 10 | ref.current = value; 11 | }, [value]); // Only re-run if value changes 12 | 13 | // Return previous value (happens before update in useEffect above) 14 | return ref.current; 15 | }; 16 | 17 | export default usePrevious; 18 | -------------------------------------------------------------------------------- /src/utils/useWallet.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | 3 | import React, { useState, useCallback } from "react"; 4 | import Paragraph from "antd/lib/typography/Paragraph"; 5 | import { notification } from "antd"; 6 | import Big from "big.js"; 7 | import { getWallet, createWallet } from "./createWallet"; 8 | import useAsyncTimeout from "./useAsyncTimeout"; 9 | import usePrevious from "./usePrevious"; 10 | import withSLP from "./withSLP"; 11 | import getSlpBanlancesAndUtxos from "./getSlpBanlancesAndUtxos"; 12 | import DividendsManager from "./dividends/dividends-manager"; 13 | 14 | const normalizeSlpBalancesAndUtxos = (SLP, slpBalancesAndUtxos, wallet) => { 15 | slpBalancesAndUtxos.nonSlpUtxos.forEach(utxo => { 16 | const derivatedAccount = wallet.Accounts.find(account => account.cashAddress === utxo.address); 17 | utxo.wif = derivatedAccount.fundingWif; 18 | }); 19 | 20 | return slpBalancesAndUtxos; 21 | }; 22 | 23 | const normalizeBalance = (SLP, slpBalancesAndUtxos) => { 24 | const totalBalanceInSatohis = slpBalancesAndUtxos.nonSlpUtxos.reduce( 25 | (previousBalance, utxo) => previousBalance + utxo.satoshis, 26 | 0 27 | ); 28 | return { 29 | totalBalanceInSatohis, 30 | totalBalance: SLP.BitcoinCash.toBitcoinCash(totalBalanceInSatohis) 31 | }; 32 | }; 33 | 34 | const update = withSLP(async (SLP, { wallet, setWalletState }) => { 35 | try { 36 | if (!wallet) { 37 | return; 38 | } 39 | const slpBalancesAndUtxos = await getSlpBanlancesAndUtxos(wallet.cashAddresses); 40 | const { tokens } = slpBalancesAndUtxos; 41 | const newState = { 42 | balances: {}, 43 | tokens: [], 44 | slpBalancesAndUtxos: [] 45 | }; 46 | 47 | newState.slpBalancesAndUtxos = normalizeSlpBalancesAndUtxos(SLP, slpBalancesAndUtxos, wallet); 48 | newState.balances = normalizeBalance(SLP, slpBalancesAndUtxos); 49 | newState.tokens = tokens; 50 | 51 | setWalletState(newState); 52 | } catch (error) {} 53 | }); 54 | 55 | export const useWallet = () => { 56 | const [wallet, setWallet] = useState(getWallet()); 57 | const [walletState, setWalletState] = useState({ 58 | balances: {}, 59 | tokens: [], 60 | slpBalancesAndUtxos: [] 61 | }); 62 | const [loading, setLoading] = useState(true); 63 | const { balances, tokens, slpBalancesAndUtxos } = walletState; 64 | const previousBalances = usePrevious(balances); 65 | 66 | if ( 67 | previousBalances && 68 | balances && 69 | "totalBalance" in previousBalances && 70 | "totalBalance" in balances && 71 | new Big(balances.totalBalance).minus(previousBalances.totalBalance).gt(0) 72 | ) { 73 | notification.success({ 74 | message: "BCH", 75 | description: ( 76 | 77 | You received {Number(balances.totalBalance - previousBalances.totalBalance).toFixed(8)}{" "} 78 | BCH! 79 | 80 | ), 81 | duration: 2 82 | }); 83 | } 84 | 85 | const updateDividends = useCallback( 86 | ({ wallet }) => DividendsManager.update({ wallet, utxos: slpBalancesAndUtxos.nonSlpUtxos }), 87 | [slpBalancesAndUtxos] 88 | ); 89 | 90 | useAsyncTimeout(() => { 91 | const wallet = getWallet(); 92 | update({ 93 | wallet, 94 | setWalletState 95 | }).finally(() => { 96 | setLoading(false); 97 | }); 98 | updateDividends({ wallet }); 99 | }, 5000); 100 | 101 | return { 102 | wallet, 103 | slpBalancesAndUtxos, 104 | balances, 105 | tokens, 106 | loading, 107 | update: () => 108 | update({ 109 | wallet: getWallet(), 110 | 111 | setLoading, 112 | setWalletState 113 | }), 114 | createWallet: importMnemonic => { 115 | setLoading(true); 116 | const newWallet = createWallet(importMnemonic); 117 | setWallet(newWallet); 118 | update({ 119 | wallet: newWallet, 120 | setWalletState 121 | }).finally(() => setLoading(false)); 122 | } 123 | }; 124 | }; 125 | -------------------------------------------------------------------------------- /src/utils/withSLP.js: -------------------------------------------------------------------------------- 1 | import SLPSDK from "slp-sdk"; 2 | 3 | export const getRestUrl = () => 4 | process.env.REACT_APP_NETWORK === `mainnet` 5 | ? window.localStorage.getItem("restAPI") || `https://rest.bch.actorforth.org/v2//` 6 | : window.localStorage.getItem("restAPI") || `https://trest.bitcoin.com/v2/`; 7 | 8 | export default callback => { 9 | const SLPInstance = new SLPSDK({ 10 | restURL: getRestUrl() 11 | }); 12 | 13 | return (...args) => callback(SLPInstance, ...args); 14 | }; 15 | --------------------------------------------------------------------------------