├── .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 | 
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 |
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 |
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 |
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 |
14 |
22 |
23 |
24 |
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 |
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 |
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 |
21 |
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 |
--------------------------------------------------------------------------------