├── .dockerignore ├── .eslintrc ├── .github └── workflows │ └── release-please.yml ├── .gitignore ├── .prettierrc ├── .release-please-manifest.json ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── apps ├── electron │ ├── CHANGELOG.md │ ├── afterSignHook.js │ ├── entitlements.mac.inherit.plist │ ├── package.json │ ├── src │ │ ├── assets │ │ │ ├── AppIcon.icns │ │ │ └── background.png │ │ ├── main.ts │ │ └── preload.ts │ └── tsconfig.json ├── express │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── routes │ │ │ ├── chart.ts │ │ │ ├── config.ts │ │ │ ├── db.ts │ │ │ ├── download.ts │ │ │ ├── hwi.ts │ │ │ ├── lightning.ts │ │ │ └── onchain.ts │ │ └── utils │ │ │ ├── index.ts │ │ │ └── setInitialConfig.ts │ └── tsconfig.json └── frontend │ ├── .env.electron │ ├── .env.umbrel │ ├── CHANGELOG.md │ ├── Dockerfile │ ├── craco.config.js │ ├── cypress.json │ ├── cypress │ ├── fixtures │ │ ├── example.json │ │ └── historical-btc-price.json │ ├── integration │ │ ├── Lightning │ │ │ ├── lightning.spec.js │ │ │ ├── lightning.spec.js.map │ │ │ ├── lightning.spec.ts │ │ │ ├── receive.spec.js │ │ │ └── send.spec.js │ │ ├── Login │ │ │ └── login.spec.js │ │ ├── Purchase │ │ │ └── purchase.spec.js │ │ ├── Receive │ │ │ └── receive.spec.js │ │ ├── Send │ │ │ ├── fees.spec.js │ │ │ ├── import-tx.spec.js │ │ │ └── send.spec.js │ │ ├── Settings │ │ │ └── settings.spec.js │ │ ├── Setup │ │ │ ├── HardwareWallet.spec.js │ │ │ ├── HardwareWalletMultiple.spec.js │ │ │ ├── Mnemonic.spec.js │ │ │ ├── Multisig.spec.js │ │ │ └── setup.spec.js │ │ ├── Support │ │ │ └── Support.spec.js │ │ └── Vaults │ │ │ ├── Vault-Export.spec.js │ │ │ ├── Vault-Settings.spec.js │ │ │ └── Vault.spec.js │ ├── plugins │ │ └── index.js │ └── support │ │ ├── commands.js │ │ ├── createConfig.js │ │ ├── getNewInvoice.js │ │ └── index.js │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── AppIcon.icns │ ├── background.png │ ├── entitlements.mac.inherit.plist │ ├── favicons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── mstile-144x144.png │ │ ├── mstile-150x150.png │ │ ├── mstile-310x150.png │ │ ├── mstile-310x310.png │ │ ├── mstile-70x70.png │ │ ├── safari-pinned-tab.svg │ │ └── site.webmanifest │ ├── icon.png │ ├── index.html │ ├── manifest.json │ ├── robots.txt │ └── screenshot.png │ ├── react-app-env.d.ts │ ├── src │ ├── App.tsx │ ├── __tests__ │ │ ├── fixtures │ │ │ ├── DAS │ │ │ │ ├── DAS-Account.json │ │ │ │ ├── DAS-Addresses.json │ │ │ │ ├── DAS-ChangeAddresses.json │ │ │ │ ├── DAS-Transactions.json │ │ │ │ ├── DAS-UTXOs.json │ │ │ │ ├── DAS-UnusedAddresses.json │ │ │ │ ├── DAS-UnusedChangeAddresses.json │ │ │ │ └── DAS-other-data.json │ │ │ ├── HWW │ │ │ │ ├── HWW-Account.json │ │ │ │ ├── HWW-Addresses.json │ │ │ │ ├── HWW-ChangeAddresses.json │ │ │ │ ├── HWW-Transactions.json │ │ │ │ ├── HWW-UTXOs.json │ │ │ │ ├── HWW-UnusedAddresses.json │ │ │ │ └── HWW-UnusedChangeAddresses.json │ │ │ ├── JB │ │ │ │ ├── JB-Addresses.json │ │ │ │ ├── JB-ChangeAddresses.json │ │ │ │ ├── JB-Config.json │ │ │ │ ├── JB-Transactions.json │ │ │ │ ├── JB-UTXOs.json │ │ │ │ ├── JB-UnusedAddresses.json │ │ │ │ ├── JB-UnusedChangeAddresses.json │ │ │ │ └── JB-other-data.json │ │ │ ├── Lightning │ │ │ │ ├── Lightning-BalanceHistory.json │ │ │ │ ├── Lightning-Channels.json │ │ │ │ ├── Lightning-ClosedChannels.json │ │ │ │ ├── Lightning-Config.json │ │ │ │ ├── Lightning-CurrentBalance.json │ │ │ │ ├── Lightning-Events.json │ │ │ │ ├── Lightning-Info.json │ │ │ │ ├── Lightning-Invoices.json │ │ │ │ └── Lightning-Payments.json │ │ │ ├── Mnemonic │ │ │ │ ├── Mnemonic-Addresses.json │ │ │ │ ├── Mnemonic-ChangeAddresses.json │ │ │ │ ├── Mnemonic-Config.json │ │ │ │ ├── Mnemonic-Transactions.json │ │ │ │ ├── Mnemonic-UTXOs.json │ │ │ │ ├── Mnemonic-UnusedAddresses.json │ │ │ │ └── Mnemonic-UnusedChangeAddresses.json │ │ │ ├── Multisig │ │ │ │ ├── Multisig-Addresses.json │ │ │ │ ├── Multisig-ChangeAddresses.json │ │ │ │ ├── Multisig-Config.json │ │ │ │ ├── Multisig-Transactions.json │ │ │ │ ├── Multisig-UTXOs.json │ │ │ │ ├── Multisig-UnusedAddresses.json │ │ │ │ ├── Multisig-UnusedChangeAddresses.json │ │ │ │ └── Multisig-other-data.json │ │ │ ├── Sunny │ │ │ │ ├── Sunny-Config.json │ │ │ │ └── Sunny-other-data.json │ │ │ ├── index.js │ │ │ ├── index.js.map │ │ │ ├── index.ts │ │ │ ├── initialAccountMap.json │ │ │ └── serializeTransactions.json │ │ ├── mock │ │ │ ├── electron-mock.js │ │ │ ├── electron-mock.js.map │ │ │ └── electron-mock.ts │ │ ├── reducers │ │ │ └── accountMap.test.js │ │ └── utils │ │ │ ├── accountMap.test.js │ │ │ ├── accountMap.test.js.map │ │ │ ├── accountMap.test.ts │ │ │ ├── files.test.js │ │ │ ├── files.test.js.map │ │ │ ├── files.test.ts │ │ │ ├── license.test.js │ │ │ ├── license.test.js.map │ │ │ ├── license.test.ts │ │ │ ├── migration.test.js │ │ │ ├── migration.test.js.map │ │ │ ├── migration.test.ts │ │ │ ├── send.test.js │ │ │ ├── send.test.js.map │ │ │ └── send.test.ts │ ├── assets │ │ ├── AppIcon.icns │ │ ├── bitbox02.png │ │ ├── bitgo.png │ │ ├── cobo.png │ │ ├── coldcard.png │ │ ├── dead-flower.svg │ │ ├── flower-loading.svg │ │ ├── flower.svg │ │ ├── fonts │ │ │ ├── Montserrat-Light.ttf │ │ │ ├── Montserrat-Medium.ttf │ │ │ ├── Montserrat-Regular.ttf │ │ │ ├── Montserrat-SemiBold.ttf │ │ │ ├── Raleway-Light.ttf │ │ │ ├── Raleway-Medium.ttf │ │ │ ├── Raleway-Regular.ttf │ │ │ └── Raleway-SemiBold.ttf │ │ ├── icon.icns │ │ ├── icon.png │ │ ├── iphone.png │ │ ├── kingdom-trust.png │ │ ├── ledger_nano_s.png │ │ ├── ledger_nano_x.png │ │ ├── lily-image.jpg │ │ ├── lily-trim.svg │ │ ├── onramp.png │ │ ├── trezor_1.png │ │ ├── trezor_t.png │ │ └── unchained.png │ ├── components │ │ ├── AlertBar.tsx │ │ ├── AnimatedQrCode.tsx │ │ ├── Badge.tsx │ │ ├── Breadcrumbs.tsx │ │ ├── Button.tsx │ │ ├── ChartEmptyState.tsx │ │ ├── ConnectToLilyMobileModal.tsx │ │ ├── ConnectToNodeModal.tsx │ │ ├── Countdown.tsx │ │ ├── Counter.tsx │ │ ├── DeviceImage.tsx │ │ ├── DeviceSelect.tsx │ │ ├── Dropdown.tsx │ │ ├── ErrorBoundary.js │ │ ├── ErrorModal.tsx │ │ ├── FileUploader.tsx │ │ ├── Input.tsx │ │ ├── LicenseInformation.tsx │ │ ├── LightningImage.tsx │ │ ├── Loading.tsx │ │ ├── MnemonicWordsDisplayer.tsx │ │ ├── Modal.tsx │ │ ├── NavLinks.tsx │ │ ├── NoAccountsEmptyState.tsx │ │ ├── OutsideClick.tsx │ │ ├── Price.tsx │ │ ├── PricingChart.tsx │ │ ├── PricingTable.tsx │ │ ├── PromptPinModal.tsx │ │ ├── PurchaseLicenseSuccess.tsx │ │ ├── ScrollToTop.ts │ │ ├── Select.tsx │ │ ├── SelectAccountMenu.tsx │ │ ├── SettingsTable.tsx │ │ ├── Sidebar.tsx │ │ ├── SlideOver.tsx │ │ ├── Spinner.tsx │ │ ├── StyledIcon.tsx │ │ ├── SupportModal.tsx │ │ ├── Table.tsx │ │ ├── Tabs.tsx │ │ ├── Textarea.tsx │ │ ├── TitleBar.tsx │ │ ├── Toggle.tsx │ │ ├── TransactionRowsLoading.tsx │ │ ├── Transition.js │ │ ├── Unit.tsx │ │ ├── UnitInput.tsx │ │ ├── index.ts │ │ └── layout.tsx │ ├── context │ │ ├── AccountMapContext.tsx │ │ ├── ConfigContext.tsx │ │ ├── ModalContext.tsx │ │ ├── PlatformContext.tsx │ │ ├── SidebarContext.tsx │ │ ├── UnitContext.tsx │ │ └── index.ts │ ├── frontend-middleware │ │ ├── BasePlatform.ts │ │ ├── ElectronPlatform.ts │ │ ├── WebPlatform.ts │ │ └── index.ts │ ├── hocs │ │ ├── index.ts │ │ ├── requireLightning.tsx │ │ ├── requireOnchain.tsx │ │ ├── useSelected.tsx │ │ └── useShiftSelected.tsx │ ├── index.css │ ├── index.tsx │ ├── pages │ │ ├── Home │ │ │ ├── AccountGridItem.tsx │ │ │ ├── AccountListItem.tsx │ │ │ ├── AccountsSection.tsx │ │ │ ├── AddNewAccountGridItem.tsx │ │ │ ├── AddNewAccountListItem.tsx │ │ │ ├── HistoricChart.tsx │ │ │ ├── index.tsx │ │ │ └── utils.ts │ │ ├── Lightning │ │ │ ├── Channels │ │ │ │ ├── ChannelView │ │ │ │ │ ├── ChannelDetailsModal.tsx │ │ │ │ │ ├── ChannelModal.tsx │ │ │ │ │ ├── ChannelRow.tsx │ │ │ │ │ ├── CloseChannel │ │ │ │ │ │ ├── CloseChannelModal.tsx │ │ │ │ │ │ └── CloseChannelSuccess.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── OpenChannel │ │ │ │ │ ├── LightningImage.tsx │ │ │ │ │ ├── OpenChannelForm.tsx │ │ │ │ │ ├── OpenChannelSuccess.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── LightningHeader.tsx │ │ │ ├── LightningView.tsx │ │ │ ├── RecentActivity │ │ │ │ ├── LightningDetailsSlideover.tsx │ │ │ │ ├── PaymentRow.tsx │ │ │ │ ├── PaymentTypeIcon.tsx │ │ │ │ └── index.tsx │ │ │ ├── Settings │ │ │ │ ├── DeleteAccountModal.tsx │ │ │ │ ├── DeviceDetailsModal.tsx │ │ │ │ ├── EditAccountNameModal.tsx │ │ │ │ ├── ExportView.tsx │ │ │ │ ├── GeneralView.tsx │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ └── utils.ts │ │ ├── Login │ │ │ ├── SignupForm.tsx │ │ │ ├── UnlockForm.tsx │ │ │ └── index.tsx │ │ ├── Purchase │ │ │ └── index.tsx │ │ ├── Receive │ │ │ ├── Lightning │ │ │ │ ├── LightningReceiveForm.tsx │ │ │ │ ├── LightningReceiveQr.tsx │ │ │ │ ├── LightningReceiveSuccess.tsx │ │ │ │ └── index.tsx │ │ │ ├── OnchainReceive.tsx │ │ │ └── index.tsx │ │ ├── Send │ │ │ ├── Lightning │ │ │ │ ├── ChannelSlideover.tsx │ │ │ │ ├── LightningPaymentConfirm.tsx │ │ │ │ ├── LightningSendTxForm.tsx │ │ │ │ ├── PaymentSuccess.tsx │ │ │ │ ├── ScanLightningQrCode.tsx │ │ │ │ └── index.tsx │ │ │ ├── Onchain │ │ │ │ ├── AddSignatureFromQrCode │ │ │ │ │ ├── DecodePsbtQrCode.tsx │ │ │ │ │ ├── PsbtQrCode.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── ConfirmTxPage.tsx │ │ │ │ ├── SignWithDevice.tsx │ │ │ │ └── index.tsx │ │ │ ├── components │ │ │ │ ├── FeeSelector.tsx │ │ │ │ ├── OnchainSendTxForm.tsx │ │ │ │ ├── PastePsbtModalContent.tsx │ │ │ │ ├── SelectInputsForm │ │ │ │ │ ├── SearchToolbar.tsx │ │ │ │ │ ├── UtxoInputSelectRow.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── ShoppingCart.tsx │ │ │ │ ├── TransactionDetails.tsx │ │ │ │ ├── TxUtxoDetails.tsx │ │ │ │ └── UnfundedPsbtAlert.tsx │ │ │ └── index.tsx │ │ ├── Settings │ │ │ ├── About.tsx │ │ │ ├── BackupSettings.tsx │ │ │ ├── NetworkSettings.tsx │ │ │ ├── PasswordModal.tsx │ │ │ └── index.tsx │ │ ├── Setup │ │ │ ├── InputNameScreen.tsx │ │ │ ├── NewHardwareWalletScreen.tsx │ │ │ ├── NewLightningScreen.tsx │ │ │ ├── NewVault │ │ │ │ ├── AddDeviceDropdown.tsx │ │ │ │ ├── InnerTransition.tsx │ │ │ │ ├── InputXpubModal.tsx │ │ │ │ ├── NoDevicesEmptyState.tsx │ │ │ │ ├── RequestDeviceViaEmail.tsx │ │ │ │ ├── RequiredDevicesModal.tsx │ │ │ │ └── index.tsx │ │ │ ├── NewWalletScreen.tsx │ │ │ ├── PageHeader.tsx │ │ │ ├── Review │ │ │ │ ├── AccountAlreadyExistsBanner.tsx │ │ │ │ ├── AddOwnerDetailsForm.tsx │ │ │ │ ├── Devices.tsx │ │ │ │ ├── LightningReview.tsx │ │ │ │ ├── OnchainReview.tsx │ │ │ │ ├── TransitionSlideLeft.tsx │ │ │ │ └── index.tsx │ │ │ ├── SelectAccountScreen.tsx │ │ │ ├── Steps.tsx │ │ │ ├── TransitionSlideLeft.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ └── Vault │ │ │ ├── RecentTransactions │ │ │ ├── NoFilteredTransactionsEmptyState.tsx │ │ │ ├── NoTransactionsEmptyState.tsx │ │ │ ├── TransactionDescription.tsx │ │ │ ├── TransactionRow.tsx │ │ │ ├── TransactionTypeIcon.tsx │ │ │ ├── TxDetailsSlideover.tsx │ │ │ └── index.tsx │ │ │ ├── RescanModal.tsx │ │ │ ├── Settings │ │ │ ├── Addresses │ │ │ │ ├── AddLabelTag.tsx │ │ │ │ ├── AddressDetailsSlideover.tsx │ │ │ │ ├── AddressRow.tsx │ │ │ │ ├── LabelTag.tsx │ │ │ │ ├── NoAddressesEmptyState.tsx │ │ │ │ ├── TagsSection.tsx │ │ │ │ └── index.tsx │ │ │ ├── DeleteAccountModal.tsx │ │ │ ├── DeviceDetailsModal.tsx │ │ │ ├── Devices │ │ │ │ ├── DeviceDetails.tsx │ │ │ │ ├── DeviceDetailsHeader.tsx │ │ │ │ ├── DeviceTechnicalDetails.tsx │ │ │ │ ├── OwnerInformation.tsx │ │ │ │ └── index.tsx │ │ │ ├── EditAccountNameModal.tsx │ │ │ ├── ExportView.tsx │ │ │ ├── GeneralView.tsx │ │ │ ├── LicenseSettings.tsx │ │ │ ├── UTXOs │ │ │ │ ├── NoUtxosEmptyState.tsx │ │ │ │ ├── UtxoRow.tsx │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ │ ├── VaultHeader.tsx │ │ │ ├── VaultView.tsx │ │ │ └── index.tsx │ ├── react-app-env.d.ts │ ├── reducers │ │ └── accountMap.ts │ ├── types │ │ └── index.d.ts │ └── utils │ │ ├── accountMap.ts │ │ ├── colors.ts │ │ ├── files.ts │ │ ├── license.ts │ │ ├── media.js │ │ ├── migration.ts │ │ ├── other.ts │ │ ├── rem.js │ │ ├── send.ts │ │ └── useLocalStorage.ts │ ├── tailwind.config.js │ └── tsconfig.json ├── babel.config.js ├── circle.yml ├── default.conf ├── docker-compose.yml ├── docker-compose:umbrel.yml ├── docs └── development.md ├── package-lock.json ├── package.json ├── packages ├── HWIs │ ├── HWI_LINUX │ │ └── HWI_LINUX │ ├── HWI_MAC │ │ ├── HWI_MAC │ │ └── HWI_MAC_BITGO │ ├── HWI_PI │ └── HWI_WINDOWS │ │ ├── HWI_BITGO.exe │ │ └── hwi.exe ├── shared-server │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── HWI │ │ │ ├── commands.ts │ │ │ └── runCommand.ts │ │ ├── LightningProviders │ │ │ ├── LND.ts │ │ │ ├── LightningBaseProvider.ts │ │ │ └── index.ts │ │ ├── OnchainProviders │ │ │ ├── BitcoinCore.ts │ │ │ ├── Electrum.ts │ │ │ ├── Esplora.ts │ │ │ ├── OnchainBaseProvider.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── sqlite │ │ │ ├── address.ts │ │ │ ├── index.ts │ │ │ └── transaction.ts │ │ └── utils │ │ │ ├── accountMap.ts │ │ │ ├── lightning.ts │ │ │ └── utils.ts │ └── tsconfig.json └── types │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ ├── @types │ │ └── declarations │ │ │ ├── @mempool │ │ │ └── electrum-client │ │ │ │ └── index.ts │ │ │ ├── bs58check │ │ │ └── index.ts │ │ │ ├── coinselect │ │ │ └── index.ts │ │ │ ├── lndconnect │ │ │ └── index.ts │ │ │ ├── streams │ │ │ └── index.ts │ │ │ └── unchained-bitcoin │ │ │ └── index.ts │ └── index.ts │ └── tsconfig.json ├── release-please-config.json ├── screenshot.png ├── tor.sh └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .dockerignore 3 | Dockerfile 4 | Dockerfile.prod -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": ["eslint:recommended"], 4 | "plugins": ["@typescript-eslint"], 5 | "env": { 6 | "es6": true, 7 | "node": true 8 | }, 9 | "rules": { 10 | "no-undef": "off", 11 | "no-unused-vars": "off", 12 | "no-empty": "off", 13 | "no-extra-boolean-cast": "off" 14 | }, 15 | "ignorePatterns": ["dist", "node_modules", "examples", "scripts"] 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | name: release-please 6 | jobs: 7 | release-please: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: google-github-actions/release-please-action@v3 11 | with: 12 | command: manifest 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .DS_Store 4 | dist/ 5 | .env 6 | 7 | coverage/ 8 | cypress/videos 9 | cypress/screenshots 10 | .nyc_output 11 | 12 | .vscode 13 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "jsxSingleQuote": true, 5 | "printWidth": 100, 6 | "singleQuote": true, 7 | "trailingComma": "none" 8 | } 9 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | {"apps/electron":"1.4.0","apps/express":"1.4.0","apps/frontend":"1.4.0","packages/shared-server":"1.4.0","packages/types":"1.4.0",".":"1.4.0"} 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.4.0](https://github.com/Lily-Technologies/lily-wallet/compare/lily-wallet-v1.3.0...lily-wallet-v1.4.0) (2023-07-19) 4 | 5 | 6 | ### Features 7 | 8 | * **Bitgo:** support bitgo vaults ([baece25](https://github.com/Lily-Technologies/lily-wallet/commit/baece25843eb7a294ea3405c517b667121459248)) 9 | 10 | ## 1.3.0 (2022-12-07) 11 | 12 | 13 | ### Features 14 | 15 | * add release-please to github ([#122](https://github.com/Lily-Technologies/lily-wallet/issues/122)) ([a2f7bc5](https://github.com/Lily-Technologies/lily-wallet/commit/a2f7bc5f43382ffa4f7b21693d28f86aa5809f27)) 16 | * **Lightning:** specify outgoing channel id ([#111](https://github.com/Lily-Technologies/lily-wallet/issues/111)) ([30a9d7c](https://github.com/Lily-Technologies/lily-wallet/commit/30a9d7c05ea01fb238329528a29c9cc755ef4a1b)) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | * **Electron:** default electrum endpoint ([45a059b](https://github.com/Lily-Technologies/lily-wallet/commit/45a059b9e794aec4bb9fdaf13c5ac945a645fe64)) 22 | * **Setup, Hardware Wallet:** import from file ([dad413c](https://github.com/Lily-Technologies/lily-wallet/commit/dad413c438f8ff835e45f9b047056db23b1ca514)) 23 | * **Vault, Empty State:** fix padding, remove double empty state ([e074f26](https://github.com/Lily-Technologies/lily-wallet/commit/e074f26d44f1cd5338c7409d9ba40628a855b8e6)) 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Install and build packages dependencies 2 | FROM node:16-buster-slim as packages 3 | WORKDIR /packages 4 | COPY package.json . 5 | COPY packages/types/package.json ./packages/types/package.json 6 | COPY packages/shared-server/package.json ./packages/shared-server/package.json 7 | 8 | # install package dependencies 9 | RUN yarn 10 | 11 | # Copy over packages files 12 | COPY packages/types ./packages/types 13 | COPY packages/shared-server ./packages/shared-server 14 | 15 | # Run build 16 | RUN npm run build:types 17 | RUN npm run build:shared-server 18 | 19 | FROM node:16-buster-slim as frontend-build 20 | WORKDIR /frontend-build 21 | 22 | COPY package.json . 23 | COPY apps/frontend/package.json ./apps/frontend/package.json 24 | COPY --from=packages /packages . 25 | 26 | RUN yarn 27 | 28 | COPY apps/frontend ./apps/frontend 29 | COPY .eslintrc . 30 | 31 | RUN npm run build:frontend:umbrel 32 | 33 | FROM node:16-buster-slim as backend-build 34 | WORKDIR /backend-build 35 | 36 | COPY package.json . 37 | COPY apps/express/package.json ./apps/express/package.json 38 | COPY --from=packages /packages . 39 | 40 | RUN yarn 41 | 42 | COPY apps/express ./apps/express 43 | # COPY --from=frontend-build /frontend-build/apps/frontend/build ./apps/frontend 44 | 45 | RUN npm run build:express 46 | 47 | FROM node:16-buster-slim as final 48 | WORKDIR /final 49 | 50 | COPY --from=backend-build /backend-build/apps/express/dist ./apps/express/dist 51 | COPY --from=backend-build /backend-build/apps/express/package.json ./apps/express 52 | COPY --from=backend-build /backend-build/node_modules ./node_modules 53 | COPY --from=frontend-build /frontend-build/apps/frontend/build ./apps/frontend 54 | COPY --from=packages /packages . 55 | COPY package.json . 56 | 57 | # Copy over HWI binary 58 | COPY packages/HWIs/HWI_PI ./apps/express/build/HWIs/ 59 | 60 | # Intall HWI dependencies 61 | RUN apt update && apt install libusb-1.0-0 libusb-1.0.0-dev libudev-dev python3-dev -y 62 | 63 | EXPOSE 42069 64 | 65 | CMD ["npm", "run", "express"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lily Wallet 2 | 3 | Secure bitcoin wallet designed for everyone on their journey towards financial freedom. 4 | 5 | ![Screenshot of Lily Wallet](./screenshot.png 'Screenshot of Lily Wallet') 6 | 7 | ### Features 8 | 9 | - Manage hardware wallets, multisignature vaults, and lightning nodes all in one beautiful interface 10 | - Import and Export PSBTs for signing transactions 11 | - Open lightning network channels from funds located in hardware wallets or multisignature vaults 12 | - Retrieve blockchain data from your own instance of Electrum Server 13 | - Stateless: There is no database. The app is self-hosted and populated from a password encrypted configuration file 14 | - Interoperable: Export or import your vault to use in other software like Unchained Capital's Caravan or BlueWallet 15 | - Dark mode 16 | 17 | ### Hardware Wallet Support 18 | 19 | - Coldcard 20 | - Ledger 21 | - Trezor 22 | - Bitbox 02 23 | - Cobo Vault 24 | 25 | ### Contributing 26 | 27 | See [development.md](/docs/development.md) for instructions on how to get a development environment up and running. 28 | 29 | ## License 30 | 31 | Lily Wallet is licensed under the [Elastic 2.0](https://github.com/Lily-Technologies/lily-wallet/blob/master/LICENSE.md) license. TL;DR — You're free to use, fork, modify, and redestribute Lily Wallet so long as it does not disable or circumvent the license key functionality. If you're interested in using Lily Wallet for commercial purposes, please reach out to us at help@lily-wallet.com. 32 | -------------------------------------------------------------------------------- /apps/electron/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.4.0](https://github.com/Lily-Technologies/lily-wallet/compare/electron-v1.3.0...electron-v1.4.0) (2023-07-19) 4 | 5 | 6 | ### Features 7 | 8 | * **Bitgo:** support bitgo vaults ([baece25](https://github.com/Lily-Technologies/lily-wallet/commit/baece25843eb7a294ea3405c517b667121459248)) 9 | 10 | 11 | ### Dependencies 12 | 13 | * The following workspace dependencies were updated 14 | * dependencies 15 | * @lily/shared-server bumped from 1.3.0 to 1.4.0 16 | * @lily/types bumped from 1.3.0 to 1.4.0 17 | 18 | ## 1.3.0 (2022-12-07) 19 | 20 | 21 | ### Features 22 | 23 | * **Lightning:** specify outgoing channel id ([#111](https://github.com/Lily-Technologies/lily-wallet/issues/111)) ([30a9d7c](https://github.com/Lily-Technologies/lily-wallet/commit/30a9d7c05ea01fb238329528a29c9cc755ef4a1b)) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * **Electron:** default electrum endpoint ([45a059b](https://github.com/Lily-Technologies/lily-wallet/commit/45a059b9e794aec4bb9fdaf13c5ac945a645fe64)) 29 | 30 | 31 | ### Dependencies 32 | 33 | * The following workspace dependencies were updated 34 | * dependencies 35 | * @lily/shared-server bumped from * to 1.3.0 36 | * @lily/types bumped from * to 1.3.0 37 | -------------------------------------------------------------------------------- /apps/electron/afterSignHook.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { notarize } = require('electron-notarize'); 3 | 4 | exports.default = async function notarizing(context) { 5 | const { electronPlatformName, appOutDir } = context; 6 | if (electronPlatformName !== 'darwin') { 7 | return; 8 | } 9 | 10 | const appName = context.packager.appInfo.productFilename; 11 | 12 | return await notarize({ 13 | appBundleId: 'com.lily-wallet.lily', 14 | appPath: `${appOutDir}/${appName}.app`, 15 | appleId: process.env.APPLEID, 16 | appleIdPassword: process.env.APPLEIDPASS 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /apps/electron/entitlements.mac.inherit.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.device.camera 10 | 11 | com.apple.security.cs.disable-library-validation 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/electron/src/assets/AppIcon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/electron/src/assets/AppIcon.icns -------------------------------------------------------------------------------- /apps/electron/src/assets/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/electron/src/assets/background.png -------------------------------------------------------------------------------- /apps/electron/src/preload.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { contextBridge, ipcRenderer } from 'electron'; 3 | 4 | contextBridge.exposeInMainWorld('ipcRenderer', { 5 | invoke: (command, args) => ipcRenderer.invoke(command, args), 6 | send: (command, args) => ipcRenderer.send(command, args), 7 | on: (command, args) => ipcRenderer.on(command, args) 8 | }); 9 | //# sourceMappingURL=preload.js.map 10 | -------------------------------------------------------------------------------- /apps/electron/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "outDir": "build", 10 | "baseUrl": ".", 11 | "skipLibCheck": true 12 | }, 13 | "references": [ 14 | { 15 | "path": "../../packages/shared-server" 16 | }, 17 | { 18 | "path": "../../packages/types" 19 | } 20 | ], 21 | "include": ["src/**/*"] 22 | } 23 | -------------------------------------------------------------------------------- /apps/express/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.4.0](https://github.com/Lily-Technologies/lily-wallet/compare/express-v1.3.0...express-v1.4.0) (2023-07-19) 4 | 5 | 6 | ### Features 7 | 8 | * **Bitgo:** support bitgo vaults ([baece25](https://github.com/Lily-Technologies/lily-wallet/commit/baece25843eb7a294ea3405c517b667121459248)) 9 | 10 | 11 | ### Dependencies 12 | 13 | * The following workspace dependencies were updated 14 | * dependencies 15 | * @lily/shared-server bumped from 1.3.0 to 1.4.0 16 | * @lily/types bumped from 1.3.0 to 1.4.0 17 | 18 | ## 1.3.0 (2022-12-07) 19 | 20 | 21 | ### Features 22 | 23 | * **Lightning:** specify outgoing channel id ([#111](https://github.com/Lily-Technologies/lily-wallet/issues/111)) ([30a9d7c](https://github.com/Lily-Technologies/lily-wallet/commit/30a9d7c05ea01fb238329528a29c9cc755ef4a1b)) 24 | 25 | 26 | ### Dependencies 27 | 28 | * The following workspace dependencies were updated 29 | * dependencies 30 | * @lily/shared-server bumped from * to 1.3.0 31 | * @lily/types bumped from * to 1.3.0 32 | -------------------------------------------------------------------------------- /apps/express/README.md: -------------------------------------------------------------------------------- 1 | # express 2 | -------------------------------------------------------------------------------- /apps/express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lily/express", 3 | "version": "1.4.0", 4 | "main": "dist/index.js", 5 | "scripts": { 6 | "build": "tsc", 7 | "start": "node .", 8 | "docker:build": "", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "dependencies": { 12 | "@lily-technologies/lnrpc": "^0.14.1-beta.14", 13 | "@lily/shared-server": "1.4.0", 14 | "axios": "^0.24.0", 15 | "body-parser": "^1.19.1", 16 | "cors": "^2.8.5", 17 | "crypto-js": "^4.1.1", 18 | "dotenv": "^10.0.0", 19 | "express": "^4.17.2", 20 | "lndconnect": "^0.2.10", 21 | "moment": "^2.29.1", 22 | "uuid": "^8.3.2" 23 | }, 24 | "devDependencies": { 25 | "@types/body-parser": "^1.19.2", 26 | "@types/cors": "^2", 27 | "@types/crypto-js": "^4.1.0", 28 | "@types/express": "^4.17.13", 29 | "@types/node": "^17.0.7", 30 | "@types/uuid": "^8.3.4", 31 | "typescript": "^4.5.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/express/src/index.ts: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | import express from 'express'; 3 | import cors from 'cors'; 4 | import bodyParser from 'body-parser'; 5 | import path from 'path'; 6 | 7 | import chartRoutes from './routes/chart'; 8 | import configRoutes from './routes/config'; 9 | import hwiRoutes from './routes/hwi'; 10 | import lightningRoutes from './routes/lightning'; 11 | import onchainRoutes from './routes/onchain'; 12 | import dbRoutes from './routes/db'; 13 | 14 | import { setInitialConfig } from './utils'; 15 | 16 | const app = express(); 17 | app.use(cors()); 18 | app.use(bodyParser.json()); 19 | 20 | process.on('unhandledRejection', (error) => { 21 | console.error('unhandledRejection', error); 22 | }); 23 | 24 | // populates umbrel lnd node info 25 | setInitialConfig(); 26 | 27 | const port = process.env.EXPRESS_PORT; // default port to listen 28 | 29 | const isTestnet = !!('TESTNET' in process.env); 30 | 31 | app.use(function (req, res, next) { 32 | res.header('Access-Control-Allow-Origin', '*'); 33 | res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); 34 | next(); 35 | }); 36 | 37 | // serve frontend 38 | app.use('/', express.static(path.join(__dirname, '../../frontend'))); 39 | 40 | app.get('/bitcoin-network', async (req, res) => { 41 | res.send(isTestnet); 42 | }); 43 | 44 | app.use(chartRoutes); 45 | app.use(configRoutes); 46 | app.use(hwiRoutes); 47 | app.use(lightningRoutes); 48 | app.use(onchainRoutes); 49 | app.use(dbRoutes); 50 | 51 | // start the Express server 52 | app.listen(port, () => { 53 | console.log(`server started on port ${port}`); 54 | }); 55 | -------------------------------------------------------------------------------- /apps/express/src/routes/chart.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { Router } from 'express'; 3 | import moment from 'moment'; 4 | 5 | import { CoindeskCurrentPriceResponse, CoindeskHistoricPriceResponse } from '@lily/types'; 6 | import { sendError } from '../utils'; 7 | 8 | const router = Router(); 9 | 10 | router.get('/current-btc-price', async (req, res) => { 11 | const { data }: { data: CoindeskCurrentPriceResponse } = await axios.get( 12 | 'https://api.coindesk.com/v1/bpi/currentprice.json' 13 | ); 14 | const currentPriceWithCommasStrippedOut = data.bpi.USD.rate.replace(',', ''); 15 | res.send(currentPriceWithCommasStrippedOut); 16 | }); 17 | 18 | router.get('/historical-btc-price', async (req, res) => { 19 | try { 20 | const { data }: { data: CoindeskHistoricPriceResponse } = await axios.get( 21 | `https://api.coindesk.com/v1/bpi/historical/close.json?start=2014-01-01&end=${moment().format( 22 | 'YYYY-MM-DD' 23 | )}` 24 | ); 25 | const historicalBitcoinPrice = data.bpi; 26 | let priceForChart: { price: number; date: string }[] = []; 27 | for (let i = 0; i < Object.keys(historicalBitcoinPrice).length; i++) { 28 | priceForChart.push({ 29 | price: Object.values(historicalBitcoinPrice)[i], 30 | date: Object.keys(historicalBitcoinPrice)[i] 31 | }); 32 | } 33 | res.send(priceForChart); 34 | } catch (e) { 35 | sendError(res, e); 36 | } 37 | }); 38 | 39 | export default router; 40 | -------------------------------------------------------------------------------- /apps/express/src/routes/config.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | import { getFile, saveFile } from '@lily/shared-server'; 4 | 5 | const APP_DATA_DIRECTORY = process.env.APP_DATA_DIR; 6 | const CONFIG_FILE_NAME = 'lily-config-encrypted.txt'; 7 | 8 | const router = Router(); 9 | 10 | router.get('/get-config', async (req, res) => { 11 | try { 12 | const file = await getFile(CONFIG_FILE_NAME, APP_DATA_DIRECTORY); 13 | res.send(JSON.stringify(file)); 14 | } catch (e) { 15 | console.log('Failed to get Lily config'); 16 | } 17 | }); 18 | 19 | router.post('/save-config', async (req, res) => { 20 | const { encryptedConfigFile } = req.body; 21 | await saveFile(encryptedConfigFile, CONFIG_FILE_NAME, APP_DATA_DIRECTORY); 22 | }); 23 | 24 | export default router; 25 | -------------------------------------------------------------------------------- /apps/express/src/routes/download.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | const router = Router(); 4 | 5 | router.post('/download-item', async (req, res) => { 6 | const { data, filename } = req.body; 7 | try { 8 | res.set({ 9 | 'Content-Disposition': `attachment; filename=${filename}`, 10 | 'Content-Type': 'text/plain' 11 | }); 12 | res.send(data); 13 | } catch (e) { 14 | console.log(`Failed to download ${filename}`); 15 | } 16 | }); 17 | 18 | export default router; 19 | -------------------------------------------------------------------------------- /apps/express/src/routes/hwi.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | import { enumerate, getXPub, sendpin, promptpin, signtx } from '@lily/shared-server'; 4 | import { HwiEnumerateResponse } from '@lily/types'; 5 | 6 | import { sendError } from '../utils'; 7 | 8 | const router = Router(); 9 | 10 | const isTestnet = !!('TESTNET' in process.env); 11 | 12 | router.get('/enumerate', async (req, res) => { 13 | try { 14 | const resp = JSON.parse(await enumerate()); 15 | if (resp.error) { 16 | sendError(res, 'Error finding devices'); 17 | } 18 | const filteredDevices = (resp as HwiEnumerateResponse[]).filter((device) => { 19 | return ( 20 | device.type === 'coldcard' || 21 | device.type === 'ledger' || 22 | device.type === 'trezor' || 23 | device.type === 'bitbox02' 24 | ); 25 | }); 26 | res.send(filteredDevices); 27 | } catch (e) { 28 | console.log('/enumerate error: ', e); 29 | sendError(res, e); 30 | } 31 | }); 32 | 33 | router.post('/xpub', async (req, res) => { 34 | const { deviceType, devicePath, path } = req.body; 35 | const resp = JSON.parse(await getXPub(deviceType, devicePath, path, isTestnet)); // responses come back as strings, need to be parsed 36 | if (resp.error) { 37 | sendError(res, 'Error getting xpub'); 38 | } 39 | res.send(resp); 40 | }); 41 | 42 | router.post('/sign', async (req, res) => { 43 | const { deviceType, devicePath, psbt } = req.body; 44 | const resp = JSON.parse(await signtx(deviceType, devicePath, psbt, isTestnet)); 45 | if (resp.error) { 46 | sendError(res, 'Error signing transaction'); 47 | } 48 | res.send(resp); 49 | }); 50 | 51 | router.post('/promptpin', async (req, res) => { 52 | const { deviceType, devicePath } = req.body; 53 | const resp = JSON.parse(await promptpin(deviceType, devicePath)); 54 | if (resp.error) { 55 | console.log('/promptpin e: ', resp); 56 | sendError(res, 'Error prompting pin'); 57 | } 58 | res.send(resp); 59 | }); 60 | 61 | router.post('/sendpin', async (req, res) => { 62 | const { deviceType, devicePath, pin } = req.body; 63 | const resp = JSON.parse(await sendpin(deviceType, devicePath, pin)); 64 | if (resp.error) { 65 | sendError(res, 'Error sending pin'); 66 | } 67 | res.send(resp); 68 | }); 69 | 70 | export default router; 71 | -------------------------------------------------------------------------------- /apps/express/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express'; 2 | 3 | export const sendError = (res: Response, message: string, code: number = 500) => { 4 | res.status(code).json({ 5 | message 6 | }); 7 | }; 8 | 9 | export * from './setInitialConfig'; 10 | -------------------------------------------------------------------------------- /apps/express/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "outDir": "dist", 10 | "baseUrl": ".", 11 | "skipLibCheck": true 12 | }, 13 | "references": [ 14 | { 15 | "path": "../../packages/shared-server" 16 | }, 17 | { 18 | "path": "../../packages/types" 19 | } 20 | ], 21 | "include": ["src/**/*"] 22 | } 23 | -------------------------------------------------------------------------------- /apps/frontend/.env.electron: -------------------------------------------------------------------------------- 1 | #Frontend 2 | 3 | REACT_APP_KEYSERVER_SIGNING_ADDRESS=bc1qujxmhy8ajeqlzhynvsfc2cv9aue9mypzv872f3 4 | REACT_APP_LILY_ENDPOINT="https://lily-server.herokuapp.com" 5 | 6 | REACT_APP_IS_ELECTRON=true 7 | GENERATE_SOURCEMAP=false -------------------------------------------------------------------------------- /apps/frontend/.env.umbrel: -------------------------------------------------------------------------------- 1 | REACT_APP_KEYSERVER_SIGNING_ADDRESS=bc1qujxmhy8ajeqlzhynvsfc2cv9aue9mypzv872f3 2 | REACT_APP_LILY_ENDPOINT="https://lily-server.herokuapp.com" 3 | 4 | REACT_APP_BACKEND_HOST=http://umbrel.local 5 | REACT_APP_BACKEND_PORT=5000 -------------------------------------------------------------------------------- /apps/frontend/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.4.0](https://github.com/Lily-Technologies/lily-wallet/compare/frontend-v1.3.0...frontend-v1.4.0) (2023-07-19) 4 | 5 | 6 | ### Features 7 | 8 | * **Bitgo:** support bitgo vaults ([baece25](https://github.com/Lily-Technologies/lily-wallet/commit/baece25843eb7a294ea3405c517b667121459248)) 9 | 10 | ## 1.3.0 (2022-12-07) 11 | 12 | 13 | ### Features 14 | 15 | * **Lightning:** specify outgoing channel id ([#111](https://github.com/Lily-Technologies/lily-wallet/issues/111)) ([30a9d7c](https://github.com/Lily-Technologies/lily-wallet/commit/30a9d7c05ea01fb238329528a29c9cc755ef4a1b)) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * **Setup, Hardware Wallet:** import from file ([dad413c](https://github.com/Lily-Technologies/lily-wallet/commit/dad413c438f8ff835e45f9b047056db23b1ca514)) 21 | * **Vault, Empty State:** fix padding, remove double empty state ([e074f26](https://github.com/Lily-Technologies/lily-wallet/commit/e074f26d44f1cd5338c7409d9ba40628a855b8e6)) 22 | 23 | 24 | ### Dependencies 25 | 26 | * The following workspace dependencies were updated 27 | * dependencies 28 | * @lily/types bumped from * to 1.3.0 29 | -------------------------------------------------------------------------------- /apps/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-buster-slim as install 2 | 3 | WORKDIR /install 4 | # TODO: this should only copy package.jsons in appropriate folders 5 | COPY . . 6 | 7 | # Use yarn for correct webpack version hoisting 8 | RUN yarn 9 | 10 | FROM node:16-buster-slim as build 11 | WORKDIR /build 12 | 13 | COPY --from=install /install . 14 | 15 | ENV GENERATE_SOURCEMAP false 16 | 17 | # ARG EXPRESS_PORT 18 | # ENV EXPRESS_PORT $EXPRESS_PORT 19 | 20 | # ARG REACT_APP_KEYSERVER_SIGNING_ADDRESS 21 | # ENV REACT_APP_KEYSERVER_SIGNING_ADDRESS $REACT_APP_KEYSERVER_SIGNING_ADDRESS 22 | 23 | # ARG REACT_APP_LILY_ENDPOINT 24 | # ENV REACT_APP_LILY_ENDPOINT $REACT_APP_LILY_ENDPOINT 25 | 26 | # ARG REACT_APP_BACKEND_HOST 27 | # ENV REACT_APP_BACKEND_HOST $REACT_APP_BACKEND_HOST 28 | 29 | # ARG REACT_APP_BACKEND_PORT 30 | # ENV REACT_APP_BACKEND_PORT $REACT_APP_BACKEND_PORT 31 | 32 | 33 | RUN npm run build:types 34 | RUN npm run build:frontend:umbrel 35 | 36 | # # production environment 37 | FROM nginx:stable-alpine 38 | WORKDIR /app 39 | COPY --from=build build/apps/frontend/build /usr/share/nginx/html 40 | EXPOSE 80 41 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /apps/frontend/craco.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | style: { 3 | postcss: { 4 | plugins: [require('tailwindcss'), require('autoprefixer')] 5 | } 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /apps/frontend/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": { 3 | "componentFolder": "src", 4 | "testFiles": "**/*spec.{js,jsx,ts,tsx}" 5 | }, 6 | "viewportWidth": 1800, 7 | "viewportHeight": 1000 8 | } 9 | -------------------------------------------------------------------------------- /apps/frontend/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /apps/frontend/cypress/integration/Lightning/receive.spec.js: -------------------------------------------------------------------------------- 1 | import { Lightning } from "../../../src/__tests__/fixtures"; 2 | import { getNewInvoice } from "../../support/getNewInvoice"; 3 | 4 | describe("Receive", () => { 5 | beforeEach(() => { 6 | cy.login(); 7 | }); 8 | it("displays a receive invoice", () => { 9 | const INVOICE_AMOUNT = 2500; 10 | const newInvoice = getNewInvoice(INVOICE_AMOUNT, 60); 11 | 12 | cy.window().then((win) => { 13 | win.ipcRenderer.invoke 14 | .withArgs("/lightning-invoice") 15 | .returns({ 16 | paymentRequest: newInvoice.paymentRequest, 17 | }) 18 | .as("/lightning-invoice"); 19 | }); 20 | 21 | cy.contains("Receive").click(); 22 | cy.get("nav").contains(Lightning.config.name).click(); 23 | 24 | cy.get("#lightning-memo").type("Testing lily wallet"); 25 | 26 | cy.get("#lightning-amount").type(INVOICE_AMOUNT); 27 | 28 | cy.contains("Generate invoice").click(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /apps/frontend/cypress/integration/Lightning/send.spec.js: -------------------------------------------------------------------------------- 1 | /* global cy */ 2 | 3 | import { Multisig, Lightning } from "../../../src/__tests__/fixtures"; 4 | import { getNewInvoice } from "../../support/getNewInvoice"; 5 | describe("Send - Lightning", () => { 6 | beforeEach(() => { 7 | cy.login(); 8 | }); 9 | it("sends a transaction", () => { 10 | cy.intercept("POST", "https://blockstream.info/api/tx", (req) => { 11 | req.reply("abc123"); 12 | }); 13 | 14 | const paymentRequest = getNewInvoice(25000).paymentRequest; 15 | 16 | cy.window() 17 | .then((win) => { 18 | win.ipcRenderer.on.withArgs("/lightning-send-payment").returns({ 19 | status: 2, 20 | }); 21 | }) 22 | .as("/lightning-send-payment"); 23 | 24 | cy.window() 25 | .then((win) => { 26 | win.ipcRenderer.on 27 | .withArgs("/lightning-send-payment") 28 | .callsFake((args, args1) => { 29 | // setTimeout so that initialAccountMap can get set 30 | // this mimicks a delay for constructing the accountMap 31 | setTimeout(() => { 32 | const response = { 33 | status: 2, 34 | paymentRequest: paymentRequest, 35 | valueSats: paymentRequest.amount, 36 | }; 37 | args1(undefined, response); 38 | }, 1); 39 | }); 40 | }) 41 | .as("/lightning-send-payment"); 42 | 43 | cy.contains("Send").click(); 44 | 45 | cy.get("nav").contains(Lightning.config.name).click(); 46 | 47 | cy.get("#lightning-invoice").type(paymentRequest); 48 | 49 | cy.contains("Preview transaction").click(); 50 | 51 | cy.contains("Payment summary").should("be.visible"); 52 | 53 | cy.contains("Send payment").click(); 54 | 55 | cy.contains("Payment success").should("be.visible"); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /apps/frontend/cypress/integration/Receive/receive.spec.js: -------------------------------------------------------------------------------- 1 | /* global cy */ 2 | 3 | import { Multisig } from "../../../src/__tests__/fixtures"; 4 | 5 | describe("Receive", () => { 6 | beforeEach(() => { 7 | cy.login(); 8 | }); 9 | it("displays a receive address", () => { 10 | cy.contains("Receive").click(); 11 | cy.get("nav").contains(Multisig.config.name).click(); 12 | cy.contains(Multisig.unusedAddresses[0].address).should("be.visible"); 13 | }); 14 | 15 | it("can generate a new receive address", () => { 16 | cy.contains("Receive").click(); 17 | cy.get("nav").contains(Multisig.config.name).click(); 18 | cy.contains(Multisig.unusedAddresses[0].address).should("be.visible"); 19 | cy.contains("Generate New Address").click(); 20 | cy.contains(Multisig.unusedAddresses[1].address).should("be.visible"); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/frontend/cypress/integration/Settings/settings.spec.js: -------------------------------------------------------------------------------- 1 | /* global cy */ 2 | 3 | import { Multisig } from "../../../src/__tests__/fixtures"; 4 | 5 | describe("Settings", () => { 6 | beforeEach(() => { 7 | cy.login(); 8 | }); 9 | it("organizes the pages into tabs", () => { 10 | cy.contains("Settings").click(); 11 | 12 | cy.contains("Network configuration").should("be.visible"); 13 | 14 | cy.get("nav#settings-navigation").contains("Backup").click(); 15 | cy.contains("Configuration File").should("be.visible"); 16 | 17 | cy.get("nav#settings-navigation").contains("About").click(); 18 | cy.contains("About Lily Wallet").should("be.visible"); 19 | }); 20 | 21 | it("allows user to input custom node connection data", () => { 22 | const HOST = "https://myfake.host:8337"; 23 | const USERNAME = "SATOSHI"; 24 | const PASSWORD = "P2P"; 25 | 26 | const CURRENT_BLOCK_HEIGHT = 684085; 27 | const PROVIDER = "Custom Node"; 28 | 29 | cy.window().then((win) => { 30 | win.ipcRenderer.invoke 31 | .withArgs("/changeNodeConfig") 32 | .returns({ 33 | blocks: CURRENT_BLOCK_HEIGHT, 34 | initialblockdownload: false, 35 | provider: PROVIDER, 36 | baseURL: HOST, 37 | connected: true, 38 | }) 39 | .as("changeNodeConfig"); 40 | }); 41 | 42 | cy.contains("Settings").click(); 43 | 44 | cy.contains("Network configuration").should("be.visible"); 45 | 46 | cy.contains("Change data source").click(); 47 | cy.contains("Connect to specific node").click(); 48 | 49 | cy.get("input#node-host").type(HOST); 50 | cy.get("input#node-username").type(USERNAME); 51 | cy.get("input#node-password").type(PASSWORD); 52 | 53 | cy.contains("Connect to node").click(); 54 | 55 | cy.contains(CURRENT_BLOCK_HEIGHT.toLocaleString()).should("be.visible"); 56 | cy.contains(HOST).should("be.visible"); 57 | cy.contains("Connected").should("be.visible"); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /apps/frontend/cypress/integration/Setup/Mnemonic.spec.js: -------------------------------------------------------------------------------- 1 | /* global cy */ 2 | 3 | describe("Mnemonic", () => { 4 | it("creates a new mnemonic wallet", () => { 5 | const ACCOUNT_NAME = "My Mnemonic Wallet"; 6 | 7 | cy.login(); 8 | 9 | cy.contains("Add a new account").click(); 10 | 11 | cy.get("#page-wrapper").find("#options-menu").click(); 12 | 13 | cy.contains("New Software Wallet").click(); 14 | 15 | cy.get("input").type(ACCOUNT_NAME); 16 | 17 | cy.contains("Continue").click(); 18 | 19 | cy.contains("I have written these words down").click(); 20 | 21 | cy.contains("View Accounts").click(); 22 | 23 | cy.contains(ACCOUNT_NAME).click(); 24 | 25 | cy.get("[data-cy=settings]").click(); 26 | 27 | cy.contains("Lily").should("be.visible"); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /apps/frontend/cypress/integration/Setup/setup.spec.js: -------------------------------------------------------------------------------- 1 | /* global cy */ 2 | 3 | describe("Setup", () => { 4 | describe("General", () => { 5 | it("displays no devices detected when enumerate returns an empty array", () => { 6 | const ACCOUNT_NAME = "No devices"; 7 | 8 | cy.login(); 9 | 10 | cy.window().then((win) => { 11 | win.ipcRenderer.invoke 12 | .withArgs("/enumerate") 13 | .returns([]) 14 | .as("Enumerate"); 15 | }); 16 | 17 | cy.contains("Add a new account").click(); 18 | 19 | cy.contains("Hardware Wallet").click(); 20 | 21 | cy.get("input").type(ACCOUNT_NAME); 22 | 23 | cy.contains("Continue").click(); 24 | 25 | cy.contains("No devices detected").should("be.visible"); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /apps/frontend/cypress/integration/Support/Support.spec.js: -------------------------------------------------------------------------------- 1 | /* global cy */ 2 | import { Multisig } from "../../../src/__tests__/fixtures"; 3 | describe("Support", () => { 4 | it("shows a valid support code when accessing support portal with purchased license", () => { 5 | cy.intercept("POST", "**/support", (req) => { 6 | req.reply({ 7 | code: "abc12", 8 | }); 9 | }).as("getSupportCode"); 10 | 11 | cy.login(); 12 | 13 | cy.get("button#options-menu").eq(1).click(); 14 | cy.contains("Support").click(); 15 | 16 | cy.contains("Lily Support Portal").should("be.visible"); 17 | 18 | cy.get("[data-cy=support-code]").contains("a").should("be.visible"); 19 | cy.get("[data-cy=support-code]").contains("b").should("be.visible"); 20 | cy.get("[data-cy=support-code]").contains("c").should("be.visible"); 21 | cy.get("[data-cy=support-code]").contains("1").should("be.visible"); 22 | cy.get("[data-cy=support-code]").contains("2").should("be.visible"); 23 | }); 24 | 25 | it("doesnt show a support code when accessing support portal without a purchased license", () => { 26 | cy.intercept("POST", "**/support", (req) => { 27 | req.reply({ 28 | statusCode: 401, 29 | body: "Invalid license", 30 | }); 31 | }).as("getSupportCode"); 32 | 33 | cy.login(); 34 | 35 | cy.get("button#options-menu").eq(1).click(); 36 | cy.contains("Support").click(); 37 | 38 | cy.contains("Lily Support Portal").should("be.visible"); 39 | 40 | cy.get("[data-cy=support-code]").contains("a").should("not.exist"); 41 | cy.get("[data-cy=support-code]").contains("b").should("not.exist"); 42 | cy.get("[data-cy=support-code]").contains("c").should("not.exist"); 43 | cy.get("[data-cy=support-code]").contains("1").should("not.exist"); 44 | cy.get("[data-cy=support-code]").contains("2").should("not.exist"); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /apps/frontend/cypress/integration/Vaults/Vault.spec.js: -------------------------------------------------------------------------------- 1 | /* global cy */ 2 | 3 | import { Multisig } from "../../../src/__tests__/fixtures"; 4 | 5 | describe("Vault - General", () => { 6 | beforeEach(() => { 7 | cy.login(); 8 | }); 9 | 10 | it("can view transaction details", () => { 11 | cy.get("[data-cy=nav-item]").contains(Multisig.config.name).click(); 12 | 13 | cy.contains(Multisig.transactions[0].address).click(); 14 | 15 | cy.contains("Transaction Details").should("be.visible"); 16 | 17 | cy.contains(Multisig.transactions[0].txid).should("be.visible"); 18 | }); 19 | 20 | it("can open tx in blockstream explorer", () => { 21 | cy.window().then((win) => { 22 | cy.stub(win, "open").as("viewInExplorer"); 23 | }); 24 | 25 | cy.get("[data-cy=nav-item]").contains(Multisig.config.name).click(); 26 | 27 | cy.contains(Multisig.transactions[0].address).click(); 28 | 29 | cy.contains("Transaction Details").should("be.visible"); 30 | 31 | cy.contains("View on Blockstream").click(); 32 | 33 | cy.get("@viewInExplorer").should( 34 | "be.calledWith", 35 | "https://blockstream.info/tx/6a37b8a2b06ad68fc5817b728aec1e2509a2b3195d4a62a147d58a8125d2ef33", 36 | "_blank", 37 | "nodeIntegration=no" 38 | ); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /apps/frontend/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | // eslint-disable-next-line no-unused-vars 19 | module.exports = (on, config) => { 20 | require("@cypress/code-coverage/task")(on, config); 21 | // `on` is used to hook into various events Cypress emits 22 | // `config` is the resolved Cypress config 23 | 24 | on("task", { 25 | log(message) { 26 | console.log(message); 27 | return null; 28 | }, 29 | }); 30 | 31 | if (config.testingType === "component") { 32 | require("@cypress/react/plugins/react-scripts")(on, config); 33 | } 34 | 35 | const { renameSync } = require("fs"); 36 | 37 | on("after:screenshot", ({ path }) => { 38 | renameSync(path, path.replace(/ \(\d*\)/i, "")); 39 | }); 40 | 41 | return config; 42 | }; 43 | -------------------------------------------------------------------------------- /apps/frontend/cypress/support/createConfig.js: -------------------------------------------------------------------------------- 1 | import { EMPTY_CONFIG } from "../../src/ConfigContext"; 2 | import { AES } from "crypto-js"; 3 | 4 | import { 5 | Multisig, 6 | Mnemonic, 7 | HWW, 8 | Lightning, 9 | } from "../../src/__tests__/fixtures"; 10 | 11 | export const createLilyAccount = (Account) => { 12 | return { 13 | id: Account.config.id, 14 | name: Account.config.name, 15 | config: Account.config, 16 | transactions: Account.transactions, 17 | addresses: Account.addresses, 18 | unusedAddresses: Account.unusedAddresses, 19 | changeAddresses: Account.changeAddresses, 20 | unusedChangeAddresses: Account.unusedChangeAddresses, 21 | availableUtxos: Account.availableUtxos, 22 | }; 23 | }; 24 | 25 | export const createConfig = (password) => { 26 | const configFile = { 27 | ...EMPTY_CONFIG, 28 | isEmpty: false, 29 | wallets: [Mnemonic.config, HWW.account.config], 30 | vaults: [Multisig.config], 31 | lightning: [Lightning.config], 32 | }; 33 | 34 | return AES.encrypt(JSON.stringify(configFile), password).toString(); 35 | }; 36 | -------------------------------------------------------------------------------- /apps/frontend/cypress/support/getNewInvoice.js: -------------------------------------------------------------------------------- 1 | import { encode, sign } from "bolt11"; 2 | import moment from "moment"; 3 | 4 | export const getNewInvoice = (amount, expirationInSeconds) => { 5 | const expirationTime = moment().add(expirationInSeconds, "seconds").unix(); 6 | 7 | const encodedInvoice = encode({ 8 | satoshis: amount, 9 | timestamp: expirationTime, 10 | tags: [ 11 | { 12 | tagName: "payment_hash", 13 | data: "100102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f", 14 | }, 15 | { 16 | tagName: "description", 17 | data: "Please consider supporting this project", 18 | }, 19 | ], 20 | }); 21 | 22 | const privateKeyHex = 23 | "e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734"; 24 | const signed = sign(encodedInvoice, privateKeyHex); 25 | 26 | return signed; 27 | }; 28 | -------------------------------------------------------------------------------- /apps/frontend/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import "@cypress/code-coverage/support"; 18 | import "./commands"; 19 | 20 | // Alternatively you can use CommonJS syntax: 21 | // require('./commands') 22 | -------------------------------------------------------------------------------- /apps/frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /apps/frontend/public/AppIcon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/public/AppIcon.icns -------------------------------------------------------------------------------- /apps/frontend/public/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/public/background.png -------------------------------------------------------------------------------- /apps/frontend/public/entitlements.mac.inherit.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | com.apple.security.device.camera 8 | 9 | 10 | -------------------------------------------------------------------------------- /apps/frontend/public/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/public/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /apps/frontend/public/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/public/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /apps/frontend/public/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/public/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /apps/frontend/public/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /apps/frontend/public/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/public/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /apps/frontend/public/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/public/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /apps/frontend/public/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/public/favicons/favicon.ico -------------------------------------------------------------------------------- /apps/frontend/public/favicons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/public/favicons/mstile-144x144.png -------------------------------------------------------------------------------- /apps/frontend/public/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/public/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /apps/frontend/public/favicons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/public/favicons/mstile-310x150.png -------------------------------------------------------------------------------- /apps/frontend/public/favicons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/public/favicons/mstile-310x310.png -------------------------------------------------------------------------------- /apps/frontend/public/favicons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/public/favicons/mstile-70x70.png -------------------------------------------------------------------------------- /apps/frontend/public/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /apps/frontend/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/public/icon.png -------------------------------------------------------------------------------- /apps/frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "CCK", 3 | "name": "Colcard Kitchen", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } -------------------------------------------------------------------------------- /apps/frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /apps/frontend/public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/public/screenshot.png -------------------------------------------------------------------------------- /apps/frontend/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/fixtures/DAS/DAS-Account.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "a713a6c7-29f0-44c4-b171-6ace35eb2eb9", 3 | "created_at": 1620691617778, 4 | "name": "DAS-CC", 5 | "network": "mainnet", 6 | "addressType": "p2sh", 7 | "type": "onchain", 8 | "quorum": { 9 | "requiredSigners": 1, 10 | "totalSigners": 1 11 | }, 12 | "extendedPublicKeys": [ 13 | { 14 | "id": "9957d83e-e3a3-46a9-b562-e5f034c901d7", 15 | "created_at": 1620691617778, 16 | "network": "mainnet", 17 | "bip32Path": "m/49'/0'/0'", 18 | "xpub": "xpub6Ciyack9bAGpopvxjuQywxeZM4HKTtfKXiY33FPynmQGDFU1mX2ySWC6XeT4DNVMMRcKd9bjBtZzGUHhJfLbUrwf4aQcapnAUeMTqzqwX8E", 19 | "parentFingerprint": "34ecf56b", 20 | "device": { 21 | "type": "coldcard", 22 | "model": "coldcard", 23 | "fingerprint": "34ecf56b" 24 | } 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/fixtures/DAS/DAS-Transactions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "txid": "5d521a815490726e5d8ef26aa4885f4883cfc487309a8bff47273781ecfe2b1c", 4 | "version": 2, 5 | "locktime": 0, 6 | "vin": [ 7 | { 8 | "txid": "9210df3e75134d66fc9e229c0eb7af3c3e145094847650a834076fb12636316b", 9 | "vout": 0, 10 | "prevout": { 11 | "scriptpubkey": "a914824669775c0691fc0540bb3bd3ad2f24323de4f987", 12 | "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 824669775c0691fc0540bb3bd3ad2f24323de4f9 OP_EQUAL", 13 | "scriptpubkey_type": "p2sh", 14 | "scriptpubkey_address": "3DZr7nsSz3UFe8DS8XbtrokL66vrroGRFZ", 15 | "value": 10000 16 | }, 17 | "scriptsig": "160014f2916c5680da285a22163db5f185dea143284342", 18 | "scriptsig_asm": "OP_PUSHBYTES_22 0014f2916c5680da285a22163db5f185dea143284342", 19 | "witness": [ 20 | "3045022100ffcd8424f7bffa192885e2868da67e1e1902e0d06d74176967888ce6d7fef51002203779d03d8dd62d6c73e6ebef8cb60d910ef643e654d0735cc9e8a4beb8138b9f01", 21 | "02c337dea513af82c08256751c0ba2d7a081a7e3622d31b7096043f39568a37684" 22 | ], 23 | "is_coinbase": false, 24 | "sequence": 4294967295, 25 | "inner_redeemscript_asm": "OP_0 OP_PUSHBYTES_20 f2916c5680da285a22163db5f185dea143284342", 26 | "isChange": false, 27 | "isMine": false 28 | } 29 | ], 30 | "vout": [ 31 | { 32 | "scriptpubkey": "a9149b203520d92ca8530cbd3626dc5a8bcbe46e952387", 33 | "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 9b203520d92ca8530cbd3626dc5a8bcbe46e9523 OP_EQUAL", 34 | "scriptpubkey_type": "p2sh", 35 | "scriptpubkey_address": "3FqFF6XPWHW1MGjJ7LcCGTCQbBkbQMNZav", 36 | "value": 9000, 37 | "isChange": false, 38 | "isMine": true 39 | } 40 | ], 41 | "size": 216, 42 | "weight": 534, 43 | "fee": 1000, 44 | "status": { 45 | "confirmed": true, 46 | "block_height": 660023, 47 | "block_hash": "000000000000000000070f733a7938515c60e693fb07c46538a9fcff5d1c6830", 48 | "block_time": 1607145038 49 | }, 50 | "type": "received", 51 | "totalValue": 9000, 52 | "address": "3FqFF6XPWHW1MGjJ7LcCGTCQbBkbQMNZav", 53 | "value": 9000 54 | } 55 | ] 56 | -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/fixtures/DAS/DAS-other-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "bip84xpub": "xpub6DWEa9prB8ccUrzTYMpMioqQvR8aygsF1wp3NWsoeyymMwnVe6atoXAaRM6JmzWZJzsu7SPuqwSBjwTc3JRSq3oCBDia4ZoweQvz48wQZd9" 3 | } 4 | -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/fixtures/HWW/HWW-Account.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Coldcard HWW", 3 | "config": { 4 | "id": "af46ea03-226f-4f76-961d-65deadfdf5df3", 5 | "type": "onchain", 6 | "created_at": 1603900550961, 7 | "name": "Coldcard HWW", 8 | "network": "mainnet", 9 | "addressType": "p2sh", 10 | "quorum": { 11 | "requiredSigners": 1, 12 | "totalSigners": 1 13 | }, 14 | "extendedPublicKeys": [ 15 | { 16 | "id": "5376cf19-5b44-442a-be76-5c137eda0078", 17 | "created_at": 1603900550960, 18 | "parentFingerprint": "4F60D1C9", 19 | "network": "mainnet", 20 | "bip32Path": "m/49'/0'/0'", 21 | "xpub": "xpub6F2wuvSo8gSRjE9JsMgSva9cDZGa2Hh9SEJ9yczCLd1q2SRFV6N4vRUKFoecbatfhgZcG5rNwTxygNLoPrKpjRt94czCzQQPnoVY1RauiL6", 22 | "device": { 23 | "type": "coldcard", 24 | "model": "coldcard", 25 | "fingerprint": "4F60D1C9" 26 | } 27 | } 28 | ] 29 | }, 30 | "transactions": [], 31 | "unusedAddresses": [], 32 | "addresses": [], 33 | "changeAddresses": [], 34 | "availableUtxos": [], 35 | "unusedChangeAddresses": [], 36 | "currentBalance": 0, 37 | "loading": true 38 | } 39 | -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/fixtures/JB/JB-Config.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "2ad7dfab-70c0-4343-abbd-fa91a7a73ae4", 3 | "created_at": 1620781853571, 4 | "name": "JB HWW", 5 | "type": "onchain", 6 | "network": "mainnet", 7 | "addressType": "P2WPKH", 8 | "quorum": { 9 | "requiredSigners": 1, 10 | "totalSigners": 1 11 | }, 12 | "extendedPublicKeys": [ 13 | { 14 | "id": "23696ec1-b9ed-47d3-818b-e76c97404501", 15 | "created_at": 1620781853571, 16 | "network": "mainnet", 17 | "bip32Path": "m/84'/0'/0'", 18 | "xpub": "xpub6DAZ8xoRdTSw7sBTvFcDc22qS1pwbDiGqfjm15wQ5w1VSYpm5a5NCuZPhDpJRcSRTWUeL4KjyY32jKPX5eX1wTjLTRCJEdpAtVX7Z8b26Xs", 19 | "parentFingerprint": "9130c3d6", 20 | "device": { 21 | "type": "coldcard", 22 | "model": "coldcard", 23 | "fingerprint": "9130c3d6" 24 | } 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/fixtures/JB/JB-UTXOs.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/fixtures/JB/JB-other-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "bip49xpub": "xpub6CvE3hAxrLPY7PS5Km3Gt5aV6dvm5N7j9JNQWEnJfEq5scjYYkx1u7YYVCkjuAdZ6cJGbrZXzSAxYEjqrnauU28P7mACJfXkZEiH8pG4BAM" 3 | } 4 | -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/fixtures/Lightning/Lightning-BalanceHistory.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "block_time": 1614381391, 4 | "totalValue": 0 5 | }, 6 | { 7 | "block_time": 1614381392, 8 | "totalValue": 123456 9 | }, 10 | { 11 | "block_time": 1619029148, 12 | "totalValue": 173456 13 | }, 14 | { 15 | "block_time": 1629240340, 16 | "totalValue": 1173456 17 | }, 18 | { 19 | "block_time": 1629240562, 20 | "totalValue": 1094563 21 | }, 22 | { 23 | "block_time": 1629434810, 24 | "totalValue": 594563 25 | }, 26 | { 27 | "block_time": 1629437648, 28 | "totalValue": 394563 29 | }, 30 | { 31 | "block_time": 1629508310, 32 | "totalValue": 10394563 33 | }, 34 | { 35 | "block_time": 1630096053, 36 | "totalValue": 10344563 37 | }, 38 | { 39 | "block_time": 1630345815, 40 | "totalValue": 10344563 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/fixtures/Lightning/Lightning-ClosedChannels.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "resolutions": [], 4 | "channel_point": "364980c4abe62f0134a637d3b6af9dab6e994740b648de099802ad6aa0c654cf:0", 5 | "chan_id": 739241249851244500, 6 | "chain_hash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", 7 | "closing_tx_hash": "583ebc5a220ec65c0eb5a0a6de3d957e65cd1f5146e46dd66f583400ed7c652f", 8 | "remote_pubkey": "02afb0e4d55bc00a5526744fef34b2b257cdfd57fa9ceb53caa9b182e648f72a4e", 9 | "capacity": 123456, 10 | "close_height": 697868, 11 | "settled_balance": 50000, 12 | "time_locked_balance": 0, 13 | "close_type": "COOPERATIVE_CLOSE", 14 | "open_initiator": "INITIATOR_REMOTE", 15 | "close_initiator": "INITIATOR_LOCAL" 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/fixtures/Lightning/Lightning-Config.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "6178d929-9799-4cd0-9321-2f44c51aa258", 3 | "type": "lightning", 4 | "created_at": 1629983321642, 5 | "name": "Umbrel", 6 | "network": "mainnet", 7 | "connectionDetails": { 8 | "lndConnectUri": "lndconnect://umbrel.local:10009?cert=foobar&macaroon=jones" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/fixtures/Lightning/Lightning-CurrentBalance.json: -------------------------------------------------------------------------------- 1 | { 2 | "balance": 10218005, 3 | "pending_open_balance": 0, 4 | "local_balance": { 5 | "sat": 10218005, 6 | "msat": 10218005000 7 | }, 8 | "remote_balance": { 9 | "sat": 780085, 10 | "msat": 780085000 11 | }, 12 | "unsettled_local_balance": { 13 | "sat": 0, 14 | "msat": 0 15 | }, 16 | "unsettled_remote_balance": { 17 | "sat": 0, 18 | "msat": 0 19 | }, 20 | "pending_open_local_balance": { 21 | "sat": 0, 22 | "msat": 0 23 | }, 24 | "pending_open_remote_balance": { 25 | "sat": 0, 26 | "msat": 0 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/fixtures/Lightning/Lightning-Info.json: -------------------------------------------------------------------------------- 1 | { 2 | "uris": [ 3 | "039c013ca5ea646253b9e7c530d630f81d443cb32d42c8a38e8cc0599ba2dfd11f@b4k3lrt66vuu563wig6rvwojnqoucdc62zhgtj5n5ctiqcoobtebziyd.onion:9735" 4 | ], 5 | "chains": [ 6 | { 7 | "chain": "bitcoin", 8 | "network": "mainnet" 9 | } 10 | ], 11 | "features": { 12 | "0": { 13 | "name": "data-loss-protect", 14 | "is_required": true, 15 | "is_known": true 16 | }, 17 | "5": { 18 | "name": "upfront-shutdown-script", 19 | "is_required": false, 20 | "is_known": true 21 | }, 22 | "7": { 23 | "name": "gossip-queries", 24 | "is_required": false, 25 | "is_known": true 26 | }, 27 | "9": { 28 | "name": "tlv-onion", 29 | "is_required": false, 30 | "is_known": true 31 | }, 32 | "12": { 33 | "name": "static-remote-key", 34 | "is_required": true, 35 | "is_known": true 36 | }, 37 | "14": { 38 | "name": "payment-addr", 39 | "is_required": true, 40 | "is_known": true 41 | }, 42 | "17": { 43 | "name": "multi-path-payments", 44 | "is_required": false, 45 | "is_known": true 46 | }, 47 | "23": { 48 | "name": "anchors-zero-fee-htlc-tx", 49 | "is_required": false, 50 | "is_known": true 51 | }, 52 | "30": { 53 | "name": "amp", 54 | "is_required": true, 55 | "is_known": true 56 | }, 57 | "31": { 58 | "name": "amp", 59 | "is_required": false, 60 | "is_known": true 61 | } 62 | }, 63 | "identity_pubkey": "039c013ca5ea646253b9e7c530d630f81d443cb32d42c8a38e8cc0599ba2dfd11f", 64 | "alias": "039c013ca5ea646253b9", 65 | "num_pending_channels": 0, 66 | "num_active_channels": 2, 67 | "num_peers": 4, 68 | "block_height": 698292, 69 | "block_hash": "000000000000000000024ffedaa4e5e69b5ac18c9039892ad1ae49e3f5815426", 70 | "synced_to_chain": true, 71 | "testnet": false, 72 | "best_header_timestamp": 1630342794, 73 | "version": "0.13.1-beta commit=v0.13.1-beta", 74 | "num_inactive_channels": 0, 75 | "color": "#3399ff", 76 | "synced_to_graph": true, 77 | "commit_hash": "596fd90ef310cd7abbf2251edaae9ba4d5f8a689" 78 | } 79 | -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/fixtures/Mnemonic/Mnemonic-Config.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "af46ea03-226f-4f76-961d-65ddfadsf9f5df3", 3 | "type": "onchain", 4 | "created_at": 1603900550961, 5 | "name": "Mobile Wallet", 6 | "network": "mainnet", 7 | "addressType": "P2WPKH", 8 | "quorum": { 9 | "requiredSigners": 1, 10 | "totalSigners": 1 11 | }, 12 | "parentFingerprint": [120, 168, 155, 4], 13 | "xprv": "xprv9yUybAnm1ENLR3yNkVHjZXmn4w3SgSPvK3Jc1UFUqvTYu7jDm3HgsgUuYb6N1ekTgmUbhcHgok7Vr1oxqzE2NKh92LevkodVSB3FEjdnet5", 14 | "xpub": "xpub6CUKzgKeqbvddY3qrWpjvfiWcxsw5u7mgGECorf6QFzXmv4NJabwRUoPPrWSxcBaa8nrd3qYo9V8EPLe59aigHCwve2JUzqsTqFtva7nWuw", 15 | "mnemonic": "citizen fit card acid crouch column bring bulb shiver twice spider ghost labor truth fetch render frequent cinnamon mother result decrease debate stove box" 16 | } 17 | -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/fixtures/Multisig/Multisig-Config.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "af46ea03-226f-4f76-961d-65de559f5df3", 3 | "created_at": 1603900550961, 4 | "name": "Coldcard Vault", 5 | "type": "onchain", 6 | "license": { 7 | "signature": "", 8 | "license": "trial:728569" 9 | }, 10 | "network": "mainnet", 11 | "addressType": "P2WSH", 12 | "quorum": { 13 | "requiredSigners": 2, 14 | "totalSigners": 3 15 | }, 16 | "extendedPublicKeys": [ 17 | { 18 | "id": "5376cf19-5b44-442a-be76-5c137eda0078", 19 | "created_at": 1603900550960, 20 | "parentFingerprint": "4F60D1C9", 21 | "network": "mainnet", 22 | "bip32Path": "m/48'/0'/0'/2'", 23 | "xpub": "xpub6F2wuvSo8gSRjE9JsMgSva9cDZGa2Hh9SEJ9yczCLd1q2SRFV6N4vRUKFoecbatfhgZcG5rNwTxygNLoPrKpjRt94czCzQQPnoVY1RauiL6", 24 | "device": { 25 | "type": "coldcard", 26 | "model": "coldcard", 27 | "fingerprint": "4F60D1C9" 28 | } 29 | }, 30 | { 31 | "id": "286a02f7-bc38-42f4-b87e-442e08fbb507", 32 | "created_at": 1603900550961, 33 | "parentFingerprint": "34ECF56B", 34 | "network": "mainnet", 35 | "bip32Path": "m/48'/0'/0'/2'", 36 | "xpub": "xpub6FCzsnvwxusaXu8rxxn1XVKXSKFKjYrynid9ntEJ1Qc18Vi6eqGSkP6MJdEtDXCGqNNCGdytUJdLSucPxnyHdJYJKK6YMcTgULAxvrQYm5J", 37 | "device": { 38 | "type": "coldcard", 39 | "model": "coldcard", 40 | "fingerprint": "34ECF56B" 41 | } 42 | }, 43 | { 44 | "id": "aa2063d6-c166-4772-8154-369b810dec32", 45 | "created_at": 1603900550961, 46 | "parentFingerprint": "9130C3D6", 47 | "network": "mainnet", 48 | "bip32Path": "m/48'/0'/0'/2'", 49 | "xpub": "xpub6F1TMXpKfN5hRMdDUwSb9qD6LQmx2LTEbNtTj4nkFJte9GN14aFbqpup5AW7m9YhnYiTvEc1PqkrXkDY4gzJ95tNWKUATL6hD2AT641pSLE", 50 | "device": { 51 | "type": "coldcard", 52 | "model": "coldcard", 53 | "fingerprint": "9130C3D6" 54 | } 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/fixtures/Sunny/Sunny-Config.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "a713a6c7-29f0-44c4-b171-6xo8e35eb2eb9", 3 | "created_at": 1620691617778, 4 | "name": "Sunny-CC", 5 | "network": "mainnet", 6 | "addressType": "P2WPKH", 7 | "quorum": { 8 | "requiredSigners": 1, 9 | "totalSigners": 1 10 | }, 11 | "extendedPublicKeys": [ 12 | { 13 | "id": "9957d83e-e3a3-46a9-b562-e5fmx84c901d7", 14 | "created_at": 1620691617778, 15 | "network": "mainnet", 16 | "bip32Path": "m/84'/0'/0'", 17 | "xpub": "xpub6BwgRrArB4xonp4waxBxJD6KQspRo5Q3L8PrCLfPyf4RoD5bVZjYcZBx1WeJQ5WWPSAoj8TjroXi6AxaHGhAYc2LbLsXNM4PkLC5mXFaEit", 18 | "parentFingerprint": "4f60d1c9", 19 | "device": { 20 | "type": "coldcard", 21 | "model": "coldcard", 22 | "fingerprint": "4f60d1c9" 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/fixtures/Sunny/Sunny-other-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "bip49xpub": "xpub6CBT38XAx6PQJiobrB9qEWEvYnzVudpgu3C2ppioD8tJZJ3kiPmDSxkktZWzfJ9PXX3B1LiQd8orMcmeBj8vAkUEse4o8zo4owcbYpFwAXp" 3 | } 4 | -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/mock/electron-mock.js: -------------------------------------------------------------------------------- 1 | import createIPCMock from 'electron-mock-ipc'; 2 | const mocked = createIPCMock(); 3 | const ipcMain = mocked.ipcMain; 4 | const ipcRenderer = mocked.ipcRenderer; 5 | export { ipcMain, ipcRenderer }; 6 | // example usage 7 | // import { IpcMainEvent } from 'electron' 8 | // import { ipcMain } from '~/spec/mock/electron-mock' 9 | // import { targetMethod } from '~/src/target' 10 | // describe('your test', () => { 11 | // it('should be received', async () => { 12 | // ipcMain.once('/estimate-fee', (event: IpcMainEvent, obj: string) => { 13 | // event.sender.send('/estimate-fee', 5) 14 | // }) 15 | // const res = await targetMethod() 16 | // expect(res).toEqual(5) 17 | // }) 18 | // }) 19 | //# sourceMappingURL=electron-mock.js.map -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/mock/electron-mock.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"electron-mock.js","sourceRoot":"","sources":["electron-mock.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,MAAM,mBAAmB,CAAA;AAE7C,MAAM,MAAM,GAAG,aAAa,EAAE,CAAA;AAC9B,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAA;AAC9B,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CAAA;AACtC,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,CAAA;AAE/B,gBAAgB;AAChB,0CAA0C;AAC1C,sDAAsD;AACtD,8CAA8C;AAE9C,gCAAgC;AAChC,2CAA2C;AAC3C,4EAA4E;AAC5E,8CAA8C;AAC9C,SAAS;AACT,uCAAuC;AACvC,6BAA6B;AAC7B,OAAO;AACP,KAAK"} -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/mock/electron-mock.ts: -------------------------------------------------------------------------------- 1 | import createIPCMock from 'electron-mock-ipc' 2 | 3 | const mocked = createIPCMock() 4 | const ipcMain = mocked.ipcMain 5 | const ipcRenderer = mocked.ipcRenderer 6 | export { ipcMain, ipcRenderer } 7 | 8 | // example usage 9 | // import { IpcMainEvent } from 'electron' 10 | // import { ipcMain } from '~/spec/mock/electron-mock' 11 | // import { targetMethod } from '~/src/target' 12 | 13 | // describe('your test', () => { 14 | // it('should be received', async () => { 15 | // ipcMain.once('/estimate-fee', (event: IpcMainEvent, obj: string) => { 16 | // event.sender.send('/estimate-fee', 5) 17 | // }) 18 | // const res = await targetMethod() 19 | // expect(res).toEqual(5) 20 | // }) 21 | // }) -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/reducers/accountMap.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | accountMapReducer, 3 | ACCOUNTMAP_SET, 4 | ACCOUNTMAP_UPDATE, 5 | } from "../../reducers/accountMap"; 6 | 7 | import initialAccountMap from "../fixtures/initialAccountMap.json"; 8 | import { Multisig } from "../fixtures/"; 9 | import { createLilyAccount } from "../../../cypress/support/createConfig"; 10 | 11 | describe("Account Map Reducer", () => { 12 | test("setAccountMap sets initial state", () => { 13 | const action = { 14 | type: ACCOUNTMAP_SET, 15 | payload: initialAccountMap, 16 | }; 17 | const state = accountMapReducer({}, action); 18 | expect(state).toStrictEqual(initialAccountMap); 19 | }); 20 | 21 | test("updateAccountMap updates account map", () => { 22 | const action = { 23 | type: ACCOUNTMAP_UPDATE, 24 | payload: { 25 | account: createLilyAccount(Multisig), 26 | }, 27 | }; 28 | const state = accountMapReducer(initialAccountMap, action); 29 | expect(state).toStrictEqual({ 30 | ...initialAccountMap, 31 | [Multisig.config.id]: createLilyAccount(Multisig), 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/utils/license.test.js: -------------------------------------------------------------------------------- 1 | import { isAtLeastTier } from "../../utils/license"; 2 | const exampleLicense = { 3 | signature: "KPP1aFqppOLQAvfusPpw2+mED6sE4qF1RuZcS/vd67fycd/j7PSNcUGnUEn9ABNEtdXV9+XlNtiqBsuAYghBYjw=", 4 | license: "basic:721676:a1b6131ba536d9994afb0bac01936869a2d2f1d59eecb2d588727c4ab1e47dee", 5 | }; 6 | describe("license.ts", () => { 7 | test("isAtLeastTier: ", () => { 8 | expect(isAtLeastTier(exampleLicense, "trial")).toBe(false); 9 | expect(isAtLeastTier(exampleLicense, "premium")).toBe(false); 10 | expect(isAtLeastTier(exampleLicense, "essential")).toBe(false); 11 | expect(isAtLeastTier(exampleLicense, "basic")).toBe(true); 12 | expect(isAtLeastTier(exampleLicense, "free")).toBe(true); 13 | }); 14 | }); 15 | //# sourceMappingURL=license.test.js.map -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/utils/license.test.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"license.test.js","sourceRoot":"","sources":["license.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,MAAM,cAAc,GAAG;IACrB,SAAS,EACP,0FAA0F;IAC5F,OAAO,EACL,+EAA+E;CAClF,CAAC;AAEF,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,IAAI,CAAC,iBAAiB,EAAE,GAAG,EAAE;QAC3B,MAAM,CAAC,aAAa,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3D,MAAM,CAAC,aAAa,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC7D,MAAM,CAAC,aAAa,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/D,MAAM,CAAC,aAAa,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1D,MAAM,CAAC,aAAa,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"} -------------------------------------------------------------------------------- /apps/frontend/src/__tests__/utils/license.test.ts: -------------------------------------------------------------------------------- 1 | import { isAtLeastTier } from "../../utils/license"; 2 | 3 | const exampleLicense = { 4 | signature: 5 | "KPP1aFqppOLQAvfusPpw2+mED6sE4qF1RuZcS/vd67fycd/j7PSNcUGnUEn9ABNEtdXV9+XlNtiqBsuAYghBYjw=", 6 | license: 7 | "basic:721676:a1b6131ba536d9994afb0bac01936869a2d2f1d59eecb2d588727c4ab1e47dee", 8 | }; 9 | 10 | describe("license.ts", () => { 11 | test("isAtLeastTier: ", () => { 12 | expect(isAtLeastTier(exampleLicense, "trial")).toBe(false); 13 | expect(isAtLeastTier(exampleLicense, "premium")).toBe(false); 14 | expect(isAtLeastTier(exampleLicense, "essential")).toBe(false); 15 | expect(isAtLeastTier(exampleLicense, "basic")).toBe(true); 16 | expect(isAtLeastTier(exampleLicense, "free")).toBe(true); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /apps/frontend/src/assets/AppIcon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/AppIcon.icns -------------------------------------------------------------------------------- /apps/frontend/src/assets/bitbox02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/bitbox02.png -------------------------------------------------------------------------------- /apps/frontend/src/assets/bitgo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/bitgo.png -------------------------------------------------------------------------------- /apps/frontend/src/assets/cobo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/cobo.png -------------------------------------------------------------------------------- /apps/frontend/src/assets/coldcard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/coldcard.png -------------------------------------------------------------------------------- /apps/frontend/src/assets/fonts/Montserrat-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/fonts/Montserrat-Light.ttf -------------------------------------------------------------------------------- /apps/frontend/src/assets/fonts/Montserrat-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/fonts/Montserrat-Medium.ttf -------------------------------------------------------------------------------- /apps/frontend/src/assets/fonts/Montserrat-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/fonts/Montserrat-Regular.ttf -------------------------------------------------------------------------------- /apps/frontend/src/assets/fonts/Montserrat-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/fonts/Montserrat-SemiBold.ttf -------------------------------------------------------------------------------- /apps/frontend/src/assets/fonts/Raleway-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/fonts/Raleway-Light.ttf -------------------------------------------------------------------------------- /apps/frontend/src/assets/fonts/Raleway-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/fonts/Raleway-Medium.ttf -------------------------------------------------------------------------------- /apps/frontend/src/assets/fonts/Raleway-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/fonts/Raleway-Regular.ttf -------------------------------------------------------------------------------- /apps/frontend/src/assets/fonts/Raleway-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/fonts/Raleway-SemiBold.ttf -------------------------------------------------------------------------------- /apps/frontend/src/assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/icon.icns -------------------------------------------------------------------------------- /apps/frontend/src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/icon.png -------------------------------------------------------------------------------- /apps/frontend/src/assets/iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/iphone.png -------------------------------------------------------------------------------- /apps/frontend/src/assets/kingdom-trust.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/kingdom-trust.png -------------------------------------------------------------------------------- /apps/frontend/src/assets/ledger_nano_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/ledger_nano_s.png -------------------------------------------------------------------------------- /apps/frontend/src/assets/ledger_nano_x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/ledger_nano_x.png -------------------------------------------------------------------------------- /apps/frontend/src/assets/lily-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/lily-image.jpg -------------------------------------------------------------------------------- /apps/frontend/src/assets/onramp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/onramp.png -------------------------------------------------------------------------------- /apps/frontend/src/assets/trezor_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/trezor_1.png -------------------------------------------------------------------------------- /apps/frontend/src/assets/trezor_t.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/trezor_t.png -------------------------------------------------------------------------------- /apps/frontend/src/assets/unchained.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/apps/frontend/src/assets/unchained.png -------------------------------------------------------------------------------- /apps/frontend/src/components/AnimatedQrCode.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { QRCode } from 'react-qr-svg'; 3 | 4 | import { white, black } from 'src/utils/colors'; 5 | 6 | interface Props { 7 | valueArray: string[]; 8 | } 9 | 10 | export const AnimatedQrCode = ({ valueArray }: Props) => { 11 | const [step, setStep] = useState(0); 12 | 13 | setTimeout(() => { 14 | if (step < valueArray.length - 1) { 15 | setStep(step + 1); 16 | } else { 17 | setStep(0); 18 | } 19 | }, 500); 20 | 21 | return ( 22 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /apps/frontend/src/components/Badge.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | interface Props { 5 | children: React.ReactChild; 6 | style?: React.CSSProperties; 7 | className?: string; 8 | } 9 | 10 | export const Badge = ({ children, style, className }: Props) => ( 11 | 12 | {children} 13 | 14 | ); 15 | 16 | const BadgeContainer = styled.span` 17 | font-size: 0.875rem; 18 | line-height: 1.25rem; 19 | padding-top: 0.125rem; 20 | padding-bottom: 0.125rem; 21 | padding-left: 0.625rem; 22 | padding-right: 0.625rem; 23 | border-radius: 0.375rem; 24 | align-items: center; 25 | display: inline-flex; 26 | text-transform: capitalize; 27 | `; 28 | -------------------------------------------------------------------------------- /apps/frontend/src/components/Breadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Link } from 'react-router-dom'; 4 | import { ChevronRight } from '@styled-icons/boxicons-regular'; 5 | import { Home } from '@styled-icons/heroicons-solid'; 6 | import { gray400, gray500, gray700 } from 'src/utils/colors'; 7 | 8 | interface Props { 9 | homeLink: string; 10 | items: { 11 | text: string; 12 | link: string; 13 | }[]; 14 | className?: string; 15 | } 16 | 17 | export const Breadcrumbs = ({ items, homeLink, className }: Props) => { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | {items.map((item) => ( 25 | 26 | 27 | {item.text} 28 | 29 | ))} 30 | 31 | 32 | ); 33 | }; 34 | 35 | const Wrapper = styled.nav` 36 | display: flex; 37 | `; 38 | 39 | const ItemsWrapper = styled.ol` 40 | display: flex; 41 | align-items: center; 42 | padding: 0; 43 | `; 44 | 45 | const Item = styled.li` 46 | display: flex; 47 | align-items: center; 48 | margin-left: 1rem; 49 | `; 50 | 51 | const ItemIcon = styled(ChevronRight)` 52 | width: 1.25rem; 53 | height: 1.25rem; 54 | color: ${gray400}; 55 | `; 56 | 57 | const HomeLinkContainer = styled(Link)``; 58 | 59 | const HomeIcon = styled(Home)` 60 | width: 1.25rem; 61 | height: 1.25rem; 62 | color: ${gray400}; 63 | cursor: pointer; 64 | 65 | &:hover { 66 | color: ${gray700}; 67 | } 68 | `; 69 | 70 | const ItemLink = styled(Link)` 71 | font-size: 0.875rem; 72 | line-height: 1.25rem; 73 | font-weight: 500; 74 | color: ${gray500}; 75 | cursor: pointer; 76 | margin-left: 1rem; 77 | text-decoration: none; 78 | 79 | &:hover { 80 | color: ${gray700}; 81 | } 82 | `; 83 | -------------------------------------------------------------------------------- /apps/frontend/src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { css, keyframes } from 'styled-components'; 2 | import darken from 'polished/lib/color/darken'; 3 | import lighten from 'polished/lib/color/lighten'; 4 | import { white, green400, green600, green700, gray600 } from 'src/utils/colors'; 5 | 6 | export const Button = css<{ color: string; background: string }>` 7 | display: inline-flex; 8 | justify-content: center; 9 | align-items: center; 10 | border: none; 11 | border-radius: 0.375rem; 12 | cursor: pointer; 13 | outline: 0; 14 | font-family: Montserrat, sans-serif; 15 | color: ${(p) => (p.color ? p.color : white)}; 16 | background: ${(p) => (p.background ? p.background : green600)}; 17 | text-decoration: none; 18 | text-align: center; 19 | white-space: nowrap; 20 | 21 | transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, 22 | transform; 23 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 24 | transition-duration: 0.15s; 25 | padding-left: 1.5rem; 26 | padding-right: 1.5rem; 27 | padding-top: 0.75rem; 28 | padding-bottom: 0.75rem; 29 | line-height: 1.5rem; 30 | font-weight: 500; 31 | 32 | &:focus { 33 | outline: 0; 34 | box-shadow: 0 0 0 3px rgb(180, 198, 252); 35 | border-color: rgb(81, 69, 205); 36 | } 37 | 38 | &:hover { 39 | cursor: pointer; 40 | background: ${(p) => (p.background ? lighten(0.1, p.background) : green400)}; 41 | color: ${(p) => 42 | p.color && p.background === white ? gray600 : p.color ? lighten(0.1, p.color) : white}; 43 | } 44 | 45 | &:active { 46 | outline: 0; 47 | background: ${(p) => (p.background ? darken(0.05, p.background) : green700)}; 48 | transform: scale(0.99); 49 | } 50 | `; 51 | 52 | export const SidewaysShake = keyframes` 53 | 0% { transform: translate3d(0px, 0, 0) } 54 | 50% { transform: translate3d(5px, 0, 0) } 55 | 100% { transform: translate3d(0px, 0, 0) } 56 | `; 57 | -------------------------------------------------------------------------------- /apps/frontend/src/components/ChartEmptyState.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const ChartEmptyState = () => { 4 | return ( 5 | 14 | 19 | 24 | 25 | 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /apps/frontend/src/components/Countdown.tsx: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import React, { useEffect, useState } from 'react'; 3 | import CSS from 'csstype'; 4 | 5 | interface Props { 6 | endTimeSeconds: number; 7 | onExpire: () => void; 8 | style?: CSS.Properties; 9 | } 10 | 11 | export const Countdown = ({ endTimeSeconds, onExpire, style }: Props) => { 12 | const [expiration, setExpiration] = useState('Calculating...'); 13 | 14 | useEffect(() => { 15 | const intervalId = setInterval(() => { 16 | if (expiration !== 'Expired') { 17 | const now = Math.floor(Date.now() / 1000); 18 | const duration = moment.duration(endTimeSeconds - now, 'seconds'); 19 | 20 | if (duration.asSeconds() < 1) { 21 | setExpiration('Expired'); 22 | onExpire(); 23 | } else if (duration.minutes() > 0) { 24 | setExpiration( 25 | `${duration.minutes()} minute${ 26 | duration.minutes() > 1 ? 's' : '' 27 | }, ${duration.seconds()} second${duration.seconds() > 1 ? 's' : ''}` 28 | ); 29 | } else { 30 | setExpiration(`${duration.seconds()} second${duration.seconds() > 1 ? 's' : ''}`); 31 | } 32 | } 33 | }); 34 | return () => clearInterval(intervalId); 35 | }, [expiration, endTimeSeconds, onExpire]); 36 | 37 | return {expiration}; 38 | }; 39 | -------------------------------------------------------------------------------- /apps/frontend/src/components/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { PlusIcon, MinusIcon } from '@heroicons/react/outline'; 4 | 5 | import { StyledIcon, Button } from 'src/components'; 6 | import { green400, green500, green600, gray400, gray500, white } from 'src/utils/colors'; 7 | 8 | interface Props { 9 | value: number; 10 | setValue: (value: number) => void; 11 | minValue?: number; 12 | maxValue?: number; 13 | } 14 | 15 | export const Counter = ({ value, setValue, minValue = 0, maxValue = Infinity }: Props) => { 16 | return ( 17 |
18 | setValue(value - 1)} 21 | disabled={value - 1 < minValue} 22 | > 23 | 24 | 25 |
29 | {value} 30 |
31 | setValue(value + 1)} 34 | disabled={value + 1 > maxValue} 35 | > 36 | 37 | 38 |
39 | ); 40 | }; 41 | 42 | const IncrementButton = styled.button<{ disabled: boolean }>` 43 | border-radius: 9999px; 44 | border: 1px solid ${(p) => (p.disabled ? gray400 : green500)}; 45 | background: ${(p) => (p.disabled ? 'transparent' : green400)}; 46 | color: ${(p) => (p.disabled ? gray500 : white)}; 47 | width: 2.5rem; 48 | height: 2.5rem; 49 | display: flex; 50 | align-items: center; 51 | justify-content: center; 52 | cursor: pointer; 53 | pointer-events: ${(p) => (p.disabled ? 'none' : 'auto')}; 54 | 55 | &:hover { 56 | background: ${(p) => !p.disabled && green400}; 57 | } 58 | 59 | &:active { 60 | background: ${(p) => !p.disabled && green600}; 61 | } 62 | `; 63 | -------------------------------------------------------------------------------- /apps/frontend/src/components/DeviceImage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { QuestionMarkCircle } from '@styled-icons/heroicons-outline'; 3 | 4 | import Coldcard from 'src/assets/coldcard.png'; 5 | import LedgerNanoS from 'src/assets/ledger_nano_s.png'; 6 | import LedgerNanoX from 'src/assets/ledger_nano_x.png'; 7 | import TrezorOne from 'src/assets/trezor_1.png'; 8 | import TrezorT from 'src/assets/trezor_t.png'; 9 | import Cobo from 'src/assets/cobo.png'; 10 | import Bitbox from 'src/assets/bitbox02.png'; 11 | import LilyLogo from 'src/assets/flower.svg'; 12 | import Unchained from 'src/assets/unchained.png'; 13 | import Bitgo from 'src/assets/bitgo.png'; 14 | import Onramp from 'src/assets/onramp.png'; 15 | import KingdomTrust from 'src/assets/kingdom-trust.png'; 16 | 17 | import { Device } from '@lily/types'; 18 | 19 | interface Props { 20 | device: Device; 21 | className?: string; 22 | } 23 | 24 | export const DeviceImage = ({ device, className }: Props) => { 25 | if (device.type === 'unknown') { 26 | return ( 27 |
28 | 29 |
30 | ); 31 | } 32 | return ( 33 | 63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /apps/frontend/src/components/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | // KBC-TODO: Figure out why this wasnt working before 2 | // KBC-TODO: reimplement this component in App.tsx 3 | import React from 'react'; 4 | import styled from 'styled-components'; 5 | 6 | export class ErrorBoundary extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { hasError: false }; 10 | } 11 | 12 | static getDerivedStateFromError(error) { 13 | // Update state so the next render will show the fallback UI. 14 | return { hasError: true }; 15 | } 16 | 17 | componentDidCatch(error, errorInfo) { 18 | // You can also log the error to an error reporting service 19 | // logErrorToMyService(error, errorInfo); 20 | } 21 | 22 | render() { 23 | if (this.state.hasError) { 24 | // You can render any custom fallback UI 25 | return ( 26 | 27 | 28 |

Oops, something went wrong.

29 | 30 |

Please contact us to report the error.

31 |
32 | ) 33 | } 34 | 35 | return this.props.children; 36 | } 37 | } 38 | 39 | const ErrorBoundaryContainer = styled.div` 40 | display: flex; 41 | align-items: center; 42 | justify-content: center; 43 | flex-direction: column; 44 | `; 45 | 46 | const LilyImage = styled.img` 47 | width: 9em; 48 | height: 9em; 49 | `; -------------------------------------------------------------------------------- /apps/frontend/src/components/ErrorModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { ExclamationIcon } from '@heroicons/react/outline'; 4 | 5 | import { StyledIcon, ModalContentWrapper, Button } from '.'; 6 | 7 | import { white, red100, red600, gray500 } from 'src/utils/colors'; 8 | 9 | interface Props { 10 | message: string; 11 | closeModal: () => void; 12 | } 13 | 14 | export const ErrorModal = ({ message, closeModal }: Props) => ( 15 | 16 | 17 |
18 | 19 |
20 |
21 | 22 |

Error

23 |

{message}

24 | closeModal()}> 25 | Dismiss 26 | 27 |
28 |
29 | ); 30 | 31 | const DismissButton = styled.button` 32 | ${Button} 33 | padding-left: 1rem; 34 | padding-right: 1rem; 35 | padding-top: 0.5rem; 36 | padding-bottom: 0.5rem; 37 | width: 100%; 38 | margin-top: 1.25rem; 39 | `; 40 | 41 | const ModifiedModalContentWrapper = styled(ModalContentWrapper)` 42 | flex-direction: column; 43 | align-items: center; 44 | margin-top: 1.25rem; 45 | max-width: ; 46 | `; 47 | 48 | const DangerTextContainer = styled.div` 49 | display: flex; 50 | flex: 1; 51 | align-items: center; 52 | flex-direction: column; 53 | margin-top: 0.75rem; 54 | width: 100%; 55 | `; 56 | 57 | const DangerIconContainer = styled.div``; 58 | 59 | const StyledIconCircle = styled.div` 60 | border-radius: 9999px; 61 | background: ${red100}; 62 | width: 3rem; 63 | height: 3rem; 64 | display: flex; 65 | justify-content: center; 66 | align-items: center; 67 | `; 68 | 69 | const DangerText = styled.div` 70 | font-size: 1.125rem; 71 | text-align: center; 72 | font-weight: 500; 73 | `; 74 | 75 | const DangerSubtext = styled.div` 76 | margin-top: 0.5rem; 77 | color: ${gray500}; 78 | text-align: center; 79 | `; 80 | -------------------------------------------------------------------------------- /apps/frontend/src/components/FileUploader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import { File } from '@lily/types'; 5 | 6 | interface Props { 7 | accept: string; 8 | id: string; 9 | onFileLoad: (file: File) => void; 10 | } 11 | 12 | export const FileUploader = ({ accept, id, onFileLoad }: Props) => ( 13 | { 18 | if (e.target.files) { 19 | const filereader = new FileReader(); 20 | const modifiedDate = e.target.files[0].lastModified; 21 | 22 | filereader.onload = (event) => { 23 | if (event.target && event.target.result) { 24 | onFileLoad({ 25 | file: event.target.result.toString(), 26 | modifiedTime: modifiedDate 27 | }); 28 | } 29 | }; 30 | filereader.readAsText(e.target.files[0]); 31 | } 32 | }} 33 | /> 34 | ); 35 | 36 | const FileInput = styled.input` 37 | width: 0.1px; 38 | height: 0.1px; 39 | opacity: 0; 40 | overflow: hidden; 41 | position: absolute; 42 | z-index: -1; 43 | `; 44 | -------------------------------------------------------------------------------- /apps/frontend/src/components/LicenseInformation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import moment from 'moment'; 4 | 5 | import { gray500, gray900 } from 'src/utils/colors'; 6 | import { licenseExpires, licenseTxId, licenseTier } from 'src/utils/license'; 7 | import { capitalize } from 'src/utils/other'; 8 | 9 | import { NodeConfigWithBlockchainInfo, VaultConfig } from '@lily/types'; 10 | 11 | interface Props { 12 | config: VaultConfig; 13 | nodeConfig: NodeConfigWithBlockchainInfo; 14 | } 15 | 16 | export const LicenseInformation = ({ config, nodeConfig }: Props) => { 17 | const blockDiff = licenseExpires(config.license) - nodeConfig.blocks; 18 | const blockDiffTimeEst = blockDiff * 10; 19 | const expireAsDate = moment().add(blockDiffTimeEst, 'minutes').format('MMMM Do YYYY, h:mma'); 20 | 21 | return ( 22 | 23 | 24 | License Tier 25 | {capitalize(licenseTier(config.license))} 26 | 27 | 28 | License Expires 29 | Block {licenseExpires(config.license).toLocaleString()} 30 | 31 | 32 | Approximate Expire Date 33 | {expireAsDate} 34 | 35 | 36 | Payment Transaction 37 | {licenseTxId(config.license)} 38 | 39 | 40 | ); 41 | }; 42 | 43 | const ItemContainer = styled.div` 44 | margin: 1em 0; 45 | `; 46 | 47 | const ItemLabel = styled.div` 48 | color: ${gray500}; 49 | font-weight: 900; 50 | `; 51 | 52 | const ItemValue = styled.div` 53 | color: ${gray900}; 54 | `; 55 | 56 | const Wrapper = styled.div` 57 | padding: 1em 2em 2em; 58 | `; 59 | -------------------------------------------------------------------------------- /apps/frontend/src/components/LightningImage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import { gray100, purple700 } from 'src/utils/colors'; 5 | 6 | const IconSvg = styled.svg` 7 | color: ${purple700}; 8 | margin-right: 0.65rem; 9 | flex-shrink: 0; 10 | border-radius: 0.375rem; 11 | object-position: center; 12 | object-fit: cover; 13 | background: ${gray100}; 14 | flex: none; 15 | width: 6rem; 16 | height: 6rem; 17 | max-width: 100%; 18 | display: block; 19 | vertical-align: middle; 20 | border-style: solid; 21 | padding: 1em; 22 | `; 23 | 24 | export const LightningImage = () => ( 25 | 26 | 32 | 33 | ); 34 | -------------------------------------------------------------------------------- /apps/frontend/src/components/NoAccountsEmptyState.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { useHistory } from 'react-router-dom'; 4 | 5 | import { Button } from '.'; 6 | 7 | import { white, gray600, green700 } from 'src/utils/colors'; 8 | 9 | export const NoAccountsEmptyState = () => { 10 | const history = useHistory(); 11 | 12 | return ( 13 | 14 | You haven't added any accounts yet! 15 | 16 | history.push('/setup')} 20 | > 21 | Add your first account 22 | 23 | 24 | ); 25 | }; 26 | 27 | const Container = styled.div` 28 | background: ${white}; 29 | padding: 8rem; 30 | display: flex; 31 | flex-direction: column; 32 | align-items: center; 33 | justify-content: center; 34 | border-radius: 0.385rem; 35 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); 36 | `; 37 | 38 | const Text = styled.span` 39 | color: ${gray600}; 40 | font-size: 2rem; 41 | line-height: 1; 42 | margin-bottom: 3rem; 43 | `; 44 | 45 | const CreateAccountButton = styled.button` 46 | ${Button}; 47 | `; 48 | -------------------------------------------------------------------------------- /apps/frontend/src/components/OutsideClick.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from "react"; 2 | 3 | /** 4 | * Hook that alerts clicks outside of the passed ref 5 | */ 6 | function useOutsideAlerter(ref: React.MutableRefObject, onOutsideClick: () => void) { 7 | useEffect(() => { 8 | /** 9 | * Alert if clicked on outside of element 10 | */ 11 | function handleClickOutside(event: MouseEvent) { 12 | if (ref.current && !(ref.current as any).contains(event.target)) { 13 | onOutsideClick() 14 | } 15 | } 16 | // Bind the event listener 17 | document.addEventListener("mousedown", handleClickOutside); 18 | return () => { 19 | // Unbind the event listener on clean up 20 | document.removeEventListener("mousedown", handleClickOutside); 21 | }; 22 | }, [ref, onOutsideClick]); 23 | } 24 | 25 | /** 26 | * Component that alerts if you click outside of it 27 | */ 28 | function OutsideAlerter({ onOutsideClick, children }: { onOutsideClick: () => void, children: React.ReactChild }) { 29 | const wrapperRef = useRef(null); 30 | useOutsideAlerter(wrapperRef, onOutsideClick); 31 | 32 | return
{children}
; 33 | } 34 | 35 | export default OutsideAlerter; -------------------------------------------------------------------------------- /apps/frontend/src/components/Price.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { ConfigContext } from 'src/context'; 3 | import { satoshisToBitcoins } from 'unchained-bitcoin'; 4 | 5 | interface Props { 6 | value: number; 7 | className?: string; 8 | } 9 | 10 | export const Price = ({ value, className }: Props) => { 11 | const { currentBitcoinPrice } = useContext(ConfigContext); 12 | 13 | const getPrice = (value: number) => 14 | satoshisToBitcoins(value).multipliedBy(currentBitcoinPrice).toFixed(2); 15 | 16 | return ${getPrice(value)}; 17 | }; 18 | -------------------------------------------------------------------------------- /apps/frontend/src/components/PurchaseLicenseSuccess.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { CheckCircle } from '@styled-icons/material'; 4 | 5 | import { LicenseInformation, StyledIcon } from '.'; 6 | 7 | import { white, green500, gray700 } from 'src/utils/colors'; 8 | 9 | import { NodeConfigWithBlockchainInfo, VaultConfig } from '@lily/types'; 10 | 11 | interface Props { 12 | config: VaultConfig; 13 | nodeConfig: NodeConfigWithBlockchainInfo; 14 | } 15 | 16 | export const PurchaseLicenseSuccess = ({ config, nodeConfig }: Props) => { 17 | return ( 18 | 19 | 20 | 21 | 22 | Payment Success! 23 | 24 | Thank you so much for purchasing a license for Lily Wallet! 25 |
26 |
27 | Your payment helps fund the development and maitanance of this open source software. 28 |
29 | 30 |
31 | ); 32 | }; 33 | 34 | const Wrapper = styled.div` 35 | display: flex; 36 | flex-direction: column; 37 | align-items: center; 38 | background: ${white}; 39 | border-radius: 0.875em; 40 | padding: 1.5em 0.75em; 41 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); 42 | `; 43 | 44 | const IconWrapper = styled.div``; 45 | 46 | const SuccessText = styled.div` 47 | margin-top: 0.5em; 48 | font-size: 1.5em; 49 | color: ${gray700}; 50 | `; 51 | 52 | const SuccessSubtext = styled.div` 53 | color: ${gray700}; 54 | margin-top: 2rem; 55 | margin-bottom: 1rem; 56 | text-align: center; 57 | `; 58 | -------------------------------------------------------------------------------- /apps/frontend/src/components/ScrollToTop.ts: -------------------------------------------------------------------------------- 1 | // ScrollToTop.ts 2 | import { useHistory } from 'react-router-dom'; 3 | import { useEffect } from 'react'; 4 | 5 | /* 6 | * Registers a history listener on mount which 7 | * scrolls to the top of the page on route change 8 | */ 9 | export const ScrollToTop = () => { 10 | const history = useHistory(); 11 | useEffect(() => { 12 | const unlisten = history.listen(() => { 13 | window.scrollTo(0, 0); 14 | }); 15 | return unlisten; 16 | }, [history]); 17 | 18 | return null; 19 | }; -------------------------------------------------------------------------------- /apps/frontend/src/components/SlideOver.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Dialog, Transition } from '@headlessui/react'; 3 | 4 | export const SlideOver = ({ open, setOpen, content, className = 'max-w-2xl' }) => { 5 | return ( 6 | 7 | setOpen(false)} 11 | > 12 |
13 | 22 | 23 | 24 | 25 |
26 | 35 |
36 |
37 | {content} 38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /apps/frontend/src/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { classNames } from 'src/utils/other'; 3 | 4 | interface Props { 5 | className: string; 6 | } 7 | 8 | export const Spinner = ({ className }: Props) => ( 9 | 15 | 23 | 28 | 29 | ); 30 | -------------------------------------------------------------------------------- /apps/frontend/src/components/StyledIcon.tsx: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | import rem from 'src/utils/rem'; 3 | 4 | const spinning = keyframes` 5 | from {transform:rotate(0deg);} 6 | to {transform:rotate(360deg);} 7 | `; 8 | 9 | export const StyledIcon = styled.div<{ size?: number }>` 10 | && { 11 | width: ${(p) => rem(p.size || 20)}; 12 | height: ${(p) => rem(p.size || 20)}; 13 | } 14 | `; 15 | 16 | export const StyledIconSpinning = styled(StyledIcon)` 17 | animation-name: ${spinning}; 18 | animation-duration: 1.4s; 19 | animation-iteration-count: infinite; 20 | animation-fill-mode: both; 21 | `; 22 | -------------------------------------------------------------------------------- /apps/frontend/src/components/Table.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { gray50, gray200, gray300, gray600, gray700 } from 'src/utils/colors'; 4 | 5 | export const TableContainer = styled.div` 6 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); 7 | border-bottom-width: 1px; 8 | border-radius: 0.5rem; 9 | border-color: ${gray200}; 10 | `; 11 | 12 | export const Table = styled.table` 13 | border: none; 14 | border-collapse: collapse; 15 | `; 16 | 17 | export const TableHeader = styled.thead``; 18 | 19 | export const TableHead = styled.th` 20 | letter-spacing: 0.05em; 21 | text-transform: uppercase; 22 | padding-left: 1.5rem; 23 | padding-right: 1.5rem; 24 | padding-top: 0.75rem; 25 | padding-bottom: 0.75rem; 26 | font-size: 0.75rem; 27 | line-height: 1rem; 28 | font-weight: 500; 29 | color: ${gray600}; 30 | background: ${gray50}; 31 | border: none; 32 | `; 33 | 34 | export const TableBody = styled.tbody``; 35 | 36 | export const TableRow = styled.tr` 37 | border: 1px solid ${gray200}; 38 | `; 39 | 40 | export const TableColumn = styled.td.attrs({ className: 'text-gray-600' })` 41 | padding-left: 1.5rem; 42 | padding-right: 1.5rem; 43 | padding-top: 0.75rem; 44 | padding-bottom: 0.75rem; 45 | font-size: 0.875rem; 46 | line-height: 1.25rem; 47 | border: none; 48 | border-bottom: 1px solid ${gray300}; 49 | border-width: thin; 50 | `; 51 | 52 | export const TableColumnBold = styled(TableColumn)` 53 | color: ${gray700}; 54 | font-weight: 500; 55 | `; 56 | -------------------------------------------------------------------------------- /apps/frontend/src/components/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { SetStateString } from 'src/types'; 4 | 5 | import { classNames } from 'src/utils/other'; 6 | 7 | interface TabItem { 8 | name: string; 9 | tabId: string; 10 | } 11 | 12 | interface Props { 13 | currentTab: string; 14 | setCurrentTab: SetStateString; 15 | items: TabItem[]; 16 | } 17 | 18 | export const Tabs = ({ currentTab, setCurrentTab, items }: Props) => { 19 | return ( 20 |
21 | 36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /apps/frontend/src/components/Toggle.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react'; 2 | import { Switch } from '@headlessui/react'; 3 | 4 | import { UnitContext } from 'src/context'; 5 | 6 | function classNames(...classes) { 7 | return classes.filter(Boolean).join(' '); 8 | } 9 | 10 | export function CurrencyToggle() { 11 | const { unit, toggleUnit } = useContext(UnitContext); 12 | 13 | const enabled = unit === 'sats'; 14 | return ( 15 | 20 | Use setting 21 | 27 |
28 | 34 | BTC 35 | 36 | 44 | Sats 45 | 46 |
47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /apps/frontend/src/components/TransactionRowsLoading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ChevronRightIcon } from '@heroicons/react/solid'; 3 | 4 | const LoadingRow = () => ( 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
25 |
26 |
27 | ); 28 | 29 | const LoadingSection = ({ numRows }: { numRows: number }) => ( 30 |
31 |
32 | {Array.from(Array(numRows).keys()).map(() => ( 33 | 34 | ))} 35 |
36 | ); 37 | 38 | export const TransactionRowsLoading = () => { 39 | const randomArray = Array.from({ length: 3 }, () => Math.ceil(Math.random() * 3)); 40 | return ( 41 | <> 42 | {randomArray.map((num) => ( 43 | 44 | ))} 45 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /apps/frontend/src/components/Unit.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | 3 | import { UnitContext } from 'src/context'; 4 | 5 | interface Props { 6 | value: number; 7 | className?: string; 8 | } 9 | 10 | // all values throughout the application should be denominated in satoshis 11 | export const Unit = ({ value, className }: Props) => { 12 | const { getValue } = useContext(UnitContext); 13 | 14 | return {getValue(value)}; 15 | }; 16 | -------------------------------------------------------------------------------- /apps/frontend/src/components/UnitInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from 'react'; 2 | import { bitcoinsToSatoshis, satoshisToBitcoins } from 'unchained-bitcoin'; 3 | 4 | import { Input, InputProps } from 'src/components'; 5 | 6 | import { UnitContext } from 'src/context'; 7 | 8 | // Note: `value` is always in satoshis 9 | export const UnitInput = ({ 10 | onChange, 11 | value, 12 | ...props 13 | }: Omit) => { 14 | const { unit } = useContext(UnitContext); 15 | const [modifiedValue, setModifiedValue] = useState(''); 16 | 17 | useEffect(() => { 18 | if (unit === 'BTC') { 19 | setModifiedValue(satoshisToBitcoins(value).toPrecision()); 20 | } else { 21 | setModifiedValue(bitcoinsToSatoshis(value).toPrecision()); 22 | } 23 | }, [unit]); 24 | 25 | // We hold and display the modified value in this state but 26 | // call the parent onChange with the value in satoshis 27 | const onChangeOverride = (value: string) => { 28 | if (unit === 'BTC') { 29 | setModifiedValue(value); 30 | onChange(bitcoinsToSatoshis(value).toPrecision()); 31 | } else { 32 | setModifiedValue(value); 33 | onChange(value); 34 | } 35 | }; 36 | 37 | return ( 38 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /apps/frontend/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Modal'; 2 | 3 | export * from './AlertBar'; 4 | export * from './AnimatedQrCode'; 5 | export * from './Badge'; 6 | export * from './Breadcrumbs'; 7 | export * from './Button'; 8 | export * from './ChartEmptyState'; 9 | export * from './ConnectToLilyMobileModal'; 10 | export * from './ConnectToNodeModal'; 11 | export * from './Countdown'; 12 | export * from './Counter'; 13 | export * from './DeviceImage'; 14 | export * from './DeviceSelect'; 15 | export * from './Dropdown'; 16 | export * from './ErrorBoundary'; 17 | export * from './ErrorModal'; 18 | export * from './FileUploader'; 19 | export * from './Input'; 20 | export * from './LicenseInformation'; 21 | export * from './LightningImage'; 22 | export * from './Loading'; 23 | export * from './MnemonicWordsDisplayer'; 24 | export * from './NavLinks'; 25 | export * from './NoAccountsEmptyState'; 26 | export * from './OutsideClick'; 27 | export * from './PricingChart'; 28 | export * from './PricingTable'; 29 | export * from './PromptPinModal'; 30 | export * from './Price'; 31 | export * from './PurchaseLicenseSuccess'; 32 | export * from './ScrollToTop'; 33 | export * from './Select'; 34 | export * from './SelectAccountMenu'; 35 | export * from './SettingsTable'; 36 | export * from './Sidebar'; 37 | export * from './SlideOver'; 38 | export * from './Spinner'; 39 | export * from './StyledIcon'; 40 | export * from './SupportModal'; 41 | export * from './Tabs'; 42 | export * from './Textarea'; 43 | export * from './TitleBar'; 44 | export * from './Toggle'; 45 | export * from './Transition'; 46 | export * from './TransactionRowsLoading'; 47 | export * from './Unit'; 48 | export * from './UnitInput'; 49 | 50 | export * from './layout'; 51 | -------------------------------------------------------------------------------- /apps/frontend/src/context/ConfigContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState } from 'react'; 2 | import { networks, Network } from 'bitcoinjs-lib'; 3 | import BigNumber from 'bignumber.js'; 4 | 5 | import { LilyConfig, EMPTY_CONFIG, NodeConfigWithBlockchainInfo } from '@lily/types'; 6 | 7 | export const ConfigContext = createContext({ 8 | setConfigFile: (config: LilyConfig) => {}, 9 | config: {} as LilyConfig, 10 | setNodeConfig: (nodeConfig: NodeConfigWithBlockchainInfo) => {}, 11 | nodeConfig: {} as any, 12 | currentBitcoinPrice: new BigNumber(0), 13 | setCurrentBitcoinPrice: (bitcoinPrice: BigNumber) => {}, 14 | currentBitcoinNetwork: {} as Network, 15 | setCurrentBitcoinNetwork: (network: Network) => {}, 16 | password: {} as string, 17 | setPassword: (password: string) => {} 18 | }); 19 | 20 | export const ConfigProvider = ({ children }: { children: React.ReactChild }) => { 21 | const [config, setConfigFile] = useState(EMPTY_CONFIG); 22 | const [currentBitcoinPrice, setCurrentBitcoinPrice] = useState(new BigNumber(0)); 23 | const [currentBitcoinNetwork, setCurrentBitcoinNetwork] = useState(networks.bitcoin); 24 | const [nodeConfig, setNodeConfig] = useState(undefined); 25 | const [password, setPassword] = useState(''); 26 | 27 | const value = { 28 | config, 29 | setConfigFile, 30 | nodeConfig, 31 | setNodeConfig, 32 | currentBitcoinPrice, 33 | setCurrentBitcoinPrice, 34 | currentBitcoinNetwork, 35 | setCurrentBitcoinNetwork, 36 | password, 37 | setPassword 38 | }; 39 | 40 | return {children}; 41 | }; 42 | -------------------------------------------------------------------------------- /apps/frontend/src/context/ModalContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState } from "react"; 2 | 3 | export const ModalContext = createContext({ 4 | openInModal: (component: JSX.Element) => {}, 5 | closeModal: () => {}, 6 | modalIsOpen: {} as boolean, 7 | modalContent: {} as JSX.Element | null, 8 | }); 9 | 10 | export const ModalProvider = ({ children }: { children: React.ReactChild }) => { 11 | const [modalIsOpen, setModalIsOpen] = useState(false); 12 | const [modalContent, setModalContent] = useState(null); 13 | 14 | const openInModal = (component: JSX.Element) => { 15 | setModalIsOpen(true); 16 | setModalContent(component); 17 | }; 18 | 19 | const closeModal = () => { 20 | setModalIsOpen(false); 21 | setModalContent(null); 22 | }; 23 | 24 | const value = { 25 | openInModal, 26 | closeModal, 27 | modalIsOpen, 28 | modalContent, 29 | }; 30 | 31 | return ( 32 | {children} 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /apps/frontend/src/context/PlatformContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext } from 'react'; 2 | 3 | import { BasePlatform, ElectronPlatform, WebPlatform } from 'src/frontend-middleware'; 4 | 5 | export const PlatformContext = createContext({ 6 | platform: {} as BasePlatform 7 | }); 8 | 9 | export const PlatformProvider = ({ children }: { children: React.ReactChild }) => { 10 | let platform: BasePlatform; 11 | if (process.env.REACT_APP_IS_ELECTRON) { 12 | platform = new ElectronPlatform(); 13 | } else { 14 | platform = new WebPlatform(); 15 | } 16 | 17 | const value = { 18 | platform 19 | }; 20 | 21 | return {children}; 22 | }; 23 | -------------------------------------------------------------------------------- /apps/frontend/src/context/SidebarContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState } from 'react'; 2 | 3 | export const SidebarContext = createContext({ 4 | setSidebarOpen: (val: boolean) => {}, 5 | sidebarOpen: false 6 | }); 7 | 8 | export const SidebarProvider = ({ children }: { children: React.ReactChild }) => { 9 | const [sidebarOpen, setSidebarOpen] = useState(false); 10 | 11 | const value = { 12 | setSidebarOpen, 13 | sidebarOpen 14 | }; 15 | 16 | return {children}; 17 | }; 18 | -------------------------------------------------------------------------------- /apps/frontend/src/context/UnitContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState } from 'react'; 2 | import { satoshisToBitcoins } from 'unchained-bitcoin'; 3 | 4 | import { useLocalStorage } from 'src/utils/useLocalStorage'; 5 | 6 | type UnitOptions = 'BTC' | 'sats'; 7 | 8 | interface ContextProps { 9 | setUnit: (unit: UnitOptions) => void; 10 | unit: UnitOptions; 11 | toggleUnit: () => void; 12 | getValue: (value: number) => string; 13 | } 14 | 15 | export const UnitContext = createContext({} as ContextProps); 16 | 17 | export const UnitProvider = ({ children }: { children: React.ReactChild }) => { 18 | const [currencyUnit, setCurrencyUnit] = useLocalStorage('currencyUnit', 'BTC'); 19 | const [unit, setUnit] = useState(currencyUnit); 20 | 21 | const toggleUnit = () => { 22 | if (unit === 'BTC') { 23 | setUnit('sats'); 24 | setCurrencyUnit('sats'); 25 | } else { 26 | setUnit('BTC'); 27 | setCurrencyUnit('BTC'); 28 | } 29 | }; 30 | 31 | const getValue = (value: number) => { 32 | if (unit === 'BTC') { 33 | return `${satoshisToBitcoins(value).toFixed()} BTC`; 34 | } else { 35 | return `${value.toLocaleString()} sat${value === 1 ? '' : 's'}`; // if 1 sat, don't add "s" 36 | } 37 | }; 38 | 39 | const value = { 40 | unit, 41 | setUnit, 42 | toggleUnit, 43 | getValue 44 | }; 45 | 46 | return {children}; 47 | }; 48 | -------------------------------------------------------------------------------- /apps/frontend/src/context/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AccountMapContext'; 2 | export * from './ConfigContext'; 3 | export * from './ModalContext'; 4 | export * from './PlatformContext'; 5 | export * from './SidebarContext'; 6 | export * from './UnitContext'; 7 | -------------------------------------------------------------------------------- /apps/frontend/src/frontend-middleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BasePlatform'; 2 | export * from './ElectronPlatform'; 3 | export * from './WebPlatform'; 4 | -------------------------------------------------------------------------------- /apps/frontend/src/hocs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './requireOnchain'; 2 | export * from './requireLightning'; 3 | export * from './useSelected'; 4 | export * from './useShiftSelected'; 5 | -------------------------------------------------------------------------------- /apps/frontend/src/hocs/requireLightning.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import React, { useContext } from 'react'; 3 | import { LilyLightningAccount } from '@lily/types'; 4 | import { AccountMapContext } from 'src/context/AccountMapContext'; 5 | import { ConfigContext } from 'src/context/ConfigContext'; 6 | 7 | interface RequireLightningProps { 8 | currentAccount: LilyLightningAccount; 9 | } 10 | 11 | export function requireLightning( 12 | WrappedComponent: React.ComponentType 13 | ) { 14 | const displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; 15 | 16 | const ComponentWithLightningAccount = (props: Omit) => { 17 | const { currentAccount, setCurrentAccountId } = useContext(AccountMapContext); 18 | const { config } = useContext(ConfigContext); 19 | if (currentAccount.config.type !== 'lightning' && !!config.lightning[0]) { 20 | setCurrentAccountId(config.lightning[0].id); 21 | } 22 | 23 | return ; 24 | }; 25 | 26 | ComponentWithLightningAccount.displayName = `requireLightning(${displayName})`; 27 | 28 | return ComponentWithLightningAccount; 29 | } 30 | -------------------------------------------------------------------------------- /apps/frontend/src/hocs/requireOnchain.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import React, { useContext } from 'react'; 3 | import { LilyOnchainAccount } from '@lily/types'; 4 | import { AccountMapContext } from 'src/context/AccountMapContext'; 5 | import { ConfigContext } from 'src/context/ConfigContext'; 6 | 7 | interface RequireOnchainProps { 8 | currentAccount: LilyOnchainAccount; 9 | } 10 | 11 | export function requireOnchain( 12 | WrappedComponent: React.ComponentType 13 | ) { 14 | const displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; 15 | 16 | const ComponentWithOnchainAccount = (props: Omit) => { 17 | const { currentAccount, setCurrentAccountId } = useContext(AccountMapContext); 18 | const { config } = useContext(ConfigContext); 19 | 20 | if (currentAccount.config.type !== 'onchain' && !!config.vaults[0]) { 21 | setCurrentAccountId(config.vaults[0].id); 22 | } else if (currentAccount.config.type !== 'onchain' && !!config.wallets[0]) { 23 | setCurrentAccountId(config.wallets[0].id); 24 | } 25 | 26 | return ; 27 | }; 28 | 29 | ComponentWithOnchainAccount.displayName = `requireOnchain(${displayName})`; 30 | 31 | return ComponentWithOnchainAccount; 32 | } 33 | -------------------------------------------------------------------------------- /apps/frontend/src/hocs/useSelected.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | import uniq from 'lodash/uniq'; 3 | import difference from 'lodash/difference'; 4 | 5 | export const useSelected = (initialState: Array

) => { 6 | const [selected, setSelected] = useState(initialState); 7 | 8 | const add = useCallback( 9 | (items: Array

) => { 10 | setSelected((oldList) => uniq([...oldList, ...items])); 11 | }, 12 | [setSelected] 13 | ); 14 | 15 | const remove = useCallback( 16 | (items: Array

) => { 17 | setSelected((oldList) => difference(oldList, items)); 18 | }, 19 | [setSelected] 20 | ); 21 | 22 | const change = useCallback( 23 | (addOrRemove: boolean, items: Array

) => { 24 | if (addOrRemove) { 25 | add(items); 26 | } else { 27 | remove(items); 28 | } 29 | }, 30 | [add, remove] 31 | ); 32 | 33 | const clear = useCallback(() => setSelected([]), [setSelected]); 34 | 35 | return { selected, add, remove, clear, change }; 36 | }; 37 | -------------------------------------------------------------------------------- /apps/frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | import { AccountMapProvider } from './context/AccountMapContext'; 7 | import { ConfigProvider } from './context/ConfigContext'; 8 | import { ModalProvider } from './context/ModalContext'; 9 | import { SidebarProvider } from './context/SidebarContext'; 10 | import { PlatformProvider } from './context/PlatformContext'; 11 | import { UnitProvider } from './context/UnitContext'; 12 | 13 | ReactDOM.render( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | , 27 | document.getElementById('root') 28 | ); 29 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Home/AccountGridItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Bitcoin } from '@styled-icons/boxicons-logos'; 4 | 5 | import { Unit } from 'src/components'; 6 | 7 | import { AccountMapContext } from 'src/context/AccountMapContext'; 8 | 9 | import { getLastTransactionTime, getAccountBalance } from 'src/pages/Home/utils'; 10 | 11 | import { LilyAccount } from '@lily/types'; 12 | interface Props { 13 | url: string; 14 | account: LilyAccount; 15 | } 16 | 17 | export const AccountGridItem = ({ url, account }: Props) => { 18 | const { setCurrentAccountId } = useContext(AccountMapContext); 19 | return ( 20 |

  • 21 | setCurrentAccountId(account.config.id)} 25 | key={account.config.id} 26 | > 27 |
    28 |
    29 | 30 |
    31 |

    32 | {account.name} 33 |

    34 | {account.loading && ( 35 | 36 | Loading... 37 | 38 | )} 39 | {!account.loading && ( 40 | 41 | 42 | 43 | )} 44 | {!account.loading && ( 45 | 46 | {getLastTransactionTime(account)} 47 | 48 | )} 49 |
    50 |
    51 |
    52 | 53 |
  • 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Home/AddNewAccountGridItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { AddCircleOutline } from '@styled-icons/material'; 4 | 5 | import { AccountMapContext } from 'src/context/AccountMapContext'; 6 | 7 | export const AddNewAccountGridItem = () => { 8 | const { accountMap } = useContext(AccountMapContext); 9 | return ( 10 |
  • 11 | 15 |
    16 |
    17 | 18 |
    19 |

    20 | Add a new account 21 |

    22 | 23 | Create a new account to send, receive, and manage bitcoin 24 | 25 |
    26 |
    27 |
    28 | 29 | {!accountMap.size &&
    } 30 |
  • 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Home/AddNewAccountListItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { AddCircleOutline } from '@styled-icons/material'; 4 | 5 | export const AddNewAccountListItem = () => { 6 | return ( 7 |
  • 8 | 9 |
    10 |
    11 |
    12 | 13 |
    14 |
    15 |
    16 |

    17 | Add a new account 18 |

    19 |
    20 |
    21 |
    22 |
    23 | 24 |
  • 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState, useEffect } from 'react'; 2 | import { useSpring, animated } from 'react-spring'; 3 | import BigNumber from 'bignumber.js'; 4 | 5 | import { PageWrapper } from '../../components'; 6 | 7 | import { AccountsSection } from './AccountsSection'; 8 | import { HistoricChart } from './HistoricChart'; 9 | 10 | const Home = ({ historicalBitcoinPrice, currentBitcoinPrice, flyInAnimation, prevFlyInAnimation }: { historicalBitcoinPrice: any, currentBitcoinPrice: BigNumber, flyInAnimation: boolean, prevFlyInAnimation: boolean }) => { 11 | const [initialLoad, setInitialLoad] = useState(false); 12 | 13 | useEffect(() => { 14 | if (flyInAnimation !== prevFlyInAnimation) { // if these values are different, change local 15 | setInitialLoad(true) 16 | } 17 | }, [flyInAnimation, prevFlyInAnimation]); 18 | 19 | const chartProps = useSpring({ transform: initialLoad || (flyInAnimation === false && prevFlyInAnimation === false) ? 'translateY(0%)' : 'translateY(-120%)' }); 20 | const accountsProps = useSpring({ transform: initialLoad || (flyInAnimation === false && prevFlyInAnimation === false) ? 'translateY(0%)' : 'translateY(120%)' }); 21 | 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ) 35 | }; 36 | 37 | export default Home -------------------------------------------------------------------------------- /apps/frontend/src/pages/Home/utils.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import { 4 | LilyAccount, 5 | LilyLightningAccount, 6 | LilyOnchainAccount, 7 | Transaction, 8 | LightningEvent 9 | } from '@lily/types'; 10 | 11 | export const getLastTransactionTime = (account: LilyAccount) => { 12 | if (account.config.type === 'onchain') { 13 | return getLastTransactionTimeOnchain((account as LilyOnchainAccount).transactions); 14 | } else { 15 | return getLastTransactionTimeLightning((account as LilyLightningAccount).events); 16 | } 17 | }; 18 | 19 | export const getLastTransactionTimeOnchain = (transactions: Transaction[]) => { 20 | if (transactions.length === 0) { 21 | // if no transactions yet 22 | return `No activity on this account yet`; 23 | } else if (!transactions[0].status.confirmed) { 24 | // if last transaction isn't confirmed yet 25 | return `Last transaction was moments ago`; 26 | } else { 27 | // if transaction is confirmed, give moments ago 28 | return `Last transaction was ${moment.unix(transactions[0].status.block_time).fromNow()}`; 29 | } 30 | }; 31 | 32 | export const getLastTransactionTimeLightning = (events: LightningEvent[]) => { 33 | if (!events || events.length === 0) { 34 | // if no transactions yet 35 | return `No activity on this account yet`; 36 | } else if (!events[0].creationDate) { 37 | return 'Last transaction was moments ago'; 38 | } else { 39 | // if transaction is confirmed, give moments ago 40 | return `Last transaction was ${moment.unix(Number(events[0].creationDate)).fromNow()}`; 41 | } 42 | }; 43 | 44 | export const getAccountBalance = (account: LilyAccount) => { 45 | if (account.config.type === 'onchain') { 46 | return (account as LilyOnchainAccount).currentBalance; 47 | } else { 48 | return Number((account as LilyLightningAccount).currentBalance.balance); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Lightning/Channels/ChannelView/ChannelModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { ModalContentWrapper } from 'src/components'; 4 | 5 | import { DecoratedLightningChannel, DecoratedPendingLightningChannel } from '@lily/types'; 6 | 7 | import ChannelDetailsModal from './ChannelDetailsModal'; 8 | import CloseChannelModal from './CloseChannel/CloseChannelModal'; 9 | import CloseChannelSuccess from './CloseChannel/CloseChannelSuccess'; 10 | 11 | interface Props { 12 | channel: DecoratedLightningChannel | DecoratedPendingLightningChannel; 13 | } 14 | 15 | const ChannelModal = ({ channel }: Props) => { 16 | const [step, setStep] = useState(0); 17 | 18 | return ( 19 | 20 | {step === 0 && } 21 | {step === 1 && ( 22 | 23 | )} 24 | {step === 2 && } 25 | 26 | ); 27 | }; 28 | 29 | export default ChannelModal; 30 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Lightning/Channels/ChannelView/CloseChannel/CloseChannelSuccess.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { CheckCircle } from '@styled-icons/material'; 4 | import { Link } from 'react-router-dom'; 5 | 6 | import { Button, StyledIcon } from 'src/components'; 7 | 8 | import { requireLightning } from 'src/hocs'; 9 | import { white, green500, gray700, gray900 } from 'src/utils/colors'; 10 | import { DecoratedLightningChannel, LilyLightningAccount } from '@lily/types'; 11 | 12 | interface Props { 13 | channel: DecoratedLightningChannel; 14 | currentAccount: LilyLightningAccount; 15 | } 16 | 17 | const CloseChannelSuccess = ({ channel, currentAccount }: Props) => ( 18 | 19 | 20 | 21 | 22 | Close channel success! 23 | You successfully closed your channel to {channel.alias}. 24 | 29 | Return to dashboard 30 | 31 | 32 | ); 33 | 34 | const Wrapper = styled.div` 35 | display: flex; 36 | flex-direction: column; 37 | align-items: center; 38 | background: ${white}; 39 | border-radius: 0.875em; 40 | padding: 1.5em 0.75em; 41 | width: 100%; 42 | `; 43 | 44 | const IconWrapper = styled.div``; 45 | 46 | const SuccessText = styled.div` 47 | margin-top: 0.5em; 48 | font-size: 1.5em; 49 | color: ${gray900}; 50 | font-weight: 500; 51 | `; 52 | 53 | const SuccessSubtext = styled.div` 54 | color: ${gray700}; 55 | margin-top: 0.5rem; 56 | margin-bottom: 1rem; 57 | text-align: center; 58 | `; 59 | 60 | const ReturnToDashboardButton = styled(Link)` 61 | ${Button} 62 | margin-top: 1rem; 63 | `; 64 | 65 | export default requireLightning(CloseChannelSuccess); 66 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Lightning/Channels/OpenChannel/LightningImage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import { gray100, purple700 } from 'src/utils/colors'; 5 | 6 | const IconSvg = styled.svg` 7 | color: ${purple700}; 8 | margin-right: 0.65rem; 9 | flex-shrink: 0; 10 | border-radius: 0.375rem; 11 | object-position: center; 12 | object-fit: cover; 13 | background: ${gray100}; 14 | flex: none; 15 | width: 6rem; 16 | height: 6rem; 17 | max-width: 100%; 18 | display: block; 19 | vertical-align: middle; 20 | border-style: solid; 21 | padding: 1em; 22 | `; 23 | 24 | const LightningImage = () => ( 25 | 26 | 32 | 33 | ); 34 | 35 | export default LightningImage; 36 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Lightning/Channels/OpenChannel/OpenChannelSuccess.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { CheckCircle } from '@styled-icons/material'; 4 | import { Link } from 'react-router-dom'; 5 | 6 | import { Button, StyledIcon } from 'src/components'; 7 | 8 | import { white, green500, gray700 } from 'src/utils/colors'; 9 | 10 | import { requireLightning } from 'src/hocs'; 11 | import { LilyLightningAccount } from '@lily/types'; 12 | 13 | interface Props { 14 | currentAccount: LilyLightningAccount; 15 | } 16 | 17 | const OpenChannelSuccess = ({ currentAccount }: Props) => ( 18 |
    19 |
    20 | 21 |
    22 |
    23 |

    24 | New channel opened! 25 |

    26 |
    27 |

    You just opened a new lightning network channel.

    28 |
    29 |
    30 |
    31 | 36 | Return to dashboard 37 | 38 |
    39 |
    40 | ); 41 | 42 | const ReturnToDashboardButton = styled(Link)` 43 | ${Button} 44 | `; 45 | 46 | export default requireLightning(OpenChannelSuccess); 47 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Lightning/Channels/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import ChannelView from './ChannelView'; 4 | import OpenChannel from './OpenChannel'; 5 | 6 | const Channels = () => { 7 | const [viewOpenChannelForm, setViewOpenChannelForm] = useState(false); 8 | let view = ; 9 | 10 | if (viewOpenChannelForm) { 11 | view = ; 12 | } 13 | return view; 14 | }; 15 | 16 | export default Channels; 17 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Lightning/RecentActivity/PaymentTypeIcon.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import styled from 'styled-components'; 3 | import { 4 | VerticalAlignBottom, 5 | ArrowUpward, 6 | OpenInFull, 7 | CloseFullscreen 8 | } from '@styled-icons/material'; 9 | 10 | import { StyledIcon } from 'src/components'; 11 | 12 | import { green400, gray500, red500 } from 'src/utils/colors'; 13 | import { LightningEvent } from '@lily/types'; 14 | 15 | interface Props { 16 | type: LightningEvent['type']; 17 | } 18 | 19 | const PaymentTypeIcon = ({ type }: Props) => { 20 | return ( 21 | 22 | {type === 'PAYMENT_RECEIVE' && ( 23 | 24 | )} 25 | {type === 'PAYMENT_SEND' && } 26 | {type === 'CHANNEL_OPEN' && } 27 | {type === 'CHANNEL_CLOSE' && ( 28 | 29 | )} 30 | 31 | ); 32 | }; 33 | 34 | const StyledIconModified = styled(StyledIcon)<{ 35 | type: LightningEvent['type']; 36 | }>` 37 | padding: 0.5em; 38 | margin-right: 0.75em; 39 | background: ${(p) => 40 | p.type === 'CHANNEL_OPEN' || p.type === 'CHANNEL_CLOSE' 41 | ? gray500 42 | : p.type === 'PAYMENT_SEND' 43 | ? green400 44 | : red500}; 45 | border-radius: 50%; 46 | ` as any; // TODO: fix 47 | 48 | export default PaymentTypeIcon; 49 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Lightning/Settings/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { Tabs } from 'src/components'; 4 | 5 | import GeneralView from './GeneralView'; 6 | import ChannelsView from '../Channels'; 7 | import ExportView from './ExportView'; 8 | 9 | const LightningSettings = () => { 10 | const [currentTab, setCurrentTab] = useState('general'); 11 | 12 | const tabItems = [ 13 | { name: 'General', tabId: 'general' }, 14 | { name: 'Channels', tabId: 'channels' } 15 | ]; 16 | 17 | return ( 18 |
    19 |
    20 |

    Settings

    21 | 22 | {currentTab === 'general' && } 23 | {currentTab === 'channels' && } 24 | {currentTab === 'export' && } 25 |
    26 |
    27 | ); 28 | }; 29 | 30 | export default LightningSettings; 31 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Lightning/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Switch, Route, useRouteMatch } from 'react-router-dom'; 3 | 4 | import { PageWrapper } from 'src/components'; 5 | 6 | import LightningHeader from './LightningHeader'; 7 | import LightningView from './LightningView'; 8 | import LightningSettings from './Settings'; 9 | 10 | interface Props { 11 | toggleRefresh(): void; 12 | } 13 | 14 | const Lightning = ({ toggleRefresh }: Props) => { 15 | let { path } = useRouteMatch(); 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | } /> 23 | } /> 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default Lightning; 31 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Lightning/utils.ts: -------------------------------------------------------------------------------- 1 | export const getFriendlyType = ( 2 | type: 'CHANNEL_OPEN' | 'CHANNEL_CLOSE' | 'PAYMENT_SEND' | 'PAYMENT_RECEIVE' 3 | ) => { 4 | if (type === 'PAYMENT_SEND') { 5 | return 'Sent'; 6 | } else if (type === 'PAYMENT_RECEIVE') { 7 | return 'Received'; 8 | } else if (type === 'CHANNEL_OPEN') { 9 | return 'Open channel'; 10 | } else { 11 | return 'Close channel'; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Receive/Lightning/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { requireLightning } from 'src/hocs'; 4 | 5 | import LightningReceiveQr from './LightningReceiveQr'; 6 | import LightningReceiveForm from './LightningReceiveForm'; 7 | import LightningReceiveSuccess from './LightningReceiveSuccess'; 8 | 9 | export const LightningReceive = () => { 10 | const [step, setStep] = useState(0); 11 | const [invoice, setInvoice] = useState(''); 12 | 13 | let view: JSX.Element; 14 | if (step === 0) { 15 | view = ; 16 | } else if (step === 1) { 17 | view = ; 18 | } else { 19 | view = ; 20 | } 21 | 22 | return view; 23 | }; 24 | 25 | export default requireLightning(LightningReceive); 26 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Receive/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useContext, useEffect } from 'react'; 2 | 3 | import { 4 | PageWrapper, 5 | PageTitle, 6 | Header, 7 | HeaderRight, 8 | HeaderLeft, 9 | NoAccountsEmptyState 10 | } from 'src/components'; 11 | 12 | import { AccountMapContext } from 'src/context/AccountMapContext'; 13 | 14 | import OnchainReceive from './OnchainReceive'; 15 | import LightningReceive from './Lightning'; 16 | 17 | const Receive = () => { 18 | const { accountMap, currentAccount, setCurrentAccountId } = useContext(AccountMapContext); 19 | const hasAccount = Object.keys(accountMap).length > 0; 20 | 21 | useEffect(() => { 22 | if (currentAccount.name === 'Loading...' && hasAccount) { 23 | setCurrentAccountId(Object.keys(accountMap)[0]); 24 | } 25 | }, []); 26 | 27 | return ( 28 | 29 |
    30 |
    31 | 32 | Receive bitcoin 33 | 34 | 35 |
    36 | 37 | {!hasAccount && } 38 | {currentAccount.config.type === 'onchain' && } 39 | {currentAccount.config.type === 'lightning' && } 40 |
    41 |
    42 | ); 43 | }; 44 | 45 | export default Receive; 46 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Send/Lightning/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import { PageTitle, Header, HeaderRight, HeaderLeft } from 'src/components'; 5 | 6 | import LightningSendTxForm from './LightningSendTxForm'; 7 | import LightningPaymentConfirm from './LightningPaymentConfirm'; 8 | 9 | import { gray500 } from 'src/utils/colors'; 10 | 11 | import { LilyLightningAccount } from '@lily/types'; 12 | 13 | interface Props { 14 | currentAccount: LilyLightningAccount; 15 | } 16 | 17 | const SendLightning = ({ currentAccount }: Props) => { 18 | const [step, setStep] = useState(0); 19 | const [paymentRequest, setPaymentRequest] = useState(''); 20 | 21 | return ( 22 |
    23 |
    24 | 25 | Send bitcoin 26 | 27 | 28 |
    29 | {step === 0 && ( 30 | 35 | )} 36 | 37 | {step === 1 && ( 38 | 43 | )} 44 |
    45 | ); 46 | }; 47 | 48 | export const InputStaticText = styled.label<{ 49 | text: string; 50 | disabled: boolean; 51 | }>` 52 | position: relative; 53 | display: flex; 54 | flex: 0 0; 55 | justify-self: center; 56 | align-self: center; 57 | margin-left: -87px; 58 | z-index: 1; 59 | margin-right: 40px; 60 | font-size: 1.5em; 61 | font-weight: 100; 62 | color: ${gray500}; 63 | 64 | &::after { 65 | content: ${(p) => p.text}; 66 | position: absolute; 67 | top: 4px; 68 | left: 94px; 69 | font-family: arial, helvetica, sans-serif; 70 | font-size: 0.75em; 71 | display: block; 72 | color: rgba(0, 0, 0, 0.6); 73 | font-weight: bold; 74 | } 75 | `; 76 | 77 | export default SendLightning; 78 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Send/Onchain/AddSignatureFromQrCode/DecodePsbtQrCode.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState } from "react"; 2 | import styled from "styled-components"; 3 | import { V2 } from "@cvbb/qr-protocol/dist"; 4 | import BarcodeScannerComponent from "react-webcam-barcode-scanner"; 5 | 6 | interface Props { 7 | importSignatureFromFile: (file: string) => void; 8 | } 9 | 10 | interface KeyValue { 11 | [key: string]: string; 12 | } 13 | 14 | const DecodePsbtQrCode = ({ importSignatureFromFile }: Props) => { 15 | const [barcodeData, setBarcodeData] = useState({} as KeyValue); 16 | 17 | const readQrData = (data: string) => { 18 | const parsed = data.split("/"); 19 | const numPieces = parsed[1].split("OF")[1]; 20 | barcodeData[parsed[1].substring(0, parsed[1].indexOf("OF"))] = data; 21 | setBarcodeData(barcodeData); 22 | 23 | if (Object.keys(barcodeData).length === parseInt(numPieces, 10)) { 24 | const file = V2.extractQRCode(Object.values(barcodeData)); 25 | importSignatureFromFile(file); 26 | } 27 | }; 28 | 29 | return ( 30 | 31 | 32 | Scan the QR code on your device 33 | 34 | 35 | { 39 | if (result) readQrData(result.getText()); 40 | else return; 41 | }} 42 | /> 43 | 44 | 45 | ); 46 | }; 47 | 48 | const BarcodeScannerComponentStyled = styled(BarcodeScannerComponent)` 49 | width: 100%; 50 | `; 51 | 52 | const ModalHeaderContainer = styled.div` 53 | border-bottom: 1px solid rgb(229, 231, 235); 54 | padding-top: 1.25rem; 55 | padding-bottom: 1.25rem; 56 | padding-left: 1.5rem; 57 | padding-right: 1.5rem; 58 | display: flex; 59 | align-items: center; 60 | justify-content: space-between; 61 | font-size: 1.5em; 62 | `; 63 | 64 | const ModalContent = styled.div``; 65 | 66 | export default DecodePsbtQrCode; 67 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Send/Onchain/AddSignatureFromQrCode/PsbtQrCode.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Psbt } from 'bitcoinjs-lib'; 4 | import { V2 } from '@cvbb/qr-protocol/dist'; 5 | 6 | import { AnimatedQrCode } from 'src/components'; 7 | 8 | import { gray100 } from 'src/utils/colors'; 9 | 10 | interface Props { 11 | psbt: Psbt; 12 | } 13 | 14 | const PsbtQrCode = ({ psbt }: Props) => { 15 | const psbtEncoded = V2.constructQRCode(psbt.toHex()); 16 | return ( 17 | <> 18 | 19 | 20 | Scan this with your device 21 | 22 | 23 |
    24 | 25 |
    26 | 27 | ); 28 | }; 29 | 30 | const ModalHeaderContainer = styled.div` 31 | padding-top: 1.75rem; 32 | padding-bottom: 1.75rem; 33 | padding-left: 1.5rem; 34 | padding-right: 1.5rem; 35 | display: flex; 36 | align-items: center; 37 | justify-content: space-between; 38 | font-size: 1.5em; 39 | height: 90px; 40 | `; 41 | 42 | export default PsbtQrCode; 43 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Send/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext } from 'react'; 2 | import { Network } from 'bitcoinjs-lib'; 3 | 4 | import { PageWrapper, PageTitle, Header, HeaderRight, HeaderLeft } from 'src/components'; 5 | import { NoAccountsEmptyState } from 'src/components'; 6 | 7 | import { AccountMapContext } from 'src/context/AccountMapContext'; 8 | 9 | import SendOnchain from './Onchain'; 10 | import SendLightning from './Lightning'; 11 | 12 | import { 13 | LilyConfig, 14 | NodeConfigWithBlockchainInfo, 15 | LilyLightningAccount, 16 | LilyOnchainAccount 17 | } from '@lily/types'; 18 | 19 | interface Props { 20 | config: LilyConfig; 21 | currentBitcoinNetwork: Network; 22 | nodeConfig: NodeConfigWithBlockchainInfo; 23 | currentBitcoinPrice: any; // KBC-TODO: more specific type 24 | } 25 | 26 | const Send = ({ config, currentBitcoinNetwork, nodeConfig, currentBitcoinPrice }: Props) => { 27 | const { accountMap, currentAccount, setCurrentAccountId } = useContext(AccountMapContext); 28 | const hasAccount = Object.keys(accountMap).length > 0; 29 | 30 | useEffect(() => { 31 | if (currentAccount.name === 'Loading...' && hasAccount) { 32 | setCurrentAccountId(Object.keys(accountMap)[0]); 33 | } 34 | }, []); 35 | 36 | return ( 37 | 38 | <> 39 | {!hasAccount && ( 40 | <> 41 |
    42 | 43 | Send bitcoin 44 | 45 | 46 |
    47 | 48 | 49 | )} 50 | 51 | {currentAccount.config.type === 'onchain' && ( 52 | 59 | )} 60 | {currentAccount.config.type === 'lightning' && ( 61 | 62 | )} 63 | 64 |
    65 | ); 66 | }; 67 | 68 | export default Send; 69 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Setup/NewVault/InnerTransition.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Transition } from '@headlessui/react'; 3 | 4 | interface Props { 5 | appear?: boolean; 6 | show: boolean; 7 | children: JSX.Element; 8 | className?: string; 9 | afterLeave?: () => void; 10 | } 11 | 12 | export const InnerTransition = ({ 13 | appear = true, 14 | show, 15 | children, 16 | className, 17 | afterLeave 18 | }: Props) => { 19 | return ( 20 | 32 | {children} 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Setup/NewVault/NoDevicesEmptyState.tsx: -------------------------------------------------------------------------------- 1 | import React, { Ref } from 'react'; 2 | import { ExclamationIcon, ExternalLinkIcon, BeakerIcon } from '@heroicons/react/outline'; 3 | 4 | interface Props {} 5 | 6 | const NoDevicesEmptyState = React.forwardRef((props, ref) => { 7 | return ( 8 |
    9 |
    10 |

    11 | No devices detected 12 |

    13 | 14 |

    15 | Please make sure your device is connected and unlocked. 16 |

    17 | 18 | 24 | 25 | 30 | I am stuck and need assistance 31 | 32 | 33 |
    34 |
    35 | ); 36 | }); 37 | 38 | export default NoDevicesEmptyState; 39 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Setup/PageHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { SetStateNumber } from 'src/types'; 4 | 5 | interface Props { 6 | headerText: string; 7 | setStep: SetStateNumber; 8 | showCancel: boolean; 9 | } 10 | 11 | const PageHeader = ({ headerText, setStep, showCancel }: Props) => { 12 | return ( 13 |
    14 |
    15 |

    New Account

    16 |

    {headerText}

    17 |
    18 |
    19 | {showCancel ? ( 20 | 29 | ) : null} 30 |
    31 |
    32 | ); 33 | }; 34 | 35 | export default PageHeader; 36 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Setup/Review/AccountAlreadyExistsBanner.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import { ExclamationIcon } from '@heroicons/react/solid'; 5 | 6 | import { AccountMapContext } from 'src/context'; 7 | import { AccountId } from '@lily/types'; 8 | 9 | interface Props { 10 | accountId: AccountId; 11 | } 12 | 13 | export default function AccountAlreadyExistsBanner({ accountId }: Props) { 14 | const { setCurrentAccountId } = useContext(AccountMapContext); 15 | 16 | return ( 17 |
    18 |
    19 |
    20 |
    22 |
    23 |

    This account already exists.

    24 |

    25 | setCurrentAccountId(accountId)} 27 | to={`/vault/${accountId}`} 28 | className='whitespace-nowrap font-medium text-yellow-700 hover:text-yellow-600' 29 | > 30 | View account 31 | 32 |

    33 |
    34 |
    35 |
    36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Setup/Review/TransitionSlideLeft.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Transition } from '@headlessui/react'; 3 | 4 | interface Props { 5 | appear?: boolean; 6 | show: boolean; 7 | children: JSX.Element; 8 | className?: string; 9 | } 10 | 11 | const TransitionSlideLeft = ({ appear = true, show, children, className }: Props) => { 12 | return ( 13 | 24 | {children} 25 | 26 | ); 27 | }; 28 | 29 | export default TransitionSlideLeft; 30 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Setup/Review/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import OnchainReview from './OnchainReview'; 4 | import LightningReview from './LightningReview'; 5 | 6 | import { LightningConfig, OnChainConfigWithoutId } from '@lily/types'; 7 | import { ChannelBalanceResponse, GetInfoResponse } from '@lily-technologies/lnrpc'; 8 | 9 | interface Props { 10 | newAccount: OnChainConfigWithoutId | LightningConfig; 11 | setStep: React.Dispatch>; 12 | setupOption: number; 13 | currentBlockHeight: number; 14 | setNewAccount: React.Dispatch>; 15 | tempLightningState: GetInfoResponse & ChannelBalanceResponse; 16 | } 17 | 18 | const ReviewScreen = ({ 19 | setStep, 20 | newAccount, 21 | setupOption, 22 | currentBlockHeight, 23 | setNewAccount, 24 | tempLightningState 25 | }: Props) => { 26 | if (newAccount.type === 'onchain') { 27 | return ( 28 | 35 | ); 36 | } else { 37 | return ( 38 | 43 | ); 44 | } 45 | }; 46 | 47 | export default ReviewScreen; 48 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Setup/TransitionSlideLeft.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Transition } from '@headlessui/react'; 3 | 4 | interface Props { 5 | appear?: boolean; 6 | show: boolean; 7 | children: JSX.Element; 8 | className?: string; 9 | } 10 | 11 | const TransitionSlideLeft = ({ appear = true, show, children, className }: Props) => { 12 | return ( 13 | 24 | {children} 25 | 26 | ); 27 | }; 28 | 29 | export default TransitionSlideLeft; 30 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Setup/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { gray500, gray600, black, green600 } from 'src/utils/colors'; 4 | 5 | export const HeaderWrapper = styled.div` 6 | color: ${black}; 7 | `; 8 | 9 | export const PageTitleSubtext = styled.div` 10 | font-size: 1em; 11 | color: ${gray600}; 12 | `; 13 | 14 | export const CancelButton = styled.div` 15 | color: ${gray500}; 16 | padding: 1em; 17 | cursor: pointer; 18 | `; 19 | 20 | export const XPubHeaderWrapper = styled.div.attrs({ 21 | className: 22 | 'text-gray-900 bg-white dark:text-gray-200 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700' 23 | })` 24 | margin: 0; 25 | display: flex; 26 | justify-content: space-between; 27 | padding: 1.25em; 28 | align-items: flex-start; 29 | `; 30 | 31 | export const SetupHeaderWrapper = styled.div` 32 | display: flex; 33 | justify-content: space-between; 34 | flex: 1; 35 | align-items: flex-start; 36 | `; 37 | 38 | export const SetupHeader = styled.span.attrs({ 39 | className: 'text-gray-900 dark:text-gray-200 font-medium' 40 | })` 41 | font-size: 1.25em; 42 | margin: 4px 0; 43 | `; 44 | 45 | export const SetupExplainerText = styled.div.attrs({ 46 | className: 'text-gray-800 dark:text-gray-300' 47 | })` 48 | font-size: 0.8em; 49 | padding: 0 3em 0 0; 50 | `; 51 | 52 | export const FormContainer = styled.div` 53 | min-height: 33em; 54 | `; 55 | 56 | export const BoxedWrapper = styled.div.attrs({ className: 'bg-white dark:bg-gray-800' })` 57 | border-radius: 0.375rem; 58 | display: flex; 59 | flex-direction: column; 60 | justify-content: space-between; 61 | border-top: 11px solid ${green600}; 62 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); 63 | `; 64 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Vault/RecentTransactions/NoFilteredTransactionsEmptyState.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DeadFlowerImage from 'src/assets/dead-flower.svg'; 3 | 4 | const NoFilteredTransactionsEmptyState = () => ( 5 |
    6 |

    No Transactions

    7 | 8 |

    9 | No transactions match the search criteria. 10 |

    11 |
    12 | ); 13 | 14 | export default NoFilteredTransactionsEmptyState; 15 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Vault/RecentTransactions/NoTransactionsEmptyState.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DeadFlowerImage from 'src/assets/dead-flower.svg'; 3 | 4 | const NoTransactionsEmptyState = () => ( 5 |
    6 |

    No Transactions

    7 | 8 |

    9 | No activity has been detected on this account yet. 10 |

    11 |
    12 | ); 13 | 14 | export default NoTransactionsEmptyState; 15 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Vault/RecentTransactions/TransactionTypeIcon.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import styled from 'styled-components'; 3 | import { VerticalAlignBottom, ArrowUpward } from '@styled-icons/material'; 4 | import { Transfer } from '@styled-icons/boxicons-regular'; 5 | 6 | import { StyledIcon } from 'src/components'; 7 | 8 | import { green400, gray500, red500 } from 'src/utils/colors'; 9 | 10 | import { Transaction } from '@lily/types'; 11 | 12 | interface Props { 13 | transaction: Transaction; 14 | flat: boolean; 15 | } 16 | 17 | const TransactionTypeIcon = ({ transaction, flat }: Props) => { 18 | return ( 19 | 20 | {transaction.type === 'received' && ( 21 | 22 | )} 23 | {transaction.type === 'sent' && } 24 | {transaction.type === 'moved' && ( 25 | 26 | )} 27 | 28 | ); 29 | }; 30 | 31 | const StyledIconModified = styled(StyledIcon)<{ 32 | receive?: boolean; 33 | moved?: boolean; 34 | }>` 35 | padding: 0.5em; 36 | margin-right: 0.75em; 37 | background: ${(p) => (p.moved ? gray500 : p.receive ? green400 : red500)}; 38 | border-radius: 50%; 39 | `; 40 | 41 | export default TransactionTypeIcon; 42 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Vault/Settings/Addresses/AddressRow.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import { SlideOver } from 'src/components'; 5 | 6 | import { LabelTag } from './LabelTag'; 7 | import AddressDetailsSlideover from './AddressDetailsSlideover'; 8 | 9 | import { Address } from '@lily/types'; 10 | 11 | interface Props { 12 | address: Address; 13 | used: boolean; 14 | } 15 | 16 | const AddressRow = ({ address, used }: Props) => { 17 | const [slideoverIsOpen, setSlideoverOpen] = useState(false); 18 | const [slideoverContent, setSlideoverContent] = useState(null); 19 | 20 | const openInSlideover = (component: JSX.Element) => { 21 | setSlideoverOpen(true); 22 | setSlideoverContent(component); 23 | }; 24 | 25 | useEffect(() => { 26 | if (slideoverIsOpen) { 27 | setSlideoverContent( 28 | 29 | ); 30 | } 31 | }, [address]); 32 | 33 | return ( 34 | <> 35 | 57 | 58 | 59 | ); 60 | }; 61 | 62 | const UtxoHeader = styled.div` 63 | font-size: 0.875rem; 64 | line-height: 1.25rem; 65 | font-weight: 500; 66 | `; 67 | 68 | export default AddressRow; 69 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Vault/Settings/Addresses/LabelTag.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AddressTag } from '@lily/types'; 3 | 4 | interface Props { 5 | label: AddressTag; 6 | deleteLabel?: (tag: AddressTag) => void; 7 | } 8 | 9 | export const LabelTag = ({ label, deleteLabel }: Props) => { 10 | return ( 11 | 12 | {label.label} 13 | {deleteLabel ? ( 14 | 26 | ) : null} 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Vault/Settings/Addresses/NoAddressesEmptyState.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DeadFlowerImage from 'src/assets/dead-flower.svg'; 3 | 4 | const NoAddressesEmptyState = () => ( 5 |
    6 |

    No addresses found

    7 | 8 |

    9 | No addresses match the search criteria. 10 |

    11 |
    12 | ); 13 | 14 | export default NoAddressesEmptyState; 15 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Vault/Settings/Addresses/TagsSection.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | 3 | import { LabelTag } from './LabelTag'; 4 | import { AddLabelTag } from './AddLabelTag'; 5 | 6 | import { AccountMapContext } from 'src/context'; 7 | 8 | import { Address, AddressTag } from '@lily/types'; 9 | 10 | interface Props { 11 | addresses: Address[]; 12 | } 13 | 14 | export const TagsSection = ({ addresses }: Props) => { 15 | const { addAddressTag, deleteAddressTag, currentAccount } = useContext(AccountMapContext); 16 | 17 | // TODO: sometimes addresses will have overlapping labels 18 | // TODO: need to consolodate them and send multiple deleteLabel calls 19 | 20 | const labelMap = addresses.reduce<{ [key: string]: AddressTag[] }>((accum, address) => { 21 | for (const tag of address.tags) { 22 | const updatedList = [...(accum[tag.label] || []), tag]; 23 | accum[tag.label] = updatedList; 24 | } 25 | return accum; 26 | }, {}); 27 | 28 | const addLabels = (addresses: Address[], label: string) => { 29 | addresses.forEach((address) => { 30 | addAddressTag(currentAccount.config.id, address.address, label); 31 | }); 32 | }; 33 | 34 | const deleteLabel = async (tag: AddressTag) => { 35 | labelMap[tag.label].map((currentTag) => { 36 | deleteAddressTag(currentAccount.config.id, currentTag); 37 | }); 38 | }; 39 | 40 | return ( 41 |
    42 |
    43 | Tags 44 |
    45 |
      46 | {Object.keys(labelMap).map((label) => ( 47 |
    • 48 | 49 |
    • 50 | ))} 51 | 52 | 53 |
    54 |
    55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Vault/Settings/UTXOs/NoUtxosEmptyState.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DeadFlowerImage from 'src/assets/dead-flower.svg'; 3 | 4 | const NoUtxosEmptyState = () => ( 5 |
    6 |

    No UTXOs found

    7 | 8 |

    9 | No unspent transaction outputs match the search criteria. 10 |

    11 |
    12 | ); 13 | 14 | export default NoUtxosEmptyState; 15 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Vault/Settings/UTXOs/UtxoRow.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Unit } from 'src/components'; 3 | 4 | import { LabelTag } from 'src/pages/Vault/Settings/Addresses/LabelTag'; 5 | 6 | import { LilyOnchainAccount, UTXO } from '@lily/types'; 7 | import { AccountMapContext } from 'src/context'; 8 | import { createMap } from 'src/utils/accountMap'; 9 | interface Props { 10 | utxo: UTXO; 11 | showTags: boolean; 12 | } 13 | 14 | const UtxoRow = ({ utxo, showTags }: Props) => { 15 | const { currentAccount } = useContext(AccountMapContext); 16 | const addressMap = createMap( 17 | [ 18 | ...(currentAccount as LilyOnchainAccount).addresses, 19 | ...(currentAccount as LilyOnchainAccount).changeAddresses 20 | ], 21 | 'address' 22 | ); 23 | const utxoAddress = addressMap[utxo.address.address]; 24 | 25 | return ( 26 |
  • 27 |
    28 | 29 | 30 | 31 |
    32 |
    33 | 34 | {utxo.txid}:{utxo.vout} 35 | 36 | {showTags ? ( 37 |
      41 | {utxoAddress.tags.length ? ( 42 | utxoAddress.tags.map((label) => ( 43 |
    • 44 | 45 |
    • 46 | )) 47 | ) : ( 48 | No tags 49 | )} 50 |
    51 | ) : null} 52 |
    53 |
  • 54 | ); 55 | }; 56 | 57 | export default UtxoRow; 58 | -------------------------------------------------------------------------------- /apps/frontend/src/pages/Vault/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Network } from 'bitcoinjs-lib'; 3 | import { Switch, Route, useRouteMatch } from 'react-router-dom'; 4 | 5 | import { PageWrapper } from '../../components'; 6 | 7 | import VaultView from './VaultView'; 8 | import VaultSettings from './Settings'; 9 | import VaultHeader from './VaultHeader'; 10 | 11 | import { NodeConfigWithBlockchainInfo } from '@lily/types'; 12 | 13 | interface Props { 14 | nodeConfig: NodeConfigWithBlockchainInfo; 15 | toggleRefresh(): void; 16 | currentBitcoinNetwork: Network; 17 | } 18 | 19 | const Vault = ({ nodeConfig, toggleRefresh, currentBitcoinNetwork }: Props) => { 20 | let { path } = useRouteMatch(); 21 | 22 | return ( 23 | 24 | 25 | 26 | 27 | ( 30 | 34 | )} 35 | /> 36 | } 39 | /> 40 | 41 | 42 | 43 | ); 44 | }; 45 | 46 | export default Vault; 47 | -------------------------------------------------------------------------------- /apps/frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/frontend/src/utils/media.js: -------------------------------------------------------------------------------- 1 | import { css } from "styled-components"; 2 | 3 | export const mobile = (inner) => css` 4 | @media (max-width: ${1000 / 16}em) { 5 | ${inner}; 6 | } 7 | `; 8 | 9 | export const phone = (inner) => css` 10 | @media (max-width: ${650 / 16}em) { 11 | ${inner}; 12 | } 13 | `; 14 | 15 | export const sm = (inner) => css` 16 | @media (min-width: 640px) { 17 | ${inner}; 18 | } 19 | `; 20 | 21 | export const md = (inner) => css` 22 | @media (min-width: 768px) { 23 | ${inner}; 24 | } 25 | `; 26 | 27 | export const lg = (inner) => css` 28 | @media (min-width: 1024px) { 29 | ${inner}; 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /apps/frontend/src/utils/rem.js: -------------------------------------------------------------------------------- 1 | import rem from 'polished/lib/helpers/rem'; 2 | 3 | const _rem = size => rem(size, '18px'); 4 | 5 | export default _rem; -------------------------------------------------------------------------------- /apps/frontend/src/utils/useLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | // Hook 4 | export function useLocalStorage(key: string, initialValue: T) { 5 | // State to store our value 6 | // Pass initial state function to useState so logic is only executed once 7 | const [storedValue, setStoredValue] = useState(() => { 8 | if (typeof window === 'undefined') { 9 | return initialValue; 10 | } 11 | try { 12 | // Get from local storage by key 13 | const item = window.localStorage.getItem(key); 14 | // Parse stored json or if none return initialValue 15 | return item ? JSON.parse(item) : initialValue; 16 | } catch (error) { 17 | // If error also return initialValue 18 | console.log(error); 19 | return initialValue; 20 | } 21 | }); 22 | // Return a wrapped version of useState's setter function that ... 23 | // ... persists the new value to localStorage. 24 | const setValue = (value: T | ((val: T) => T)) => { 25 | try { 26 | // Allow value to be a function so we have same API as useState 27 | const valueToStore = value instanceof Function ? value(storedValue) : value; 28 | // Save state 29 | setStoredValue(valueToStore); 30 | // Save to local storage 31 | if (typeof window !== 'undefined') { 32 | window.localStorage.setItem(key, JSON.stringify(valueToStore)); 33 | } 34 | } catch (error) { 35 | // A more advanced implementation would handle the error case 36 | console.log(error); 37 | } 38 | }; 39 | return [storedValue, setValue] as const; 40 | } 41 | -------------------------------------------------------------------------------- /apps/frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { default: flattenColorPalette } = require('tailwindcss/lib/util/flattenColorPalette'); 2 | 3 | module.exports = { 4 | purge: ['./src/**/*.{js,jsx,ts,tsx}'], 5 | darkMode: 'media', 6 | mode: 'jit', 7 | theme: { 8 | extend: { 9 | animation: { 10 | xBounce: 'xBounce 1s ease-in-out infinite', 11 | float: 'float 4s ease-in-out infinite' 12 | }, 13 | colors: { 14 | slate: { 15 | 50: '#f8fafc', 16 | 100: '#f1f5f9', 17 | 200: '#e2e8f0', 18 | 300: '#cbd5e1', 19 | 400: '#94a3b8', 20 | 500: '#64748b', 21 | 600: '#475569', 22 | 700: '#334155', 23 | 800: '#1e293b', 24 | 900: '#0f172a' 25 | } 26 | }, 27 | keyframes: { 28 | xBounce: { 29 | '0%, 100%': { transform: 'translateX(0.1rem)' }, 30 | '50%': { transform: 'translateX(0rem)' } 31 | }, 32 | float: { 33 | '0%, 100%': { transform: 'translateY(0.4rem)' }, 34 | '50%': { transform: 'translateY(0rem)' } 35 | } 36 | }, 37 | transitionProperty: { 38 | height: 'height' 39 | } 40 | } 41 | }, 42 | variants: { 43 | extend: { 44 | borderWidth: ['focus-within'], 45 | scale: ['group-hover'], 46 | rotate: ['group-hover'], 47 | transform: ['group-hover'], 48 | translate: ['group-hover'], 49 | transformOrigin: ['group-hover'] 50 | } 51 | }, 52 | plugins: [ 53 | require('@tailwindcss/forms'), 54 | function ({ matchUtilities, theme }) { 55 | matchUtilities( 56 | { 57 | highlight: (value) => ({ boxShadow: `inset 0 1px 0 0 ${value}` }) 58 | }, 59 | { values: flattenColorPalette(theme('backgroundColor')), type: 'color' } 60 | ); 61 | } 62 | ] 63 | }; 64 | -------------------------------------------------------------------------------- /apps/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "sourceMap": true, 5 | "strictNullChecks": true, 6 | "module": "esnext", 7 | "jsx": "react", 8 | "target": "esnext", 9 | "allowJs": true, 10 | "baseUrl": ".", 11 | "lib": [ 12 | "dom", 13 | "dom.iterable", 14 | "esnext" 15 | ], 16 | "skipLibCheck": true, 17 | "esModuleInterop": true, 18 | "allowSyntheticDefaultImports": true, 19 | "strict": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "moduleResolution": "node", 22 | "resolveJsonModule": true, 23 | "isolatedModules": true, 24 | "noEmit": true, 25 | "noImplicitAny": false, 26 | "noFallthroughCasesInSwitch": true, 27 | "typeRoots": [ 28 | "node_modules/@types", 29 | "../../packages/types/dist/**" 30 | ] 31 | }, 32 | "references": [ 33 | { 34 | "path": "../../packages/types" 35 | } 36 | ], 37 | "include": [ 38 | "src/**/*" 39 | ], 40 | "exclude": [ 41 | "src/__tests__/*", 42 | "dist" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // babel.config.js 2 | module.exports = { 3 | presets: [ 4 | [ 5 | "env", 6 | { 7 | targets: { 8 | node: "current", 9 | }, 10 | }, 11 | ], 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | cypress: cypress-io/cypress@1 4 | jobs: 5 | build: 6 | docker: 7 | - image: cypress/base:12.14.0 8 | 9 | working_directory: ~/repo 10 | 11 | steps: 12 | - checkout 13 | 14 | - restore_cache: 15 | keys: 16 | - v1-dependencies-{{ checksum "package.json" }} 17 | # fallback to using the latest cache if no exact match is found 18 | - v1-dependencies- 19 | - run: npm install 20 | 21 | - save_cache: 22 | paths: 23 | - node_modules 24 | - ~/.npm 25 | - ~/.cache 26 | key: v1-dependencies-{{ checksum "package.json" }} 27 | 28 | - run: npm run ci:cypress-run 29 | -------------------------------------------------------------------------------- /default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name frontend; 4 | 5 | location / { 6 | root /usr/share/nginx/html; 7 | index index.html index.htm; 8 | } 9 | 10 | error_page 500 502 503 504 /50x.html; 11 | location = /50x.html { 12 | root /usr/share/nginx/html; 13 | } 14 | 15 | # 16 | # CORS config for nginx 17 | # 18 | location /services { 19 | 20 | # 21 | # the request made to localhost/services are enabled to CORS 22 | # 23 | add_header 'Access-Control-Allow-Origin' '*'; 24 | 25 | # 26 | # the request made to localhost/services forwards to backend:8080 service 27 | # 28 | proxy_pass http://express:8080; 29 | } 30 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | lily-wallet: 5 | privileged: true 6 | restart: on-failure 7 | volumes: 8 | - ${APP_DATA_DIR}/config 9 | - ${LND_DATA_DIR}/:/lnd:ro 10 | - /dev/bus:/dev/bus:ro 11 | build: 12 | dockerfile: ./Dockerfile 13 | context: ./ 14 | environment: 15 | - EXPRESS_PORT=8080 16 | - ELECTRUM_IP=electrum1.bluewallet.io 17 | - ELECTRUM_PORT=50001 18 | - APP_PASSWORD=testtest 19 | - APP_DATA_DIR=${APP_DATA_DIR}/data/lily-wallet:/data 20 | - LND_IP=$LND_IP 21 | - LND_GRPC_PORT=$LND_GRPC_PORT 22 | - LND_WALLET_NAME='LND_WALLET_NAME' 23 | ports: 24 | - '42069:8080' 25 | device_cgroup_rules: 26 | - 'c 189:* rmw' 27 | -------------------------------------------------------------------------------- /docker-compose:umbrel.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | lily-wallet: 5 | image: kaybesee/lily-wallet:latest 6 | restart: on-failure 7 | volumes: 8 | - ${APP_DATA_DIR} 9 | - ${LND_DATA_DIR}/:/lnd:ro 10 | - /dev/bus:/dev/bus:ro 11 | - /run/udev:/run/udev:ro 12 | environment: 13 | - EXPRESS_PORT=8080 14 | - ELECTRUM_IP=$ELECTRUM_IP 15 | - ELECTRUM_PORT=$ELECTRUM_PORT 16 | - APP_PASSWORD=$APP_PASSWORD 17 | - APP_DATA_DIR=${APP_DATA_DIR} 18 | - LND_IP=$LND_IP 19 | - LND_GRPC_PORT=$LND_GRPC_PORT 20 | devices: 21 | - /dev/bus/usb 22 | ports: 23 | - '42069:8080' 24 | device_cgroup_rules: 25 | - 'c 189:* rmw' -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | This document describes the process for running this application on your local computer. 4 | 5 | ## Getting started 6 | 7 | Lily Wallet is a monorepo consisting of a frontend React application, an Electron app, an Express server, and some shared functions and types. 8 | 9 | The frontend runs either within the Electron application or against the Express server depending on whether you are running Lily Wallet as a desktop application or web app like Umbrel. 10 | 11 | There are a few different scripts in the root package.json file that will orchestrate getting your development environment up and running. 12 | 13 | ``` 14 | git clone https://github.com/Lily-Technologies/lily-wallet.git 15 | cd lily-wallet 16 | npm install 17 | 18 | npm run build:types 19 | npm run build:shared-server 20 | 21 | npm run build:electron 22 | 23 | npm run dev:frontend:electron 24 | # in a different terminal window (CMD + D on Mac), run the following command: 25 | npm run dev:electron 26 | ``` 27 | 28 | An Electron window should open up. You now have a running desktop application! 29 | 30 | When you're ready to stop your local application, type Ctrl+C in your terminal window. 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "version": "1.4.0", 4 | "name": "lily-wallet", 5 | "author": "Lily Technologies, Inc. (https://lily-wallet.com)", 6 | "description": "Lily is the best way to secure your Bitcoin", 7 | "license": "Custom", 8 | "scripts": { 9 | "electron": "npm run start -w @lily/electron", 10 | "dev:electron": "npm run start-dev -w @lily/electron", 11 | "frontend": "npm run start:dev -w @lily/frontend", 12 | "dev:frontend:electron": "npm run start:dev:electron -w @lily/frontend", 13 | "dev:frontend:umbrel": "npm run start:dev:umbrel -w @lily/frontend", 14 | "dev:start": "npm run build:types && npm run build:shared-server && npm run build:electron && npm run dev:frontend:electron", 15 | "express": "npm run start -w @lily/express", 16 | "build:electron": "npm run build -w @lily/electron", 17 | "build:frontend:electron": "npm run build:electron -w @lily/frontend", 18 | "build:frontend:umbrel": "npm run build:umbrel -w @lily/frontend", 19 | "build:express": "npm run build -w @lily/express", 20 | "build:types": "npm run build -w @lily/types", 21 | "release:umbrel": "docker buildx build --file Dockerfile --platform linux/arm64,linux/amd64 --tag kaybesee/lily-wallet:latest --output 'type=registry' .", 22 | "build:shared-server": "npm run build -w @lily/shared-server", 23 | "dist:electron": "npm run dist -w @lily/electron", 24 | "pack:electron": "npm run pack -w @lily/electron" 25 | }, 26 | "homepage": "./", 27 | "workspaces": [ 28 | "apps/*", 29 | "packages/*" 30 | ], 31 | "devDependencies": { 32 | "typescript": "^4.5.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/HWIs/HWI_LINUX/HWI_LINUX: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/packages/HWIs/HWI_LINUX/HWI_LINUX -------------------------------------------------------------------------------- /packages/HWIs/HWI_MAC/HWI_MAC: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/packages/HWIs/HWI_MAC/HWI_MAC -------------------------------------------------------------------------------- /packages/HWIs/HWI_MAC/HWI_MAC_BITGO: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/packages/HWIs/HWI_MAC/HWI_MAC_BITGO -------------------------------------------------------------------------------- /packages/HWIs/HWI_PI: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/packages/HWIs/HWI_PI -------------------------------------------------------------------------------- /packages/HWIs/HWI_WINDOWS/HWI_BITGO.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/packages/HWIs/HWI_WINDOWS/HWI_BITGO.exe -------------------------------------------------------------------------------- /packages/HWIs/HWI_WINDOWS/hwi.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/packages/HWIs/HWI_WINDOWS/hwi.exe -------------------------------------------------------------------------------- /packages/shared-server/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.4.0](https://github.com/Lily-Technologies/lily-wallet/compare/shared-server-v1.3.0...shared-server-v1.4.0) (2023-07-19) 4 | 5 | 6 | ### Features 7 | 8 | * **Bitgo:** support bitgo vaults ([baece25](https://github.com/Lily-Technologies/lily-wallet/commit/baece25843eb7a294ea3405c517b667121459248)) 9 | 10 | 11 | ### Dependencies 12 | 13 | * The following workspace dependencies were updated 14 | * dependencies 15 | * @lily/types bumped from 1.3.0 to 1.4.0 16 | 17 | ## 1.3.0 (2022-12-07) 18 | 19 | 20 | ### Features 21 | 22 | * **Lightning:** specify outgoing channel id ([#111](https://github.com/Lily-Technologies/lily-wallet/issues/111)) ([30a9d7c](https://github.com/Lily-Technologies/lily-wallet/commit/30a9d7c05ea01fb238329528a29c9cc755ef4a1b)) 23 | 24 | 25 | ### Dependencies 26 | 27 | * The following workspace dependencies were updated 28 | * dependencies 29 | * @lily/types bumped from * to 1.3.0 30 | -------------------------------------------------------------------------------- /packages/shared-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lily/shared-server", 3 | "version": "1.4.0", 4 | "author": "Lily Technologies, Inc. (https://lily-wallet.com)", 5 | "description": "Lily is the best way to secure your Bitcoin", 6 | "license": "Custom", 7 | "private": true, 8 | "main": "./dist/index.js", 9 | "types": "./dist/index.d.ts", 10 | "scripts": { 11 | "build": "tsc -b" 12 | }, 13 | "dependencies": { 14 | "@lily-technologies/electrum-client": "^1.1.8", 15 | "@lily-technologies/lnrpc": "^0.14.1-beta.14", 16 | "agent-base": "^6.0.2", 17 | "app-root-dir": "^1.0.2", 18 | "axios": "^0.24.0", 19 | "bignumber.js": "^9.0.1", 20 | "bitcoin-simple-rpc": "^0.0.3", 21 | "bitcoinjs-lib": "^6.0.1", 22 | "detect-rpi": "^1.4.0", 23 | "lndconnect": "^0.2.10", 24 | "socks-proxy-agent": "^6.1.0", 25 | "sqlite": "^4.1.2", 26 | "sqlite3": "^5.1.2", 27 | "typescript": "^4.4.4", 28 | "unchained-bitcoin": "^0.1.8" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/shared-server/src/HWI/commands.ts: -------------------------------------------------------------------------------- 1 | import { runCommand } from './runCommand'; 2 | 3 | export const enumerate = async () => { 4 | const response = await runCommand(['enumerate']); 5 | return response; 6 | }; 7 | 8 | export const getMasterXPub = async (deviceType: string, devicePath: string, testnet: boolean) => { 9 | if (testnet) 10 | return await runCommand(['-t', deviceType, '-d', devicePath, '--testnet', 'getmasterxpub']); 11 | else return await runCommand(['-t', deviceType, '-d', devicePath, 'getmasterxpub']); 12 | }; 13 | 14 | export const getXPub = async ( 15 | deviceType: string, 16 | devicePath: string, 17 | path: string, 18 | testnet: boolean 19 | ) => { 20 | if (testnet) 21 | return await runCommand(['-t', deviceType, '-d', devicePath, '--testnet', 'getxpub', path]); 22 | else return await runCommand(['-t', deviceType, '-d', devicePath, 'getxpub', path]); 23 | }; 24 | 25 | export const signtx = async ( 26 | deviceType: string, 27 | devicePath: string, 28 | psbt: string, 29 | testnet: boolean, 30 | bitgo: boolean 31 | ) => { 32 | if (testnet) 33 | return await runCommand( 34 | ['-t', deviceType, '-d', devicePath, '--testnet', 'signtx', psbt], 35 | bitgo 36 | ); 37 | else return await runCommand(['-t', deviceType, '-d', devicePath, 'signtx', psbt], bitgo); 38 | }; 39 | 40 | export const displayaddress = async ( 41 | deviceType: string, 42 | devicePath: string, 43 | path: string, 44 | testnet: boolean 45 | ) => { 46 | if (testnet) 47 | return await runCommand([ 48 | '-t', 49 | deviceType, 50 | '-d', 51 | devicePath, 52 | '--testnet', 53 | 'displayaddress', 54 | path 55 | ]); 56 | else return await runCommand(['-t', deviceType, '-d', devicePath, 'displayaddress', path]); 57 | }; 58 | 59 | export const promptpin = async (deviceType: string, devicePath: string) => { 60 | return await runCommand(['-t', deviceType, '-d', devicePath, 'promptpin']); 61 | }; 62 | 63 | export const sendpin = async (deviceType: string, devicePath: string, pin: string) => { 64 | return await runCommand(['-t', deviceType, '-d', devicePath, 'sendpin', pin]); 65 | }; 66 | -------------------------------------------------------------------------------- /packages/shared-server/src/HWI/runCommand.ts: -------------------------------------------------------------------------------- 1 | import { get as getAppRootDir } from 'app-root-dir'; 2 | import { execFile } from 'child_process'; 3 | import { join, resolve as pathResolve, dirname } from 'path'; 4 | import { platform } from 'os'; 5 | import isPi from 'detect-rpi'; 6 | 7 | export const runCommand = async (command: string[], bitgo?: boolean): Promise => { 8 | return new Promise((resolve, reject) => { 9 | let hwiFile = 'hwi.exe'; 10 | if (bitgo) hwiFile = 'HWI_BITGO.exe'; 11 | if (platform() === 'linux') hwiFile = 'HWI_LINUX'; // BITGO not supported 12 | if (platform() === 'darwin') hwiFile = 'HWI_MAC'; 13 | if (platform() === 'darwin' && bitgo) hwiFile = 'HWI_MAC_BITGO'; 14 | if (isPi()) hwiFile = 'HWI_PI'; 15 | const appRootDir = getAppRootDir(); 16 | 17 | const binariesPath = join(`${appRootDir}/build`, './HWIs'); 18 | const pathToHwi = pathResolve(join(binariesPath, hwiFile)); 19 | 20 | execFile(pathToHwi, command, (error, stdout, stderr) => { 21 | if (error) { 22 | reject(error.message); 23 | } 24 | resolve(stdout); 25 | }); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/shared-server/src/LightningProviders/index.ts: -------------------------------------------------------------------------------- 1 | export * from './LightningBaseProvider'; 2 | export * from './LND'; 3 | -------------------------------------------------------------------------------- /packages/shared-server/src/OnchainProviders/index.ts: -------------------------------------------------------------------------------- 1 | export * from './OnchainBaseProvider'; 2 | export * from './Esplora'; 3 | export * from './BitcoinCore'; 4 | export * from './Electrum'; 5 | -------------------------------------------------------------------------------- /packages/shared-server/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './HWI/commands'; 2 | export * from './HWI/runCommand'; 3 | export * from './LightningProviders'; 4 | export * from './OnchainProviders'; 5 | export * from './sqlite'; 6 | export * from './utils/utils'; 7 | export * from './utils/lightning'; 8 | export * from './utils/accountMap'; 9 | -------------------------------------------------------------------------------- /packages/shared-server/src/sqlite/address.ts: -------------------------------------------------------------------------------- 1 | import sqlite3 from 'sqlite3'; 2 | import { Database } from 'sqlite'; 3 | import { AddressTag } from '@lily/types'; 4 | 5 | export async function createAddressTable(db: Database) { 6 | const query = `CREATE TABLE IF NOT EXISTS address_labels (id INTEGER PRIMARY KEY, address TEXT NOT NULL, label TEXT NOT NULL)`; 7 | db.exec(query); 8 | } 9 | 10 | export async function addAddressTag( 11 | db: Database, 12 | address: string, 13 | label: string 14 | ): Promise { 15 | try { 16 | const query = `INSERT INTO address_labels (id, address, label) VALUES (null, ?, ?)`; 17 | const entry = await db.run(query, [address, label]); 18 | return entry.lastID!; 19 | } catch (error) { 20 | console.error(error); 21 | throw error; 22 | } 23 | } 24 | 25 | export async function deleteAddressTag( 26 | db: Database, 27 | id: number 28 | ) { 29 | try { 30 | const query = `DELETE FROM address_labels WHERE id = ?`; 31 | await db.run(query, [id]); 32 | } catch (error) { 33 | console.error(error); 34 | throw error; 35 | } 36 | } 37 | 38 | export async function getAllLabelsForAddress( 39 | db: Database, 40 | address: string 41 | ): Promise { 42 | try { 43 | const query = `SELECT * FROM address_labels WHERE address = ?`; 44 | const labels = await db.all(query, [address]); 45 | return labels; 46 | } catch (error) { 47 | console.error(error); 48 | throw error; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/shared-server/src/sqlite/index.ts: -------------------------------------------------------------------------------- 1 | import sqlite3 from 'sqlite3'; 2 | import { open } from 'sqlite'; 3 | 4 | function createDbConnection(filename: string) { 5 | return open({ 6 | filename, 7 | driver: sqlite3.Database 8 | }); 9 | } 10 | 11 | export async function dbConnect(userDataPath: string) { 12 | sqlite3.verbose(); 13 | const db = await createDbConnection(`${userDataPath}/lily.db`); 14 | return db; 15 | } 16 | 17 | export * from './address'; 18 | export * from './transaction'; 19 | -------------------------------------------------------------------------------- /packages/shared-server/src/sqlite/transaction.ts: -------------------------------------------------------------------------------- 1 | import sqlite3 from 'sqlite3'; 2 | import { Database } from 'sqlite'; 3 | import { TransactionDescription } from '@lily/types'; 4 | 5 | export async function createTransactionTable(db: Database) { 6 | const query = `CREATE TABLE IF NOT EXISTS transaction_descriptions (txid TEXT UNIQUE PRIMARY KEY, description TEXT NOT NULL)`; 7 | db.exec(query); 8 | } 9 | 10 | export async function addTransactionDescription( 11 | db: Database, 12 | txid: string, 13 | description: string 14 | ) { 15 | try { 16 | const query = `REPLACE INTO transaction_descriptions (txid, description) VALUES (?, ?)`; 17 | const entry = await db.run(query, [txid, description]); 18 | return entry.lastID; 19 | } catch (error) { 20 | console.error(error); 21 | throw error; 22 | } 23 | } 24 | 25 | export async function updateTransactionDescription( 26 | db: Database, 27 | txid: string, 28 | description: string 29 | ) { 30 | try { 31 | const query = `UPDATE transaction_descriptions SET description = ? WHERE txid = ?`; 32 | await db.run(query, [description, txid]); 33 | } catch (error) { 34 | console.error(error); 35 | throw error; 36 | } 37 | } 38 | 39 | export async function deleteTransactionDescription( 40 | db: Database, 41 | txid: string 42 | ) { 43 | try { 44 | const query = `DELETE FROM transaction_descriptions WHERE txid = ?`; 45 | await db.run(query, [txid]); 46 | } catch (error) { 47 | console.error(error); 48 | throw error; 49 | } 50 | } 51 | 52 | export async function getTransactionDescription( 53 | db: Database, 54 | txid: string 55 | ): Promise { 56 | try { 57 | const query = `SELECT * FROM transaction_descriptions WHERE txid = ?`; 58 | const labels = await db.get(query, [txid]); 59 | return labels; 60 | } catch (error) { 61 | console.error(error); 62 | throw error; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/shared-server/src/utils/lightning.ts: -------------------------------------------------------------------------------- 1 | import { URL, URLSearchParams } from 'url'; 2 | import { decodeCert, decodeMacaroon } from 'lndconnect'; 3 | 4 | export const parseLndConnectUri = (string = '') => { 5 | const parsedUrl = new URL(string); 6 | const parsedQuery = new URLSearchParams(parsedUrl.searchParams); 7 | 8 | if (parsedUrl.protocol !== 'lndconnect:') { 9 | throw new Error('Invalid protocol'); 10 | } 11 | 12 | return { 13 | server: parsedUrl.host, 14 | tls: '', 15 | cert: decodeCert(parsedQuery.get('cert')!) || undefined, 16 | macaroon: decodeMacaroon(parsedQuery.get('macaroon')!) || undefined 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/shared-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "sourceMap": true, 6 | // "strictNullChecks": true, 7 | "module": "commonjs", 8 | "target": "esnext", 9 | "rootDir": "./src", 10 | "allowJs": true, 11 | "baseUrl": ".", 12 | "composite": true, 13 | "lib": ["esnext"], 14 | "skipLibCheck": true, 15 | "esModuleInterop": true, 16 | "allowSyntheticDefaultImports": true, 17 | "strict": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "moduleResolution": "node", 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "noEmit": false, 23 | "noImplicitAny": false, 24 | "typeRoots": ["node_modules/@types", "../types/dist/**"] 25 | }, 26 | "references": [{ "path": "../types" }], 27 | "include": ["src/**/*"], 28 | "exclude": ["./dist"] 29 | } 30 | -------------------------------------------------------------------------------- /packages/types/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.4.0](https://github.com/Lily-Technologies/lily-wallet/compare/types-v1.3.0...types-v1.4.0) (2023-07-19) 4 | 5 | 6 | ### Features 7 | 8 | * **Bitgo:** support bitgo vaults ([baece25](https://github.com/Lily-Technologies/lily-wallet/commit/baece25843eb7a294ea3405c517b667121459248)) 9 | 10 | ## 1.3.0 (2022-12-07) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * **Setup, Hardware Wallet:** import from file ([dad413c](https://github.com/Lily-Technologies/lily-wallet/commit/dad413c438f8ff835e45f9b047056db23b1ca514)) 16 | -------------------------------------------------------------------------------- /packages/types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lily/types", 3 | "version": "1.4.0", 4 | "author": "Lily Technologies, Inc. (https://lily-wallet.com)", 5 | "description": "Lily is the best way to secure your Bitcoin", 6 | "license": "Custom", 7 | "private": true, 8 | "main": "./dist/index.js", 9 | "types": "./dist/index.d.ts", 10 | "scripts": { 11 | "build": "tsc -b" 12 | }, 13 | "dependencies": { 14 | "@lily-technologies/lnrpc": "^0.14.1-beta.2", 15 | "bignumber.js": "^9.0.1", 16 | "bitcoin-simple-rpc": "^0.0.3", 17 | "bitcoinjs-lib": "^6.0.1", 18 | "typescript": "^4.4.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/types/src/@types/declarations/bs58check/index.ts: -------------------------------------------------------------------------------- 1 | declare module 'bs58check' { 2 | function decode(text: string): string; 3 | } 4 | -------------------------------------------------------------------------------- /packages/types/src/@types/declarations/coinselect/index.ts: -------------------------------------------------------------------------------- 1 | declare module 'coinselect' { 2 | import { UTXO } from 'src'; 3 | 4 | function coinSelect(utxos: UTXO[], outputs: Output[], feeRate: number): CoinSelectResponse; 5 | 6 | interface Output { 7 | address?: string; 8 | value: number; 9 | } 10 | 11 | interface CoinSelectResponse { 12 | inputs: UTXO[]; 13 | outputs: Output[]; 14 | fee: number; 15 | } 16 | 17 | export = coinSelect; 18 | } 19 | -------------------------------------------------------------------------------- /packages/types/src/@types/declarations/lndconnect/index.ts: -------------------------------------------------------------------------------- 1 | interface EncodeProps { 2 | host: string; 3 | cert: string; 4 | macaroon: string; 5 | } 6 | 7 | declare module 'lndconnect' { 8 | function decodeCert(cert: string): string; 9 | function decodeMacaroon(cert: string): string; 10 | function encode({ host, cert, macaroon }: EncodeProps): string; 11 | } 12 | -------------------------------------------------------------------------------- /packages/types/src/@types/declarations/unchained-bitcoin/index.ts: -------------------------------------------------------------------------------- 1 | declare module 'unchained-bitcoin' { 2 | import { Network, Payment } from 'bitcoinjs-lib'; 3 | import BigNumber from 'bignumber.js'; 4 | 5 | function satoshisToBitcoins(n: number | string | BigNumber): BigNumber; 6 | function bitcoinsToSatoshis(n: number | string | BigNumber): BigNumber; 7 | function multisigWitnessScript(multisig: Payment): WitnessScript; 8 | function blockExplorerAPIURL(path: string, network: string): string; 9 | function blockExplorerTransactionURL(txid: string, network: 'mainnet' | 'testnet'): string; 10 | function deriveChildPublicKey(xpub: string, path: string, network: 'mainnet' | 'testnet'): string; 11 | function generateMultisigFromPublicKeys( 12 | network: 'mainnet' | 'testnet', 13 | addressType: string, 14 | requiredSigners: number, 15 | ...publicKeys: string[] 16 | ): Payment; 17 | 18 | const MAINNET = 'mainnet'; 19 | const TESTNET = 'testnet'; 20 | 21 | interface MultisigConfig { 22 | addressType: any; 23 | numInputs: number; 24 | numOutputs: number; 25 | m: number; 26 | n: number; 27 | feesPerByteInSatoshis: string; 28 | } 29 | 30 | interface WitnessScript { 31 | input?: any; 32 | m: number; 33 | n: number; 34 | network: Network; 35 | output: Buffer; 36 | pubkeys: Buffer[]; 37 | signatures?: any; 38 | witness?: any; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "group-pull-request-title-pattern": "chore: release v${version}", 3 | "packages": { 4 | ".": {}, 5 | "apps/electron": { "skip-github-release": true }, 6 | "apps/express": { "skip-github-release": true }, 7 | "apps/frontend": { "skip-github-release": true }, 8 | "packages/shared-server": { "skip-github-release": true }, 9 | "packages/types": { "skip-github-release": true } 10 | }, 11 | "plugins": [{ "type": "node-workspace", "merge": false }], 12 | "release-type": "node" 13 | } 14 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lily-Technologies/lily-wallet/6a6485e5f7ee1bc83574ab0d64ad173893bb3a2a/screenshot.png -------------------------------------------------------------------------------- /tor.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 'Wi-Fi' or 'Ethernet' or 'Display Ethernet' 4 | INTERFACE=Wi-Fi 5 | 6 | # Ask for the administrator password upfront 7 | sudo -v 8 | 9 | # Keep-alive: update existing `sudo` time stamp until finished 10 | while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null & 11 | 12 | # trap ctrl-c and call disable_proxy() 13 | function disable_proxy() { 14 | sudo networksetup -setsocksfirewallproxystate $INTERFACE off 15 | echo "$(tput setaf 64)" #green 16 | echo "SOCKS proxy disabled." 17 | echo "$(tput sgr0)" # color reset 18 | } 19 | trap disable_proxy INT 20 | 21 | # Let's roll 22 | sudo networksetup -setsocksfirewallproxy $INTERFACE 127.0.0.1 9050 off 23 | sudo networksetup -setsocksfirewallproxystate $INTERFACE on 24 | 25 | echo "$(tput setaf 64)" # green 26 | echo "SOCKS proxy 127.0.0.1:9050 enabled." 27 | echo "$(tput setaf 136)" # orange 28 | echo "Starting Tor..." 29 | echo "$(tput sgr0)" # color reset 30 | 31 | tor -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "moduleResolution": "Node", 5 | "module": "ESNext", 6 | "target": "ESNext", 7 | "sourceMap": true, 8 | "lib": ["ESNext"], 9 | "esModuleInterop": true, 10 | // "strictNullChecks": true, 11 | // "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | // "noImplicitAny": false, 14 | // "noUnusedLocals": true, 15 | "resolveJsonModule": true, 16 | "types": ["node"] 17 | }, 18 | "references": [ 19 | { 20 | "path": "packages/shared-server" 21 | } 22 | ], 23 | "exclude": ["node_modules", "dist"] 24 | } 25 | --------------------------------------------------------------------------------